5use once_cell::unsync::OnceCell;
7use syntect::highlighting::{Theme, ThemeSet};
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 serialized_syntax_set::*;
20#[cfg(feature = "build-assets")]
21pub use crate::assets::build_assets::*;
23pub(crate) mod assets_metadata;
24#[cfg(feature = "build-assets")]
26mod serialized_syntax_set;
29pub struct HighlightingAssets {
30 syntax_set_cell: OnceCell<SyntaxSet>,
31 serialized_syntax_set: SerializedSyntaxSet,
34 fallback_theme: Option<&'static str>,
38pub struct SyntaxReferenceInSet<'a> {
39 pub syntax: &'a SyntaxReference,
40 pub syntax_set: &'a SyntaxSet,
43/// Compress for size of ~700 kB instead of ~4600 kB at the cost of ~30% longer deserialization time
44pub(crate) const COMPRESS_SYNTAXES: bool = true;
46/// Compress for size of ~20 kB instead of ~200 kB at the cost of ~30% longer deserialization time
47pub(crate) const COMPRESS_THEMES: bool = true;
49impl HighlightingAssets {
50 fn new(serialized_syntax_set: SerializedSyntaxSet, theme_set: ThemeSet) -> Self {
52 syntax_set_cell: OnceCell::new(),
53 serialized_syntax_set,
59 pub fn default_theme() -> &'static str {
63 pub fn from_cache(cache_path: &Path) -> Result<Self> {
64 Ok(HighlightingAssets::new(
65 SerializedSyntaxSet::FromFile(cache_path.join("syntaxes.bin")),
66 asset_from_cache(&cache_path.join("themes.bin"), "theme set", COMPRESS_THEMES)?,
70 pub fn from_binary() -> Self {
71 HighlightingAssets::new(
72 SerializedSyntaxSet::FromBinary(get_serialized_integrated_syntaxset()),
73 get_integrated_themeset(),
77 pub fn set_fallback_theme(&mut self, theme: &'static str) {
78 self.fallback_theme = Some(theme);
81 fn get_syntax_set(&self) -> Result<&SyntaxSet> {
83 .get_or_try_init(|| self.serialized_syntax_set.deserialize())
86 /// Use [Self::get_syntaxes] instead
88 pub fn syntaxes(&self) -> &[SyntaxReference] {
90 .expect(".syntaxes() is deprecated, use .get_syntaxes() instead")
94 pub fn get_syntaxes(&self) -> Result<&[SyntaxReference]> {
95 Ok(self.get_syntax_set()?.syntaxes())
98 fn get_theme_set(&self) -> &ThemeSet {
102 pub fn themes(&self) -> impl Iterator<Item = &str> {
103 self.get_theme_set().themes.keys().map(|s| s.as_ref())
106 /// Use [Self::get_syntax_for_path] instead
108 pub fn syntax_for_file_name(
110 file_name: impl AsRef<Path>,
111 mapping: &SyntaxMapping,
112 ) -> Option<&SyntaxReference> {
113 self.get_syntax_for_path(file_name, mapping)
115 .map(|syntax_in_set| syntax_in_set.syntax)
118 /// Detect the syntax based on, in order:
119 /// 1. Syntax mappings with [MappingTarget::MapTo] and [MappingTarget::MapToUnknown]
120 /// (e.g. `/etc/profile` -> `Bourne Again Shell (bash)`)
121 /// 2. The file name (e.g. `Dockerfile`)
122 /// 3. Syntax mappings with [MappingTarget::MapExtensionToUnknown]
124 /// 4. The file name extension (e.g. `.rs`)
126 /// When detecting syntax based on syntax mappings, the full path is taken
127 /// into account. When detecting syntax based on file name, no regard is
128 /// taken to the path of the file. Only the file name itself matters. When
129 /// detecting syntax based on file name extension, only the file name
130 /// extension itself matters.
132 /// Returns [Error::UndetectedSyntax] if it was not possible detect syntax
133 /// based on path/file name/extension (or if the path was mapped to
134 /// [MappingTarget::MapToUnknown] or [MappingTarget::MapExtensionToUnknown]).
135 /// In this case it is appropriate to fall back to other methods to detect
136 /// syntax. Such as using the contents of the first line of the file.
138 /// Returns [Error::UnknownSyntax] if a syntax mapping exist, but the mapped
139 /// syntax does not exist.
140 pub fn get_syntax_for_path(
142 path: impl AsRef<Path>,
143 mapping: &SyntaxMapping,
144 ) -> Result<SyntaxReferenceInSet> {
145 let path = path.as_ref();
147 let syntax_match = mapping.get_syntax_for(path);
149 if let Some(MappingTarget::MapToUnknown) = syntax_match {
150 return Err(Error::UndetectedSyntax(path.to_string_lossy().into()));
153 if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match {
155 .find_syntax_by_name(syntax_name)?
156 .ok_or_else(|| Error::UnknownSyntax(syntax_name.to_owned()));
159 let file_name = path.file_name().unwrap_or_default();
162 self.get_syntax_for_file_name(file_name, &mapping.ignored_suffixes)?,
165 (Some(syntax), _) => Ok(syntax),
167 (_, Some(MappingTarget::MapExtensionToUnknown)) => {
168 Err(Error::UndetectedSyntax(path.to_string_lossy().into()))
172 .get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)?
173 .ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())),
177 pub(crate) fn get_theme(&self, theme: &str) -> &Theme {
178 match self.get_theme_set().themes.get(theme) {
179 Some(theme) => theme,
181 if theme == "ansi-light" || theme == "ansi-dark" {
182 bat_warning!("Theme '{}' is deprecated, using 'ansi' instead.", theme);
183 return self.get_theme("ansi");
185 if !theme.is_empty() {
186 bat_warning!("Unknown theme '{}', using default.", theme)
188 &self.get_theme_set().themes
189 [self.fallback_theme.unwrap_or_else(|| Self::default_theme())]
194 pub(crate) fn get_syntax(
196 language: Option<&str>,
197 input: &mut OpenedInput,
198 mapping: &SyntaxMapping,
199 ) -> Result<SyntaxReferenceInSet> {
200 if let Some(language) = language {
201 let syntax_set = self.get_syntax_set()?;
203 .find_syntax_by_token(language)
204 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set })
205 .ok_or_else(|| Error::UnknownSyntax(language.to_owned()));
208 let path = input.path();
209 let path_syntax = if let Some(path) = path {
210 self.get_syntax_for_path(
211 PathAbs::new(path).map_or_else(|_| path.to_owned(), |p| p.as_path().to_path_buf()),
215 Err(Error::UndetectedSyntax("[unknown]".into()))
219 // If a path wasn't provided, or if path based syntax detection
220 // above failed, we fall back to first-line syntax detection.
221 Err(Error::UndetectedSyntax(path)) => self
222 .get_first_line_syntax(&mut input.reader)?
223 .ok_or(Error::UndetectedSyntax(path)),
228 pub(crate) fn find_syntax_by_name(
231 ) -> Result<Option<SyntaxReferenceInSet>> {
232 let syntax_set = self.get_syntax_set()?;
234 .find_syntax_by_name(syntax_name)
235 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
238 fn find_syntax_by_extension(&self, e: Option<&OsStr>) -> Result<Option<SyntaxReferenceInSet>> {
239 let syntax_set = self.get_syntax_set()?;
240 let extension = e.and_then(|x| x.to_str()).unwrap_or_default();
242 .find_syntax_by_extension(extension)
243 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
246 fn get_syntax_for_file_name(
249 ignored_suffixes: &IgnoredSuffixes,
250 ) -> Result<Option<SyntaxReferenceInSet>> {
251 let mut syntax = self.find_syntax_by_extension(Some(file_name))?;
252 if syntax.is_none() {
254 ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
256 self.get_syntax_for_file_name(stripped_file_name, ignored_suffixes)
262 fn get_syntax_for_file_extension(
265 ignored_suffixes: &IgnoredSuffixes,
266 ) -> Result<Option<SyntaxReferenceInSet>> {
267 let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?;
268 if syntax.is_none() {
270 ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
272 self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes)
278 fn get_first_line_syntax(
280 reader: &mut InputReader,
281 ) -> Result<Option<SyntaxReferenceInSet>> {
282 let syntax_set = self.get_syntax_set()?;
283 Ok(String::from_utf8(reader.first_line.clone())
285 .and_then(|l| syntax_set.find_syntax_by_first_line(&l))
286 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
290pub(crate) fn get_serialized_integrated_syntaxset() -> &'static [u8] {
291 include_bytes!("../assets/syntaxes.bin")
294pub(crate) fn get_integrated_themeset() -> ThemeSet {
295 from_binary(include_bytes!("../assets/themes.bin"), COMPRESS_THEMES)
298pub(crate) fn from_binary<T: serde::de::DeserializeOwned>(v: &[u8], compressed: bool) -> T {
299 asset_from_contents(v, "n/a", compressed)
300 .expect("data integrated in binary is never faulty, but make sure `compressed` is in sync!")
303fn asset_from_contents<T: serde::de::DeserializeOwned>(
309 bincode::deserialize_from(flate2::read::ZlibDecoder::new(contents))
311 bincode::deserialize_from(contents)
313 .map_err(|_| format!("Could not parse {}", description).into())
316fn asset_from_cache<T: serde::de::DeserializeOwned>(
321 let contents = fs::read(path).map_err(|_| {
323 "Could not load cached {} '{}'",
325 path.to_string_lossy()
328 asset_from_contents(&contents[..], description, compressed)
329 .map_err(|_| format!("Could not parse cached {}", description).into())
339 use std::io::{BufReader, Write};
340 use tempfile::TempDir;
342 use crate::input::Input;
344 struct SyntaxDetectionTest<'a> {
345 assets: HighlightingAssets,
346 pub syntax_mapping: SyntaxMapping<'a>,
347 pub temp_dir: TempDir,
350 impl<'a> SyntaxDetectionTest<'a> {
352 SyntaxDetectionTest {
353 assets: HighlightingAssets::from_binary(),
354 syntax_mapping: SyntaxMapping::builtin(),
355 temp_dir: TempDir::new().expect("creation of temporary directory"),
361 language: Option<&str>,
362 input: &mut OpenedInput,
363 mapping: &SyntaxMapping,
366 .get_syntax(language, input, mapping)
367 .map(|syntax_in_set| syntax_in_set.syntax.name.clone())
368 .unwrap_or_else(|_| "!no syntax!".to_owned())
371 fn syntax_for_real_file_with_content_os(
376 let file_path = self.temp_dir.path().join(file_name);
378 let mut temp_file = File::create(&file_path).unwrap();
379 writeln!(temp_file, "{}", first_line).unwrap();
382 let input = Input::ordinary_file(&file_path);
383 let dummy_stdin: &[u8] = &[];
384 let mut opened_input = input.open(dummy_stdin, None).unwrap();
386 self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
389 fn syntax_for_file_with_content_os(&self, file_name: &OsStr, first_line: &str) -> String {
390 let file_path = self.temp_dir.path().join(file_name);
391 let input = Input::from_reader(Box::new(BufReader::new(first_line.as_bytes())))
392 .with_name(Some(&file_path));
393 let dummy_stdin: &[u8] = &[];
394 let mut opened_input = input.open(dummy_stdin, None).unwrap();
396 self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
400 fn syntax_for_file_os(&self, file_name: &OsStr) -> String {
401 self.syntax_for_file_with_content_os(file_name, "")
404 fn syntax_for_file_with_content(&self, file_name: &str, first_line: &str) -> String {
405 self.syntax_for_file_with_content_os(OsStr::new(file_name), first_line)
408 fn syntax_for_file(&self, file_name: &str) -> String {
409 self.syntax_for_file_with_content(file_name, "")
412 fn syntax_for_stdin_with_content(&self, file_name: &str, content: &[u8]) -> String {
413 let input = Input::stdin().with_name(Some(file_name));
414 let mut opened_input = input.open(content, None).unwrap();
416 self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
419 fn syntax_is_same_for_inputkinds(&self, file_name: &str, content: &str) -> bool {
420 let as_file = self.syntax_for_real_file_with_content_os(file_name.as_ref(), content);
421 let as_reader = self.syntax_for_file_with_content_os(file_name.as_ref(), content);
422 let consistent = as_file == as_reader;
423 // TODO: Compare StdIn somehow?
427 "Inconsistent syntax detection:\nFor File: {}\nFor Reader: {}",
437 fn syntax_detection_basic() {
438 let test = SyntaxDetectionTest::new();
440 assert_eq!(test.syntax_for_file("test.rs"), "Rust");
441 assert_eq!(test.syntax_for_file("test.cpp"), "C++");
442 assert_eq!(test.syntax_for_file("test.build"), "NAnt Build File");
444 test.syntax_for_file("PKGBUILD"),
445 "Bourne Again Shell (bash)"
447 assert_eq!(test.syntax_for_file(".bashrc"), "Bourne Again Shell (bash)");
448 assert_eq!(test.syntax_for_file("Makefile"), "Makefile");
453 fn syntax_detection_invalid_utf8() {
454 use std::os::unix::ffi::OsStrExt;
456 let test = SyntaxDetectionTest::new();
459 test.syntax_for_file_os(OsStr::from_bytes(b"invalid_\xFEutf8_filename.rs")),
465 fn syntax_detection_same_for_inputkinds() {
466 let mut test = SyntaxDetectionTest::new();
469 .insert("*.myext", MappingTarget::MapTo("C"))
472 .insert("MY_FILE", MappingTarget::MapTo("Markdown"))
475 assert!(test.syntax_is_same_for_inputkinds("Test.md", ""));
476 assert!(test.syntax_is_same_for_inputkinds("Test.txt", "#!/bin/bash"));
477 assert!(test.syntax_is_same_for_inputkinds(".bashrc", ""));
478 assert!(test.syntax_is_same_for_inputkinds("test.h", ""));
479 assert!(test.syntax_is_same_for_inputkinds("test.js", "#!/bin/bash"));
480 assert!(test.syntax_is_same_for_inputkinds("test.myext", ""));
481 assert!(test.syntax_is_same_for_inputkinds("MY_FILE", ""));
482 assert!(test.syntax_is_same_for_inputkinds("MY_FILE", "<?php"));
486 fn syntax_detection_well_defined_mapping_for_duplicate_extensions() {
487 let test = SyntaxDetectionTest::new();
489 assert_eq!(test.syntax_for_file("test.h"), "C++");
490 assert_eq!(test.syntax_for_file("test.sass"), "Sass");
491 assert_eq!(test.syntax_for_file("test.js"), "JavaScript (Babel)");
492 assert_eq!(test.syntax_for_file("test.fs"), "F#");
493 assert_eq!(test.syntax_for_file("test.v"), "Verilog");
497 fn syntax_detection_first_line() {
498 let test = SyntaxDetectionTest::new();
501 test.syntax_for_file_with_content("my_script", "#!/bin/bash"),
502 "Bourne Again Shell (bash)"
505 test.syntax_for_file_with_content("build", "#!/bin/bash"),
506 "Bourne Again Shell (bash)"
509 test.syntax_for_file_with_content("my_script", "<?php"),
515 fn syntax_detection_with_custom_mapping() {
516 let mut test = SyntaxDetectionTest::new();
518 assert_eq!(test.syntax_for_file("test.h"), "C++");
520 .insert("*.h", MappingTarget::MapTo("C"))
522 assert_eq!(test.syntax_for_file("test.h"), "C");
526 fn syntax_detection_with_extension_mapping_to_unknown() {
527 let mut test = SyntaxDetectionTest::new();
529 // Normally, a CMakeLists.txt file shall use the CMake syntax, even if it is
530 // a bash script in disguise
532 test.syntax_for_file_with_content("CMakeLists.txt", "#!/bin/bash"),
536 // Other .txt files shall use the Plain Text syntax
538 test.syntax_for_file_with_content("some-other.txt", "#!/bin/bash"),
542 // If we setup MapExtensionToUnknown on *.txt, the match on the full
543 // file name of "CMakeLists.txt" shall have higher prio, and CMake shall
544 // still be used for it
546 .insert("*.txt", MappingTarget::MapExtensionToUnknown)
549 test.syntax_for_file_with_content("CMakeLists.txt", "#!/bin/bash"),
553 // However, for *other* files with a .txt extension, first-line fallback
556 test.syntax_for_file_with_content("some-other.txt", "#!/bin/bash"),
557 "Bourne Again Shell (bash)"
562 fn syntax_detection_is_case_sensitive() {
563 let mut test = SyntaxDetectionTest::new();
565 assert_ne!(test.syntax_for_file("README.MD"), "Markdown");
567 .insert("*.MD", MappingTarget::MapTo("Markdown"))
569 assert_eq!(test.syntax_for_file("README.MD"), "Markdown");
573 fn syntax_detection_stdin_filename() {
574 let test = SyntaxDetectionTest::new();
576 // from file extension
577 assert_eq!(test.syntax_for_stdin_with_content("test.cpp", b"a"), "C++");
578 // from first line (fallback)
580 test.syntax_for_stdin_with_content("my_script", b"#!/bin/bash"),
581 "Bourne Again Shell (bash)"
587 fn syntax_detection_for_symlinked_file() {
588 use std::os::unix::fs::symlink;
590 let test = SyntaxDetectionTest::new();
591 let file_path = test.temp_dir.path().join("my_ssh_config_filename");
593 File::create(&file_path).unwrap();
595 let file_path_symlink = test.temp_dir.path().join(".ssh").join("config");
597 std::fs::create_dir(test.temp_dir.path().join(".ssh"))
598 .expect("creation of directory succeeds");
599 symlink(&file_path, &file_path_symlink).expect("creation of symbolic link succeeds");
601 let input = Input::ordinary_file(&file_path_symlink);
602 let dummy_stdin: &[u8] = &[];
603 let mut opened_input = input.open(dummy_stdin, None).unwrap();
606 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