5use once_cell::unsync::OnceCell;
7use syntect::highlighting::Theme;
8use syntect::parsing::{SyntaxReference, SyntaxSet};
13use crate::input::{InputReader, OpenedInput};
14use crate::syntax_mapping::ignored_suffixes::IgnoredSuffixes;
15use crate::syntax_mapping::MappingTarget;
16use crate::{bat_warning, SyntaxMapping};
18use lazy_theme_set::LazyThemeSet;
20use serialized_syntax_set::*;
22#[cfg(feature = "build-assets")]
23pub use crate::assets::build_assets::*;
25pub(crate) mod assets_metadata;
26#[cfg(feature = "build-assets")]
29mod serialized_syntax_set;
32pub struct HighlightingAssets {
33 syntax_set_cell: OnceCell<SyntaxSet>,
34 serialized_syntax_set: SerializedSyntaxSet,
36 theme_set: LazyThemeSet,
37 fallback_theme: Option<&'static str>,
41pub struct SyntaxReferenceInSet<'a> {
42 pub syntax: &'a SyntaxReference,
43 pub syntax_set: &'a SyntaxSet,
46/// Compress for size of ~700 kB instead of ~4600 kB at the cost of ~30% longer deserialization time
47pub(crate) const COMPRESS_SYNTAXES: bool = true;
49/// We don't want to compress our [LazyThemeSet] since the lazy-loaded themes
50/// within it are already compressed, and compressing another time just makes
52pub(crate) const COMPRESS_THEMES: bool = false;
54/// Compress for size of ~40 kB instead of ~200 kB without much difference in
55/// performance due to lazy-loading
56pub(crate) const COMPRESS_LAZY_THEMES: bool = true;
58/// Compress for size of ~10 kB instead of ~120 kB
59pub(crate) const COMPRESS_ACKNOWLEDGEMENTS: bool = true;
61impl HighlightingAssets {
62 fn new(serialized_syntax_set: SerializedSyntaxSet, theme_set: LazyThemeSet) -> Self {
64 syntax_set_cell: OnceCell::new(),
65 serialized_syntax_set,
71 pub fn default_theme() -> &'static str {
75 pub fn from_cache(cache_path: &Path) -> Result<Self> {
76 Ok(HighlightingAssets::new(
77 SerializedSyntaxSet::FromFile(cache_path.join("syntaxes.bin")),
78 asset_from_cache(&cache_path.join("themes.bin"), "theme set", COMPRESS_THEMES)?,
82 pub fn from_binary() -> Self {
83 HighlightingAssets::new(
84 SerializedSyntaxSet::FromBinary(get_serialized_integrated_syntaxset()),
85 get_integrated_themeset(),
89 pub fn set_fallback_theme(&mut self, theme: &'static str) {
90 self.fallback_theme = Some(theme);
93 /// Return the collection of syntect syntax definitions.
94 pub fn get_syntax_set(&self) -> Result<&SyntaxSet> {
96 .get_or_try_init(|| self.serialized_syntax_set.deserialize())
99 /// Use [Self::get_syntaxes] instead
101 pub fn syntaxes(&self) -> &[SyntaxReference] {
102 self.get_syntax_set()
103 .expect(".syntaxes() is deprecated, use .get_syntaxes() instead")
107 pub fn get_syntaxes(&self) -> Result<&[SyntaxReference]> {
108 Ok(self.get_syntax_set()?.syntaxes())
111 fn get_theme_set(&self) -> &LazyThemeSet {
115 pub fn themes(&self) -> impl Iterator<Item = &str> {
116 self.get_theme_set().themes()
119 /// Use [Self::get_syntax_for_path] instead
121 pub fn syntax_for_file_name(
123 file_name: impl AsRef<Path>,
124 mapping: &SyntaxMapping,
125 ) -> Option<&SyntaxReference> {
126 self.get_syntax_for_path(file_name, mapping)
128 .map(|syntax_in_set| syntax_in_set.syntax)
131 /// Detect the syntax based on, in order:
132 /// 1. Syntax mappings with [MappingTarget::MapTo] and [MappingTarget::MapToUnknown]
133 /// (e.g. `/etc/profile` -> `Bourne Again Shell (bash)`)
134 /// 2. The file name (e.g. `Dockerfile`)
135 /// 3. Syntax mappings with [MappingTarget::MapExtensionToUnknown]
137 /// 4. The file name extension (e.g. `.rs`)
139 /// When detecting syntax based on syntax mappings, the full path is taken
140 /// into account. When detecting syntax based on file name, no regard is
141 /// taken to the path of the file. Only the file name itself matters. When
142 /// detecting syntax based on file name extension, only the file name
143 /// extension itself matters.
145 /// Returns [Error::UndetectedSyntax] if it was not possible detect syntax
146 /// based on path/file name/extension (or if the path was mapped to
147 /// [MappingTarget::MapToUnknown] or [MappingTarget::MapExtensionToUnknown]).
148 /// In this case it is appropriate to fall back to other methods to detect
149 /// syntax. Such as using the contents of the first line of the file.
151 /// Returns [Error::UnknownSyntax] if a syntax mapping exist, but the mapped
152 /// syntax does not exist.
153 pub fn get_syntax_for_path(
155 path: impl AsRef<Path>,
156 mapping: &SyntaxMapping,
157 ) -> Result<SyntaxReferenceInSet> {
158 let path = path.as_ref();
160 let syntax_match = mapping.get_syntax_for(path);
162 if let Some(MappingTarget::MapToUnknown) = syntax_match {
163 return Err(Error::UndetectedSyntax(path.to_string_lossy().into()));
166 if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match {
168 .find_syntax_by_name(syntax_name)?
169 .ok_or_else(|| Error::UnknownSyntax(syntax_name.to_owned()));
172 let file_name = path.file_name().unwrap_or_default();
175 self.get_syntax_for_file_name(file_name, &mapping.ignored_suffixes)?,
178 (Some(syntax), _) => Ok(syntax),
180 (_, Some(MappingTarget::MapExtensionToUnknown)) => {
181 Err(Error::UndetectedSyntax(path.to_string_lossy().into()))
185 .get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)?
186 .ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())),
190 /// Look up a syntect theme by name.
191 pub fn get_theme(&self, theme: &str) -> &Theme {
192 match self.get_theme_set().get(theme) {
193 Some(theme) => theme,
195 if theme == "ansi-light" || theme == "ansi-dark" {
196 bat_warning!("Theme '{}' is deprecated, using 'ansi' instead.", theme);
197 return self.get_theme("ansi");
199 if !theme.is_empty() {
200 bat_warning!("Unknown theme '{}', using default.", theme)
203 .get(self.fallback_theme.unwrap_or_else(Self::default_theme))
204 .expect("something is very wrong if the default theme is missing")
209 pub(crate) fn get_syntax(
211 language: Option<&str>,
212 input: &mut OpenedInput,
213 mapping: &SyntaxMapping,
214 ) -> Result<SyntaxReferenceInSet> {
215 if let Some(language) = language {
216 let syntax_set = self.get_syntax_set()?;
218 .find_syntax_by_token(language)
219 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set })
220 .ok_or_else(|| Error::UnknownSyntax(language.to_owned()));
223 let path = input.path();
224 let path_syntax = if let Some(path) = path {
225 self.get_syntax_for_path(
226 PathAbs::new(path).map_or_else(|_| path.to_owned(), |p| p.as_path().to_path_buf()),
230 Err(Error::UndetectedSyntax("[unknown]".into()))
234 // If a path wasn't provided, or if path based syntax detection
235 // above failed, we fall back to first-line syntax detection.
236 Err(Error::UndetectedSyntax(path)) => self
237 .get_first_line_syntax(&mut input.reader)?
238 .ok_or(Error::UndetectedSyntax(path)),
243 pub(crate) fn find_syntax_by_name(
246 ) -> Result<Option<SyntaxReferenceInSet>> {
247 let syntax_set = self.get_syntax_set()?;
249 .find_syntax_by_name(syntax_name)
250 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
253 fn find_syntax_by_extension(&self, e: Option<&OsStr>) -> Result<Option<SyntaxReferenceInSet>> {
254 let syntax_set = self.get_syntax_set()?;
255 let extension = e.and_then(|x| x.to_str()).unwrap_or_default();
257 .find_syntax_by_extension(extension)
258 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
261 fn get_syntax_for_file_name(
264 ignored_suffixes: &IgnoredSuffixes,
265 ) -> Result<Option<SyntaxReferenceInSet>> {
266 let mut syntax = self.find_syntax_by_extension(Some(file_name))?;
267 if syntax.is_none() {
269 ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
271 self.get_syntax_for_file_name(stripped_file_name, ignored_suffixes)
277 fn get_syntax_for_file_extension(
280 ignored_suffixes: &IgnoredSuffixes,
281 ) -> Result<Option<SyntaxReferenceInSet>> {
282 let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?;
283 if syntax.is_none() {
285 ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
287 self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes)
293 fn get_first_line_syntax(
295 reader: &mut InputReader,
296 ) -> Result<Option<SyntaxReferenceInSet>> {
297 let syntax_set = self.get_syntax_set()?;
298 Ok(String::from_utf8(reader.first_line.clone())
300 .and_then(|l| syntax_set.find_syntax_by_first_line(&l))
301 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
305pub(crate) fn get_serialized_integrated_syntaxset() -> &'static [u8] {
306 include_bytes!("../assets/syntaxes.bin")
309pub(crate) fn get_integrated_themeset() -> LazyThemeSet {
310 from_binary(include_bytes!("../assets/themes.bin"), COMPRESS_THEMES)
313pub fn get_acknowledgements() -> String {
315 include_bytes!("../assets/acknowledgements.bin"),
316 COMPRESS_ACKNOWLEDGEMENTS,
320pub(crate) fn from_binary<T: serde::de::DeserializeOwned>(v: &[u8], compressed: bool) -> T {
321 asset_from_contents(v, "n/a", compressed)
322 .expect("data integrated in binary is never faulty, but make sure `compressed` is in sync!")
325fn asset_from_contents<T: serde::de::DeserializeOwned>(
331 bincode::deserialize_from(flate2::read::ZlibDecoder::new(contents))
333 bincode::deserialize_from(contents)
335 .map_err(|_| format!("Could not parse {}", description).into())
338fn asset_from_cache<T: serde::de::DeserializeOwned>(
343 let contents = fs::read(path).map_err(|_| {
345 "Could not load cached {} '{}'",
347 path.to_string_lossy()
350 asset_from_contents(&contents[..], description, compressed)
351 .map_err(|_| format!("Could not parse cached {}", description).into())
361 use std::io::{BufReader, Write};
362 use tempfile::TempDir;
364 use crate::input::Input;
366 struct SyntaxDetectionTest<'a> {
367 assets: HighlightingAssets,
368 pub syntax_mapping: SyntaxMapping<'a>,
369 pub temp_dir: TempDir,
372 impl<'a> SyntaxDetectionTest<'a> {
374 SyntaxDetectionTest {
375 assets: HighlightingAssets::from_binary(),
376 syntax_mapping: SyntaxMapping::builtin(),
377 temp_dir: TempDir::new().expect("creation of temporary directory"),
383 language: Option<&str>,
384 input: &mut OpenedInput,
385 mapping: &SyntaxMapping,
388 .get_syntax(language, input, mapping)
389 .map(|syntax_in_set| syntax_in_set.syntax.name.clone())
390 .unwrap_or_else(|_| "!no syntax!".to_owned())
393 fn syntax_for_real_file_with_content_os(
398 let file_path = self.temp_dir.path().join(file_name);
400 let mut temp_file = File::create(&file_path).unwrap();
401 writeln!(temp_file, "{}", first_line).unwrap();
404 let input = Input::ordinary_file(&file_path);
405 let dummy_stdin: &[u8] = &[];
406 let mut opened_input = input.open(dummy_stdin, None).unwrap();
408 self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
411 fn syntax_for_file_with_content_os(&self, file_name: &OsStr, first_line: &str) -> String {
412 let file_path = self.temp_dir.path().join(file_name);
413 let input = Input::from_reader(Box::new(BufReader::new(first_line.as_bytes())))
414 .with_name(Some(&file_path));
415 let dummy_stdin: &[u8] = &[];
416 let mut opened_input = input.open(dummy_stdin, None).unwrap();
418 self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
422 fn syntax_for_file_os(&self, file_name: &OsStr) -> String {
423 self.syntax_for_file_with_content_os(file_name, "")
426 fn syntax_for_file_with_content(&self, file_name: &str, first_line: &str) -> String {
427 self.syntax_for_file_with_content_os(OsStr::new(file_name), first_line)
430 fn syntax_for_file(&self, file_name: &str) -> String {
431 self.syntax_for_file_with_content(file_name, "")
434 fn syntax_for_stdin_with_content(&self, file_name: &str, content: &[u8]) -> String {
435 let input = Input::stdin().with_name(Some(file_name));
436 let mut opened_input = input.open(content, None).unwrap();
438 self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
441 fn syntax_is_same_for_inputkinds(&self, file_name: &str, content: &str) -> bool {
442 let as_file = self.syntax_for_real_file_with_content_os(file_name.as_ref(), content);
443 let as_reader = self.syntax_for_file_with_content_os(file_name.as_ref(), content);
444 let consistent = as_file == as_reader;
445 // TODO: Compare StdIn somehow?
449 "Inconsistent syntax detection:\nFor File: {}\nFor Reader: {}",
459 fn syntax_detection_basic() {
460 let test = SyntaxDetectionTest::new();
462 assert_eq!(test.syntax_for_file("test.rs"), "Rust");
463 assert_eq!(test.syntax_for_file("test.cpp"), "C++");
464 assert_eq!(test.syntax_for_file("test.build"), "NAnt Build File");
466 test.syntax_for_file("PKGBUILD"),
467 "Bourne Again Shell (bash)"
469 assert_eq!(test.syntax_for_file(".bashrc"), "Bourne Again Shell (bash)");
470 assert_eq!(test.syntax_for_file("Makefile"), "Makefile");
475 fn syntax_detection_invalid_utf8() {
476 use std::os::unix::ffi::OsStrExt;
478 let test = SyntaxDetectionTest::new();
481 test.syntax_for_file_os(OsStr::from_bytes(b"invalid_\xFEutf8_filename.rs")),
487 fn syntax_detection_same_for_inputkinds() {
488 let mut test = SyntaxDetectionTest::new();
491 .insert("*.myext", MappingTarget::MapTo("C"))
494 .insert("MY_FILE", MappingTarget::MapTo("Markdown"))
497 assert!(test.syntax_is_same_for_inputkinds("Test.md", ""));
498 assert!(test.syntax_is_same_for_inputkinds("Test.txt", "#!/bin/bash"));
499 assert!(test.syntax_is_same_for_inputkinds(".bashrc", ""));
500 assert!(test.syntax_is_same_for_inputkinds("test.h", ""));
501 assert!(test.syntax_is_same_for_inputkinds("test.js", "#!/bin/bash"));
502 assert!(test.syntax_is_same_for_inputkinds("test.myext", ""));
503 assert!(test.syntax_is_same_for_inputkinds("MY_FILE", ""));
504 assert!(test.syntax_is_same_for_inputkinds("MY_FILE", "<?php"));
508 fn syntax_detection_well_defined_mapping_for_duplicate_extensions() {
509 let test = SyntaxDetectionTest::new();
511 assert_eq!(test.syntax_for_file("test.h"), "C++");
512 assert_eq!(test.syntax_for_file("test.sass"), "Sass");
513 assert_eq!(test.syntax_for_file("test.js"), "JavaScript (Babel)");
514 assert_eq!(test.syntax_for_file("test.fs"), "F#");
515 assert_eq!(test.syntax_for_file("test.v"), "Verilog");
519 fn syntax_detection_first_line() {
520 let test = SyntaxDetectionTest::new();
523 test.syntax_for_file_with_content("my_script", "#!/bin/bash"),
524 "Bourne Again Shell (bash)"
527 test.syntax_for_file_with_content("build", "#!/bin/bash"),
528 "Bourne Again Shell (bash)"
531 test.syntax_for_file_with_content("my_script", "<?php"),
537 fn syntax_detection_with_custom_mapping() {
538 let mut test = SyntaxDetectionTest::new();
540 assert_eq!(test.syntax_for_file("test.h"), "C++");
542 .insert("*.h", MappingTarget::MapTo("C"))
544 assert_eq!(test.syntax_for_file("test.h"), "C");
548 fn syntax_detection_with_extension_mapping_to_unknown() {
549 let mut test = SyntaxDetectionTest::new();
551 // Normally, a CMakeLists.txt file shall use the CMake syntax, even if it is
552 // a bash script in disguise
554 test.syntax_for_file_with_content("CMakeLists.txt", "#!/bin/bash"),
558 // Other .txt files shall use the Plain Text syntax
560 test.syntax_for_file_with_content("some-other.txt", "#!/bin/bash"),
564 // If we setup MapExtensionToUnknown on *.txt, the match on the full
565 // file name of "CMakeLists.txt" shall have higher prio, and CMake shall
566 // still be used for it
568 .insert("*.txt", MappingTarget::MapExtensionToUnknown)
571 test.syntax_for_file_with_content("CMakeLists.txt", "#!/bin/bash"),
575 // However, for *other* files with a .txt extension, first-line fallback
578 test.syntax_for_file_with_content("some-other.txt", "#!/bin/bash"),
579 "Bourne Again Shell (bash)"
584 fn syntax_detection_is_case_sensitive() {
585 let mut test = SyntaxDetectionTest::new();
587 assert_ne!(test.syntax_for_file("README.MD"), "Markdown");
589 .insert("*.MD", MappingTarget::MapTo("Markdown"))
591 assert_eq!(test.syntax_for_file("README.MD"), "Markdown");
595 fn syntax_detection_stdin_filename() {
596 let test = SyntaxDetectionTest::new();
598 // from file extension
599 assert_eq!(test.syntax_for_stdin_with_content("test.cpp", b"a"), "C++");
600 // from first line (fallback)
602 test.syntax_for_stdin_with_content("my_script", b"#!/bin/bash"),
603 "Bourne Again Shell (bash)"
609 fn syntax_detection_for_symlinked_file() {
610 use std::os::unix::fs::symlink;
612 let test = SyntaxDetectionTest::new();
613 let file_path = test.temp_dir.path().join("my_ssh_config_filename");
615 File::create(&file_path).unwrap();
617 let file_path_symlink = test.temp_dir.path().join(".ssh").join("config");
619 std::fs::create_dir(test.temp_dir.path().join(".ssh"))
620 .expect("creation of directory succeeds");
621 symlink(&file_path, &file_path_symlink).expect("creation of symbolic link succeeds");
623 let input = Input::ordinary_file(&file_path_symlink);
624 let dummy_stdin: &[u8] = &[];
625 let mut opened_input = input.open(dummy_stdin, None).unwrap();
628 test.get_syntax_name(None, &mut opened_input, &test.syntax_mapping),
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list