Merge branch 'master' into feature/salesforce_apex_support

pull/579/head
Rodolphe Blancho 2023-12-05 12:31:59 +07:00 committed by GitHub
commit e18b5d0712
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 507898 additions and 463265 deletions

@ -27,7 +27,7 @@ jobs:
- name: Install jq
run: sudo apt-get install -y jq
- uses: dtolnay/rust-toolchain@1.59.0
- uses: dtolnay/rust-toolchain@1.63.0
- run: cargo doc
- name: Setup mdBook

@ -12,19 +12,31 @@ jobs:
job:
- { target: x86_64-unknown-linux-gnu, os: ubuntu-20.04 }
- { target: x86_64-apple-darwin, os: macos-latest }
- { target: x86_64-pc-windows-gnu, os: windows-latest }
- { target: x86_64-unknown-linux-musl, os: ubuntu-20.04, use-cross: true }
- { target: x86_64-pc-windows-msvc, os: windows-latest }
- { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, use-cross: true }
- { target: aarch64-apple-darwin, os: macos-latest, use-cross: true }
# TODO: Support cross-compiling on macOS on CI, see
# https://github.com/cross-rs/cross-toolchains#apple-targets
#
# - { target: aarch64-apple-darwin, os: macos-latest, use-cross: true }
# musl binaries produced by GitHub actions segfault when run, see
# https://github.com/Wilfred/difftastic/issues/563
#
# musl builds seem to work fine elsewhere, so just verify
# that compilation works for now.
- { target: x86_64-unknown-linux-musl, os: ubuntu-20.04, use-cross: true, build-only: true }
env:
BUILD_CMD: cargo
SUBCOMMAND: test
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.59.0
uses: dtolnay/rust-toolchain@1.63.0
with:
targets: ${{ matrix.job.target }}
@ -32,23 +44,28 @@ jobs:
if: matrix.job.use-cross
uses: taiki-e/install-action@v2
with:
tool: cross
tool: cross@0.2.5
- name: Overwrite build command env variable
if: matrix.job.use-cross
shell: bash
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
- name: Overwrite build subcommand env variable
if: matrix.job.build-only
shell: bash
run: echo "SUBCOMMAND=build" >> $GITHUB_ENV
- name: Test
shell: bash
run: $BUILD_CMD test
run: $BUILD_CMD $SUBCOMMAND --target ${{ matrix.job.target }}
test_mime_db:
name: Test with MIME database
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.59.0
- uses: dtolnay/rust-toolchain@1.63.0
# This runs tests that rely on the MIME database being present.
- run: cargo test -- --ignored
@ -65,7 +82,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.59.0
- uses: dtolnay/rust-toolchain@1.63.0
- name: Generate output for all sample files
run: ./sample_files/compare_all.sh
- name: Verify output is unchanged
@ -76,7 +93,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.59.0
- uses: dtolnay/rust-toolchain@1.63.0
- run: cargo package --allow-dirty
fmt:
@ -84,5 +101,14 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.59.0
- uses: dtolnay/rust-toolchain@1.63.0
- run: cargo fmt --all -- --check
actionlint:
name: Actionlint
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
shellcheck: false
- uses: raven-actions/actionlint@v1

1
.gitignore vendored

@ -2,6 +2,7 @@
**/*.rs.bk
perf.data*
flamegraph.svg
.idea
sample_files/compare.result

@ -1,4 +1,40 @@
## 0.53 (unreleased)
## 0.54 (unreleased)
### Display
Fixed a rare crash when the last non-blank line had changes for
certain parsers, particularly YAML.
### Build
Difftastic now requires Rust 1.63 to build.
## 0.53.1 (released 26th November 2023)
### Build
Fixed a dependency issue that broke aarch64 builds on older rustc versions.
## 0.53 (released 24th November 2023)
### Parsing
Added support for SCSS.
Updated the Kotlin parser and improved handling of Kotlin nullable
types.
`.snap` files (Jest snapshots) are now detected as JavaScript files.
### Diffing
Fixed an issue where adding or removing blank lines would be ignored
by the textual diffing logic.
Directory diffing now respects `.gitignore` files.
Directory diffing can now be sorted by path with the `--sort-paths`
option.
### Command Line Interface
@ -6,6 +42,13 @@ Added the option `--strip-cr`. This removes all carriage return
characters before diffing, which is helpful when dealing with a mix of
Windows and non-Windows flies.
The option `--skip-unchanged` now has a corresponding environment
option `DFT_SKIP_UNCHANGED`.
### Build
Difftastic now requires Rust 1.60 to build.
## 0.52 (released 8th October 2023)
### Parsing

283
Cargo.lock generated

@ -15,9 +15,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "0.7.20"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
dependencies = [
"memchr",
]
@ -28,7 +28,7 @@ version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5c2ca00549910ec251e3bd15f87aeeb206c9456b9a77b43ff6c97c54042a472"
dependencies = [
"bstr",
"bstr 0.2.17",
"doc-comment",
"predicates",
"predicates-core",
@ -42,7 +42,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"hermit-abi 0.1.19",
"libc",
"winapi",
]
@ -59,6 +59,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bstr"
version = "0.2.17"
@ -67,7 +73,17 @@ checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"regex-automata 0.1.10",
]
[[package]]
name = "bstr"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
]
[[package]]
@ -104,7 +120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
dependencies = [
"atty",
"bitflags",
"bitflags 1.3.2",
"clap_lex",
"indexmap",
"lazy_static",
@ -190,11 +206,11 @@ dependencies = [
[[package]]
name = "crossterm"
version = "0.26.1"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags",
"bitflags 2.4.1",
"crossterm_winapi",
"libc",
"mio",
@ -206,9 +222,9 @@ dependencies = [
[[package]]
name = "crossterm_winapi"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
@ -237,8 +253,9 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "difftastic"
version = "0.53.0"
version = "0.54.0"
dependencies = [
"aho-corasick",
"assert_cmd",
"bumpalo",
"cc",
@ -248,12 +265,14 @@ dependencies = [
"glob",
"hashbrown 0.12.3",
"humansize",
"ignore",
"itertools 0.11.0",
"lazy_static",
"libc",
"libmimalloc-sys",
"line-numbers",
"log",
"memchr",
"mimalloc",
"owo-colors",
"predicates",
@ -272,7 +291,6 @@ dependencies = [
"typed-arena",
"unicode-width",
"version_check",
"walkdir",
"wu-diff",
]
@ -290,17 +308,27 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "env_logger"
version = "0.7.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
"atty",
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "errno"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "fixedbitset"
version = "0.4.1"
@ -339,6 +367,19 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
dependencies = [
"aho-corasick",
"bstr 1.6.0",
"fnv",
"log",
"regex",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
@ -369,6 +410,12 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "humansize"
version = "2.1.3"
@ -380,11 +427,26 @@ dependencies = [
[[package]]
name = "humantime"
version = "1.3.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "ignore"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [
"quick-error",
"crossbeam-utils",
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
[[package]]
@ -397,6 +459,29 @@ dependencies = [
"hashbrown 0.11.2",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.3",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "is-terminal"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi 0.3.3",
"io-lifetimes",
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "itertools"
version = "0.10.3"
@ -429,9 +514,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.139"
version = "0.2.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "libm"
@ -450,9 +535,15 @@ dependencies = [
[[package]]
name = "line-numbers"
version = "0.2.2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b25f5068853805faa3c19f44d0c401446e4eb3f47cc808fa331eec30f0ba35c"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793a75315eb63b8699158825bdea85d63eeb850e7543cb834abef3c7b5b53780"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "lock_api"
@ -512,7 +603,7 @@ dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -546,7 +637,7 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi",
"hermit-abi 0.1.19",
"libc",
]
@ -597,7 +688,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -654,9 +745,9 @@ dependencies = [
[[package]]
name = "pretty_env_logger"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
dependencies = [
"env_logger",
"log",
@ -671,12 +762,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.32"
@ -720,17 +805,18 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.7.3"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.7",
"regex-syntax",
]
@ -740,11 +826,22 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-automata"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "rustc-hash"
@ -752,6 +849,20 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.37.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "rustversion"
version = "1.0.12"
@ -812,9 +923,9 @@ dependencies = [
[[package]]
name = "signal-hook"
version = "0.3.14"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
@ -930,6 +1041,16 @@ dependencies = [
"terminal_size",
]
[[package]]
name = "thread_local"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "tree-sitter"
version = "0.20.9"
@ -1045,13 +1166,37 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.1",
"windows_aarch64_msvc 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm 0.42.1",
"windows_x86_64_msvc 0.42.1",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
@ -1060,42 +1205,84 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "wu-diff"
version = "0.1.2"

@ -4,7 +4,7 @@ description = "A structural diff that understands syntax."
repository = "https://github.com/wilfred/difftastic"
homepage = "http://difftastic.wilfred.me.uk/"
license = "MIT"
version = "0.53.0"
version = "0.54.0"
authors = ["Wilfred Hughes <me@wilfred.me.uk>"]
keywords = ["diff", "syntax"]
categories = ["development-tools", "command-line-utilities", "parser-implementations"]
@ -12,7 +12,7 @@ edition = "2021"
# Goal: support at least 12 months of Rust versions, and also the Rust
# version on Debian stable:
# https://tracker.debian.org/pkg/rustc
rust-version = "1.59.0"
rust-version = "1.63.0"
include = [
"/build.rs",
"/src/",
@ -31,8 +31,11 @@ pkg-url = "{ repo }/releases/download/{ version }/difft-{ target }.{ archive-for
pkg-fmt = "zip"
[dependencies]
# regex 1.8 requires rust 1.60.
regex = ">= 1.7, < 1.8"
# regex 1.10 requires rust 1.65.
# regex 1.9.5 depends on memchr 2.6, which requires rust 1.61.
regex = ">= 1.9, < 1.9.5"
memchr = ">= 2, < 2.6"
clap = { version = "3.1.8", features = ["cargo", "env", "wrap_help"] }
itertools = "0.11.0"
typed-arena = "2.0.2"
@ -43,13 +46,22 @@ lazy_static = "1.4.0"
tree-sitter = "0.20.9"
libc = "0.2.108"
log = "0.4.14"
pretty_env_logger = "0.4.0"
pretty_env_logger = "0.5.0"
mimalloc = { version = "0.1.28", default-features = false }
# Pin libmimalloc-sys due to 0.1.25 producing a huge slowdown in very
# large textual files, as discussed in #297.
libmimalloc-sys = "=0.1.24"
radix-heap = "0.4.2"
walkdir = "2.3.3"
# ignore 0.4.19 requires scoped_threads, which was added in rust 1.63.
ignore = ">= 0.4, < 0.4.19"
# aho-corasick is a dependency of regex and ignore. In 1.1.0 it added
# usage of the target feature 'neon' on aarch64, which is unstable on
# rust 1.60.
#
# https://github.com/BurntSushi/aho-corasick/commit/0be6fe43822aeb193d8894fd1400c8b843320ef0
# https://github.com/rust-lang/rust/pull/90621#issuecomment-1068998914
aho-corasick = ">= 1.0, < 1.1.0"
const_format = "0.2.22"
owo-colors = "3.5.0"
wu-diff = "0.1.2"
@ -58,7 +70,7 @@ tree_magic_mini = "3.0.2"
# bumpalo 3.12 requires rust 1.60
bumpalo = "3.11.1"
unicode-width = "0.1.9"
crossterm = { version = "0.26.1", features = [] }
crossterm = { version = "0.27.0", features = [] }
glob = "0.3.1"
strum = { version = "0.25", features = ["derive"] }
# hashbrown 0.13 requires rust 1.61
@ -66,7 +78,7 @@ hashbrown = "0.12.3"
humansize = "2.1.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
line-numbers = "0.2.2"
line-numbers = "0.3.0"
[dev-dependencies]
# assert_cmd 2.0.6 requires rust 1.60

@ -19,11 +19,6 @@ struct TreeSitterParser {
impl TreeSitterParser {
fn build(&self) {
// In rustc 1.61+, we need to specify +whole-archive.
// See https://github.com/rust-lang/rust/blob/1.61.0/RELEASES.md#compatibility-notes
// and https://github.com/Wilfred/difftastic/issues/339.
let rustc_supports_whole_archive = !rustc::is_max_version("1.60.0").unwrap_or(false);
let dir = PathBuf::from(&self.src_dir);
let mut c_files = vec!["parser.c"];
@ -54,16 +49,13 @@ impl TreeSitterParser {
.flag_if_supported("-Wno-unused-but-set-variable")
// Workaround for: https://github.com/ganezdragon/tree-sitter-perl/issues/16
// should be removed after fixed.
.flag_if_supported("-Wno-return-type");
.flag_if_supported("-Wno-return-type")
.link_lib_modifier("+whole-archive");
for file in cpp_files {
cpp_build.file(dir.join(file));
}
if rustc_supports_whole_archive {
cpp_build.link_lib_modifier("+whole-archive");
}
cpp_build.compile(&format!("{}-cpp", self.name));
}
@ -76,9 +68,7 @@ impl TreeSitterParser {
build.file(dir.join(file));
}
if rustc_supports_whole_archive {
build.link_lib_modifier("+whole-archive");
}
build.link_lib_modifier("+whole-archive");
build.compile(self.name);
}
@ -311,6 +301,11 @@ fn main() {
src_dir: "vendored_parsers/tree-sitter-scala-src",
extra_files: vec!["scanner.c"],
},
TreeSitterParser {
name: "tree-sitter-scss",
src_dir: "vendored_parsers/tree-sitter-scss-src",
extra_files: vec!["scanner.c"],
},
TreeSitterParser {
name: "tree-sitter-sql",
src_dir: "vendored_parsers/tree-sitter-sql-src",
@ -370,6 +365,10 @@ fn main() {
parsers.par_iter().for_each(|p| p.build());
commit_info();
if let Some((version, _, _)) = rustc::triple() {
println!("cargo:rustc-env=DFT_RUSTC_VERSION={}", version);
}
}
fn commit_info() {
@ -390,7 +389,7 @@ fn commit_info() {
let stdout = String::from_utf8(output.stdout).unwrap();
let mut parts = stdout.split_whitespace();
let mut next = || parts.next().unwrap();
println!("cargo:rustc-env=DFT_COMMIT_HASH={}", next());
let _commit_hash = next();
println!("cargo:rustc-env=DFT_COMMIT_SHORT_HASH={}", next());
println!("cargo:rustc-env=DFT_COMMIT_DATE={}", next())
}

@ -28,7 +28,7 @@ $ brew install difftastic
Difftastic is written in Rust, so you will need Rust installed. I
recommend [rustup](https://rustup.rs/) to install Rust. Difftastic
requires Rust version 1.59 or later.
requires Rust version 1.63 or later.
You will also need a C++ compiler that supports C++14. If you're using
GCC, you need at least version 8.

@ -62,6 +62,7 @@ with `difft --list-languages`.
| JSON | [tree-sitter/tree-sitter-json](https://github.com/tree-sitter/tree-sitter-json) |
| LaTeX | [latex-lsp/tree-sitter-latex](https://github.com/latex-lsp/tree-sitter-latex) |
| Newick | [delehef/tree-sitter-newick](https://github.com/delehef/tree-sitter-newick) |
| SCSS | [serenadeai/tree-sitter-scss](https://github.com/serenadeai/tree-sitter-scss) |
| TOML | [ikatyang/tree-sitter-toml](https://github.com/ikatyang/tree-sitter-toml) |
| XML | [ObserverOfTime/tree-sitter-xml](https://github.com/ObserverOfTime/tree-sitter-xml) |
| YAML | [ikatyang/tree-sitter-yaml](https://github.com/ikatyang/tree-sitter-yaml) |

@ -1,4 +1,4 @@
[toolchain]
channel = "1.59"
channel = "1.63"
components = ["rustfmt"]
profile = "minimal"

@ -0,0 +1,3 @@
before
x
after

@ -0,0 +1,4 @@
before
foo x
y
after

@ -1,5 +1,5 @@
sample_files/Session_before.kt sample_files/Session_after.kt
7b0c38d1fcc9b4e20c5fcf0070cb1620 -
80c53658ee849d38f397863803677a02 -
sample_files/ada_before.adb sample_files/ada_after.adb
626e22f4f256ae591ece3f280176b3f7 -
@ -10,6 +10,9 @@ sample_files/added_line_before.txt sample_files/added_line_after.txt
sample_files/apex_before.cls sample_files/apex_after.cls
fde5c2cb739fb8bb42a592f64190137e -
sample_files/align_footer_before.txt sample_files/align_footer_after.txt
202e1821f9214f74568578dbc12ca83e -
sample_files/b2_math_before.h sample_files/b2_math_after.h
e1391702d8059d127110017bf6e9ef85 -
@ -94,6 +97,9 @@ da6c8d397a9d3992b494d761d082366c -
sample_files/if_before.py sample_files/if_after.py
4b25e3a7ae97516afa1064984754abe7 -
sample_files/insert_blank_before.txt sample_files/insert_blank_after.txt
a985e0bd8d728d59f71621de35552bed -
sample_files/janet_before.janet sample_files/janet_after.janet
e02b243bbcce07d5b7cd68954bd5344d -
@ -154,6 +160,9 @@ sample_files/newick_before.nwk sample_files/newick_after.nwk
sample_files/nix_before.nix sample_files/nix_after.nix
e00b95a4cf3fa3edf994155d8656063f -
sample_files/nullable_before.kt sample_files/nullable_after.kt
66da628a2c20e18059b8669aaa14a163 -
sample_files/ocaml_before.ml sample_files/ocaml_after.ml
2113c6c7959b8099f678d13953f7f44a -
@ -190,6 +199,9 @@ sample_files/scala_before.scala sample_files/scala_after.scala
sample_files/simple_before.js sample_files/simple_after.js
c9c5d1c61b7e8a953a361343bcd13c95 -
sample_files/simple_before.scss sample_files/simple_after.scss
276a637404ace5617aadd09751cc0b07 -
sample_files/simple_before.txt sample_files/simple_after.txt
8b88ed3d752ca7ae72ed2a44e3cfc9df -
@ -232,6 +244,9 @@ sample_files/todomvc_before.gleam sample_files/todomvc_after.gleam
sample_files/toml_before.toml sample_files/toml_after.toml
34fbe2ec9ff514cd53edfe8d961d5093 -
sample_files/trailling_newline_before.yaml sample_files/trailling_newline_after.yaml
6c2b9be2f28bac9d242f5145e4732c43 -
sample_files/typescript_before.ts sample_files/typescript_after.ts
27bc9e394d8119c7dd208a9d7985681e -

@ -0,0 +1,3 @@
class Foo {
val str: String
}

@ -0,0 +1,3 @@
class Foo {
val str: String?
}

@ -0,0 +1,28 @@
@mixin buttons($basicBorder:1px, $gradient1:#333, $gradient2:#d8dee7){
button{
border:$basicBorder dotted #acbed3;
//brings in Compass' background-image mixin: http://compass-style.org/reference/compass/css3/images/
@include background-image(linear-gradient($gradient1, $gradient2));
padding:3px 14px;
font-size:1rem;
color:#3b557d;
//brings in Compass' border-radius mixin: http://compass-style.org/reference/compass/css3/border_radius/
@include border-radius($border-radius, $border-radius);
cursor:pointer;
//& attribute adds
&.primary {
border:2px dotted #3b557d;
padding:5px 15px;
//requires a $border-radius variable
@include border-radius($border-radius + 2, $border-radius + 2);
}
&.disabled {
opacity: .6;
}
&:hover {
@include background-image(linear-gradient($gradient2, $gradient1));
}
}
}

@ -0,0 +1,28 @@
@mixin buttons($basicBorder:1px, $gradient1:#fff, $gradient2:#d8dee7){
button{
border:$basicBorder solid #acbed3;
//brings in Compass' background-image mixin: http://compass-style.org/reference/compass/css3/images/
@include background-image(linear-gradient($gradient1, $gradient2));
padding:3px 14px;
font-size:12px;
color:#3b557d;
//brings in Compass' border-radius mixin: http://compass-style.org/reference/compass/css3/border_radius/
@include border-radius($border-radius, $border-radius);
cursor:pointer;
//& attribute adds
&.primary {
border:2px solid #3b557d;
padding:5px 15px;
//requires a $border-radius variable
@include border-radius($border-radius + 2, $border-radius + 2);
}
&.disabled {
opacity: .8;
}
&:hover {
@include background-image(linear-gradient($gradient2, $gradient1));
}
}
}

@ -0,0 +1,2 @@
foo: |
${{ BAR }}

@ -12,22 +12,22 @@ enum ConflictState {
Right,
}
pub const START_LHS_MARKER: &str = "<<<<<<<";
pub(crate) const START_LHS_MARKER: &str = "<<<<<<<";
const START_BASE_MARKER: &str = "|||||||";
const START_RHS_MARKER: &str = "=======";
const END_RHS_MARKER: &str = ">>>>>>>";
pub struct ConflictFiles {
pub lhs_name: Option<String>,
pub lhs_content: String,
pub rhs_name: Option<String>,
pub rhs_content: String,
pub num_conflicts: usize,
pub(crate) struct ConflictFiles {
pub(crate) lhs_name: Option<String>,
pub(crate) lhs_content: String,
pub(crate) rhs_name: Option<String>,
pub(crate) rhs_content: String,
pub(crate) num_conflicts: usize,
}
/// Convert a string with conflict markers into the two conflicting
/// file contents.
pub fn apply_conflict_markers(s: &str) -> Result<ConflictFiles, String> {
pub(crate) fn apply_conflict_markers(s: &str) -> Result<ConflictFiles, String> {
let mut lhs_name: Option<String> = None;
let mut rhs_name: Option<String> = None;

@ -1,5 +1,5 @@
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Side {
pub(crate) enum Side {
/// The left-hand side, or the 'before' file. Often abbreviated to
/// LHS.
Left,

@ -6,7 +6,7 @@ use crate::{
};
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum ChangeKind<'a> {
pub(crate) enum ChangeKind<'a> {
Unchanged(&'a Syntax<'a>),
ReplacedComment(&'a Syntax<'a>, &'a Syntax<'a>),
ReplacedString(&'a Syntax<'a>, &'a Syntax<'a>),
@ -14,21 +14,21 @@ pub enum ChangeKind<'a> {
}
#[derive(Debug, Default)]
pub struct ChangeMap<'a> {
pub(crate) struct ChangeMap<'a> {
changes: DftHashMap<SyntaxId, ChangeKind<'a>>,
}
impl<'a> ChangeMap<'a> {
pub fn insert(&mut self, node: &'a Syntax<'a>, ck: ChangeKind<'a>) {
pub(crate) fn insert(&mut self, node: &'a Syntax<'a>, ck: ChangeKind<'a>) {
self.changes.insert(node.id(), ck);
}
pub fn get(&self, node: &Syntax<'a>) -> Option<ChangeKind<'a>> {
pub(crate) fn get(&self, node: &Syntax<'a>) -> Option<ChangeKind<'a>> {
self.changes.get(&node.id()).copied()
}
}
pub fn insert_deep_unchanged<'a>(
pub(crate) fn insert_deep_unchanged<'a>(
node: &'a Syntax<'a>,
opposite_node: &'a Syntax<'a>,
change_map: &mut ChangeMap<'a>,
@ -55,7 +55,7 @@ pub fn insert_deep_unchanged<'a>(
}
}
pub fn insert_deep_novel<'a>(node: &'a Syntax<'a>, change_map: &mut ChangeMap<'a>) {
pub(crate) fn insert_deep_novel<'a>(node: &'a Syntax<'a>, change_map: &mut ChangeMap<'a>) {
change_map.insert(node, ChangeKind::Novel);
if let Syntax::List { children, .. } = node {

@ -15,7 +15,7 @@ use crate::{
};
#[derive(Debug)]
pub struct ExceededGraphLimit {}
pub(crate) struct ExceededGraphLimit {}
/// Return the shortest route from `start` to the end vertex.
fn shortest_vertex_path<'s, 'b>(
@ -59,8 +59,8 @@ fn shortest_vertex_path<'s, 'b>(
if seen.len() > graph_limit {
info!(
"Reached graph limit, arena consumed {} bytes",
vertex_arena.allocated_bytes(),
"Reached graph limit, arena consumed {}",
humansize::format_size(vertex_arena.allocated_bytes(), humansize::BINARY),
);
return Err(ExceededGraphLimit {});
}
@ -70,10 +70,10 @@ fn shortest_vertex_path<'s, 'b>(
};
info!(
"Saw {} vertices (a Vertex is {} bytes), arena consumed {} bytes, with {} vertices left on heap.",
"Saw {} vertices (a Vertex is {} bytes), arena consumed {}, with {} vertices left on heap.",
seen.len(),
std::mem::size_of::<Vertex>(),
vertex_arena.allocated_bytes(),
humansize::format_size(vertex_arena.allocated_bytes(), humansize::BINARY),
heap.len(),
);
@ -186,7 +186,7 @@ fn tree_count(root: Option<&Syntax>) -> u32 {
count
}
pub fn mark_syntax<'a>(
pub(crate) fn mark_syntax<'a>(
lhs_syntax: Option<&'a Syntax<'a>>,
rhs_syntax: Option<&'a Syntax<'a>>,
change_map: &mut ChangeMap<'a>,

@ -50,13 +50,13 @@ use crate::{
/// ^ ^
/// ```
#[derive(Debug, Clone)]
pub struct Vertex<'s, 'b> {
pub neighbours: RefCell<Option<Vec<(Edge, &'b Vertex<'s, 'b>)>>>,
pub predecessor: Cell<Option<(u32, &'b Vertex<'s, 'b>)>>,
pub(crate) struct Vertex<'s, 'b> {
pub(crate) neighbours: RefCell<Option<Vec<(Edge, &'b Vertex<'s, 'b>)>>>,
pub(crate) predecessor: Cell<Option<(u32, &'b Vertex<'s, 'b>)>>,
// TODO: experiment with storing SyntaxId only, and have a HashMap
// from SyntaxId to &Syntax.
pub lhs_syntax: Option<&'s Syntax<'s>>,
pub rhs_syntax: Option<&'s Syntax<'s>>,
pub(crate) lhs_syntax: Option<&'s Syntax<'s>>,
pub(crate) rhs_syntax: Option<&'s Syntax<'s>>,
parents: Stack<EnteredDelimiter<'s>>,
lhs_parent_id: Option<SyntaxId>,
rhs_parent_id: Option<SyntaxId>,
@ -249,11 +249,14 @@ fn push_rhs_delimiter<'s>(
}
impl<'s, 'b> Vertex<'s, 'b> {
pub fn is_end(&self) -> bool {
pub(crate) fn is_end(&self) -> bool {
self.lhs_syntax.is_none() && self.rhs_syntax.is_none() && self.parents.is_empty()
}
pub fn new(lhs_syntax: Option<&'s Syntax<'s>>, rhs_syntax: Option<&'s Syntax<'s>>) -> Self {
pub(crate) fn new(
lhs_syntax: Option<&'s Syntax<'s>>,
rhs_syntax: Option<&'s Syntax<'s>>,
) -> Self {
let parents = Stack::new();
Vertex {
neighbours: RefCell::new(None),
@ -275,9 +278,12 @@ impl<'s, 'b> Vertex<'s, 'b> {
///
/// See [`set_neighbours`] for all the edges available for a given `Vertex`.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Edge {
pub(crate) enum Edge {
UnchangedNode {
depth_difference: u32,
/// Is this node just punctuation? We penalise this case,
/// because it's more useful to match e.g. a variable name
/// than a comma.
probably_punctuation: bool,
},
EnterUnchangedDelimiter {
@ -298,7 +304,7 @@ pub enum Edge {
}
impl Edge {
pub fn cost(self) -> u32 {
pub(crate) fn cost(self) -> u32 {
match self {
// Matching nodes is always best.
UnchangedNode {
@ -475,7 +481,7 @@ fn pop_all_parents<'s>(
/// Compute the neighbours of `v` if we haven't previously done so,
/// and write them to the .neighbours cell inside `v`.
pub fn set_neighbours<'s, 'b>(
pub(crate) fn set_neighbours<'s, 'b>(
v: &Vertex<'s, 'b>,
alloc: &'b Bump,
seen: &mut DftHashMap<&Vertex<'s, 'b>, Vec<&'b Vertex<'s, 'b>>>,
@ -485,7 +491,7 @@ pub fn set_neighbours<'s, 'b>(
}
// There are only seven pushes in this function, so that's sufficient.
let mut res: Vec<(Edge, &Vertex)> = Vec::with_capacity(7);
let mut neighbours: Vec<(Edge, &Vertex)> = Vec::with_capacity(7);
if let (Some(lhs_syntax), Some(rhs_syntax)) = (&v.lhs_syntax, &v.rhs_syntax) {
if lhs_syntax == rhs_syntax {
@ -504,7 +510,7 @@ pub fn set_neighbours<'s, 'b>(
&v.parents,
);
res.push((
neighbours.push((
UnchangedNode {
depth_difference,
probably_punctuation,
@ -563,7 +569,7 @@ pub fn set_neighbours<'s, 'b>(
&parents_next,
);
res.push((
neighbours.push((
EnterUnchangedDelimiter { depth_difference },
allocate_if_new(
Vertex {
@ -614,7 +620,7 @@ pub fn set_neighbours<'s, 'b>(
v.rhs_parent_id,
&v.parents,
);
res.push((
neighbours.push((
edge,
allocate_if_new(
Vertex {
@ -647,7 +653,7 @@ pub fn set_neighbours<'s, 'b>(
&v.parents,
);
res.push((
neighbours.push((
NovelAtomLHS {},
allocate_if_new(
Vertex {
@ -679,7 +685,7 @@ pub fn set_neighbours<'s, 'b>(
&parents_next,
);
res.push((
neighbours.push((
EnterNovelDelimiterLHS {},
allocate_if_new(
Vertex {
@ -712,7 +718,7 @@ pub fn set_neighbours<'s, 'b>(
&v.parents,
);
res.push((
neighbours.push((
NovelAtomRHS {},
allocate_if_new(
Vertex {
@ -743,7 +749,7 @@ pub fn set_neighbours<'s, 'b>(
&parents_next,
);
res.push((
neighbours.push((
EnterNovelDelimiterRHS {},
allocate_if_new(
Vertex {
@ -763,14 +769,14 @@ pub fn set_neighbours<'s, 'b>(
}
}
assert!(
!res.is_empty(),
!neighbours.is_empty(),
"Must always find some next steps if node is not the end"
);
v.neighbours.replace(Some(res));
v.neighbours.replace(Some(neighbours));
}
pub fn populate_change_map<'s, 'b>(
pub(crate) fn populate_change_map<'s, 'b>(
route: &[(Edge, &'b Vertex<'s, 'b>)],
change_map: &mut ChangeMap<'s>,
) {

@ -1,7 +1,7 @@
pub mod changes;
pub mod dijkstra;
pub(crate) mod changes;
pub(crate) mod dijkstra;
mod graph;
pub mod myers_diff;
pub mod sliders;
pub(crate) mod myers_diff;
pub(crate) mod sliders;
mod stack;
pub mod unchanged;
pub(crate) mod unchanged;

@ -7,7 +7,7 @@ use rustc_hash::FxHashSet;
use crate::hash::DftHashMap;
#[derive(Debug, PartialEq)]
pub enum DiffResult<T> {
pub(crate) enum DiffResult<T> {
Left(T),
Both(T, T),
Right(T),
@ -15,7 +15,10 @@ pub enum DiffResult<T> {
/// Compute a linear diff between `lhs` and `rhs`. This is the
/// traditional Myer's diff algorithm.
pub fn slice<'a, T: PartialEq + Clone>(lhs: &'a [T], rhs: &'a [T]) -> Vec<DiffResult<&'a T>> {
pub(crate) fn slice<'a, T: PartialEq + Clone>(
lhs: &'a [T],
rhs: &'a [T],
) -> Vec<DiffResult<&'a T>> {
wu_diff::diff(lhs, rhs)
.into_iter()
.map(|result| match result {
@ -35,7 +38,10 @@ pub fn slice<'a, T: PartialEq + Clone>(lhs: &'a [T], rhs: &'a [T]) -> Vec<DiffRe
///
/// This is faster when equality checks on `T` are expensive, such as
/// large strings.
pub fn slice_by_hash<'a, T: Eq + Hash>(lhs: &'a [T], rhs: &'a [T]) -> Vec<DiffResult<&'a T>> {
pub(crate) fn slice_by_hash<'a, T: Eq + Hash>(
lhs: &'a [T],
rhs: &'a [T],
) -> Vec<DiffResult<&'a T>> {
// Compute a unique numeric value for each item, use that for
// diffing, then return diff results in terms of the original
// type.
@ -95,7 +101,7 @@ pub fn slice_by_hash<'a, T: Eq + Hash>(lhs: &'a [T], rhs: &'a [T]) -> Vec<DiffRe
///
/// (This heuristic is used in traditional diff tools too, such as GNU
/// diff.)
pub fn slice_unique_by_hash<'a, T: Eq + Clone + Hash>(
pub(crate) fn slice_unique_by_hash<'a, T: Eq + Clone + Hash>(
lhs: &'a [T],
rhs: &'a [T],
) -> Vec<DiffResult<&'a T>> {

@ -37,7 +37,7 @@ use crate::{
parse::syntax::Syntax::{self, *},
};
pub fn fix_all_sliders<'a>(
pub(crate) fn fix_all_sliders<'a>(
language: guess_language::Language,
nodes: &[&'a Syntax<'a>],
change_map: &mut ChangeMap<'a>,

@ -41,13 +41,13 @@ impl<T> Stack<T> {
// O(n)
pub(crate) fn size(&self) -> usize {
let mut res = 0;
let mut count = 0;
let mut node = &self.head;
while let Some(next) = node {
res += 1;
count += 1;
node = &next.next;
}
res
count
}
pub(crate) fn is_empty(&self) -> bool {

@ -11,23 +11,23 @@ use crate::parse::syntax::Syntax;
const TINY_TREE_THRESHOLD: u32 = 10;
const MOSTLY_UNCHANGED_MIN_COMMON_CHILDREN: usize = 4;
/// Set [`ChangeKind`] on nodes that are obviously unchanged, and return a
/// vec of pairs that need proper diffing.
pub fn mark_unchanged<'a>(
/// Set [`ChangeKind`] on nodes that have exactly the same structure
/// on both sides, and return a vec of pairs that need proper diffing.
pub(crate) fn mark_unchanged<'a>(
lhs_nodes: &[&'a Syntax<'a>],
rhs_nodes: &[&'a Syntax<'a>],
change_map: &mut ChangeMap<'a>,
) -> Vec<(Vec<&'a Syntax<'a>>, Vec<&'a Syntax<'a>>)> {
let (_, lhs_nodes, rhs_nodes) = shrink_unchanged_at_ends(lhs_nodes, rhs_nodes, change_map);
let mut res = vec![];
let mut nodes_to_diff = vec![];
for (lhs_nodes, rhs_nodes) in split_mostly_unchanged_toplevel(&lhs_nodes, &rhs_nodes) {
let (_, lhs_nodes, rhs_nodes) =
shrink_unchanged_at_ends(&lhs_nodes, &rhs_nodes, change_map);
res.extend(split_unchanged(&lhs_nodes, &rhs_nodes, change_map));
nodes_to_diff.extend(split_unchanged(&lhs_nodes, &rhs_nodes, change_map));
}
res
nodes_to_diff
}
#[derive(Debug)]

@ -11,7 +11,7 @@ use crate::{
parse::syntax::{zip_repeat_shorter, MatchKind, MatchedPos},
};
pub fn all_matched_lines_filled(
pub(crate) fn all_matched_lines_filled(
lhs_mps: &[MatchedPos],
rhs_mps: &[MatchedPos],
lhs_lines: &[&str],
@ -128,9 +128,9 @@ fn all_lines(mps: &[MatchedPos]) -> Vec<LineNumber> {
for mp in mps {
lines.insert(mp.pos.line);
}
let mut res: Vec<LineNumber> = lines.into_iter().collect();
res.sort_unstable();
res
let mut line_nums: Vec<LineNumber> = lines.into_iter().collect();
line_nums.sort_unstable();
line_nums
}
fn matched_lines_from_unchanged(
@ -327,7 +327,9 @@ fn match_preceding_blanks(
res
}
pub fn opposite_positions(mps: &[MatchedPos]) -> DftHashMap<LineNumber, HashSet<LineNumber>> {
pub(crate) fn opposite_positions(
mps: &[MatchedPos],
) -> DftHashMap<LineNumber, HashSet<LineNumber>> {
let mut res: DftHashMap<LineNumber, HashSet<LineNumber>> = DftHashMap::default();
for mp in mps {
@ -416,7 +418,7 @@ fn before_with_opposites(
}
fn pad_before(ln: LineNumber, num_context_lines: usize) -> Vec<LineNumber> {
let mut res = vec![];
let mut line_nums = vec![];
let mut current = ln;
// Use one more line than num_context_lines so we merge
@ -427,15 +429,15 @@ fn pad_before(ln: LineNumber, num_context_lines: usize) -> Vec<LineNumber> {
}
current = (current.0 - 1).into();
res.push(current);
line_nums.push(current);
}
res.reverse();
res
line_nums.reverse();
line_nums
}
fn pad_after(ln: LineNumber, max_line: LineNumber, num_context_lines: usize) -> Vec<LineNumber> {
let mut res = vec![];
let mut line_nums = vec![];
let mut current = ln;
// Use one more line than num_context_lines so we merge
@ -446,18 +448,18 @@ fn pad_after(ln: LineNumber, max_line: LineNumber, num_context_lines: usize) ->
}
current = (current.0 + 1).into();
res.push(current);
line_nums.push(current);
}
res
line_nums
}
pub fn flip_tuple<Tx: Copy, Ty: Copy>(pair: (Tx, Ty)) -> (Ty, Tx) {
pub(crate) fn flip_tuple<Tx: Copy, Ty: Copy>(pair: (Tx, Ty)) -> (Ty, Tx) {
let (x, y) = pair;
(y, x)
}
pub fn flip_tuples<Tx: Copy, Ty: Copy>(items: &[(Tx, Ty)]) -> Vec<(Ty, Tx)> {
pub(crate) fn flip_tuples<Tx: Copy, Ty: Copy>(items: &[(Tx, Ty)]) -> Vec<(Ty, Tx)> {
items.iter().copied().map(flip_tuple).collect()
}
@ -513,7 +515,7 @@ fn after_with_opposites(
res
}
pub fn calculate_before_context(
pub(crate) fn calculate_before_context(
lines: &[(Option<LineNumber>, Option<LineNumber>)],
opposite_to_lhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,
opposite_to_rhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,
@ -535,7 +537,7 @@ pub fn calculate_before_context(
}
}
pub fn calculate_after_context(
pub(crate) fn calculate_after_context(
lines: &[(Option<LineNumber>, Option<LineNumber>)],
opposite_to_lhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,
opposite_to_rhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,
@ -585,7 +587,7 @@ pub fn calculate_after_context(
}
}
pub fn add_context(
pub(crate) fn add_context(
lines: &[(Option<LineNumber>, Option<LineNumber>)],
opposite_to_lhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,
opposite_to_rhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,

@ -20,18 +20,18 @@ use crate::{
/// A hunk represents a series of modified lines that are displayed
/// together.
#[derive(Debug, Clone)]
pub struct Hunk {
pub(crate) struct Hunk {
/// The LHS line numbers that contain novel content.
pub novel_lhs: HashSet<LineNumber>,
pub(crate) novel_lhs: HashSet<LineNumber>,
/// The RHS line numbers that contain novel content.
pub novel_rhs: HashSet<LineNumber>,
pub(crate) novel_rhs: HashSet<LineNumber>,
/// Line pairs that contain modified lines. This does not include
/// padding, so at least one of the two lines has novel content.
pub lines: Vec<(Option<LineNumber>, Option<LineNumber>)>,
pub(crate) lines: Vec<(Option<LineNumber>, Option<LineNumber>)>,
}
impl Hunk {
pub fn merge(self, other: &Self) -> Self {
pub(crate) fn merge(self, other: &Self) -> Self {
let mut lines = self.lines;
lines.extend(other.lines.iter());
@ -130,7 +130,7 @@ fn extract_lines(hunk: &Hunk) -> Vec<(Option<LineNumber>, Option<LineNumber>)> {
relevant
}
pub fn merge_adjacent(
pub(crate) fn merge_adjacent(
hunks: &[Hunk],
opposite_to_lhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,
opposite_to_rhs: &DftHashMap<LineNumber, HashSet<LineNumber>>,
@ -138,7 +138,7 @@ pub fn merge_adjacent(
max_rhs_src_line: LineNumber,
num_context_lines: usize,
) -> Vec<Hunk> {
let mut res: Vec<Hunk> = vec![];
let mut merged_hunks: Vec<Hunk> = vec![];
let mut prev_hunk: Option<Hunk> = None;
let mut prev_lhs_lines: HashSet<LineNumber> = HashSet::new();
@ -171,7 +171,7 @@ pub fn merge_adjacent(
if lhs_lines.is_disjoint(&prev_lhs_lines) && rhs_lines.is_disjoint(&prev_rhs_lines)
{
// No overlaps, start a new hunk.
res.push(hunk_so_far.clone());
merged_hunks.push(hunk_so_far.clone());
prev_hunk = Some(hunk.clone());
prev_lhs_lines = lhs_lines;
@ -193,10 +193,10 @@ pub fn merge_adjacent(
}
if let Some(current_hunk) = prev_hunk {
res.push(current_hunk);
merged_hunks.push(current_hunk);
}
res
merged_hunks
}
fn lines_are_close(
@ -227,7 +227,7 @@ fn lines_are_close(
fn enforce_increasing(
lines: &[(Option<LineNumber>, Option<LineNumber>)],
) -> Vec<(Option<LineNumber>, Option<LineNumber>)> {
let mut res = vec![];
let mut ordered_lines = vec![];
let mut max_lhs_line: Option<LineNumber> = None;
let mut max_rhs_line: Option<LineNumber> = None;
@ -264,11 +264,11 @@ fn enforce_increasing(
}
if lhs_line.is_some() || rhs_line.is_some() {
res.push((lhs_line, rhs_line));
ordered_lines.push((lhs_line, rhs_line));
}
}
res
ordered_lines
}
fn find_novel_lines(
@ -599,7 +599,7 @@ fn matched_novel_lines(
lines
}
pub fn matched_pos_to_hunks(lhs_mps: &[MatchedPos], rhs_mps: &[MatchedPos]) -> Vec<Hunk> {
pub(crate) fn matched_pos_to_hunks(lhs_mps: &[MatchedPos], rhs_mps: &[MatchedPos]) -> Vec<Hunk> {
lines_to_hunks(&matched_novel_lines(lhs_mps, rhs_mps), lhs_mps, rhs_mps)
}
@ -623,7 +623,7 @@ fn either_side_equal(
false
}
pub fn matched_lines_indexes_for_hunk(
pub(crate) fn matched_lines_indexes_for_hunk(
matched_lines: &[(Option<LineNumber>, Option<LineNumber>)],
hunk: &Hunk,
num_context_lines: usize,

@ -11,7 +11,7 @@ use crate::{
summary::FileFormat,
};
pub fn print(
pub(crate) fn print(
lhs_src: &str,
rhs_src: &str,
display_options: &DisplayOptions,

@ -273,7 +273,7 @@ impl Highlight {
}
}
pub fn print_directory(diffs: Vec<DiffResult>) {
pub(crate) fn print_directory(diffs: Vec<DiffResult>) {
let files = diffs.iter().map(File::from).collect::<Vec<File>>();
println!(
"{}",
@ -281,7 +281,7 @@ pub fn print_directory(diffs: Vec<DiffResult>) {
);
}
pub fn print(diff: &DiffResult) {
pub(crate) fn print(diff: &DiffResult) {
let file = File::from(diff);
println!(
"{}",

@ -1,6 +1,6 @@
pub mod context;
pub mod hunks;
pub mod inline;
pub mod json;
pub mod side_by_side;
pub mod style;
pub(crate) mod context;
pub(crate) mod hunks;
pub(crate) mod inline;
pub(crate) mod json;
pub(crate) mod side_by_side;
pub(crate) mod style;

@ -18,7 +18,7 @@ use crate::{
BackgroundColor,
},
hash::DftHashMap,
lines::{codepoint_len, format_line_num},
lines::format_line_num,
options::{DisplayMode, DisplayOptions},
parse::syntax::{zip_pad_shorter, MatchedPos},
summary::FileFormat,
@ -76,7 +76,7 @@ fn display_single_column(
) -> Vec<String> {
let column_width = format_line_num((src_lines.len() as u32).into()).len();
let mut result = Vec::with_capacity(src_lines.len());
let mut formatted_lines = Vec::with_capacity(src_lines.len());
let mut header_line = String::new();
header_line.push_str(&style::header(
@ -88,7 +88,7 @@ fn display_single_column(
display_options,
));
header_line.push('\n');
result.push(header_line);
formatted_lines.push(header_line);
let mut style = Style::new();
if display_options.use_color {
@ -103,10 +103,10 @@ fn display_single_column(
.to_string(),
);
formatted_line.push_str(line);
result.push(formatted_line);
formatted_lines.push(formatted_line);
}
result
formatted_lines
}
fn display_line_nums(
@ -166,31 +166,16 @@ struct SourceDimensions {
}
impl SourceDimensions {
fn new(
terminal_width: usize,
line_nums: &[(Option<LineNumber>, Option<LineNumber>)],
lhs_lines: &[&str],
rhs_lines: &[&str],
) -> Self {
fn new(terminal_width: usize, line_nums: &[(Option<LineNumber>, Option<LineNumber>)]) -> Self {
let mut lhs_max_line: LineNumber = 1.into();
let mut rhs_max_line: LineNumber = 1.into();
let mut lhs_max_content = 1;
let mut rhs_max_content = 1;
for (lhs_line_num, rhs_line_num) in line_nums {
if let Some(lhs_line_num) = lhs_line_num {
lhs_max_line = max(lhs_max_line, *lhs_line_num);
lhs_max_content = max(
lhs_max_content,
codepoint_len(lhs_lines[lhs_line_num.as_usize()]),
);
}
if let Some(rhs_line_num) = rhs_line_num {
rhs_max_line = max(rhs_max_line, *rhs_line_num);
rhs_max_content = max(
rhs_max_content,
codepoint_len(rhs_lines[rhs_line_num.as_usize()]),
);
}
}
@ -231,7 +216,7 @@ impl SourceDimensions {
}
}
pub fn lines_with_novel(
pub(crate) fn lines_with_novel(
lhs_mps: &[MatchedPos],
rhs_mps: &[MatchedPos],
) -> (HashSet<LineNumber>, HashSet<LineNumber>) {
@ -317,7 +302,7 @@ fn highlight_as_novel(
false
}
pub fn print(
pub(crate) fn print(
hunks: &[Hunk],
display_options: &DisplayOptions,
display_path: &str,
@ -436,12 +421,7 @@ pub fn print(
let no_rhs_changes = hunk.novel_rhs.is_empty();
let same_lines = aligned_lines.iter().all(|(l, r)| l == r);
let source_dims = SourceDimensions::new(
display_options.display_width,
aligned_lines,
&lhs_lines,
&rhs_lines,
);
let source_dims = SourceDimensions::new(display_options.display_width, aligned_lines);
for (lhs_line_num, rhs_line_num) in aligned_lines {
let lhs_line_novel = highlight_as_novel(
*lhs_line_num,
@ -606,14 +586,7 @@ mod tests {
#[test]
fn test_width_calculations() {
let line_nums = [(Some(1.into()), Some(10.into()))];
let source_dims = SourceDimensions::new(
80,
&line_nums,
&"foo\nbar\n".lines().collect::<Vec<_>>(),
&"x\nx\nx\nx\nx\nx\nx\nx\nx\nx\nx\n"
.lines()
.collect::<Vec<_>>(),
);
let source_dims = SourceDimensions::new(80, &line_nums);
assert_eq!(source_dims.lhs_line_nums_width, 2);
assert_eq!(source_dims.rhs_line_nums_width, 3);
@ -627,8 +600,6 @@ mod tests {
(Some(0.into()), Some(0.into())),
(Some(1.into()), Some(1.into())),
],
&"foo\nbar\n".lines().collect::<Vec<_>>(),
&"fox\nbax\n".lines().collect::<Vec<_>>(),
);
assert_eq!(
@ -649,8 +620,6 @@ mod tests {
(Some(0.into()), Some(0.into())),
(Some(1.into()), Some(1.into())),
],
&"foo\nbar\n".lines().collect::<Vec<_>>(),
&"fox\nbax\n".lines().collect::<Vec<_>>(),
);
assert_eq!(

@ -18,13 +18,13 @@ use crate::{
};
#[derive(Clone, Copy, Debug)]
pub enum BackgroundColor {
pub(crate) enum BackgroundColor {
Dark,
Light,
}
impl BackgroundColor {
pub fn is_dark(self) -> bool {
pub(crate) fn is_dark(self) -> bool {
matches!(self, BackgroundColor::Dark)
}
}
@ -88,7 +88,7 @@ fn width_respecting_tabs(s: &str, tab_width: usize) -> usize {
/// split_string_by_width("fooba", 3) // vec![("foo", 0), ("ba", 1)]
/// ```
fn split_string_by_width(s: &str, max_width: usize, tab_width: usize) -> Vec<(&str, usize)> {
let mut res: Vec<(&str, usize)> = vec![];
let mut parts: Vec<(&str, usize)> = vec![];
let mut s = s;
while width_respecting_tabs(s, tab_width) > max_width {
@ -103,19 +103,19 @@ fn split_string_by_width(s: &str, max_width: usize, tab_width: usize) -> Vec<(&s
} else {
0
};
res.push((part, padding));
parts.push((part, padding));
}
if res.is_empty() || !s.is_empty() {
res.push((s, max_width - width_respecting_tabs(s, tab_width)));
if parts.is_empty() || !s.is_empty() {
parts.push((s, max_width - width_respecting_tabs(s, tab_width)));
}
res
parts
}
/// Return a copy of `src` with all the tab characters replaced by
/// `tab_width` strings.
pub fn replace_tabs(src: &str, tab_width: usize) -> String {
pub(crate) fn replace_tabs(src: &str, tab_width: usize) -> String {
let tab_as_spaces = " ".repeat(tab_width);
src.replace('\t', &tab_as_spaces)
}
@ -123,7 +123,7 @@ pub fn replace_tabs(src: &str, tab_width: usize) -> String {
/// Split `line` (from the source code) into multiple lines of
/// `max_len` (i.e. word wrapping), and apply `styles` to each part
/// according to its original position in `line`.
pub fn split_and_apply(
pub(crate) fn split_and_apply(
line: &str,
max_len: usize,
tab_width: usize,
@ -147,13 +147,13 @@ pub fn split_and_apply(
.map(|(part, pad)| {
let part = replace_tabs(part, tab_width);
let mut res = String::with_capacity(part.len() + pad);
res.push_str(&part);
let mut parts = String::with_capacity(part.len() + pad);
parts.push_str(&part);
if matches!(side, Side::Left) {
res.push_str(&" ".repeat(pad));
parts.push_str(&" ".repeat(pad));
}
res
parts
})
.collect();
}
@ -230,7 +230,7 @@ pub fn split_and_apply(
/// specified.
fn apply_line(line: &str, styles: &[(SingleLineSpan, Style)]) -> String {
let line_bytes = byte_len(line);
let mut res = String::with_capacity(line.len());
let mut styled_line = String::with_capacity(line.len());
let mut i = 0;
for (span, style) in styles {
let start_col = span.start_col as usize;
@ -244,21 +244,21 @@ fn apply_line(line: &str, styles: &[(SingleLineSpan, Style)]) -> String {
// Unstyled text before the next span.
if i < start_col {
res.push_str(substring_by_byte(line, i, start_col));
styled_line.push_str(substring_by_byte(line, i, start_col));
}
// Apply style to the substring in this span.
let span_s = substring_by_byte(line, start_col, min(line_bytes, end_col));
res.push_str(&span_s.style(*style).to_string());
styled_line.push_str(&span_s.style(*style).to_string());
i = end_col;
}
// Unstyled text after the last span.
if i < line_bytes {
let span_s = substring_by_byte(line, i, line_bytes);
res.push_str(span_s);
styled_line.push_str(span_s);
}
res
styled_line
}
fn group_by_line(
@ -283,7 +283,7 @@ fn group_by_line(
fn style_lines(lines: &[&str], styles: &[(SingleLineSpan, Style)]) -> Vec<String> {
let mut ranges_by_line = group_by_line(styles);
let mut res = Vec::with_capacity(lines.len());
let mut styled_lines = Vec::with_capacity(lines.len());
for (i, line) in lines.iter().enumerate() {
let mut styled_line = String::with_capacity(line.len());
let ranges = ranges_by_line
@ -292,12 +292,12 @@ fn style_lines(lines: &[&str], styles: &[(SingleLineSpan, Style)]) -> Vec<String
styled_line.push_str(&apply_line(line, &ranges));
styled_line.push('\n');
res.push(styled_line);
styled_lines.push(styled_line);
}
res
styled_lines
}
pub fn novel_style(style: Style, side: Side, background: BackgroundColor) -> Style {
pub(crate) fn novel_style(style: Style, side: Side, background: BackgroundColor) -> Style {
if background.is_dark() {
match side {
Side::Left => style.bright_red(),
@ -311,7 +311,7 @@ pub fn novel_style(style: Style, side: Side, background: BackgroundColor) -> Sty
}
}
pub fn color_positions(
pub(crate) fn color_positions(
side: Side,
background: BackgroundColor,
syntax_highlight: bool,
@ -392,7 +392,7 @@ pub fn color_positions(
styles
}
pub fn apply_colors(
pub(crate) fn apply_colors(
s: &str,
side: Side,
syntax_highlight: bool,
@ -454,7 +454,7 @@ pub(crate) fn apply_line_number_color(
}
}
pub fn header(
pub(crate) fn header(
display_path: &str,
extra_info: Option<&String>,
hunk_num: usize,

@ -1,12 +1,12 @@
/// Successfully ran a diff, found no syntactic changes in text files
/// or byte changes in binary files.
pub const EXIT_SUCCESS: i32 = 0;
pub(crate) const EXIT_SUCCESS: i32 = 0;
/// Successfully ran a diff, found syntactic changes in text files or
/// byte changes in binary files.
pub const EXIT_FOUND_CHANGES: i32 = 1;
pub(crate) const EXIT_FOUND_CHANGES: i32 = 1;
/// Invalid arguments given to difftastic. This could be usage errors
/// (e.g. invalid numbers of arguments) or invalid paths (e.g. files
/// we don't have permission to read).
pub const EXIT_BAD_ARGUMENTS: i32 = 2;
pub(crate) const EXIT_BAD_ARGUMENTS: i32 = 2;

@ -7,13 +7,13 @@ use std::{
path::{Path, PathBuf},
};
use ignore::Walk;
use rustc_hash::FxHashSet;
use walkdir::WalkDir;
use crate::exit_codes::EXIT_BAD_ARGUMENTS;
use crate::options::FileArgument;
pub fn read_file_or_die(path: &FileArgument) -> Vec<u8> {
pub(crate) fn read_file_or_die(path: &FileArgument) -> Vec<u8> {
match read_file_arg(path) {
Ok(src) => src,
Err(e) => {
@ -23,7 +23,7 @@ pub fn read_file_or_die(path: &FileArgument) -> Vec<u8> {
}
}
pub fn read_files_or_die(
pub(crate) fn read_files_or_die(
lhs_path: &FileArgument,
rhs_path: &FileArgument,
missing_as_empty: bool,
@ -102,7 +102,7 @@ fn eprint_read_error(file_arg: &FileArgument, e: &std::io::Error) {
};
}
pub fn read_or_die(path: &Path) -> Vec<u8> {
pub(crate) fn read_or_die(path: &Path) -> Vec<u8> {
match fs::read(path) {
Ok(src) => src,
Err(e) => {
@ -139,13 +139,13 @@ fn u16_from_bytes(bytes: &[u8]) -> Vec<u16> {
}
#[derive(Debug, Eq, PartialEq)]
pub enum ProbableFileKind {
pub(crate) enum ProbableFileKind {
Text(String),
Binary,
}
/// Do these bytes look like a binary (non-textual) format?
pub fn guess_content(bytes: &[u8]) -> ProbableFileKind {
pub(crate) fn guess_content(bytes: &[u8]) -> ProbableFileKind {
// If the bytes are entirely valid UTF-8, treat them as a string.
if let Ok(valid_utf8_string) = std::str::from_utf8(bytes) {
return ProbableFileKind::Text(valid_utf8_string.to_string());
@ -235,10 +235,10 @@ pub fn guess_content(bytes: &[u8]) -> ProbableFileKind {
/// All the files in `dir`, including subdirectories.
fn relative_file_paths_in_dir(dir: &Path) -> Vec<PathBuf> {
WalkDir::new(dir)
Walk::new(dir)
.into_iter()
.filter_map(Result::ok)
.map(|entry| entry.into_path())
.map(|entry| Path::new(entry.path()).to_owned())
.filter(|path| !path.is_dir())
.map(|path| path.strip_prefix(dir).unwrap().to_path_buf())
.collect()
@ -248,12 +248,12 @@ fn relative_file_paths_in_dir(dir: &Path) -> Vec<PathBuf> {
/// that occur in at least one directory.
///
/// Attempts to preserve the ordering of files in both directories.
pub fn relative_paths_in_either(lhs_dir: &Path, rhs_dir: &Path) -> Vec<PathBuf> {
pub(crate) fn relative_paths_in_either(lhs_dir: &Path, rhs_dir: &Path) -> Vec<PathBuf> {
let lhs_paths = relative_file_paths_in_dir(lhs_dir);
let rhs_paths = relative_file_paths_in_dir(rhs_dir);
let mut seen = FxHashSet::default();
let mut res: Vec<PathBuf> = vec![];
let mut paths: Vec<PathBuf> = vec![];
let mut i = 0;
let mut j = 0;
@ -264,7 +264,7 @@ pub fn relative_paths_in_either(lhs_dir: &Path, rhs_dir: &Path) -> Vec<PathBuf>
if !seen.contains(lhs_path) {
// It should be impossible to get duplicates, but
// be defensive.
res.push(lhs_path.clone());
paths.push(lhs_path.clone());
seen.insert(lhs_path);
}
@ -277,8 +277,8 @@ pub fn relative_paths_in_either(lhs_dir: &Path, rhs_dir: &Path) -> Vec<PathBuf>
} else if seen.contains(rhs_path) {
j += 1;
} else {
res.push(lhs_path.clone());
res.push(rhs_path.clone());
paths.push(lhs_path.clone());
paths.push(rhs_path.clone());
seen.insert(lhs_path);
seen.insert(rhs_path);
@ -291,10 +291,10 @@ pub fn relative_paths_in_either(lhs_dir: &Path, rhs_dir: &Path) -> Vec<PathBuf>
}
}
res.extend(lhs_paths.into_iter().skip(i));
res.extend(rhs_paths.into_iter().skip(j));
paths.extend(lhs_paths.into_iter().skip(i));
paths.extend(rhs_paths.into_iter().skip(j));
res
paths
}
#[cfg(test)]

@ -9,4 +9,4 @@ use rustc_hash::FxHasher;
/// benchmarks) in a hashbrown::HashMap rather than std HashMap is a
/// little faster, and it also allows us to use the entry_ref API
/// which is unavailable in stable Rust.
pub type DftHashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<FxHasher>>;
pub(crate) type DftHashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<FxHasher>>;

@ -4,9 +4,10 @@ use lazy_static::lazy_static;
use line_numbers::LinePositions;
use regex::Regex;
use crate::words::split_words;
use crate::{
diff::myers_diff,
parse::syntax::{split_words, AtomKind, MatchKind, MatchedPos, TokenKind},
parse::syntax::{AtomKind, MatchKind, MatchedPos, TokenKind},
};
fn split_lines_keep_newline(s: &str) -> Vec<&str> {
@ -15,17 +16,17 @@ fn split_lines_keep_newline(s: &str) -> Vec<&str> {
}
let mut offset = 0;
let mut res = vec![];
let mut lines = vec![];
for newline_match in NEWLINE_RE.find_iter(s) {
res.push(s[offset..newline_match.end()].into());
lines.push(s[offset..newline_match.end()].into());
offset = newline_match.end();
}
if offset < s.len() {
res.push(s[offset..].into());
lines.push(s[offset..].into());
}
res
lines
}
#[derive(Debug)]
@ -105,7 +106,7 @@ fn line_len_in_bytes(line: &str) -> usize {
}
// TODO: Prefer src/opposite_src nomenclature as this function is called from both sides.
pub fn change_positions(lhs_src: &str, rhs_src: &str) -> Vec<MatchedPos> {
pub(crate) fn change_positions(lhs_src: &str, rhs_src: &str) -> Vec<MatchedPos> {
// TODO: If either side is "", don't split each line by words
// pointlessly. This is common for file additions/removals.
let lhs_lp = LinePositions::from(lhs_src);
@ -114,17 +115,17 @@ pub fn change_positions(lhs_src: &str, rhs_src: &str) -> Vec<MatchedPos> {
let mut lhs_offset = 0;
let mut rhs_offset = 0;
let mut res = vec![];
let mut mps = vec![];
for (kind, lhs_lines, rhs_lines) in changed_parts(lhs_src, rhs_src) {
match kind {
TextChangeKind::Unchanged => {
for (lhs_line, rhs_line) in lhs_lines.iter().zip(rhs_lines) {
let lhs_pos =
lhs_lp.from_offsets(lhs_offset, lhs_offset + line_len_in_bytes(lhs_line));
lhs_lp.from_region(lhs_offset, lhs_offset + line_len_in_bytes(lhs_line));
let rhs_pos =
rhs_lp.from_offsets(rhs_offset, rhs_offset + line_len_in_bytes(rhs_line));
rhs_lp.from_region(rhs_offset, rhs_offset + line_len_in_bytes(rhs_line));
res.push(MatchedPos {
mps.push(MatchedPos {
kind: MatchKind::UnchangedToken {
highlight: TokenKind::Atom(AtomKind::Normal),
self_pos: lhs_pos.clone(),
@ -147,27 +148,25 @@ pub fn change_positions(lhs_src: &str, rhs_src: &str) -> Vec<MatchedPos> {
) {
match diff_res {
myers_diff::DiffResult::Left(lhs_word) => {
if *lhs_word != "\n" {
let lhs_pos =
lhs_lp.from_offsets(lhs_offset, lhs_offset + lhs_word.len());
res.push(MatchedPos {
kind: MatchKind::NovelWord {
highlight: TokenKind::Atom(AtomKind::Normal),
},
pos: lhs_pos[0],
});
}
let lhs_pos =
lhs_lp.from_region(lhs_offset, lhs_offset + lhs_word.len());
mps.push(MatchedPos {
kind: MatchKind::NovelWord {
highlight: TokenKind::Atom(AtomKind::Normal),
},
pos: lhs_pos[0],
});
lhs_offset += lhs_word.len();
}
myers_diff::DiffResult::Both(lhs_word, rhs_word) => {
if *lhs_word != "\n" {
let lhs_pos =
lhs_lp.from_offsets(lhs_offset, lhs_offset + lhs_word.len());
lhs_lp.from_region(lhs_offset, lhs_offset + lhs_word.len());
let rhs_pos =
rhs_lp.from_offsets(rhs_offset, rhs_offset + rhs_word.len());
rhs_lp.from_region(rhs_offset, rhs_offset + rhs_word.len());
res.push(MatchedPos {
mps.push(MatchedPos {
kind: MatchKind::NovelLinePart {
highlight: TokenKind::Atom(AtomKind::Normal),
self_pos: lhs_pos[0],
@ -189,7 +188,7 @@ pub fn change_positions(lhs_src: &str, rhs_src: &str) -> Vec<MatchedPos> {
}
}
res
mps
}
#[cfg(test)]
@ -251,7 +250,7 @@ mod tests {
fn test_novel_lhs_trailing_newlines() {
let positions = change_positions("foo\n", "");
assert_eq!(positions.len(), 1);
assert_eq!(positions.len(), 2);
assert!(positions[0].kind.is_novel());
}

@ -4,7 +4,7 @@ use std::ops::Sub;
use line_numbers::LineNumber;
pub fn format_line_num(line_num: LineNumber) -> String {
pub(crate) fn format_line_num(line_num: LineNumber) -> String {
format!("{} ", line_num.display())
}
@ -12,25 +12,19 @@ pub fn format_line_num(line_num: LineNumber) -> String {
#[derive(Debug, PartialEq, Clone, Copy)]
struct LinePosition {
/// Both zero-indexed.
pub line: LineNumber,
pub(crate) line: LineNumber,
column: usize,
}
/// Return the length of `s` in codepoints. This is important when
/// finding character boundaries for slicing without errors.
pub fn codepoint_len(s: &str) -> usize {
s.chars().count()
}
/// Return the length of `s` in bytes.
///
/// This is a trivial wrapper to make it clear when we want bytes not
/// codepoints.
pub fn byte_len(s: &str) -> usize {
pub(crate) fn byte_len(s: &str) -> usize {
s.len()
}
pub trait MaxLine {
pub(crate) trait MaxLine {
fn max_line(&self) -> LineNumber;
}
@ -46,7 +40,7 @@ impl<S: AsRef<str>> MaxLine for S {
}
}
pub fn is_all_whitespace(s: &str) -> bool {
pub(crate) fn is_all_whitespace(s: &str) -> bool {
s.chars().all(|c| c.is_whitespace())
}
@ -80,11 +74,6 @@ mod tests {
assert_eq!(line.max_line().0, 1);
}
#[test]
fn codepoint_len_non_ascii() {
assert_eq!(codepoint_len("ƒoo"), 3);
}
#[test]
fn test_is_all_whiteapce() {
assert!(is_all_whitespace(" \n\t"));

@ -22,6 +22,9 @@
// implementation does not consider the mutable fields, so it is still
// correct.
#![allow(clippy::mutable_key_type)]
// Debugging features shouldn't be in checked-in code.
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
mod conflicts;
mod constants;
@ -36,6 +39,7 @@ mod options;
mod parse;
mod summary;
mod version;
mod words;
#[macro_use]
extern crate log;
@ -238,6 +242,8 @@ fn main() {
options::FileArgument::NamedPath(rhs_path),
) if lhs_path.is_dir() && rhs_path.is_dir() => {
let encountered_changes = encountered_changes.clone();
// Diffs in parallel when iterating this iterator.
let diff_iter = diff_directories(
lhs_path,
rhs_path,
@ -258,10 +264,20 @@ fn main() {
.collect();
display::json::print_directory(results);
} else if display_options.sort_paths {
let mut result: Vec<DiffResult> = diff_iter.collect();
result.sort_unstable_by(|a, b| a.display_path.cmp(&b.display_path));
for diff_result in result {
print_diff_result(&display_options, &diff_result);
if diff_result.has_reportable_change() {
encountered_changes.store(true, Ordering::Relaxed);
}
}
} else {
// We want to diff files in the directory in
// parallel, but print the results serially (to
// prevent display interleaving).
// parallel, but print the results serially
// (to prevent display interleaving).
// https://github.com/rayon-rs/rayon/issues/210#issuecomment-551319338
let (send, recv) = std::sync::mpsc::sync_channel(1);
@ -474,7 +490,7 @@ fn diff_file_content(
};
let language = guess(guess_path, guess_src, overrides);
let lang_config = language.map(|lang| (lang.clone(), tsp::from_language(lang)));
let lang_config = language.map(|lang| (lang, tsp::from_language(lang)));
if lhs_src == rhs_src {
let file_format = match language {
@ -482,7 +498,7 @@ fn diff_file_content(
None => FileFormat::PlainText,
};
// If the two files are completely identical, return early
// If the two files are byte-for-byte identical, return early
// rather than doing any more work.
return DiffResult {
extra_info,

@ -14,35 +14,35 @@ use crate::{
version::VERSION,
};
pub const DEFAULT_BYTE_LIMIT: usize = 1_000_000;
pub(crate) const DEFAULT_BYTE_LIMIT: usize = 1_000_000;
// Chosen experimentally: this is sufficiently many for all the sample
// files (the highest is slow_before/after.rs at 1.3M nodes), but
// small enough to terminate in ~5 seconds like the test file in #306.
pub const DEFAULT_GRAPH_LIMIT: usize = 3_000_000;
pub const DEFAULT_PARSE_ERROR_LIMIT: usize = 0;
pub(crate) const DEFAULT_GRAPH_LIMIT: usize = 3_000_000;
pub(crate) const DEFAULT_PARSE_ERROR_LIMIT: usize = 0;
pub const DEFAULT_TAB_WIDTH: usize = 8;
pub(crate) const DEFAULT_TAB_WIDTH: usize = 8;
const USAGE: &str = concat!(env!("CARGO_BIN_NAME"), " [OPTIONS] OLD-PATH NEW-PATH");
#[derive(Debug, Clone, Copy)]
pub enum ColorOutput {
pub(crate) enum ColorOutput {
Always,
Auto,
Never,
}
#[derive(Debug, Clone)]
pub struct DisplayOptions {
pub background_color: BackgroundColor,
pub use_color: bool,
pub display_mode: DisplayMode,
pub print_unchanged: bool,
pub tab_width: usize,
pub display_width: usize,
pub num_context_lines: u32,
pub in_vcs: bool,
pub syntax_highlight: bool,
pub(crate) struct DisplayOptions {
pub(crate) background_color: BackgroundColor,
pub(crate) use_color: bool,
pub(crate) display_mode: DisplayMode,
pub(crate) print_unchanged: bool,
pub(crate) tab_width: usize,
pub(crate) display_width: usize,
pub(crate) num_context_lines: u32,
pub(crate) syntax_highlight: bool,
pub(crate) sort_paths: bool,
}
impl Default for DisplayOptions {
@ -55,20 +55,20 @@ impl Default for DisplayOptions {
tab_width: 8,
display_width: 80,
num_context_lines: 3,
in_vcs: false,
syntax_highlight: true,
sort_paths: false,
}
}
}
#[derive(Debug, Clone)]
pub struct DiffOptions {
pub graph_limit: usize,
pub byte_limit: usize,
pub parse_error_limit: usize,
pub check_only: bool,
pub ignore_comments: bool,
pub strip_cr: bool,
pub(crate) struct DiffOptions {
pub(crate) graph_limit: usize,
pub(crate) byte_limit: usize,
pub(crate) parse_error_limit: usize,
pub(crate) check_only: bool,
pub(crate) ignore_comments: bool,
pub(crate) strip_cr: bool,
}
impl Default for DiffOptions {
@ -218,6 +218,7 @@ json: Output the results as a machine-readable JSON array with an element per fi
)
.arg(
Arg::new("skip-unchanged").long("skip-unchanged")
.env("DFT_SKIP_UNCHANGED")
.help("Don't display anything if a file is unchanged.")
)
.arg(
@ -288,11 +289,16 @@ When multiple overrides are specified, the first matching override wins."))
.hide(true)
.allow_invalid_utf8(true),
)
.arg(
Arg::new("sort-paths").long("sort-paths")
.env("DFT_SORT_PATHS")
.help("When diffing a directory, output the results sorted by path. This is slower.")
)
.arg_required_else_help(true)
}
#[derive(Debug, Copy, Clone)]
pub enum DisplayMode {
pub(crate) enum DisplayMode {
Inline,
SideBySide,
SideBySideShowBoth,
@ -300,7 +306,7 @@ pub enum DisplayMode {
}
#[derive(Eq, PartialEq, Debug)]
pub enum FileArgument {
pub(crate) enum FileArgument {
NamedPath(std::path::PathBuf),
Stdin,
DevNull,
@ -326,7 +332,7 @@ fn relative_to_current(path: &Path) -> PathBuf {
impl FileArgument {
/// Return a `FileArgument` representing this command line
/// argument.
pub fn from_cli_argument(arg: &OsStr) -> Self {
pub(crate) fn from_cli_argument(arg: &OsStr) -> Self {
if arg == "/dev/null" {
FileArgument::DevNull
} else if arg == "-" {
@ -338,7 +344,7 @@ impl FileArgument {
/// Return a `FileArgument` that always represents a path that
/// exists, with the exception of `/dev/null`, which is turned into [FileArgument::DevNull].
pub fn from_path_argument(arg: &OsStr) -> Self {
pub(crate) fn from_path_argument(arg: &OsStr) -> Self {
// For new and deleted files, Git passes `/dev/null` as the reference file.
if arg == "/dev/null" {
FileArgument::DevNull
@ -347,7 +353,7 @@ impl FileArgument {
}
}
pub fn display(&self) -> String {
pub(crate) fn display(&self) -> String {
match self {
FileArgument::NamedPath(path) => relative_to_current(path).display().to_string(),
FileArgument::Stdin => "(stdin)".to_string(),
@ -356,7 +362,7 @@ impl FileArgument {
}
}
pub enum Mode {
pub(crate) enum Mode {
Diff {
diff_options: DiffOptions,
display_options: DisplayOptions,
@ -443,7 +449,7 @@ fn build_display_path(lhs_path: &FileArgument, rhs_path: &FileArgument) -> Strin
}
fn parse_overrides_or_die(raw_overrides: &[String]) -> Vec<(LanguageOverride, Vec<glob::Pattern>)> {
let mut res: Vec<(LanguageOverride, Vec<glob::Pattern>)> = vec![];
let mut overrides: Vec<(LanguageOverride, Vec<glob::Pattern>)> = vec![];
let mut invalid_syntax = false;
for raw_override in raw_overrides {
@ -451,7 +457,7 @@ fn parse_overrides_or_die(raw_overrides: &[String]) -> Vec<(LanguageOverride, Ve
match glob::Pattern::new(glob_str) {
Ok(pattern) => {
if let Some(language_override) = language_override_from_name(lang_name) {
res.push((language_override, vec![pattern]));
overrides.push((language_override, vec![pattern]));
} else {
eprintln!("No such language '{}'", lang_name);
eprintln!("See --list-languages for the names of all languages available. Language overrides are case insensitive.");
@ -475,7 +481,7 @@ fn parse_overrides_or_die(raw_overrides: &[String]) -> Vec<(LanguageOverride, Ve
std::process::exit(EXIT_BAD_ARGUMENTS);
}
res.into_iter()
overrides.into_iter()
.coalesce(
|(prev_lang, mut prev_globs), (current_lang, current_globs)| {
if prev_lang == current_lang {
@ -490,7 +496,7 @@ fn parse_overrides_or_die(raw_overrides: &[String]) -> Vec<(LanguageOverride, Ve
}
/// Parse CLI arguments passed to the binary.
pub fn parse_args() -> Mode {
pub(crate) fn parse_args() -> Mode {
let matches = app().get_matches();
let color_output = match matches.value_of("color").expect("color has a default") {
@ -552,7 +558,7 @@ pub fn parse_args() -> Mode {
"side-by-side-show-both" => DisplayMode::SideBySideShowBoth,
"inline" => DisplayMode::Inline,
"json" => {
if env::var(format!("DFT_UNSTABLE")).is_err() {
if env::var("DFT_UNSTABLE").is_err() {
eprintln!("JSON output is an unstable feature and its format may change in future. To enable JSON output, set the environment variable DFT_UNSTABLE=yes.");
std::process::exit(EXIT_BAD_ARGUMENTS);
}
@ -575,6 +581,8 @@ pub fn parse_args() -> Mode {
let syntax_highlight = matches.value_of("syntax-highlight") == Some("on");
let sort_paths = matches.is_present("sort-paths");
let graph_limit = matches
.value_of("graph-limit")
.expect("Always present as we've given clap a default")
@ -626,12 +634,12 @@ pub fn parse_args() -> Mode {
info!("CLI arguments: {:?}", args);
// TODO: document these different ways of calling difftastic.
let (display_path, lhs_path, rhs_path, old_path, in_vcs) = match &args[..] {
let (display_path, lhs_path, rhs_path, old_path) = match &args[..] {
[lhs_path, rhs_path] => {
let lhs_arg = FileArgument::from_cli_argument(lhs_path);
let rhs_arg = FileArgument::from_cli_argument(rhs_path);
let display_path = build_display_path(&lhs_arg, &rhs_arg);
(display_path, lhs_arg, rhs_arg, None, false)
(display_path, lhs_arg, rhs_arg, None)
}
[display_path, lhs_tmp_file, _lhs_hash, _lhs_mode, rhs_tmp_file, _rhs_hash, _rhs_mode] => {
// https://git-scm.com/docs/git#Documentation/git.txt-codeGITEXTERNALDIFFcode
@ -640,7 +648,6 @@ pub fn parse_args() -> Mode {
FileArgument::from_path_argument(lhs_tmp_file),
FileArgument::from_path_argument(rhs_tmp_file),
None,
true,
)
}
[old_name, lhs_tmp_file, _lhs_hash, _lhs_mode, rhs_tmp_file, _rhs_hash, _rhs_mode, new_name, _similarity] =>
@ -657,7 +664,6 @@ pub fn parse_args() -> Mode {
FileArgument::from_path_argument(lhs_tmp_file),
FileArgument::from_path_argument(rhs_tmp_file),
Some(renamed),
true,
)
}
[path] => {
@ -670,7 +676,7 @@ pub fn parse_args() -> Mode {
display_width,
num_context_lines,
syntax_highlight,
in_vcs: true,
sort_paths,
};
let display_path = path.to_string_lossy().to_string();
@ -707,7 +713,7 @@ pub fn parse_args() -> Mode {
display_width,
num_context_lines,
syntax_highlight,
in_vcs,
sort_paths,
};
Mode::Diff {
@ -732,7 +738,7 @@ fn detect_display_width() -> usize {
80
}
pub fn should_use_color(color_output: ColorOutput) -> bool {
pub(crate) fn should_use_color(color_output: ColorOutput) -> bool {
match color_output {
ColorOutput::Always => true,
ColorOutput::Auto => {

@ -17,7 +17,7 @@ use strum::{EnumIter, IntoEnumIterator};
/// Languages supported by difftastic. Each language here has a
/// corresponding tree-sitter parser.
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
pub enum Language {
pub(crate) enum Language {
Ada,
Apex,
Bash,
@ -65,6 +65,7 @@ pub enum Language {
Ruby,
Rust,
Scala,
Scss,
Solidity,
Sql,
Swift,
@ -77,14 +78,14 @@ pub enum Language {
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LanguageOverride {
pub(crate) enum LanguageOverride {
Language(Language),
PlainText,
}
/// If there is a language called `name` (comparing case
/// insensitively), return it. Treat `"text"` as an additional option.
pub fn language_override_from_name(name: &str) -> Option<LanguageOverride> {
pub(crate) fn language_override_from_name(name: &str) -> Option<LanguageOverride> {
let name = name.trim().to_lowercase();
if name == "text" {
@ -102,7 +103,7 @@ pub fn language_override_from_name(name: &str) -> Option<LanguageOverride> {
}
/// The language name shown to the user.
pub fn language_name(language: Language) -> &'static str {
pub(crate) fn language_name(language: Language) -> &'static str {
match language {
Ada => "Ada",
Apex => "Apex",
@ -151,6 +152,7 @@ pub fn language_name(language: Language) -> &'static str {
Ruby => "Ruby",
Rust => "Rust",
Scala => "Scala",
Scss => "SCSS",
Solidity => "Solidity",
Sql => "SQL",
Swift => "Swift",
@ -166,7 +168,7 @@ pub fn language_name(language: Language) -> &'static str {
use Language::*;
/// File globs that identify languages based on the file path.
pub fn language_globs(language: Language) -> Vec<glob::Pattern> {
pub(crate) fn language_globs(language: Language) -> Vec<glob::Pattern> {
let glob_strs: &'static [&'static str] = match language {
Ada => &["*.ada", "*.adb", "*.ads"],
Bash => &[
@ -258,7 +260,7 @@ pub fn language_globs(language: Language) -> Vec<glob::Pattern> {
Html => &["*.html", "*.htm", "*.xhtml"],
Janet => &["*.janet", "*.jdn"],
Java => &["*.java"],
JavaScript => &["*.cjs", "*.js", "*.mjs"],
JavaScript => &["*.cjs", "*.js", "*.mjs", "*.snap"],
Json => &[
"*.json",
"*.avsc",
@ -334,6 +336,7 @@ pub fn language_globs(language: Language) -> Vec<glob::Pattern> {
],
Rust => &["*.rs"],
Scala => &["*.scala", "*.sbt", "*.sc"],
Scss => &["*.scss"],
Solidity => &["*.sol"],
Sql => &["*.sql", "*.pgsql"],
Swift => &["*.swift"],
@ -387,7 +390,7 @@ fn looks_like_hacklang(path: &Path, src: &str) -> bool {
false
}
pub fn guess(
pub(crate) fn guess(
path: &Path,
src: &str,
overrides: &[(LanguageOverride, Vec<glob::Pattern>)],
@ -471,6 +474,7 @@ fn from_emacs_mode_header(src: &str) -> Option<Language> {
"ruby" => Some(Ruby),
"rust" => Some(Rust),
"scala" => Some(Scala),
"scss" => Some(Scss),
"sh" => Some(Bash),
"solidity" => Some(Solidity),
"sql" => Some(Sql),

@ -1,3 +1,3 @@
pub mod guess_language;
pub mod syntax;
pub mod tree_sitter_parser;
pub(crate) mod guess_language;
pub(crate) mod syntax;
pub(crate) mod tree_sitter_parser;

@ -9,6 +9,7 @@ use line_numbers::SingleLineSpan;
use typed_arena::Arena;
use self::Syntax::*;
use crate::words::split_words_and_numbers;
use crate::{
diff::changes::ChangeKind,
diff::changes::{ChangeKind::*, ChangeMap},
@ -44,10 +45,10 @@ impl<'a> fmt::Debug for ChangeKind<'a> {
}
}
pub type SyntaxId = NonZeroU32;
pub(crate) type SyntaxId = NonZeroU32;
/// Fields that are common to both `Syntax::List` and `Syntax::Atom`.
pub struct SyntaxInfo<'a> {
pub(crate) struct SyntaxInfo<'a> {
/// The previous node with the same parent as this one.
previous_sibling: Cell<Option<&'a Syntax<'a>>>,
/// The next node with the same parent as this one.
@ -59,7 +60,7 @@ pub struct SyntaxInfo<'a> {
parent: Cell<Option<&'a Syntax<'a>>>,
/// The number of nodes that are ancestors of this one.
num_ancestors: Cell<u32>,
pub num_after: Cell<usize>,
pub(crate) num_after: Cell<usize>,
/// A number that uniquely identifies this syntax node.
unique_id: Cell<SyntaxId>,
/// A number that uniquely identifies the content of this syntax
@ -74,7 +75,7 @@ pub struct SyntaxInfo<'a> {
}
impl<'a> SyntaxInfo<'a> {
pub fn new() -> Self {
pub(crate) fn new() -> Self {
Self {
previous_sibling: Cell::new(None),
next_sibling: Cell::new(None),
@ -95,7 +96,7 @@ impl<'a> Default for SyntaxInfo<'a> {
}
}
pub enum Syntax<'a> {
pub(crate) enum Syntax<'a> {
List {
info: SyntaxInfo<'a>,
open_position: Vec<SingleLineSpan>,
@ -191,7 +192,7 @@ impl<'a> fmt::Debug for Syntax<'a> {
}
impl<'a> Syntax<'a> {
pub fn new_list(
pub(crate) fn new_list(
arena: &'a Arena<Syntax<'a>>,
open_content: &str,
open_position: Vec<SingleLineSpan>,
@ -244,7 +245,7 @@ impl<'a> Syntax<'a> {
})
}
pub fn new_atom(
pub(crate) fn new_atom(
arena: &'a Arena<Syntax<'a>>,
mut position: Vec<SingleLineSpan>,
mut content: &str,
@ -269,42 +270,42 @@ impl<'a> Syntax<'a> {
})
}
pub fn info(&self) -> &SyntaxInfo<'a> {
pub(crate) fn info(&self) -> &SyntaxInfo<'a> {
match self {
List { info, .. } | Atom { info, .. } => info,
}
}
pub fn parent(&self) -> Option<&'a Syntax<'a>> {
pub(crate) fn parent(&self) -> Option<&'a Syntax<'a>> {
self.info().parent.get()
}
pub fn next_sibling(&self) -> Option<&'a Syntax<'a>> {
pub(crate) fn next_sibling(&self) -> Option<&'a Syntax<'a>> {
self.info().next_sibling.get()
}
/// A unique ID of this syntax node. Every node is guaranteed to
/// have a different value.
pub fn id(&self) -> SyntaxId {
pub(crate) fn id(&self) -> SyntaxId {
self.info().unique_id.get()
}
/// A content ID of this syntax node. Two nodes have the same
/// content ID if they have the same content, regardless of
/// position.
pub fn content_id(&self) -> u32 {
pub(crate) fn content_id(&self) -> u32 {
self.info().content_id.get()
}
pub fn content_is_unique(&self) -> bool {
pub(crate) fn content_is_unique(&self) -> bool {
self.info().content_is_unique.get()
}
pub fn num_ancestors(&self) -> u32 {
pub(crate) fn num_ancestors(&self) -> u32 {
self.info().num_ancestors.get()
}
pub fn dbg_content(&self) -> String {
pub(crate) fn dbg_content(&self) -> String {
match self {
List {
open_content,
@ -332,7 +333,7 @@ impl<'a> Syntax<'a> {
}
}
pub fn comment_positions<'a>(nodes: &[&'a Syntax<'a>]) -> Vec<SingleLineSpan> {
pub(crate) fn comment_positions<'a>(nodes: &[&'a Syntax<'a>]) -> Vec<SingleLineSpan> {
fn walk_comment_positions(node: &Syntax<'_>, positions: &mut Vec<SingleLineSpan>) {
match node {
List { children, .. } => {
@ -348,16 +349,16 @@ pub fn comment_positions<'a>(nodes: &[&'a Syntax<'a>]) -> Vec<SingleLineSpan> {
}
}
let mut res = vec![];
let mut positions = vec![];
for node in nodes {
walk_comment_positions(node, &mut res);
walk_comment_positions(node, &mut positions);
}
res
positions
}
/// Initialise all the fields in `SyntaxInfo`.
pub fn init_all_info<'a>(lhs_roots: &[&'a Syntax<'a>], rhs_roots: &[&'a Syntax<'a>]) {
pub(crate) fn init_all_info<'a>(lhs_roots: &[&'a Syntax<'a>], rhs_roots: &[&'a Syntax<'a>]) {
init_info(lhs_roots, rhs_roots);
init_next_prev(lhs_roots);
init_next_prev(rhs_roots);
@ -439,7 +440,7 @@ fn set_num_after(nodes: &[&Syntax], parent_num_after: usize) {
}
}
}
pub fn init_next_prev<'a>(roots: &[&'a Syntax<'a>]) {
pub(crate) fn init_next_prev<'a>(roots: &[&'a Syntax<'a>]) {
set_prev_sibling(roots);
set_next_sibling(roots);
set_prev(roots, None);
@ -562,7 +563,7 @@ impl<'a> Eq for Syntax<'a> {}
/// Different types of strings. We want to diff these the same way,
/// but highlight them differently.
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
pub enum StringKind {
pub(crate) enum StringKind {
/// A string literal, such as `"foo"`.
StringLiteral,
/// Plain text, such as the content of `<p>foo</p>`.
@ -570,7 +571,7 @@ pub enum StringKind {
}
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
pub enum AtomKind {
pub(crate) enum AtomKind {
Normal,
// TODO: We should either have a AtomWithWords(HighlightKind) or a
// separate String, Text and Comment kind.
@ -583,14 +584,14 @@ pub enum AtomKind {
/// Unlike atoms, tokens can be delimiters like `{`.
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum TokenKind {
pub(crate) enum TokenKind {
Delimiter,
Atom(AtomKind),
}
/// A matched token (an atom, a delimiter, or a comment word).
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum MatchKind {
pub(crate) enum MatchKind {
UnchangedToken {
highlight: TokenKind,
self_pos: Vec<SingleLineSpan>,
@ -613,7 +614,7 @@ pub enum MatchKind {
}
impl MatchKind {
pub fn is_novel(&self) -> bool {
pub(crate) fn is_novel(&self) -> bool {
matches!(
self,
MatchKind::Novel { .. } | MatchKind::NovelWord { .. } | MatchKind::NovelLinePart { .. }
@ -622,91 +623,9 @@ impl MatchKind {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchedPos {
pub kind: MatchKind,
pub pos: SingleLineSpan,
}
/// Split `s` into a vec of things that look like words and individual
/// non-word characters.
///
/// "foo..bar23" -> vec!["foo", ".", ".", "bar23"]
///
/// See also `split_words_and_numbers`. Both these functions are hot,
/// so they are separate implementations rather than passing a bool to
/// customise number handling.
pub fn split_words(s: &str) -> Vec<&str> {
let mut res = vec![];
let mut word_start: Option<usize> = None;
for (idx, c) in s.char_indices() {
match word_start {
Some(start) => {
if c.is_alphanumeric() || c == '-' || c == '_' {
// Just carry on in this word.
} else {
// Push the previous word, then this non-word character.
res.push(&s[start..idx]);
res.push(&s[idx..idx + c.len_utf8()]);
word_start = None;
}
}
None => {
if c.is_alphanumeric() || c == '-' || c == '_' {
word_start = Some(idx);
} else {
res.push(&s[idx..idx + c.len_utf8()]);
}
}
}
}
if let Some(start) = word_start {
res.push(&s[start..]);
}
res
}
/// Split `s` into a vec of things that look like words and individual
/// non-word characters.
///
/// "foo..bar23" -> vec!["foo", ".", ".", "bar23"]
pub fn split_words_and_numbers(s: &str) -> Vec<&str> {
let mut res = vec![];
let mut word_start: Option<(usize, char)> = None;
for (idx, c) in s.char_indices() {
match word_start {
Some((start, start_c)) => {
if c.is_alphanumeric() || c == '_' {
// Word character, add to the current word if it's
// not a number.
if c.is_ascii_digit() == start_c.is_ascii_digit() {
// Just carry on in this word.
} else {
// Finish previous word, start a new one.
res.push(&s[start..idx]);
word_start = Some((idx, c));
}
} else {
// Push the previous word, then this non-word character.
res.push(&s[start..idx]);
res.push(&s[idx..idx + c.len_utf8()]);
word_start = None;
}
}
None => {
if c.is_alphanumeric() || c == '-' || c == '_' {
word_start = Some((idx, c));
} else {
res.push(&s[idx..idx + c.len_utf8()]);
}
}
}
}
if let Some((start, _)) = word_start {
res.push(&s[start..]);
}
res
pub(crate) struct MatchedPos {
pub(crate) kind: MatchKind,
pub(crate) pos: SingleLineSpan,
}
/// Given the text `content` from a comment or strings, split it into
@ -748,17 +667,17 @@ fn split_atom_words(
let mut offset = 0;
let mut opposite_offset = 0;
let mut res = vec![];
let mut mps = vec![];
for diff_res in word_diffs {
match diff_res {
myers_diff::DiffResult::Left(word) => {
// This word is novel to this side.
if !is_all_whitespace(word) {
res.push(MatchedPos {
mps.push(MatchedPos {
kind: MatchKind::NovelWord {
highlight: TokenKind::Atom(kind),
},
pos: content_newlines.from_offsets_relative_to(
pos: content_newlines.from_region_relative_to(
// TODO: don't assume a single line atom.
pos[0],
offset,
@ -772,15 +691,15 @@ fn split_atom_words(
// This word is present on both sides.
// TODO: don't assume this atom is on a single line.
let word_pos =
content_newlines.from_offsets_relative_to(pos[0], offset, offset + word.len())
content_newlines.from_region_relative_to(pos[0], offset, offset + word.len())
[0];
let opposite_word_pos = opposite_content_newlines.from_offsets_relative_to(
let opposite_word_pos = opposite_content_newlines.from_region_relative_to(
opposite_pos[0],
opposite_offset,
opposite_offset + opposite_word.len(),
);
res.push(MatchedPos {
mps.push(MatchedPos {
kind: MatchKind::NovelLinePart {
highlight: TokenKind::Atom(kind),
self_pos: word_pos,
@ -798,7 +717,7 @@ fn split_atom_words(
}
}
res
mps
}
/// Are there sufficient common words that we should only highlight
@ -830,13 +749,34 @@ fn has_common_words(word_diffs: &Vec<myers_diff::DiffResult<&&str>>) -> bool {
unchanged_count > 2 && unchanged_count * 2 >= novel_count
}
/// Skip line spans at the beginning or end that have zero width.
fn filter_empty_ends(line_spans: &[SingleLineSpan]) -> Vec<SingleLineSpan> {
let mut spans: Vec<SingleLineSpan> = vec![];
for (i, span) in line_spans.iter().enumerate() {
if (i == 0 || i == line_spans.len() - 1) && span.start_col == span.end_col {
continue;
}
spans.push(*span);
}
spans
}
impl MatchedPos {
fn new(
ck: ChangeKind,
highlight: TokenKind,
pos: &[SingleLineSpan],
is_close: bool,
is_close_delim: bool,
) -> Vec<Self> {
// Don't create a MatchedPos for empty positions at the start
// or end. We still want empty positions in the middle of
// multiline atoms, as a multiline string literal may include
// empty lines.
let pos = filter_empty_ends(pos);
match ck {
ReplacedComment(this, opposite) | ReplacedString(this, opposite) => {
let this_content = match this {
@ -862,7 +802,7 @@ impl MatchedPos {
AtomKind::Comment
};
split_atom_words(this_content, pos, opposite_content, opposite_pos, kind)
split_atom_words(this_content, &pos, opposite_content, opposite_pos, kind)
}
Unchanged(opposite) => {
let opposite_pos = match opposite {
@ -871,7 +811,7 @@ impl MatchedPos {
close_position,
..
} => {
if is_close {
if is_close_delim {
close_position.clone()
} else {
open_position.clone()
@ -888,18 +828,9 @@ impl MatchedPos {
};
// Create a MatchedPos for every line that `pos` covers.
let mut res = vec![];
for (i, line_pos) in pos.iter().enumerate() {
// Don't create a MatchedPos for empty positions
// at the start or end. We still want empty
// positions in the middle of multiline atoms, as
// a multiline string literal may include empty
// lines.
if (i == 0 || i == pos.len() - 1) && line_pos.start_col == line_pos.end_col {
continue;
}
res.push(Self {
let mut mps = vec![];
for line_pos in &pos {
mps.push(Self {
kind: kind.clone(),
pos: *line_pos,
});
@ -908,47 +839,38 @@ impl MatchedPos {
// MatchedPos on the LHS and RHS. This allows us
// to consider unchanged MatchedPos values
// pairwise.
if res.len() == opposite_pos_len {
if mps.len() == opposite_pos_len {
break;
}
}
res
mps
}
Novel => {
let kind = MatchKind::Novel { highlight };
// Create a MatchedPos for every line that `pos` covers.
let mut res = vec![];
for (i, line_pos) in pos.iter().enumerate() {
// Don't create a MatchedPos for entirly empty positions. This
let mut mps = vec![];
for line_pos in &pos {
// Don't create a MatchedPos for entirely empty positions. This
// occurs when we have lists with empty open/close
// delimiter positions, such as the top-level list of syntax items.
if pos.len() == 1 && line_pos.start_col == line_pos.end_col {
continue;
}
// Don't create a MatchedPos for empty positions
// at the start or end. We still want empty
// positions in the middle of multiline atoms, as
// a multiline string literal may include empty
// lines.
if (i == 0 || i == pos.len() - 1) && line_pos.start_col == line_pos.end_col {
continue;
}
res.push(Self {
mps.push(Self {
kind: kind.clone(),
pos: *line_pos,
});
}
res
mps
}
}
}
}
/// Walk `nodes` and return a vec of all the changed positions.
pub fn change_positions<'a>(
pub(crate) fn change_positions<'a>(
nodes: &[&'a Syntax<'a>],
change_map: &ChangeMap<'a>,
) -> Vec<MatchedPos> {
@ -1002,7 +924,7 @@ fn change_positions_<'a>(
}
}
pub fn zip_pad_shorter<Tx: Clone, Ty: Clone>(
pub(crate) fn zip_pad_shorter<Tx: Clone, Ty: Clone>(
lhs: &[Tx],
rhs: &[Ty],
) -> Vec<(Option<Tx>, Option<Ty>)> {
@ -1022,7 +944,7 @@ pub fn zip_pad_shorter<Tx: Clone, Ty: Clone>(
/// Zip `lhs` with `rhs`, but repeat the last item from the shorter
/// slice.
pub fn zip_repeat_shorter<Tx: Clone, Ty: Clone>(lhs: &[Tx], rhs: &[Ty]) -> Vec<(Tx, Ty)> {
pub(crate) fn zip_repeat_shorter<Tx: Clone, Ty: Clone>(lhs: &[Tx], rhs: &[Ty]) -> Vec<(Tx, Ty)> {
let lhs_last: Tx = match lhs.last() {
Some(last) => last.clone(),
None => return vec![],
@ -1339,67 +1261,4 @@ mod tests {
],
);
}
#[test]
fn test_split_words() {
let s = "example.com";
let res = split_words(s);
assert_eq!(res, vec!["example", ".", "com"])
}
#[test]
fn test_split_words_punctuation() {
let s = "example..";
let res = split_words(s);
assert_eq!(res, vec!["example", ".", "."])
}
#[test]
fn test_split_words_numbers() {
let s = "foo123bar";
let res = split_words(s);
assert_eq!(res, vec!["foo123bar"])
}
#[test]
fn test_split_words_treats_newline_separately() {
let s = "example.\ncom";
let res = split_words(s);
assert_eq!(res, vec!["example", ".", "\n", "com"])
}
#[test]
fn test_split_words_single_unicode() {
let s = "a ö b";
let res = split_words(s);
assert_eq!(res, vec!["a", " ", "ö", " ", "b"])
}
#[test]
fn test_split_words_single_unicode_not_alphabetic() {
let s = "a 💝 b";
let res = split_words(s);
assert_eq!(res, vec!["a", " ", "💝", " ", "b"])
}
#[test]
fn test_split_words_unicode() {
let s = "a xöy b";
let res = split_words(s);
assert_eq!(res, vec!["a", " ", "xöy", " ", "b"])
}
#[test]
fn test_split_words_and_numbers() {
let s = "a123b";
let res = split_words_and_numbers(s);
assert_eq!(res, vec!["a", "123", "b"])
}
#[test]
fn test_split_words_and_numbers_spaces() {
let s = "foo bar";
let res = split_words_and_numbers(s);
assert_eq!(res, vec!["foo", " ", "bar"])
}
}

@ -19,7 +19,7 @@ use crate::parse::syntax::{AtomKind, Syntax};
/// languages we should parse them as.
///
/// Note that we don't support sub-languages more than one layer deep.
pub struct TreeSitterSubLanguage {
pub(crate) struct TreeSitterSubLanguage {
/// How to identify a node. The query must contain exactly one
/// capture group (the name is arbitrary).
query: ts::Query,
@ -29,9 +29,9 @@ pub struct TreeSitterSubLanguage {
}
/// Configuration for a tree-sitter parser.
pub struct TreeSitterConfig {
pub(crate) struct TreeSitterConfig {
/// The tree-sitter language parser.
pub language: ts::Language,
pub(crate) language: ts::Language,
/// Tree-sitter nodes that we treat as indivisible atoms.
///
@ -106,6 +106,7 @@ extern "C" {
fn tree_sitter_ruby() -> ts::Language;
fn tree_sitter_rust() -> ts::Language;
fn tree_sitter_scala() -> ts::Language;
fn tree_sitter_scss() -> ts::Language;
fn tree_sitter_solidity() -> ts::Language;
fn tree_sitter_sql() -> ts::Language;
fn tree_sitter_swift() -> ts::Language;
@ -127,7 +128,7 @@ const OCAML_ATOM_NODES: [&str; 6] = [
"attribute_id",
];
pub fn from_language(language: guess::Language) -> TreeSitterConfig {
pub(crate) fn from_language(language: guess::Language) -> TreeSitterConfig {
use guess::Language::*;
match language {
Ada => {
@ -639,7 +640,11 @@ pub fn from_language(language: guess::Language) -> TreeSitterConfig {
let language = unsafe { tree_sitter_kotlin() };
TreeSitterConfig {
language,
atom_nodes: vec!["line_string_literal", "character_literal"]
// Flattening nullable type means we can't diff the
// structure of complex types within, but it beats
// ignoring nullable changes.
// https://github.com/Wilfred/difftastic/issues/411
atom_nodes: vec!["line_string_literal", "character_literal", "nullable_type"]
.into_iter()
.collect(),
delimiter_tokens: vec![("(", ")"), ("{", "}"), ("[", "]"), ("<", ">")]
@ -931,6 +936,22 @@ pub fn from_language(language: guess::Language) -> TreeSitterConfig {
sub_languages: vec![],
}
}
Scss => {
let language = unsafe { tree_sitter_scss() };
TreeSitterConfig {
language,
atom_nodes: vec!["integer_value", "float_value", "color_value"]
.into_iter()
.collect(),
delimiter_tokens: vec![("{", "}"), ("(", ")")],
highlight_query: ts::Query::new(
language,
include_str!("../../vendored_parsers/highlights/scss.scm"),
)
.unwrap(),
sub_languages: vec![],
}
}
Solidity => {
let language = unsafe { tree_sitter_solidity() };
TreeSitterConfig {
@ -1083,7 +1104,7 @@ pub fn from_language(language: guess::Language) -> TreeSitterConfig {
}
/// Parse `src` with tree-sitter.
pub fn to_tree(src: &str, config: &TreeSitterConfig) -> tree_sitter::Tree {
pub(crate) fn to_tree(src: &str, config: &TreeSitterConfig) -> tree_sitter::Tree {
let mut parser = ts::Parser::new();
parser
.set_language(config.language)
@ -1093,9 +1114,9 @@ pub fn to_tree(src: &str, config: &TreeSitterConfig) -> tree_sitter::Tree {
}
#[derive(Debug)]
pub struct ExceededByteLimit(pub usize);
pub(crate) struct ExceededByteLimit(pub(crate) usize);
pub fn to_tree_with_limit(
pub(crate) fn to_tree_with_limit(
diff_options: &DiffOptions,
config: &TreeSitterConfig,
lhs_src: &str,
@ -1112,7 +1133,7 @@ pub fn to_tree_with_limit(
/// Find any nodes that can be parsed as other languages (e.g. JavaScript embedded in HTML),
/// and return a map of their node IDs mapped to parsed trees. Every time we see such a node,
/// we will ignore it and recurse into the root node of the given tree instead.
pub fn parse_subtrees(
pub(crate) fn parse_subtrees(
src: &str,
config: &TreeSitterConfig,
tree: &tree_sitter::Tree,
@ -1238,7 +1259,7 @@ fn tree_highlights(
}
}
pub fn print_tree(src: &str, tree: &tree_sitter::Tree) {
pub(crate) fn print_tree(src: &str, tree: &tree_sitter::Tree) {
let mut cursor = tree.walk();
print_cursor(src, &mut cursor, 0);
}
@ -1273,7 +1294,7 @@ fn print_cursor(src: &str, cursor: &mut ts::TreeCursor, depth: usize) {
}
}
pub fn comment_positions(
pub(crate) fn comment_positions(
tree: &tree_sitter::Tree,
src: &str,
config: &TreeSitterConfig,
@ -1296,9 +1317,9 @@ pub fn comment_positions(
}
#[derive(Debug)]
pub struct ExceededParseErrorLimit(pub usize);
pub(crate) struct ExceededParseErrorLimit(pub(crate) usize);
pub fn to_syntax_with_limit<'a>(
pub(crate) fn to_syntax_with_limit<'a>(
lhs_src: &str,
rhs_src: &str,
lhs_tree: &tree_sitter::Tree,
@ -1331,7 +1352,7 @@ pub fn to_syntax_with_limit<'a>(
Ok((lhs_nodes, rhs_nodes))
}
pub fn to_syntax<'a>(
pub(crate) fn to_syntax<'a>(
tree: &tree_sitter::Tree,
src: &str,
arena: &'a Arena<Syntax<'a>>,
@ -1378,7 +1399,7 @@ pub fn to_syntax<'a>(
}
/// Parse `src` with tree-sitter and convert to difftastic Syntax.
pub fn parse<'a>(
pub(crate) fn parse<'a>(
arena: &'a Arena<Syntax<'a>>,
src: &str,
config: &TreeSitterConfig,
@ -1437,7 +1458,7 @@ fn find_delim_positions(
None
}
pub struct HighlightedNodeIds {
pub(crate) struct HighlightedNodeIds {
keyword_ids: HashSet<usize>,
comment_ids: HashSet<usize>,
string_ids: HashSet<usize>,
@ -1459,10 +1480,10 @@ fn all_syntaxes_from_cursor<'a>(
subtrees: &DftHashMap<usize, (tree_sitter::Tree, TreeSitterConfig, HighlightedNodeIds)>,
ignore_comments: bool,
) -> Vec<&'a Syntax<'a>> {
let mut result: Vec<&Syntax> = vec![];
let mut nodes: Vec<&Syntax> = vec![];
loop {
result.extend(syntax_from_cursor(
nodes.extend(syntax_from_cursor(
arena,
src,
nl_pos,
@ -1479,7 +1500,7 @@ fn all_syntaxes_from_cursor<'a>(
}
}
result
nodes
}
/// Convert the tree-sitter node at `cursor` to a difftastic syntax
@ -1558,9 +1579,9 @@ fn list_from_cursor<'a>(
// the delimiter text and the start/end of this node as the
// delimiter positions.
let outer_open_content = "";
let outer_open_position = nl_pos.from_offsets(root_node.start_byte(), root_node.start_byte());
let outer_open_position = nl_pos.from_region(root_node.start_byte(), root_node.start_byte());
let outer_close_content = "";
let outer_close_position = nl_pos.from_offsets(root_node.end_byte(), root_node.end_byte());
let outer_close_position = nl_pos.from_region(root_node.end_byte(), root_node.end_byte());
// TODO: this should probably only allow the delimiters to be the
// first and last child in the list.
@ -1608,7 +1629,7 @@ fn list_from_cursor<'a>(
));
} else if node_i == i {
inner_open_content = &src[node.start_byte()..node.end_byte()];
inner_open_position = nl_pos.from_offsets(node.start_byte(), node.end_byte());
inner_open_position = nl_pos.from_region(node.start_byte(), node.end_byte());
} else if node_i < j {
between_delim.extend(syntax_from_cursor(
arena,
@ -1623,7 +1644,7 @@ fn list_from_cursor<'a>(
));
} else if node_i == j {
inner_close_content = &src[node.start_byte()..node.end_byte()];
inner_close_position = nl_pos.from_offsets(node.start_byte(), node.end_byte());
inner_close_position = nl_pos.from_region(node.start_byte(), node.end_byte());
} else if node_i > j {
after_delim.extend(syntax_from_cursor(
arena,
@ -1689,7 +1710,7 @@ fn atom_from_cursor<'a>(
ignore_comments: bool,
) -> Option<&'a Syntax<'a>> {
let node = cursor.node();
let position = nl_pos.from_offsets(node.start_byte(), node.end_byte());
let position = nl_pos.from_region(node.start_byte(), node.end_byte());
let mut content = &src[node.start_byte()..node.end_byte()];
// The C and C++ grammars have a '\n' node with the

@ -11,13 +11,13 @@ use crate::{
};
#[derive(Debug, PartialEq, Eq)]
pub enum FileContent {
pub(crate) enum FileContent {
Text(String),
Binary,
}
#[derive(Debug, Clone)]
pub enum FileFormat {
pub(crate) enum FileFormat {
SupportedLanguage(guess_language::Language),
PlainText,
TextFallback { reason: String },
@ -36,26 +36,26 @@ impl Display for FileFormat {
}
#[derive(Debug)]
pub struct DiffResult {
pub display_path: String,
pub(crate) struct DiffResult {
pub(crate) display_path: String,
/// Additional information to display about this file, such as
/// "Renamed from x.js to y.js".
pub extra_info: Option<String>,
pub(crate) extra_info: Option<String>,
pub file_format: FileFormat,
pub lhs_src: FileContent,
pub rhs_src: FileContent,
pub hunks: Vec<Hunk>,
pub(crate) file_format: FileFormat,
pub(crate) lhs_src: FileContent,
pub(crate) rhs_src: FileContent,
pub(crate) hunks: Vec<Hunk>,
pub lhs_positions: Vec<MatchedPos>,
pub rhs_positions: Vec<MatchedPos>,
pub(crate) lhs_positions: Vec<MatchedPos>,
pub(crate) rhs_positions: Vec<MatchedPos>,
pub has_byte_changes: bool,
pub has_syntactic_changes: bool,
pub(crate) has_byte_changes: bool,
pub(crate) has_syntactic_changes: bool,
}
impl DiffResult {
pub fn has_reportable_change(&self) -> bool {
pub(crate) fn has_reportable_change(&self) -> bool {
if matches!(self.lhs_src, FileContent::Binary)
|| matches!(self.rhs_src, FileContent::Binary)
{

@ -2,49 +2,62 @@ use std::fmt;
use lazy_static::lazy_static;
pub struct CommitInfo {
pub short_commit_hash: &'static str,
pub commit_hash: &'static str,
pub commit_date: &'static str,
pub(crate) struct CommitInfo {
pub(crate) short_commit_hash: &'static str,
pub(crate) commit_date: &'static str,
}
pub struct VersionInfo {
pub version: &'static str,
pub commit_info: Option<CommitInfo>,
pub(crate) struct VersionInfo {
pub(crate) version: &'static str,
pub(crate) commit_info: Option<CommitInfo>,
pub(crate) rustc_version: Option<&'static str>,
}
impl fmt::Display for VersionInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.version)?;
if let Some(ref ci) = &self.commit_info {
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
match (&self.commit_info, self.rustc_version) {
(Some(commit_info), Some(rustc_version)) => write!(
f,
" ({} {}, built with rustc {})",
commit_info.short_commit_hash, commit_info.commit_date, rustc_version
)?,
(Some(commit_info), None) => write!(
f,
" ({} {})",
commit_info.short_commit_hash, commit_info.commit_date
)?,
(None, Some(rustc_version)) => write!(f, " (built with rustc {})", rustc_version)?,
(None, None) => {}
}
Ok(())
}
}
lazy_static! {
pub static ref VERSION: String = version().to_string();
pub(crate) static ref VERSION: String = version().to_string();
}
pub const fn version() -> VersionInfo {
pub(crate) const fn version() -> VersionInfo {
let version = env!("CARGO_PKG_VERSION");
let commit_info = match (
option_env!("DFT_COMMIT_SHORT_HASH"),
option_env!("DFT_COMMIT_HASH"),
option_env!("DFT_COMMIT_DATE"),
) {
(Some(short_commit_hash), Some(commit_hash), Some(commit_date)) => Some(CommitInfo {
(Some(short_commit_hash), Some(commit_date)) => Some(CommitInfo {
short_commit_hash,
commit_hash,
commit_date,
}),
_ => None,
};
let rustc_version = option_env!("DFT_RUSTC_VERSION");
VersionInfo {
version,
commit_info,
rustc_version,
}
}

@ -0,0 +1,151 @@
/// Split `s` into a vec of things that look like words and individual
/// non-word characters.
///
/// "foo..bar23" -> vec!["foo", ".", ".", "bar23"]
///
/// See also `split_words_and_numbers`. Both these functions are hot,
/// so they are separate implementations rather than passing a bool to
/// customise number handling.
pub(crate) fn split_words(s: &str) -> Vec<&str> {
let mut words = vec![];
let mut word_start: Option<usize> = None;
for (idx, c) in s.char_indices() {
match word_start {
Some(start) => {
if c.is_alphanumeric() || c == '-' || c == '_' {
// Just carry on in this word.
} else {
// Push the previous word, then this non-word character.
words.push(&s[start..idx]);
words.push(&s[idx..idx + c.len_utf8()]);
word_start = None;
}
}
None => {
if c.is_alphanumeric() || c == '-' || c == '_' {
word_start = Some(idx);
} else {
words.push(&s[idx..idx + c.len_utf8()]);
}
}
}
}
if let Some(start) = word_start {
words.push(&s[start..]);
}
words
}
/// Split `s` into a vec of things that look like words and individual
/// non-word characters.
///
/// "foo..bar23" -> vec!["foo", ".", ".", "bar23"]
pub(crate) fn split_words_and_numbers(s: &str) -> Vec<&str> {
let mut words = vec![];
let mut word_start: Option<(usize, char)> = None;
for (idx, c) in s.char_indices() {
match word_start {
Some((start, start_c)) => {
if c.is_alphanumeric() || c == '_' {
// Word character, add to the current word if it's
// not a number.
if c.is_ascii_digit() == start_c.is_ascii_digit() {
// Just carry on in this word.
} else {
// Finish previous word, start a new one.
words.push(&s[start..idx]);
word_start = Some((idx, c));
}
} else {
// Push the previous word, then this non-word character.
words.push(&s[start..idx]);
words.push(&s[idx..idx + c.len_utf8()]);
word_start = None;
}
}
None => {
if c.is_alphanumeric() || c == '-' || c == '_' {
word_start = Some((idx, c));
} else {
words.push(&s[idx..idx + c.len_utf8()]);
}
}
}
}
if let Some((start, _)) = word_start {
words.push(&s[start..]);
}
words
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_split_words() {
let s = "example.com";
let res = split_words(s);
assert_eq!(res, vec!["example", ".", "com"])
}
#[test]
fn test_split_words_punctuation() {
let s = "example..";
let res = split_words(s);
assert_eq!(res, vec!["example", ".", "."])
}
#[test]
fn test_split_words_numbers() {
let s = "foo123bar";
let res = split_words(s);
assert_eq!(res, vec!["foo123bar"])
}
#[test]
fn test_split_words_treats_newline_separately() {
let s = "example.\ncom";
let res = split_words(s);
assert_eq!(res, vec!["example", ".", "\n", "com"])
}
#[test]
fn test_split_words_single_unicode() {
let s = "a ö b";
let res = split_words(s);
assert_eq!(res, vec!["a", " ", "ö", " ", "b"])
}
#[test]
fn test_split_words_single_unicode_not_alphabetic() {
let s = "a 💝 b";
let res = split_words(s);
assert_eq!(res, vec!["a", " ", "💝", " ", "b"])
}
#[test]
fn test_split_words_unicode() {
let s = "a xöy b";
let res = split_words(s);
assert_eq!(res, vec!["a", " ", "xöy", " ", "b"])
}
#[test]
fn test_split_words_and_numbers() {
let s = "a123b";
let res = split_words_and_numbers(s);
assert_eq!(res, vec!["a", "123", "b"])
}
#[test]
fn test_split_words_and_numbers_spaces() {
let s = "foo bar";
let res = split_words_and_numbers(s);
assert_eq!(res, vec!["foo", " ", "bar"])
}
}

@ -3,9 +3,37 @@ use std::process::Command;
use assert_cmd::prelude::*;
use predicates::prelude::*;
fn find_runner() -> Option<String> {
for (key, value) in std::env::vars() {
if key.starts_with("CARGO_TARGET_") && key.ends_with("_RUNNER") && !value.is_empty() {
return Some(value);
}
}
None
}
// Sample code from
// https://github.com/assert-rs/assert_cmd/issues/139, supports
// cross-compiled binaries.
fn get_base_command() -> Command {
let mut cmd;
let path = assert_cmd::cargo::cargo_bin("difft");
if let Some(runner) = find_runner() {
let mut runner = runner.split_whitespace();
cmd = Command::new(runner.next().unwrap());
for arg in runner {
cmd.arg(arg);
}
cmd.arg(path);
} else {
cmd = Command::new(path);
}
cmd
}
#[test]
fn no_such_files() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("no_such_file").arg("no_such_file_either");
cmd.assert().failure().code(2);
@ -13,7 +41,7 @@ fn no_such_files() {
#[test]
fn inline() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--display=inline")
.arg("sample_files/simple_before.js")
@ -23,7 +51,7 @@ fn inline() {
#[test]
fn binary_changed() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--display=inline")
.arg("img/logo.png")
@ -35,7 +63,7 @@ fn binary_changed() {
#[test]
fn has_changes_default_exit_code() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("sample_files/simple_before.js")
.arg("sample_files/simple_after.js");
@ -44,7 +72,7 @@ fn has_changes_default_exit_code() {
#[test]
fn has_changes_requested_exit_code() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--exit-code")
.arg("sample_files/simple_before.js")
@ -54,7 +82,7 @@ fn has_changes_requested_exit_code() {
#[test]
fn ignore_comments() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--ignore-comments")
.arg("sample_files/comma_and_comment_before.js")
@ -66,7 +94,7 @@ fn ignore_comments() {
#[test]
fn check_only() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--check-only")
.arg("sample_files/simple_before.js")
@ -78,7 +106,7 @@ fn check_only() {
#[test]
fn check_only_text_file() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--check-only")
.arg("sample_files/text_before.txt")
@ -90,7 +118,7 @@ fn check_only_text_file() {
#[test]
fn makefile_text_as_atom() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("sample_files/cli_tests/makefile_before.mk")
.arg("sample_files/cli_tests/makefile_after.mk");
@ -101,7 +129,7 @@ fn makefile_text_as_atom() {
#[test]
fn yaml_parse_errors() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("sample_files/cli_tests/bad_yaml_before.yml")
.arg("sample_files/cli_tests/bad_yaml_after.yml");
@ -112,7 +140,7 @@ fn yaml_parse_errors() {
#[test]
fn list_languages() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--list-languages");
@ -125,7 +153,7 @@ fn list_languages() {
#[test]
fn test_mime_type_false_positive() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("sample_files/cli_tests/x_mod_false_positive.js")
.arg("sample_files/cli_tests/empty.js");
@ -136,7 +164,7 @@ fn test_mime_type_false_positive() {
#[test]
fn slightly_invalid_utf8() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("sample_files/cli_tests/MainWindowViewModel.cs")
.arg("/dev/null");
@ -147,7 +175,7 @@ fn slightly_invalid_utf8() {
#[test]
fn directory_arguments() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("sample_files/dir_before")
.arg("sample_files/dir_after");
@ -159,7 +187,7 @@ fn directory_arguments() {
#[test]
fn git_style_arguments_rename() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("elisp_oldname.el")
.arg("sample_files/elisp_before.el")
@ -176,7 +204,7 @@ fn git_style_arguments_rename() {
#[test]
fn drop_different_path_starts() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("sample_files/dir_before/clojure.clj")
.arg("sample_files/dir_after/clojure.clj");
@ -186,7 +214,7 @@ fn drop_different_path_starts() {
#[test]
fn dump_tree_sitter() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--dump-ts")
.arg("sample_files/simple_before.js")
@ -196,7 +224,7 @@ fn dump_tree_sitter() {
#[test]
fn dump_syntax() {
let mut cmd = Command::cargo_bin("difft").unwrap();
let mut cmd = get_base_command();
cmd.arg("--dump-syntax")
.arg("sample_files/simple_before.js")

@ -5,6 +5,7 @@
- [使用](./usage.md)
- [Git](./git.md)
- [Mercurial](./mercurial.md)
- [Fossil](./fossil.md)
- [支持的语言](./languages_supported.md)
- [解析](./parsing.md)
- [差异分析](./diffing.md)

@ -0,0 +1,29 @@
# Fossil
[Fossil](https://fossil-scm.org/) 通过为当前存储库 [设置`diff-command`](https://fossil-scm.org/home/help?cmd=diff-command) 来支持外部 `diff` 命令:
```
fossil settings diff-command difft
```
如果你想对所有存储库使用 Difftastic可以使用 `--global`
```
fossil settings diff-command --global difft
```
## 在 Fossil 上跳过 Difftastic
如果你已经将 Difftastic 设置为 Fossil 的 `diff` 命令,但想使用一次 Fossil 的内置差异分析工具,可以使用 `-i` 暂时跳过一次 Difftastic
```
fossil diff -i
```
如果你想从某个存储库(或全局)移除 Difftastic请使用 `unset` 命令:
```
fossil unset diff-command
```
`unset` 命令支持 `--global` 选项。

@ -2,7 +2,7 @@
## 从二进制安装
Difftastic 将预先编译好的二进制文件 [提供到 GitHub realeases](https://github.com/Wilfred/difftastic/releases) 。
Difftastic 将预先编译好的二进制文件 [提供到 GitHub realeases](https://github.com/Wilfred/difftastic/releases) 。[更新日志](https://github.com/Wilfred/difftastic/blob/master/CHANGELOG.md) 描述了每个发行版的变更。
在以下平台上也可以使用软件包。
@ -22,7 +22,7 @@ $ brew install difftastic
### 要求
Difftastic 使用 Rust 编写,所以你需安装 Rust。我推荐使用 [rustup](https://rustup.rs/) 安装 Rust。Difftastic 要求 Rust 版本不低于 1.59
Difftastic 使用 Rust 编写,所以你需安装 Rust。我推荐使用 [rustup](https://rustup.rs/) 安装 Rust。Difftastic 要求 Rust 版本不低于 1.63
你也需要一个支持 C++14 的 C++ 编译器。如果你正在使用 GCC则 GCC 版本至少为 8。
@ -32,7 +32,7 @@ Difftastic 使用 Rust 编写,所以你需安装 Rust。我推荐使用 [rustu
crates.io](https://crates.io/crates/difftastic)。
```
$ cargo install difftastic
$ cargo install --locked difftastic
```
Difftastic 使用 `cc` 程序箱来构建 C/C++ 的依赖关系,这使你能通过环境变量 `CC``CXX` 来控制使用的编译器(参照 [cc

@ -7,24 +7,27 @@ Mercurial 在使用 Extdiff 拓展时,[支持使用外部差异分析工具](h
extdiff =
```
接下来,你可以运行 `hg extdiff -p difft` 命令(假定 `difft` 二进制文件已经存放在 `$PATH` 中)。
接下来,你可以运行 `hg extdiff -p difft` 命令,但不是 `hg diff` 命令(假定 `difft` 二进制文件已经存放在 `$PATH` 中)。
您还可以定义一个别名,用 hg 运行 Difftastic。将以下内容添加到您的 `.hgrc` 中,以使用 `hg dft` 命令运行 Difftastic。
```
[extdiff]
cmd.dft = difft
opts.dft = --missing-as-empty
# 你可以添加更多选项,它们将被传递至命令行,例如:
# opts.dft = --background light
```
`hg dft` 也支持 `hg diff` 的所有选项。例如,`hg dft --stat` 会显示更改行的统计信息,`hg dft -r 42 -r 45` 会显示两个修订版之间的差异。
## hg log -p
Mercurial 不能改变默认的差异工具,至少就作者所知。
如果你只想查看最近的一次提交的差异,你可以使用下面的方法
如果你想查看最近一次提交的差异,可以使用下面的命令
```
GIT_PAGER_IN_USE=1 hg dft -r .^ -r . | less
hg dft -r .^ -r .
```
这就等同于`hg log -l 1 -p`,尽管它不显示提交信息。

@ -1,8 +1,8 @@
# 棘手的例子
在某些情况下,树状图的差异分析是具有挑战性的。本页展示了在开发过程中所观察到的困难情况
树状差异分析有时具有挑战性。本页展示了开发过程中观察到的困难情况和处理结果
并非所有这些情况在Difftastic中都能很好地工作。
并非所有这些情况在 Difftastic 中都能很好地工作。
## 添加定界符
@ -14,11 +14,15 @@ x
(x)
```
理想输出: <code><span style="background-color: PaleGreen; color: #000">(</span>x<span style="background-color: PaleGreen; color: #000">)</span></code>
可能输出:<code><span style="background-color: PaleGreen; color: #000">(x)</span></code>
这个是十分棘手,因为`x`已经改变了它在树中的深度,但`x`本身却未发生改变。
理想输出:<code><span style="background-color: PaleGreen; color: #000">(</span>x<span style="background-color: PaleGreen; color: #000">)</span></code>
并不是所有的树形差异分析算法可以处理这个例子。同时仔细地展示出范例是具有挑战性的:我们希望去高亮出已改变的定界符,但不是他们的内容。这同样在更大的表达式是具有挑战性的。
这十分棘手,因为 `x` 改变了在树中的深度,但其本身却未发生改变。
并不是所有的树状差异分析算法可以处理这个例子。同时仔细地展示出范例是具有挑战性的:我们希望高亮已改变的定界符,而非他们的内容。这同样在更大的表达式中具有挑战性。
**Difftastic**Difftastic 即使认为节点在不同深度,在这种情况下也能实现预期结果。
## 改变定界符
@ -30,9 +34,13 @@ x
[x]
```
正如这个包裹的例子,我们想要去高亮出定界符而不是`x`这个内容。
理想输出:<code><span style="background-color: #fbbd98; color: #000">(</span>x<span style="background-color: #fbbd98; color: #000">)</span></code>, <code><span style="background-color: PaleGreen; color: #000">[</span>x<span style="background-color: PaleGreen; color: #000">]</span></code>
正如这个例子,我们想要高亮定界符而非 `x` 这个内容。
**Difftastic**通过树状差异分析Difftastic 正确处理这个问题。
## 拓展定界符
## 展定界符
```
;; Before
@ -42,9 +50,13 @@ x
(x y)
```
理想输出:<code>(x <span style="background-color: PaleGreen; color: #000">y</span>)</code>
可能输出 1<code><span style="background-color: #fbbd98; color: #000">(</span>x<span style="background-color: #fbbd98; color: #000">)</span> y</code>, <code><span style="background-color: PaleGreen; color: #000">(</span>x y<span style="background-color: PaleGreen; color: #000">)</span></code>
在这个例子下,我们想要去高亮`y`。高亮显示定界符的话可能会让`x`看起来有所变化。
可能输出 2<code>(x) <span style="background-color: #fbbd98; color: #000">y</span></code>, <code>(x <span style="background-color: PaleGreen; color: #000">y</span>)</code>
目前还不清楚在这种情况下,哪个结果更好。
**Difftastic**Difftastic 目前显示结果 2但这种情况下对成本模型很敏感。一些以前版本的 Difftastic 显示结果 1。
## 缩小定界符
@ -56,7 +68,7 @@ x
(x) y
```
应该与扩展定界符的情况类似,去高亮定界符
这与扩展定界符的情况类似。
## 使定界符不连贯
@ -70,7 +82,7 @@ x
理想输出:<code>(foo <span style="background-color:PaleGreen; color: #000">(novel)</span> (bar)</code>
很容易会变成<code>(foo (<span style="background-color:PaleGreen; color: #000">novel</span>) <span style="background-color:PaleGreen; color: #000">(</span>bar<span style="background-color:PaleGreen; color: #000">)</span>)</code>,
很容易会变成 <code>(foo (<span style="background-color:PaleGreen; color: #000">novel</span>) <span style="background-color:PaleGreen; color: #000">(</span>bar<span style="background-color:PaleGreen; color: #000">)</span>)</code>
其中后一组的定界符会被选中。
## 重新组织大节点
@ -84,8 +96,7 @@ x
([[foo]] x y)
```
我们想高亮`[[foo]]`被移到括号内了。然而,一个简单的语法差异者更倾向于认为在前面删除`()`,在后面增加`()`,是最小的差异表现。
(见[issue 44](https://github.com/Wilfred/difftastic/issues/44)。)
我们想高亮 `[[foo]]` 被移到了括号内。然而,一个简单的语法差异者更倾向于认为在前面删除 `()` 并在后面增加 `()`,因为这是最小的差异表现(见[议题 #44](https://github.com/Wilfred/difftastic/issues/44))。
## 在列表内重新排列
@ -99,7 +110,7 @@ x
理想输出:<code>(<span style="background-color: PaleGreen; color: #000">y</span> <span style="background-color: PaleGreen; color: #000">x</span>)</code>
我们想突出显示列表的内容,而不是定界符。
我们想高亮列表内容,而非定界符。
## 中间插入
@ -113,7 +124,25 @@ foo(extra(bar(123)))
理想输出:<code>foo(<span style="background-color: PaleGreen; color: #000">extra(</span>bar(123)<span style="background-color: PaleGreen; color: #000">)</span>)</code>
我们想把`foo`和`bar`都看作是不变的。这种情况对于对树进行自下而上然后自上而下匹配的衍合算法来说是具有挑战性的。
我们想把 `foo``bar` 看作是未改变的。这种情况对树进行自下而上然后自上而下匹配的衍合算法具有挑战性。
## 标点符号元素
```
// Before
foo(1, bar)
// After
foo(bar, 2)
```
可能输出:<code>foo(<span style="background-color: PaleGreen; color: #000">bar</span>, <span style="background-color: PaleGreen; color: #000">2</span>)</code>
理想输出:<code>foo(bar<span style="background-color: PaleGreen; color: #000">,</span> <span style="background-color: PaleGreen; color: #000">2</span>)</code>
`()` 内有两个元素,我们可以认为 `bar``,` 中有一个未改变(但不能认为两者都不改变,因为它们已经重新排序)。
我们想把 `bar` 看作是未改变的,因为它相比于 `,` 元素更加重要。在语言不可知的方式下完成这一点存在困难,所以 Difftastic 有一个小的低优先级标点符号元素列表。
## 滑块(平移)
@ -128,9 +157,9 @@ foo(extra(bar(123)))
}
```
git-diff有一些启发式方法来减少这种风险(比如说"patience diff"),但这个问题仍然可能发生。
git-diff 有一些启发式方法(比如 Patience Diff降低这种风险但这个问题仍可能发生。
接下来是一个在树状差异分析时常见的问题。
在树状差异分析时也有类似的问题。
```
;; Before
@ -143,7 +172,21 @@ A B
C D
```
理想情况下,我们更愿意将连续的节点标记为新的,所以我们强调`A B`而不是`B/nA`。从最长公序算法的角度来看,这两种选择是等价的。
可能输出:
<pre><code>A <span style="background-color: PaleGreen; color: #000">B</span>
<span style="background-color: PaleGreen; color: #000">A</span> B
C D
</code></pre>
理想输出:
<pre><code>A B
<span style="background-color: PaleGreen; color: #000">A</span> <span style="background-color: PaleGreen; color: #000">B</span>
C D
</code></pre>
理想情况下我们更期望将连续节点标记为新的。从最长共子序列算法LCS这两个选择等价。
## 滑块(嵌套)
@ -155,10 +198,11 @@ old1(old2)
old1(new1(old2))
```
这个应该是 <code>old1(<span style="background-color: PaleGreen; color: #000">new1(</span>old2<span style="background-color: PaleGreen; color: #000">)</span>)</code> 还是
<code>old1<span style="background-color: PaleGreen; color: #000">(new1</span>(old2)<span style="background-color: PaleGreen; color: #000">)</span></code>?
可能输出:<code>old1<span style="background-color: PaleGreen; color: #000">(new1</span>(old2)<span style="background-color: PaleGreen; color: #000">)</span></code>
理想输出:<code>old1(<span style="background-color: PaleGreen; color: #000">new1(</span>old2<span style="background-color: PaleGreen; color: #000">)</span>)</code>
正确的答案是取决于语言。大多数语言希望优先使用内部分隔符而Lisps和JSON则喜欢使用外部分隔符。
正确的答案是取决于语言。大多数语言希望优先使用内部定界符,而 Lisps 与 JSON 则期望使用外部定界符。
## 最小化深度改变
@ -173,8 +217,7 @@ foo(456);
foo(789);
```
我们认为`foo(123)`还是`foo(456)`与`foo(789)`匹配?
Difftastic优先考虑`foo(456)`,通过优先考虑相同嵌套深度的节点。
我们认为 `foo(123)``foo(456)` 中,哪个与 `foo(789)` 匹配Difftastic 优先考虑 `foo(456)`,因为其优先考虑相同嵌套深度的节点。
## 有少量相似处的替代做法
@ -186,9 +229,11 @@ function foo(x) { return x + 1; }
function bar(y) { baz(y); }
```
在这个例子中,我们删除了一个函数,写了一个完全不同的函数。基于树状结构的差异可能会匹配 "函数 "和外部定界符,从而导致显示出许多令人困惑的小的变化。
可能结果:<code>function <span style="background-color: PaleGreen; color: #000">bar</span>(<span style="background-color: PaleGreen; color: #000">y</span>) { <span style="background-color: PaleGreen; color: #000">baz(y)</span>; }</code>
与滑块一样,替换问题也可能发生在基于文本的行差中。如果有少量的共同行,行差就会陷入困境。但树形差分的更精确、更细化的行为使这个问题更加普遍。
在这个例子中,我们删除了一个函数并写了一个完全不同的函数。树状结构差异分析可能会匹配 `function` 和外部定界符,从而导致高亮许多令人困惑的小变化。
与滑块一样,替代问题也可能发生在基于文本的行差中。如果有少量的共同行,行差就会陷入困境。但树状结构差异分析的精确、细化行为会使这个问题更加普遍。
## 匹配注释中的子字符串
@ -202,7 +247,7 @@ foobar();
foobaz();
```
`foobar`和`foobaz`是完全不同的,它们的共同前缀`fooba`不应该被匹配起来。然而,为注释匹配共同的前缀或后缀是可取的。
`foobar``foobaz` 完全不同,它们的共同前缀 `fooba` 不应该被匹配。然而,为注释匹配共同的前缀或后缀是可取的。
## 多行注释
@ -218,7 +263,7 @@ if (x) {
}
```
这两个注释的内部内容在技术上是不同的。然而,我们把它们当作是相同的。
这两个注释的内部内容在技术上是不同的。然而,我们期望把它们当作是相同的。
## 文档注释的换行
@ -234,7 +279,7 @@ if (x) {
* jumps over the lazy dog. */
```
里面的内容已经从 `jumps * over`变成了`immediately * jumps over`。然而,`*`是装饰性的,我们并不关心它的移动。
里面的内容已经从 `jumps * over` 变成了 `immediately * jumps over`。然而,`*` 是装饰性的,我们并不关心它的移动。
## 长字符串的小变化
@ -250,11 +295,11 @@ with lots of NOVEL words about
lots of stuff."""
```
将整个字符串字头突出显示为被删除并被一个新的字符串字头取代是正确的。然而,这让人很难看出实际改变了什么。
将整个字符串字头高亮为被删除并被一个新的字符串字头取代是正确的。然而,这让人很难看出实际改变了什么。
很明显,变量名应该被原子化处理,并且 注释是安全的可以显示子字的变化。但不清楚如何处理一个20行字符串字面的小变化。
很明显,变量名应该被元素化处理,并且注释是安全的,可以显示子字符串的变化。但不清楚如何处理一个 20 行字符串字面值的小变化。
在空格上分割字符串并加以区别是很具有挑战的,但用户仍然想知道字符串内部的空白何时改变。`" "`和`" "`是不一样的。
在空格上分割字符串并加以区别很具有挑战性,但用户仍期望知道字符串内部的空白何时改变。`" "` 与 `" "` 是不同的。
## 自动格式化工具的拼写
@ -270,13 +315,13 @@ foo(
);
```
自动格式化(例如[prettier](https://prettier.io/))有时会在格式化时添加或删除标点符号。逗号和括号是最常见的
自动格式化(例如 [Prettier](https://prettier.io/))有时会在格式化时添加或删除标点符号,其中逗号和括号最常见
语法差异可以忽略空白处的变化,但它必须假设标点符号是有意义的。这可能导致标点符号的变化被突出显示,而这可能与相关的内容变化相差甚远。
语法差异可以忽略空白处的变化,但它必须假定标点符号有意义。这可能导致标点符号的变化被突出显示,而这可能与相关的内容变化相差甚远。
## 新空行
空行对于句法差异来说是一种挑战。我们要比较的是语法标记,所以我们不会看到空行。
空行对于语法差异分析来说是一种挑战。我们要比较的是语法标记,所以我们不会看到空行。
```
// Before
@ -289,9 +334,9 @@ A
B
```
一般来说,我们望语法差异能够忽略空行。在第一个例子中,这应该不会显示任何变化。
一般来说,我们望语法差异能够忽略空行。在第一个例子中,这应该不会显示任何变化。
这有时是有问题的,因为它可以会意外地隐藏被重新格式化代码。
这有时是有问题的,因为它可以会意外地隐藏被重新格式化代码。
```
// Before
@ -306,7 +351,7 @@ Y
B
```
第二个例子中我们插入了X和Y以及一个空行。我们想把空行作为一个补充来高亮。
在第二个例子中,我们插入了 X Y 以及一个空行。我们想把空行作为一个补充来高亮。
```
// Before
@ -321,10 +366,18 @@ X
B
```
第三个例子中,语法上的差异只看到了一个增加。从用户角度来看,也有两个空行被删除。
在第三个例子中,语法差异分析只看到了一个增加。从用户角度来看,也有两个空行被删除。
## 无效语法
我们不能保证我们得到的输入是有效的语法。即使代码是有效的,它也可能使用解析器不支持的语法。
Tree-sitter可以显示出显式的错误节点而Difftastic会将它们视为原子因此它可以不顾一切地运行相同的树形差异算法。
**Difftastic**如果发生任何解析错误Difftastic 将退回到基于文本的差异,以避免差异不完整的语法树。发生这种情况时,文件头会报告错误计数。
```
$ difft sample_files/syntax_error_before.js sample_files/syntax_error_after.js
sample_files/syntax_error_after.js --- Text (2 errors, exceeded DFT_PARSE_ERROR_LIMIT)
...
```
用户可以选择通过将 `DFT_PARSE_ERROR_LIMIT` 设置为一个更大的值加入语法差异分析。在这种模式下Difftastic 会将树状差异分析的错误节点看作元素,并像通常一样进行树状差异分析。

@ -16,7 +16,7 @@ $ difft sample_files/dir_before/ sample_files/dir_after/
Difftastic 会递归地浏览这两个文件夹,对同名的文件进行差异分析。
对比的文件夹间有许多未改变的文件时,`--skip-unchanged`选项会十分有用。
对比的文件夹间有许多未改变的文件时,`--skip-unchanged` 选项会十分有用。
### 从 stdin 读取
@ -26,13 +26,22 @@ Difftastic 会递归地浏览这两个文件夹,对同名的文件进行差异
$ cat sample_files/before.js | difft - sample_files/after.js
```
### 带冲突标记的文件
*(在 0.50 版本新增)*
如果你有一个带 `<<<<<<<` 冲突标记的文件,可以将它作为一个参数传入 Difftastic。Difftastic 会构建和比较文件的两个状态。
```
$ difft sample_files/conflicts.el
```
## 语言检测
Difftastic 根据文件的扩展名、文件名和第一行的内容猜测文件所用的语言。
你可以通过 `--language` 选项覆盖语言检测。如果输入的文件有所设定的后缀, Difftastic 将会处理它们,并且忽略其他语言。
```
$ difft --language cpp before.c after.c
```

@ -0,0 +1 @@
../tree-sitter-scss/queries/highlights.scm

@ -10,12 +10,18 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
EMSCRIPTEN_VERSION: '2.0.17'
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Emscripten
uses: mymindstorm/setup-emsdk@v12
with:
version: ${{ env.EMSCRIPTEN_VERSION }}
- name: Install dependencies
run: npm install
- name: Generate parser
@ -27,6 +33,13 @@ jobs:
test -z "$diff"
- name: Run tests
run: npm test
- name: Build WASM binary
run: npm run build-wasm
- name: Upload WASM binary
uses: actions/upload-artifact@v3
with:
name: tree-sitter-kotlin.wasm
path: ./tree-sitter-kotlin.wasm
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:

@ -7,12 +7,18 @@ on:
jobs:
deploy:
runs-on: ubuntu-latest
env:
EMSCRIPTEN_VERSION: '2.0.17'
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Emscripten
uses: mymindstorm/setup-emsdk@v12
with:
version: ${{ env.EMSCRIPTEN_VERSION }}
- name: Install dependencies
run: npm install
- name: Compile grammar

@ -52,7 +52,7 @@ dependencies = [
[[package]]
name = "tree-sitter-kotlin"
version = "0.2.11"
version = "0.3.2"
dependencies = [
"cc",
"tree-sitter",

@ -1,7 +1,7 @@
[package]
name = "tree-sitter-kotlin"
description = "Kotlin grammar for the tree-sitter parsing library"
version = "0.2.11"
version = "0.3.2"
keywords = ["incremental", "parsing", "kotlin"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/fwcd/tree-sitter-kotlin"

@ -1,4 +1,4 @@
VERSION := 0.2.11
VERSION := 0.3.2
# Repository
SRC_DIR := src

@ -1,6 +1,8 @@
# Kotlin Grammar for Tree-Sitter
[![Build](https://github.com/fwcd/tree-sitter-kotlin/actions/workflows/build.yml/badge.svg)](https://github.com/fwcd/tree-sitter-kotlin/actions/workflows/build.yml)
[![NPM](https://img.shields.io/npm/v/tree-sitter-kotlin)](https://www.npmjs.com/package/tree-sitter-kotlin)
[![crates.io](https://img.shields.io/crates/v/tree-sitter-kotlin)](https://crates.io/crates/tree-sitter-kotlin)
[Kotlin](https://kotlinlang.org) language grammar for [Tree-Sitter](http://tree-sitter.github.io/tree-sitter/). You can try it out directly [on the web](https://fwcd.github.io/tree-sitter-kotlin).

@ -4,7 +4,7 @@ This crate provides a Kotlin grammar for the [tree-sitter](https://tree-sitter.g
```toml
tree-sitter = "0.20"
tree-sitter-kotlin = "0.2.11"
tree-sitter-kotlin = "0.3.2"
```
Typically, you will use the `language` function to add this grammar to a tree-sitter [`Parser`](https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html), and then use the parser to parse some code:

@ -60,14 +60,6 @@ module.exports = grammar({
// Ambiguous when used in an explicit delegation expression,
// since the '{' could either be interpreted as the class body
// or as the anonymous function body. Consider the following sequence:
//
// 'class' simple_identifier ':' user_type 'by' 'fun' '(' ')' • '{' …
//
// Possible interpretations:
//
// 'class' simple_identifier ':' user_type 'by' (anonymous_function 'fun' '(' ')' • function_body)
// 'class' simple_identifier ':' user_type 'by' (anonymous_function 'fun' '(' ')') • '{' …
[$.anonymous_function],
// Member access operator '::' conflicts with callable reference
[$._primary_expression, $.callable_reference],
@ -84,7 +76,6 @@ module.exports = grammar({
[$._postfix_unary_expression, $._expression],
// ambiguity between generics and comparison operations (foo < b > c)
[$.call_expression, $.prefix_expression, $.comparison_expression],
[$.call_expression, $.range_expression, $.comparison_expression],
[$.call_expression, $.elvis_expression, $.comparison_expression],
[$.call_expression, $.check_expression, $.comparison_expression],
@ -995,7 +986,8 @@ module.exports = grammar({
"sealed",
"annotation",
"data",
"inner"
"inner",
"value",
),
member_modifier: $ => choice(
@ -1095,6 +1087,7 @@ module.exports = grammar({
"expect",
"data",
"inner",
"value",
"actual",
"set",
"get"

@ -1,12 +1,12 @@
{
"name": "tree-sitter-kotlin",
"version": "0.2.11",
"version": "0.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-kotlin",
"version": "0.2.11",
"version": "0.3.2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "tree-sitter-kotlin",
"version": "0.2.11",
"version": "0.3.2",
"description": "Tree-Sitter grammar for Kotlin",
"main": "bindings/node",
"scripts": {

@ -5068,6 +5068,10 @@
{
"type": "STRING",
"value": "inner"
},
{
"type": "STRING",
"value": "value"
}
]
},
@ -5378,6 +5382,10 @@
"type": "STRING",
"value": "inner"
},
{
"type": "STRING",
"value": "value"
},
{
"type": "STRING",
"value": "actual"
@ -6089,9 +6097,6 @@
}
],
"conflicts": [
[
"anonymous_function"
],
[
"_primary_expression",
"callable_reference"
@ -6112,11 +6117,6 @@
"_postfix_unary_expression",
"_expression"
],
[
"call_expression",
"prefix_expression",
"comparison_expression"
],
[
"call_expression",
"range_expression",

@ -9519,6 +9519,10 @@
"type": "val",
"named": false
},
{
"type": "value",
"named": false
},
{
"type": "var",
"named": false

File diff suppressed because it is too large Load Diff

@ -358,3 +358,28 @@ data class JwtConfiguration(
(simple_identifier)
(user_type
(type_identifier))))))
================================================================================
Value class: https://kotlinlang.org/docs/inline-classes.html
================================================================================
@JvmInline
value class Password(private val s: String)
--------------------------------------------------------------------------------
(source_file
(class_declaration
(modifiers
(annotation
(user_type
(type_identifier)))
(class_modifier))
(type_identifier)
(primary_constructor
(class_parameter
(modifiers
(visibility_modifier))
(simple_identifier)
(user_type
(type_identifier))))))

@ -0,0 +1 @@
tree-sitter-scss/src

@ -0,0 +1,2 @@
/src/** linguist-vendored
/examples/* linguist-vendored

@ -0,0 +1,4 @@
node_modules
build
*.log
package-lock.json

@ -0,0 +1,26 @@
[package]
name = "tree-sitter-scss"
description = "scss grammar for the tree-sitter parsing library"
version = "0.0.1"
keywords = ["incremental", "parsing", "scss"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/tree-sitter/tree-sitter-javascript"
edition = "2018"
license = "MIT"
build = "bindings/rust/build.rs"
include = [
"bindings/rust/*",
"grammar.js",
"queries/*",
"src/*",
]
[lib]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter = "0.20"
[build-dependencies]
cc = "1.0"

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 Serenade Labs, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,5 @@
# tree-sitter-scss
SCSS grammar for [tree-sitter](https://github.com/tree-sitter/tree-sitter).
Based on [tree-sitter-css](https://github.com/tree-sitter/tree-sitter-css).

@ -0,0 +1,19 @@
{
"targets": [
{
"target_name": "tree_sitter_scss_binding",
"include_dirs": [
"<!(node -e \"require('nan')\")",
"src"
],
"sources": [
"src/parser.c",
"src/scanner.c",
"bindings/node/binding.cc"
],
"cflags_c": [
"-std=c99",
]
}
]
}

@ -0,0 +1,28 @@
#include "tree_sitter/parser.h"
#include <node.h>
#include "nan.h"
using namespace v8;
extern "C" TSLanguage * tree_sitter_scss();
namespace {
NAN_METHOD(New) {}
void Init(Local<Object> exports, Local<Object> module) {
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
tpl->SetClassName(Nan::New("Language").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_scss());
Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("scss").ToLocalChecked());
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
}
NODE_MODULE(tree_sitter_scss_binding, Init)
} // namespace

@ -0,0 +1,19 @@
try {
module.exports = require("../../build/Release/tree_sitter_scss_binding");
} catch (error1) {
if (error1.code !== 'MODULE_NOT_FOUND') {
throw error1;
}
try {
module.exports = require("../../build/Debug/tree_sitter_scss_binding");
} catch (error2) {
if (error2.code !== 'MODULE_NOT_FOUND') {
throw error2;
}
throw error1
}
}
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

@ -0,0 +1,16 @@
fn main() {
let src_dir = std::path::Path::new("src");
let mut c_config = cc::Build::new();
c_config.include(&src_dir);
c_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable")
.flag_if_supported("-Wno-trigraphs");
let parser_path = src_dir.join("parser.c");
let scanner_path = src_dir.join("scanner.c");
c_config.file(&parser_path);
c_config.file(&scanner_path);
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
c_config.compile("parser");
}

@ -0,0 +1,52 @@
//! This crate provides scss language support for the [tree-sitter][] parsing library.
//!
//! Typically, you will use the [language][language func] function to add this language to a
//! tree-sitter [Parser][], and then use the parser to parse some code:
//!
//! ```
//! let code = "";
//! let mut parser = tree_sitter::Parser::new();
//! parser.set_language(tree_sitter_scss::language()).expect("Error loading scss grammar");
//! let tree = parser.parse(code, None).unwrap();
//! ```
//!
//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
//! [language func]: fn.language.html
//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
//! [tree-sitter]: https://tree-sitter.github.io/
use tree_sitter::Language;
extern "C" {
fn tree_sitter_scss() -> Language;
}
/// Get the tree-sitter [Language][] for this grammar.
///
/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
pub fn language() -> Language {
unsafe { tree_sitter_scss() }
}
/// The content of the [`node-types.json`][] file for this grammar.
///
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json");
// Uncomment these to include any queries that this grammar contains
// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm");
// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm");
// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm");
#[cfg(test)]
mod tests {
#[test]
fn test_can_load_grammar() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(super::language())
.expect("Error loading scss language");
}
}

@ -0,0 +1,379 @@
==========================
Function calls
==========================
a {
color: rgba(0, 255, 0, 0.5);
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration
(property_name)
(call_expression (function_name) (arguments
(integer_value)
(integer_value)
(integer_value)
(float_value)))))))
=============================================
Calls where each argument has multiple values
=============================================
div {
background: repeating-linear-gradient(red, orange 50px);
clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
}
---
(stylesheet
(rule_set (selectors (tag_name)) (block
(declaration
(property_name)
(call_expression (function_name) (arguments
(plain_value)
(plain_value)
(integer_value (unit)))))
(declaration
(property_name)
(call_expression (function_name) (arguments
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))))))))
============================
Color literals
============================
a {
b: #fafd04;
c: #fafd0401;
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration (property_name) (color_value))
(declaration (property_name) (color_value)))))
============================
Numbers
============================
a {
b: 0.5%;
c: 5em;
margin: 10E3px;
margin: -456.8px;
margin: -5px;
margin: -0.0px;
}
---
(stylesheet
(rule_set (selectors (tag_name)) (block
(declaration (property_name) (float_value (unit)))
(declaration (property_name) (integer_value (unit)))
(declaration (property_name) (float_value (unit)))
(declaration (property_name) (float_value (unit)))
(declaration (property_name) (integer_value (unit)))
(declaration (property_name) (float_value (unit))))))
============================
Binary arithmetic operators
============================
a {
width: calc(100% - 80px);
aspect-ratio: 1/2;
font-size: calc(10px + (56 - 10) * ((100vw - 320px) / (1920 - 320)));
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration
(property_name)
(call_expression (function_name) (arguments (binary_expression (integer_value (unit)) (integer_value (unit))))))
(declaration
(property_name)
(binary_expression (integer_value) (integer_value)))
(declaration
(property_name)
(call_expression
(function_name)
(arguments
(binary_expression
(binary_expression
(integer_value (unit))
(parenthesized_value (binary_expression (integer_value) (integer_value))))
(parenthesized_value
(binary_expression
(parenthesized_value (binary_expression (integer_value (unit)) (integer_value (unit))))
(parenthesized_value (binary_expression (integer_value) (integer_value))))))))))))
============================
Strings
============================
a {
b: '';
c: '\'hi\'';
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration (property_name) (string_value))
(declaration (property_name) (string_value)))))
============================
URLs
============================
a {
b: http://something-else?foo=bar;
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration (property_name) (plain_value)))))
============================
Important declarations
============================
a {
b: c !important;
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration (property_name) (plain_value) (important)))))
============================
Declarations without trailing semicolons
============================
a {
b: c;
d: e
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration (property_name) (plain_value))
(declaration (property_name) (plain_value)))))
=======================================
Comments right after numbers
=======================================
a {
shape-outside: circle(20em/*=*/at 50% 50%);
shape-outside: inset(1em, 1em, 1em, 1em);
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(declaration (property_name) (call_expression (function_name) (arguments
(integer_value (unit))
(comment)
(plain_value)
(integer_value (unit))
(integer_value (unit)))))
(declaration (property_name) (call_expression (function_name) (arguments
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))
(integer_value (unit))))))))
=================================
Declarations at the top level
=================================
--a-variable: -5px;
a-property: calc(5px + var(--a-variable));
---
(stylesheet
(declaration (property_name) (integer_value (unit)))
(declaration (property_name) (call_expression (function_name) (arguments (binary_expression (integer_value (unit)) (call_expression (function_name) (arguments (plain_value))))))))
=================================
Variables
=================================
$font-stack: Helvetica, sans-serif;
a {
$primary-color: red;
color: $primary-color;
font: 100% $font-stack;
}
---
(stylesheet
(declaration (variable_name) (plain_value) (plain_value))
(rule_set
(selectors (tag_name))
(block
(declaration (variable_name) (plain_value))
(declaration (property_name) (variable_value))
(declaration (property_name) (integer_value (unit)) (variable_value)))))
=================================
Mixins with no arguments
=================================
@mixin red {
color: red;
}
---
(stylesheet
(mixin_statement (name)
(block
(declaration (property_name) (plain_value)))))
=================================
Mixins with arguments
=================================
@mixin replace-text($image, $color: red) {
text-align: left;
}
---
(stylesheet
(mixin_statement
(name)
(parameters
(parameter (variable_name))
(parameter (variable_name) (default_value (plain_value))))
(block (declaration (property_name) (plain_value)))))
=================================
Mixins with interpolations and content
=================================
@mixin media($queryString) {
@media #{$queryString} {
@content;
}
}
---
(stylesheet
(mixin_statement
(name)
(parameters (parameter (variable_name)))
(block (media_statement (keyword_query) (block (at_rule (at_keyword)))))))
=================================
Mixins with at-root
=================================
@mixin unify-parent($child) {
@at-root #{selector.unify(&, $child)} {
@content;
}
}
---
(stylesheet
(mixin_statement
(name)
(parameters (parameter (variable_name)))
(block
(at_root_statement (plain_value) (block (at_rule (at_keyword)))))))
=================================
Placeholders
=================================
%bar {
width: 100%;
}
---
(stylesheet
(placeholder
(name)
(block
(declaration (property_name) (integer_value (unit))))))
=============================================
Spaces after colons in property declarations
=============================================
div {
margin : 0;
padding : 0;
}
---
(stylesheet
(rule_set
(selectors
(tag_name))
(block
(declaration
(property_name)
(integer_value))
(declaration
(property_name)
(integer_value)))))

@ -0,0 +1,23 @@
=========================
Universal selectors
=========================
@import './styles/reset.scss';
@import './styles/main.scss';
#app {
-webkit-font-smoothing: antialiased;
}
.MoreChancesModal {
&_Title {
@extend .t--heading-tertiary;
}
&_Spacer {
background: red;
}
}
---
(stylesheet (import_statement (string_value)) (import_statement (string_value)) (rule_set (selectors (id_selector (id_name))) (block (declaration (property_name) (plain_value)))) (rule_set (selectors (class_selector (class_name))) (block (rule_set (selectors (class_selector (nesting_selector) (class_name))) (block (extend_statement (class_selector (class_name))))) (rule_set (selectors (class_selector (nesting_selector) (class_name))) (block (declaration (property_name) (plain_value)))))))

@ -0,0 +1,224 @@
=========================
Universal selectors
=========================
* {}
---
(stylesheet
(rule_set (selectors (universal_selector)) (block)))
=========================
Type selectors
=========================
div, span {}
h1, h2, h3, h4 {}
---
(stylesheet
(rule_set (selectors (tag_name) (tag_name)) (block))
(rule_set (selectors (tag_name) (tag_name) (tag_name) (tag_name)) (block)))
=========================
Class selectors
=========================
.class-a {}
div.class-b, .class-c.class-d {}
---
(stylesheet
(rule_set
(selectors (class_selector (class_name)))
(block))
(rule_set
(selectors
(class_selector (tag_name) (class_name))
(class_selector (class_selector (class_name)) (class_name)))
(block)))
=========================
Id selectors
=========================
#some-id, a#another-id {}
---
(stylesheet
(rule_set
(selectors (id_selector (id_name)) (id_selector (tag_name) (id_name)))
(block)))
=========================
Attribute selectors
=========================
[a] {}
[b=c] {}
[d~=e] {}
a[b] {}
---
(stylesheet
(rule_set (selectors (attribute_selector (attribute_name))) (block))
(rule_set (selectors (attribute_selector (attribute_name) (plain_value))) (block))
(rule_set (selectors (attribute_selector (attribute_name) (plain_value))) (block))
(rule_set (selectors (attribute_selector (tag_name) (attribute_name))) (block)))
=========================
Pseudo-class selectors
=========================
a:hover {}
:nth-child(2) {}
---
(stylesheet
(rule_set
(selectors (pseudo_class_selector (tag_name) (class_name)))
(block))
(rule_set
(selectors (pseudo_class_selector (class_name) (arguments (integer_value))))
(block)))
=========================
Pseudo-element selectors
=========================
a::first-line {}
---
(stylesheet
(rule_set
(selectors (pseudo_element_selector (tag_name) (tag_name)))
(block)))
=========================
Child selectors
=========================
a > b {}
c > d > e {}
---
(stylesheet
(rule_set
(selectors (child_selector (tag_name) (tag_name)))
(block))
(rule_set
(selectors (child_selector
(child_selector (tag_name) (tag_name))
(tag_name)))
(block)))
=========================
Descendant selectors
=========================
a b {}
c d e {}
---
(stylesheet
(rule_set
(selectors (descendant_selector (tag_name) (tag_name)))
(block))
(rule_set
(selectors (descendant_selector
(descendant_selector (tag_name) (tag_name))
(tag_name)))
(block)))
===========================
Nesting selectors
===========================
a {
&.b {}
& c {}
& > d {}
&e {}
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(rule_set (selectors (class_selector (nesting_selector) (class_name))) (block))
(rule_set (selectors (descendant_selector (nesting_selector) (tag_name))) (block))
(rule_set (selectors (child_selector (nesting_selector) (tag_name))) (block))
(rule_set (selectors (class_selector (nesting_selector) (class_name))) (block)))))
===========================
Sibling selectors
===========================
a.b ~ c.d {}
.e.f + .g.h {}
---
(stylesheet
(rule_set
(selectors (sibling_selector
(class_selector (tag_name) (class_name))
(class_selector (tag_name) (class_name))))
(block))
(rule_set
(selectors (adjacent_sibling_selector
(class_selector (class_selector (class_name)) (class_name))
(class_selector (class_selector (class_name)) (class_name))))
(block)))
===========================
The :not selector
===========================
a:not(:hover) {}
.b:not(c > .d) {}
---
(stylesheet
(rule_set
(selectors (pseudo_class_selector
(tag_name)
(class_name)
(arguments (pseudo_class_selector (class_name)))))
(block))
(rule_set
(selectors (pseudo_class_selector
(class_selector (class_name))
(class_name)
(arguments (child_selector (tag_name) (class_selector (class_name))))))
(block)))
===========================
Parent selectors
===========================
html {
.foo & {}
}
---
(stylesheet
(rule_set
(selectors (tag_name))
(block
(rule_set
(selectors (descendant_selector (class_selector (class_name)) (nesting_selector)))
(block)))))

@ -0,0 +1,526 @@
==============================
Import statements
==============================
@import url("fineprint.css") print;
@import url("bluish.css") speech;
@import 'custom.css';
@import url("chrome://communicator/skin/");
@import "common.css" screen;
---
(stylesheet
(import_statement (call_expression (function_name) (arguments (string_value))) (keyword_query))
(import_statement (call_expression (function_name) (arguments (string_value))) (keyword_query))
(import_statement (string_value))
(import_statement (call_expression (function_name) (arguments (string_value))))
(import_statement (string_value) (keyword_query)))
==============================
Namespace statements
==============================
/* Default namespace */
@namespace url(XML-namespace-URL);
@namespace "XML-namespace-URL";
@namespace url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);
/* Prefixed namespace */
@namespace prefix url(XML-namespace-URL);
@namespace prefix "XML-namespace-URL";
---
(stylesheet
(comment)
(namespace_statement (call_expression (function_name) (arguments (plain_value))))
(namespace_statement (string_value))
(namespace_statement (call_expression (function_name) (arguments (plain_value))))
(namespace_statement (namespace_name) (call_expression (function_name) (arguments (plain_value))))
(comment)
(namespace_statement (namespace_name) (call_expression (function_name) (arguments (plain_value))))
(namespace_statement (namespace_name) (string_value)))
==============================
Keyframes statements
==============================
@keyframes important1 {
from { margin-top: 50px; }
50% { margin-top: 150px !important; } /* ignored */
to { margin-top: 100px; }
}
---
(stylesheet
(keyframes_statement (keyframes_name) (keyframe_block_list
(keyframe_block (from) (block (declaration (property_name) (integer_value (unit)))))
(keyframe_block (integer_value (unit)) (block (declaration (property_name) (integer_value (unit)) (important))))
(comment)
(keyframe_block (to) (block (declaration (property_name) (integer_value (unit))))))))
==============================
Media statements
==============================
@media screen and (min-width: 30em) and (orientation: landscape) {}
@media (min-height: 680px), screen and (orientation: portrait) {}
@media not all and (monochrome) {}
@media only screen {}
---
(stylesheet
(media_statement
(binary_query
(binary_query
(keyword_query)
(feature_query (feature_name) (integer_value (unit))))
(feature_query (feature_name) (plain_value)))
(block))
(media_statement
(feature_query (feature_name) (integer_value (unit)))
(binary_query (keyword_query) (feature_query (feature_name) (plain_value)))
(block))
(media_statement
(binary_query (unary_query (keyword_query)) (parenthesized_query (keyword_query)))
(block))
(media_statement (unary_query (keyword_query)) (block)))
==============================
Supports statements
==============================
@supports (animation-name: test) {
div { animation-name: test; }
}
@supports (transform-style: preserve) or (-moz-transform-style: preserve) {}
@supports not ((text-align-last: justify) or (-moz-text-align-last: justify)) {}
@supports not selector(:matches(a, b)) {}
---
(stylesheet
(supports_statement
(feature_query (feature_name) (plain_value))
(block
(rule_set (selectors (tag_name)) (block
(declaration (property_name) (plain_value))))))
(supports_statement
(binary_query
(feature_query (feature_name) (plain_value))
(feature_query (feature_name) (plain_value)))
(block))
(supports_statement
(unary_query (parenthesized_query (binary_query
(feature_query (feature_name) (plain_value))
(feature_query (feature_name) (plain_value)))))
(block))
(supports_statement
(unary_query (selector_query (pseudo_class_selector
(class_name)
(arguments (tag_name) (tag_name)))))
(block)))
==============================
Charset statements
==============================
@charset "utf-8";
---
(stylesheet
(charset_statement (string_value)))
==============================
Other at-statements
==============================
@font-face {
font-family: "Open Sans";
src: url("/a") format("woff2"), url("/b/c") format("woff");
}
---
(stylesheet
(at_rule
(at_keyword)
(block
(declaration (property_name) (string_value))
(declaration (property_name)
(call_expression (function_name) (arguments (string_value)))
(call_expression (function_name) (arguments (string_value)))
(call_expression (function_name) (arguments (string_value)))
(call_expression (function_name) (arguments (string_value)))))))
==============================
Single-line comments
==============================
// https://awardwinningfjords.com/2010/04/09/example-scss-sassy-css-file.html
html {
background: url(https://google.com);
}
---
(stylesheet
(single_line_comment)
(rule_set
(selectors (tag_name))
(block
(declaration (property_name) (call_expression (function_name) (arguments (plain_value)))))))
==============================
Use statements
==============================
@use 'custom.css';
.inverse {
background-color: base.$primary-color;
color: white;
}
---
(stylesheet
(use_statement (string_value))
(rule_set
(selectors (class_selector (class_name)))
(block
(declaration (property_name) (variable_value))
(declaration (property_name) (plain_value)))))
==============================
Forward statements
==============================
@forward 'custom.css';
---
(stylesheet
(forward_statement (string_value)))
==============================
Include statements
==============================
.foo {
@include bar;
@include transform(50px, $float: left);
}
---
(stylesheet
(rule_set
(selectors (class_selector (class_name)))
(block
(include_statement (identifier))
(include_statement (identifier)
(arguments
(argument (argument_value (integer_value (unit))))
(argument (argument_name) (argument_value (plain_value))))))))
==============================
Extend statements
==============================
.foo {
@extend red;
.bar {
@extend blue;
@extend .green;
}
}
---
(stylesheet
(rule_set
(selectors (class_selector (class_name)))
(block
(extend_statement (plain_value))
(rule_set
(selectors (class_selector (class_name)))
(block (extend_statement (plain_value))
(extend_statement (class_selector (class_name))))))))
==============================
Apply statements
==============================
@apply fill-red-50 border-black col-span-1;
---
(stylesheet
(apply_statement (plain_value) (plain_value) (plain_value)))
=================================
Operators
=================================
article[role="main"] {
width: 600px / 960px * 100%;
}
---
(stylesheet
(rule_set
(selectors (attribute_selector (tag_name) (attribute_name) (string_value)))
(block
(declaration (property_name)
(binary_expression
(binary_expression (integer_value (unit)) (integer_value (unit))) (integer_value (unit)))))))
=================================
Interpolation
=================================
.icon-#{$name} {
position: absolute;
#{$top-or-bottom}: 0;
}
---
(stylesheet
(rule_set
(selectors (class_selector (class_name)))
(block
(declaration (property_name) (plain_value))
(declaration (property_name) (integer_value)))))
=================================
If statements
=================================
@mixin avatar($size, $circle: false) {
width: $size;
height: $size;
@if $circle {
border-radius: $size / 2;
}
}
---
(stylesheet
(mixin_statement
(name)
(parameters
(parameter (variable_name))
(parameter (variable_name) (default_value (plain_value))))
(block
(declaration (property_name) (variable_value))
(declaration (property_name) (variable_value))
(if_statement
(if_clause
(condition (variable_value))
(block
(declaration (property_name) (binary_expression (variable_value) (integer_value)))))))))
=================================
Else statements
=================================
@mixin theme-colors($light-theme: true) {
@if $light-theme {
color: $light-text;
} @else {
color: $dark-text;
}
}
---
(stylesheet
(mixin_statement
(name)
(parameters
(parameter (variable_name) (default_value (plain_value))))
(block
(if_statement
(if_clause
(condition (variable_value))
(block (declaration (property_name) (variable_value))))
(else_clause
(block (declaration (property_name) (variable_value))))))))
=================================
Else if statements
=================================
@mixin triangle($direction) {
@if $direction == up {
border-bottom-color: $color;
} @else if $direction == right {
border-left-color: $color;
} @else if $direction == down {
border-top-color: $color;
} @else {
border-right-color: $color;
}
}
---
(stylesheet
(mixin_statement
(name)
(parameters
(parameter (variable_name)))
(block
(if_statement
(if_clause
(condition (binary_expression (variable_value) (plain_value)))
(block (declaration (property_name) (variable_value))))
(else_if_clause
(condition (binary_expression (variable_value) (plain_value)))
(block (declaration (property_name) (variable_value))))
(else_if_clause
(condition (binary_expression (variable_value) (plain_value)))
(block (declaration (property_name) (variable_value))))
(else_clause
(block (declaration (property_name) (variable_value))))))))
=================================
Each statements
=================================
@each $size in $sizes {
.icon-#{$size} {
font-size: $size;
}
}
---
(stylesheet
(each_statement
(value)
(variable_value)
(block
(rule_set
(selectors (class_selector (class_name)))
(block (declaration (property_name) (variable_value)))))))
=================================
For statements
=================================
@for $i from 1 through 3 {
background-color: lighten($base-color, $i * 5%);
}
---
(stylesheet
(for_statement
(variable)
(from (integer_value))
(through (integer_value))
(block
(declaration
(property_name)
(call_expression
(function_name)
(arguments (variable_value)
(binary_expression (variable_value) (integer_value (unit)))))))))
=================================
While statements
=================================
@while $i < 3 {
background-color: lighten($base-color, $i * 5%);
$i: $i + 1;
}
---
(stylesheet
(while_statement
(binary_expression (variable_value) (integer_value))
(block
(declaration
(property_name)
(call_expression
(function_name)
(arguments (variable_value) (binary_expression (variable_value) (integer_value (unit))))))
(declaration (variable_name) (binary_expression (variable_value) (integer_value))))))
=================================
Functions
=================================
@function pow($base, $exponent) {
$result: 1;
@for $_ from 1 through $exponent {
$result: $result * $base;
}
@return $result;
}
.sidebar {
float: left;
margin-left: pow(4, 3) * 1px;
}
---
(stylesheet
(function_statement
(name)
(parameters
(parameter (variable_name))
(parameter (variable_name)))
(block
(declaration (variable_name) (integer_value))
(for_statement
(variable)
(from (integer_value))
(through (variable_value))
(block
(declaration (variable_name) (binary_expression (variable_value) (variable_value)))))
(return_statement (variable_value))))
(rule_set
(selectors (class_selector (class_name)))
(block
(declaration (property_name) (plain_value))
(declaration
(property_name)
(binary_expression
(call_expression (function_name) (arguments (integer_value) (integer_value)))
(integer_value (unit)))))))
=================================
Logging statements
=================================
@mixin foo {
@debug "debug";
@warn "warn";
@error "error";
}
---
(stylesheet
(mixin_statement
(name)
(block
(debug_statement (string_value))
(warn_statement (string_value))
(error_statement (string_value)))))

@ -0,0 +1,15 @@
============================
Rule sets
============================
#some-id {
some-property: 5px;
}
---
(stylesheet
(rule_set
(selectors (id_selector (id_name)))
(block
(declaration (property_name) (integer_value (unit))))))

File diff suppressed because it is too large Load Diff

@ -0,0 +1,283 @@
// from https://awardwinningfjords.com/2010/04/09/example-scss-sassy-css-file.html
@import "compass";
@import "blueprint";
@import "blueprint/fancy_type";
$blueprint-grid-columns: 5;
$blueprint-grid-width: 150px;
$blueprint-grid-margin: 20px;
$blueprint-font-family: Helvetica Neue, Arial, Helvetica, sans-serif;
$blueprint-fixed-font-family:'andale mono', 'lucida console', monospace;
$blueprint-font-size: 12px;
$text-color: #555555;
$light-text-color: #a0a0a0;
$quote-text-color: #f27a00;
$disclosure-text-color: #ed7c06;
$thick-border: 8px solid black;
$dotted-border: 1px dotted #999999;
$header-background-color: #f07c05;
$paging-background-color: black;
$unfocused-background-color: #f1f0ec;
$quote-background-color: #f1f0ec;
$story-title-color: #f07c05;
@include global-reset;
@include blueprint-typography;
body {
background: $unfocused-background-color image_url("white-bg.jpg") repeat-y 50% 0;
color: $text-color;
}
.content_wrapper {
@include container;
width: 910px;
}
#header {
height: 72px;
background: $header-background-color;
@include clearfix;
h1 {
color: white;
@include float-left;
a {
@include replace-text("logo.jpg");
width: 116px;
height: 72px;
display: block;
text-decoration: none;
}
}
ul {
@include horizontal-list;
padding: 0 0 0 25px;
li {
@include incr(18px);
padding: 0 20px 0 0;
text-transform: uppercase;
a {
color: #febf0f;
text-decoration: none;
width: 100px;
height: 72px;
display: block;
}
&#header-threads a {
@include replace-text("header-threads-text.jpg");
}
&#header-timeline a {
@include replace-text("header-timeline-text.jpg");
}
}
}
}
#paging {
background: $paging-background-color;
height: 33px;
padding: 15px 0 0 0;
h6 {
@include float-left;
color: #908f8b;
padding-right: 20px;
text-transform: uppercase;
a {
color: #f07b07;
text-decoration: none;
}
}
ul {
@include horizontal-list;
li {
padding: 3px 4px;
a {
color: #4c4c4c;
@include replace-text("thread-paging-inactive-bullet.jpg");
width: 10px;
height: 10px;
display: block;
}
&.active a {
color: #f17d06;
background-image: image_@import "compass";
}
}
}
}
#content {
position: relative;
#previous {
position: absolute;
top: 0;
left: 0;
text-align: right;
}
#next {
position: absolute;
top: 0;
right: 0;
text-align: left;
}
}
#thread {
@include container;
width: 910px;
top: 0px;
left: 0px;
@include transition-property(left);
@include transition-duration(0.5s);
@include transition-timing-function(ease-in-out);
@for $i from 0 through 30 {
&.position#{$i} {
left: ($i * -910px);
}
}
}
.js {
#content {
overflow: hidden;
width: 100%;
}
#thread {
padding: 0;
width: 5000px;
position: absolute;
}
}
.story {
@include float-left;
width: 810px;
padding: 0 50px;
h4.date {
@include float-right;
@include span(1);
background: $unfocused-background-color;
padding: 10px 20px;
@include border-radius(3px);
}
h1 {
padding: 40px 0 20px 0;
border-bottom: $thick-border;
color: $story-title-color;
@include incr(30px);
}
.artifacts {
@include column(3);
.row {
@include clearfix;
border-bottom: $dotted-border;
margin-bottom: $blueprint-grid-margin;
}
.artifact {
color: $light-text-color;
img {
margin-bottom: 0.5em;
}
a {
font-weight: bold;
text-decoration: none;
color: #0a83e0;
}
}
.threecol { @include column(3); }
.twocol { @include column(2); }
.onecol { @include column(1); }
.last { @include last; }
}
.information {
@include column(1.6, true);
border-bottom: $dotted-border;
margin-left: 40px;
h2 {
@include incr(20px, $blueprint-font-size, 35px);
}
.textblock p {
@include incr(13px, $blueprint-font-size, 26px);
}
.quote {
color: $quote-text-color;
blockquote {
@include incr(18px, $blueprint-font-size, 30px);
color: $quote-text-color;
margin: 0;
padding: 12px 18px;
background: $quote-background-color;
@include border-radius(5px);
}
cite {
display: block;
padding: 0.5em 18px 1.5em 18px;
}
}
.disclosure {
border-top: $dotted-border;
h5 {
padding: 10px 0;
margin: 0;
color: $disclosure-text-color;
}
p {
color: #a8a8a8;
}
}
}
}
#footer {
@include container;
background: white;
width: 810px;
padding: $blueprint-grid-margin 0;
.inner {
border-top: $thick-border;
padding: 30px 0;
}
p {
@include float-right;
@include incr(10px);
color: #b3b3b3;
}
ul {
@include horizontal-list;
li {
padding: 0 30px 0 0;
a {
text-decoration: none;
color: #ef7a06;
}
}
}
}

@ -0,0 +1,57 @@
// from https://scotch.io/tutorials/getting-started-with-sass
// ----
// Sass (v3.4.4)
// Compass (v1.0.1)
// ----
@mixin renderGridStyles($settings) {
.container {
padding-right: map-get($settings, "margin");
padding-left: map-get($settings, "margin");
margin-right: auto;
margin-left: auto;
max-width: map-get($settings, "maxWidth");
}
.row {
margin-right: map-get($settings, "margin") * -1;
margin-left: map-get($settings, "margin") * -1;
}
$breakpoints: map-get($settings, "breakpoints");
@each $key, $breakpoint in $breakpoints {
@include media($breakpoint) {
@include renderGrid($key, $settings);
}
}
}
@mixin renderGrid($key, $settings) {
$i: 1;
@while $i <= map-get($settings, "columns") {
.col-#{$key}-#{$i} {
float: left;
width: 100% * $i / map-get($settings, "columns");
}
$i: $i + 1;
}
}
@mixin media($queryString) {
@media #{$queryString} {
@content;
}
}
@include renderGridStyles($settings);
p {
padding: 20px;
color: white;
background: #9b59b6;
margin: 20px;
}

@ -0,0 +1,398 @@
module.exports = grammar({
name: "scss",
extras: ($) => [/\s/, $.comment, $.single_line_comment],
externals: ($) => [$._descendant_operator],
conflicts: ($) => [[$._selector, $.declaration]],
inline: ($) => [$._top_level_item, $._block_item],
rules: {
stylesheet: ($) => repeat($._top_level_item),
_top_level_item: ($) =>
choice(
$.declaration,
$.rule_set,
$.import_statement,
$.media_statement,
$.charset_statement,
$.namespace_statement,
$.keyframes_statement,
$.supports_statement,
$.use_statement,
$.forward_statement,
$.apply_statement,
$.mixin_statement,
$.include_statement,
$.if_statement,
$.each_statement,
$.for_statement,
$.while_statement,
$.function_statement,
$.error_statement,
$.warn_statement,
$.debug_statement,
$.at_rule,
$.placeholder
),
// Statements
import_statement: ($) => seq("@import", $._value, sep(",", $._query), ";"),
media_statement: ($) => seq("@media", sep1(",", $._query), $.block),
charset_statement: ($) => seq("@charset", $._value, ";"),
namespace_statement: ($) =>
seq(
"@namespace",
optional(alias($.identifier, $.namespace_name)),
choice($.string_value, $.call_expression),
";"
),
keyframes_statement: ($) =>
seq(
choice("@keyframes", alias(/@[-a-z]+keyframes/, $.at_keyword)),
alias($.identifier, $.keyframes_name),
$.keyframe_block_list
),
keyframe_block_list: ($) => seq("{", repeat($.keyframe_block), "}"),
keyframe_block: ($) => seq(choice($.from, $.to, $.integer_value), $.block),
from: ($) => "from",
to: ($) => "to",
supports_statement: ($) => seq("@supports", $._query, $.block),
at_rule: ($) => seq($.at_keyword, sep(",", $._query), choice(";", $.block)),
use_statement: ($) => seq("@use", $._value, ";"),
forward_statement: ($) => seq("@forward", $._value, ";"),
apply_statement: ($) => seq("@apply", repeat($._value), ";"),
parameters: ($) => seq("(", sep1(",", $.parameter), ")"),
parameter: ($) =>
seq(
alias($.variable_identifier, $.variable_name),
optional(seq(":", alias($._value, $.default_value)))
),
mixin_statement: ($) =>
seq("@mixin", alias($.identifier, $.name), optional($.parameters), $.block),
include_statement: ($) =>
seq(
"@include",
$.identifier,
optional(alias($.include_arguments, $.arguments)),
choice($.block, ";")
),
include_arguments: ($) =>
seq(
token.immediate("("),
sep1(",", alias($.include_argument, $.argument)),
token.immediate(")")
),
include_argument: ($) =>
seq(
optional(seq(alias($.variable_identifier, $.argument_name), ":")),
alias($._value, $.argument_value)
),
placeholder: ($) => seq("%", alias($.identifier, $.name), $.block),
extend_statement: ($) => seq("@extend", choice($._value, $.class_selector), ";"),
if_statement: ($) => seq($.if_clause, repeat($.else_if_clause), optional($.else_clause)),
if_clause: ($) => seq("@if", alias($._value, $.condition), $.block),
else_if_clause: ($) => seq("@else", "if", alias($._value, $.condition), $.block),
else_clause: ($) => seq("@else", $.block),
each_statement: ($) =>
seq(
"@each",
optional(seq(alias($.variable_identifier, $.key), ",")),
alias($.variable_identifier, $.value),
"in",
$._value,
$.block
),
for_statement: ($) =>
seq(
"@for",
alias($.variable_identifier, $.variable),
"from",
alias($._value, $.from),
"through",
alias($._value, $.through),
$.block
),
while_statement: ($) => seq("@while", $._value, $.block),
function_statement: ($) =>
seq("@function", alias($.identifier, $.name), optional($.parameters), $.block),
return_statement: ($) => seq("@return", $._value, ";"),
at_root_statement: ($) => seq("@at-root", $._value, $.block),
error_statement: ($) => seq("@error", $._value, ";"),
warn_statement: ($) => seq("@warn", $._value, ";"),
debug_statement: ($) => seq("@debug", $._value, ";"),
// Rule sets
rule_set: ($) => seq($.selectors, $.block),
selectors: ($) => sep1(",", $._selector),
block: ($) =>
seq("{", repeat($._block_item), optional(alias($.last_declaration, $.declaration)), "}"),
_block_item: ($) =>
choice(
$.declaration,
$.rule_set,
$.import_statement,
$.media_statement,
$.charset_statement,
$.namespace_statement,
$.keyframes_statement,
$.supports_statement,
$.mixin_statement,
$.include_statement,
$.extend_statement,
$.if_statement,
$.each_statement,
$.for_statement,
$.while_statement,
$.function_statement,
$.return_statement,
$.at_root_statement,
$.error_statement,
$.warn_statement,
$.debug_statement,
$.at_rule
),
// Selectors
_selector: ($) =>
choice(
$.universal_selector,
alias($.identifier, $.tag_name),
$.class_selector,
$.nesting_selector,
$.pseudo_class_selector,
$.pseudo_element_selector,
$.id_selector,
$.attribute_selector,
$.string_value,
$.child_selector,
$.descendant_selector,
$.sibling_selector,
$.adjacent_sibling_selector
),
nesting_selector: ($) => "&",
universal_selector: ($) => "*",
class_selector: ($) =>
prec(1, seq(optional($._selector), choice(".", $.nesting_selector), alias($.identifier, $.class_name))),
pseudo_class_selector: ($) =>
seq(
optional($._selector),
":",
alias($.identifier, $.class_name),
optional(alias($.pseudo_class_arguments, $.arguments))
),
pseudo_element_selector: ($) =>
seq(optional($._selector), "::", alias($.identifier, $.tag_name)),
id_selector: ($) => seq(optional($._selector), "#", alias($.identifier, $.id_name)),
attribute_selector: ($) =>
seq(
optional($._selector),
"[",
alias($.identifier, $.attribute_name),
optional(seq(choice("=", "~=", "^=", "|=", "*=", "$="), $._value)),
"]"
),
child_selector: ($) => prec.left(seq($._selector, ">", $._selector)),
descendant_selector: ($) => prec.left(seq($._selector, $._descendant_operator, $._selector)),
sibling_selector: ($) => prec.left(seq($._selector, "~", $._selector)),
adjacent_sibling_selector: ($) => prec.left(seq($._selector, "+", $._selector)),
pseudo_class_arguments: ($) =>
seq(token.immediate("("), sep(",", choice($._selector, repeat1($._value))), ")"),
// Declarations
declaration: ($) =>
seq(
choice(alias($.variable_identifier, $.variable_name), alias($.identifier, $.property_name)),
":",
$._value,
repeat(seq(optional(","), $._value)),
optional($.important),
";"
),
last_declaration: ($) =>
prec(
1,
seq(
alias($.identifier, $.property_name),
":",
$._value,
repeat(seq(optional(","), $._value)),
optional($.important)
)
),
important: ($) => "!important",
// Media queries
_query: ($) =>
choice(
alias($.identifier, $.keyword_query),
$.feature_query,
$.binary_query,
$.unary_query,
$.selector_query,
$.parenthesized_query
),
feature_query: ($) =>
seq("(", alias($.identifier, $.feature_name), ":", repeat1($._value), ")"),
parenthesized_query: ($) => seq("(", $._query, ")"),
binary_query: ($) => prec.left(seq($._query, choice("and", "or"), $._query)),
unary_query: ($) => prec(1, seq(choice("not", "only"), $._query)),
selector_query: ($) => seq("selector", "(", $._selector, ")"),
// Property Values
_value: ($) =>
prec(
-1,
choice(
alias($.identifier, $.plain_value),
alias($.variable_identifier, $.variable_value),
$.plain_value,
$.color_value,
$.integer_value,
$.float_value,
$.string_value,
$.binary_expression,
$.parenthesized_value,
$.call_expression
)
),
parenthesized_value: ($) => seq("(", $._value, ")"),
color_value: ($) => seq("#", token.immediate(/[0-9a-fA-F]{3,8}/)),
string_value: ($) =>
token(choice(seq("'", /([^'\n]|\\(.|\n))*/, "'"), seq('"', /([^"\n]|\\(.|\n))*/, '"'))),
integer_value: ($) => seq(token(seq(optional(choice("+", "-")), /\d+/)), optional($.unit)),
float_value: ($) =>
seq(
token(
seq(
optional(choice("+", "-")),
/\d*/,
choice(
seq(".", /\d+/),
seq(/[eE]/, optional("-"), /\d+/),
seq(".", /\d+/, /[eE]/, optional("-"), /\d+/)
)
)
),
optional($.unit)
),
unit: ($) => token.immediate(/[a-zA-Z%]+/),
call_expression: ($) => seq(alias($.identifier, $.function_name), $.arguments),
binary_expression: ($) =>
prec.left(
seq($._value, choice("+", "-", "*", "/", "==", "<", ">", "!=", "<=", ">="), $._value)
),
arguments: ($) => seq(token.immediate("("), sep(choice(",", ";"), repeat1($._value)), ")"),
identifier: ($) =>
/((#\{[a-zA-Z0-9-_,&\$\.\(\) ]*\})|(--|-?[a-zA-Z_]))([a-zA-Z0-9-_]|(#\{[a-zA-Z0-9-_,&\$\.\(\) ]*\}))*/,
variable_identifier: ($) => /([a-zA-Z_]+\.)?\$[a-zA-Z-_][a-zA-Z0-9-_]*/,
at_keyword: ($) => /@[a-zA-Z-_]+/,
comment: ($) => token(seq("/*", /[^*]*\*+([^/*][^*]*\*+)*/, "/")),
single_line_comment: ($) => token(seq("//", /.*/)),
plain_value: ($) =>
token(
seq(
repeat(
choice(
/[-_]/,
/\/[^\*\s,;!{}()\[\]]/ // Slash not followed by a '*' (which would be a comment)
)
),
/[a-zA-Z]/,
repeat(
choice(
/[^/\s,;!{}()\[\]]/, // Not a slash, not a delimiter character
/\/[^\*\s,;!{}()\[\]]/ // Slash not followed by a '*' (which would be a comment)
)
)
)
),
},
});
function sep(separator, rule) {
return optional(sep1(separator, rule));
}
function sep1(separator, rule) {
return seq(rule, repeat(seq(separator, rule)));
}

@ -0,0 +1,13 @@
try {
module.exports = require("./build/Release/tree_sitter_scss_binding");
} catch (error) {
try {
module.exports = require("./build/Debug/tree_sitter_scss_binding");
} catch (_) {
throw error
}
}
try {
module.exports.nodeTypeInfo = require("./src/node-types.json");
} catch (_) {}

@ -0,0 +1,33 @@
{
"name": "tree-sitter-scss",
"version": "0.0.1",
"description": "SCSS grammar for tree-sitter",
"main": "bindings/node",
"keywords": [
"parser",
"lexer"
],
"author": "Serenade Labs, Inc.",
"license": "MIT",
"dependencies": {
"nan": "^2.11.1"
},
"devDependencies": {
"tree-sitter-cli": "^0.19.4"
},
"scripts": {
"build": "tree-sitter generate && node-gyp build",
"build-test": "tree-sitter generate && node-gyp build && tree-sitter test",
"test": "tree-sitter test && tree-sitter parse examples/*.scss --quiet --time",
"test-windows": "tree-sitter test"
},
"tree-sitter": [
{
"scope": "source.scss",
"file-types": [
"scss"
],
"injection-regex": "^scss$"
}
]
}

@ -0,0 +1,63 @@
(comment) @comment
(tag_name) @tag
(nesting_selector) @tag
(universal_selector) @tag
"~" @operator
">" @operator
"+" @operator
"-" @operator
"*" @operator
"/" @operator
"=" @operator
"^=" @operator
"|=" @operator
"~=" @operator
"$=" @operator
"*=" @operator
"and" @operator
"or" @operator
"not" @operator
"only" @operator
(attribute_selector (plain_value) @string)
(pseudo_element_selector (tag_name) @attribute)
(pseudo_class_selector (class_name) @attribute)
(class_name) @property
(id_name) @property
(namespace_name) @property
(property_name) @property
(feature_name) @property
(attribute_name) @attribute
(function_name) @function
((property_name) @variable
(match? @variable "^--"))
((plain_value) @variable
(match? @variable "^--"))
"@media" @keyword
"@import" @keyword
"@charset" @keyword
"@namespace" @keyword
"@supports" @keyword
"@keyframes" @keyword
(at_keyword) @keyword
(to) @keyword
(from) @keyword
(important) @keyword
(string_value) @string
(color_value) @string.special
(integer_value) @number
(float_value) @number
(unit) @type
"#" @punctuation.delimiter
"," @punctuation.delimiter

@ -0,0 +1,28 @@
#include "tree_sitter/parser.h"
#include <node.h>
#include "nan.h"
using namespace v8;
extern "C" TSLanguage * tree_sitter_scss();
namespace {
NAN_METHOD(New) {}
void Init(Local<Object> exports, Local<Object> module) {
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
tpl->SetClassName(Nan::New("Language").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_scss());
Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("scss").ToLocalChecked());
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
}
NODE_MODULE(tree_sitter_scss_binding, Init)
} // namespace

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More