From ba97230b9854da12cb6b861d9a7823c667e51d21 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 13:34:33 +0100 Subject: [PATCH 01/67] feat: Map BUILD to Python (Starlark) for Bazel (fixes #3575) See #3575. --- src/syntax_mapping/builtins/common/50-bazel.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/syntax_mapping/builtins/common/50-bazel.toml diff --git a/src/syntax_mapping/builtins/common/50-bazel.toml b/src/syntax_mapping/builtins/common/50-bazel.toml new file mode 100644 index 00000000..5f44ae2d --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -0,0 +1,2 @@ +[mappings] +"Python" = ["BUILD"] From 5a4a7de9338be2b888674259ee29db3ff6b9b6fe Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 13:40:51 +0100 Subject: [PATCH 02/67] docs: Document mapping BUILD to Python (Starlark) for Bazel in CHANGELOG.md (see #3576) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9934329f..bfc516c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ - Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX) - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) +- Map BUILD to Python (Starlark) for Bazel, see #3576 (@vorburger) ## Bugfixes + - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) From ab393a02816e7e76b763abd53a55cdf8a418c563 Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Thu, 12 Feb 2026 21:06:03 +0000 Subject: [PATCH 03/67] Add COBOL syntax highlighting --- .gitmodules | 3 + assets/syntaxes/02_Extra/COBOL | 1 + tests/syntax-tests/highlighted/COBOL/test.cbl | 61 +++++++++++++++++++ tests/syntax-tests/source/COBOL/test.cbl | 61 +++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 160000 assets/syntaxes/02_Extra/COBOL create mode 100644 tests/syntax-tests/highlighted/COBOL/test.cbl create mode 100644 tests/syntax-tests/source/COBOL/test.cbl diff --git a/.gitmodules b/.gitmodules index ee84fe05..b64280c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -278,3 +278,6 @@ [submodule "assets/syntaxes/02_Extra/Gomod"] path = assets/syntaxes/02_Extra/Gomod url = https://github.com/mitranim/sublime-gomod +[submodule "assets/syntaxes/02_Extra/COBOL"] + path = assets/syntaxes/02_Extra/COBOL + url = https://github.com/adukhan99/sublime_cobol.git diff --git a/assets/syntaxes/02_Extra/COBOL b/assets/syntaxes/02_Extra/COBOL new file mode 160000 index 00000000..90f88bf6 --- /dev/null +++ b/assets/syntaxes/02_Extra/COBOL @@ -0,0 +1 @@ +Subproject commit 90f88bf65f339be3b42d6c490bca0a4a85cefad0 diff --git a/tests/syntax-tests/highlighted/COBOL/test.cbl b/tests/syntax-tests/highlighted/COBOL/test.cbl new file mode 100644 index 00000000..11e19fe9 --- /dev/null +++ b/tests/syntax-tests/highlighted/COBOL/test.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file diff --git a/tests/syntax-tests/source/COBOL/test.cbl b/tests/syntax-tests/source/COBOL/test.cbl new file mode 100644 index 00000000..ab552167 --- /dev/null +++ b/tests/syntax-tests/source/COBOL/test.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file From 9f8ac934abbdbb3a71270356aa51f18e635df79f Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Thu, 12 Feb 2026 21:43:34 +0000 Subject: [PATCH 04/67] Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a628e329..0ef08a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) - Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) +- Add syntax highlighting support for COBOL, see #3479 (@adukhan99) ## Themes From 2b47f3c5eb314cdccc66319a8204304f75d22d5a Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Thu, 12 Feb 2026 22:10:08 +0000 Subject: [PATCH 05/67] Updated CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef08a67..181e1bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ - Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) - Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) -- Add syntax highlighting support for COBOL, see #3479 (@adukhan99) +- Add syntax highlighting support for COBOL, see #3584 (@adukhan99) ## Themes From 91965d28a6cebd6ed7aebc27f7c5b3d0b73d9261 Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Wed, 11 Mar 2026 14:36:20 +0000 Subject: [PATCH 06/67] Add COBOL sans CPY ext --- assets/syntaxes/02_Extra/COBOL | 2 +- .../highlighted/COBOL/payroll.cbl | 61 +++++++++++++++++++ tests/syntax-tests/source/COBOL/payroll.cbl | 61 +++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/syntax-tests/highlighted/COBOL/payroll.cbl create mode 100644 tests/syntax-tests/source/COBOL/payroll.cbl diff --git a/assets/syntaxes/02_Extra/COBOL b/assets/syntaxes/02_Extra/COBOL index 90f88bf6..00a12937 160000 --- a/assets/syntaxes/02_Extra/COBOL +++ b/assets/syntaxes/02_Extra/COBOL @@ -1 +1 @@ -Subproject commit 90f88bf65f339be3b42d6c490bca0a4a85cefad0 +Subproject commit 00a12937cfff328f24db55ce7955682542cc872d diff --git a/tests/syntax-tests/highlighted/COBOL/payroll.cbl b/tests/syntax-tests/highlighted/COBOL/payroll.cbl new file mode 100644 index 00000000..11e19fe9 --- /dev/null +++ b/tests/syntax-tests/highlighted/COBOL/payroll.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file diff --git a/tests/syntax-tests/source/COBOL/payroll.cbl b/tests/syntax-tests/source/COBOL/payroll.cbl new file mode 100644 index 00000000..ab552167 --- /dev/null +++ b/tests/syntax-tests/source/COBOL/payroll.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file From 927873392b87e50b349c2ec11a0d07d5f400d3de Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Wed, 11 Mar 2026 15:06:47 +0000 Subject: [PATCH 07/67] Drop .ss extension from COBOL --- assets/syntaxes/02_Extra/COBOL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/syntaxes/02_Extra/COBOL b/assets/syntaxes/02_Extra/COBOL index 00a12937..27825ee0 160000 --- a/assets/syntaxes/02_Extra/COBOL +++ b/assets/syntaxes/02_Extra/COBOL @@ -1 +1 @@ -Subproject commit 00a12937cfff328f24db55ce7955682542cc872d +Subproject commit 27825ee0220fb7fee73617f7404d4d214dc71b8f From ac97356930695dd33b3ff664758406b2384b7440 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:54:46 +0000 Subject: [PATCH 08/67] build(deps): bump assets/syntaxes/02_Extra/cmd-help Bumps [assets/syntaxes/02_Extra/cmd-help](https://github.com/victor-gp/cmd-help-sublime-syntax) from `273cb98` to `db0a616`. - [Commits](https://github.com/victor-gp/cmd-help-sublime-syntax/compare/273cb988177e96f4187e06008b13fa72ad22ae4d...db0a616cfa367a4f52fd81d1a46c8ebea11e867d) --- updated-dependencies: - dependency-name: assets/syntaxes/02_Extra/cmd-help dependency-version: db0a616cfa367a4f52fd81d1a46c8ebea11e867d dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- assets/syntaxes/02_Extra/cmd-help | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/syntaxes/02_Extra/cmd-help b/assets/syntaxes/02_Extra/cmd-help index 273cb988..2757ac16 160000 --- a/assets/syntaxes/02_Extra/cmd-help +++ b/assets/syntaxes/02_Extra/cmd-help @@ -1 +1 @@ -Subproject commit 273cb988177e96f4187e06008b13fa72ad22ae4d +Subproject commit 2757ac168123de230cd4374d6a44e0db9cd10f4c From b1f04995a64e3359d4519c04f31efa56ac282292 Mon Sep 17 00:00:00 2001 From: XploitMonk0x01 Date: Sat, 14 Mar 2026 11:44:38 +0530 Subject: [PATCH 09/67] docs: clarify supported custom theme format --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 771db7ad..95f4e2a4 100644 --- a/README.md +++ b/README.md @@ -581,7 +581,8 @@ syntax: This works very similar to how we add new syntax definitions. > [!NOTE] -> Themes are stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). +> Custom themes must be stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). +> Newer `.sublime-color-scheme` files are currently not supported. First, create a folder with the new syntax highlighting themes: ```bash From 56fe0fa2260675c2603607ac35c4c367e5d8d797 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Sat, 14 Mar 2026 09:18:56 +0200 Subject: [PATCH 10/67] Add case-sensitive glob support to syntax mapping to allow us to map `BUILD` case sensitively to Python for Skylark --- build/syntax_mapping.rs | 40 +++++++++++++-- src/syntax_mapping.rs | 50 +++++++++++++++++-- src/syntax_mapping/builtin.rs | 9 ++-- src/syntax_mapping/builtins/README.md | 6 +-- .../builtins/common/50-bazel.toml | 2 +- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 64be4bb9..f17b1cf5 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -51,7 +51,13 @@ impl ToTokens for MappingTarget { /// A single matcher. /// /// Codegen converts this into a `Lazy>`. -struct Matcher(Vec); +struct Matcher { + segments: Vec, + /// Whether the glob pattern should be matched case-insensitively. + /// + /// Defaults to `true` (case-insensitive) for backwards compatibility. + case_insensitive: bool, +} /// Parse a matcher. /// /// Note that this implementation is rather strict: it will greedily interpret @@ -116,18 +122,24 @@ impl FromStr for Matcher { bail!(r#"Invalid matcher: "{s}""#); } - Ok(Self(non_empty_segments)) + Ok(Self { + segments: non_empty_segments, + case_insensitive: true, + }) } } impl ToTokens for Matcher { fn to_tokens(&self, tokens: &mut TokenStream) { - let t = match self.0.as_slice() { + let case_insensitive = self.case_insensitive; + let t = match self.segments.as_slice() { [] => unreachable!("0-length matcher should never be created"), [MatcherSegment::Text(text)] => { - quote! { Lazy::new(|| Some(build_matcher_fixed(#text))) } + quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case_insensitive))) } } // parser logic ensures that this case can only happen when there are dynamic segments - segs @ [_, ..] => quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ])) }, + segs @ [_, ..] => { + quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case_insensitive)) } + } }; tokens.append_all(t); } @@ -175,7 +187,12 @@ impl MatcherSegment { /// A struct that models a single .toml file in /src/syntax_mapping/builtins/. #[derive(Clone, Debug, Deserialize)] struct MappingDefModel { + #[serde(default)] mappings: IndexMap>, + /// Case-sensitive mappings. Unlike `mappings`, these glob patterns are + /// matched case-sensitively. + #[serde(default)] + case_sensitive_mappings: IndexMap>, } impl MappingDefModel { fn into_mapping_list(self) -> MappingList { @@ -188,6 +205,19 @@ impl MappingDefModel { .map(|matcher| (matcher, target.clone())) .collect::>() }) + .chain( + self.case_sensitive_mappings + .into_iter() + .flat_map(|(target, matchers)| { + matchers + .into_iter() + .map(|mut matcher| { + matcher.case_insensitive = false; + (matcher, target.clone()) + }) + .collect::>() + }), + ) .collect(); MappingList(list) } diff --git a/src/syntax_mapping.rs b/src/syntax_mapping.rs index 0cd2d655..8fc4008a 100644 --- a/src/syntax_mapping.rs +++ b/src/syntax_mapping.rs @@ -17,9 +17,9 @@ use ignored_suffixes::IgnoredSuffixes; mod builtin; pub mod ignored_suffixes; -fn make_glob_matcher(from: &str) -> Result { +fn make_glob_matcher(from: &str, case_insensitive: bool) -> Result { let matcher = GlobBuilder::new(from) - .case_insensitive(true) + .case_insensitive(case_insensitive) .literal_separator(true) .build()? .compile_matcher(); @@ -97,7 +97,14 @@ impl<'a> SyntaxMapping<'a> { } pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { - let matcher = make_glob_matcher(from)?; + let matcher = make_glob_matcher(from, true)?; + self.custom_mappings.push((matcher, to)); + Ok(()) + } + + /// Like [`Self::insert`], but the glob pattern is matched case-sensitively. + pub fn insert_case_sensitive(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { + let matcher = make_glob_matcher(from, false)?; self.custom_mappings.push((matcher, to)); Ok(()) } @@ -261,4 +268,41 @@ mod tests { Some(MappingTarget::MapTo("alpha")) ); } + + #[test] + fn case_sensitive_custom_mappings_work() { + let mut map = SyntaxMapping::new(); + map.insert_case_sensitive("MY_SPECIAL_FILE", MappingTarget::MapTo("Python")) + .ok(); + + // Exact case matches + assert_eq!( + map.get_syntax_for("/path/to/MY_SPECIAL_FILE"), + Some(MappingTarget::MapTo("Python")) + ); + // Different case should NOT match the case-sensitive rule + assert_eq!(map.get_syntax_for("/path/to/my_special_file"), None); + assert_eq!(map.get_syntax_for("/path/to/My_Special_File"), None); + } + + #[test] + fn builtin_mappings_build_is_case_sensitive() { + let map = SyntaxMapping::new(); + + // "BUILD" (uppercase) should map to Python via case-sensitive builtin + assert_eq!( + map.get_syntax_for("/path/to/BUILD"), + Some(MappingTarget::MapTo("Python")) + ); + // "build" (lowercase) should still map to MapToUnknown + assert_eq!( + map.get_syntax_for("/path/to/build"), + Some(MappingTarget::MapToUnknown) + ); + // Mixed case should NOT match the Python rule + assert_eq!( + map.get_syntax_for("/path/to/Build"), + Some(MappingTarget::MapToUnknown) + ); + } } diff --git a/src/syntax_mapping/builtin.rs b/src/syntax_mapping/builtin.rs index 1822be57..52f1d47b 100644 --- a/src/syntax_mapping/builtin.rs +++ b/src/syntax_mapping/builtin.rs @@ -53,8 +53,9 @@ include!(concat!( /// A failure to compile is a fatal error. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_fixed(from: &str) -> GlobMatcher { - make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile") +fn build_matcher_fixed(from: &str, case_insensitive: bool) -> GlobMatcher { + make_glob_matcher(from, case_insensitive) + .expect("A builtin fixed glob matcher failed to compile") } /// Join a list of matcher segments to create a glob string, replacing all @@ -64,7 +65,7 @@ fn build_matcher_fixed(from: &str) -> GlobMatcher { /// to compile. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option { +fn build_matcher_dynamic(segs: &[MatcherSegment], case_insensitive: bool) -> Option { // join segments let mut buf = String::new(); for seg in segs { @@ -77,7 +78,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option { } } // compile glob matcher - let matcher = make_glob_matcher(&buf).ok()?; + let matcher = make_glob_matcher(&buf, case_insensitive).ok()?; Some(matcher) } diff --git a/src/syntax_mapping/builtins/README.md b/src/syntax_mapping/builtins/README.md index 29cf43ee..7152fd33 100644 --- a/src/syntax_mapping/builtins/README.md +++ b/src/syntax_mapping/builtins/README.md @@ -20,9 +20,9 @@ syntax mappings defined by all TOML files, and embed them into the binary. ## File syntax -Each TOML file should contain a single section named `mappings`, with each of -its keys being a language identifier (first column of `bat -L`; also referred to -as "target"). +Each TOML file should contain a single section named `mappings` and/or a single +section named `case_sensitive_mappings`, with each of its keys being a language +identifier (first column of `bat -L`; also referred to as "target"). The value of each key should be an array of strings, with each item being a glob matcher. We will call each of these items a "rule". diff --git a/src/syntax_mapping/builtins/common/50-bazel.toml b/src/syntax_mapping/builtins/common/50-bazel.toml index 5f44ae2d..2b0e17e2 100644 --- a/src/syntax_mapping/builtins/common/50-bazel.toml +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -1,2 +1,2 @@ -[mappings] +[case_sensitive_mappings] "Python" = ["BUILD"] From a1917275cdf4df5a1a744681651cb64db6e0f559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Gonz=C3=A1lez=20Prieto?= Date: Mon, 16 Mar 2026 15:37:23 +0100 Subject: [PATCH 11/67] Add syntax mappings for GCloud CLI config files > .boto => INI I was uninstalling gcloud-cli and the docs said to review the contents of ~/.boto [^1]. `bat` doesn't recognize it but it's just an INI-like file to configure gsutil [^2]. I took the chance to add a few more mappings for other GCloud config files without extension... > .gcloudignore => Git Ignore This one's self-explanatory, but it's documented in [^3]. Makes me wonder if we could just do a blanket `Git Ignore = ".?*ignore"` for every kind of ignore file. For instance, `bat` doesn't catch .dockerignore either. > **/gcloud/configurations/config_* => INI These are configuration files for each different profile you have configured for the gcloud-cli [^4]. They're named like "config_default" or "config_", without a file extension, but they are also INI syntax [^5][^6]. [^1]: https://docs.cloud.google.com/sdk/docs/uninstall-cloud-sdk [^2]: https://docs.cloud.google.com/storage/docs/boto-gsutil [^3]: https://docs.cloud.google.com/sdk/gcloud/reference/topic/gcloudignore [^4]: https://docs.cloud.google.com/sdk/docs/configurations [^5]: https://stackoverflow.com/questions/74290882/return-active-gcp-project-name-programatically-for-both-local-dev-and-deployed-c/7^4292151#74292151 [^6]: https://stackoverflow.com/questions/49212350/where-does-gcloud-store-its-defaults/49256835#49256835 --- CHANGELOG.md | 1 + src/syntax_mapping/builtins/common/50-gcloud-cli-config.toml | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 src/syntax_mapping/builtins/common/50-gcloud-cli-config.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index bd979d14..23e4a29d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) - Add syntax highlighting support for COBOL, see #3584 (@adukhan99) - Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) +- Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp) ## Themes diff --git a/src/syntax_mapping/builtins/common/50-gcloud-cli-config.toml b/src/syntax_mapping/builtins/common/50-gcloud-cli-config.toml new file mode 100644 index 00000000..ea3eec67 --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-gcloud-cli-config.toml @@ -0,0 +1,3 @@ +[mappings] +"INI" = [".boto", "**/gcloud/configurations/config_*"] +"Git Ignore" = [".gcloudignore"] From de209f8a23ad418040da14b882c4a0ce6194ee87 Mon Sep 17 00:00:00 2001 From: rohan436 Date: Mon, 16 Mar 2026 16:47:50 +0800 Subject: [PATCH 12/67] docs: fix README sidebar wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95f4e2a4..f0d4efb4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ languages: ### Git integration `bat` communicates with `git` to show modifications with respect to the index -(see left side bar): +(see left sidebar): ![Git integration example](https://i.imgur.com/2lSW4RE.png) From a4e853c4fa20016a4be5a705f7d637eb90aa012d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Gonz=C3=A1lez=20Prieto?= Date: Tue, 17 Mar 2026 22:59:21 +0100 Subject: [PATCH 13/67] Map ignore files to Git Ignore syntax This would catch any dotfiles that end in "ignore", but not `.ignore` Set as a lower priority rule (90 vs the usual 50) because it's a blanket assignation. If there are conflict with other, more specific rules, those will take precedence. --- CHANGELOG.md | 1 + src/syntax_mapping/builtins/common/90-ignore-files.toml | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 src/syntax_mapping/builtins/common/90-ignore-files.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 23e4a29d..7de41716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Add syntax highlighting support for COBOL, see #3584 (@adukhan99) - Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) - Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp) +- Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp) ## Themes diff --git a/src/syntax_mapping/builtins/common/90-ignore-files.toml b/src/syntax_mapping/builtins/common/90-ignore-files.toml new file mode 100644 index 00000000..90984aff --- /dev/null +++ b/src/syntax_mapping/builtins/common/90-ignore-files.toml @@ -0,0 +1,2 @@ +[mappings] +"Git Ignore" = [".?*ignore"] From ffed52f6ebc449b6dc74041829168541ef46b767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Gonz=C3=A1lez=20Prieto?= Date: Wed, 18 Mar 2026 14:20:35 +0100 Subject: [PATCH 14/67] Document how to contribute syntax mappings --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3852a755..3dcd11bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,8 @@ users, please read the corresponding [documentation](https://github.com/sharkdp/bat/blob/master/doc/assets.md) first. +To map a file name pattern to an existing syntax, read [the documentation here](https://github.com/sharkdp/bat/blob/master/src/syntax_mapping/builtins/README.md). + Note: We are currently not accepting new default themes. From 2a29802dd57a202589e555e896432575225fe76b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 03:35:38 +0000 Subject: [PATCH 15/67] Refactor: string-or-struct matcher syntax, Case enum, remove case_sensitive_mappings table Co-authored-by: keith-hall <11882719+keith-hall@users.noreply.github.com> --- build/syntax_mapping.rs | 83 +++++++++++++------ src/syntax_mapping.rs | 15 +++- src/syntax_mapping/builtin.rs | 10 +-- .../builtins/common/50-bazel.toml | 4 +- 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index f17b1cf5..17722f67 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -47,16 +47,34 @@ impl ToTokens for MappingTarget { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)] +/// Whether a glob pattern should be matched case-sensitively or case-insensitively. +/// +/// Mirrors the runtime `Case` type in `src/syntax_mapping.rs`. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum Case { + Sensitive, + Insensitive, +} +impl ToTokens for Case { + fn to_tokens(&self, tokens: &mut TokenStream) { + let t = match self { + Self::Sensitive => quote! { Case::Sensitive }, + Self::Insensitive => quote! { Case::Insensitive }, + }; + tokens.append_all(t); + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] /// A single matcher. /// /// Codegen converts this into a `Lazy>`. struct Matcher { segments: Vec, - /// Whether the glob pattern should be matched case-insensitively. + /// Whether the glob pattern should be matched case-sensitively. /// - /// Defaults to `true` (case-insensitive) for backwards compatibility. - case_insensitive: bool, + /// Defaults to `Case::Insensitive` for backwards compatibility. + case: Case, } /// Parse a matcher. /// @@ -124,21 +142,53 @@ impl FromStr for Matcher { Ok(Self { segments: non_empty_segments, - case_insensitive: true, + case: Case::Insensitive, }) } } + +/// Helper type for deserializing a `Matcher` from either a plain string or a +/// `{ glob = "...", case_sensitive = true }` struct. +#[derive(Deserialize)] +#[serde(untagged)] +enum RawMatcher { + Simple(String), + Full { + glob: String, + #[serde(default)] + case_sensitive: bool, + }, +} + +impl<'de> serde::Deserialize<'de> for Matcher { + fn deserialize>(deserializer: D) -> Result { + let raw = RawMatcher::deserialize(deserializer)?; + match raw { + RawMatcher::Simple(s) => Matcher::from_str(&s).map_err(serde::de::Error::custom), + RawMatcher::Full { glob, case_sensitive } => { + let mut matcher = + Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; + matcher.case = if case_sensitive { + Case::Sensitive + } else { + Case::Insensitive + }; + Ok(matcher) + } + } + } +} impl ToTokens for Matcher { fn to_tokens(&self, tokens: &mut TokenStream) { - let case_insensitive = self.case_insensitive; + let case = &self.case; let t = match self.segments.as_slice() { [] => unreachable!("0-length matcher should never be created"), [MatcherSegment::Text(text)] => { - quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case_insensitive))) } + quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case))) } } // parser logic ensures that this case can only happen when there are dynamic segments segs @ [_, ..] => { - quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case_insensitive)) } + quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case)) } } }; tokens.append_all(t); @@ -189,10 +239,6 @@ impl MatcherSegment { struct MappingDefModel { #[serde(default)] mappings: IndexMap>, - /// Case-sensitive mappings. Unlike `mappings`, these glob patterns are - /// matched case-sensitively. - #[serde(default)] - case_sensitive_mappings: IndexMap>, } impl MappingDefModel { fn into_mapping_list(self) -> MappingList { @@ -205,19 +251,6 @@ impl MappingDefModel { .map(|matcher| (matcher, target.clone())) .collect::>() }) - .chain( - self.case_sensitive_mappings - .into_iter() - .flat_map(|(target, matchers)| { - matchers - .into_iter() - .map(|mut matcher| { - matcher.case_insensitive = false; - (matcher, target.clone()) - }) - .collect::>() - }), - ) .collect(); MappingList(list) } diff --git a/src/syntax_mapping.rs b/src/syntax_mapping.rs index 8fc4008a..584a5cb6 100644 --- a/src/syntax_mapping.rs +++ b/src/syntax_mapping.rs @@ -17,9 +17,16 @@ use ignored_suffixes::IgnoredSuffixes; mod builtin; pub mod ignored_suffixes; -fn make_glob_matcher(from: &str, case_insensitive: bool) -> Result { +/// Whether a glob pattern should be matched case-sensitively or case-insensitively. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Case { + Sensitive, + Insensitive, +} + +fn make_glob_matcher(from: &str, case: Case) -> Result { let matcher = GlobBuilder::new(from) - .case_insensitive(case_insensitive) + .case_insensitive(matches!(case, Case::Insensitive)) .literal_separator(true) .build()? .compile_matcher(); @@ -97,14 +104,14 @@ impl<'a> SyntaxMapping<'a> { } pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { - let matcher = make_glob_matcher(from, true)?; + let matcher = make_glob_matcher(from, Case::Insensitive)?; self.custom_mappings.push((matcher, to)); Ok(()) } /// Like [`Self::insert`], but the glob pattern is matched case-sensitively. pub fn insert_case_sensitive(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { - let matcher = make_glob_matcher(from, false)?; + let matcher = make_glob_matcher(from, Case::Sensitive)?; self.custom_mappings.push((matcher, to)); Ok(()) } diff --git a/src/syntax_mapping/builtin.rs b/src/syntax_mapping/builtin.rs index 52f1d47b..41007be5 100644 --- a/src/syntax_mapping/builtin.rs +++ b/src/syntax_mapping/builtin.rs @@ -3,7 +3,7 @@ use std::env; use globset::GlobMatcher; use once_cell::sync::Lazy; -use crate::syntax_mapping::{make_glob_matcher, MappingTarget}; +use crate::syntax_mapping::{make_glob_matcher, Case, MappingTarget}; // Static syntax mappings generated from /src/syntax_mapping/builtins/ by the // build script (/build/syntax_mapping.rs). @@ -53,8 +53,8 @@ include!(concat!( /// A failure to compile is a fatal error. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_fixed(from: &str, case_insensitive: bool) -> GlobMatcher { - make_glob_matcher(from, case_insensitive) +fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher { + make_glob_matcher(from, case) .expect("A builtin fixed glob matcher failed to compile") } @@ -65,7 +65,7 @@ fn build_matcher_fixed(from: &str, case_insensitive: bool) -> GlobMatcher { /// to compile. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_dynamic(segs: &[MatcherSegment], case_insensitive: bool) -> Option { +fn build_matcher_dynamic(segs: &[MatcherSegment], case: Case) -> Option { // join segments let mut buf = String::new(); for seg in segs { @@ -78,7 +78,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment], case_insensitive: bool) -> Opt } } // compile glob matcher - let matcher = make_glob_matcher(&buf, case_insensitive).ok()?; + let matcher = make_glob_matcher(&buf, case).ok()?; Some(matcher) } diff --git a/src/syntax_mapping/builtins/common/50-bazel.toml b/src/syntax_mapping/builtins/common/50-bazel.toml index 2b0e17e2..2ced1399 100644 --- a/src/syntax_mapping/builtins/common/50-bazel.toml +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -1,2 +1,2 @@ -[case_sensitive_mappings] -"Python" = ["BUILD"] +[mappings] +"Python" = [{ glob = "BUILD", case_sensitive = true }] From c6e661d80bc9654583771f2ccd00aa1bf4a3e28a Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Wed, 18 Mar 2026 22:25:07 +0200 Subject: [PATCH 16/67] cargo fmt --- build/syntax_mapping.rs | 8 +++++--- src/syntax_mapping/builtin.rs | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 17722f67..11d778e3 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -165,9 +165,11 @@ impl<'de> serde::Deserialize<'de> for Matcher { let raw = RawMatcher::deserialize(deserializer)?; match raw { RawMatcher::Simple(s) => Matcher::from_str(&s).map_err(serde::de::Error::custom), - RawMatcher::Full { glob, case_sensitive } => { - let mut matcher = - Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; + RawMatcher::Full { + glob, + case_sensitive, + } => { + let mut matcher = Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; matcher.case = if case_sensitive { Case::Sensitive } else { diff --git a/src/syntax_mapping/builtin.rs b/src/syntax_mapping/builtin.rs index 41007be5..79d298c3 100644 --- a/src/syntax_mapping/builtin.rs +++ b/src/syntax_mapping/builtin.rs @@ -54,8 +54,7 @@ include!(concat!( /// /// Used internally by `Lazy>`'s lazy evaluation closure. fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher { - make_glob_matcher(from, case) - .expect("A builtin fixed glob matcher failed to compile") + make_glob_matcher(from, case).expect("A builtin fixed glob matcher failed to compile") } /// Join a list of matcher segments to create a glob string, replacing all From 9fccdbc48420b1bb9de852c275969e838cfbc6ef Mon Sep 17 00:00:00 2001 From: XploitMonk0x01 Date: Sat, 14 Mar 2026 12:11:41 +0530 Subject: [PATCH 17/67] docs: clarify Ubuntu and Debian executable name --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f0d4efb4..d5a68440 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ bat main.cpp | xclip export MANPAGER="bat -plman" man 2 select ``` -(replace `bat` with `batcat` if you are on Debian or Ubuntu) +(on some older Debian or Ubuntu releases, the executable is named `batcat` instead of `bat`) If you prefer to have this bundled in a new command, you can also use [`batman`](https://github.com/eth-p/bat-extras/blob/master/doc/batman.md). @@ -276,8 +276,8 @@ If your Ubuntu/Debian installation is new enough you can simply run: sudo apt install bat ``` -**Important**: If you install `bat` this way, please note that the executable may be installed as `batcat` instead of `bat` (due to [a name -clash with another package](https://github.com/sharkdp/bat/issues/982)). You can set up a `bat -> batcat` symlink or alias to prevent any issues that may come up because of this and to be consistent with other distributions: +**Important**: On some older Ubuntu/Debian releases, the executable is installed as `batcat` instead of `bat` (due to [a name +clash with another package](https://github.com/sharkdp/bat/issues/982)). On newer releases, the executable is available as `bat`. If `bat --version` does not work after installation, try `batcat --version` instead. You can set up a `bat -> batcat` symlink or alias to prevent any issues that may come up because of this and to be consistent with other distributions: ``` bash mkdir -p ~/.local/bin ln -s /usr/bin/batcat ~/.local/bin/bat From 618d7340bb63fa6168615585075c6debb7240a82 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:34:12 -0700 Subject: [PATCH 18/67] fix: consistent .deb MUSL package names Fix two inconsistencies in MUSL .deb package naming: 1. Change dpkg_arch for aarch64-unknown-linux-musl from `arm64` to `musl-linux-arm64` to match the convention used by other MUSL targets. 2. Change the DPKG_BASENAME case pattern from `*-musl)` to `*-musl*)` so it also matches `musleabihf`, giving the arm target the `bat-musl` prefix like all other MUSL packages. After this fix, all MUSL .deb packages follow the consistent pattern: `bat-musl_VERSION_musl-linux-ARCH.deb` Closes #3482 --- .github/workflows/CICD.yml | 4 ++-- CHANGELOG.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f1bc3887..e1b82424 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -161,7 +161,7 @@ jobs: fail-fast: false matrix: job: - - { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: arm64, use-cross: true } + - { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-arm64, use-cross: true } - { target: aarch64-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: arm64, use-cross: true } - { target: arm-unknown-linux-gnueabihf , os: ubuntu-latest , dpkg_arch: armhf, use-cross: true } - { target: arm-unknown-linux-musleabihf, os: ubuntu-latest , dpkg_arch: musl-linux-armhf, use-cross: true } @@ -335,7 +335,7 @@ jobs: DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }} DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl - case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac; + case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac; DPKG_VERSION=${{ needs.crate_metadata.outputs.version }} DPKG_ARCH="${{ matrix.job.dpkg_arch }}" DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de41716..ea1475bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) ## Bugfixes +- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix), see #3482 (@mvanhorn) - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) - Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir) - Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) From 2a3ed948ecc1f98dc7347ddf170ffcd384139ca1 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:35:48 -0700 Subject: [PATCH 19/67] feat: preserve change markers when combining --diff with --plain When --plain is set (via CLI or config file), --diff output loses all visual markers, making it indistinguishable from plain text. The diff line filtering still works, but without Changes markers or Snip separators the output is not useful. Automatically include Changes and Snip style components when --diff is active alongside --plain. This preserves the --plain intent (no grid, no header, no line numbers) while keeping diff output readable. Closes #3630 --- CHANGELOG.md | 1 + src/bin/bat/app.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de41716..a9508f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Features +- Preserve `--diff` change markers and snip separators when `--plain` is set, see #3630 (@mvanhorn) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) - Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index dddb5559..a580ff02 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -590,7 +590,16 @@ impl App { // Plain if `--plain` is specified at least once. if self.matches.get_count("plain") > 0 { - return Some(StyleComponents(HashSet::from([StyleComponent::Plain]))); + let mut components = HashSet::from([StyleComponent::Plain]); + // When --diff is active, preserve change markers and snip separators + // so that diff output remains visually useful. + if self.matches.try_contains_id("diff").unwrap_or_default() + && self.matches.get_flag("diff") + { + components.insert(StyleComponent::Changes); + components.insert(StyleComponent::Snip); + } + return Some(StyleComponents(components)); } // Default behavior. From 99c8e15c27c7fc03c9b50aa494f7d0db14885040 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:37:45 -0700 Subject: [PATCH 20/67] fix: update changelog entry with PR number for CI check --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1475bb..c3c893b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) ## Bugfixes -- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix), see #3482 (@mvanhorn) +- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) - Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir) - Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) From 5e140558b13f6b87e1dc74ac67cabeb48fc773e4 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:38:03 -0700 Subject: [PATCH 21/67] fix: update changelog entry with PR number for CI check --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9508f39..457b5cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ## Features -- Preserve `--diff` change markers and snip separators when `--plain` is set, see #3630 (@mvanhorn) +- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) - Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) From 169dc7c45b0c5aacd386c46eb40c9e8abf6cad8b Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:33:33 -0700 Subject: [PATCH 22/67] test: add integration tests for --diff combined with --plain --- tests/integration_tests.rs | 146 +++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index cfbad253..9a8a7f41 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3965,3 +3965,149 @@ fn word_wrap_short_line_no_wrap() { .success() .stdout("Single Line\n"); } + +#[cfg(unix)] +#[cfg(feature = "git")] +fn setup_diff_test_repo() -> tempfile::TempDir { + use std::process::Command; + + let dir = tempfile::tempdir().expect("can create temporary directory"); + let repo = dir.path(); + + // Initialize a git repo and commit a file + Command::new("git") + .args(["init"]) + .current_dir(repo) + .output() + .expect("git init"); + + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(repo) + .output() + .expect("git config email"); + + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(repo) + .output() + .expect("git config name"); + + std::fs::write(repo.join("test.txt"), "line 1\nline 2\nline 3\n") + .expect("can write test file"); + + Command::new("git") + .args(["add", "test.txt"]) + .current_dir(repo) + .output() + .expect("git add"); + + Command::new("git") + .args(["commit", "-m", "initial"]) + .current_dir(repo) + .output() + .expect("git commit"); + + // Modify the file so --diff has something to show + std::fs::write(repo.join("test.txt"), "line 1\nline 2 modified\nline 3\nline 4 added\n") + .expect("can write modified test file"); + + dir +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn diff_plain_preserves_change_markers() { + let repo = setup_diff_test_repo(); + + // With --diff --plain, output should contain the change marker column + // but not other decorations like line numbers or grid + let output = bat() + .current_dir(repo.path()) + .arg("--diff") + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("test.txt") + .assert() + .success() + .get_output() + .stdout + .clone(); + + let stdout = std::str::from_utf8(&output).expect("valid utf-8"); + + // The output should contain the modified and added lines + assert!( + stdout.contains("line 2 modified"), + "diff plain output should contain modified line, got: {stdout}" + ); + assert!( + stdout.contains("line 4 added"), + "diff plain output should contain added line, got: {stdout}" + ); + + // Should NOT contain line numbers (a decoration that --plain disables) + assert!( + !stdout.contains(" 1"), + "diff plain output should not contain line numbers, got: {stdout}" + ); +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn diff_plain_does_not_show_grid_or_header() { + let repo = setup_diff_test_repo(); + + let output = bat() + .current_dir(repo.path()) + .arg("--diff") + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("--terminal-width=80") + .arg("test.txt") + .assert() + .success() + .get_output() + .stdout + .clone(); + + let stdout = std::str::from_utf8(&output).expect("valid utf-8"); + + // Grid lines use box-drawing characters + assert!( + !stdout.contains('─'), + "diff plain output should not contain grid lines, got: {stdout}" + ); + assert!( + !stdout.contains('│'), + "diff plain output should not contain grid separators, got: {stdout}" + ); + + // Header shows "File: " + assert!( + !stdout.contains("File:"), + "diff plain output should not contain file header, got: {stdout}" + ); +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn plain_without_diff_still_works() { + let repo = setup_diff_test_repo(); + + // --plain without --diff should output file content with no decorations at all + bat() + .current_dir(repo.path()) + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("test.txt") + .assert() + .success() + .stdout("line 1\nline 2 modified\nline 3\nline 4 added\n"); +} From fc94a0ec49dcea3d5c022d38299f82b1f894f844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Thu, 19 Mar 2026 13:26:14 +0300 Subject: [PATCH 23/67] fix: account for caret notation width in text wrapping Control characters displayed in caret notation (e.g. ^@ for NUL) occupy 2 terminal columns, but the width calculation treated them as 0-width. Add a char_width() helper that returns 2 for control characters, fixing incorrect line wrapping with --binary=as-text. Fixes #3631 --- CHANGELOG.md | 1 + src/printer.rs | 18 ++++++++++++----- .../examples/regression_tests/issue_3631.txt | Bin 0 -> 24 bytes tests/integration_tests.rs | 19 ++++++++++++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/examples/regression_tests/issue_3631.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de41716..34bd719e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) ## Bugfixes +- Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3631 (@eyupcanakman) - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) - Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir) - Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) diff --git a/src/printer.rs b/src/printer.rs index 6a57fb62..c58c914d 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -37,6 +37,16 @@ use crate::wrapping::WrappingMode; use crate::BinaryBehavior; use crate::StripAnsiMode; +// Return the displayed width of a character. +// +// Control characters (0x00..=0x1F and 0x7F) are rendered by the terminal +// in caret notation (e.g. ^@, ^A, ..., ^?), which occupies two columns. +// UnicodeWidthChar::width() returns None for these, so we map them to 2 +// here instead of the previous default of 0. +fn char_width(c: char) -> usize { + c.width().unwrap_or(if c.is_control() { 2 } else { 0 }) +} + const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI { raw_sequence: "\x1B[4m", parameters: "4", @@ -793,7 +803,7 @@ impl Printer for InteractivePrinter<'_> { for c in text.chars() { // calculate the displayed width for next character - let cw = c.width().unwrap_or(0); + let cw = char_width(c); current_width += cw; // Track whitespace positions for word wrapping. @@ -868,10 +878,8 @@ impl Printer for InteractivePrinter<'_> { if let Some(rs) = rest_start { // Word wrap: carry remainder to next line. let remainder = line_buf[rs..].to_string(); - let rem_width: usize = remainder - .chars() - .map(|ch| ch.width().unwrap_or(0)) - .sum(); + let rem_width: usize = + remainder.chars().map(char_width).sum(); line_buf.clear(); line_buf.push_str(&remainder); current_width = rem_width + cw; diff --git a/tests/examples/regression_tests/issue_3631.txt b/tests/examples/regression_tests/issue_3631.txt new file mode 100644 index 0000000000000000000000000000000000000000..8449691fdeab44ee83cf8b8059c2aff05c2e76b5 GIT binary patch literal 24 OcmZQzzyz*-E?fWtr~u*s literal 0 HcmV?d00001 diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index cfbad253..c6ea83e9 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2681,6 +2681,25 @@ fn binary_as_text() { .stderr(""); } +#[test] +fn binary_as_text_control_char_width() { + // Control characters are displayed as caret notation (e.g. ^@) by the + // terminal, occupying 2 columns each. With 20 NUL bytes (40 columns) + + // "END" (3 columns) = 43 columns, wrapping at terminal width 40 must + // produce 2 lines, not 1. See #3631. + bat() + .arg("--binary=as-text") + .arg("--wrap=character") + .arg("--terminal-width=40") + .arg("--decorations=always") + .arg("--style=plain") + .arg("--color=never") + .arg("regression_tests/issue_3631.txt") + .assert() + .success() + .stdout(predicate::function(|s: &str| s.lines().count() == 2)); +} + #[test] fn no_strip_overstrike_for_plain_text() { // Overstrike is preserved for plain text files (no syntax highlighting) From 1f540752ef21f64aafa46007bb2bf7f328ad5230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Thu, 19 Mar 2026 19:24:26 +0300 Subject: [PATCH 24/67] fix: reference PR number in CHANGELOG entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34bd719e..180f9ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) ## Bugfixes -- Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3631 (@eyupcanakman) +- Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) - Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir) - Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) From 3767f15c2abf07ef1edfa499fa27cfff03977393 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Fri, 20 Mar 2026 23:11:23 +0200 Subject: [PATCH 25/67] improvements from PR review fix documentation about syntax mappings --- src/syntax_mapping/builtins/README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/syntax_mapping/builtins/README.md b/src/syntax_mapping/builtins/README.md index 7152fd33..220e45df 100644 --- a/src/syntax_mapping/builtins/README.md +++ b/src/syntax_mapping/builtins/README.md @@ -20,12 +20,10 @@ syntax mappings defined by all TOML files, and embed them into the binary. ## File syntax -Each TOML file should contain a single section named `mappings` and/or a single -section named `case_sensitive_mappings`, with each of its keys being a language +Each TOML file should contain a single section named `mappings`, with each of its keys being a language identifier (first column of `bat -L`; also referred to as "target"). -The value of each key should be an array of strings, with each item being a glob -matcher. We will call each of these items a "rule". +The value of each key should be an array of "rules". The rules are expected to be objects with a `glob` string and a `case_sensitive` boolean. For simplification, a rule can be just a glob string, which is shorthand for the default case insensitive mode. For example, if `foo-application` uses both TOML and YAML configuration files, we could write something like this: @@ -98,6 +96,15 @@ like this: ] ``` +### Case sensitivity + +By default, all glob patterns are matched case-insensitively. To match a pattern case-sensitively, use the object form of the rule with the `case_sensitive` option: + +```toml +[mappings] +"Python" = [{ glob = "BUILD", case_sensitive = true }] +``` + ## Ordering At compile time, all TOML files applicable to the target are processed in From 7a6f442c8666bcda1649786ee48256f09b21363e Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Fri, 20 Mar 2026 23:35:27 +0200 Subject: [PATCH 26/67] improvements from PR review - prefer to use `Default::default` because it's semantically clearer why we've chosen to use a particular value --- build/syntax_mapping.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 11d778e3..bc4601fc 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -50,9 +50,10 @@ impl ToTokens for MappingTarget { /// Whether a glob pattern should be matched case-sensitively or case-insensitively. /// /// Mirrors the runtime `Case` type in `src/syntax_mapping.rs`. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] enum Case { Sensitive, + #[default] Insensitive, } impl ToTokens for Case { @@ -65,7 +66,7 @@ impl ToTokens for Case { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] /// A single matcher. /// /// Codegen converts this into a `Lazy>`. From a19593b383f208106d019cbca3546cb0748eb377 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Fri, 20 Mar 2026 23:53:30 +0200 Subject: [PATCH 27/67] improvements from PR review 1. Added `Deserialize` derive and `#[serde(try_from = "RawMatcher")]` to the `Matcher` struct. With `try_from`, serde generates a `Deserialize` impl that first deserializes into `RawMatcher`, then calls `TryFrom for Matcher`. It never attempts to deserialize `Matcher`'s fields directly, so `Case` and `MatcherSegment` don't need `Deserialize` impls. 2. Replaced the manual `impl<'de> serde::Deserialize<'de> for Matcher` with a standard `impl TryFrom for Matcher`. The logic is identical - the conversion is fallible because `Matcher::from_str` returns `Result<_, anyhow::Error>`, so `try_from` (not `from`) is the correct choice, avoiding any panics. The net effect: same behavior, same error handling, but using the idiomatic serde pattern instead of a manual deserializer impl. The `RawMatcher` intermediate type and its `#[serde(untagged)]` derive remain unchanged. --- build/syntax_mapping.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index bc4601fc..c7133be9 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -66,7 +66,8 @@ impl ToTokens for Case { } } -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Deserialize)] +#[serde(try_from = "RawMatcher")] /// A single matcher. /// /// Codegen converts this into a `Lazy>`. @@ -161,16 +162,17 @@ enum RawMatcher { }, } -impl<'de> serde::Deserialize<'de> for Matcher { - fn deserialize>(deserializer: D) -> Result { - let raw = RawMatcher::deserialize(deserializer)?; +impl TryFrom for Matcher { + type Error = anyhow::Error; + + fn try_from(raw: RawMatcher) -> Result { match raw { - RawMatcher::Simple(s) => Matcher::from_str(&s).map_err(serde::de::Error::custom), + RawMatcher::Simple(s) => Matcher::from_str(&s), RawMatcher::Full { glob, case_sensitive, } => { - let mut matcher = Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; + let mut matcher = Matcher::from_str(&glob)?; matcher.case = if case_sensitive { Case::Sensitive } else { From e60875ac125f6698775679fa9a8beb495299b4e5 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:27:10 -0700 Subject: [PATCH 28/67] fix: gate Changes component on git feature flag When bat is built without the `git` feature, `StyleComponent::Changes` is not available. Add `#[cfg(feature = "git")]` guard so `--diff --plain` works in both configurations. Co-Authored-By: Claude Opus 4.6 --- src/bin/bat/app.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index a580ff02..f95bbb91 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -596,6 +596,7 @@ impl App { if self.matches.try_contains_id("diff").unwrap_or_default() && self.matches.get_flag("diff") { + #[cfg(feature = "git")] components.insert(StyleComponent::Changes); components.insert(StyleComponent::Snip); } From e86797fbf4acf3685f96539e9d3118f86186217d Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:46:54 -0700 Subject: [PATCH 29/67] style: auto-format integration tests Co-Authored-By: Claude Opus 4.6 --- tests/integration_tests.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 9a8a7f41..b0165126 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3993,8 +3993,7 @@ fn setup_diff_test_repo() -> tempfile::TempDir { .output() .expect("git config name"); - std::fs::write(repo.join("test.txt"), "line 1\nline 2\nline 3\n") - .expect("can write test file"); + std::fs::write(repo.join("test.txt"), "line 1\nline 2\nline 3\n").expect("can write test file"); Command::new("git") .args(["add", "test.txt"]) @@ -4009,8 +4008,11 @@ fn setup_diff_test_repo() -> tempfile::TempDir { .expect("git commit"); // Modify the file so --diff has something to show - std::fs::write(repo.join("test.txt"), "line 1\nline 2 modified\nline 3\nline 4 added\n") - .expect("can write modified test file"); + std::fs::write( + repo.join("test.txt"), + "line 1\nline 2 modified\nline 3\nline 4 added\n", + ) + .expect("can write modified test file"); dir } From 0b4c886efc31d0adf8a99303006620f289146375 Mon Sep 17 00:00:00 2001 From: Sungjoon Moon Date: Thu, 25 Dec 2025 20:19:30 +0000 Subject: [PATCH 30/67] ci: Use git version of cross for better target support --- .github/workflows/CICD.yml | 4 +--- CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index e1b82424..4c3db0c8 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -195,9 +195,7 @@ jobs: - name: Install cross if: matrix.job.use-cross - uses: taiki-e/install-action@v2 - with: - tool: cross + run: cargo install cross --git https://github.com/cross-rs/cross --rev 588b3c99db52b5a9c5906fab96cfadcf1bde7863 - name: Overwrite build command env variable if: matrix.job.use-cross diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2c2f84..08b116ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) ## Other +- Use git version of cross. See #3533 (@OctopusET) - Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) From ca3ef28d56a2b010f2ddf8fa01be64aec905ab4f Mon Sep 17 00:00:00 2001 From: Sim-hu Date: Tue, 24 Mar 2026 19:20:18 +0900 Subject: [PATCH 31/67] fix: use correct Debian architecture name for i686 .deb package The i686 .deb package declared its architecture as "i686", which Debian does not recognize. Debian uses "i386" for 32-bit x86, matching the convention already used by the other targets (arm64, armhf, amd64). Closes #3611 --- .github/workflows/CICD.yml | 2 +- CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 4c3db0c8..f881cada 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -166,7 +166,7 @@ jobs: - { target: arm-unknown-linux-gnueabihf , os: ubuntu-latest , dpkg_arch: armhf, use-cross: true } - { target: arm-unknown-linux-musleabihf, os: ubuntu-latest , dpkg_arch: musl-linux-armhf, use-cross: true } - { target: i686-pc-windows-msvc , os: windows-2025 , } - - { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i686, use-cross: true } + - { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i386, use-cross: true } - { target: i686-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-i686, use-cross: true } - { target: x86_64-apple-darwin , os: macos-15-intel, } - { target: aarch64-apple-darwin , os: macos-latest , } diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b116ff..b9611731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) ## Bugfixes +- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611 - Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) - Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) From b8d462ba87f4ec4c5e3d6862f2dfd5865b97b481 Mon Sep 17 00:00:00 2001 From: Sim-hu Date: Tue, 24 Mar 2026 19:24:36 +0900 Subject: [PATCH 32/67] fix: add PR number and author to changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9611731..217d3baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) ## Bugfixes -- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611 +- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) - Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) - Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) From 87e043b91ce47f95829ad74ac06e04d0a163dccf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:46:46 +0000 Subject: [PATCH 33/67] Add Home/End key bindings to builtin minus pager Co-authored-by: keith-hall <11882719+keith-hall@users.noreply.github.com> --- src/output.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/output.rs b/src/output.rs index 0205f48e..8054be46 100644 --- a/src/output.rs +++ b/src/output.rs @@ -23,6 +23,18 @@ pub struct BuiltinPager { impl BuiltinPager { fn new() -> Self { let pager = minus::Pager::new(); + + let mut input_register = minus::input::HashedEventRegister::default(); + input_register.add_key_events(&["home"], |_, _| { + minus::input::InputEvent::UpdateUpperMark(0) + }); + input_register.add_key_events(&["end"], |_, _| { + minus::input::InputEvent::UpdateUpperMark(usize::MAX) + }); + pager + .set_input_classifier(Box::new(input_register)) + .expect("failed to set input classifier on newly created pager"); + let handle = { let pager = pager.clone(); Some(spawn(move || { From eef71074070adebb33cc3144db0bbf154c669927 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Tue, 24 Mar 2026 21:38:07 +0200 Subject: [PATCH 34/67] update changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b116ff..52d732f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,5 @@ # unreleased -- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) -- Fixed syntax tests path, see #3610 (@foxfromworld) ## Features @@ -27,11 +25,13 @@ - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) +- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) +- Fixed syntax tests path, see #3610 (@foxfromworld) ## Other - Use git version of cross. See #3533 (@OctopusET) - - Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) +- Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) ## Syntaxes From 3e789f5241dfedc789de7a8b7e2bd704858514d2 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:49:50 +0800 Subject: [PATCH 35/67] Imprv: cleanup matcher glob parsing logic - Data flow is now strictly linear from `RawMatcher` to `Matcher` - I've also hoisted `RawMatcher` in front of `Matcher` to signal this - Re-type `RawMatcher.case_sensitive` from `bool` to `Option` - This moves all parser logic away from `RawMatcher`, making it a more faithful representation of the data - Favour default consts in `Matcher::try_from` to `Default` impl on `Case` - Because the default choice of casing is a design decision of the logic, not an intrinsic property of the type --- build/syntax_mapping.rs | 153 ++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index c7133be9..b0db01eb 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -47,13 +47,24 @@ impl ToTokens for MappingTarget { } } +/// Helper type for deserializing a `Matcher` from either a plain string or a +/// `{ glob = "...", case_sensitive = true }` struct. +#[derive(Deserialize)] +#[serde(untagged)] +enum RawMatcher { + Simple(String), + Full { + glob: String, + case_sensitive: Option, + }, +} + /// Whether a glob pattern should be matched case-sensitively or case-insensitively. /// /// Mirrors the runtime `Case` type in `src/syntax_mapping.rs`. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] enum Case { Sensitive, - #[default] Insensitive, } impl ToTokens for Case { @@ -66,11 +77,11 @@ impl ToTokens for Case { } } -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Deserialize)] -#[serde(try_from = "RawMatcher")] /// A single matcher. /// /// Codegen converts this into a `Lazy>`. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[serde(try_from = "RawMatcher")] struct Matcher { segments: Vec, /// Whether the glob pattern should be matched case-sensitively. @@ -78,7 +89,7 @@ struct Matcher { /// Defaults to `Case::Insensitive` for backwards compatibility. case: Case, } -/// Parse a matcher. +/// Parse the glob pattern of a matcher. /// /// Note that this implementation is rather strict: it will greedily interpret /// every valid environment variable replacement as such, then immediately @@ -92,93 +103,79 @@ struct Matcher { /// /// Revision history: /// - 2024-02-20: allow `{` and `}` (glob brace expansion) -impl FromStr for Matcher { - type Err = anyhow::Error; - fn from_str(s: &str) -> Result { - use MatcherSegment as Seg; - static VAR_REGEX: Lazy = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap()); +fn parse_glob(s: &str) -> Result, anyhow::Error> { + use MatcherSegment as Seg; + static VAR_REGEX: Lazy = 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(); + 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(); + // 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:?}"); - } - - // guard empty case - if non_empty_segments.is_empty() { - bail!(r#"Parsed an empty matcher: "{s}""#); - } - - // guard variable syntax leftover fragments - if non_empty_segments - .iter() - .filter_map(Seg::text) - .any(|t| t.contains('$')) - { - bail!(r#"Invalid matcher: "{s}""#); - } - - Ok(Self { - segments: non_empty_segments, - case: Case::Insensitive, - }) + // this var + segments.push(Seg::Env(capture.get(1).unwrap().as_str().into())); } -} + // possible trailing text + segments.push(Seg::Text(s[text_start..].into())); -/// Helper type for deserializing a `Matcher` from either a plain string or a -/// `{ glob = "...", case_sensitive = true }` struct. -#[derive(Deserialize)] -#[serde(untagged)] -enum RawMatcher { - Simple(String), - Full { - glob: String, - #[serde(default)] - case_sensitive: bool, - }, -} + // 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:?}"); + } + + // guard empty case + if non_empty_segments.is_empty() { + bail!(r#"Parsed an empty matcher: "{s}""#); + } + + // guard variable syntax leftover fragments + if non_empty_segments + .iter() + .filter_map(Seg::text) + .any(|t| t.contains('$')) + { + bail!(r#"Invalid matcher: "{s}""#); + } + + Ok(non_empty_segments) +} impl TryFrom for Matcher { type Error = anyhow::Error; - fn try_from(raw: RawMatcher) -> Result { - match raw { - RawMatcher::Simple(s) => Matcher::from_str(&s), + const DEFAULT_CASE: Case = Case::Insensitive; + match &raw { + RawMatcher::Simple(s) => { + let segments = parse_glob(s)?; + Ok(Self { + segments, + case: DEFAULT_CASE, + }) + } RawMatcher::Full { glob, case_sensitive, } => { - let mut matcher = Matcher::from_str(&glob)?; - matcher.case = if case_sensitive { - Case::Sensitive - } else { - Case::Insensitive + let segments = parse_glob(glob)?; + let case = match case_sensitive { + None => DEFAULT_CASE, + Some(false) => Case::Insensitive, + Some(true) => Case::Sensitive, }; - Ok(matcher) + Ok(Self { segments, case }) } } } From b511b928f440ed1f5fcba305a47d93bd68f379a0 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:00:46 +0800 Subject: [PATCH 36/67] Write changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd7770b..251e699d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Use git version of cross. See #3533 (@OctopusET) - Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) - Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) +- Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon) ## Syntaxes From 89f3d2d31afe5487c89a7d14b48717b56758f243 Mon Sep 17 00:00:00 2001 From: Claw Explorer Date: Thu, 26 Mar 2026 12:18:52 -0400 Subject: [PATCH 37/67] docs: add instructions for removing fish help abbreviations Add documentation showing how to erase the fish abbreviations for --help and -h, since the dash-prefixed names make removal non-obvious. The key is using -- before the abbreviation name to prevent fish from interpreting it as a flag. Closes #3536 --- CHANGELOG.md | 4 ++++ README.md | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 251e699d..cd21680c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # unreleased +## Other + +- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536 + ## Features - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) diff --git a/README.md b/README.md index d5a68440..140c3022 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,15 @@ abbr -a --position anywhere -- --help '--help | bat -plhelp' abbr -a --position anywhere -- -h '-h | bat -plhelp' ``` +To remove these abbreviations later: + +```fish +abbr -e -- --help +abbr -e -- -h +``` + +The `--` before the abbreviation name is required because `--help` and `-h` start with dashes, which would otherwise be interpreted as flags to `abbr` itself. + This way, you can keep on using `cp --help`, but get colorized help pages. Be aware that in some cases, `-h` may not be a shorthand of `--help` (for example with `ls`). In cases where you need to use `-h` From 652489251b5e8eeefea0c339adf293e5f8341db7 Mon Sep 17 00:00:00 2001 From: Claw Explorer Date: Sat, 28 Mar 2026 16:34:28 -0400 Subject: [PATCH 38/67] docs: move removal instructions into a TIP callout Address review feedback from @keith-hall: wrap the abbreviation removal instructions in a GitHub callout (> [!TIP]) to visually separate them from the main setup flow. Also reorder so the 'This way, you can keep on using...' sentence follows directly after the abbreviation creation, improving readability. --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 140c3022..d6ef3381 100644 --- a/README.md +++ b/README.md @@ -244,17 +244,16 @@ abbr -a --position anywhere -- --help '--help | bat -plhelp' abbr -a --position anywhere -- -h '-h | bat -plhelp' ``` -To remove these abbreviations later: - -```fish -abbr -e -- --help -abbr -e -- -h -``` - -The `--` before the abbreviation name is required because `--help` and `-h` start with dashes, which would otherwise be interpreted as flags to `abbr` itself. - This way, you can keep on using `cp --help`, but get colorized help pages. +> [!TIP] +> To remove these abbreviations later, run: +> ```fish +> abbr -e -- --help +> abbr -e -- -h +> ``` +> The `--` before the abbreviation name is required because `--help` and `-h` start with dashes, which would otherwise be interpreted as flags to `abbr` itself. + Be aware that in some cases, `-h` may not be a shorthand of `--help` (for example with `ls`). In cases where you need to use `-h` as a command argument you can prepend `\` to the argument (eg. `ls \-h`) to escape the aliasing defined above. From abab6533e4d1ff26c1a4f706b8f54235683bd997 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Tue, 31 Mar 2026 22:22:31 +0300 Subject: [PATCH 39/67] Update --diagnostic output with BAT_THEME_LIGHT/DARK env vars --- src/bin/bat/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 7ca08b9d..5a771cd6 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -307,6 +307,8 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) { "BAT_STYLE", "BAT_TABS", "BAT_THEME", + bat::theme::env::BAT_THEME_DARK, + bat::theme::env::BAT_THEME_LIGHT, "COLORTERM", "LANG", "LC_ALL", From ba223289ce7325ddb0c95d22b4bf74e2fb7ee3e5 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Tue, 31 Mar 2026 22:23:10 +0300 Subject: [PATCH 40/67] Update --diagnostic output with detected terminal color scheme type --- src/bin/bat/main.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 5a771cd6..3cdf0531 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -285,7 +285,29 @@ fn run_controller(inputs: Vec, config: &Config, cache_dir: &Path) -> Resu #[cfg(feature = "bugreport")] fn invoke_bugreport(app: &App, cache_dir: &Path) { - use bugreport::{bugreport, collector::*, format::Markdown}; + use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry}; + + struct ColorSchemeCollector; + + impl Collector for ColorSchemeCollector { + fn description(&self) -> &str { + "Detected terminal color scheme" + } + + fn collect( + &mut self, + _: &bugreport::CrateInfo, + ) -> std::result::Result { + let color_scheme = + bat::theme::color_scheme(bat::theme::DetectColorScheme::Always); + let text = match color_scheme { + Some(bat::theme::ColorScheme::Dark) => "dark", + Some(bat::theme::ColorScheme::Light) => "light", + None => "not detected", + }; + Ok(ReportEntry::Text(text.to_string())) + } + } let pager = bat::config::get_pager_executable( app.matches.get_one::("pager").map(|s| s.as_str()), ) @@ -328,6 +350,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) { custom_assets_metadata, )) .info(DirectoryEntries::new("Custom assets", cache_dir)) + .info(ColorSchemeCollector) .info(CompileTimeInformation::default()); #[cfg(feature = "paging")] From 3e4169b8c3cc3b5d5824ff821279c5ce6872467a Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Tue, 31 Mar 2026 22:35:21 +0300 Subject: [PATCH 41/67] cargo fmt --- src/bin/bat/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 3cdf0531..a4a2c176 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -298,8 +298,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) { &mut self, _: &bugreport::CrateInfo, ) -> std::result::Result { - let color_scheme = - bat::theme::color_scheme(bat::theme::DetectColorScheme::Always); + let color_scheme = bat::theme::color_scheme(bat::theme::DetectColorScheme::Always); let text = match color_scheme { Some(bat::theme::ColorScheme::Dark) => "dark", Some(bat::theme::ColorScheme::Light) => "light", From 31b0a357190eec0b9df23b5f6ee2115402ab3f78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:03:59 +0000 Subject: [PATCH 42/67] build(deps): bump unicode-segmentation from 1.12.0 to 1.13.2 Bumps [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) from 1.12.0 to 1.13.2. - [Commits](https://github.com/unicode-rs/unicode-segmentation/compare/v1.12.0...v1.13.2) --- updated-dependencies: - dependency-name: unicode-segmentation dependency-version: 1.13.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e777a2e..baedc438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1808,9 +1808,9 @@ checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" diff --git a/Cargo.toml b/Cargo.toml index 6c783336..4a8ecdc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ bytesize = { version = "2.3.1" } encoding_rs = "0.8.35" execute = { version = "0.2.15", optional = true } terminal-colorsaurus = "1.0" -unicode-segmentation = "1.12.0" +unicode-segmentation = "1.13.2" itertools = "0.14.0" [dependencies.git2] From f820ad1050654b36d1244f06c534dc9f73665087 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:41:02 +0000 Subject: [PATCH 43/67] build(deps): bump unicode-width from 0.2.1 to 0.2.2 Bumps [unicode-width](https://github.com/unicode-rs/unicode-width) from 0.2.1 to 0.2.2. - [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.2.1...v0.2.2) --- updated-dependencies: - dependency-name: unicode-width dependency-version: 0.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index baedc438..db7f3f36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1814,9 +1814,9 @@ checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unsafe-libyaml" diff --git a/Cargo.toml b/Cargo.toml index 4a8ecdc9..99a97871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ minus = { version = "5.6", optional = true, features = [ "dynamic_output", "search", ] } -unicode-width = "0.2.1" +unicode-width = "0.2.2" globset = "0.4" serde = "1.0" serde_derive = "1.0" From de9ca4ef7b5e4a9b949684c2e0cc6872ec41a8eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:13:41 +0000 Subject: [PATCH 44/67] build(deps): bump toml from 0.9.8 to 1.1.1+spec-1.1.0 Bumps [toml](https://github.com/toml-rs/toml) from 0.9.8 to 1.1.1+spec-1.1.0. - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.8...toml-v1.1.1) --- updated-dependencies: - dependency-name: toml dependency-version: 1.1.1+spec-1.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db7f3f36..f7c76d47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1406,9 +1406,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -1757,9 +1757,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "994b95d9e7bae62b34bab0e2a4510b801fa466066a6a8b2b57361fa1eba068ee" dependencies = [ "indexmap", "serde_core", @@ -1772,27 +1772,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "typenum" @@ -2186,9 +2186,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" [[package]] name = "wit-bindgen-rt" diff --git a/Cargo.toml b/Cargo.toml index 99a97871..68add205 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,7 +119,7 @@ serde = "1.0" serde_derive = "1.0" serde_with = { version = "3.17.0", default-features = false, features = ["macros"] } syn = { version = "2.0.104", features = ["full"] } -toml = { version = "0.9.8", features = ["preserve_order"] } +toml = { version = "1.1.1", features = ["preserve_order"] } walkdir = "2.5" [build-dependencies.clap] From 57e99e9bcdcd4d58946c687d8b7d3684d5072890 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:38:08 +0000 Subject: [PATCH 45/67] build(deps): bump tempfile from 3.23.0 to 3.27.0 Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.23.0 to 3.27.0. - [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md) - [Commits](https://github.com/Stebalien/tempfile/compare/v3.23.0...v3.27.0) --- updated-dependencies: - dependency-name: tempfile dependency-version: 3.27.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7c76d47..62f29a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -951,9 +951,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1324,14 +1324,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -1603,14 +1603,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.61.2", ] diff --git a/Cargo.toml b/Cargo.toml index 68add205..4d85a6e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ expect-test = "1.5.0" serial_test = { version = "2.0.0", default-features = false } predicates = "3.1.3" wait-timeout = "0.2.1" -tempfile = "3.23.0" +tempfile = "3.27.0" serde = { version = "1.0", features = ["derive"] } [target.'cfg(unix)'.dev-dependencies] From 956f0aeb08f8e4cefa59279e15eeb5be1ed41721 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:54:15 +0000 Subject: [PATCH 46/67] build(deps): bump quote from 1.0.40 to 1.0.45 Bumps [quote](https://github.com/dtolnay/quote) from 1.0.40 to 1.0.45. - [Release notes](https://github.com/dtolnay/quote/releases) - [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.45) --- updated-dependencies: - dependency-name: quote dependency-version: 1.0.45 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62f29a10..202a5295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1235,9 +1235,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] diff --git a/Cargo.toml b/Cargo.toml index 4d85a6e3..b63c4e83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,7 @@ itertools = "0.14.0" once_cell = "1.20" prettyplease = "0.2.37" proc-macro2 = "1.0.106" -quote = "1.0.40" +quote = "1.0.45" regex = "1.12.2" serde = "1.0" serde_derive = "1.0" From c64b6761feded76d519c91dda256e0779a4842b4 Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Tue, 7 Apr 2026 23:04:24 +0200 Subject: [PATCH 47/67] Add .NET slnx extension This is the new XML-based format for solution files. --- CHANGELOG.md | 1 + src/syntax_mapping/builtins/common/50-dotnet-xml.toml | 2 +- tests/syntax-tests/highlighted/XML/solution.slnx | 7 +++++++ tests/syntax-tests/source/XML/solution.slnx | 7 +++++++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/syntax-tests/highlighted/XML/solution.slnx create mode 100644 tests/syntax-tests/source/XML/solution.slnx diff --git a/CHANGELOG.md b/CHANGELOG.md index cd21680c..c67cf148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Other - Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536 +- Add .NET slnx extension, see #3682 (@ltrzesniewski) ## Features diff --git a/src/syntax_mapping/builtins/common/50-dotnet-xml.toml b/src/syntax_mapping/builtins/common/50-dotnet-xml.toml index 1e3a860a..70319f83 100644 --- a/src/syntax_mapping/builtins/common/50-dotnet-xml.toml +++ b/src/syntax_mapping/builtins/common/50-dotnet-xml.toml @@ -1,2 +1,2 @@ [mappings] -"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets"] +"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets", "*.slnx"] diff --git a/tests/syntax-tests/highlighted/XML/solution.slnx b/tests/syntax-tests/highlighted/XML/solution.slnx new file mode 100644 index 00000000..7946e72b --- /dev/null +++ b/tests/syntax-tests/highlighted/XML/solution.slnx @@ -0,0 +1,7 @@ +<Solution> + <Folder Name="/Build/"> + <File Path="Directory.Build.props" /> + <File Path="projectname.targets" /> +  + <Project Path="console.csproj" /> + diff --git a/tests/syntax-tests/source/XML/solution.slnx b/tests/syntax-tests/source/XML/solution.slnx new file mode 100644 index 00000000..e836bb51 --- /dev/null +++ b/tests/syntax-tests/source/XML/solution.slnx @@ -0,0 +1,7 @@ + + + + + + + From 2459aa94047279f9d7e6290010c6e8547b870962 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 10 Apr 2026 14:35:33 +0000 Subject: [PATCH 48/67] Detect ZIP archives as binary content --- CHANGELOG.md | 1 + src/input.rs | 41 +++++++++++++++++++++++++++++++++----- tests/integration_tests.rs | 18 +++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c67cf148..faf869a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) ## Bugfixes +- Treat ZIP archives as binary content based on their magic header, see #0000 (@officialasishkumar) - Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) - Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) - Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) diff --git a/src/input.rs b/src/input.rs index 29846abe..30f13f98 100644 --- a/src/input.rs +++ b/src/input.rs @@ -261,11 +261,7 @@ impl<'a> InputReader<'a> { let mut first_line = vec![]; reader.read_until(b'\n', &mut first_line).ok(); - let content_type = if first_line.is_empty() { - None - } else { - Some(content_inspector::inspect(&first_line[..])) - }; + let content_type = inspect_content_type(&first_line); if content_type == Some(ContentType::UTF_16LE) { read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok(); @@ -319,6 +315,25 @@ impl<'a> InputReader<'a> { } } +fn inspect_content_type(first_line: &[u8]) -> Option { + if first_line.is_empty() { + return None; + } + + let content_type = content_inspector::inspect(first_line); + if content_type == ContentType::UTF_8 && has_zip_signature(first_line) { + Some(ContentType::BINARY) + } else { + Some(content_type) + } +} + +fn has_zip_signature(bytes: &[u8]) -> bool { + [b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08"] + .into_iter() + .any(|signature| bytes.starts_with(signature)) +} + fn read_utf16_line( reader: &mut R, buf: &mut Vec, @@ -374,6 +389,22 @@ fn basic() { assert!(buffer.is_empty()); } +#[test] +fn zip_magic_headers_are_treated_as_binary() { + for content in [b"PK\x03\x04hello", b"PK\x05\x06hello", b"PK\x07\x08hello"] { + let reader = InputReader::new(&content[..]); + assert_eq!(Some(ContentType::BINARY), reader.content_type); + } +} + +#[test] +fn non_zip_pk_prefix_is_not_treated_as_binary() { + assert_eq!( + Some(ContentType::UTF_8), + inspect_content_type(b"PK\x03\x03hello") + ); +} + #[test] fn utf16le() { let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00"; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 96da50d7..d7e44301 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2093,6 +2093,24 @@ fn header_binary() { .stderr(""); } +#[test] +fn header_zip_file_is_binary() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("test.zip"); + std::fs::write(&tmp_path, b"PK\x03\x04hello").expect("can write temporary file"); + + bat() + .arg(&tmp_path) + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("--file-name=test.zip") + .assert() + .success() + .stdout("File: test.zip \n") + .stderr(""); +} + #[test] fn header_full_binary() { bat() From b3aec318cf1fc16edefe8a90d78e3eb2f62100f8 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 10 Apr 2026 14:37:00 +0000 Subject: [PATCH 49/67] Update changelog entry for #3686 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faf869a5..796aac21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) ## Bugfixes -- Treat ZIP archives as binary content based on their magic header, see #0000 (@officialasishkumar) +- Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar) - Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) - Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) - Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) From a9137fab05f8204804c5cfecf480f4c8f2d7ab82 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 10 Apr 2026 14:35:33 +0000 Subject: [PATCH 50/67] Support BAT_WIDTH as an alias for --terminal-width Signed-off-by: Asish Kumar --- CHANGELOG.md | 1 + doc/long-help.txt | 3 ++- src/bin/bat/clap_app.rs | 8 ++++-- src/bin/bat/config.rs | 1 + src/bin/bat/main.rs | 1 + tests/integration_tests.rs | 52 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 796aac21..0539669b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) +- Support configuring `--terminal-width` via `BAT_WIDTH`, see #0000 (@officialasishkumar) - Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) - Added an initial `flake.nix` for a ready made development environment; see #3578 (@vorburger) - Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX) diff --git a/doc/long-help.txt b/doc/long-help.txt index 82878acd..7d3cc0e5 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -77,7 +77,8 @@ Options: --terminal-width Explicitly set the width of the terminal instead of determining it automatically. If prefixed with '+' or '-', the value will be treated as an offset to the actual terminal - width. See also: '--wrap'. + width. This can also be configured via the BAT_WIDTH environment variable (e.g. export + BAT_WIDTH="100"). See also: '--wrap'. -n, --number Only show line numbers, no other decorations. This is an alias for '--style=numbers' diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 3636f081..6dfdf829 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -240,6 +240,7 @@ pub fn build_app(interactive_output: bool) -> Command { .arg( Arg::new("terminal-width") .long("terminal-width") + .overrides_with("terminal-width") .value_name("width") .hide_short_help(true) .allow_hyphen_values(true) @@ -255,10 +256,13 @@ pub fn build_app(interactive_output: bool) -> Command { }) .map_err(|e| e.to_string()) }) - .help( + .help("Explicitly set the width of the terminal instead of determining it automatically.") + .long_help( "Explicitly set the width of the terminal instead of determining it \ automatically. If prefixed with '+' or '-', the value will be treated \ - as an offset to the actual terminal width. See also: '--wrap'.", + as an offset to the actual terminal width. This can also be configured \ + via the BAT_WIDTH environment variable (e.g. export BAT_WIDTH=\"100\"). \ + See also: '--wrap'.", ), ) .arg( diff --git a/src/bin/bat/config.rs b/src/bin/bat/config.rs index 77d691c3..3e2fb72e 100644 --- a/src/bin/bat/config.rs +++ b/src/bin/bat/config.rs @@ -153,6 +153,7 @@ fn get_args_from_str(content: &str) -> Result, shell_words::ParseE pub fn get_args_from_env_vars() -> Vec { [ ("--tabs", "BAT_TABS"), + ("--terminal-width", "BAT_WIDTH"), ("--theme", bat::theme::env::BAT_THEME), ("--theme-dark", bat::theme::env::BAT_THEME_DARK), ("--theme-light", bat::theme::env::BAT_THEME_LIGHT), diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index a4a2c176..f3fa01f4 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -328,6 +328,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) { "BAT_STYLE", "BAT_TABS", "BAT_THEME", + "BAT_WIDTH", bat::theme::env::BAT_THEME_DARK, bat::theme::env::BAT_THEME_LIGHT, "COLORTERM", diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d7e44301..8ae4cfdd 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1047,6 +1047,57 @@ fn tabs_4_arg_overrides_env_noconfig() { ); } +#[test] +fn terminal_width_env_var_is_respected() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("long.txt"); + std::fs::write( + &tmp_path, + "0123456789abcdef0123456789abcdef0123456789abcdef\n", + ) + .expect("can write temporary file"); + + bat() + .env("BAT_WIDTH", "20") + .arg(&tmp_path) + .arg("--paging=never") + .arg("--color=never") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--wrap=character") + .assert() + .success() + .stdout(" 1 0123456789abcde\n f0123456789abcd\n ef0123456789abc\n def\n") + .stderr(""); +} + +#[test] +fn terminal_width_arg_overrides_env() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("long.txt"); + std::fs::write( + &tmp_path, + "0123456789abcdef0123456789abcdef0123456789abcdef\n", + ) + .expect("can write temporary file"); + + bat() + .env("BAT_WIDTH", "20") + .arg(&tmp_path) + .arg("--paging=never") + .arg("--color=never") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--wrap=character") + .arg("--terminal-width=10") + .assert() + .success() + .stdout( + " 1 01234\n 56789\n abcde\n f0123\n 45678\n 9abcd\n ef012\n 34567\n 89abc\n def\n", + ) + .stderr(""); +} + #[test] fn fail_non_existing() { bat().arg("non-existing-file").assert().failure(); @@ -1474,6 +1525,7 @@ fn diagnostic_sanity_check() { .assert() .success() .stdout(predicate::str::contains("BAT_PAGER=")) + .stdout(predicate::str::contains("BAT_WIDTH=")) .stderr(""); } From 01174b31f5276d5142fdcb4f74a8815ff6c4ea4f Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 10 Apr 2026 14:36:49 +0000 Subject: [PATCH 51/67] Update changelog entry for #3687 Signed-off-by: Asish Kumar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0539669b..ee313425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) -- Support configuring `--terminal-width` via `BAT_WIDTH`, see #0000 (@officialasishkumar) +- Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar) - Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) - Added an initial `flake.nix` for a ready made development environment; see #3578 (@vorburger) - Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX) From db647c98134e4a2aaeded2deee6d32f1dc7a9c27 Mon Sep 17 00:00:00 2001 From: orbisai0security Date: Sun, 12 Apr 2026 11:43:12 +0000 Subject: [PATCH 52/67] fix: V-001 security vulnerability Automated security fix generated by Orbis Security AI --- tests/snapshots/generate_snapshots.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/snapshots/generate_snapshots.py b/tests/snapshots/generate_snapshots.py index bb7d69ba..f8c14541 100755 --- a/tests/snapshots/generate_snapshots.py +++ b/tests/snapshots/generate_snapshots.py @@ -23,18 +23,19 @@ def generate_style_snapshot(style): def generate_snapshot(name, arguments): - command = "cargo run -- --paging=never --color=never --decorations=always " - command += "{args} sample.rs > output/{name}.snapshot.txt".format( - name=name, - args=arguments - ) + output_file = "output/{name}.snapshot.txt".format(name=name) + command = [ + "cargo", "run", "--", "--paging=never", "--color=never", + "--decorations=always", arguments, "sample.rs" + ] print("generating snapshot for {}".format(name)) - subprocess.call(command, shell=True) + with open(output_file, "w") as f: + subprocess.call(command, stdout=f) def build_bat(): print("building bat") - subprocess.call("cargo build", cwd="../..", shell=True) + subprocess.call(["cargo", "build"], cwd="../..") def prepare_output_dir(): @@ -49,7 +50,7 @@ def modify_sample_file(): def undo_sample_file_modification(): print("undoing sample.rs modifications") - subprocess.call("git checkout -- sample.rs", shell=True) + subprocess.call(["git", "checkout", "--", "sample.rs"]) build_bat() From 6876a782daf2055b3ab743885b3868e3c268b66e Mon Sep 17 00:00:00 2001 From: Kira Security Bot Date: Sun, 12 Apr 2026 12:37:43 +0000 Subject: [PATCH 53/67] Apply code changes: @orbisai0security can you address code review comm... --- tests/snapshots/generate_snapshots.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) mode change 100755 => 100644 tests/snapshots/generate_snapshots.py diff --git a/tests/snapshots/generate_snapshots.py b/tests/snapshots/generate_snapshots.py old mode 100755 new mode 100644 index f8c14541..db7839e9 --- a/tests/snapshots/generate_snapshots.py +++ b/tests/snapshots/generate_snapshots.py @@ -4,6 +4,7 @@ import itertools import subprocess import pathlib import shutil +from typing import Iterable def generate_snapshots(): @@ -19,14 +20,14 @@ def generate_snapshots(): def generate_style_snapshot(style): - generate_snapshot(style.replace(",", "_"), "--style={}".format(style)) + generate_snapshot(style.replace(",", "_"), ["--style={}".format(style)]) -def generate_snapshot(name, arguments): +def generate_snapshot(name: str, arguments: Iterable[str]): output_file = "output/{name}.snapshot.txt".format(name=name) command = [ "cargo", "run", "--", "--paging=never", "--color=never", - "--decorations=always", arguments, "sample.rs" + "--decorations=always", *arguments, "sample.rs" ] print("generating snapshot for {}".format(name)) with open(output_file, "w") as f: From e89c515e9af81cb1058f3074cf01c08b952e69ed Mon Sep 17 00:00:00 2001 From: Jan Larres Date: Mon, 13 Apr 2026 17:52:03 +1200 Subject: [PATCH 54/67] [Python] Support uv as script runner in shebang --- CHANGELOG.md | 1 + assets/patches/Python.sublime-syntax.patch | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee313425..4fc4e54e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) - Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) +- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) ## Bugfixes - Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar) diff --git a/assets/patches/Python.sublime-syntax.patch b/assets/patches/Python.sublime-syntax.patch index 36a50894..bacb9db9 100644 --- a/assets/patches/Python.sublime-syntax.patch +++ b/assets/patches/Python.sublime-syntax.patch @@ -2,6 +2,15 @@ diff --git syntaxes/01_Packages/Python/Python.sublime-syntax syntaxes/01_Package index 2acd86d8..86257f7b 100644 --- syntaxes/01_Packages/Python/Python.sublime-syntax +++ syntaxes/01_Packages/Python/Python.sublime-syntax +@@ -25,7 +31,7 @@ file_extensions: + - wscript + - bazel + - bzl +-first_line_match: ^#!\s*/.*\bpython(\d(\.\d)?)?\b ++first_line_match: ^#!\s*/.*\b(python(\d(\.\d)?)?|uv)\b + scope: source.python + + variables: @@ -988,10 +988,6 @@ contexts: - match: \} scope: punctuation.section.mapping-or-set.end.python From 52763e0205f00b9b4f388e9f994aebe0fa0b41b5 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:04:26 -0700 Subject: [PATCH 55/67] feat: add tclsh, wish, and expect shebang detection for Tcl syntax Add first_line_match to the Tcl syntax definition via a patch file, enabling automatic Tcl highlighting for scripts with tclsh, wish, or expect shebangs. Fixes #3513 --- CHANGELOG.md | 1 + assets/patches/Tcl.sublime-syntax.patch | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 assets/patches/Tcl.sublime-syntax.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc4e54e..3682bb82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## Features - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) +- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3513 (@mvanhorn) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) - Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar) diff --git a/assets/patches/Tcl.sublime-syntax.patch b/assets/patches/Tcl.sublime-syntax.patch new file mode 100644 index 00000000..487a1841 --- /dev/null +++ b/assets/patches/Tcl.sublime-syntax.patch @@ -0,0 +1,12 @@ +diff --git syntaxes/01_Packages/TCL/Tcl.sublime-syntax syntaxes/01_Packages/TCL/Tcl.sublime-syntax +index 1234567..abcdefg 100644 +--- syntaxes/01_Packages/TCL/Tcl.sublime-syntax ++++ syntaxes/01_Packages/TCL/Tcl.sublime-syntax +@@ -3,6 +3,7 @@ + # http://www.sublimetext.com/docs/3/syntax.html + name: Tcl + file_extensions: + - tcl ++first_line_match: ^\#!.*\b(tclsh|wish|expect)\b + scope: source.tcl + variables: From 1f89178fce7dde0f6241091fff241768317a9c46 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:06:42 -0700 Subject: [PATCH 56/67] fix: reference PR number in changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3682bb82..79b65708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,12 @@ ## Features +<<<<<<< HEAD - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) - Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3513 (@mvanhorn) +======= +- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn) +>>>>>>> 785fff9 (fix: reference PR number in changelog entry) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) - Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar) From e070d105b57f854d982212265e2e52d6ea6d24ec Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:22:54 -0700 Subject: [PATCH 57/67] test: add shebang regression tests and move changelog to Syntaxes section Add extensionless regression test files for tclsh, wish, and expect shebangs so syntect bumps don't silently break first-line detection. Move changelog entry from Features to Syntaxes per reviewer suggestion. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 ++-- .../regression_tests/issue_3647_expect | 2 ++ .../regression_tests/issue_3647_tclsh | 2 ++ .../examples/regression_tests/issue_3647_wish | 2 ++ tests/integration_tests.rs | 31 +++++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 tests/examples/regression_tests/issue_3647_expect create mode 100644 tests/examples/regression_tests/issue_3647_tclsh create mode 100644 tests/examples/regression_tests/issue_3647_wish diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b65708..99222693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,9 @@ <<<<<<< HEAD - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) -- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3513 (@mvanhorn) -======= - Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn) ->>>>>>> 785fff9 (fix: reference PR number in changelog entry) +======= +>>>>>>> 29c913f (test: add shebang regression tests and move changelog to Syntaxes section) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) - Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar) @@ -50,6 +49,7 @@ ## Syntaxes +- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn) - Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) - Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) - Add syntax highlighting support for COBOL, see #3584 (@adukhan99) diff --git a/tests/examples/regression_tests/issue_3647_expect b/tests/examples/regression_tests/issue_3647_expect new file mode 100644 index 00000000..3786df69 --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_expect @@ -0,0 +1,2 @@ +#!/usr/bin/expect -f +set timeout 30 diff --git a/tests/examples/regression_tests/issue_3647_tclsh b/tests/examples/regression_tests/issue_3647_tclsh new file mode 100644 index 00000000..0f4b3c5c --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_tclsh @@ -0,0 +1,2 @@ +#!/usr/bin/env tclsh +puts "Hello from tclsh" diff --git a/tests/examples/regression_tests/issue_3647_wish b/tests/examples/regression_tests/issue_3647_wish new file mode 100644 index 00000000..8bfe4baf --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_wish @@ -0,0 +1,2 @@ +#!/usr/bin/wish +button .b -text "Click" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 8ae4cfdd..9f18f598 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -4201,4 +4201,35 @@ fn plain_without_diff_still_works() { .assert() .success() .stdout("line 1\nline 2 modified\nline 3\nline 4 added\n"); +#[test] +fn tcl_shebang_detection_tclsh() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_tclsh") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_wish() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_wish") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_expect() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_expect") + .assert() + .success(); } From cafad6b03668abc1ab41e1e10e72fd449ac609e4 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:18:30 -0700 Subject: [PATCH 58/67] test: add highlighted outputs for Tcl shebang regression tests Generate highlighted test outputs for tclsh, wish, and expect shebang detection files to prevent regressions. --- CHANGELOG.md | 4 ---- tests/integration_tests.rs | 2 ++ tests/syntax-tests/highlighted/Tcl/expect_shebang | 7 +++++++ tests/syntax-tests/highlighted/Tcl/tclsh_shebang | 7 +++++++ tests/syntax-tests/highlighted/Tcl/wish_shebang | 5 +++++ 5 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 tests/syntax-tests/highlighted/Tcl/expect_shebang create mode 100644 tests/syntax-tests/highlighted/Tcl/tclsh_shebang create mode 100644 tests/syntax-tests/highlighted/Tcl/wish_shebang diff --git a/CHANGELOG.md b/CHANGELOG.md index 99222693..8049cba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,7 @@ ## Features -<<<<<<< HEAD - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) -- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn) -======= ->>>>>>> 29c913f (test: add shebang regression tests and move changelog to Syntaxes section) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) - Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 9f18f598..432faed8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -4201,6 +4201,8 @@ fn plain_without_diff_still_works() { .assert() .success() .stdout("line 1\nline 2 modified\nline 3\nline 4 added\n"); +} + #[test] fn tcl_shebang_detection_tclsh() { bat() diff --git a/tests/syntax-tests/highlighted/Tcl/expect_shebang b/tests/syntax-tests/highlighted/Tcl/expect_shebang new file mode 100644 index 00000000..22f2101e --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/expect_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/expect -f +# Expect script detected via expect shebang +set timeout 30 +spawn ssh user@host +expect "password:" +send "secret\r" +expect eof diff --git a/tests/syntax-tests/highlighted/Tcl/tclsh_shebang b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang new file mode 100644 index 00000000..de50978f --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/env tclsh +# Tcl script detected via tclsh shebang +puts "Hello from tclsh" +set x 42 +if {$x > 0} { + puts "positive" +} diff --git a/tests/syntax-tests/highlighted/Tcl/wish_shebang b/tests/syntax-tests/highlighted/Tcl/wish_shebang new file mode 100644 index 00000000..134b185d --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/wish_shebang @@ -0,0 +1,5 @@ +#!/usr/bin/wish +# Tk script detected via wish shebang +package require Tk +button .b -text "Click" -command {puts "clicked"} +pack .b From 77ea750e662d6dd21196acec1cae32517fe57dac Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 18 Apr 2026 06:58:45 -0700 Subject: [PATCH 59/67] test(Tcl): add shebang source files for regression test CI reported FileNotFoundError for Tcl/{tclsh,expect,wish}_shebang in tests/syntax-tests/source/Tcl/. The highlighted files existed but the source files had been omitted, so create_highlighted_versions.py had nothing to read from. Source files match the shebang regression test inputs at tests/examples/regression_tests/issue_3647_*. --- tests/syntax-tests/source/Tcl/expect_shebang | 7 +++++++ tests/syntax-tests/source/Tcl/tclsh_shebang | 7 +++++++ tests/syntax-tests/source/Tcl/wish_shebang | 5 +++++ 3 files changed, 19 insertions(+) create mode 100644 tests/syntax-tests/source/Tcl/expect_shebang create mode 100644 tests/syntax-tests/source/Tcl/tclsh_shebang create mode 100644 tests/syntax-tests/source/Tcl/wish_shebang diff --git a/tests/syntax-tests/source/Tcl/expect_shebang b/tests/syntax-tests/source/Tcl/expect_shebang new file mode 100644 index 00000000..ef288ba8 --- /dev/null +++ b/tests/syntax-tests/source/Tcl/expect_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/expect -f +# Expect script detected via expect shebang +set timeout 30 +spawn ssh user@host +expect "password:" +send "secret\r" +expect eof diff --git a/tests/syntax-tests/source/Tcl/tclsh_shebang b/tests/syntax-tests/source/Tcl/tclsh_shebang new file mode 100644 index 00000000..08faa4d6 --- /dev/null +++ b/tests/syntax-tests/source/Tcl/tclsh_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/env tclsh +# Tcl script detected via tclsh shebang +puts "Hello from tclsh" +set x 42 +if {$x > 0} { + puts "positive" +} diff --git a/tests/syntax-tests/source/Tcl/wish_shebang b/tests/syntax-tests/source/Tcl/wish_shebang new file mode 100644 index 00000000..690e73ef --- /dev/null +++ b/tests/syntax-tests/source/Tcl/wish_shebang @@ -0,0 +1,5 @@ +#!/usr/bin/wish +# Tk script detected via wish shebang +package require Tk +button .b -text "Click" -command {puts "clicked"} +pack .b From 0ecdeb28d43c7a259c59cc35dd5e1f0c2a266d3e Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:42:26 -0700 Subject: [PATCH 60/67] test(Tcl): regenerate shebang golden files to match patched syntax The Tcl.sublime-syntax.patch adds first_line_match so bat recognizes tclsh/wish/expect shebang files as Tcl. With the patch applied, the leading '#!/...' and '# comment' lines get tokenized as Tcl comments and rendered in Monokai-Extended's comment color instead of default foreground. The goldens were generated against unpatched bat in the original commit, so CI's 'Run tests with updated syntaxes and themes' job (which runs assets/create.sh before the regression test) disagreed. Regenerate against the patched build so the regression test passes. --- tests/syntax-tests/highlighted/Tcl/expect_shebang | 14 +++++++------- tests/syntax-tests/highlighted/Tcl/tclsh_shebang | 12 ++++++------ tests/syntax-tests/highlighted/Tcl/wish_shebang | 10 +++++----- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/syntax-tests/highlighted/Tcl/expect_shebang b/tests/syntax-tests/highlighted/Tcl/expect_shebang index 22f2101e..871eed6c 100644 --- a/tests/syntax-tests/highlighted/Tcl/expect_shebang +++ b/tests/syntax-tests/highlighted/Tcl/expect_shebang @@ -1,7 +1,7 @@ -#!/usr/bin/expect -f -# Expect script detected via expect shebang -set timeout 30 -spawn ssh user@host -expect "password:" -send "secret\r" -expect eof +#!/usr/bin/expect -f +# Expect script detected via expect shebang +set timeout 30 +spawn ssh user@host +expect "password:" +send "secret\r" +expect eof diff --git a/tests/syntax-tests/highlighted/Tcl/tclsh_shebang b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang index de50978f..5461f5f8 100644 --- a/tests/syntax-tests/highlighted/Tcl/tclsh_shebang +++ b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang @@ -1,7 +1,7 @@ -#!/usr/bin/env tclsh -# Tcl script detected via tclsh shebang -puts "Hello from tclsh" -set x 42 -if {$x > 0} { - puts "positive" +#!/usr/bin/env tclsh +# Tcl script detected via tclsh shebang +puts "Hello from tclsh" +set x 42 +if {$x > 0} { + puts "positive" } diff --git a/tests/syntax-tests/highlighted/Tcl/wish_shebang b/tests/syntax-tests/highlighted/Tcl/wish_shebang index 134b185d..94fb971f 100644 --- a/tests/syntax-tests/highlighted/Tcl/wish_shebang +++ b/tests/syntax-tests/highlighted/Tcl/wish_shebang @@ -1,5 +1,5 @@ -#!/usr/bin/wish -# Tk script detected via wish shebang -package require Tk -button .b -text "Click" -command {puts "clicked"} -pack .b +#!/usr/bin/wish +# Tk script detected via wish shebang +package require Tk +button .b -text "Click" -command {puts "clicked"} +pack .b From ea7fafca1e6191dbfc4c55f039930e350b4412b2 Mon Sep 17 00:00:00 2001 From: Barry <100205797+barry3406@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:19:00 -0700 Subject: [PATCH 61/67] windows: statically link the CRT to remove vcruntime dependency Add a .cargo/config.toml that sets target-feature=+crt-static for the i686, x86_64, and aarch64 MSVC targets so bat.exe no longer depends on the vcruntime DLL. Mirrors the fix applied to fd in sharkdp/fd#1891. Closes #3634 --- .cargo/config.toml | 11 +++++++++++ CHANGELOG.md | 1 + 2 files changed, 12 insertions(+) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..67a37e28 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,11 @@ +# On Windows MSVC, statically link the C runtime so that the resulting EXE does +# not depend on the vcruntime DLL. +# +# See: https://github.com/sharkdp/bat/issues/3634 + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 8049cba3..5a37d298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) - Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) - Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon) +- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406) ## Syntaxes From f39d63b85d5b4bc675c718ef7832d786ed38a9a1 Mon Sep 17 00:00:00 2001 From: YoshKoz <77861115+YoshKoz@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:19:53 +0200 Subject: [PATCH 62/67] fix(zsh): use newline-splitting for language completions to fix word-splitting on names with spaces Language names like "HTML (Jinja2)" and "Apache Conf" contain spaces, which caused $() command substitution to word-split them into garbage tokens. Use ${(f)"$(...)"} (split on newlines only) to match the pattern already used for theme completions on line 100. Fixes #2897 --- assets/completions/bat.zsh.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 3c3f81d1..f8de56e4 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -90,7 +90,7 @@ _{{PROJECT_EXECUTABLE}}_main() { languages) local IFS=$'\n' local -a languages - languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') ) + languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }')"} ) _describe 'language' languages && ret=0 ;; From 5722311b2e96f6c6751e848a99c265e386ab43f3 Mon Sep 17 00:00:00 2001 From: Yoshi Tacke <77861115+YoshKoz@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:06:00 +0200 Subject: [PATCH 63/67] docs(changelog): add bugfix entry for zsh completion fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a37d298..8eabb7d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) - Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) - Fixed syntax tests path, see #3610 (@foxfromworld) +- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz) ## Other - Use git version of cross. See #3533 (@OctopusET) From 64567c4819ff76cec66a74e87c9e47b1f6f9b609 Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Tue, 28 Apr 2026 01:11:06 +1000 Subject: [PATCH 64/67] Propagate initial input read errors --- CHANGELOG.md | 1 + src/input.rs | 44 +++++++++++++++++++++++++++++++++++--------- src/lessopen.rs | 4 ++-- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eabb7d4..066b1161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) ## Bugfixes +- Report initial input read errors instead of treating them as empty input. Closes #3002, see #3706 (@lawrence3699) - Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar) - Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) - Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) diff --git a/src/input.rs b/src/input.rs index 30f13f98..a3420123 100644 --- a/src/input.rs +++ b/src/input.rs @@ -207,7 +207,7 @@ impl<'a> Input<'a> { kind: OpenedInputKind::StdIn, description, metadata: self.metadata, - reader: InputReader::new(stdin), + reader: InputReader::try_new(stdin)?, }) } @@ -236,14 +236,14 @@ impl<'a> Input<'a> { file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened..."); } - InputReader::new(BufReader::new(file)) + InputReader::try_new(BufReader::new(file))? }, }), InputKind::CustomReader(reader) => Ok(OpenedInput { description, kind: OpenedInputKind::CustomReader, metadata: self.metadata, - reader: InputReader::new(BufReader::new(reader)), + reader: InputReader::try_new(BufReader::new(reader))?, }), } } @@ -257,24 +257,29 @@ pub(crate) struct InputReader<'a> { } impl<'a> InputReader<'a> { - pub(crate) fn new(mut reader: R) -> InputReader<'a> { + #[cfg(test)] + pub(crate) fn new(reader: R) -> InputReader<'a> { + Self::try_new(reader).expect("reading the first line failed") + } + + pub(crate) fn try_new(mut reader: R) -> io::Result> { let mut first_line = vec![]; - reader.read_until(b'\n', &mut first_line).ok(); + reader.read_until(b'\n', &mut first_line)?; let content_type = inspect_content_type(&first_line); if content_type == Some(ContentType::UTF_16LE) { - read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok(); + read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?; } else if content_type == Some(ContentType::UTF_16BE) { - read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00).ok(); + read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?; } - InputReader { + Ok(InputReader { inner: Box::new(reader), first_line, content_type, unbuffered: false, - } + }) } pub(crate) fn read_line(&mut self, buf: &mut Vec) -> io::Result { @@ -405,6 +410,27 @@ fn non_zip_pk_prefix_is_not_treated_as_binary() { ); } +#[test] +fn input_open_returns_initial_read_errors() { + struct FailingRead; + + impl Read for FailingRead { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::other("initial read failed")) + } + } + + let input = Input::from_reader(Box::new(FailingRead)); + let result = input.open(io::empty(), None); + + assert!(result.is_err()); + assert!(result + .err() + .unwrap() + .to_string() + .contains("initial read failed")); +} + #[test] fn utf16le() { let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00"; diff --git a/src/lessopen.rs b/src/lessopen.rs index 966bc2c0..116206ba 100644 --- a/src/lessopen.rs +++ b/src/lessopen.rs @@ -155,7 +155,7 @@ impl LessOpenPreprocessor { Ok(OpenedInput { kind, - reader: InputReader::new(BufReader::new( + reader: InputReader::try_new(BufReader::new( if matches!(self.kind, LessOpenKind::TempFile) { let lessopen_string = match String::from_utf8(lessopen_stdout) { Ok(string) => string, @@ -192,7 +192,7 @@ impl LessOpenPreprocessor { .map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)), } }, - )), + ))?, metadata: input.metadata, description: input.description, }) From bceb260e91919aebe48f4230f69d5c3f0ebee1e9 Mon Sep 17 00:00:00 2001 From: guille Date: Fri, 24 Apr 2026 16:20:49 +0200 Subject: [PATCH 65/67] Improve Kotlin syntax --- .gitmodules | 6 +- CHANGELOG.md | 1 + assets/syntaxes/02_Extra/Kotlin | 2 +- .../syntaxes/02_Extra/Kotlin.sublime-syntax | 398 ------------------ tests/syntax-tests/highlighted/Kotlin/test.kt | 104 ++--- 5 files changed, 57 insertions(+), 454 deletions(-) delete mode 100644 assets/syntaxes/02_Extra/Kotlin.sublime-syntax diff --git a/.gitmodules b/.gitmodules index b64280c6..20e0bbb4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -49,9 +49,6 @@ [submodule "assets/themes/zenburn"] path = assets/themes/zenburn url = https://github.com/colinta/zenburn.git -[submodule "assets/syntaxes/Kotlin"] - path = assets/syntaxes/02_Extra/Kotlin - url = https://github.com/vkostyukov/kotlin-sublime-package [submodule "assets/syntaxes/Elm"] path = assets/syntaxes/02_Extra/Elm url = https://github.com/elm-community/SublimeElmLanguageSupport @@ -281,3 +278,6 @@ [submodule "assets/syntaxes/02_Extra/COBOL"] path = assets/syntaxes/02_Extra/COBOL url = https://github.com/adukhan99/sublime_cobol.git +[submodule "assets/syntaxes/02_Extra/Kotlin"] + path = assets/syntaxes/02_Extra/Kotlin + url = https://github.com/guille/sublime-kotlin diff --git a/CHANGELOG.md b/CHANGELOG.md index 066b1161..341dbf42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ - Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) - Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp) - Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp) +- Improved Kotlin syntax, see #3698 (@guille) ## Themes diff --git a/assets/syntaxes/02_Extra/Kotlin b/assets/syntaxes/02_Extra/Kotlin index aeeed278..c3536941 160000 --- a/assets/syntaxes/02_Extra/Kotlin +++ b/assets/syntaxes/02_Extra/Kotlin @@ -1 +1 @@ -Subproject commit aeeed2780b04aea3d293c547c24cae27cafef0c5 +Subproject commit c353694169c047bd746c93c214289fc52d152cae diff --git a/assets/syntaxes/02_Extra/Kotlin.sublime-syntax b/assets/syntaxes/02_Extra/Kotlin.sublime-syntax deleted file mode 100644 index eab59192..00000000 --- a/assets/syntaxes/02_Extra/Kotlin.sublime-syntax +++ /dev/null @@ -1,398 +0,0 @@ -%YAML 1.2 ---- -# http://www.sublimetext.com/docs/3/syntax.html -name: Kotlin -file_extensions: - - kt - - kts -scope: source.Kotlin -contexts: - main: - - include: comments - - match: '^\s*(package)\b(?:\s*([^ ;$]+)\s*)?' - captures: - 1: keyword.other.kotlin - 2: entity.name.package.kotlin - - include: imports - - include: statements - classes: - - match: (?" - pop: true - - include: generics - - match: \( - push: - - match: \) - pop: true - - include: parameters - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: "(?={|$)" - pop: true - - match: \w+ - scope: entity.other.inherited-class.kotlin - - match: \( - push: - - match: \) - pop: true - - include: expressions - - match: '\{' - push: - - match: '\}' - pop: true - - include: statements - comments: - - match: /\* - captures: - 0: punctuation.definition.comment.kotlin - push: - - meta_scope: comment.block.kotlin - - match: \*/ - captures: - 0: punctuation.definition.comment.kotlin - pop: true - - match: \s*((//).*$\n?) - captures: - 1: comment.line.double-slash.kotlin - 2: punctuation.definition.comment.kotlin - constants: - - match: \b(true|false|null|this|super)\b - scope: constant.language.kotlin - - match: '\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)([LlFf])?\b' - scope: constant.numeric.kotlin - - match: '\b([A-Z][A-Z0-9_]+)\b' - scope: constant.other.kotlin - expressions: - - match: \( - push: - - match: \) - pop: true - - include: expressions - - include: types - - include: strings - - include: constants - - include: comments - - include: keywords - functions: - - match: (?=\s*\b(?:fun)\b) - push: - - match: '(?=$|\})' - pop: true - - match: \b(fun)\b - captures: - 1: keyword.other.kotlin - push: - - match: (?=\() - pop: true - - match: < - push: - - match: ">" - pop: true - - include: generics - - match: '([\.<\?>\w]+\.)?(\w+)' - captures: - 2: entity.name.function.kotlin - - match: \( - push: - - match: \) - pop: true - - include: parameters - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: "(?={|=|$)" - pop: true - - include: types - - match: '\{' - push: - - match: '(?=\})' - pop: true - - include: statements - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$) - pop: true - - include: expressions - generics: - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?=,|>) - pop: true - - include: types - - include: keywords - - match: \w+ - scope: storage.type.generic.kotlin - getters-and-setters: - - match: \b(get)\b\s*\(\s*\) - captures: - 1: entity.name.function.kotlin - push: - - match: '\}|(?=\bset\b)|$' - pop: true - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$|\bset\b) - pop: true - - include: expressions - - match: '\{' - push: - - match: '\}' - pop: true - - include: expressions - - match: \b(set)\b\s*(?=\() - captures: - 1: entity.name.function.kotlin - push: - - match: '\}|(?=\bget\b)|$' - pop: true - - match: \( - push: - - match: \) - pop: true - - include: parameters - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$|\bset\b) - pop: true - - include: expressions - - match: '\{' - push: - - match: '\}' - pop: true - - include: expressions - imports: - - match: '^\s*(import)\s+[^ $]+\s+(as)?' - captures: - 1: keyword.other.kotlin - 2: keyword.other.kotlin - keywords: - - match: \b(var|val|public|private|protected|abstract|final|sealed|enum|open|attribute|annotation|override|inline|vararg|in|out|internal|data|tailrec|operator|infix|const|yield|typealias|typeof|reified|suspend)\b - scope: storage.modifier.kotlin - - match: \b(try|catch|finally|throw)\b - scope: keyword.control.catch-exception.kotlin - - match: \b(if|else|while|for|do|return|when|where|break|continue)\b - scope: keyword.control.kotlin - - match: \b(in|is|!in|!is|as|as\?|assert)\b - scope: keyword.operator.kotlin - - match: (==|!=|===|!==|<=|>=|<|>) - scope: keyword.operator.comparison.kotlin - - match: (=) - scope: keyword.operator.assignment.kotlin - - match: (::) - scope: keyword.operator.kotlin - - match: (:) - scope: keyword.operator.declaration.kotlin - - match: \b(by)\b - scope: keyword.other.by.kotlin - - match: (\?\.) - scope: keyword.operator.safenav.kotlin - - match: (\.) - scope: keyword.operator.dot.kotlin - - match: (\?:) - scope: keyword.operator.elvis.kotlin - - match: (\-\-|\+\+) - scope: keyword.operator.increment-decrement.kotlin - - match: (\+=|\-=|\*=|\/=) - scope: keyword.operator.arithmetic.assign.kotlin - - match: (\.\.) - scope: keyword.operator.range.kotlin - - match: (\-|\+|\*|\/|%) - scope: keyword.operator.arithmetic.kotlin - - match: (!|&&|\|\|) - scope: keyword.operator.logical.kotlin - - match: (;) - scope: punctuation.terminator.kotlin - namespaces: - - match: \b(namespace)\b - scope: keyword.other.kotlin - - match: '\{' - push: - - match: '\}' - pop: true - - include: statements - parameters: - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?=,|\)|=) - pop: true - - include: types - - match: (=) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?=,|\)) - pop: true - - include: expressions - - include: keywords - - match: \w+ - scope: variable.parameter.function.kotlin - statements: - - include: namespaces - - include: typedefs - - include: classes - - include: functions - - include: variables - - include: getters-and-setters - - include: expressions - strings: - - match: '"""' - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.third.kotlin - - match: '"""' - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - - match: '(\$\w+|\$\{[^\}]+\})' - scope: variable.parameter.template.kotlin - - match: \\. - scope: constant.character.escape.kotlin - - match: '"' - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.double.kotlin - - match: '"' - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - - match: '(\$\w+|\$\{[^\}]+\})' - scope: variable.parameter.template.kotlin - - match: \\. - scope: constant.character.escape.kotlin - - match: "'" - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.single.kotlin - - match: "'" - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - - match: \\. - scope: constant.character.escape.kotlin - - match: "`" - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.single.kotlin - - match: "`" - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - typedefs: - - match: (?=\s*(?:type)) - push: - - match: (?=$) - pop: true - - match: \b(type)\b - scope: keyword.other.kotlin - - match: < - push: - - match: ">" - pop: true - - include: generics - - include: expressions - types: - - match: \b(Nothing|Any|Unit|String|CharSequence|Int|Boolean|Char|Long|Double|Float|Short|Byte|dynamic)\b - scope: storage.type.buildin.kotlin - - match: \b(IntArray|BooleanArray|CharArray|LongArray|DoubleArray|FloatArray|ShortArray|ByteArray)\b - scope: storage.type.buildin.array.kotlin - - match: \b(Array|Collection|List|Map|Set|MutableList|MutableMap|MutableSet|Sequence)<\b - captures: - 1: storage.type.buildin.collection.kotlin - push: - - match: ">" - pop: true - - include: types - - include: keywords - - match: \w+< - push: - - match: ">" - pop: true - - include: types - - include: keywords - - match: '\{' - push: - - match: '\}' - pop: true - - include: statements - - match: \( - push: - - match: \) - pop: true - - include: types - - match: (->) - scope: keyword.operator.declaration.kotlin - variables: - - match: (?=\s*\b(?:var|val)\b) - push: - - match: (?=:|=|(\b(by)\b)|$) - pop: true - - match: \b(var|val)\b - captures: - 1: keyword.other.kotlin - push: - - match: (?=:|=|(\b(by)\b)|$) - pop: true - - match: < - push: - - match: ">" - pop: true - - include: generics - - match: '([\.<\?>\w]+\.)?(\w+)' - captures: - 2: entity.name.variable.kotlin - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?==|$) - pop: true - - include: types - - include: getters-and-setters - - match: \b(by)\b - captures: - 1: keyword.other.kotlin - push: - - match: (?=$) - pop: true - - include: expressions - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$) - pop: true - - include: expressions - - include: getters-and-setters diff --git a/tests/syntax-tests/highlighted/Kotlin/test.kt b/tests/syntax-tests/highlighted/Kotlin/test.kt index dcb1560e..bb17aba2 100644 --- a/tests/syntax-tests/highlighted/Kotlin/test.kt +++ b/tests/syntax-tests/highlighted/Kotlin/test.kt @@ -1,85 +1,85 @@ -import kotlin.math.* +import kotlin.math.* -data class Example( - val name: String, - val numbers: List<Int?> +data class Example( + val name: String, + val numbers: List<Int?> ) -fun interface JokeInterface { - fun isFunny(): Boolean +fun interface JokeInterface { + fun isFunny(): Boolean } -abstract class AbstractJoke : JokeInterface { - override fun isFunny() = false - abstract fun content(): String +abstract class AbstractJoke : JokeInterface { + override fun isFunny() = false + abstract fun content(): String } -class Joke : AbstractJoke() { - override fun isFunny(): Boolean { +class Joke : AbstractJoke() { + override fun isFunny(): Boolean {  return true - } - override fun content(): String = "content of joke here, haha" + } + override fun content(): String = "content of joke here, haha" } -class DelegatedJoke(val joke: Joke) : JokeInterface by joke { - val number: Long = 123L +class DelegatedJoke(val joke: Joke) : JokeInterface by joke { + val number: Long = 123L - companion object { - const val someConstant = "some constant text" - } + companion object { + const val someConstant = "some constant text" + } } -object SomeSingleton +object SomeSingleton -sealed class Shape { - abstract fun area(): Double +sealed class Shape { + abstract fun area(): Double } -data class Square(val sideLength: Double) : Shape() { - override fun area(): Double = sideLength.pow(2) +data class Square(val sideLength: Double) : Shape() { + override fun area(): Double = sideLength.pow(2) } -object Point : Shape() { - override fun area() = .0 +object Point : Shape() { + override fun area() = .0 } -class Circle(val radius: Double) : Shape() { - override fun area(): Double { - return PI * radius * radius - } +class Circle(val radius: Double) : Shape() { + override fun area(): Double { + return PI * radius * radius + } } -fun String.extensionMethod() = "test" +fun String.extensionMethod() = "test" -fun main() { - val name = """ +fun main() { + val name = """  multiline  string    some numbers: 123123 42 - """.trimIndent() - val example = Example(name = name, numbers = listOf(512, 42, null, -1)) + """.trimIndent() + val example = Example(name = name, numbers = listOf(512, 42, null, -1)) - example.numbers - .filterNotNull() - .forEach { println(it) } + example.numbers + .filterNotNull() + .forEach { println(it) } - setOf(Joke(), DelegatedJoke(Joke()).joke) - .filter(JokeInterface::isFunny) - .map(AbstractJoke::content) - .forEachIndexed { index: Int, joke -> - println("I heard a funny joke(#${index + 1}): $joke") - } + setOf(Joke(), DelegatedJoke(Joke()).joke) + .filter(JokeInterface::isFunny) + .map(AbstractJoke::content) + .forEachIndexed { index: Int, joke -> + println("I heard a funny joke(#${index + 1}): $joke") + } - listOf(Square(12.3), Point, Circle(5.2)) - .associateWith(Shape::area) - .toList() - .sortedBy { it.second } - .forEach { - println("${it.first}: ${it.second}") - } + listOf(Square(12.3), Point, Circle(5.2)) + .associateWith(Shape::area) + .toList() + .sortedBy { it.second } + .forEach { + println("${it.first}: ${it.second}") + } - println("some string".extensionMethod()) + println("some string".extensionMethod()) - require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." } + require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." } } From 492c387ce7fdc02ea7c0aceabef3c2e1b15f09c4 Mon Sep 17 00:00:00 2001 From: guille Date: Fri, 24 Apr 2026 20:52:52 +0200 Subject: [PATCH 66/67] CHANGELOG: reference PR not issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 341dbf42..637dc209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,7 +55,7 @@ - Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) - Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp) - Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp) -- Improved Kotlin syntax, see #3698 (@guille) +- Improved Kotlin syntax, see #3699 (@guille) ## Themes From de64d3a0ebe6f70637d215d6042e7eb9239755e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:28:44 +0000 Subject: [PATCH 67/67] build(deps): bump assets/syntaxes/02_Extra/Idris2 Bumps [assets/syntaxes/02_Extra/Idris2](https://github.com/buzden/sublime-syntax-idris2) from `bbfe50e` to `4d8eb35`. - [Commits](https://github.com/buzden/sublime-syntax-idris2/compare/bbfe50e023e0edc74f5e0c003eb946528d49279f...4d8eb35a38254d422030e77b4933530008dd3c6e) --- updated-dependencies: - dependency-name: assets/syntaxes/02_Extra/Idris2 dependency-version: 4d8eb35a38254d422030e77b4933530008dd3c6e dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- assets/syntaxes/02_Extra/Idris2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/syntaxes/02_Extra/Idris2 b/assets/syntaxes/02_Extra/Idris2 index bbfe50e0..4d8eb35a 160000 --- a/assets/syntaxes/02_Extra/Idris2 +++ b/assets/syntaxes/02_Extra/Idris2 @@ -1 +1 @@ -Subproject commit bbfe50e023e0edc74f5e0c003eb946528d49279f +Subproject commit 4d8eb35a38254d422030e77b4933530008dd3c6e