summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOliver Looney <88001922+Oliver-Looney@users.noreply.github.com>2024-02-11 22:53:48 +0000
committerGitHub <noreply@github.com>2024-02-11 22:53:48 +0000
commitc3f2ddf5092e9f81052e3183fdbf5ae68147a8af (patch)
tree5db103ecca22c96828cfed69dff304be35c4631f
parent8a51172b119d95fd5984ad46639307140b675c1b (diff)
parent5a2a20af42e7a426943122524287844acc5ee975 (diff)
Merge branch 'master' into 2783-setting-terminal-title
-rw-r--r--CHANGELOG.md11
-rw-r--r--Cargo.lock166
-rw-r--r--Cargo.toml17
-rw-r--r--README.md44
m---------assets/syntaxes/02_Extra/SublimeJQ0
m---------assets/syntaxes/02_Extra/cmd-help0
m---------assets/themes/zenburn0
-rw-r--r--build/main.rs3
-rw-r--r--build/syntax_mapping.rs292
-rw-r--r--src/assets.rs2
-rw-r--r--src/assets/assets_metadata.rs2
-rw-r--r--src/assets/lazy_theme_set.rs3
-rw-r--r--src/bin/bat/app.rs6
-rw-r--r--src/bin/bat/main.rs10
-rw-r--r--src/controller.rs2
-rw-r--r--src/printer.rs136
-rw-r--r--src/syntax_mapping.rs334
-rw-r--r--src/syntax_mapping/builtin.rs91
-rw-r--r--src/syntax_mapping/builtins/README.md116
-rw-r--r--src/syntax_mapping/builtins/bsd-family/.gitkeep0
-rw-r--r--src/syntax_mapping/builtins/bsd-family/50-os-release.toml2
-rw-r--r--src/syntax_mapping/builtins/common/.gitkeep0
-rw-r--r--src/syntax_mapping/builtins/common/50-apache.toml2
-rw-r--r--src/syntax_mapping/builtins/common/50-bat.toml2
-rw-r--r--src/syntax_mapping/builtins/common/50-container.toml2
-rw-r--r--src/syntax_mapping/builtins/common/50-cpp.toml6
-rw-r--r--src/syntax_mapping/builtins/common/50-f-sharp.toml2
-rw-r--r--src/syntax_mapping/builtins/common/50-git.toml10
-rw-r--r--src/syntax_mapping/builtins/common/50-jsonl.toml3
-rw-r--r--src/syntax_mapping/builtins/common/50-nginx.toml2
-rw-r--r--src/syntax_mapping/builtins/common/50-nmap.toml3
-rw-r--r--src/syntax_mapping/builtins/common/50-proxy-auto-config.toml3
-rw-r--r--src/syntax_mapping/builtins/common/50-ron.toml3
-rw-r--r--src/syntax_mapping/builtins/common/50-sarif.toml3
-rw-r--r--src/syntax_mapping/builtins/common/50-ssh.toml2
-rw-r--r--src/syntax_mapping/builtins/common/99-unset-ambiguous-extensions.toml5
-rw-r--r--src/syntax_mapping/builtins/common/99-unset-ambiguous-filenames.toml7
-rw-r--r--src/syntax_mapping/builtins/linux/.gitkeep0
-rw-r--r--src/syntax_mapping/builtins/linux/50-os-release.toml7
-rw-r--r--src/syntax_mapping/builtins/linux/50-pacman.toml3
-rw-r--r--src/syntax_mapping/builtins/linux/50-systemd.toml21
-rw-r--r--src/syntax_mapping/builtins/macos/.gitkeep0
-rw-r--r--src/syntax_mapping/builtins/unix-family/.gitkeep0
-rw-r--r--src/syntax_mapping/builtins/unix-family/50-apache.toml2
-rw-r--r--src/syntax_mapping/builtins/unix-family/50-fish-shell.toml2
-rw-r--r--src/syntax_mapping/builtins/unix-family/50-korn-shell.toml3
-rw-r--r--src/syntax_mapping/builtins/unix-family/50-mail-spool.toml2
-rw-r--r--src/syntax_mapping/builtins/unix-family/50-nginx.toml2
-rw-r--r--src/syntax_mapping/builtins/unix-family/50-shell.toml5
-rw-r--r--src/syntax_mapping/builtins/windows/.gitkeep0
-rw-r--r--src/vscreen.rs735
-rwxr-xr-xtests/benchmarks/run-benchmarks.sh53
-rw-r--r--tests/benchmarks/startup-time-src/Containerfile3
-rw-r--r--tests/benchmarks/startup-time-src/mystery-file3
-rw-r--r--tests/examples/regression_tests/issue_2541.txt1
-rw-r--r--tests/examples/this-file-path-is-really-long-and-would-have-broken-the-layout-of-the-header.txt1
-rw-r--r--tests/integration_tests.rs125
-rw-r--r--tests/syntax-tests/highlighted/JQ/sample.jq32
58 files changed, 1935 insertions, 357 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3399922..b41d8d10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,9 @@
## Bugfixes
+- Fix long file name wrapping in header, see #2835 (@FilipRazek)
- Fix `NO_COLOR` support, see #2767 (@acuteenvy)
+- Fix handling of inputs with OSC ANSI escape sequences, see #2541 and #2544 (@eth-p)
## Other
@@ -17,18 +19,27 @@
- Minor benchmark script improvements #2768 (@cyqsimon)
- Update Arch Linux package URL in README files #2779 (@brunobell)
- Update and improve `zsh` completion, see #2772 (@okapia)
+- More extensible syntax mapping mechanism #2755 (@cyqsimon)
- Use proper Architecture for Debian packages built for musl, see #2811 (@Enselic)
- Pull in fix for unsafe-libyaml security advisory, see #2812 (@dtolnay)
- Update git-version dependency to use Syn v2, see #2816 (@dtolnay)
+- Update git2 dependency to v0.18.2, see #2852 (@eth-p)
## Syntaxes
- `cmd-help`: scope subcommands followed by other terms, and other misc improvements, see #2819 (@victor-gp)
+- Upgrade JQ syntax, see #2820 (@dependabot[bot])
## Themes
## `bat` as a library
+- Changes to `syntax_mapping::SyntaxMapping` #2755 (@cyqsimon)
+ - `SyntaxMapping::get_syntax_for` is now correctly public
+ - [BREAKING] `SyntaxMapping::{empty,builtin}` are removed; use `SyntaxMapping::new` instead
+ - [BREAKING] `SyntaxMapping::mappings` is replaced by `SyntaxMapping::{builtin,custom,all}_mappings`
+- Make `Controller::run_with_error_handler`'s error handler `FnMut`, see #2831 (@rhysd)
+- Improve compile time by 20%, see #2815 (@dtolnay)
# v0.24.0
diff --git a/Cargo.lock b/Cargo.lock
index 19ee5928..ff674b9d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -129,6 +129,8 @@ dependencies = [
"globset",
"grep-cli",
"home",
+ "indexmap 2.2.2",
+ "itertools",
"nix",
"nu-ansi-term",
"once_cell",
@@ -140,12 +142,15 @@ dependencies = [
"run_script",
"semver",
"serde",
+ "serde_derive",
+ "serde_with",
"serde_yaml",
"serial_test",
"shell-words",
"syntect",
"tempfile",
"thiserror",
+ "toml",
"unicode-width",
"wait-timeout",
"walkdir",
@@ -224,11 +229,12 @@ checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
[[package]]
name = "cc"
-version = "1.0.73"
+version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"jobserver",
+ "libc",
]
[[package]]
@@ -267,13 +273,14 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "clircle"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e87cbed5354f17bd8ca8821a097fb62599787fe8f611743fad7ee156a0a600"
+checksum = "ec0b92245ea62a7a751db4b0e4a583f8978e508077ef6de24fcc0d0dc5311a8d"
dependencies = [
"cfg-if",
"libc",
"serde",
+ "serde_derive",
"winapi",
]
@@ -315,6 +322,41 @@ dependencies = [
]
[[package]]
+name = "darling"
+version = "0.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "dashmap"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -540,7 +582,7 @@ dependencies = [
"bstr",
"log",
"regex-automata",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
[[package]]
@@ -579,6 +621,12 @@ dependencies = [
]
[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -600,12 +648,13 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.0.2"
+version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
+checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
dependencies = [
"equivalent",
"hashbrown 0.14.1",
+ "serde",
]
[[package]]
@@ -646,9 +695,9 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "libgit2-sys"
-version = "0.16.1+1.7.1"
+version = "0.16.2+1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c"
+checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
dependencies = [
"cc",
"libc",
@@ -980,7 +1029,7 @@ dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
[[package]]
@@ -991,17 +1040,11 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
[[package]]
name = "regex-syntax"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
-
-[[package]]
-name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
@@ -1066,9 +1109,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
-version = "1.0.20"
+version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
+checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
[[package]]
name = "serde"
@@ -1102,12 +1145,44 @@ dependencies = [
]
[[package]]
+name = "serde_spanned"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "serde_with_macros",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "serde_yaml"
version = "0.9.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15e0ef66bf939a7c890a0bf6d5a733c70202225f9888a89ed5c62298b019129"
dependencies = [
- "indexmap 2.0.2",
+ "indexmap 2.2.2",
"itoa",
"ryu",
"serde",
@@ -1180,9 +1255,9 @@ dependencies = [
[[package]]
name = "syntect"
-version = "5.1.0"
+version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91"
+checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
dependencies = [
"bincode",
"bitflags 1.3.2",
@@ -1192,8 +1267,9 @@ dependencies = [
"once_cell",
"onig",
"plist",
- "regex-syntax 0.7.5",
+ "regex-syntax",
"serde",
+ "serde_derive",
"serde_json",
"thiserror",
"walkdir",
@@ -1295,6 +1371,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
+name = "toml"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325"
+dependencies = [
+ "indexmap 2.2.2",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
+dependencies = [
+ "indexmap 2.2.2",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
name = "unicode-bidi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1614,6 +1725,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
+name = "winnow"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 53bc2da4..05a2acb7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,11 +53,12 @@ content_inspector = "0.2.4"
shell-words = { version = "1.1.0", optional = true }
unicode-width = "0.1.11"
globset = "0.4"
-serde = { version = "1.0", features = ["derive"] }
+serde = "1.0"
+serde_derive = "1.0"
serde_yaml = "0.9.28"
semver = "1.0"
path_abs = { version = "0.5", default-features = false }
-clircle = "0.4"
+clircle = "0.5"
bugreport = { version = "0.5.0", optional = true }
etcetera = { version = "0.8.0", optional = true }
grep-cli = { version = "0.1.10", optional = true }
@@ -74,7 +75,7 @@ optional = true
default-features = false
[dependencies.syntect]
-version = "5.1.0"
+version = "5.2.0"
default-features = false
features = ["parsing"]
@@ -94,12 +95,22 @@ serial_test = { version = "2.0.0", default-features = false }
predicates = "3.0.4"
wait-timeout = "0.2.0"
tempfile = "3.8.1"
+serde = { version = "1.0", features = ["derive"] }
[target.'cfg(unix)'.dev-dependencies]
nix = { version = "0.26.4", default-features = false, features = ["term"] }
[build-dependencies]
anyhow = "1.0.78"
+indexmap = { version = "2.2.2", features = ["serde"] }
+itertools = "0.11.0"
+once_cell = "1.18"
+regex = "1.10.2"
+serde = "1.0"
+serde_derive = "1.0"
+serde_with = { version = "3.6.1", default-features = false, features = ["macros"] }
+toml = { version = "0.8.9", features = ["preserve_order"] }
+walkdir = "2.4"
[build-dependencies.clap]
version = "4.4.12"
diff --git a/README.md b/README.md
index 352ae64d..57baf2b0 100644
--- a/README.md
+++ b/README.md
@@ -602,7 +602,8 @@ set, `less` is used by default. If you want to use a different pager, you can ei
`PAGER` variable or set the `BAT_PAGER` environment variable to override what is specified in
`PAGER`.
-**Note**: If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
+>[!NOTE]
+> If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
If you want to pass command-line arguments to the pager, you can also set them via the
`PAGER`/`BAT_PAGER` variables:
@@ -613,20 +614,37 @@ export BAT_PAGER="less -RF"
Instead of using environment variables, you can also use `bat`s [configuration file](https://github.com/sharkdp/bat#configuration-file) to configure the pager (`--pager` option).
-**Note**: By default, if the pager is set to `less` (and no command-line options are specified),
-`bat` will pass the following command line options to the pager: `-R`/`--RAW-CONTROL-CHARS`,
-`-F`/`--quit-if-one-screen` and `-X`/`--no-init`. The last option (`-X`) is only used for `less`
-versions older than 530.
-The `-R` option is needed to interpret ANSI colors correctly. The second option (`-F`) instructs
-less to exit immediately if the output size is smaller than the vertical size of the terminal.
-This is convenient for small files because you do not have to press `q` to quit the pager. The
-third option (`-X`) is needed to fix a bug with the `--quit-if-one-screen` feature in old versions
-of `less`. Unfortunately, it also breaks mouse-wheel support in `less`.
+### Using `less` as a pager
-If you want to enable mouse-wheel scrolling on older versions of `less`, you can pass just `-R` (as
-in the example above, this will disable the quit-if-one-screen feature). For less 530 or newer,
-it should work out of the box.
+When using `less` as a pager, `bat` will automatically pass extra options along to `less`
+to improve the experience. Specifically, `-R`/`--RAW-CONTROL-CHARS`, `-F`/`--quit-if-one-screen`,
+and under certain conditions, `-X`/`--no-init` and/or `-S`/`--chop-long-lines`.
+
+>[!IMPORTANT]
+> These options will not be added if:
+> - The pager is not named `less`.
+> - The `--pager` argument contains any command-line arguments (e.g. `--pager="less -R"`).
+> - The `BAT_PAGER` environment variable contains any command-line arguments (e.g. `export BAT_PAGER="less -R"`)
+>
+> The `--quit-if-one-screen` option will not be added when:
+> - The `--paging=always` argument is used.
+> - The `BAT_PAGING` environment is set to `always`.
+
+The `-R` option is needed to interpret ANSI colors correctly.
+
+The `-F` option instructs `less` to exit immediately if the output size is smaller than
+the vertical size of the terminal. This is convenient for small files because you do not
+have to press `q` to quit the pager.
+
+The `-X` option is needed to fix a bug with the `--quit-if-one-screen` feature in versions
+of `less` older than version 530. Unfortunately, it also breaks mouse-wheel support in `less`.
+If you want to enable mouse-wheel scrolling on older versions of `less` and do not mind losing
+the quit-if-one-screen feature, you can set the pager (via `--pager` or `BAT_PAGER`) to `less -R`.
+For `less` 530 or newer, it should work out of the box.
+
+The `-S` option is added when `bat`'s `-S`/`--chop-long-lines` option is used. This tells `less`
+to truncate any lines larger than the terminal width.
### Indentation
diff --git a/assets/syntaxes/02_Extra/SublimeJQ b/assets/syntaxes/02_Extra/SublimeJQ
-Subproject 687058289c1a888e0895378432d66b41609a84d
+Subproject b7e53e5d86814f04a48d2e441bcf5f9fdf07e9c
diff --git a/assets/syntaxes/02_Extra/cmd-help b/assets/syntaxes/02_Extra/cmd-help
-Subproject b150d84534dd060afdcaf3f58977faeaf5917e5
+Subproject 209559b72f7e8848c988828088231b3a4d8b683
diff --git a/assets/themes/zenburn b/assets/themes/zenburn
-Subproject e627f1cb223c1171ab0a6a48d166c87aeae2a1d
+Subproject 86d4ee7a1f884851a1d21d66249687f527fced3
diff --git a/build/main.rs b/build/main.rs
index 416d90d5..8966ee52 100644
--- a/build/main.rs
+++ b/build/main.rs
@@ -1,5 +1,6 @@
#[cfg(feature = "application")]
mod application;
+mod syntax_mapping;
mod util;
fn main() -> anyhow::Result<()> {
@@ -7,6 +8,8 @@ fn main() -> anyhow::Result<()> {
// see: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed
println!("cargo:rerun-if-changed=build/");
+ syntax_mapping::build_static_mappings()?;
+
#[cfg(feature = "application")]
application::gen_man_and_comp()?;
diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs
new file mode 100644
index 00000000..959caea8
--- /dev/null
+++ b/build/syntax_mapping.rs
@@ -0,0 +1,292 @@
+use std::{
+ convert::Infallible,
+ env, fs,
+ path::{Path, PathBuf},
+ str::FromStr,
+};
+
+use anyhow::{anyhow, bail};
+use indexmap::IndexMap;
+use itertools::Itertools;
+use once_cell::sync::Lazy;
+use regex::Regex;
+use serde_derive::Deserialize;
+use serde_with::DeserializeFromStr;
+use walkdir::WalkDir;
+
+/// Known mapping targets.
+///
+/// Corresponds to `syntax_mapping::MappingTarget`.
+#[allow(clippy::enum_variant_names)]
+#[derive(Clone, Debug, Eq, PartialEq, Hash, DeserializeFromStr)]
+pub enum MappingTarget {
+ MapTo(String),
+ MapToUnknown,
+ MapExtensionToUnknown,
+}
+impl FromStr for MappingTarget {
+ type Err = Infallible;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "MappingTarget::MapToUnknown" => Ok(Self::MapToUnknown),
+ "MappingTarget::MapExtensionToUnknown" => Ok(Self::MapExtensionToUnknown),
+ syntax => Ok(Self::MapTo(syntax.into())),
+ }
+ }
+}
+impl MappingTarget {
+ fn codegen(&self) -> String {
+ match self {
+ Self::MapTo(syntax) => format!(r###"MappingTarget::MapTo(r#"{syntax}"#)"###),
+ Self::MapToUnknown => "MappingTarget::MapToUnknown".into(),
+ Self::MapExtensionToUnknown => "MappingTarget::MapExtensionToUnknown".into(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)]
+/// A single matcher.
+///
+/// Codegen converts this into a `Lazy<Option<GlobMatcher>>`.
+struct Matcher(Vec<MatcherSegment>);
+/// Parse a matcher.
+///
+/// Note that this implementation is rather strict: it will greedily interpret
+/// every valid environment variable replacement as such, then immediately
+/// hard-error if it finds a '$', '{', or '}' anywhere in the remaining text
+/// segments.
+///
+/// The reason for this strictness is I currently cannot think of a valid reason
+/// why you would ever need '$', '{', or '}' as plaintext in a glob pattern.
+/// Therefore any such occurrences are likely human errors.
+///
+/// If we later discover some edge cases, it's okay to make it more permissive.
+impl FromStr for Matcher {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ use MatcherSegment as Seg;
+ static VAR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap());
+
+ let mut segments = vec![];
+ let mut text_start = 0;
+ for capture in VAR_REGEX.captures_iter(s) {
+ let match_0 = capture.get(0).unwrap();
+
+ // text before this var
+ let text_end = match_0.start();
+ segments.push(Seg::Text(s[text_start..text_end].into()));
+ text_start = match_0.end();
+
+ // this var
+ segments.push(Seg::Env(capture.get(1).unwrap().as_str().into()));
+ }
+ // possible trailing text
+ segments.push(Seg::Text(s[text_start..].into()));
+
+ // cleanup empty text segments
+ let non_empty_segments = segments
+ .into_iter()
+ .filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true))
+ .collect_vec();
+
+ // sanity check
+ if non_empty_segments
+ .windows(2)
+ .any(|segs| segs[0].is_text() && segs[1].is_text())
+ {
+ unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}");
+ }
+
+