aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock878
-rw-r--r--Cargo.toml1
-rw-r--r--book/src/configuration.md2
-rw-r--r--helix-core/src/lib.rs2
-rw-r--r--helix-term/Cargo.toml3
-rw-r--r--helix-term/src/application.rs50
-rw-r--r--helix-term/src/commands/typed.rs11
-rw-r--r--helix-term/src/ui/editor.rs2
-rw-r--r--helix-vcs/Cargo.toml28
-rw-r--r--helix-vcs/src/diff.rs198
-rw-r--r--helix-vcs/src/diff/line_cache.rs130
-rw-r--r--helix-vcs/src/diff/worker.rs207
-rw-r--r--helix-vcs/src/diff/worker/test.rs149
-rw-r--r--helix-vcs/src/git.rs80
-rw-r--r--helix-vcs/src/git/test.rs121
-rw-r--r--helix-vcs/src/lib.rs51
-rw-r--r--helix-view/Cargo.toml2
-rw-r--r--helix-view/src/document.rs52
-rw-r--r--helix-view/src/editor.rs75
-rw-r--r--helix-view/src/gutter.rs55
-rw-r--r--helix-view/src/view.rs15
21 files changed, 2036 insertions, 76 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 05a0396f..2e021197 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
version = 3
[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -56,6 +62,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
[[package]]
+name = "atoi"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -79,12 +94,43 @@ dependencies = [
]
[[package]]
+name = "bstr"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd"
+dependencies = [
+ "memchr",
+ "once_cell",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "btoi"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97c0869a9faa81f8bbf8102371105d6d0a7b79167a04c340b04ab16892246a11"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
name = "bumpalo"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
+name = "byte-unit"
+version = "4.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581ad4b3d627b0c09a0ccb2912148f839acaca0b93cf54cbe42b6c674e86079c"
+dependencies = [
+ "serde",
+ "utf8-width",
+]
+
+[[package]]
name = "bytecount"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -97,12 +143,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]]
+name = "bytesize"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70"
+
+[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
+name = "castaway"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
name = "cc"
version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -149,6 +210,12 @@ dependencies = [
]
[[package]]
+name = "clru"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "218d6bd3dde8e442a975fa1cd233c0e5fded7596bccfe39f58eca98d22421e0a"
+
+[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -159,6 +226,17 @@ dependencies = [
]
[[package]]
+name = "compact_str"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5138945395949e7dfba09646dc9e766b548ff48e23deb5246890e6b64ae9e1b9"
+dependencies = [
+ "castaway",
+ "itoa",
+ "ryu",
+]
+
+[[package]]
name = "content_inspector"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -174,6 +252,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "crossbeam-utils"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -253,6 +340,28 @@ dependencies = [
]
[[package]]
+name = "dashmap"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
+dependencies = [
+ "cfg-if",
+ "hashbrown 0.12.3",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -263,6 +372,17 @@ dependencies = [
]
[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -337,6 +457,28 @@ dependencies = [
]
[[package]]
+name = "filetime"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -408,13 +550,505 @@ dependencies = [
]
[[package]]
+name = "git-actor"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d4ce09c0a6c71c044700e5932877667f427f007b77e6c39ab49aebc4719e25"
+dependencies = [
+ "bstr 1.0.1",
+ "btoi",
+ "git-date",
+ "itoa",
+ "nom",
+ "quick-error",
+]
+
+[[package]]
+name = "git-attributes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c62e66a042c6b39c6dbfa3be37d134900d99ff9c54bbe489ed560a573895d5d"
+dependencies = [
+ "bstr 1.0.1",
+ "compact_str",
+ "git-features",
+ "git-glob",
+ "git-path",
+ "git-quote",
+ "thiserror",
+ "unicode-bom",
+]
+
+[[package]]
+name = "git-bitmap"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327098a7ad27ae298d7e71602dbd4375cc828d755d10a720e4be0be1b4ec38f0"
+dependencies = [
+ "quick-error",
+]
+
+[[package]]
+name = "git-chunk"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07b2bc1635b660ad6e30379a84a4946590a3c124b747107c2cca1d9dbb98f588"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "git-command"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e4b01997b6551554fdac6f02277d0d04c3e869daa649bedd06d38c86f11dc42"
+dependencies = [
+ "bstr 1.0.1",
+]
+
+[[package]]
+name = "git-config"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd8603e953bd4c9bf310e74e43697400f5542f1cc75fad46fbd7427135a9534f"
+dependencies = [
+ "bstr 1.0.1",
+ "git-config-value",
+ "git-features",
+ "git-glob",
+ "git-path",
+ "git-ref",
+ "git-sec",
+ "memchr",
+ "nom",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+ "unicode-bom",
+]
+
+[[package]]
+name = "git-config-value"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f276bfe5806b414915112f1eec0f006206cdf5b8cc9bbb44ef7e52286dc3eb"
+dependencies = [
+ "bitflags",
+ "bstr 1.0.1",
+ "git-path",
+ "libc",
+ "thiserror",
+]
+
+[[package]]
+name = "git-credentials"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f540186ea56fd075ba2b923180ebf4318e66ceaeac0a2a518e75dab8517d339"
+dependencies = [
+ "bstr 1.0.1",
+ "git-command",
+ "git-config-value",
+ "git-path",
+ "git-prompt",
+ "git-sec",
+ "git-url",
+ "thiserror",
+]
+
+[[package]]
+name = "git-date"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37881e9725df41e15d16216d3a0cee251fd8a39d425f75b389112df5c7f20f3d"
+dependencies = [
+ "bstr 1.0.1",
+ "itoa",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "git-diff"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a88666a0ae4365b55a0cbf2efde68d2a4cff0747894ad229403bd60b0b2abc5"
+dependencies = [
+ "git-hash",
+ "git-object",
+ "imara-diff",
+ "thiserror",
+]
+
+[[package]]
+name = "git-discover"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "881e4136d5599cfdb79d8ef60d650823d1a563589fa493d8e4961e64d78a79f2"
+dependencies = [
+ "bstr 1.0.1",
+ "git-hash",
+ "git-path",
+ "git-ref",
+ "git-sec",
+ "thiserror",
+]
+
+[[package]]
+name = "git-features"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be88ae837674c71b30c6517c6f5f1335f8135bb8a9ffef20000d211933bed08"
+dependencies = [
+ "crc32fast",
+ "flate2",
+ "git-hash",
+ "libc",
+ "once_cell",
+ "prodash",
+ "quick-error",
+ "sha1_smol",
+ "walkdir",
+]
+
+[[package]]
+name = "git-glob"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d756430237112f8c89049236f60fdcdb0005127b1f7e531d40984e4fe7daa90"
+dependencies = [
+ "bitflags",
+ "bstr 1.0.1",
+]
+
+[[package]]
+name = "git-hash"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16d46e6c2d1e8da4438a87bf516a6761b300964a353541fea61e96b3c7b34554"
+dependencies = [
+ "hex",
+ "thiserror",
+]
+
+[[package]]
+name = "git-index"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "821583c2d12b1e864694eb0bf1cca10ff6a3f45966f5f834e0f921b496dbe7cb"
+dependencies = [
+ "atoi",
+ "bitflags",
+ "bstr 1.0.1",
+ "filetime",
+ "git-bitmap",
+ "git-features",
+ "git-hash",
+ "git-lock",
+ "git-object",
+ "git-traverse",
+ "itoa",
+ "memmap2",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "git-lock"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0fe10bf961f62b1335b4c07785e64fb4d86c5ed367dc7cd9360f13c3eb7c78"
+dependencies = [
+ "fastrand",
+ "git-tempfile",
+ "quick-error",
+]
+
+[[package]]
+name = "git-mailmap"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb3f85ce84b2328aeb3124a809f7b3a63e59c4d63c227dba7a9cdf6fca6c0987"
+dependencies = [
+ "bstr 1.0.1",
+ "git-actor",
+ "quick-error",
+]
+
+[[package]]
+name = "git-object"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9469a8c00d8bb500ee76a12e455bb174b4ddf71674713335dd1a84313723f7b3"
+dependencies = [
+ "bstr 1.0.1",
+ "btoi",
+ "git-actor",
+ "git-features",
+ "git-hash",
+ "git-validate",
+ "hex",
+ "itoa",
+ "nom",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "git-odb"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aaaea7031ac7d8dfee232a16d7114395d118226214fb03fe4e15d1f4d62a88a6"
+dependencies = [
+ "arc-swap",
+ "git-features",
+ "git-hash",
+ "git-object",
+ "git-pack",
+ "git-path",
+ "git-quote",
+ "parking_lot",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "git-pack"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc4386dff835ffdc3697c3558111f708fd7b7695c42a4347f2d211cf3246c8e1"
+dependencies = [
+ "bytesize",
+ "clru",
+ "dashmap",
+ "git-chunk",
+ "git-diff",
+ "git-features",
+ "git-hash",
+ "git-object",
+ "git-path",
+ "git-tempfile",
+ "git-traverse",
+ "hash_hasher",
+ "memmap2",
+ "parking_lot",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "git-path"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "425dc1022690be13e6c5bde4b7e04d9504d323605ec314cd367cebf38a812572"
+dependencies = [
+ "bstr 1.0.1",
+ "thiserror",
+]
+
+[[package]]
+name = "git-prompt"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa6947935c0671342277bc883ff0687978477b570c1ffe2200b9ba5ac8afdd9f"
+dependencies = [
+ "git-command",
+ "git-config-value",
+ "nix",
+ "parking_lot",
+ "thiserror",
+]
+
+[[package]]
+name = "git-quote"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea17931d07cbe447f371bbdf45ff03c30ea86db43788166655a5302df87ecfc"
+dependencies = [
+ "bstr 1.0.1",
+ "btoi",
+ "quick-error",
+]
+
+[[package]]
+name = "git-ref"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "638c9e454bacb2965a43f05b4a383c8f66dc64f3a770bd0324b221c2a20e121d"
+dependencies = [
+ "git-actor",
+ "git-features",
+ "git-hash",
+ "git-lock",
+ "git-object",
+ "git-path",
+ "git-tempfile",
+ "git-validate",
+ "memmap2",
+ "nom",
+ "thiserror",
+]
+
+[[package]]
+name = "git-refspec"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9497af773538ae8cfda053ff7dd0a9e6c28d333ba653040f54b8b4ee32f14187"
+dependencies = [
+ "bstr 1.0.1",
+ "git-hash",
+ "git-revision",
+ "git-validate",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "git-repository"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeb43e59612e493af6a433bf0a960de0042c8aa6f4e4c4cb414f03b97e296b82"
+dependencies = [
+ "byte-unit",
+ "clru",
+ "git-actor",
+ "git-attributes",
+ "git-config",
+ "git-credentials",
+ "git-date",
+ "git-diff",
+ "git-discover",
+ "git-features",
+ "git-glob",
+ "git-hash",
+ "git-index",
+ "git-lock",
+ "git-mailmap",
+ "git-object",
+ "git-odb",
+ "git-pack",
+ "git-path",
+ "git-prompt",
+ "git-ref",
+ "git-refspec",
+ "git-revision",
+ "git-sec",
+ "git-tempfile",
+ "git-traverse",
+ "git-url",
+ "git-validate",
+ "git-worktree",
+ "log",
+ "once_cell",
+ "signal-hook",
+ "smallvec",
+ "thiserror",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "git-revision"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efd31c63c3745b5dba5ec7109eec41a9c717f4e1e797fe0ef93098f33f31b25"
+dependencies = [
+ "bstr 1.0.1",
+ "git-date",
+ "git-hash",
+ "git-object",
+ "hash_hasher",
+ "thiserror",
+]
+
+[[package]]
+name = "git-sec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c79769f6546814d0774db7295c768441016b7e40bdd414fa8dfae2c616a1892"
+dependencies = [
+ "bitflags",
+ "dirs",
+ "git-path",
+ "libc",
+ "windows",
+]
+
+[[package]]
+name = "git-tempfile"
+version = "2.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d23bc6129de3cbd81e6c9d0d685b5540c6b41bd9fa0cc38f381bc300743d708"
+dependencies = [
+ "dashmap",
+ "libc",
+ "once_cell",
+ "signal-hook",
+ "signal-hook-registry",
+ "tempfile",
+]
+
+[[package]]
+name = "git-traverse"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d0c4dd773c69f294f43ace8373d48eb770129791f104c6857fa8cac0505af89"
+dependencies = [
+ "git-hash",
+ "git-object",
+ "hash_hasher",
+ "thiserror",
+]
+
+[[package]]
+name = "git-url"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b7f8323196840e7932f5b60e1d9c1d6c140fd806bc512f8beedc3f990a1f81"
+dependencies = [
+ "bstr 1.0.1",
+ "git-features",
+ "git-path",
+ "home",
+ "thiserror",
+ "url",
+]
+
+[[package]]
+name = "git-validate"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5439d6aa0de838dfadd74a71e97a9e23ebc719fd11a9ab6788b835b112c8c3d"
+dependencies = [
+ "bstr 1.0.1",
+ "thiserror",
+]
+
+[[package]]
+name = "git-worktree"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bcc69c36a29cfa283710b7901877ab251d658935f5a41ed824416af500e0ed"
+dependencies = [
+ "bstr 1.0.1",
+ "git-attributes",
+ "git-features",
+ "git-glob",
+ "git-hash",
+ "git-index",
+ "git-object",
+ "git-path",
+ "io-close",
+ "thiserror",
+]
+
+[[package]]
name = "globset"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
dependencies = [
"aho-corasick",
- "bstr",
+ "bstr 0.2.17",
"fnv",
"log",
"regex",
@@ -436,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e"
dependencies = [
"aho-corasick",
- "bstr",
+ "bstr 0.2.17",
"grep-matcher",
"log",
"regex",
@@ -450,7 +1084,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc"
dependencies = [
- "bstr",
+ "bstr 0.2.17",
"bytecount",
"encoding_rs",
"encoding_rs_io",
@@ -460,6 +1094,12 @@ dependencies = [
]
[[package]]
+name = "hash_hasher"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74721d007512d0cb3338cd20f0654ac913920061a4c4d0d8708edb3f2a698c0c"
+
+[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -576,6 +1216,7 @@ dependencies = [
"helix-loader",
"helix-lsp",
"helix-tui",
+ "helix-vcs",
"helix-view",
"ignore",
"indoc",
@@ -609,6 +1250,19 @@ dependencies = [
]
[[package]]
+name = "helix-vcs"
+version = "0.6.0"
+dependencies = [
+ "git-repository",
+ "helix-core",
+ "imara-diff",
+ "log",
+ "parking_lot",
+ "tempfile",
+ "tokio",
+]
+
+[[package]]
name = "helix-view"
version = "0.6.0"
dependencies = [
@@ -624,6 +1278,7 @@ dependencies = [
"helix-loader",
"helix-lsp",
"helix-tui",
+ "helix-vcs",
"log",
"once_cell",
"serde",
@@ -646,6 +1301,27 @@ dependencies = [
]
[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "home"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "human_format"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86cce260d758a9aa3d7c4b99d55c815a540f8a37514ba6046ab6be402a157cb0"
+
+[[package]]
name = "iana-time-zone"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -723,6 +1399,16 @@ dependencies = [
]
[[package]]
+name = "io-close"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "itoa"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -816,6 +1502,21 @@ dependencies = [
]
[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+dependencies = [
+ "adler",
+]
+
+[[package]]
name = "mio"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -828,6 +1529,28 @@ dependencies = [
]
[[package]]
+name = "nix"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -857,6 +1580,15 @@ dependencies = [
]
[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "once_cell"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -913,6 +1645,16 @@ dependencies = [
]
[[package]]
+name = "prodash"
+version = "21.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e13d7bd38cdab08b3a8b780cedcc54238c84fdca4084eb188807b308bcf11e6"
+dependencies = [
+ "bytesize",
+ "human_format",
+]
+
+[[package]]
name = "pulldown-cmark"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -924,6 +1666,12 @@ dependencies = [
]
[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
name = "quickcheck"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1022,6 +1770,12 @@ dependencies = [
]
[[package]]
+name = "rustversion"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
+
+[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1091,6 +1845,12 @@ dependencies = [
]
[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
+[[package]]
name = "signal-hook"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1294,6 +2054,35 @@ dependencies = [
]
[[package]]
+name = "time"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "time-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1385,6 +2174,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
+name = "unicode-bom"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63ec69f541d875b783ca40184d655f2927c95f0bffd486faa83cd3ac3529ec32"
+
+[[package]]
name = "unicode-general-category"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1440,6 +2235,12 @@ dependencies = [
]
[[package]]
+name = "utf8-width"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1559,58 +2360,115 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
+name = "windows"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e30acc718a52fb130fec72b1cb5f55ffeeec9253e1b785e94db222178a6acaa1"
+dependencies = [
+ "windows_aarch64_gnullvm 0.40.0",
+ "windows_aarch64_msvc 0.40.0",
+ "windows_i686_gnu 0.40.0",
+ "windows_i686_msvc 0.40.0",
+ "windows_x86_64_gnu 0.40.0",
+ "windows_x86_64_gnullvm 0.40.0",
+ "windows_x86_64_msvc 0.40.0",
+]
+
+[[package]]
name = "windows-sys"
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.0",
+ "windows_aarch64_msvc 0.42.0",
+ "windows_i686_gnu 0.42.0",
+ "windows_i686_msvc 0.42.0",
+ "windows_x86_64_gnu 0.42.0",
+ "windows_x86_64_gnullvm 0.42.0",
+ "windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3caa4a1a16561b714323ca6b0817403738583033a6a92e04c5d10d4ba37ca10"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328973c62dfcc50fb1aaa8e7100676e0b642fe56bac6bafff3327902db843ab4"
+
+[[package]]
+name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa5b09fad70f0df85dea2ac2a525537e415e2bf63ee31cf9b8e263645ee9f3c1"
+
+[[package]]
+name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a1ad4031c1a98491fa195d8d43d7489cb749f135f2e5c4eed58da094bd0d876"
+
+[[package]]
+name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520ff37edd72da8064b49d2281182898e17f0688ae9f4070bca27e4b5c162ac7"
+
+[[package]]
+name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046e5b82215102c44fd75f488f1b9158973d02aa34d06ed85c23d6f5520a2853"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0c9c6df55dd1bfa76e131cef44bdd8ec9c819ef3611f04dfe453fd5bfeda28"
+
+[[package]]
+name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
diff --git a/Cargo.toml b/Cargo.toml
index 9e985ddc..ecf6848e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
"helix-lsp",
"helix-dap",
"helix-loader",
+ "helix-vcs",
"xtask",
]
diff --git a/book/src/configuration.md b/book/src/configuration.md
index e4854cda..0890d283 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -46,7 +46,7 @@ on unix operating systems.
| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
| `cursorline` | Highlight all lines with a cursor. | `false` |
| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
-| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers"]` |
+| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` |
| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 5f60c048..0e76ebbb 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -83,7 +83,7 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::Path
top_marker.map_or(current_dir, |a| a.to_path_buf())
}
-pub use ropey::{str_utils, Rope, RopeBuilder, RopeSlice};
+pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
// pub use tendril::StrTendril as Tendril;
pub use smartstring::SmartString;
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 485cabe9..30bfc7ea 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -17,8 +17,10 @@ build = true
app = true
[features]
+default = ["git"]
unicode-lines = ["helix-core/unicode-lines"]
integration = []
+git = ["helix-vcs/git"]
[[bin]]
name = "hx"
@@ -29,6 +31,7 @@ helix-core = { version = "0.6", path = "../helix-core" }
helix-view = { version = "0.6", path = "../helix-view" }
helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" }
+helix-vcs = { version = "0.6", path = "../helix-vcs" }
helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1"
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 99d3af18..dc12ba3c 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -274,16 +274,27 @@ impl Application {
}
#[cfg(feature = "integration")]
- fn render(&mut self) {}
+ async fn render(&mut self) {}
#[cfg(not(feature = "integration"))]
- fn render(&mut self) {
+ async fn render(&mut self) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
+ // Acquire mutable access to the redraw_handle lock
+ // to ensure that there are no tasks running that want to block rendering
+ drop(cx.editor.redraw_handle.1.write().await);
+ cx.editor.needs_redraw = false;
+ {
+ // exhaust any leftover redraw notifications
+ let notify = cx.editor.redraw_handle.0.notified();
+ tokio::pin!(notify);
+ notify.enable();
+ }
+
let area = self
.terminal
.autoresize()
@@ -304,7 +315,7 @@ impl Application {
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
- self.render();
+ self.render().await;
self.last_render = Instant::now();
loop {
@@ -329,18 +340,18 @@ impl Application {
biased;
Some(event) = input_stream.next() => {
- self.handle_terminal_events(event);
+ self.handle_terminal_events(event).await;
}
Some(signal) = self.signals.next() => {
self.handle_signals(signal).await;
}
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
- self.render();
+ self.render().await;
}
Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
- self.render();
+ self.render().await;
}
event = self.editor.wait_event() => {
let _idle_handled = self.handle_editor_event(event).await;
@@ -445,25 +456,25 @@ impl Application {
self.compositor.resize(area);
self.terminal.clear().expect("couldn't clear terminal");
- self.render();
+ self.render().await;
}
signal::SIGUSR1 => {
self.refresh_config();
- self.render();
+ self.render().await;
}
_ => unreachable!(),
}
}
- pub fn handle_idle_timeout(&mut self) {
+ pub async fn handle_idle_timeout(&mut self) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
- if should_render {
- self.render();
+ if should_render || self.editor.needs_redraw {
+ self.render().await;
}
}
@@ -536,11 +547,11 @@ impl Application {
match event {
EditorEvent::DocumentSaved(event) => {
self.handle_document_write(event);
- self.render();
+ self.render().await;
}
EditorEvent::ConfigEvent(event) => {
self.handle_config_events(event);
- self.render();
+ self.render().await;
}
EditorEvent::LanguageServerMessage((id, call)) => {
self.handle_language_server_message(call, id).await;
@@ -548,19 +559,19 @@ impl Application {
let last = self.editor.language_servers.incoming.is_empty();
if last || self.last_render.elapsed() > LSP_DEADLINE {
- self.render();
+ self.render().await;
self.last_render = Instant::now();
}
}
EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await;
if needs_render {
- self.render();
+ self.render().await;
}
}
EditorEvent::IdleTimer => {
self.editor.clear_idle_timer();
- self.handle_idle_timeout();
+ self.handle_idle_timeout().await;
#[cfg(feature = "integration")]
{
@@ -572,7 +583,10 @@ impl Application {
false
}
- pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
+ pub async fn handle_terminal_events(
+ &mut self,
+ event: Result<CrosstermEvent, crossterm::ErrorKind>,
+ ) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
@@ -596,7 +610,7 @@ impl Application {
};
if should_redraw && !self.editor.should_close() {
- self.render();
+ self.render().await;
}
}
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 2fa903a7..9f848efd 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1028,10 +1028,12 @@ fn reload(
}
let scrolloff = cx.editor.config().scrolloff;
+ let redraw_handle = cx.editor.redraw_handle.clone();
let (view, doc) = current!(cx.editor);
- doc.reload(view).map(|_| {
- view.ensure_cursor_in_view(doc, scrolloff);
- })
+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)
+ .map(|_| {
+ view.ensure_cursor_in_view(doc, scrolloff);
+ })
}
fn reload_all(
@@ -1066,7 +1068,8 @@ fn reload_all(
// Every doc is guaranteed to have at least 1 view at this point.
let view = view_mut!(cx.editor, view_ids[0]);
- doc.reload(view)?;
+ let redraw_handle = cx.editor.redraw_handle.clone();
+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
for view_id in view_ids {
let view = view_mut!(cx.editor, view_id);
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 7bda74d2..32c8fe91 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -730,7 +730,7 @@ impl EditorView {
let mut text = String::with_capacity(8);
for gutter_type in view.gutters() {
- let gutter = gutter_type.style(editor, doc, view, theme, is_focused);
+ let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
let width = gutter_type.width(view, doc);
text.reserve(width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml
new file mode 100644
index 00000000..c114666d
--- /dev/null
+++ b/helix-vcs/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "helix-vcs"
+version = "0.6.0"
+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
+edition = "2021"
+license = "MPL-2.0"
+categories = ["editor"]
+repository = "https://github.com/helix-editor/helix"
+homepage = "https://helix-editor.com"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+helix-core = { version = "0.6", path = "../helix-core" }
+
+tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
+parking_lot = "0.12"
+
+git-repository = { version = "0.26", default-features = false , optional = true }
+imara-diff = "0.1.5"
+
+log = "0.4"
+
+[features]
+git = ["git-repository"]
+
+[dev-dependencies]
+tempfile = "3.3" \ No newline at end of file
diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs
new file mode 100644
index 00000000..b1acd1f2
--- /dev/null
+++ b/helix-vcs/src/diff.rs
@@ -0,0 +1,198 @@
+use std::ops::Range;
+use std::sync::Arc;
+
+use helix_core::Rope;
+use imara_diff::Algorithm;
+use parking_lot::{Mutex, MutexGuard};
+use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
+use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
+use tokio::task::JoinHandle;
+use tokio::time::Instant;
+
+use crate::diff::worker::DiffWorker;
+
+mod line_cache;
+mod worker;
+
+type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
+
+/// A rendering lock passed to the differ the prevents redraws from occurring
+struct RenderLock {
+ pub lock: OwnedRwLockReadGuard<()>,
+ pub timeout: Option<Instant>,
+}
+
+struct Event {
+ text: Rope,
+ is_base: bool,
+ render_lock: Option<RenderLock>,
+}
+
+#[derive(Clone, Debug)]
+pub struct DiffHandle {
+ channel: UnboundedSender<Event>,
+ render_lock: Arc<RwLock<()>>,
+ hunks: Arc<Mutex<Vec<Hunk>>>,
+ inverted: bool,
+}
+
+impl DiffHandle {
+ pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
+ DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
+ }
+
+ fn new_with_handle(
+ diff_base: Rope,
+ doc: Rope,
+ redraw_handle: RedrawHandle,
+ ) -> (DiffHandle, JoinHandle<()>) {
+ let (sender, receiver) = unbounded_channel();
+ let hunks: Arc<Mutex<Vec<Hunk>>> = Arc::default();
+ let worker = DiffWorker {
+ channel: receiver,
+ hunks: hunks.clone(),
+ new_hunks: Vec::default(),
+ redraw_notify: redraw_handle.0,
+ diff_finished_notify: Arc::default(),
+ };
+ let handle = tokio::spawn(worker.run(diff_base, doc));
+ let differ = DiffHandle {
+ channel: sender,
+ hunks,
+ inverted: false,
+ render_lock: redraw_handle.1,
+ };
+ (differ, handle)
+ }
+
+ pub fn invert(&mut self) {
+ self.inverted = !self.inverted;
+ }
+
+ pub fn hunks(&self) -> FileHunks {
+ FileHunks {
+ hunks: self.hunks.lock(),
+ inverted: self.inverted,
+ }
+ }
+
+ /// Updates the document associated with this redraw handle
+ /// This function is only intended to be called from within the rendering loop
+ /// if called from elsewhere it may fail to acquire the render lock and panic
+ pub fn update_document(&self, doc: Rope, block: bool) -> bool {
+ // unwrap is ok here because the rendering lock is
+ // only exclusively locked during redraw.
+ // This function is only intended to be called
+ // from the core rendering loop where no redraw can happen in parallel
+ let lock = self.render_lock.clone().try_read_owned().unwrap();
+ let timeout = if block {
+ None
+ } else {
+ Some(Instant::now() + tokio::time::Duration::from_millis(SYNC_DIFF_TIMEOUT))
+ };
+ self.update_document_impl(doc, self.inverted, Some(RenderLock { lock, timeout }))
+ }
+
+ pub fn update_diff_base(&self, diff_base: Rope) -> bool {
+ self.update_document_impl(diff_base, !self.inverted, None)
+ }
+
+ fn update_document_impl(
+ &self,
+ text: Rope,
+ is_base: bool,
+ render_lock: Option<RenderLock>,
+ ) -> bool {
+ let event = Event {
+ text,
+ is_base,
+ render_lock,
+ };
+ self.channel.send(event).is_ok()
+ }
+}
+
+/// synchronous debounce value should be low
+/// so we can update synchronously most of the time
+const DIFF_DEBOUNCE_TIME_SYNC: u64 = 1;
+/// maximum time that rendering should be blocked until the diff finishes
+const SYNC_DIFF_TIMEOUT: u64 = 12;
+const DIFF_DEBOUNCE_TIME_ASYNC: u64 = 96;
+const ALGORITHM: Algorithm = Algorithm::Histogram;
+const MAX_DIFF_LINES: usize = 64 * u16::MAX as usize;
+// cap average line length to 128 for files with MAX_DIFF_LINES
+const MAX_DIFF_BYTES: usize = MAX_DIFF_LINES * 128;
+
+/// A single change in a file potentially spanning multiple lines
+/// Hunks produced by the differs are always ordered by their position
+/// in the file and non-overlapping.
+/// Specifically for any two hunks `x` and `y` the following properties hold:
+///
+/// ``` no_compile
+/// assert!(x.before.end <= y.before.start);
+/// assert!(x.after.end <= y.after.start);
+/// ```
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub struct Hunk {
+ pub before: Range<u32>,
+ pub after: Range<u32>,
+}
+
+impl Hunk {
+ /// Can be used instead of `Option::None` for better performance
+ /// because lines larger then `i32::MAX` are not supported by `imara-diff` anyways.
+ /// Has some nice properties where it usually is not necessary to check for `None` separately:
+ /// Empty ranges fail contains checks and also fails smaller then checks.
+ pub const NONE: Hunk = Hunk {
+ before: u32::MAX..u32::MAX,
+ after: u32::MAX..u32::MAX,
+ };
+
+ /// Inverts a change so that `before`
+ pub fn invert(&self) -> Hunk {
+ Hunk {
+ before: self.after.clone(),
+ after: self.before.clone(),
+ }
+ }
+
+ pub fn is_pure_insertion(&self) -> bool {
+ self.before.is_empty()
+ }
+
+ pub fn is_pure_removal(&self) -> bool {
+ self.after.is_empty()
+ }
+}
+
+/// A list of changes in a file sorted in ascending
+/// non-overlapping order
+#[derive(Debug)]
+pub struct FileHunks<'a> {
+ hunks: MutexGuard<'a, Vec<Hunk>>,
+ inverted: bool,
+}
+
+impl FileHunks<'_> {
+ pub fn is_inverted(&self) -> bool {
+ self.inverted
+ }
+
+ /// Returns the `Hunk` for the `n`th change in this file.
+ /// if there is no `n`th change `Hunk::NONE` is returned instead.
+ pub fn nth_hunk(&self, n: u32) -> Hunk {
+ match self.hunks.get(n as usize) {
+ Some(hunk) if self.inverted => hunk.invert(),
+ Some(hunk) => hunk.clone(),
+ None => Hunk::NONE,
+ }
+ }
+
+ pub fn len(&self) -> u32 {
+ self.hunks.len() as u32
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+}
diff --git a/helix-vcs/src/diff/line_cache.rs b/helix-vcs/src/diff/line_cache.rs
new file mode 100644
index 00000000..c3ee5daa
--- /dev/null
+++ b/helix-vcs/src/diff/line_cache.rs
@@ -0,0 +1,130 @@
+//! This modules encapsulates a tiny bit of unsafe code that
+//! makes diffing significantly faster and more ergonomic to implement.
+//! This code is necessary because diffing requires quick random
+//! access to the lines of the text that is being diffed.
+//!
+//! Therefore it is best to collect the `Rope::lines` iterator into a vec
+//! first because access to the vec is `O(1)` where `Rope::line` is `O(log N)`.
+//! However this process can allocate a (potentially quite large) vector.
+//!
+//! To avoid reallocation for every diff, the vector is reused.
+//! However the RopeSlice references the original rope and therefore forms a self-referential data structure.
+//! A transmute is used to change the lifetime of the slice to static to circumvent that project.
+use std::mem::transmute;
+
+use helix_core::{Rope, RopeSlice};
+use imara_diff::intern::{InternedInput, Interner};
+
+use super::{MAX_DIFF_BYTES, MAX_DIFF_LINES};
+
+/// A cache that stores the `lines` of a rope as a vector.
+/// It allows safely reusing the allocation of the vec when updating the rope
+pub(crate) struct InternedRopeLines {
+ diff_base: Rope,
+ doc: Rope,
+ num_tokens_diff_base: u32,
+ interned: InternedInput<RopeSlice<'static>>,
+}
+
+impl InternedRopeLines {
+ pub fn new(diff_base: Rope, doc: Rope) -> InternedRopeLines {
+ let mut res = InternedRopeLines {
+ interned: InternedInput {
+ before: Vec::with_capacity(diff_base.len_lines()),
+ after: Vec::with_capacity(doc.len_lines()),
+ interner: Interner::new(diff_base.len_lines() + doc.len_lines()),
+ },
+ diff_base,
+ doc,
+ // will be populated by update_diff_base_impl
+ num_tokens_diff_base: 0,
+ };
+ res.update_diff_base_impl();
+ res
+ }
+
+ /// Updates the `diff_base` and optionally the document if `doc` is not None
+ pub fn update_diff_base(&mut self, diff_base: Rope, doc: Option<Rope>) {
+ self.interned.clear();
+ self.diff_base = diff_base;
+ if let Some(doc) = doc {
+ self.doc = doc
+ }
+ if !self.is_too_large() {
+ self.update_diff_base_impl();
+ }
+ }
+
+ /// Updates the `doc` without reinterning the `diff_base`, this function
+ /// is therefore significantly faster than `update_diff_base` when only the document changes.
+ pub fn update_doc(&mut self, doc: Rope) {
+ // Safety: we clear any tokens that were added after
+ // the interning of `self.diff_base` finished so
+ // all lines that refer to `self.doc` have been purged.
+
+ self.interned
+ .interner
+ .erase_tokens_after(self.num_tokens_diff_base.into());
+
+ self.doc = doc;
+ if self.is_too_large() {
+ self.interned.after.clear();
+ } else {
+ self.update_doc_impl();
+ }
+ }
+
+ fn update_diff_base_impl(&mut self) {
+ // Safety: This transmute is safe because it only transmutes a lifetime, which has no effect.
+ // The backing storage for the RopeSlices referred to by the lifetime is stored in `self.diff_base`.
+ // Therefore as long as `self.diff_base` is not dropped/replaced this memory remains valid.
+ // `self.diff_base` is only changed in `self.update_diff_base`, which clears the interner.
+ // When the interned lines are exposed to consumer in `self.diff_input`, the lifetime is bounded to a reference to self.
+ // That means that on calls to update there exist no references to `self.interned`.
+ let before = self
+ .diff_base
+ .lines()
+ .map(|line: RopeSlice| -> RopeSlice<'static> { unsafe { transmute(line) } });
+ self.interned.update_before(before);
+ self.num_tokens_diff_base = self.interned.interner.num_tokens();
+ // the has to be interned again because the interner was fully cleared
+ self.update_doc_impl()
+ }
+
+ fn update_doc_impl(&mut self) {
+ // Safety: This transmute is save because it only transmutes a lifetime, which has no effect.
+ // The backing storage for the RopeSlices referred to by the lifetime is stored in `self.doc`.
+ // Therefore as long as `self.doc` is not dropped/replaced this memory remains valid.
+ // `self.doc` is only changed in `self.update_doc`, which clears the interner.
+ // When the interned lines are exposed to consumer in `self.diff_input`, the lifetime is bounded to a reference to self.
+ // That means that on calls to update there exist no references to `self.interned`.
+ let after = self
+ .doc
+ .lines()
+ .map(|line: RopeSlice| -> RopeSlice<'static> { unsafe { transmute(line) } });
+ self.interned.update_after(after);
+ }
+
+ fn is_too_large(&self) -> bool {
+ // bound both lines and bytes to avoid huge files with few (but huge) lines
+ // or huge file with tiny lines. While this makes no difference to
+ // diff itself (the diff performance only depends on the number of tokens)
+ // the interning runtime depends mostly on filesize and is actually dominant
+ // for large files
+ self.doc.len_lines() > MAX_DIFF_LINES
+ || self.diff_base.len_lines() > MAX_DIFF_LINES
+ || self.doc.len_bytes() > MAX_DIFF_BYTES
+ || self.diff_base.len_bytes() > MAX_DIFF_BYTES
+ }
+
+ /// Returns the `InternedInput` for performing the diff.
+ /// If `diff_base` or `doc` is so large that performing a diff could slow the editor
+ /// this function returns `None`.
+ pub fn interned_lines(&self) -> Option<&InternedInput<RopeSlice>> {
+ if self.is_too_large() {
+ None
+ } else {
+ Some(&self.interned)
+ }
+ }
+}
diff --git a/helix-vcs/src/diff/worker.rs b/helix-vcs/src/diff/worker.rs
new file mode 100644
index 00000000..b8659c9b
--- /dev/null
+++ b/helix-vcs/src/diff/worker.rs
@@ -0,0 +1,207 @@
+use std::mem::swap;
+use std::ops::Range;
+use std::sync::Arc;
+
+use helix_core::{Rope, RopeSlice};
+use imara_diff::intern::InternedInput;
+use parking_lot::Mutex;
+use tokio::sync::mpsc::UnboundedReceiver;
+use tokio::sync::Notify;
+use tokio::time::{timeout, timeout_at, Duration};
+
+use crate::diff::{
+ Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC,
+};
+
+use super::line_cache::InternedRopeLines;
+use super::Hunk;
+
+#[cfg(test)]
+mod test;
+
+pub(super) struct DiffWorker {
+ pub channel: UnboundedReceiver<Event>,
+ pub hunks: Arc<Mutex<Vec<Hunk>>>,
+ pub new_hunks: Vec<Hunk>,
+ pub redraw_notify: Arc<Notify>,
+ pub diff_finished_notify: Arc<Notify>,
+}
+
+impl DiffWorker {
+ async fn accumulate_events(&mut self, event: Event) -> (Option<Rope>, Option<Rope>) {
+ let mut accumulator = EventAccumulator::new();
+ accumulator.handle_event(event).await;
+ accumulator
+ .accumulate_debounced_events(
+ &mut self.channel,
+ self.redraw_notify.clone(),
+ self.diff_finished_notify.clone(),
+ )
+ .await;
+ (accumulator.doc, accumulator.diff_base)
+ }
+
+ pub async fn run(mut self, diff_base: Rope, doc: Rope) {
+ let mut interner = InternedRopeLines::new(diff_base, doc);
+ if let Some(lines) = interner.interned_lines() {
+ self.perform_diff(lines);
+ }
+ self.apply_hunks();
+ while let Some(event) = self.channel.recv().await {
+ let (doc, diff_base) = self.accumulate_events(event).await;
+
+ let process_accumulated_events = || {
+ if let Some(new_base) = diff_base {
+ interner.update_diff_base(new_base, doc)
+ } else {
+ interner.update_doc(doc.unwrap())
+ }
+
+ if let Some(lines) = interner.interned_lines() {
+ self.perform_diff(lines)
+ }
+ };
+
+ // Calculating diffs is computationally expensive and should
+ // not run inside an async function to avoid blocking other futures.
+ // Note: tokio::task::block_in_place does not work during tests
+ #[cfg(test)]
+ process_accumulated_events();
+ #[cfg(not(test))]
+ tokio::task::block_in_place(process_accumulated_events);
+
+ self.apply_hunks();
+ }
+ }
+
+ /// update the hunks (used by the gutter) by replacing it with `self.new_hunks`.
+ /// `self.new_hunks` is always empty after this function runs.
+ /// To improve performance this function tries to reuse the allocation of the old diff previously stored in `self.line_diffs`
+ fn apply_hunks(&mut self) {
+ swap(&mut *self.hunks.lock(), &mut self.new_hunks);
+ self.diff_finished_notify.notify_waiters();
+ self.new_hunks.clear();
+ }
+
+ fn perform_diff(&mut self, input: &InternedInput<RopeSlice>) {
+ imara_diff::diff(ALGORITHM, input, |before: Range<u32>, after: Range<u32>| {
+ self.new_hunks.push(Hunk { before, after })
+ })
+ }
+}
+
+struct EventAccumulator {
+ diff_base: Option<Rope>,
+ doc: Option<Rope>,
+ render_lock: Option<RenderLock>,
+}
+
+impl<'a> EventAccumulator {
+ fn new() -> EventAccumulator {
+ EventAccumulator {
+ diff_base: None,
+ doc: None,
+ render_lock: None,
+ }
+ }
+
+ async fn handle_event(&mut self, event: Event) {
+ let dst = if event.is_base {
+ &mut self.diff_base
+ } else {
+ &mut self.doc
+ };
+
+ *dst = Some(event.text);
+
+ // always prefer the most synchronous requested render mode
+ if let Some(render_lock) = event.render_lock {
+ match &mut self.render_lock {
+ Some(RenderLock { timeout, .. }) => {
+ // A timeout of `None` means that the render should
+ // always wait for the diff to complete (so no timeout)
+ // remove the existing timeout, otherwise keep the previous timeout
+ // because it will be shorter then the current timeout
+ if render_lock.timeout.is_none() {
+ timeout.take();
+ }
+ }
+ None => self.render_lock = Some(render_lock),
+ }
+ }
+ }
+
+ async fn accumulate_debounced_events(
+ &mut self,
+ channel: &mut UnboundedReceiver<Event>,
+ redraw_notify: Arc<Notify>,
+ diff_finished_notify: Arc<Notify>,
+ ) {
+ let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
+ let sync_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_SYNC);
+ loop {
+ // if we are not blocking rendering use a much longer timeout
+ let debounce = if self.render_lock.is_none() {
+ async_debounce
+ } else {
+ sync_debounce
+ };
+
+ if let Ok(Some(event)) = timeout(debounce, channel.recv()).await {
+ self.handle_event(event).await;
+ } else {
+ break;
+ }
+ }
+
+ // setup task to trigger the rendering
+ match self.render_lock.take() {
+ // diff is performed outside of the rendering loop
+ // request a redraw after the diff is done
+ None => {
+ tokio::spawn(async move {
+ diff_finished_notify.notified().await;
+ redraw_notify.notify_one();
+ });
+ }
+ // diff is performed inside the rendering loop
+ // block redraw until the diff is done or the timeout is expired
+ Some(RenderLock {
+ lock,
+ timeout: Some(timeout),
+ }) => {
+ tokio::spawn(async move {
+ let res = {
+ // Acquire a lock on the redraw handle.
+ // The lock will block the rendering from occurring while held.
+ // The rendering waits for the diff if it doesn't time out
+ timeout_at(timeout, diff_finished_notify.notified()).await
+ };
+ // we either reached the timeout or the diff is finished, release the render lock
+ drop(lock);
+ if res.is_ok() {
+ // Diff finished in time we are done.
+ return;
+ }
+ // Diff failed to complete in time log the event
+ // and wait until the diff occurs to trigger an async redraw
+ log::warn!("Diff computation timed out, update of diffs might appear delayed");
+ diff_finished_notify.notified().await;
+ redraw_notify.notify_one();
+ });
+ }
+ // a blocking diff is performed inside the rendering loop
+ // block redraw until the diff is done
+ Some(RenderLock {
+ lock,
+ timeout: None,
+ }) => {
+ tokio::spawn(async move {
+ diff_finished_notify.notified().await;
+ // diff is done release the lock
+ drop(lock)
+ });
+ }
+ };
+ }
+}
diff --git a/helix-vcs/src/diff/worker/test.rs b/helix-vcs/src/diff/worker/test.rs
new file mode 100644
index 00000000..14442426
--- /dev/null
+++ b/helix-vcs/src/diff/worker/test.rs
@@ -0,0 +1,149 @@
+use helix_core::Rope;
+use tokio::task::JoinHandle;
+
+use crate::diff::{DiffHandle, Hunk};
+
+impl DiffHandle {
+ fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
+ DiffHandle::new_with_handle(
+ Rope::from_str(diff_base),
+ Rope::from_str(doc),
+ Default::default(),
+ )
+ }
+ async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
+ let hunks = self.hunks;
+ // dropping the channel terminates the task
+ drop(self.channel);
+ handle.await.unwrap();
+ let hunks = hunks.lock();
+ Vec::clone(&*hunks)
+ }
+}
+
+#[tokio::test]
+async fn append_line() {
+ let (differ, handle) = DiffHandle::new_test("foo\n", "foo\nbar\n");
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[Hunk {
+ before: 1..1,
+ after: 1..2
+ }]
+ )
+}
+
+#[tokio::test]
+async fn prepend_line() {
+ let (differ, handle) = DiffHandle::new_test("foo\n", "bar\nfoo\n");
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[Hunk {
+ before: 0..0,
+ after: 0..1
+ }]
+ )
+}
+
+#[tokio::test]
+async fn modify() {
+ let (differ, handle) = DiffHandle::new_test("foo\nbar\n", "foo bar\nbar\n");
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[Hunk {
+ before: 0..1,
+ after: 0..1
+ }]
+ )
+}
+
+#[tokio::test]
+async fn delete_line() {
+ let (differ, handle) = DiffHandle::new_test("foo\nfoo bar\nbar\n", "foo\nbar\n");
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[Hunk {
+ before: 1..2,
+ after: 1..1
+ }]
+ )
+}
+
+#[tokio::test]
+async fn delete_line_and_modify() {
+ let (differ, handle) = DiffHandle::new_test("foo\nbar\ntest\nfoo", "foo\ntest\nfoo bar");
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[
+ Hunk {
+ before: 1..2,
+ after: 1..1
+ },
+ Hunk {
+ before: 3..4,
+ after: 2..3
+ },
+ ]
+ )
+}
+
+#[tokio::test]
+async fn add_use() {
+ let (differ, handle) = DiffHandle::new_test(
+ "use ropey::Rope;\nuse tokio::task::JoinHandle;\n",
+ "use ropey::Rope;\nuse ropey::RopeSlice;\nuse tokio::task::JoinHandle;\n",
+ );
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[Hunk {
+ before: 1..1,
+ after: 1..2
+ },]
+ )
+}
+
+#[tokio::test]
+async fn update_document() {
+ let (differ, handle) = DiffHandle::new_test("foo\nbar\ntest\nfoo", "foo\nbar\ntest\nfoo");
+ differ.update_document(Rope::from_str("foo\ntest\nfoo bar"), false);
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[
+ Hunk {
+ before: 1..2,
+ after: 1..1
+ },
+ Hunk {
+ before: 3..4,
+ after: 2..3
+ },
+ ]
+ )
+}
+
+#[tokio::test]
+async fn update_base() {
+ let (differ, handle) = DiffHandle::new_test("foo\ntest\nfoo bar", "foo\ntest\nfoo bar");
+ differ.update_diff_base(Rope::from_str("foo\nbar\ntest\nfoo"));
+ let line_diffs = differ.into_diff(handle).await;
+ assert_eq!(
+ &line_diffs,
+ &[
+ Hunk {
+ before: 1..2,
+ after: 1..1
+ },
+ Hunk {
+ before: 3..4,
+ after: 2..3
+ },
+ ]
+ )
+}
diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs
new file mode 100644
index 00000000..82b2b558
--- /dev/null
+++ b/helix-vcs/src/git.rs
@@ -0,0 +1,80 @@
+use std::path::Path;
+
+use git::objs::tree::EntryMode;
+use git::sec::trust::DefaultForLevel;
+use git::{Commit, ObjectId, Repository, ThreadSafeRepository};
+use git_repository as git;
+
+use crate::DiffProvider;
+
+#[cfg(test)]
+mod test;
+
+pub struct Git;
+
+impl Git {
+ fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option<ThreadSafeRepository> {
+ // custom open options
+ let mut git_open_opts_map = git::sec::trust::Mapping::<git::open::Options>::default();
+
+ // don't use the global git configs (not needed)
+ let config = git::permissions::Config {
+ system: false,
+ git: false,
+ user: false,
+ env: true,
+ includes: true,
+ git_binary: false,
+ };
+ // change options for config permissions without touching anything else
+ git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(git::Permissions {
+ config,
+ ..git::Permissions::default_for_level(git::sec::Trust::Reduced)
+ });
+ git_open_opts_map.full = git_open_opts_map.full.permissions(git::Permissions {
+ config,
+ ..git::Permissions::default_for_level(git::sec::Trust::Full)
+ });
+
+ let mut open_options = git::discover::upwards::Options::default();
+ if let Some(ceiling_dir) = ceiling_dir {
+ open_options.ceiling_dirs = vec![ceiling_dir.to_owned()];
+ }
+
+ ThreadSafeRepository::discover_with_environment_overrides_opts(
+ path,
+ open_options,
+ git_open_opts_map,
+ )
+ .ok()
+ }
+}
+
+impl DiffProvider for Git {
+ fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
+ debug_assert!(!file.exists() || file.is_file());
+ debug_assert!(file.is_absolute());
+
+ // TODO cache repository lookup
+ let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
+ let head = repo.head_commit().ok()?;
+ let file_oid = find_file_in_commit(&repo, &head, file)?;
+
+ let file_object = repo.find_object(file_oid).ok()?;
+ Some(file_object.detach().data)
+ }
+}
+
+/// Finds the object that contains the contents of a file at a specific commit.
+fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Option<ObjectId> {
+ let repo_dir = repo.work_dir()?;
+ let rel_path = file.strip_prefix(repo_dir).ok()?;
+ let tree = commit.tree().ok()?;
+ let tree_entry = tree.lookup_entry_by_path(rel_path).ok()??;
+ match tree_entry.mode() {
+ // not a file, everything is new, do not show diff
+ EntryMode::Tree | EntryMode::Commit | EntryMode::Link => None,
+ // found a file
+ EntryMode::Blob | EntryMode::BlobExecutable => Some(tree_entry.object_id()),
+ }
+}
diff --git a/helix-vcs/src/git/test.rs b/helix-vcs/src/git/test.rs
new file mode 100644
index 00000000..d6e9af08
--- /dev/null
+++ b/helix-vcs/src/git/test.rs
@@ -0,0 +1,121 @@
+use std::{fs::File, io::Write, path::Path, process::Command};
+
+use tempfile::TempDir;
+
+use crate::{DiffProvider, Git};
+
+fn exec_git_cmd(args: &str, git_dir: &Path) {
+ let res = Command::new("git")
+ .arg("-C")
+ .arg(git_dir) // execute the git command in this directory
+ .args(args.split_whitespace())
+ .env_remove("GIT_DIR")
+ .env_remove("GIT_ASKPASS")
+ .env_remove("SSH_ASKPASS")
+ .env("GIT_TERMINAL_PROMPT", "false")
+ .env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
+ .env("GIT_AUTHOR_EMAIL", "author@example.com")
+ .env("GIT_AUTHOR_NAME", "author")
+ .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
+ .env("GIT_COMMITTER_EMAIL", "committer@example.com")
+ .env("GIT_COMMITTER_NAME", "committer")
+ .env("GIT_CONFIG_COUNT", "2")
+ .env("GIT_CONFIG_KEY_0", "commit.gpgsign")
+ .env("GIT_CONFIG_VALUE_0", "false")
+ .env("GIT_CONFIG_KEY_1", "init.defaultBranch")
+ .env("GIT_CONFIG_VALUE_1", "main")
+ .output()
+ .unwrap_or_else(|_| panic!("`git {args}` failed"));
+ if !res.status.success() {
+ println!("{}", String::from_utf8_lossy(&res.stdout));
+ eprintln!("{}", String::from_utf8_lossy(&res.stderr));
+ panic!("`git {args}` failed (see output above)")
+ }
+}
+
+fn create_commit(repo: &Path, add_modified: bool) {
+ if add_modified {
+ exec_git_cmd("add -A", repo);
+ }
+ exec_git_cmd("commit -m message", repo);
+}
+
+fn empty_git_repo() -> TempDir {
+ let tmp = tempfile::tempdir().expect("create temp dir for git testing");
+ exec_git_cmd("init", tmp.path());
+ exec_git_cmd("config user.email test@helix.org", tmp.path());
+ exec_git_cmd("config user.name helix-test", tmp.path());
+ tmp
+}
+
+#[test]
+fn missing_file() {
+ let temp_git = empty_git_repo();
+ let file = temp_git.path().join("file.txt");
+ File::create(&file).unwrap().write_all(b"foo").unwrap();
+
+ assert_eq!(Git.get_diff_base(&file), None);
+}
+
+#[test]
+fn unmodified_file() {
+ let temp_git = empty_git_repo();
+ let file = temp_git.path().join("file.txt");
+ let contents = b"foo".as_slice();
+ File::create(&file).unwrap().write_all(contents).unwrap();
+ create_commit(temp_git.path(), true);
+ assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
+}
+
+#[test]
+fn modified_file() {
+ let temp_git = empty_git_repo();
+ let file = temp_git.path().join("file.txt");
+ let contents = b"foo".as_slice();
+ File::create(&file).unwrap().write_all(contents).unwrap();
+ create_commit(temp_git.path(), true);
+ File::create(&file).unwrap().write_all(b"bar").unwrap();
+
+ assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
+}
+
+/// Test that `get_file_head` does not return content for a directory.
+/// This is important to correctly cover cases where a directory is removed and replaced by a file.
+/// If the contents of the directory object were returned a diff between a path and the directory children would be produced.
+#[test]
+fn directory() {
+ let temp_git = empty_git_repo();
+ let dir = temp_git.path().join("file.txt");
+ std::fs::create_dir(&dir).expect("");
+ let file = dir.join("file.txt");
+ let contents = b"foo".as_slice();
+ File::create(&file).unwrap().write_all(contents).unwrap();
+
+ create_commit(temp_git.path(), true);
+
+ std::fs::remove_dir_all(&dir).unwrap();
+ File::create(&dir).unwrap().write_all(b"bar").unwrap();
+ assert_eq!(Git.get_diff_base(&dir), None);
+}
+
+/// Test that `get_file_head` does not return content for a symlink.
+/// This is important to correctly cover cases where a symlink is removed and replaced by a file.
+/// If the contents of the symlink object were returned a diff between a path and the actual file would be produced (bad ui).
+#[cfg(any(unix, windows))]
+#[test]
+fn symlink() {
+ #[cfg(unix)]
+ use std::os::unix::fs::symlink;
+ #[cfg(not(unix))]
+ use std::os::windows::fs::symlink_file as symlink;
+ let temp_git = empty_git_repo();
+ let file = temp_git.path().join("file.txt");
+ let contents = b"foo".as_slice();
+ File::create(&file).unwrap().write_all(contents).unwrap();
+ let file_link = temp_git.path().join("file_link.txt");
+ symlink("file.txt", &file_link).unwrap();
+
+ create_commit(temp_git.path(), true);
+ assert_eq!(Git.get_diff_base(&file_link), None);
+ assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
+}
diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs
new file mode 100644
index 00000000..97320d32
--- /dev/null
+++ b/helix-vcs/src/lib.rs
@@ -0,0 +1,51 @@
+use std::path::Path;
+
+#[cfg(feature = "git")]
+pub use git::Git;
+#[cfg(not(feature = "git"))]
+pub use Dummy as Git;
+
+#[cfg(feature = "git")]
+mod git;
+
+mod diff;
+
+pub use diff::{DiffHandle, Hunk};
+
+pub trait DiffProvider {
+ /// Returns the data that a diff should be computed against
+ /// if this provider is used.
+ /// The data is returned as raw byte without any decoding or encoding performed
+ /// to ensure all file encodings are handled correctly.
+ fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>>;
+}
+
+#[doc(hidden)]
+pub struct Dummy;
+impl DiffProvider for Dummy {
+ fn get_diff_base(&self, _file: &Path) -> Option<Vec<u8>> {
+ None
+ }
+}
+
+pub struct DiffProviderRegistry {
+ providers: Vec<Box<dyn DiffProvider>>,
+}
+
+impl DiffProviderRegistry {
+ pub fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
+ self.providers
+ .iter()
+ .find_map(|provider| provider.get_diff_base(file))
+ }
+}
+
+impl Default for DiffProviderRegistry {
+ fn default() -> Self {
+ // currently only git is supported
+ // TODO make this configurable when more providers are added
+ let git: Box<dyn DiffProvider> = Box::new(Git);
+ let providers = vec![git];
+ DiffProviderRegistry { providers }
+ }
+}
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index a2a88001..13d5da0e 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -21,6 +21,7 @@ helix-loader = { version = "0.6", path = "../helix-loader" }
helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.25", optional = true }
+helix-vcs = { version = "0.6", path = "../helix-vcs" }
# Conversion traits
once_cell = "1.16"
@@ -43,6 +44,7 @@ log = "~0.4"
which = "4.2"
+
[target.'cfg(windows)'.dependencies]
clipboard-win = { version = "4.4", features = ["std"] }
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index ad47f838..856e5628 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -3,6 +3,8 @@ use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs;
use helix_core::Range;
+use helix_vcs::{DiffHandle, DiffProviderRegistry};
+
use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize;
use std::borrow::Cow;
@@ -24,6 +26,7 @@ use helix_core::{
DEFAULT_LINE_ENDING,
};
+use crate::editor::RedrawHandle;
use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -133,6 +136,8 @@ pub struct Document {
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
+
+ diff_handle: Option<DiffHandle>,
}
use std::{fmt, mem};
@@ -371,6 +376,7 @@ impl Document {
last_saved_revision: 0,
modified_since_accessed: false,
language_server: None,
+ diff_handle: None,
}
}
@@ -624,16 +630,20 @@ impl Document {
}
/// Reload the document from its path.
- pub fn reload(&mut self, view: &mut View) -> Result<(), Error> {
+ pub fn reload(
+ &mut self,
+ view: &mut View,
+ provider_registry: &DiffProviderRegistry,
+ redraw_handle: RedrawHandle,
+ ) -> Result<(), Error> {
let encoding = &self.encoding;
- let path = self.path().filter(|path| path.exists());
-
- // If there is no path or the path no longer exists.
- if path.is_none() {
- bail!("can't find file to reload from");
- }
+ let path = self
+ .path()
+ .filter(|path| path.exists())
+ .ok_or_else(|| anyhow!("can't find file to reload from"))?
+ .to_owned();
- let mut file = std::fs::File::open(path.unwrap())?;
+ let mut file = std::fs::File::open(&path)?;
let (rope, ..) = from_reader(&mut file, Some(encoding))?;
// Calculate the difference between the buffer and source text, and apply it.
@@ -646,6 +656,11 @@ impl Document {
self.detect_indent_and_line_ending();
+ match provider_registry.get_diff_base(&path) {
+ Some(diff_base) => self.set_diff_base(diff_base, redraw_handle),
+ None => self.diff_handle = None,
+ }
+
Ok(())
}
@@ -787,6 +802,10 @@ impl Document {
if !transaction.changes().is_empty() {
self.version += 1;
+ // start computing the diff in parallel
+ if let Some(diff_handle) = &self.diff_handle {
+ diff_handle.update_document(self.text.clone(), false);
+ }
// generate revert to savepoint
if self.savepoint.is_some() {
@@ -1046,6 +1065,23 @@ impl Document {
server.is_initialized().then(|| server)
}
+ pub fn diff_handle(&self) -> Option<&DiffHandle> {
+ self.diff_handle.as_ref()
+ }
+
+ /// Intialize/updates the differ for this document with a new base.
+ pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
+ if let Ok((diff_base, _)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
+ if let Some(differ) = &self.diff_handle {
+ differ.update_diff_base(diff_base);
+ return;
+ }
+ self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle))
+ } else {
+ self.diff_handle = None;
+ }
+ }
+
#[inline]
/// Tree-sitter AST tree
pub fn syntax(&self) -> Option<&Syntax> {
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 5a1ac6b1..973cf82e 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -9,6 +9,7 @@ use crate::{
tree::{self, Tree},
Align, Document, DocumentId, View, ViewId,
};
+use helix_vcs::DiffProviderRegistry;
use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt};
@@ -26,7 +27,10 @@ use std::{
};
use tokio::{
- sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
+ sync::{
+ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
+ Notify, RwLock,
+ },
time::{sleep, Duration, Instant, Sleep},
};
@@ -454,6 +458,8 @@ pub enum GutterType {
LineNumbers,
/// Show one blank space
Spacer,
+ /// Highlight local changes
+ Diff,
}
impl std::str::FromStr for GutterType {
@@ -464,6 +470,7 @@ impl std::str::FromStr for GutterType {
"diagnostics" => Ok(Self::Diagnostics),
"spacer" => Ok(Self::Spacer),
"line-numbers" => Ok(Self::LineNumbers),
+ "diff" => Ok(Self::Diff),
_ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
}
}
@@ -600,6 +607,8 @@ impl Default for Config {
GutterType::Diagnostics,
GutterType::Spacer,
GutterType::LineNumbers,
+ GutterType::Spacer,
+ GutterType::Diff,
],
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
@@ -681,6 +690,7 @@ pub struct Editor {
pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
+ pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>,
pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@@ -711,8 +721,15 @@ pub struct Editor {
pub exit_code: i32,
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
+ /// Allows asynchronous tasks to control the rendering
+ /// The `Notify` allows asynchronous tasks to request the editor to perform a redraw
+ /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
+ pub redraw_handle: RedrawHandle,
+ pub needs_redraw: bool,
}
+pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
+
#[derive(Debug)]
pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult),
@@ -785,6 +802,7 @@ impl Editor {
theme: theme_loader.default(),
language_servers: helix_lsp::Registry::new(),
diagnostics: BTreeMap::new(),
+ diff_providers: DiffProviderRegistry::default(),
debugger: None,
debugger_events: SelectAll::new(),
breakpoints: HashMap::new(),
@@ -803,6 +821,8 @@ impl Editor {
auto_pairs,
exit_code: 0,
config_events: unbounded_channel(),
+ redraw_handle: Default::default(),
+ needs_redraw: false,
}
}
@@ -1109,7 +1129,9 @@ impl Editor {
let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
-
+ if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
+ doc.set_diff_base(diff_base, self.redraw_handle.clone());
+ }
self.new_document(doc)
};
@@ -1348,24 +1370,39 @@ impl Editor {
}
pub async fn wait_event(&mut self) -> EditorEvent {
- tokio::select! {
- biased;
+ // the loop only runs once or twice and would be better implemented with a recursion + const generic
+ // however due to limitations with async functions that can not be implemented right now
+ loop {
+ tokio::select! {
+ biased;
+
+ Some(event) = self.save_queue.next() => {
+ self.write_count -= 1;
+ return EditorEvent::DocumentSaved(event)
+ }
+ Some(config_event) = self.config_events.1.recv() => {
+ return EditorEvent::ConfigEvent(config_event)
+ }
+ Some(message) = self.language_servers.incoming.next() => {
+ return EditorEvent::LanguageServerMessage(message)
+ }
+ Some(event) = self.debugger_events.next() => {
+ return EditorEvent::DebuggerEvent(event)
+ }
- Some(event) = self.save_queue.next() => {
- self.write_count -= 1;
- EditorEvent::DocumentSaved(event)
- }
- Some(config_event) = self.config_events.1.recv() => {
- EditorEvent::ConfigEvent(config_event)
- }
- Some(message) = self.language_servers.incoming.next() => {
- EditorEvent::LanguageServerMessage(message)
- }
- Some(event) = self.debugger_events.next() => {
- EditorEvent::DebuggerEvent(event)
- }
- _ = &mut self.idle_timer => {
- EditorEvent::IdleTimer
+ _ = self.redraw_handle.0.notified() => {
+ if !self.needs_redraw{
+ self.needs_redraw = true;
+ let timeout = Instant::now() + Duration::from_millis(96);
+ if timeout < self.idle_timer.deadline(){
+ self.idle_timer.as_mut().reset(timeout)
+ }
+ }
+ }
+
+ _ = &mut self.idle_timer => {
+ return EditorEvent::IdleTimer
+ }
}
}
}
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
index 61a17791..377518fb 100644
--- a/helix-view/src/gutter.rs
+++ b/helix-view/src/gutter.rs
@@ -12,7 +12,7 @@ fn count_digits(n: usize) -> usize {
std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
}
-pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
+pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, &mut String) -> Option<Style> + 'doc>;
pub type Gutter =
for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
@@ -31,6 +31,7 @@ impl GutterType {
}
GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
+ GutterType::Diff => diff(editor, doc, view, theme, is_focused),
}
}
@@ -39,6 +40,7 @@ impl GutterType {
GutterType::Diagnostics => 1,
GutterType::LineNumbers => line_numbers_width(_view, doc),
GutterType::Spacer => 1,
+ GutterType::Diff => 1,
}
}
}
@@ -83,6 +85,53 @@ pub fn diagnostic<'doc>(
})
}
+pub fn diff<'doc>(
+ _editor: &'doc Editor,
+ doc: &'doc Document,
+ _view: &View,
+ theme: &Theme,
+ _is_focused: bool,
+) -> GutterFn<'doc> {
+ let added = theme.get("diff.plus");
+ let deleted = theme.get("diff.minus");
+ let modified = theme.get("diff.delta");
+ if let Some(diff_handle) = doc.diff_handle() {
+ let hunks = diff_handle.hunks();
+ let mut hunk_i = 0;
+ let mut hunk = hunks.nth_hunk(hunk_i);
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ // truncating the line is fine here because we don't compute diffs
+ // for files with more lines than i32::MAX anyways
+ // we need to special case removals here
+ // these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`).
+ // However we still want to display these hunks correctly we must not yet skip to the next hunk here
+ while hunk.after.end < line as u32
+ || !hunk.is_pure_removal() && line as u32 == hunk.after.end
+ {
+ hunk_i += 1;
+ hunk = hunks.nth_hunk(hunk_i);
+ }
+
+ if hunk.after.start > line as u32 {
+ return None;
+ }
+
+ let (icon, style) = if hunk.is_pure_insertion() {
+ ("▍", added)
+ } else if hunk.is_pure_removal() {
+ ("▔", deleted)
+ } else {
+ ("▍", modified)
+ };
+
+ write!(out, "{}", icon).unwrap();
+ Some(style)
+ })
+ } else {
+ Box::new(move |_, _, _| None)
+ }
+}
+
pub fn line_numbers<'doc>(
editor: &'doc Editor,
doc: &'doc Document,
@@ -226,8 +275,8 @@ pub fn diagnostics_or_breakpoints<'doc>(
theme: &Theme,
is_focused: bool,
) -> GutterFn<'doc> {
- let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
- let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
+ let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
+ let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
Box::new(move |line, selected, out| {
breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 845a5458..ecc8e8be 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -158,17 +158,10 @@ impl View {
}
pub fn gutter_offset(&self, doc: &Document) -> u16 {
- let mut offset = self
- .gutters
+ self.gutters
.iter()
.map(|gutter| gutter.width(self, doc) as u16)
- .sum();
-
- if offset > 0 {
- offset += 1
- }
-
- offset
+ .sum()
}
//
@@ -392,8 +385,8 @@ impl View {
mod tests {
use super::*;
use helix_core::Rope;
- const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter
- const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
+ const OFFSET: u16 = 3; // 1 diagnostic + 2 linenr (< 100 lines)
+ const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 1; // 1 diagnostic
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
use crate::document::Document;
use crate::editor::GutterType;