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/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f1bc3887..f881cada 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -161,12 +161,12 @@ 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 } - { 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 , } @@ -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 @@ -335,7 +333,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/.gitmodules b/.gitmodules index ee84fe05..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 @@ -278,3 +275,9 @@ [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 +[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 5d1ee483..121cc732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,32 @@ # unreleased -- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) -- Fixed syntax tests path, see #3610 (@foxfromworld) + +## Other + +- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536 +- Add .NET slnx extension, see #3682 (@ltrzesniewski) ## Features -- Map `justfile`, `Justfile`, `.justfile`, and `*.justfile` to Makefile syntax highlighting, see #XXXX (@zachvalenta) +- Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta) +- 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 #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) - 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 +- 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) +- 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) @@ -24,16 +36,27 @@ - `--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) +- 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) - 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 +- 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) - 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 #3699 (@guille) ## Themes 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. diff --git a/Cargo.lock b/Cargo.lock index 2e777a2e..202a5295 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" @@ -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", ] @@ -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", ] @@ -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", ] @@ -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", ] @@ -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" @@ -1808,15 +1808,15 @@ 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" -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" @@ -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 6c783336..b63c4e83 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" @@ -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] @@ -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] @@ -113,13 +113,13 @@ 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" 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] diff --git a/README.md b/README.md index 771db7ad..d6ef3381 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) @@ -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). @@ -246,6 +246,14 @@ abbr -a --position anywhere -- -h '-h | bat -plhelp' 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. @@ -276,8 +284,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 @@ -581,7 +589,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 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 ;; 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 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: diff --git a/assets/syntaxes/02_Extra/COBOL b/assets/syntaxes/02_Extra/COBOL new file mode 160000 index 00000000..27825ee0 --- /dev/null +++ b/assets/syntaxes/02_Extra/COBOL @@ -0,0 +1 @@ +Subproject commit 27825ee0220fb7fee73617f7404d4d214dc71b8f 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 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/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 diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 64be4bb9..b0db01eb 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -47,12 +47,49 @@ impl ToTokens for MappingTarget { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)] +/// 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)] +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); + } +} + /// A single matcher. /// /// Codegen converts this into a `Lazy>`. -struct Matcher(Vec); -/// Parse a matcher. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[serde(try_from = "RawMatcher")] +struct Matcher { + segments: Vec, + /// Whether the glob pattern should be matched case-sensitively. + /// + /// Defaults to `Case::Insensitive` for backwards compatibility. + case: Case, +} +/// 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 @@ -66,68 +103,95 @@ struct Matcher(Vec); /// /// 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())); + // 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(non_empty_segments) +} +impl TryFrom for Matcher { + type Error = anyhow::Error; + fn try_from(raw: RawMatcher) -> Result { + 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 segments = parse_glob(glob)?; + let case = match case_sensitive { + None => DEFAULT_CASE, + Some(false) => Case::Insensitive, + Some(true) => Case::Sensitive, + }; + Ok(Self { segments, case }) + } } - // 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(non_empty_segments)) } } impl ToTokens for Matcher { fn to_tokens(&self, tokens: &mut TokenStream) { - let t = match self.0.as_slice() { + 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))) } + 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),* ])) }, + segs @ [_, ..] => { + quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case)) } + } }; tokens.append_all(t); } @@ -175,6 +239,7 @@ 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>, } impl MappingDefModel { 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/app.rs b/src/bin/bat/app.rs index dddb5559..f95bbb91 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -590,7 +590,17 @@ 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") + { + #[cfg(feature = "git")] + components.insert(StyleComponent::Changes); + components.insert(StyleComponent::Snip); + } + return Some(StyleComponents(components)); } // Default behavior. 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 7ca08b9d..f3fa01f4 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -285,7 +285,28 @@ 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()), ) @@ -307,6 +328,9 @@ 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", "LANG", "LC_ALL", @@ -326,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")] diff --git a/src/input.rs b/src/input.rs index 29846abe..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,28 +257,29 @@ pub(crate) struct InputReader<'a> { } impl<'a> InputReader<'a> { - pub(crate) fn new(mut reader: R) -> InputReader<'a> { - let mut first_line = vec![]; - reader.read_until(b'\n', &mut first_line).ok(); + #[cfg(test)] + pub(crate) fn new(reader: R) -> InputReader<'a> { + Self::try_new(reader).expect("reading the first line failed") + } - let content_type = if first_line.is_empty() { - None - } else { - Some(content_inspector::inspect(&first_line[..])) - }; + pub(crate) fn try_new(mut reader: R) -> io::Result> { + let mut first_line = vec![]; + 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 { @@ -319,6 +320,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 +394,43 @@ 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 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, }) 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 || { 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/src/syntax_mapping.rs b/src/syntax_mapping.rs index 0cd2d655..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) -> 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(true) + .case_insensitive(matches!(case, Case::Insensitive)) .literal_separator(true) .build()? .compile_matcher(); @@ -97,7 +104,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, 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, Case::Sensitive)?; self.custom_mappings.push((matcher, to)); Ok(()) } @@ -261,4 +275,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..79d298c3 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) -> GlobMatcher { - make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile") +fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher { + 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 @@ -64,7 +64,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: Case) -> Option { // join segments let mut buf = String::new(); for seg in segs { @@ -77,7 +77,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).ok()?; Some(matcher) } diff --git a/src/syntax_mapping/builtins/README.md b/src/syntax_mapping/builtins/README.md index 29cf43ee..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`, 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`, 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 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..2ced1399 --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -0,0 +1,2 @@ +[mappings] +"Python" = [{ glob = "BUILD", case_sensitive = true }] 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/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"] 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"] diff --git a/tests/examples/regression_tests/issue_3631.txt b/tests/examples/regression_tests/issue_3631.txt new file mode 100644 index 00000000..8449691f Binary files /dev/null and b/tests/examples/regression_tests/issue_3631.txt differ 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 cfbad253..432faed8 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(""); } @@ -2093,6 +2145,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() @@ -2681,6 +2751,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) @@ -3965,3 +4054,184 @@ 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"); +} + +#[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(); +} diff --git a/tests/snapshots/generate_snapshots.py b/tests/snapshots/generate_snapshots.py old mode 100755 new mode 100644 index bb7d69ba..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,22 +20,23 @@ 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): - command = "cargo run -- --paging=never --color=never --decorations=always " - command += "{args} sample.rs > output/{name}.snapshot.txt".format( - name=name, - args=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" + ] 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 +51,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() 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/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/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..." } } diff --git a/tests/syntax-tests/highlighted/Tcl/expect_shebang b/tests/syntax-tests/highlighted/Tcl/expect_shebang new file mode 100644 index 00000000..871eed6c --- /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..5461f5f8 --- /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..94fb971f --- /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 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/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 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 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 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 @@ + + + + + + +