on master help sign in
Initial implementation of glob-based syntax mapping
commitbd8a13dbc920b8739ef0f5e3fa84d90b3a24608b authorsharkdp <davidpeter@web.de> dateSun 22 Mar 2020, 09:55 (7 years ago) parentba29e07
7 files changed+138−43
Cargo.lock+23 Cargo.toml+1 src/assets.rs+39−19 src/bin/bat/app.rs+4−4 src/config.rs+2−2 src/errors.rs+1 src/syntax_mapping.rs+68−18
Cargo.lock+23
@@ -127,6 +127,7 @@ dependencies = [
127127 "encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
128128 "error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)",
129129 "git2 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
130+ "globset 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
130131 "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
131132 "liquid 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
132133 "shell-words 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -201,6 +202,14 @@ dependencies = [
201202 "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
202203]
203204
205+[[package]]
206+name = "bstr"
207+version = "0.2.12"
208+source = "registry+https://github.com/rust-lang/crates.io-index"
209+dependencies = [
210+ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
211+]
212+
204213[[package]]
205214name = "byte-tools"
206215version = "0.3.1"
@@ -542,6 +551,18 @@ name = "glob"
542551version = "0.3.0"
543552source = "registry+https://github.com/rust-lang/crates.io-index"
544553
554+[[package]]
555+name = "globset"
556+version = "0.4.5"
557+source = "registry+https://github.com/rust-lang/crates.io-index"
558+dependencies = [
559+ "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)",
560+ "bstr 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
561+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
562+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
563+ "regex 1.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
564+]
565+
545566[[package]]
546567name = "hermit-abi"
547568version = "0.1.8"
@@ -1453,6 +1474,7 @@ dependencies = [
14531474"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
14541475"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
14551476"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
1477+"checksum bstr 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "2889e6d50f394968c8bf4240dc3f2a7eb4680844d27308f798229ac9d4725f41"
14561478"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
14571479"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
14581480"checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
@@ -1495,6 +1517,7 @@ dependencies = [
14951517"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
14961518"checksum git2 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b7da16ceafe24cedd9ba02c4463a2b506b6493baf4317c79c5acb553134a3c15"
14971519"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
1520+"checksum globset 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "7ad1da430bd7281dde2576f44c84cc3f0f7b475e7202cd503042dff01a8c8120"
14981521"checksum hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
14991522"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
15001523"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
Cargo.toml+1
@@ -27,6 +27,7 @@ content_inspector = "0.2.4"
2727encoding = "0.2"
2828shell-words = "0.1.0"
2929unicode-width = "0.1.7"
30+globset = "0.4"
3031
3132[dependencies.git2]
3233version = "0.13"
src/assets.rs+39−19
@@ -9,7 +9,7 @@ use syntect::parsing::{SyntaxReference, SyntaxSet, SyntaxSetBuilder};
99
1010use crate::errors::*;
1111use crate::inputfile::{InputFile, InputFileReader};
12−use crate::syntax_mapping::SyntaxMapping;
12+use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
1313
1414#[derive(Debug)]
1515pub struct HighlightingAssets {
@@ -184,25 +184,28 @@ impl HighlightingAssets {
184184 (Some(language), _) => self.syntax_set.find_syntax_by_token(language),
185185 (None, InputFile::Ordinary(filename)) => {
186186 let path = Path::new(filename);
187+
187188 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
188189 let extension = path.extension().and_then(|x| x.to_str()).unwrap_or("");
189190
190− let file_name = mapping.replace(file_name);
191− let extension = mapping.replace(extension);
192−
193191 let ext_syntax = self
194192 .syntax_set
195193 .find_syntax_by_extension(&file_name)
196194 .or_else(|| self.syntax_set.find_syntax_by_extension(&extension));
197− let line_syntax = if ext_syntax.is_none() {
198− String::from_utf8(reader.first_line.clone())
199− .ok()
200− .and_then(|l| self.syntax_set.find_syntax_by_first_line(&l))
201− } else {
202− None
203− };
204−
205− ext_syntax.or(line_syntax)
195+ let line_syntax = String::from_utf8(reader.first_line.clone())
196+ .ok()
197+ .and_then(|l| self.syntax_set.find_syntax_by_first_line(&l));
198+
199+ dbg!(path);
200+ match dbg!(mapping.get_syntax_for(path)) {
201+ Some(MappingTarget::MapTo(syntax_name)) => {
202+ // TODO: we should probably return an error here if this syntax can not be
203+ // found. Currently, we just fall back to 'plain'.
204+ self.syntax_set.find_syntax_by_name(syntax_name)
205+ }
206+ Some(MappingTarget::MapToUnknown) => line_syntax,
207+ None => ext_syntax.or(line_syntax),
208+ }
206209 }
207210 (None, InputFile::StdIn) => String::from_utf8(reader.first_line.clone())
208211 .ok()
@@ -225,19 +228,19 @@ mod tests {
225228
226229 use crate::assets::HighlightingAssets;
227230 use crate::inputfile::InputFile;
228− use crate::syntax_mapping::SyntaxMapping;
231+ use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
229232
230− struct SyntaxDetectionTest {
233+ struct SyntaxDetectionTest<'a> {
231234 assets: HighlightingAssets,
232− pub syntax_mapping: SyntaxMapping,
235+ pub syntax_mapping: SyntaxMapping<'a>,
233236 temp_dir: TempDir,
234237 }
235238
236− impl SyntaxDetectionTest {
239+ impl<'a> SyntaxDetectionTest<'a> {
237240 fn new() -> Self {
238241 SyntaxDetectionTest {
239242 assets: HighlightingAssets::from_binary(),
240− syntax_mapping: SyntaxMapping::new(),
243+ syntax_mapping: SyntaxMapping::builtin(),
241244 temp_dir: TempDir::new("bat_syntax_detection_tests")
242245 .expect("creation of temporary directory"),
243246 }
@@ -294,6 +297,10 @@ mod tests {
294297 test.syntax_name_with_content("my_script", "#!/bin/bash"),
295298 "Bourne Again Shell (bash)"
296299 );
300+ assert_eq!(
301+ test.syntax_name_with_content("build", "#!/bin/bash"),
302+ "Bourne Again Shell (bash)"
303+ );
297304 assert_eq!(test.syntax_name_with_content("my_script", "<?php"), "PHP");
298305 }
299306
@@ -302,7 +309,20 @@ mod tests {
302309 let mut test = SyntaxDetectionTest::new();
303310
304311 assert_ne!(test.syntax_name("test.h"), "C++");
305− test.syntax_mapping.insert("h", "cpp");
312+ test.syntax_mapping
313+ .insert("*.h", MappingTarget::MapTo("C++"))
314+ .ok();
306315 assert_eq!(test.syntax_name("test.h"), "C++");
307316 }
317+
318+ #[test]
319+ fn syntax_detection_is_case_sensitive() {
320+ let mut test = SyntaxDetectionTest::new();
321+
322+ assert_ne!(test.syntax_name("README.MD"), "Markdown");
323+ test.syntax_mapping
324+ .insert("*.MD", MappingTarget::MapTo("Markdown"))
325+ .ok();
326+ assert_eq!(test.syntax_name("README.MD"), "Markdown");
327+ }
308328}
src/bin/bat/app.rs+4−4
@@ -18,8 +18,8 @@ use ansi_term;
1818
1919use bat::{
2020 config::{
21− Config, HighlightedLineRanges, InputFile, LineRange, LineRanges, OutputWrap, PagingMode,
22− StyleComponent, StyleComponents, SyntaxMapping,
21+ Config, HighlightedLineRanges, InputFile, LineRange, LineRanges, MappingTarget, OutputWrap,
22+ PagingMode, StyleComponent, StyleComponents, SyntaxMapping,
2323 },
2424 errors::*,
2525 HighlightingAssets,
@@ -104,7 +104,7 @@ impl App {
104104 }
105105 };
106106
107− let mut syntax_mapping = SyntaxMapping::new();
107+ let mut syntax_mapping = SyntaxMapping::builtin();
108108
109109 if let Some(values) = self.matches.values_of("map-syntax") {
110110 for from_to in values {
@@ -114,7 +114,7 @@ impl App {
114114 return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is 'from:to'.".into());
115115 }
116116
117− syntax_mapping.insert(parts[0], parts[1]);
117+ syntax_mapping.insert(parts[0], MappingTarget::MapTo(parts[1]))?;
118118 }
119119 }
120120
src/config.rs+2−2
@@ -1,7 +1,7 @@
11pub use crate::inputfile::InputFile;
22pub use crate::line_range::{HighlightedLineRanges, LineRange, LineRanges};
33pub use crate::style::{StyleComponent, StyleComponents};
4−pub use crate::syntax_mapping::SyntaxMapping;
4+pub use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
55pub use crate::wrap::OutputWrap;
66
77#[derive(Debug, Clone, Copy, PartialEq)]
@@ -60,7 +60,7 @@ pub struct Config<'a> {
6060 pub theme: String,
6161
6262 /// File extension/name mappings
63− pub syntax_mapping: SyntaxMapping,
63+ pub syntax_mapping: SyntaxMapping<'a>,
6464
6565 /// Command to start the pager
6666 pub pager: Option<&'a str>,
src/errors.rs+1
@@ -6,6 +6,7 @@ error_chain! {
66 Io(::std::io::Error);
77 SyntectError(::syntect::LoadingError);
88 ParseIntError(::std::num::ParseIntError);
9+ GlobParsingError(::globset::Error);
910 }
1011}
1112
src/syntax_mapping.rs+68−18
@@ -1,35 +1,85 @@
1−use std::borrow::Cow;
2−use std::collections::HashMap;
1+use std::path::Path;
2+
3+use crate::errors::Result;
4+
5+use globset::{Candidate, GlobBuilder, GlobMatcher};
6+
7+#[derive(Debug, Clone, Copy, PartialEq)]
8+pub enum MappingTarget<'a> {
9+ MapTo(&'a str),
10+ MapToUnknown,
11+}
312
413#[derive(Debug, Clone, Default)]
5−pub struct SyntaxMapping(HashMap<String, String>);
14+pub struct SyntaxMapping<'a> {
15+ mappings: Vec<(GlobMatcher, MappingTarget<'a>)>,
16+}
617
7−impl SyntaxMapping {
8− pub fn new() -> SyntaxMapping {
18+impl<'a> SyntaxMapping<'a> {
19+ pub fn empty() -> SyntaxMapping<'a> {
920 Default::default()
1021 }
1122
12− pub fn insert(&mut self, from: impl Into<String>, to: impl Into<String>) -> Option<String> {
13− self.0.insert(from.into(), to.into())
23+ pub fn builtin() -> SyntaxMapping<'a> {
24+ let mut mapping = Self::empty();
25+ mapping
26+ .insert("build", MappingTarget::MapToUnknown)
27+ .unwrap();
28+
29+ mapping
30+ }
31+
32+ pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
33+ let glob = GlobBuilder::new(from)
34+ .case_insensitive(false)
35+ .literal_separator(true)
36+ .build()?;
37+ self.mappings.push((glob.compile_matcher(), to));
38+ Ok(())
1439 }
1540
16− pub(crate) fn replace<'a>(&self, input: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
17− let input = input.into();
18− match self.0.get(input.as_ref()) {
19− Some(s) => Cow::from(s.clone()),
20− None => input,
41+ pub(crate) fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
42+ let candidate = Candidate::new(path.as_ref());
43+ let canddidate_filename = path.as_ref().file_name().map(Candidate::new);
44+ for (ref glob, ref syntax) in &self.mappings {
45+ if glob.is_match_candidate(&candidate)
46+ || canddidate_filename
47+ .as_ref()
48+ .map_or(false, |filename| glob.is_match_candidate(filename))
49+ {
50+ return Some(*syntax);
51+ }
2152 }
53+ None
2254 }
2355}
2456
2557#[test]
2658fn basic() {
27− let mut map = SyntaxMapping::new();
28− map.insert("Cargo.lock", "toml");
29− map.insert(".ignore", ".gitignore");
59+ let mut map = SyntaxMapping::empty();
60+ map.insert("/path/to/Cargo.lock", MappingTarget::MapTo("TOML"))
61+ .ok();
62+ map.insert("/path/to/.ignore", MappingTarget::MapTo("Git Ignore"))
63+ .ok();
64+
65+ assert_eq!(
66+ map.get_syntax_for("/path/to/Cargo.lock"),
67+ Some(MappingTarget::MapTo("TOML"))
68+ );
69+ assert_eq!(map.get_syntax_for("/path/to/other.lock"), None);
3070
31− assert_eq!("toml", map.replace("Cargo.lock"));
32− assert_eq!("other.lock", map.replace("other.lock"));
71+ assert_eq!(
72+ map.get_syntax_for("/path/to/.ignore"),
73+ Some(MappingTarget::MapTo("Git Ignore"))
74+ );
75+}
76+
77+#[test]
78+fn builtin_mappings() {
79+ let map = SyntaxMapping::builtin();
3380
34− assert_eq!(".gitignore", map.replace(".ignore"));
81+ assert_eq!(
82+ map.get_syntax_for("/path/to/build"),
83+ Some(MappingTarget::MapToUnknown)
84+ );
3585}