Merge branch 'tree_explore' of github.com:cossonfork/helix into tree_explore

imgbot
trivernis 2 years ago
commit e5ed461ca7
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -1,2 +1,3 @@
[alias]
xtask = "run --package xtask --"
integration-test = "test --features integration --workspace --test integration"

11
.gitattributes vendored

@ -0,0 +1,11 @@
# Auto detect text files and perform normalization
* text=auto
*.rs text diff=rust
*.toml text diff=toml
*.scm text diff=scheme
*.md text diff=markdown
book/theme/highlight.js linguist-vendored
Cargo.lock text

@ -38,6 +38,9 @@ jobs:
test:
name: Test Suite
runs-on: ${{ matrix.os }}
env:
RUST_BACKTRACE: 1
HELIX_LOG_LEVEL: info
steps:
- name: Checkout sources
uses: actions/checkout@v3
@ -50,9 +53,6 @@ jobs:
- uses: Swatinem/rust-cache@v1
- name: Copy minimal languages config
run: cp .github/workflows/languages.toml ./languages.toml
- name: Cache test tree-sitter grammar
uses: actions/cache@v3
with:
@ -66,6 +66,11 @@ jobs:
command: test
args: --workspace
- name: Run cargo integration-test
uses: actions-rs/cargo@v1
with:
command: integration-test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

@ -64,10 +64,12 @@ jobs:
rust: stable
target: x86_64-pc-windows-msvc
cross: false
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
- build: aarch64-macos
os: macos-latest
rust: stable
target: aarch64-apple-darwin
cross: false
skip_tests: true # x86_64 host can't run aarch64 code
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu
@ -100,6 +102,7 @@ jobs:
- name: Run cargo test
uses: actions-rs/cargo@v1
if: "!matrix.skip_tests"
with:
use-cross: ${{ matrix.cross }}
command: test
@ -113,7 +116,7 @@ jobs:
args: --release --locked --target ${{ matrix.target }}
- name: Strip release binary (linux and macos)
if: matrix.build == 'x86_64-linux' || matrix.build == 'x86_64-macos'
if: matrix.build == 'x86_64-linux' || endsWith(matrix.build, 'macos')
run: strip "target/${{ matrix.target }}/release/hx"
- name: Strip release binary (arm)

1
.gitignore vendored

@ -1,6 +1,5 @@
target
.direnv
helix-term/rustfmt.toml
helix-syntax/languages/
result
runtime/grammars

@ -0,0 +1,5 @@
# Things that we don't want ripgrep to search that we do want in git
# https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering
# Minified JS vendored from mdbook
book/theme/highlight.js

343
Cargo.lock generated

@ -11,17 +11,26 @@ dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.58"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305"
[[package]]
name = "arc-swap"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f"
checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
[[package]]
name = "autocfg"
@ -46,17 +55,23 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "bumpalo"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
[[package]]
name = "bytecount"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e"
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
[[package]]
name = "bytes"
version = "1.1.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "cassowary"
@ -89,11 +104,11 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.19"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"libc",
"iana-time-zone",
"num-integer",
"num-traits",
"winapi",
@ -101,9 +116,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
version = "4.4.1"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db"
checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219"
dependencies = [
"error-code",
"str-buf",
@ -119,21 +134,27 @@ dependencies = [
"memchr",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "crossbeam-utils"
version = "0.8.8"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
dependencies = [
"cfg-if",
"lazy_static",
"once_cell",
]
[[package]]
name = "crossterm"
version = "0.23.2"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags",
"crossterm_winapi",
@ -178,9 +199,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
[[package]]
name = "encoding_rs"
@ -221,6 +242,15 @@ dependencies = [
"thiserror",
]
[[package]]
name = "fastrand"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
dependencies = [
"instant",
]
[[package]]
name = "fern"
version = "0.6.1"
@ -248,15 +278,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.21"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115"
[[package]]
name = "futures-executor"
version = "0.3.21"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528"
dependencies = [
"futures-core",
"futures-task",
@ -265,15 +295,15 @@ dependencies = [
[[package]]
name = "futures-task"
version = "0.3.21"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306"
[[package]]
name = "futures-util"
version = "0.3.21"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577"
dependencies = [
"futures-core",
"futures-task",
@ -293,20 +323,20 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.6"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if",
"libc",
"wasi 0.10.2+wasi-snapshot-preview1",
"wasi",
]
[[package]]
name = "globset"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
dependencies = [
"aho-corasick",
"bstr",
@ -326,9 +356,9 @@ dependencies = [
[[package]]
name = "grep-regex"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06"
checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e"
dependencies = [
"aho-corasick",
"bstr",
@ -341,9 +371,9 @@ dependencies = [
[[package]]
name = "grep-searcher"
version = "0.1.8"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d"
checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc"
dependencies = [
"bstr",
"bytecount",
@ -452,6 +482,7 @@ dependencies = [
"helix-tui",
"helix-view",
"ignore",
"indoc",
"log",
"once_cell",
"pulldown-cmark",
@ -460,6 +491,8 @@ dependencies = [
"serde_json",
"signal-hook",
"signal-hook-tokio",
"smallvec",
"tempfile",
"tokio",
"tokio-stream",
"toml",
@ -515,6 +548,19 @@ dependencies = [
"libc",
]
[[package]]
name = "iana-time-zone"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf7d67cf4a22adc5be66e75ebdf769b3f2ea032041437a7061f97a63dad4b"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "idna"
version = "0.2.3"
@ -544,11 +590,35 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indoc"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "itoa"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "js-sys"
version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
@ -558,9 +628,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.126"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b"
[[package]]
name = "libloading"
@ -618,22 +688,22 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memmap2"
version = "0.3.1"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357"
checksum = "3a79b39c93a7a5a27eeaf9a23b5ff43f1b9e0ad6b1cdd441140ae53c35613fc7"
dependencies = [
"libc",
]
[[package]]
name = "mio"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"windows-sys",
]
@ -668,15 +738,15 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.12.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
[[package]]
name = "parking_lot"
version = "0.12.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
@ -715,18 +785,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.39"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6"
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
dependencies = [
"bitflags",
"memchr",
@ -744,9 +814,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.18"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
@ -771,9 +841,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.2.13"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
@ -791,9 +861,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.5.6"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [
"aho-corasick",
"memchr",
@ -808,9 +878,18 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.6.26"
version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "retain_mut"
@ -830,9 +909,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "same-file"
@ -851,18 +930,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.137"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.137"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
dependencies = [
"proc-macro2",
"quote",
@ -871,9 +950,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.81"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
dependencies = [
"itoa",
"ryu",
@ -882,9 +961,9 @@ dependencies = [
[[package]]
name = "serde_repr"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed"
checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca"
dependencies = [
"proc-macro2",
"quote",
@ -935,15 +1014,18 @@ dependencies = [
[[package]]
name = "similar"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
[[package]]
name = "slab"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
dependencies = [
"autocfg",
]
[[package]]
name = "slotmap"
@ -956,9 +1038,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
[[package]]
name = "smartstring"
@ -1007,15 +1089,29 @@ checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0"
[[package]]
name = "syn"
version = "1.0.95"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
name = "textwrap"
version = "0.15.0"
@ -1029,18 +1125,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
dependencies = [
"proc-macro2",
"quote",
@ -1082,10 +1178,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.19.2"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
@ -1102,9 +1199,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
dependencies = [
"proc-macro2",
"quote",
@ -1133,9 +1230,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.6"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3b781640108d29892e8b9684642d2cda5ea05951fd58f0fea1db9edeb9b71"
checksum = "268bf3e3ca0c09e5d21b59c2638e12cb6dcf7ea2681250a696a2d0936cb57ba0"
dependencies = [
"cc",
"regex",
@ -1164,9 +1261,9 @@ checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6"
[[package]]
name = "unicode-ident"
version = "1.0.0"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
[[package]]
name = "unicode-linebreak"
@ -1179,9 +1276,9 @@ dependencies = [
[[package]]
name = "unicode-normalization"
version = "0.1.19"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
dependencies = [
"tinyvec",
]
@ -1230,15 +1327,63 @@ dependencies = [
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
name = "wasm-bindgen"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
[[package]]
name = "which"

@ -1,6 +1,5 @@
# Helix
[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)
![Screenshot](./screenshot.png)
@ -49,10 +48,11 @@ tree-sitter grammars may be manually fetched and built with `hx --grammar fetch`
Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
| OS | command |
|-----------|-----------|
|windows |`xcopy runtime %AppData%/helix/runtime`|
|linux/macos|`ln -s $PWD/runtime ~/.config/helix/runtime`
| OS | Command |
| -------------------- | -------------------------------------------- |
| Windows (cmd.exe) | `xcopy /e runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e runtime $Env:AppData\helix\runtime` |
| Linux/macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
This location can be overridden via the `HELIX_RUNTIME` environment variable.
@ -76,7 +76,7 @@ Helix can be installed on MacOS through homebrew via:
brew tap helix-editor/helix
brew install helix
```
# Contributing
Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).

@ -25,6 +25,9 @@ select = "underline"
hidden = false
```
You may also specify a file to use for configuration with the `-c` or
`--config` CLI argument: `hx -c path/to/custom-config.toml`.
## Editor
### `[editor]` Section
@ -37,7 +40,8 @@ hidden = false
| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `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` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers`, note that `diagnostics` also includes other features like breakpoints | `["diagnostics", "line-numbers"]` |
| `cursorline` | Highlight all lines 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", "line-numbers"]` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
@ -45,20 +49,58 @@ hidden = false
| `auto-info` | Whether to display infoboxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
### `[editor.statusline]` Section
Allows configuring the statusline at the bottom of the editor.
The configuration distinguishes between three areas of the status line:
`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]`
Statusline elements can be defined as follows:
```toml
[editor.statusline]
left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
separator = "│"
```
The following elements can be configured:
| Key | Description |
| ------ | ----------- |
| `mode` | The current editor mode (`NOR`/`INS`/`SEL`) |
| `spinner` | A progress spinner indicating LSP activity |
| `file-name` | The path/name of the opened file |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
| `selections` | The number of active selections |
| `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines |
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |
| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
### `[editor.lsp]` Section
| Key | Description | Default |
| --- | ----------- | ------- |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| Key | Description | Default |
| --- | ----------- | ------- |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
[^1]: A progress spinner is always shown in the statusline beside the file path.
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
### `[editor.cursor-shape]` Section
Defines the shape of cursor in each mode. Note that due to limitations
of the terminal environment, only the primary cursor can change shape.
Valid values for these options are `block`, `bar`, `underline`, or `none`.
Valid values for these options are `block`, `bar`, `underline`, or `hidden`.
| Key | Description | Default |
| --- | ----------- | ------- |
@ -78,6 +120,8 @@ files and files listed within ignore files are ignored by (not visible in) the
helix file picker and global search. There is also one other key, `max-depth`
available, which is not defined by default.
All git related options are only enabled in a git repository.
| Key | Description | Default |
|--|--|---------|
|`hidden` | Enables ignoring hidden files. | true
@ -150,7 +194,7 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r
| Key | Description | Default |
|-----|-------------|---------|
| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `tab`, and `newline`. | `"none"` |
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp` or `newline` | See example below |
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below |
Example
@ -168,6 +212,36 @@ space = "·"
nbsp = "⍽"
tab = "→"
newline = "⏎"
tabpad = "·" # Tabs will look like "→···" (depending on tab width)
```
<<<<<<< HEAD
### `[editor.explorer]` Section
Sets explorer side width and style.
| Key | Description | Default |
| --- | ----------- | ------- |
| `column-width` | explorer side width | 30 |
| `style` | explorer item style, tree or list | tree |
| `position` | explorer widget position, embed or overlay | overlay |
||||||| 43027d91
=======
### `[editor.indent-guides]` Section
Options for rendering vertical indent guides.
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides. | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
Example:
```toml
[editor.indent-guides]
render = true
character = "╎"
```
### `[editor.explorer]` Section
@ -178,3 +252,4 @@ Sets explorer side width and style.
| `column-width` | explorer side width | 30 |
| `style` | explorer item style, tree or list | tree |
| `position` | explorer widget position, embed or overlay | overlay |
>>>>>>> 0e04c4c93caadb704c11a72bcf626b1f10ff2d98

@ -1,6 +1,8 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | | | `bash-language-server` |
| beancount | ✓ | | | |
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | | | `OmniSharp` |
| cairo | ✓ | | | |
@ -10,17 +12,22 @@
| cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` |
| css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` |
| dart | ✓ | | ✓ | `dart` |
| devicetree | ✓ | | ✓ | |
| dockerfile | ✓ | | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` |
| edoc | ✓ | | | |
| eex | ✓ | | | |
| ejs | ✓ | | | |
| elixir | ✓ | ✓ | | `elixir-ls` |
| elm | ✓ | | | `elm-language-server` |
| elvish | ✓ | | | `elvish` |
| erb | ✓ | | | |
| erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | |
| fortran | ✓ | | ✓ | `fortls` |
| gdscript | ✓ | | ✓ | |
| git-attributes | ✓ | | | |
| git-commit | ✓ | | | |
@ -28,26 +35,28 @@
| git-diff | ✓ | | | |
| git-ignore | ✓ | | | |
| git-rebase | ✓ | | | |
| gleam | ✓ | ✓ | | |
| glsl | ✓ | | ✓ | |
| gleam | ✓ | ✓ | | `gleam` |
| glsl | ✓ | | ✓ | |
| go | ✓ | ✓ | ✓ | `gopls` |
| gomod | ✓ | | | `gopls` |
| gotmpl | ✓ | | | `gopls` |
| gowork | ✓ | | | `gopls` |
| graphql | ✓ | | | |
| hare | ✓ | | ✓ | |
| haskell | ✓ | | | `haskell-language-server-wrapper` |
| hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | | | |
| heex | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| java | ✓ | | | `jdtls` |
| javascript | ✓ | | ✓ | `typescript-language-server` |
| javascript | ✓ | | ✓ | `typescript-language-server` |
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| jsx | ✓ | | ✓ | `typescript-language-server` |
| jsx | ✓ | | ✓ | `typescript-language-server` |
| julia | ✓ | | | `julia` |
| kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | | | `texlab` |
| latex | ✓ | | | `texlab` |
| lean | ✓ | | | `lean` |
| ledger | ✓ | | | |
| llvm | ✓ | ✓ | ✓ | |
@ -56,6 +65,7 @@
| lua | ✓ | | ✓ | `lua-language-server` |
| make | ✓ | | | |
| markdown | ✓ | | | |
| markdown.inline | ✓ | | | |
| meson | ✓ | | ✓ | |
| mint | | | | `mint` |
| nickel | ✓ | | ✓ | `nls` |
@ -63,7 +73,7 @@
| nu | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | | |
| odin | ✓ | | | `ols` |
| openscad | ✓ | | | `openscad-language-server` |
| org | ✓ | | | |
| perl | ✓ | ✓ | ✓ | |
@ -71,7 +81,7 @@
| prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | |
| python | ✓ | ✓ | | `pylsp` |
| python | ✓ | ✓ | | `pylsp` |
| r | ✓ | | | `R` |
| racket | | | | `racket` |
| regex | ✓ | | | |
@ -82,22 +92,28 @@
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
| scala | ✓ | | ✓ | `metals` |
| scheme | ✓ | | | |
| scss | ✓ | | | `vscode-css-language-server` |
| slint | ✓ | | ✓ | `slint-lsp` |
| solidity | ✓ | | | `solc` |
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| swift | ✓ | | | `sourcekit-lsp` |
| tablegen | ✓ | ✓ | ✓ | |
| task | ✓ | | | |
| tfvars | | | | `terraform-ls` |
| toml | ✓ | | | `taplo` |
| tsq | ✓ | | | |
| tsx | ✓ | | | `typescript-language-server` |
| tsx | ✓ | | | `typescript-language-server` |
| twig | ✓ | | | |
| typescript | ✓ | | ✓ | `typescript-language-server` |
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| ungrammar | ✓ | | | |
| v | ✓ | | | `vls` |
| vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` |
| vue | ✓ | | | `vls` |
| wgsl | ✓ | | | |
| wgsl | ✓ | | | `wgsl_analyzer` |
| xit | ✓ | | | |
| yaml | ✓ | | ✓ | `yaml-language-server` |
| zig | ✓ | | ✓ | `zls` |

@ -1,18 +1,18 @@
| Name | Description |
| --- | --- |
| `:quit`, `:q` | Close the current view. |
| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
| `:quit!`, `:q!` | Force close the current view, ignoring unsaved changes. |
| `:open`, `:o` | Open a file from disk into the current view. |
| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully, ignoring unsaved changes. |
| `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. |
| `:buffer-close-others!`, `:bco!`, `:bcloseother!` | Close all buffers but the currently focused one. |
| `:buffer-close-all`, `:bca`, `:bcloseall` | Close all buffers, without quitting. |
| `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Close all buffers forcefully (ignoring unsaved changes), without quitting. |
| `:buffer-next`, `:bn`, `:bnext` | Go to next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Go to previous buffer. |
| `:buffer-close-others!`, `:bco!`, `:bcloseother!` | Force close all buffers but the currently focused one. |
| `:buffer-close-all`, `:bca`, `:bcloseall` | Close all buffers without quitting. |
| `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Force close all buffers ignoring unsaved changes without quitting. |
| `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. |
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Write changes to disk forcefully (creating necessary subdirectories). Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write some/path.txt) |
| `:new`, `:n` | Create a new scratch buffer. |
| `:format`, `:fmt` | Format the file using the LSP formatter. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
@ -25,9 +25,9 @@
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). |
| `:quit-all`, `:qa` | Close all views. |
| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
| `:quit-all!`, `:qa!` | Force close all views ignoring unsaved changes. |
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2). |
| `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme. |
| `:clipboard-yank` | Yank main selection into system clipboard. |
| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
@ -42,7 +42,7 @@
| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
| `:change-current-directory`, `:cd` | Change the current working directory. |
| `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
| `:reload` | Discard changes and reload from the source file. |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
@ -53,7 +53,7 @@
| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
| `:hsplit-new`, `:hnew` | Open a scratch buffer in a horizontal split. |
| `:tutor` | Open the tutorial. |
| `:goto`, `:g` | Go to line number. |
| `:goto`, `:g` | Goto line number. |
| `:set-language`, `:lang` | Set the language of current buffer. |
| `:set-option`, `:set` | Set a config option at runtime.<br>For example to disable smart case search, use `:set search.smart-case false`. |
| `:get-option`, `:get` | Get the current value of a config option. |
@ -61,8 +61,8 @@
| `:rsort` | Sort ranges in selection in reverse order. |
| `:reflow` | Hard-wrap the current selection of lines to a given width. |
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refreshes helix's config. |
| `:config-open` | Open the helix config.toml file. |
| `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml file. |
| `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output after each selection. |
| `:append-output` | Run shell command, appending output after each selection. |

@ -20,6 +20,8 @@ The following [captures][tree-sitter-captures] are recognized:
| `function.around` |
| `class.inside` |
| `class.around` |
| `test.inside` |
| `test.around` |
| `parameter.inside` |
| `comment.inside` |
| `comment.around` |

@ -22,8 +22,12 @@ the project root. The flake can also be used to spin up a reproducible developme
shell for working on Helix with `nix develop`.
Flake outputs are cached for each push to master using
[Cachix](https://www.cachix.org/). With Cachix
[installed](https://docs.cachix.org/installation), `cachix use helix` will
[Cachix](https://www.cachix.org/). The flake is configured to
automatically make use of this cache assuming the user accepts
the new settings on first use.
If you are using a version of Nix without flakes enabled you can
[install Cachix cli](https://docs.cachix.org/installation); `cachix use helix` will
configure Nix to use cached outputs when possible.
### Arch Linux
@ -61,10 +65,11 @@ Helix also needs it's runtime files so make sure to copy/symlink the `runtime/`
config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overridden
via the `HELIX_RUNTIME` environment variable.
| OS | command |
|-----------|-----------|
|windows |`xcopy runtime %AppData%/helix/runtime`|
|linux/macos|`ln -s $PWD/runtime ~/.config/helix/runtime`
| OS | command |
|-------------------|-----------|
|windows(cmd.exe) |`xcopy /e runtime %AppData%/helix/runtime` |
|windows(powershell)|`xcopy /e runtime $Env:AppData\helix\runtime` |
|linux/macos |`ln -s $PWD/runtime ~/.config/helix/runtime`|
## Finishing up the installation
@ -75,8 +80,13 @@ hx --health
For more information on the information displayed in the healthcheck results refer to [Healthcheck](https://github.com/helix-editor/helix/wiki/Healthcheck).
## Building tree-sitter grammars
### Building tree-sitter grammars
Tree-sitter grammars must be fetched and compiled if not pre-packaged.
Fetch grammars with `hx --grammar fetch` (requires `git`) and compile them
with `hx --grammar build` (requires a C++ compiler).
### Installing language servers
Language servers can optionally be installed if you want their features (auto-complete, diagnostics etc.).
Follow the [instructions on the wiki page](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) to add your language servers of choice.

@ -1,7 +1,27 @@
# Keymap
- Mappings marked (**LSP**) require an active language server for the file.
- Mappings marked (**TS**) require a tree-sitter grammar for the filetype.
- [Normal mode](#normal-mode)
- [Movement](#movement)
- [Changes](#changes)
- [Shell](#shell)
- [Selection manipulation](#selection-manipulation)
- [Search](#search)
- [Minor modes](#minor-modes)
- [View mode](#view-mode)
- [Goto mode](#goto-mode)
- [Match mode](#match-mode)
- [Window mode](#window-mode)
- [Space mode](#space-mode)
- [Popup](#popup)
- [Unimpaired](#unimpaired)
- [Insert Mode](#insert-mode)
- [Select / extend mode](#select--extend-mode)
- [Picker](#picker)
- [Prompt](#prompt)
> 💡 Mappings marked (**LSP**) require an active language server for the file.
> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the filetype.
## Normal mode
@ -52,7 +72,7 @@
| `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
| `.` | Repeat last change | N/A |
| `.` | Repeat last insert | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `Alt-u` | Move backward in history | `earlier` |
@ -238,9 +258,12 @@ This layer is a kludge of mappings, mostly pickers.
| ----- | ----------- | ------- |
| `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` |
| `j` | Open jumplist picker | `jumplist_picker` |
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
| `g` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` |
| `G` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker`
| `r` | Rename symbol (**LSP**) | `rename_symbol` |
| `a` | Apply code action (**LSP**) | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` |
@ -284,6 +307,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]o` | Go to next comment (**TS**) | `goto_next_comment` |
| `[o` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]t` | Go to next test (**TS**) | `goto_next_test` |
| `]t` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `[space` | Add newline above | `add_newline_above` |
@ -296,30 +321,30 @@ convenience. These can be helpful for making simple modifications
without escaping to normal mode, but beware that you will not have an
undo-able "save point" until you return to normal mode.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-r` | Insert a register content | `insert_register` |
| `Ctrl-w`, `Alt-Backspace` | Delete previous word | `delete_word_backward` |
| `Alt-d` | Delete next word | `delete_word_forward` |
| `Alt-b`, `Ctrl-Left` | Backward a word | `move_prev_word_end` |
| `Ctrl-b`, `Left` | Backward a char | `move_char_left` |
| `Alt-f`, `Ctrl-Right` | Forward a word | `move_next_word_start` |
| `Ctrl-f`, `Right` | Forward a char | `move_char_right` |
| `Ctrl-e`, `End` | Move to line end | `goto_line_end_newline` |
| `Ctrl-a`, `Home` | Move to line start | `goto_line_start` |
| `Ctrl-u` | Delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | Delete to end of line | `kill_to_line_end` |
| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` |
| `Backspace`, `Ctrl-h` | Delete previous char | `delete_char_backward` |
| `Delete`, `Ctrl-d` | Delete next char | `delete_char_forward` |
| `Ctrl-p`, `Up` | Move to previous line | `move_line_up` |
| `Ctrl-n`, `Down` | Move to next line | `move_line_down` |
| `PageUp` | Move one page up | `page_up` |
| `PageDown` | Move one page down | `page_down` |
| `Alt->` | Go to end of buffer | `goto_file_end` |
| `Alt-<` | Go to start of buffer | `goto_file_start` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-r` | Insert a register content | `insert_register` |
| `Ctrl-w`, `Alt-Backspace`, `Ctrl-Backspace` | Delete previous word | `delete_word_backward` |
| `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word | `delete_word_forward` |
| `Alt-b`, `Ctrl-Left` | Backward a word | `move_prev_word_end` |
| `Ctrl-b`, `Left` | Backward a char | `move_char_left` |
| `Alt-f`, `Ctrl-Right` | Forward a word | `move_next_word_start` |
| `Ctrl-f`, `Right` | Forward a char | `move_char_right` |
| `Ctrl-e`, `End` | Move to line end | `goto_line_end_newline` |
| `Ctrl-a`, `Home` | Move to line start | `goto_line_start` |
| `Ctrl-u` | Delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | Delete to end of line | `kill_to_line_end` |
| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` |
| `Backspace`, `Ctrl-h` | Delete previous char | `delete_char_backward` |
| `Delete`, `Ctrl-d` | Delete next char | `delete_char_forward` |
| `Ctrl-p`, `Up` | Move to previous line | `move_line_up` |
| `Ctrl-n`, `Down` | Move to next line | `move_line_down` |
| `PageUp` | Move one page up | `page_up` |
| `PageDown` | Move one page down | `page_down` |
| `Alt->` | Go to end of buffer | `goto_file_end` |
| `Alt-<` | Go to start of buffer | `goto_file_start` |
## Select / extend mode
@ -334,15 +359,15 @@ mode before pressing `n` or `N` makes it possible to keep the current
selection. Toggling it on and off during your iterative searching allows
you to selectively add search terms to your selections.
# Picker
## Picker
Keys to use within picker. Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Up`, `Ctrl-p` | Previous entry |
| `Tab`, `Up`, `Ctrl-p` | Previous entry |
| `PageUp`, `Ctrl-u` | Page up |
| `Down`, `Ctrl-n` | Next entry |
| `Shift-tab`, `Down`, `Ctrl-n`| Next entry |
| `PageDown`, `Ctrl-d` | Page down |
| `Home` | Go to first entry |
| `End` | Go to last entry |
@ -350,34 +375,35 @@ Keys to use within picker. Remapping currently not supported.
| `Enter` | Open selected |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Ctrl-t` | Toggle preview |
| `Escape`, `Ctrl-c` | Close picker |
# Prompt
## Prompt
Keys to use within prompt, Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Escape`, `Ctrl-c` | Close prompt |
| `Alt-b`, `Alt-Left` | Backward a word |
| `Ctrl-b`, `Left` | Backward a char |
| `Alt-f`, `Alt-Right` | Forward a word |
| `Ctrl-f`, `Right` | Forward a char |
| `Ctrl-e`, `End` | Move prompt end |
| `Ctrl-a`, `Home` | Move prompt start |
| `Ctrl-w` | Delete previous word |
| `Alt-d` | Delete next word |
| `Ctrl-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line |
| `backspace`, `Ctrl-h` | Delete previous char |
| `delete`, `Ctrl-d` | Delete next char |
| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |
| `Ctrl-r` | Insert the content of the register selected by following input char |
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected |
| Key | Description |
| ----- | ------------- |
| `Escape`, `Ctrl-c` | Close prompt |
| `Alt-b`, `Ctrl-Left` | Backward a word |
| `Ctrl-b`, `Left` | Backward a char |
| `Alt-f`, `Ctrl-Right` | Forward a word |
| `Ctrl-f`, `Right` | Forward a char |
| `Ctrl-e`, `End` | Move prompt end |
| `Ctrl-a`, `Home` | Move prompt start |
| `Ctrl-w`, `Alt-Backspace`, `Ctrl-Backspace` | Delete previous word |
| `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word |
| `Ctrl-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line |
| `backspace`, `Ctrl-h` | Delete previous char |
| `delete`, `Ctrl-d` | Delete next char |
| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |
| `Ctrl-r` | Insert the content of the register selected by following input char |
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected |
# File explorer
Keys to use within explorer, Remapping currently not supported.

@ -40,6 +40,7 @@ file-types = ["mylang", "myl"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "mylang-lsp", args = ["--stdio"] }
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
```
These configuration keys are available:
@ -59,6 +60,7 @@ These configuration keys are available:
| `language-server` | The Language Server to run. See the Language Server configuration section below. |
| `config` | Language Server configuration |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
### Language Server configuration

@ -103,6 +103,8 @@ We use a similar set of scopes as
[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
- `attribute` - Class attributes, html tag attributes
- `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`)
- `constructor`
@ -133,13 +135,13 @@ We use a similar set of scopes as
- `parameter` - Function parameters
- `other`
- `member` - Fields of composite data types (e.g. structs, unions)
- `function` (TODO: ?)
- `label`
- `punctuation`
- `delimiter` - Commas, colons
- `bracket` - Parentheses, angle brackets, etc.
- `special` - String interpolation brackets.
- `keyword`
- `control`
@ -151,7 +153,9 @@ We use a similar set of scopes as
- `operator` - `or`, `in`
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
- `storage` - Keywords that affect the storage of a variable, function or data structure `static`, `mut`, `const`, `ref`
- `storage` - Keywords describing how things are stored
- `type` - The type of something, `class`, `function`, `var`, `let`, etc.
- `modifier` - Storage modifiers like `static`, `mut`, `const`, `ref`, etc.
- `operator` - `||`, `+=`, `>`
@ -219,6 +223,10 @@ These scopes are used for theming the editor interface.
| `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.popup` | Documentation popups (e.g space-k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Border lines separating splits |
@ -226,12 +234,16 @@ These scopes are used for theming the editor interface.
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][rulers-config])|
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section])|
| `ui.virtual.whitespace` | Visible white-space characters |
| `ui.virtual.indent-guide` | Vertical indent width guides |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `ui.cursorline.primary` | The line of the primary cursor |
| `ui.cursorline.secondary` | The lines of any other cursors |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
@ -242,4 +254,4 @@ These scopes are used for theming the editor interface.
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
[rulers-config]: ./configuration.md#editor-section
[editor-section]: ./configuration.md#editor-section

@ -6,6 +6,7 @@ Docs for bleeding edge master can be found at
See the [usage] section for a quick overview of the editor, [keymap]
section for all available keybindings and the [configuration] section
for defining custom keybindings, setting themes, etc.
For everything else (e.g., how to install supported language servers), see the [Helix Wiki].
Refer the [FAQ] for common questions.
@ -13,3 +14,4 @@ Refer the [FAQ] for common questions.
[usage]: ./usage.md
[keymap]: ./keymap.md
[configuration]: ./configuration.md
[Helix Wiki]: https://github.com/helix-editor/helix/wiki

@ -143,6 +143,7 @@ Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
| `c` | Class |
| `a` | Argument/parameter |
| `o` | Comment |
| `t` | Test |
> NOTE: `f`, `c`, etc need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only

@ -16,7 +16,7 @@ _hx() {
COMPREPLY=($(compgen -W "$languages" -- $2))
;;
*)
COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar" -- $2))
COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config" -- $2))
;;
esac
} && complete -F _hx hx

@ -0,0 +1,49 @@
# You can move it here ~/.config/elvish/lib/hx.elv
# Or add `eval (slurp < ~/$REPOS/helix/contrib/completion/hx.elv)`
# Be sure to replace `$REPOS` with something that makes sense for you!
### Renders a pretty completion candidate
var candidate = { | _stem _desc |
edit:complex-candidate $_stem &display=(styled $_stem bold)(styled " "$_desc dim)
}
### These commands will invalidate further input (i.e. not react to them)
var skips = [ "--tutor" "--help" "--version" "-V" "--health" ]
### Grammar commands
var grammar = [ "--grammar" "-g" ]
### Config commands
var config = [ "--config" "-c" ]
### Set an arg-completer for the `hx` binary
set edit:completion:arg-completer[hx] = {|@args|
var n = (count $args)
if (>= $n 3) {
# Stop completions if passed arg will take presedence
# and invalidate further input
if (has-value $skips $args[-2]) {
return
}
# If the previous arg == --grammar, then only suggest:
if (has-value $grammar $args[-2]) {
$candidate "fetch" "Fetch the tree-sitter grammars"
$candidate "build" "Build the tree-sitter grammars"
return
}
# When we have --config, we need a file
if (has-values $config $args[-2]) {
edit:complete-filename $args[-1] | each { |v| put $v[stem] }
return
}
}
edit:complete-filename $args[-1] | each { |v| put $v[stem]}
$candidate "--help" "(Prints help information)"
$candidate "--version" "(Prints version information)"
$candidate "--tutor" "(Loads the tutorial)"
$candidate "--health" "(Checks for errors in editor setup)"
$candidate "--grammar" "(Fetch or build the tree-sitter grammars)"
$candidate "--vsplit" "(Splits all given files vertically)"
$candidate "--hsplit" "(Splits all given files horizontally)"
$candidate "--config" "(Specifies a file to use for configuration)"
}

@ -9,4 +9,6 @@ complete -c hx -l health -x -a "$langs" -d "Checks for errors in editor setup"
complete -c hx -s g -l grammar -x -a "fetch build" -d "Fetches or builds tree-sitter grammars"
complete -c hx -s v -o vv -o vvv -d "Increases logging verbosity"
complete -c hx -s V -l version -d "Prints version information"
complete -c hx -l vsplit -d "Splits all given files vertically into different windows"
complete -c hx -l hsplit -d "Splits all given files horizontally into different windows"
complete -c hx -s c -l config -d "Specifies a file to use for completion"

@ -14,6 +14,10 @@ _hx() {
"--health[Checks for errors in editor setup]:language:->health" \
"-g[Fetches or builds tree-sitter grammars]:action:->grammar" \
"--grammar[Fetches or builds tree-sitter grammars]:action:->grammar" \
"--vsplit[Splits all given files vertically into different windows]" \
"--hsplit[Splits all given files horizontally into different windows]" \
"-c[Specifies a file to use for configuration]" \
"--config[Specifies a file to use for configuration]" \
"*:file:_files"
case "$state" in

@ -1 +0,0 @@
../runtime/themes

@ -0,0 +1,8 @@
# Flake's default package for non-flake-enabled nix instances
let
compat = builtins.fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz";
sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7";
};
in
(import compat {src = ./.;}).defaultNix.default

@ -30,8 +30,16 @@ inside the project. We use [xtask][xtask] as an ad-hoc task runner and
thus do not require any dependencies other than `cargo` (You don't have
to `cargo install` anything either).
# Integration tests
Integration tests for helix-term can be run with `cargo integration-test`. Code
contributors are strongly encouraged to write integration tests for their code.
Existing tests can be used as examples. Helpers can be found in
[helpers.rs][helpers.rs]
[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy
[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file
[architecture.md]: ./architecture.md
[docs]: https://docs.helix-editor.com/
[xtask]: https://github.com/matklad/cargo-xtask
[helpers.rs]: ../helix-term/tests/test/helpers.rs

@ -32,7 +32,7 @@ represented by a `Selection`. Each `Range` in the selection consists of a moving
a selection with a single range, with the head and the anchor in the same
position.
Ropes are modified by constructing an OT-like `Transaction`. It's represents
Ropes are modified by constructing an OT-like `Transaction`. It represents
a single coherent change to the document and can be applied to the rope.
A transaction can be inverted to produce an undo. Selections and marks can be
mapped over a transaction to translate to a position in the new text state after

@ -0,0 +1,59 @@
## Checklist
Helix releases are versioned in the Calendar Versioning scheme:
`YY.0M(.MICRO)`, for example `22.05` for May of 2022. In these instructions
we'll use `<tag>` as a placeholder for the tag being published.
* Merge the changelog PR
* Tag and push
* `git tag -s -m "<tag>" -a <tag> && git push`
* Make sure to switch to master and pull first
* Edit the `VERSION` file and change the date to the next planned release
* Releases are planned to happen every two months, so `22.05` would change to `22.07`
* Wait for the Release CI to finish
* It will automatically turn the git tag into a GitHub release when it uploads artifacts
* Edit the new release
* Use `<tag>` as the title
* Link to the changelog and release notes
* Merge the release notes PR
* Download the macos and linux binaries and update the `sha256`s in the [homebrew formula]
* Use `sha256sum` on the downloaded `.tar.xz` files to determine the hash
* Link to the release notes in this-week-in-rust
* [Example PR](https://github.com/rust-lang/this-week-in-rust/pull/3300)
* Post to reddit
* [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/)
[homebrew formula]: https://github.com/helix-editor/homebrew-helix/blob/master/Formula/helix.rb
## Changelog Curation
The changelog is currently created manually by reading through commits in the
log since the last release. GitHub's compare view is a nice way to approach
this. For example when creating the 22.07 release notes, this compare link
may be used
```
https://github.com/helix-editor/helix/compare/22.05...master
```
Either side of the triple-dot may be replaced with an exact revision, so if
you wish to incrementally compile the changelog, you can tackle a weeks worth
or so, record the revision where you stopped, and use that as a starting point
next week:
```
https://github.com/helix-editor/helix/compare/7706a4a0d8b67b943c31d0c5f7b00d357b5d838d...master
```
A work-in-progress commit for a changelog might look like
[this example](https://github.com/helix-editor/helix/commit/831adfd4c709ca16b248799bfef19698d5175e55).
Not every PR or commit needs a blurb in the changelog. Each release section
tends to have a blurb that links to a GitHub comparison between release
versions for convenience:
> As usual, the following is a summary of each of the changes since the last
> release. For the full log, check out the git log.
Typically, small changes like dependencies or documentation updates, refactors,
or meta changes like GitHub Actions work are left out.

@ -19,11 +19,11 @@
"devshell": {
"flake": false,
"locked": {
"lastModified": 1653917170,
"narHash": "sha256-FyxOnEE/V4PNEcMU62ikY4FfYPo349MOhMM97HS0XEo=",
"lastModified": 1655976588,
"narHash": "sha256-VreHyH6ITkf/1EX/8h15UqhddJnUleb0HgbC3gMkAEQ=",
"owner": "numtide",
"repo": "devshell",
"rev": "fc7a3e3adde9bbcab68af6d1e3c6eb738e296a92",
"rev": "899ca4629020592a13a46783587f6e674179d1db",
"type": "github"
},
"original": {
@ -73,11 +73,11 @@
]
},
"locked": {
"lastModified": 1654451959,
"narHash": "sha256-yWztC96o8Dw65jDbmNUxV1i61T3uLqvqhC3ziwnB/Fk=",
"lastModified": 1655975833,
"narHash": "sha256-g8sdfuglIZ24oWVbntVzniNTJW+Z3n9DNL9w9Tt+UCE=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "90b353682ef927bd39b59085e0dc6b7454888de7",
"rev": "4e75e665ec3a1cddae5266bed0dd72fce0b74a23",
"type": "github"
},
"original": {
@ -108,16 +108,16 @@
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": [
"rust-overlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1654531591,
"narHash": "sha256-DtDAwkl2Pn8w1BW1z2OssT/bWjVhMZQBBpr2uDY7tHY=",
"lastModified": 1656453541,
"narHash": "sha256-ZCPVnS6zJOZJvIlwU3rKR8MBVm6A3F4/0mA7G1lQ3D0=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "c935099d6851d0ff94098e9a12f42147524f0c5b",
"rev": "9eb74345b30cd2e536d9dac9d4435d3c475605c7",
"type": "github"
},
"original": {
@ -128,11 +128,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1654230545,
"narHash": "sha256-8Vlwf0x8ow6pPOK2a04bT+pxIeRnM1+O0Xv9/CuDzRs=",
"lastModified": 1655624069,
"narHash": "sha256-7g1zwTdp35GMTERnSzZMWJ7PG3QdDE8VOX3WsnOkAtM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "236cc2971ac72acd90f0ae3a797f9f83098b17ec",
"rev": "0d68d7c857fe301d49cdcd56130e0beea4ecd5aa",
"type": "github"
},
"original": {
@ -157,11 +157,11 @@
]
},
"locked": {
"lastModified": 1654483484,
"narHash": "sha256-Ki/sMgrUEj+31P3YMzZZp5Nea7+MQVVTdaRWQVS1PL4=",
"lastModified": 1655779671,
"narHash": "sha256-6feeiGa6fb7ZPVHR71uswkmN1701TAJpwYQA8QffmRk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "6bc59b9c4ad1cc1089219e935aa727a96d948c5d",
"rev": "8159585609a772b041cce6019d5c21d240709244",
"type": "github"
},
"original": {

@ -10,7 +10,7 @@
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
inputs.rust-overlay.follows = "rust-overlay";
};
};
@ -18,82 +18,121 @@
nixpkgs,
nixCargoIntegration,
...
}:
nixCargoIntegration.lib.makeOutputs {
root = ./.;
renameOutputs = {"helix-term" = "helix";};
# Set default app to hx (binary is from helix-term release build)
# Set default package to helix-term release build
defaultOutputs = {
app = "hx";
package = "helix";
};
overrides = {
cCompiler = common:
with common.pkgs;
if stdenv.isLinux
then gcc
else clang;
crateOverrides = common: _: {
helix-term = prev: let
inherit (common) pkgs;
mkRootPath = rel:
builtins.path {
path = "${common.root}/${rel}";
name = rel;
};
grammars = pkgs.callPackage ./grammars.nix {};
runtimeDir = pkgs.runCommandNoCC "helix-runtime" {} ''
mkdir -p $out
ln -s ${mkRootPath "runtime"}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
in {
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
# link languages and theme toml files since helix-term expects them (for tests)
preConfigure =
pkgs.lib.concatMapStringsSep
"\n"
(path: "ln -sf ${mkRootPath path} ..")
["languages.toml" "theme.toml" "base16_theme.toml"];
buildInputs = (prev.buildInputs or []) ++ [common.cCompiler.cc.lib];
nativeBuildInputs = [pkgs.makeWrapper];
}: let
outputs = config:
nixCargoIntegration.lib.makeOutputs {
root = ./.;
renameOutputs = {"helix-term" = "helix";};
# Set default app to hx (binary is from helix-term release build)
# Set default package to helix-term release build
defaultOutputs = {
app = "hx";
package = "helix";
};
overrides = {
cCompiler = common:
with common.pkgs;
if stdenv.isLinux
then gcc
else clang;
crateOverrides = common: _: {
helix-term = prev: let
inherit (common) pkgs;
mkRootPath = rel:
builtins.path {
path = "${common.root}/${rel}";
name = rel;
};
grammars = pkgs.callPackage ./grammars.nix config;
runtimeDir = pkgs.runCommandNoCC "helix-runtime" {} ''
mkdir -p $out
ln -s ${mkRootPath "runtime"}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
overridedAttrs = {
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
# link languages and theme toml files since helix-term expects them (for tests)
preConfigure =
pkgs.lib.concatMapStringsSep
"\n"
(path: "ln -sf ${mkRootPath path} ..")
["languages.toml" "theme.toml" "base16_theme.toml"];
buildInputs = (prev.buildInputs or []) ++ [common.cCompiler.cc.lib];
nativeBuildInputs = [pkgs.makeWrapper];
postFixup = ''
if [ -f "$out/bin/hx" ]; then
wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
fi
'';
postFixup = ''
if [ -f "$out/bin/hx" ]; then
wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
fi
'';
};
in
overridedAttrs
// (
pkgs.lib.optionalAttrs
(config ? makeWrapperArgs)
{inherit (config) makeWrapperArgs;}
);
};
shell = common: prev: {
packages =
prev.packages
++ (
with common.pkgs;
[lld_13 lldb cargo-flamegraph rust-analyzer] ++
(lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin)
);
env =
prev.env
++ [
{
name = "HELIX_RUNTIME";
eval = "$PWD/runtime";
}
{
name = "RUST_BACKTRACE";
value = "1";
}
{
name = "RUSTFLAGS";
value =
if common.pkgs.stdenv.isLinux
then "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"
else "";
}
];
};
};
shell = common: prev: {
packages =
prev.packages
++ (
with common.pkgs; [lld_13 lldb cargo-tarpaulin cargo-flamegraph rust-analyzer]
);
env =
prev.env
++ [
{
name = "HELIX_RUNTIME";
eval = "$PWD/runtime";
}
{
name = "RUST_BACKTRACE";
value = "1";
}
{
name = "RUSTFLAGS";
value =
if common.pkgs.stdenv.isLinux
then "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"
else "";
}
];
};
};
defaultOutputs = outputs {};
makeOverridableHelix = system: old:
old
// {
override = args:
makeOverridableHelix
system
(outputs args).packages.${system}.helix;
};
in
defaultOutputs
// {
packages =
nixpkgs.lib.mapAttrs
(
system: packages:
packages
// rec {
default = helix;
helix = makeOverridableHelix system packages.helix;
}
)
defaultOutputs.packages;
};
nixConfig = {
extra-substituters = ["https://helix.cachix.org"];
extra-trusted-public-keys = ["helix.cachix.org-1:ejp9KQpR1FBI2onstMQ34yogDm4OgU2ru6lIwPvuCVs="];
};
}

@ -4,6 +4,8 @@
runCommandLocal,
runCommandNoCC,
yj,
includeGrammarIf ? _: true,
...
}: let
# HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
# before parsing
@ -102,12 +104,13 @@
runHook postFixup
'';
};
grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
builtGrammars =
builtins.map (grammar: {
inherit (grammar) name;
artifact = buildGrammar grammar;
})
gitGrammars;
grammarsToBuild;
grammarLinks =
builtins.map (grammar: "ln -s ${grammar.artifact}/${grammar.name}.so $out/${grammar.name}.so")
builtGrammars;

@ -12,12 +12,13 @@ include = ["src/**/*", "README.md"]
[features]
unicode-lines = ["ropey/unicode_lines"]
integration = []
[dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" }
ropey = { version = "1.5", default-features = false, features = ["simd"] }
smallvec = "1.8"
smallvec = "1.9"
smartstring = "1.0.1"
unicode-segmentation = "1.9"
unicode-width = "0.1"
@ -25,7 +26,7 @@ unicode-general-category = "0.5"
# slab = "0.4.2"
slotmap = "1.0"
tree-sitter = "0.20"
once_cell = "1.12"
once_cell = "1.13"
arc-swap = "1"
regex = "1"
@ -34,7 +35,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5"
similar = "2.1"
similar = "2.2"
encoding_rs = "0.8"

@ -1,12 +1,9 @@
//! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character.
use crate::{
graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
};
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use std::collections::HashMap;
use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@ -125,7 +122,7 @@ impl Default for AutoPairs {
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
debug!("autopairs hook selection: {:#?}", selection);
log::trace!("autopairs hook selection: {:#?}", selection);
if let Some(pair) = pairs.get(ch) {
if pair.same() {
@ -149,14 +146,6 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
doc.get_char(pos - 1)
}
fn is_single_grapheme(doc: &Rope, range: &Range) -> bool {
let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to()));
let first = graphemes.next();
let second = graphemes.next();
debug!("first: {:#?}, second: {:#?}", first, second);
first.is_some() && second.is_none()
}
/// calculate what the resulting range should be for an auto pair insertion
fn get_next_range(
doc: &Rope,
@ -189,8 +178,8 @@ fn get_next_range(
);
}
let single_grapheme = is_single_grapheme(doc, start_range);
let doc_slice = doc.slice(..);
let single_grapheme = start_range.is_single_grapheme(doc_slice);
// just skip over graphemes
if len_inserted == 0 {
@ -235,9 +224,11 @@ fn get_next_range(
// other end of the grapheme to get to where the new characters
// are inserted, then move the head to where it should be
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
debug!(
log::trace!(
"prev_bound: {}, offset: {}, len_inserted: {}",
prev_bound, offset, len_inserted
prev_bound,
offset,
len_inserted
);
prev_bound + offset + len_inserted
};
@ -312,7 +303,7 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
debug!("auto pair transaction: {:#?}", t);
log::debug!("auto pair transaction: {:#?}", t);
t
}
@ -344,7 +335,7 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
debug!("auto pair transaction: {:#?}", t);
log::debug!("auto pair transaction: {:#?}", t);
t
}
@ -384,7 +375,7 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
debug!("auto pair transaction: {:#?}", t);
log::debug!("auto pair transaction: {:#?}", t);
t
}

@ -23,6 +23,12 @@ pub struct Range {
pub end: usize,
}
#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)]
pub enum NumberOrString {
Number(i32),
String(String),
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug, Clone)]
pub struct Diagnostic {
@ -30,4 +36,5 @@ pub struct Diagnostic {
pub line: usize,
pub message: String,
pub severity: Option<Severity>,
pub code: Option<NumberOrString>,
}

@ -5,6 +5,7 @@ use ropey::RopeSlice;
use std::borrow::Cow;
use std::cmp;
use std::fmt::Write;
use super::Increment;
use crate::{Range, Tendril};
@ -162,7 +163,7 @@ impl Format {
fields.push(field);
max_len += field.max_len + remaining[..i].len();
regex += &remaining[..i];
regex += &format!("({})", field.regex);
write!(regex, "({})", field.regex).unwrap();
remaining = &after[spec_len..];
}

@ -453,7 +453,7 @@ fn query_indents(
///
/// ```ignore
/// some_function(
/// parm1,
/// param1,
/// || {
/// // Here we get 2 indent levels because the 'parameters' and the 'block' node begin on different lines
/// },

@ -63,7 +63,9 @@ pub type Tendril = SmartString<smartstring::LazyCompact>;
pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes;
pub use position::{coords_at_pos, pos_at_coords, visual_coords_at_pos, Position};
pub use position::{
coords_at_pos, pos_at_coords, pos_at_visual_coords, visual_coords_at_pos, Position,
};
pub use selection::{Range, Selection};
pub use smallvec::{smallvec, SmallVec};
pub use syntax::Syntax;

@ -305,8 +305,17 @@ mod line_ending_tests {
fn line_end_char_index_rope_slice() {
let r = Rope::from_str("Hello\rworld\nhow\r\nare you?");
let s = &r.slice(..);
assert_eq!(line_end_char_index(s, 0), 11);
assert_eq!(line_end_char_index(s, 1), 15);
assert_eq!(line_end_char_index(s, 2), 25);
#[cfg(not(feature = "unicode-lines"))]
{
assert_eq!(line_end_char_index(s, 0), 11);
assert_eq!(line_end_char_index(s, 1), 15);
assert_eq!(line_end_char_index(s, 2), 25);
}
#[cfg(feature = "unicode-lines")]
{
assert_eq!(line_end_char_index(s, 0), 5);
assert_eq!(line_end_char_index(s, 1), 11);
assert_eq!(line_end_char_index(s, 2), 15);
}
}
}

@ -5,16 +5,15 @@ use tree_sitter::{Node, QueryCursor};
use crate::{
chars::{categorize_char, char_is_line_ending, CharCategory},
coords_at_pos,
graphemes::{
next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
prev_grapheme_boundary,
},
line_ending::rope_is_line_ending,
pos_at_coords,
pos_at_visual_coords,
syntax::LanguageConfiguration,
textobject::TextObject,
Position, Range, RopeSlice,
visual_coords_at_pos, Position, Range, RopeSlice,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -35,6 +34,7 @@ pub fn move_horizontally(
dir: Direction,
count: usize,
behaviour: Movement,
_: usize,
) -> Range {
let pos = range.cursor(slice);
@ -54,15 +54,12 @@ pub fn move_vertically(
dir: Direction,
count: usize,
behaviour: Movement,
tab_width: usize,
) -> Range {
let pos = range.cursor(slice);
// Compute the current position's 2d coordinates.
// TODO: switch this to use `visual_coords_at_pos` rather than
// `coords_at_pos` as this will cause a jerky movement when the visual
// position does not match, like moving from a line with tabs/CJK to
// a line without
let Position { row, col } = coords_at_pos(slice, pos);
let Position { row, col } = visual_coords_at_pos(slice, pos, tab_width);
let horiz = range.horiz.unwrap_or(col as u32);
// Compute the new position.
@ -71,7 +68,7 @@ pub fn move_vertically(
Direction::Backward => row.saturating_sub(count),
};
let new_col = col.max(horiz as usize);
let new_pos = pos_at_coords(slice, Position::new(new_row, new_col), true);
let new_pos = pos_at_visual_coords(slice, Position::new(new_row, new_col), tab_width);
// Special-case to avoid moving to the end of the last non-empty line.
if behaviour == Movement::Extend && slice.line(new_row).len_chars() == 0 {
@ -399,9 +396,9 @@ pub fn goto_treesitter_object(
dir: Direction,
slice_tree: Node,
lang_config: &LanguageConfiguration,
_count: usize,
count: usize,
) -> Range {
let get_range = move || -> Option<Range> {
let get_range = move |range: Range| -> Option<Range> {
let byte_pos = slice.char_to_byte(range.cursor(slice));
let cap_name = |t: TextObject| format!("{}.{}", object_name, t);
@ -439,13 +436,15 @@ pub fn goto_treesitter_object(
// head of range should be at beginning
Some(Range::new(end_char, start_char))
};
get_range().unwrap_or(range)
(0..count).fold(range, |range, _| get_range(range).unwrap_or(range))
}
#[cfg(test)]
mod test {
use ropey::Rope;
use crate::{coords_at_pos, pos_at_coords};
use super::*;
const SINGLE_LINE_SAMPLE: &str = "This is a simple alphabetic line";
@ -472,7 +471,7 @@ mod test {
assert_eq!(
coords_at_pos(
slice,
move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
move_vertically(slice, range, Direction::Forward, 1, Movement::Move, 4).head
),
(1, 3).into()
);
@ -496,7 +495,7 @@ mod test {
];
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
range = move_horizontally(slice, range, direction, amount, Movement::Move, 0);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
}
}
@ -522,7 +521,7 @@ mod test {
];
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
range = move_horizontally(slice, range, direction, amount, Movement::Move, 0);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);
}
@ -544,7 +543,7 @@ mod test {
];
for (direction, amount) in moves {
range = move_horizontally(slice, range, direction, amount, Movement::Extend);
range = move_horizontally(slice, range, direction, amount, Movement::Extend, 0);
assert_eq!(range.anchor, original_anchor);
}
}
@ -568,7 +567,7 @@ mod test {
];
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_vertically(slice, range, direction, amount, Movement::Move);
range = move_vertically(slice, range, direction, amount, Movement::Move, 4);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);
}
@ -602,8 +601,8 @@ mod test {
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move, 0),
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move, 4),
};
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);
@ -627,18 +626,18 @@ mod test {
let moves_and_expected_coordinates = [
// Places cursor at the fourth kana.
((Axis::H, Direction::Forward, 4), (0, 4)),
// Descent places cursor at the 4th character.
((Axis::V, Direction::Forward, 1usize), (1, 4)),
// Moving back 1 character.
((Axis::H, Direction::Backward, 1usize), (1, 3)),
// Descent places cursor at the 8th character.
((Axis::V, Direction::Forward, 1usize), (1, 8)),
// Moving back 2 characters.
((Axis::H, Direction::Backward, 2usize), (1, 6)),
// Jumping back up 1 line.
((Axis::V, Direction::Backward, 1usize), (0, 3)),
];
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move, 0),
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move, 4),
};
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);

@ -2,12 +2,11 @@ use crate::{Range, RopeSlice, Selection, Syntax};
use tree_sitter::Node;
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(syntax, text, selection, |descendant, from, to| {
if descendant.start_byte() == from && descendant.end_byte() == to {
descendant.parent()
} else {
Some(descendant)
select_node_impl(syntax, text, selection, |mut node, from, to| {
while node.start_byte() == from && node.end_byte() == to {
node = node.parent()?;
}
Some(node)
})
}

@ -90,3 +90,54 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
};
fold_home_dir(path)
}
/// Returns a truncated filepath where the basepart of the path is reduced to the first
/// char of the folder and the whole filename appended.
///
/// Also strip the current working directory from the beginning of the path.
/// Note that this function does not check if the truncated path is unambiguous.
///
/// ```
/// use helix_core::path::get_truncated_path;
/// use std::path::Path;
///
/// assert_eq!(
/// get_truncated_path("/home/cnorris/documents/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("jokes.txt").as_path(),
/// Path::new("jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/jokes.txt").as_path(),
/// Path::new("/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/h/c/d/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
/// ```
///
pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
let cwd = std::env::current_dir().unwrap_or_default();
let path = path
.as_ref()
.strip_prefix(cwd)
.unwrap_or_else(|_| path.as_ref());
let file = path.file_name().unwrap_or_default();
let base = path.parent().unwrap_or_else(|| Path::new(""));
let mut ret = PathBuf::new();
for d in base {
ret.push(
d.to_string_lossy()
.chars()
.next()
.unwrap_or_default()
.to_string(),
);
}
ret.push(file);
ret
}

@ -109,9 +109,6 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
/// with left-side block-cursor positions, as this prevents the the block cursor
/// from jumping to the next line. Otherwise you typically want it to be `false`,
/// such as when dealing with raw anchor/head positions.
///
/// TODO: this should be changed to work in terms of visual row/column, not
/// graphemes.
pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending: bool) -> usize {
let Position { mut row, col } = coords;
if limit_before_line_ending {
@ -135,6 +132,43 @@ pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending
line_start + col_char_offset
}
/// Convert visual (line, column) coordinates to a character index.
///
/// If the `line` coordinate is beyond the end of the file, the EOF
/// position will be returned.
///
/// If the `column` coordinate is past the end of the given line, the
/// line-end position (in this case, just before the line ending
/// character) will be returned.
pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize) -> usize {
let Position { mut row, col } = coords;
row = row.min(text.len_lines() - 1);
let line_start = text.line_to_char(row);
let line_end = line_end_char_index(&text, row);
let mut col_char_offset = 0;
let mut cols_remaining = col;
for grapheme in RopeGraphemes::new(text.slice(line_start..line_end)) {
let grapheme_width = if grapheme == "\t" {
tab_width - ((col - cols_remaining) % tab_width)
} else {
let grapheme = Cow::from(grapheme);
grapheme_width(&grapheme)
};
// If pos is in the middle of a wider grapheme (tab for example)
// return the starting offset.
if grapheme_width > cols_remaining {
break;
}
cols_remaining -= grapheme_width;
col_char_offset += grapheme.chars().count();
}
line_start + col_char_offset
}
#[cfg(test)]
mod test {
use super::*;
@ -305,4 +339,70 @@ mod test {
assert_eq!(pos_at_coords(slice, (0, 10).into(), true), 0);
assert_eq!(pos_at_coords(slice, (10, 10).into(), true), 0);
}
#[test]
fn test_pos_at_visual_coords() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
let slice = text.slice(..);
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 5).into(), 4), 5); // position on \n
assert_eq!(pos_at_visual_coords(slice, (0, 6).into(), 4), 5); // position after \n
assert_eq!(pos_at_visual_coords(slice, (1, 0).into(), 4), 6); // position on w
assert_eq!(pos_at_visual_coords(slice, (1, 1).into(), 4), 7); // position on o
assert_eq!(pos_at_visual_coords(slice, (1, 4).into(), 4), 10); // position on d
// Test with wide characters.
let text = Rope::from("今日はいい\n");
let slice = text.slice(..);
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 1);
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 1);
assert_eq!(pos_at_visual_coords(slice, (0, 4).into(), 4), 2);
assert_eq!(pos_at_visual_coords(slice, (0, 5).into(), 4), 2);
assert_eq!(pos_at_visual_coords(slice, (0, 6).into(), 4), 3);
assert_eq!(pos_at_visual_coords(slice, (0, 7).into(), 4), 3);
assert_eq!(pos_at_visual_coords(slice, (0, 8).into(), 4), 4);
assert_eq!(pos_at_visual_coords(slice, (0, 9).into(), 4), 4);
// assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4, false), 5);
// assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4, true), 5);
assert_eq!(pos_at_visual_coords(slice, (1, 0).into(), 4), 6);
// Test with grapheme clusters.
let text = Rope::from("a̐éö̲\r\n");
let slice = text.slice(..);
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 2);
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 4);
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 7); // \r\n is one char here
assert_eq!(pos_at_visual_coords(slice, (0, 4).into(), 4), 7);
assert_eq!(pos_at_visual_coords(slice, (1, 0).into(), 4), 9);
// Test with wide-character grapheme clusters.
let text = Rope::from("किमपि");
// 2 - 1 - 2 codepoints
// TODO: delete handling as per https://news.ycombinator.com/item?id=20058454
let slice = text.slice(..);
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 2);
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 3);
// Test with tabs.
let text = Rope::from("\tHello\n");
let slice = text.slice(..);
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 4).into(), 4), 1);
assert_eq!(pos_at_visual_coords(slice, (0, 5).into(), 4), 2);
// Test out of bounds.
let text = Rope::new();
let slice = text.slice(..);
assert_eq!(pos_at_visual_coords(slice, (10, 0).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4), 0);
assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0);
}
}

@ -73,6 +73,10 @@ impl Registers {
self.read(name).and_then(|entries| entries.first())
}
pub fn last(&self, name: char) -> Option<&String> {
self.read(name).and_then(|entries| entries.last())
}
pub fn inner(&self) -> &HashMap<char, Register> {
&self.inner
}

@ -8,7 +8,7 @@ use crate::{
prev_grapheme_boundary,
},
movement::Direction,
Assoc, ChangeSet, RopeSlice,
Assoc, ChangeSet, RopeGraphemes, RopeSlice,
};
use smallvec::{smallvec, SmallVec};
use std::borrow::Cow;
@ -222,9 +222,23 @@ impl Range {
// groupAt
/// Returns the text inside this range given the text of the whole buffer.
///
/// The returned `Cow` is a reference if the range of text is inside a single
/// chunk of the rope. Otherwise a copy of the text is returned. Consider
/// using `slice` instead if you do not need a `Cow` or `String` to avoid copying.
#[inline]
pub fn fragment<'a, 'b: 'a>(&'a self, text: RopeSlice<'b>) -> Cow<'b, str> {
text.slice(self.from()..self.to()).into()
self.slice(text).into()
}
/// Returns the text inside this range given the text of the whole buffer.
///
/// The returned value is a reference to the passed slice. This method never
/// copies any contents.
#[inline]
pub fn slice<'a, 'b: 'a>(&'a self, text: RopeSlice<'b>) -> RopeSlice<'b> {
text.slice(self.from()..self.to())
}
//--------------------------------
@ -339,6 +353,14 @@ impl Range {
pub fn cursor_line(&self, text: RopeSlice) -> usize {
text.char_to_line(self.cursor(text))
}
/// Returns true if this Range covers a single grapheme in the given text
pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool {
let mut graphemes = RopeGraphemes::new(doc.slice(self.from()..self.to()));
let first = graphemes.next();
let second = graphemes.next();
first.is_some() && second.is_none()
}
}
impl From<(usize, usize)> for Range {
@ -540,6 +562,10 @@ impl Selection {
self.ranges.iter().map(move |range| range.fragment(text))
}
pub fn slices<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = RopeSlice> + 'a {
self.ranges.iter().map(move |range| range.slice(text))
}
#[inline(always)]
pub fn iter(&self) -> std::slice::Iter<'_, Range> {
self.ranges.iter()

@ -24,9 +24,13 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
state = match state {
Normal => match c {
'\\' => {
escaped.push_str(&input[start..i]);
start = i + 1;
NormalEscaped
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
NormalEscaped
} else {
Normal
}
}
'"' => {
end = i;
@ -45,9 +49,13 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
NormalEscaped => Normal,
Quoted => match c {
'\\' => {
escaped.push_str(&input[start..i]);
start = i + 1;
QuoteEscaped
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
QuoteEscaped
} else {
Quoted
}
}
'\'' => {
end = i;
@ -58,9 +66,13 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' => {
escaped.push_str(&input[start..i]);
start = i + 1;
DquoteEscaped
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
DquoteEscaped
} else {
Dquoted
}
}
'"' => {
end = i;
@ -99,6 +111,25 @@ mod test {
use super::*;
#[test]
#[cfg(windows)]
fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
let result = shellwords(input);
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó"),
Cow::from("wörds"),
Cow::from("\\three\\"),
Cow::from("\\"),
Cow::from("with\\ escaping\\\\"),
];
// TODO test is_owned and is_borrowed, once they get stabilized.
assert_eq!(expected, result);
}
#[test]
#[cfg(unix)]
fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
let result = shellwords(input);
@ -114,6 +145,7 @@ mod test {
}
#[test]
#[cfg(unix)]
fn test_quoted() {
let quoted =
r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
@ -129,6 +161,7 @@ mod test {
}
#[test]
#[cfg(unix)]
fn test_dquoted() {
let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
let result = shellwords(dquoted);
@ -143,6 +176,7 @@ mod test {
}
#[test]
#[cfg(unix)]
fn test_mixed() {
let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
let result = shellwords(dquoted);

@ -79,6 +79,9 @@ pub struct LanguageConfiguration {
#[serde(default)]
pub auto_format: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub formatter: Option<FormatterConfiguration>,
#[serde(default)]
pub diagnostic_severity: Severity,
@ -126,6 +129,15 @@ pub struct LanguageServerConfiguration {
pub language_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct FormatterConfiguration {
pub command: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AdvancedCompletion {
@ -328,29 +340,15 @@ fn read_query(language: &str, filename: &str) -> String {
let query = load_runtime_file(language, filename).unwrap_or_default();
// TODO: the collect() is not ideal
let inherits = INHERITS_REGEX
.captures_iter(&query)
.flat_map(|captures| {
// replaces all "; inherits <language>(,<language>)*" with the queries of the given language(s)
INHERITS_REGEX
.replace_all(&query, |captures: &regex::Captures| {
captures[1]
.split(',')
.map(str::to_owned)
.collect::<Vec<_>>()
.map(|language| format!("\n{}\n", read_query(language, filename)))
.collect::<String>()
})
.collect::<Vec<_>>();
if inherits.is_empty() {
return query;
}
let mut queries = inherits
.iter()
.map(|language| read_query(language, filename))
.collect::<Vec<_>>();
queries.push(query);
queries.concat()
.to_string()
}
impl LanguageConfiguration {
@ -766,7 +764,7 @@ impl Syntax {
);
let mut injections = Vec::new();
for mat in matches {
let (language_name, content_node, include_children) = injection_for_match(
let (language_name, content_node, included_children) = injection_for_match(
&layer.config,
&layer.config.injections_query,
&mat,
@ -783,7 +781,7 @@ impl Syntax {
{
if let Some(config) = (injection_callback)(&language_name) {
let ranges =
intersect_ranges(&layer.ranges, &[content_node], include_children);
intersect_ranges(&layer.ranges, &[content_node], included_children);
if !ranges.is_empty() {
injections.push((config, ranges));
@ -795,7 +793,10 @@ impl Syntax {
// Process combined injections.
if let Some(combined_injections_query) = &layer.config.combined_injections_query {
let mut injections_by_pattern_index =
vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
vec![
(None, Vec::new(), IncludedChildren::default());
combined_injections_query.pattern_count()
];
let matches = cursor.matches(
combined_injections_query,
layer.tree().root_node(),
@ -803,7 +804,7 @@ impl Syntax {
);
for mat in matches {
let entry = &mut injections_by_pattern_index[mat.pattern_index];
let (language_name, content_node, include_children) = injection_for_match(
let (language_name, content_node, included_children) = injection_for_match(
&layer.config,
combined_injections_query,
&mat,
@ -815,16 +816,16 @@ impl Syntax {
if let Some(content_node) = content_node {
entry.1.push(content_node);
}
entry.2 = include_children;
entry.2 = included_children;
}
for (lang_name, content_nodes, includes_children) in injections_by_pattern_index
for (lang_name, content_nodes, included_children) in injections_by_pattern_index
{
if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
if let Some(config) = (injection_callback)(&lang_name) {
let ranges = intersect_ranges(
&layer.ranges,
&content_nodes,
includes_children,
included_children,
);
if !ranges.is_empty() {
injections.push((config, ranges));
@ -1355,8 +1356,8 @@ impl HighlightConfiguration {
/// Tree-sitter syntax-highlighting queries specify highlights in the form of dot-separated
/// highlight names like `punctuation.bracket` and `function.method.builtin`. Consumers of
/// these queries can choose to recognize highlights with different levels of specificity.
/// For example, the string `function.builtin` will match against `function.method.builtin`
/// and `function.builtin.constructor`, but will not match `function.method`.
/// For example, the string `function.builtin` will match against `function.builtin.constructor`
/// but will not match `function.method.builtin` and `function.method`.
///
/// When highlighting, results are returned as `Highlight` values, which contain the index
/// of the matched highlight this list of highlight names.
@ -1376,11 +1377,13 @@ impl HighlightConfiguration {
let recognized_name = recognized_name;
let mut len = 0;
let mut matches = true;
for part in recognized_name.split('.') {
len += 1;
if !capture_parts.contains(&part) {
matches = false;
break;
for (i, part) in recognized_name.split('.').enumerate() {
match capture_parts.get(i) {
Some(capture_part) if *capture_part == part => len += 1,
_ => {
matches = false;
break;
}
}
}
if matches && len > best_match_len {
@ -1422,6 +1425,19 @@ impl<'a> HighlightIterLayer<'a> {
}
}
#[derive(Clone)]
enum IncludedChildren {
None,
All,
Unnamed,
}
impl Default for IncludedChildren {
fn default() -> Self {
Self::None
}
}
// Compute the ranges that should be included when parsing an injection.
// This takes into account three things:
// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
@ -1434,7 +1450,7 @@ impl<'a> HighlightIterLayer<'a> {
fn intersect_ranges(
parent_ranges: &[Range],
nodes: &[Node],
includes_children: bool,
included_children: IncludedChildren,
) -> Vec<Range> {
let mut cursor = nodes[0].walk();
let mut result = Vec::new();
@ -1458,11 +1474,15 @@ fn intersect_ranges(
for excluded_range in node
.children(&mut cursor)
.filter_map(|child| {
if includes_children {
None
} else {
Some(child.range())
.filter_map(|child| match included_children {
IncludedChildren::None => Some(child.range()),
IncludedChildren::All => None,
IncludedChildren::Unnamed => {
if child.is_named() {
Some(child.range())
} else {
None
}
}
})
.chain([following_range].iter().cloned())
@ -1791,7 +1811,7 @@ fn injection_for_match<'a>(
query: &'a Query,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) {
) -> (Option<Cow<'a, str>>, Option<Node<'a>>, IncludedChildren) {
let content_capture_index = config.injection_content_capture_index;
let language_capture_index = config.injection_language_capture_index;
@ -1807,7 +1827,7 @@ fn injection_for_match<'a>(
}
}
let mut include_children = false;
let mut included_children = IncludedChildren::default();
for prop in query.property_settings(query_match.pattern_index) {
match prop.key.as_ref() {
// In addition to specifying the language name via the text of a
@ -1823,12 +1843,17 @@ fn injection_for_match<'a>(
// `injection.content` node - only the ranges that belong to the
// node itself. This can be changed using a `#set!` predicate that
// sets the `injection.include-children` key.
"injection.include-children" => include_children = true,
"injection.include-children" => included_children = IncludedChildren::All,
// Some queries might only exclude named children but include unnamed
// children in their `injection.content` node. This can be enabled using
// a `#set!` predicate that sets the `injection.include-unnamed-children` key.
"injection.include-unnamed-children" => included_children = IncludedChildren::Unnamed,
_ => {}
}
}
(language_name, content_node, include_children)
(language_name, content_node, included_children)
}
pub struct Merge<I> {

@ -198,26 +198,26 @@ pub fn textobject_paragraph(
Range::new(anchor, head)
}
pub fn textobject_surround(
pub fn textobject_pair_surround(
slice: RopeSlice,
range: Range,
textobject: TextObject,
ch: char,
count: usize,
) -> Range {
textobject_surround_impl(slice, range, textobject, Some(ch), count)
textobject_pair_surround_impl(slice, range, textobject, Some(ch), count)
}
pub fn textobject_surround_closest(
pub fn textobject_pair_surround_closest(
slice: RopeSlice,
range: Range,
textobject: TextObject,
count: usize,
) -> Range {
textobject_surround_impl(slice, range, textobject, None, count)
textobject_pair_surround_impl(slice, range, textobject, None, count)
}
fn textobject_surround_impl(
fn textobject_pair_surround_impl(
slice: RopeSlice,
range: Range,
textobject: TextObject,
@ -562,7 +562,7 @@ mod test {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range, ch, count) = case;
let result = textobject_surround(slice, Range::point(pos), objtype, ch, count);
let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count);
assert_eq!(
result,
expected_range.into(),

@ -1 +0,0 @@
../../../src/indent.rs

@ -34,7 +34,7 @@ pub struct Client {
pub caps: Option<DebuggerCapabilities>,
// thread_id -> frames
pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
pub thread_states: HashMap<ThreadId, String>,
pub thread_states: ThreadStates,
pub thread_id: Option<ThreadId>,
/// Currently active frame for the current thread.
pub active_frame: Option<usize>,

@ -14,6 +14,8 @@ impl std::fmt::Display for ThreadId {
}
}
pub type ThreadStates = HashMap<ThreadId, String>;
pub trait Request {
type Arguments: serde::de::DeserializeOwned + serde::Serialize;
type Result: serde::de::DeserializeOwned + serde::Serialize;
@ -25,9 +27,11 @@ pub trait Request {
pub struct ColumnDescriptor {
pub attribute_name: String,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(rename = "type")]
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub ty: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<usize>,
}
@ -36,52 +40,94 @@ pub struct ColumnDescriptor {
pub struct ExceptionBreakpointsFilter {
pub filter: String,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_condition: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_description: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DebuggerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_configuration_done_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_function_breakpoints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_conditional_breakpoints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_hit_conditional_breakpoints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_evaluate_for_hovers: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_step_back: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_set_variable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_restart_frame: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_goto_targets_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_step_in_targets_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_completions_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_modules_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_restart_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_exception_options: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_value_formatting_options: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_exception_info_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub support_terminate_debuggee: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub support_suspend_debuggee: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_delayed_stack_trace_loading: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_loaded_sources_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_log_points: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_terminate_threads_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_set_expression: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_terminate_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_data_breakpoints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_read_memory_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_write_memory_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_disassemble_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_cancel_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_breakpoint_locations_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_clipboard_context: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_stepping_granularity: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_instruction_breakpoints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_exception_filter_options: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exception_breakpoint_filters: Option<Vec<ExceptionBreakpointsFilter>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_trigger_characters: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_module_columns: Option<Vec<ColumnDescriptor>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_checksum_algorithms: Option<Vec<String>>,
}
@ -95,13 +141,21 @@ pub struct Checksum {
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_reference: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presentation_hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sources: Option<Vec<Source>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub adapter_data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksums: Option<Vec<Checksum>>,
}
@ -109,36 +163,56 @@ pub struct Source {
#[serde(rename_all = "camelCase")]
pub struct SourceBreakpoint {
pub line: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hit_condition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_message: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Breakpoint {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<usize>,
pub verified: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Source>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instruction_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StackFrameFormat {
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameter_types: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameter_names: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameter_values: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub module: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_all: Option<bool>,
}
@ -147,14 +221,21 @@ pub struct StackFrameFormat {
pub struct StackFrame {
pub id: usize,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Source>,
pub line: usize,
pub column: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub can_restart: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instruction_pointer_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub module_id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presentation_hint: Option<String>,
}
@ -169,29 +250,41 @@ pub struct Thread {
#[serde(rename_all = "camelCase")]
pub struct Scope {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub presentation_hint: Option<String>,
pub variables_reference: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub named_variables: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_variables: Option<usize>,
pub expensive: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Source>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_column: Option<usize>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ValueFormat {
#[serde(skip_serializing_if = "Option::is_none")]
pub hex: Option<bool>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VariablePresentationHint {
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility: Option<String>,
}
@ -200,13 +293,18 @@ pub struct VariablePresentationHint {
pub struct Variable {
pub name: String,
pub value: String,
#[serde(rename = "type")]
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub ty: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presentation_hint: Option<VariablePresentationHint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evaluate_name: Option<String>,
pub variables_reference: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub named_variables: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_variables: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_reference: Option<String>,
}
@ -215,13 +313,21 @@ pub struct Variable {
pub struct Module {
pub id: String, // TODO: || number
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_optimized: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_user_code: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_file_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_time_stamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address_range: Option<String>,
}
@ -230,22 +336,31 @@ pub mod requests {
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeArguments {
#[serde(rename = "clientID")]
#[serde(rename = "clientID", skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
#[serde(rename = "adapterID")]
pub adapter_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(rename = "linesStartAt1")]
#[serde(rename = "linesStartAt1", skip_serializing_if = "Option::is_none")]
pub lines_start_at_one: Option<bool>,
#[serde(rename = "columnsStartAt1")]
#[serde(rename = "columnsStartAt1", skip_serializing_if = "Option::is_none")]
pub columns_start_at_one: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_variable_type: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_variable_paging: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_run_in_terminal_request: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_memory_references: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_progress_reporting: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_invalidated_event: Option<bool>,
}
@ -298,14 +413,17 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct SetBreakpointsArguments {
pub source: Source,
#[serde(skip_serializing_if = "Option::is_none")]
pub breakpoints: Option<Vec<SourceBreakpoint>>,
// lines is deprecated
#[serde(skip_serializing_if = "Option::is_none")]
pub source_modified: Option<bool>,
}
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetBreakpointsResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub breakpoints: Option<Vec<Breakpoint>>,
}
@ -327,6 +445,7 @@ pub mod requests {
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ContinueResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub all_threads_continued: Option<bool>,
}
@ -343,14 +462,18 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct StackTraceArguments {
pub thread_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_frame: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub levels: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<StackFrameFormat>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StackTraceResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub total_frames: Option<usize>,
pub stack_frames: Vec<StackFrame>,
}
@ -404,9 +527,13 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct VariablesArguments {
pub variables_reference: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ValueFormat>,
}
@ -429,7 +556,9 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct StepInArguments {
pub thread_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_id: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub granularity: Option<String>,
}
@ -446,6 +575,7 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct StepOutArguments {
pub thread_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub granularity: Option<String>,
}
@ -462,6 +592,7 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct NextArguments {
pub thread_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub granularity: Option<String>,
}
@ -493,8 +624,11 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct EvaluateArguments {
pub expression: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame_id: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ValueFormat>,
}
@ -502,12 +636,16 @@ pub mod requests {
#[serde(rename_all = "camelCase")]
pub struct EvaluateResponse {
pub result: String,
#[serde(rename = "type")]
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub ty: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presentation_hint: Option<VariablePresentationHint>,
pub variables_reference: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub named_variables: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_variables: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_reference: Option<String>,
}
@ -531,6 +669,7 @@ pub mod requests {
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetExceptionBreakpointsResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub breakpoints: Option<Vec<Breakpoint>>,
}
@ -548,17 +687,22 @@ pub mod requests {
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunInTerminalResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub process_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shell_process_id: Option<u32>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunInTerminalArguments {
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub cwd: Option<String>,
pub cwd: String,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, Option<String>>>,
}
@ -605,11 +749,17 @@ pub mod events {
#[serde(rename_all = "camelCase")]
pub struct Stopped {
pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_id: Option<ThreadId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preserve_focus_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub all_threads_stopped: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hit_breakpoint_ids: Option<Vec<usize>>,
}
@ -617,6 +767,7 @@ pub mod events {
#[serde(rename_all = "camelCase")]
pub struct Continued {
pub thread_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub all_threads_continued: Option<bool>,
}
@ -629,6 +780,7 @@ pub mod events {
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Terminated {
#[serde(skip_serializing_if = "Option::is_none")]
pub restart: Option<Value>,
}
@ -643,12 +795,19 @@ pub mod events {
#[serde(rename_all = "camelCase")]
pub struct Output {
pub output: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variables_reference: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Source>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
@ -677,9 +836,13 @@ pub mod events {
#[serde(rename_all = "camelCase")]
pub struct Process {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_process_id: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_local_process: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_method: Option<String>, // TODO: use enum
#[serde(skip_serializing_if = "Option::is_none")]
pub pointer_size: Option<usize>,
}

@ -19,8 +19,7 @@ serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
etcetera = "0.4"
tree-sitter = "0.20"
once_cell = "1.12"
once_cell = "1.13"
log = "0.4"
# TODO: these two should be on !wasm32 only

@ -19,7 +19,23 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
.into_iter()
.chain([default_lang_config()].into_iter())
.fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
crate::merge_toml_values(b, a, true)
// combines for example
// b:
// [[language]]
// name = "toml"
// language-server = { command = "taplo", args = ["lsp", "stdio"] }
//
// a:
// [[language]]
// language-server = { command = "/usr/bin/taplo" }
//
// into:
// [[language]]
// name = "toml"
// language-server = { command = "/usr/bin/taplo" }
//
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(b, a, 3)
});
Ok(config)

@ -89,11 +89,102 @@ pub fn fetch_grammars() -> Result<()> {
let mut grammars = get_grammar_configs()?;
grammars.retain(|grammar| !matches!(grammar.source, GrammarSource::Local { .. }));
run_parallel(grammars, fetch_grammar, "fetch")
println!("Fetching {} grammars", grammars.len());
let results = run_parallel(grammars, fetch_grammar);
let mut errors = Vec::new();
let mut git_updated = Vec::new();
let mut git_up_to_date = 0;
let mut non_git = Vec::new();
for res in results {
match res {
Ok(FetchStatus::GitUpToDate) => git_up_to_date += 1,
Ok(FetchStatus::GitUpdated {
grammar_id,
revision,
}) => git_updated.push((grammar_id, revision)),
Ok(FetchStatus::NonGit { grammar_id }) => non_git.push(grammar_id),
Err(e) => errors.push(e),
}
}
non_git.sort_unstable();
git_updated.sort_unstable_by(|a, b| a.0.cmp(&b.0));
if git_up_to_date != 0 {
println!("{} up to date git grammars", git_up_to_date);
}
if !non_git.is_empty() {
println!("{} non git grammars", non_git.len());
println!("\t{:?}", non_git);
}
if !git_updated.is_empty() {
println!("{} updated grammars", git_updated.len());
// We checked the vec is not empty, unwrapping will not panic
let longest_id = git_updated.iter().map(|x| x.0.len()).max().unwrap();
for (id, rev) in git_updated {
println!(
"\t{id:width$} now on {rev}",
id = id,
width = longest_id,
rev = rev
);
}
}
if !errors.is_empty() {
let len = errors.len();
println!("{} grammars failed to fetch", len);
for (i, error) in errors.into_iter().enumerate() {
println!("\tFailure {}/{}: {}", i, len, error);
}
}
Ok(())
}
pub fn build_grammars() -> Result<()> {
run_parallel(get_grammar_configs()?, build_grammar, "build")
pub fn build_grammars(target: Option<String>) -> Result<()> {
let grammars = get_grammar_configs()?;
println!("Building {} grammars", grammars.len());
let results = run_parallel(grammars, move |grammar| {
build_grammar(grammar, target.as_deref())
});
let mut errors = Vec::new();
let mut already_built = 0;
let mut built = Vec::new();
for res in results {
match res {
Ok(BuildStatus::AlreadyBuilt) => already_built += 1,
Ok(BuildStatus::Built { grammar_id }) => built.push(grammar_id),
Err(e) => errors.push(e),
}
}
built.sort_unstable();
if already_built != 0 {
println!("{} grammars already built", already_built);
}
if !built.is_empty() {
println!("{} grammars built now", built.len());
println!("\t{:?}", built);
}
if !errors.is_empty() {
let len = errors.len();
println!("{} grammars failed to build", len);
for (i, error) in errors.into_iter().enumerate() {
println!("\tFailure {}/{}: {}", i, len, error);
}
}
Ok(())
}
// Returns the set of grammar configurations the user requests.
@ -122,15 +213,17 @@ fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
Ok(grammars)
}
fn run_parallel<F>(grammars: Vec<GrammarConfiguration>, job: F, action: &'static str) -> Result<()>
fn run_parallel<F, Res>(grammars: Vec<GrammarConfiguration>, job: F) -> Vec<Result<Res>>
where
F: Fn(GrammarConfiguration) -> Result<()> + std::marker::Send + 'static + Copy,
F: Fn(GrammarConfiguration) -> Result<Res> + Send + 'static + Clone,
Res: Send + 'static,
{
let pool = threadpool::Builder::new().build();
let (tx, rx) = channel();
for grammar in grammars {
let tx = tx.clone();
let job = job.clone();
pool.execute(move || {
// Ignore any SendErrors, if any job in another thread has encountered an
@ -141,14 +234,21 @@ where
drop(tx);
// TODO: print all failures instead of the first one found.
rx.iter()
.find(|result| result.is_err())
.map(|err| err.with_context(|| format!("Failed to {} some grammar(s)", action)))
.unwrap_or(Ok(()))
rx.iter().collect()
}
fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
enum FetchStatus {
GitUpToDate,
GitUpdated {
grammar_id: String,
revision: String,
},
NonGit {
grammar_id: String,
},
}
fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
if let GrammarSource::Git {
remote, revision, ..
} = grammar.source
@ -184,16 +284,18 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
)?;
git(&grammar_dir, ["checkout", &revision])?;
println!(
"Grammar '{}' checked out at '{}'.",
grammar.grammar_id, revision
);
Ok(FetchStatus::GitUpdated {
grammar_id: grammar.grammar_id,
revision,
})
} else {
println!("Grammar '{}' is already up to date.", grammar.grammar_id);
Ok(FetchStatus::GitUpToDate)
}
} else {
Ok(FetchStatus::NonGit {
grammar_id: grammar.grammar_id,
})
}
Ok(())
}
// Sets the remote for a repository to the given URL, creating the remote if
@ -240,7 +342,12 @@ where
}
}
fn build_grammar(grammar: GrammarConfiguration) -> Result<()> {
enum BuildStatus {
AlreadyBuilt,
Built { grammar_id: String },
}
fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result<BuildStatus> {
let grammar_dir = if let GrammarSource::Local { path } = &grammar.source {
PathBuf::from(&path)
} else {
@ -273,10 +380,14 @@ fn build_grammar(grammar: GrammarConfiguration) -> Result<()> {
}
.join("src");
build_tree_sitter_library(&path, grammar)
build_tree_sitter_library(&path, grammar, target)
}
fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) -> Result<()> {
fn build_tree_sitter_library(
src_path: &Path,
grammar: GrammarConfiguration,
target: Option<&str>,
) -> Result<BuildStatus> {
let header_path = src_path;
let parser_path = src_path.join("parser.c");
let mut scanner_path = src_path.join("scanner.c");
@ -299,27 +410,25 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
.context("Failed to compare source and binary timestamps")?;
if !recompile {
println!("Grammar '{}' is already built.", grammar.grammar_id);
return Ok(());
return Ok(BuildStatus::AlreadyBuilt);
}
println!("Building grammar '{}'", grammar.grammar_id);
let mut config = cc::Build::new();
config
.cpp(true)
.opt_level(3)
.cargo_metadata(false)
.host(BUILD_TARGET)
.target(BUILD_TARGET);
.target(target.unwrap_or(BUILD_TARGET));
let compiler = config.get_compiler();
let mut command = Command::new(compiler.path());
command.current_dir(src_path);
for (key, value) in compiler.env() {
command.env(key, value);
}
command.args(compiler.args());
if cfg!(windows) {
if cfg!(all(windows, target_env = "msvc")) {
command
.args(&["/nologo", "/LD", "/I"])
.arg(header_path)
@ -371,7 +480,9 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
));
}
Ok(())
Ok(BuildStatus::Built {
grammar_id: grammar.grammar_id,
})
}
fn needs_recompile(

@ -2,26 +2,45 @@ pub mod config;
pub mod grammar;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::PathBuf;
pub static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
once_cell::sync::Lazy::new(runtime_dir);
pub static RUNTIME_DIR: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(runtime_dir);
pub fn runtime_dir() -> std::path::PathBuf {
static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(|| {
let config_dir = config_dir();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
}
config_dir.join("config.toml")
});
// We should only initialize this value once.
CONFIG_FILE.set(config_file).ok();
}
pub fn runtime_dir() -> PathBuf {
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
return dir.into();
}
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
log::debug!("runtime dir: {}", path.to_string_lossy());
return path;
}
const RT_DIR: &str = "runtime";
let conf_dir = config_dir().join(RT_DIR);
if conf_dir.exists() {
return conf_dir;
}
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
}
// fallback to location of the executable being run
std::env::current_exe()
.ok()
@ -29,7 +48,7 @@ pub fn runtime_dir() -> std::path::PathBuf {
.unwrap()
}
pub fn config_dir() -> std::path::PathBuf {
pub fn config_dir() -> PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.config_dir();
@ -37,7 +56,7 @@ pub fn config_dir() -> std::path::PathBuf {
path
}
pub fn local_config_dirs() -> Vec<std::path::PathBuf> {
pub fn local_config_dirs() -> Vec<PathBuf> {
let directories = find_root_impl(None, &[".helix".to_string()])
.into_iter()
.map(|path| path.join(".helix"))
@ -46,7 +65,7 @@ pub fn local_config_dirs() -> Vec<std::path::PathBuf> {
directories
}
pub fn cache_dir() -> std::path::PathBuf {
pub fn cache_dir() -> PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.cache_dir();
@ -54,19 +73,22 @@ pub fn cache_dir() -> std::path::PathBuf {
path
}
pub fn config_file() -> std::path::PathBuf {
config_dir().join("config.toml")
pub fn config_file() -> PathBuf {
CONFIG_FILE
.get()
.map(|path| path.to_path_buf())
.unwrap_or_else(|| config_dir().join("config.toml"))
}
pub fn lang_config_file() -> std::path::PathBuf {
pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml")
}
pub fn log_file() -> std::path::PathBuf {
pub fn log_file() -> PathBuf {
cache_dir().join("helix.log")
}
pub fn find_root_impl(root: Option<&str>, root_markers: &[String]) -> Vec<std::path::PathBuf> {
pub fn find_root_impl(root: Option<&str>, root_markers: &[String]) -> Vec<PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let mut directories = Vec::new();
@ -111,11 +133,7 @@ pub fn find_root_impl(root: Option<&str>, root_markers: &[String]) -> Vec<std::p
/// documents that use a top-level array of values like the `languages.toml`,
/// where one usually wants to override or add to the array instead of
/// replacing it altogether.
pub fn merge_toml_values(
left: toml::Value,
right: toml::Value,
merge_toplevel_arrays: bool,
) -> toml::Value {
pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value {
use toml::Value;
fn get_name(v: &Value) -> Option<&str> {
@ -129,7 +147,7 @@ pub fn merge_toml_values(
// that you can specify a sub-set of languages in an overriding
// `languages.toml` but that nested arrays like Language Server
// arguments are replaced instead of merged.
if merge_toplevel_arrays {
if merge_depth > 0 {
left_items.reserve(right_items.len());
for rvalue in right_items {
let lvalue = get_name(&rvalue)
@ -138,7 +156,7 @@ pub fn merge_toml_values(
})
.map(|lpos| left_items.remove(lpos));
let mvalue = match lvalue {
Some(lvalue) => merge_toml_values(lvalue, rvalue, false),
Some(lvalue) => merge_toml_values(lvalue, rvalue, merge_depth - 1),
None => rvalue,
};
left_items.push(mvalue);
@ -149,18 +167,22 @@ pub fn merge_toml_values(
}
}
(Value::Table(mut left_map), Value::Table(right_map)) => {
for (rname, rvalue) in right_map {
match left_map.remove(&rname) {
Some(lvalue) => {
let merged_value = merge_toml_values(lvalue, rvalue, merge_toplevel_arrays);
left_map.insert(rname, merged_value);
}
None => {
left_map.insert(rname, rvalue);
if merge_depth > 0 {
for (rname, rvalue) in right_map {
match left_map.remove(&rname) {
Some(lvalue) => {
let merged_value = merge_toml_values(lvalue, rvalue, merge_depth - 1);
left_map.insert(rname, merged_value);
}
None => {
left_map.insert(rname, rvalue);
}
}
}
Value::Table(left_map)
} else {
Value::Table(right_map)
}
Value::Table(left_map)
}
// Catch everything else we didn't handle, and use the right value
(_, value) => value,
@ -185,7 +207,7 @@ mod merge_toml_tests {
.expect("Couldn't parse built-in languages config");
let user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user, true);
let merged = merge_toml_values(base, user, 3);
let languages = merged.get("language").unwrap().as_array().unwrap();
let nix = languages
.iter()
@ -218,7 +240,7 @@ mod merge_toml_tests {
.expect("Couldn't parse built-in languages config");
let user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user, true);
let merged = merge_toml_values(base, user, 3);
let languages = merged.get("language").unwrap().as_array().unwrap();
let ts = languages
.iter()

@ -271,8 +271,8 @@ impl Client {
// -------------------------------------------------------------------------------------------
pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
if self.config.is_some() {
log::info!("Using custom LSP config: {}", self.config.as_ref().unwrap());
if let Some(config) = &self.config {
log::info!("Using custom LSP config: {}", config);
}
#[allow(deprecated)]
@ -294,6 +294,11 @@ impl Client {
dynamic_registration: Some(false),
}),
workspace_folders: Some(true),
apply_edit: Some(true),
symbol: Some(lsp::WorkspaceSymbolClientCapabilities {
dynamic_registration: Some(false),
..Default::default()
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@ -321,6 +326,16 @@ impl Client {
content_format: Some(vec![lsp::MarkupKind::Markdown]),
..Default::default()
}),
signature_help: Some(lsp::SignatureHelpClientCapabilities {
signature_information: Some(lsp::SignatureInformationSettings {
documentation_format: Some(vec![lsp::MarkupKind::Markdown]),
parameter_information: Some(lsp::ParameterInformationSettings {
label_offset_support: Some(true),
}),
active_parameter_support: Some(true),
}),
..Default::default()
}),
rename: Some(lsp::RenameClientCapabilities {
dynamic_registration: Some(false),
prepare_support: Some(false),
@ -645,7 +660,12 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if signature help is not supported
capabilities.signature_help_provider.as_ref()?;
let params = lsp::SignatureHelpParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
@ -656,7 +676,7 @@ impl Client {
// lsp::SignatureHelpContext
};
self.call::<lsp::request::SignatureHelpRequest>(params)
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
}
pub fn text_document_hover(
@ -759,6 +779,26 @@ impl Client {
Ok(response.unwrap_or_default())
}
pub fn text_document_document_highlight(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::DocumentHighlightParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
position,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
};
self.call::<lsp::request::DocumentHighlightRequest>(params)
}
fn goto_request<
T: lsp::request::Request<
Params = lsp::GotoDefinitionParams,
@ -896,8 +936,8 @@ impl Client {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false)
_ => {
log::warn!("rename_symbol failed: The server does not support rename");
let err = "The server does not support rename";
log::warn!("rename_symbol failed: {}", err);
return Err(anyhow!(err));
}
};

@ -58,7 +58,7 @@ pub enum OffsetEncoding {
pub mod util {
use super::*;
use helix_core::{Range, Rope, Transaction};
use helix_core::{diagnostic::NumberOrString, Range, Rope, Transaction};
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
///
@ -78,11 +78,19 @@ pub mod util {
Error => lsp::DiagnosticSeverity::ERROR,
});
let code = match diag.code.clone() {
Some(x) => match x {
NumberOrString::Number(x) => Some(lsp::NumberOrString::Number(x)),
NumberOrString::String(x) => Some(lsp::NumberOrString::String(x)),
},
None => None,
};
// TODO: add support for Diagnostic.data
lsp::Diagnostic::new(
range_to_lsp_range(doc, range, offset_encoding),
severity,
None,
code,
None,
diag.message.to_owned(),
None,
@ -205,22 +213,6 @@ pub mod util {
}),
)
}
/// The result of asking the language server to format the document. This can be turned into a
/// `Transaction`, but the advantage of not doing that straight away is that this one is
/// `Send` and `Sync`.
#[derive(Clone, Debug)]
pub struct LspFormatting {
pub doc: Rope,
pub edits: Vec<lsp::TextEdit>,
pub offset_encoding: OffsetEncoding,
}
impl From<LspFormatting> for Transaction {
fn from(fmt: LspFormatting) -> Transaction {
generate_transaction_from_edits(&fmt.doc, fmt.edits, fmt.offset_encoding)
}
}
}
#[derive(Debug, PartialEq, Clone)]

@ -10,6 +10,7 @@ repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
default-run = "hx"
rust-version = "1.57"
[package.metadata.nix]
build = true
@ -17,6 +18,7 @@ app = true
[features]
unicode-lines = ["helix-core/unicode-lines"]
integration = []
[[bin]]
name = "hx"
@ -30,17 +32,17 @@ helix-dap = { version = "0.6", path = "../helix-dap" }
helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1"
once_cell = "1.12"
once_cell = "1.13"
which = "4.2"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.23", features = ["event-stream"] }
crossterm = { version = "0.25", features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
arc-swap = { version = "1.5.0" }
arc-swap = { version = "1.5.1" }
# Logging
fern = "0.6"
@ -62,8 +64,8 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
# ripgrep for global search
grep-regex = "0.1.9"
grep-searcher = "0.1.8"
grep-regex = "0.1.10"
grep-searcher = "0.1.10"
# Remove once retain_mut lands in stable rust
retain_mut = "0.1.7"
@ -73,3 +75,8 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
[build-dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.9"
indoc = "1.0.6"
tempfile = "3.3.0"

@ -19,7 +19,8 @@ fn main() {
if std::env::var("HELIX_DISABLE_AUTO_GRAMMAR_BUILD").is_err() {
fetch_grammars().expect("Failed to fetch tree-sitter grammars");
build_grammars().expect("Failed to compile tree-sitter grammars");
build_grammars(Some(std::env::var("TARGET").unwrap()))
.expect("Failed to compile tree-sitter grammars");
}
println!("cargo:rerun-if-changed=../runtime/grammars/");

@ -1,16 +1,18 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{
config::{default_syntax_loader, user_syntax_loader},
diagnostic::NumberOrString,
pos_at_coords, syntax, Selection,
};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{align_view, editor::ConfigEvent, theme, Align, Editor};
use helix_view::{align_view, editor::ConfigEvent, theme, tree::Layout, Align, Editor};
use serde_json::json;
use crate::{
args::Args,
commands::apply_workspace_edit,
compositor::Compositor,
compositor::{Compositor, Event},
config::Config,
job::Jobs,
keymap::Keymaps,
@ -24,10 +26,10 @@ use std::{
time::{Duration, Instant},
};
use anyhow::Error;
use anyhow::{Context, Error};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
event::{DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent},
execute, terminal,
tty::IsTty,
};
@ -39,9 +41,11 @@ use {
#[cfg(windows)]
type Signals = futures_util::stream::Empty<()>;
const LSP_DEADLINE: Duration = Duration::from_millis(16);
pub struct Application {
compositor: Compositor,
editor: Editor,
pub editor: Editor,
config: Arc<ArcSwap<Config>>,
@ -53,34 +57,40 @@ pub struct Application {
signals: Signals,
jobs: Jobs,
lsp_progress: LspProgressMap,
last_render: Instant,
}
impl Application {
pub fn new(args: Args) -> Result<Self, Error> {
use helix_view::editor::Action;
#[cfg(feature = "integration")]
fn setup_integration_logging() {
let level = std::env::var("HELIX_LOG_LEVEL")
.map(|lvl| lvl.parse().unwrap())
.unwrap_or(log::LevelFilter::Info);
// Separate file config so we can include year, month and day in file logs
let _ = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{} {} [{}] {}",
chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"),
record.target(),
record.level(),
message
))
})
.level(level)
.chain(std::io::stdout())
.apply();
}
let config_dir = helix_loader::config_dir();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
}
impl Application {
pub fn new(args: Args, config: Config) -> Result<Self, Error> {
#[cfg(feature = "integration")]
setup_integration_logging();
let config = match std::fs::read_to_string(config_dir.join("config.toml")) {
Ok(config) => toml::from_str(&config)
.map(crate::keymap::merge_keys)
.unwrap_or_else(|err| {
eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
Config::default()
}),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
};
use helix_view::editor::Action;
let theme_loader = std::sync::Arc::new(theme::Loader::new(
&config_dir,
&helix_loader::config_dir(),
&helix_loader::runtime_dir(),
));
@ -98,13 +108,7 @@ impl Application {
.ok()
.filter(|theme| (true_color || theme.is_16_color()))
})
.unwrap_or_else(|| {
if true_color {
theme_loader.default()
} else {
theme_loader.base16_default()
}
});
.unwrap_or_else(|| theme_loader.default_theme(true_color));
let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
@ -116,7 +120,7 @@ impl Application {
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut compositor = Compositor::new()?;
let mut compositor = Compositor::new().context("build compositor")?;
let config = Arc::new(ArcSwap::from_pointee(config));
let mut editor = Editor::new(
compositor.size(),
@ -135,27 +139,42 @@ impl Application {
if args.load_tutor {
let path = helix_loader::runtime_dir().join("tutor.txt");
editor.open(path, Action::VerticalSplit)?;
editor.open(&path, Action::VerticalSplit)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
} else if !args.files.is_empty() {
let first = &args.files[0].0; // we know it's not empty
if first.is_dir() {
std::env::set_current_dir(&first)?;
std::env::set_current_dir(&first).context("set current dir")?;
editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.load().editor);
compositor.push(Box::new(overlayed(picker)));
} else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
for (file, pos) in args.files {
for (i, (file, pos)) in args.files.into_iter().enumerate() {
if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
));
} else {
let doc_id = editor.open(file, Action::Load)?;
// If the user passes in either `--vsplit` or
// `--hsplit` as a command line argument, all the given
// files will be opened according to the selected
// option. If neither of those two arguments are passed
// in, just load the files normally.
let action = match args.split {
_ if i == 0 => Action::VerticalSplit,
Some(Layout::Vertical) => Action::VerticalSplit,
Some(Layout::Horizontal) => Action::HorizontalSplit,
None => Action::Load,
};
let doc_id = editor
.open(&file, action)
.context(format!("open '{}'", file.to_string_lossy()))?;
// with Action::Load all documents have the same view
// NOTE: this isn't necessarily true anymore. If
// `--vsplit` or `--hsplit` are used, the file which is
// opened last is focused on.
let view_id = editor.tree.focus;
let doc = editor.document_mut(doc_id).unwrap();
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
@ -168,7 +187,7 @@ impl Application {
let (view, doc) = current!(editor);
align_view(doc, view, Align::Center);
}
} else if stdin().is_tty() {
} else if stdin().is_tty() || cfg!(feature = "integration") {
editor.new_file(Action::VerticalSplit);
} else if cfg!(target_os = "macos") {
// On Linux and Windows, we allow the output of a command to be piped into the new buffer.
@ -186,7 +205,8 @@ impl Application {
#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
let signals =
Signals::new(&[signal::SIGTSTP, signal::SIGCONT]).context("build signal handler")?;
let app = Self {
compositor,
@ -200,40 +220,57 @@ impl Application {
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
last_render: Instant::now(),
};
Ok(app)
}
fn render(&mut self) {
let compositor = &mut self.compositor;
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
self.compositor.render(&mut cx);
compositor.render(&mut cx);
}
pub async fn event_loop(&mut self) {
let mut reader = EventStream::new();
let mut last_render = Instant::now();
let deadline = Duration::from_secs(1) / 60;
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
self.render();
self.last_render = Instant::now();
loop {
if self.editor.should_close() {
if !self.event_loop_until_idle(input_stream).await {
break;
}
}
}
pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
#[cfg(feature = "integration")]
let mut idle_handled = false;
loop {
if self.editor.should_close() {
return false;
}
use futures_util::StreamExt;
tokio::select! {
biased;
event = reader.next() => {
self.handle_terminal_events(event)
Some(event) = input_stream.next() => {
self.handle_terminal_events(event);
}
Some(signal) = self.signals.next() => {
self.handle_signals(signal).await;
@ -242,9 +279,10 @@ impl Application {
self.handle_language_server_message(call, id).await;
// limit render calls for fast language server messages
let last = self.editor.language_servers.incoming.is_empty();
if last || last_render.elapsed() > deadline {
if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render();
last_render = Instant::now();
self.last_render = Instant::now();
}
}
Some(payload) = self.editor.debugger_events.next() => {
@ -269,7 +307,23 @@ impl Application {
// idle timeout
self.editor.clear_idle_timer();
self.handle_idle_timeout();
#[cfg(feature = "integration")]
{
idle_handled = true;
}
}
}
// for integration tests only, reset the idle timer after every
// event to make a signal when test events are done processing
#[cfg(feature = "integration")]
{
if idle_handled {
return true;
}
self.editor.reset_idle_timer();
}
}
}
@ -294,7 +348,7 @@ impl Application {
}
fn refresh_config(&mut self) {
let config = Config::load(helix_loader::config_file()).unwrap_or_else(|err| {
let config = Config::load_default().unwrap_or_else(|err| {
self.editor.set_error(err.to_string());
Config::default()
});
@ -311,13 +365,7 @@ impl Application {
})
.ok()
.filter(|theme| (true_color || theme.is_16_color()))
.unwrap_or_else(|| {
if true_color {
self.theme_loader.default()
} else {
self.theme_loader.base16_default()
}
}),
.unwrap_or_else(|| self.theme_loader.default_theme(true_color)),
);
}
@ -370,7 +418,7 @@ impl Application {
}
}
pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
pub 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,
@ -378,15 +426,13 @@ impl Application {
};
// Handle key events
let should_redraw = match event {
Some(Ok(Event::Resize(width, height))) => {
Ok(CrosstermEvent::Resize(width, height)) => {
self.compositor.resize(width, height);
self.compositor
.handle_event(Event::Resize(width, height), &mut cx)
}
Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
Some(Err(x)) => panic!("{}", x),
None => panic!(),
Ok(event) => self.compositor.handle_event(event.into(), &mut cx),
Err(x) => panic!("{}", x),
};
if should_redraw && !self.editor.should_close() {
@ -449,7 +495,7 @@ impl Application {
));
}
}
Notification::PublishDiagnostics(params) => {
Notification::PublishDiagnostics(mut params) => {
let path = params.uri.to_file_path().unwrap();
let doc = self.editor.document_by_path_mut(&path);
@ -459,12 +505,9 @@ impl Application {
let diagnostics = params
.diagnostics
.into_iter()
.iter()
.filter_map(|diagnostic| {
use helix_core::{
diagnostic::{Range, Severity::*},
Diagnostic,
};
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity;
let language_server = doc.language_server().unwrap();
@ -512,12 +555,24 @@ impl Application {
}
};
let code = match diagnostic.code.clone() {
Some(x) => match x {
lsp::NumberOrString::Number(x) => {
Some(NumberOrString::Number(x))
}
lsp::NumberOrString::String(x) => {
Some(NumberOrString::String(x))
}
},
None => None,
};
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message,
message: diagnostic.message.clone(),
severity,
// code
code,
// source
})
})
@ -525,6 +580,19 @@ impl Application {
doc.set_diagnostics(diagnostics);
}
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params
.diagnostics
.sort_unstable_by_key(|d| (d.severity, d.range.start));
// Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand.
self.editor
.diagnostics
.insert(params.uri, params.diagnostics);
}
Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params);
@ -731,7 +799,7 @@ impl Application {
fn restore_term(&mut self) -> Result<(), Error> {
let mut stdout = stdout();
// reset cursor shape
write!(stdout, "\x1B[2 q")?;
write!(stdout, "\x1B[0 q")?;
// Ignore errors on disabling, this might trigger on windows if we call
// disable without calling enable previously
let _ = execute!(stdout, DisableMouseCapture);
@ -740,7 +808,10 @@ impl Application {
Ok(())
}
pub async fn run(&mut self) -> Result<i32, Error> {
pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
self.claim_term().await?;
// Exit the alternate screen and disable raw mode before panicking
@ -755,16 +826,20 @@ impl Application {
hook(info);
}));
self.event_loop().await;
self.event_loop(input_stream).await;
self.close().await?;
self.restore_term()?;
self.jobs.finish().await;
Ok(self.editor.exit_code)
}
pub async fn close(&mut self) -> anyhow::Result<()> {
self.jobs.finish().await?;
if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown");
};
self.restore_term()?;
Ok(self.editor.exit_code)
Ok(())
}
}

@ -1,5 +1,6 @@
use anyhow::Result;
use helix_core::Position;
use helix_view::tree::Layout;
use std::path::{Path, PathBuf};
#[derive(Default)]
@ -11,7 +12,9 @@ pub struct Args {
pub load_tutor: bool,
pub fetch_grammars: bool,
pub build_grammars: bool,
pub split: Option<Layout>,
pub verbosity: u64,
pub config_file: Option<PathBuf>,
pub files: Vec<(PathBuf, Position)>,
}
@ -28,6 +31,8 @@ impl Args {
"--version" => args.display_version = true,
"--help" => args.display_help = true,
"--tutor" => args.load_tutor = true,
"--vsplit" => args.split = Some(Layout::Vertical),
"--hsplit" => args.split = Some(Layout::Horizontal),
"--health" => {
args.health = true;
args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
@ -39,6 +44,10 @@ impl Args {
anyhow::bail!("--grammar must be followed by either 'fetch' or 'build'")
}
},
"-c" | "--config" => match argv.next().as_deref() {
Some(path) => args.config_file = Some(path.into()),
None => anyhow::bail!("--config must specify a path to read"),
},
arg if arg.starts_with("--") => {
anyhow::bail!("unexpected double dash argument: {}", arg)
}

File diff suppressed because it is too large Load Diff

@ -4,13 +4,15 @@ use crate::{
job::{Callback, Jobs},
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion};
use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, Client};
use helix_lsp::block_on;
use helix_view::editor::Breakpoint;
use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tui::text::Spans;
use std::collections::HashMap;
use std::future::Future;
@ -20,6 +22,38 @@ use anyhow::{anyhow, bail};
use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
impl ui::menu::Item for StackFrame {
type Data = ();
fn label(&self, _data: &Self::Data) -> Spans {
self.name.as_str().into() // TODO: include thread_states in the label
}
}
impl ui::menu::Item for DebugTemplate {
type Data = ();
fn label(&self, _data: &Self::Data) -> Spans {
self.name.as_str().into()
}
}
impl ui::menu::Item for Thread {
type Data = ThreadStates;
fn label(&self, thread_states: &Self::Data) -> Spans {
format!(
"{} ({})",
self.name,
thread_states
.get(&self.id)
.map(|state| state.as_str())
.unwrap_or("unknown")
)
.into()
}
}
fn thread_picker(
cx: &mut Context,
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
@ -41,17 +75,7 @@ fn thread_picker(
let thread_states = debugger.thread_states.clone();
let picker = FilePicker::new(
threads,
move |thread| {
format!(
"{} ({})",
thread.name,
thread_states
.get(&thread.id)
.map(|state| state.as_str())
.unwrap_or("unknown")
)
.into()
},
thread_states,
move |cx, thread, _action| callback_fn(cx.editor, thread),
move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
@ -192,6 +216,8 @@ pub fn dap_start_impl(
}
}
args.insert("cwd", to_value(std::env::current_dir().unwrap())?);
let args = to_value(args).unwrap();
let callback = |_editor: &mut Editor, _compositor: &mut Compositor, _response: Value| {
@ -243,7 +269,7 @@ pub fn dap_launch(cx: &mut Context) {
cx.push_layer(Box::new(overlayed(Picker::new(
templates,
|template| template.name.as_str().into(),
(),
|cx, template, _action| {
let completions = template.completion.clone();
let name = template.name.clone();
@ -475,7 +501,7 @@ pub fn dap_variables(cx: &mut Context) {
for scope in scopes.iter() {
// use helix_view::graphics::Style;
use tui::text::{Span, Spans};
use tui::text::Span;
let response = block_on(debugger.variables(scope.variables_reference));
variables.push(Spans::from(Span::styled(
@ -652,7 +678,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let picker = FilePicker::new(
frames,
|frame| frame.name.as_str().into(), // TODO: include thread_states in the label
(),
move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find

@ -1,20 +1,24 @@
use helix_lsp::{
block_on, lsp,
block_on,
lsp::{self, DiagnosticSeverity, NumberOrString},
util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
};
use tui::text::{Span, Spans};
use super::{align_view, push_jump, Align, Context, Editor};
use super::{align_view, push_jump, Align, Context, Editor, Open};
use helix_core::Selection;
use helix_view::editor::Action;
use helix_core::{path, Selection};
use helix_view::{editor::Action, theme::Style};
use crate::{
compositor::{self, Compositor},
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
ui::{
self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent,
},
};
use std::borrow::Cow;
use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};
/// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro
@ -34,6 +38,112 @@ macro_rules! language_server {
};
}
impl ui::menu::Item for lsp::Location {
/// Current working directory.
type Data = PathBuf;
fn label(&self, cwdir: &Self::Data) -> Spans {
let file: Cow<'_, str> = (self.uri.scheme() == "file")
.then(|| {
self.uri
.to_file_path()
.map(|path| {
// strip root prefix
path.strip_prefix(&cwdir)
.map(|path| path.to_path_buf())
.unwrap_or(path)
})
.map(|path| Cow::from(path.to_string_lossy().into_owned()))
.ok()
})
.flatten()
.unwrap_or_else(|| self.uri.as_str().into());
let line = self.range.start.line;
format!("{}:{}", file, line).into()
}
}
impl ui::menu::Item for lsp::SymbolInformation {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
fn label(&self, current_doc_path: &Self::Data) -> Spans {
if current_doc_path.as_ref() == Some(&self.location.uri) {
self.name.as_str().into()
} else {
match self.location.uri.to_file_path() {
Ok(path) => {
let relative_path = helix_core::path::get_relative_path(path.as_path())
.to_string_lossy()
.into_owned();
format!("{} ({})", &self.name, relative_path).into()
}
Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
}
}
}
}
struct DiagnosticStyles {
hint: Style,
info: Style,
warning: Style,
error: Style,
}
struct PickerDiagnostic {
url: lsp::Url,
diag: lsp::Diagnostic,
}
impl ui::menu::Item for PickerDiagnostic {
type Data = (DiagnosticStyles, DiagnosticsFormat);
fn label(&self, (styles, format): &Self::Data) -> Spans {
let mut style = self
.diag
.severity
.map(|s| match s {
DiagnosticSeverity::HINT => styles.hint,
DiagnosticSeverity::INFORMATION => styles.info,
DiagnosticSeverity::WARNING => styles.warning,
DiagnosticSeverity::ERROR => styles.error,
_ => Style::default(),
})
.unwrap_or_default();
// remove background as it is distracting in the picker list
style.bg = None;
let code = self
.diag
.code
.as_ref()
.map(|c| match c {
NumberOrString::Number(n) => n.to_string(),
NumberOrString::String(s) => s.to_string(),
})
.map(|code| format!(" ({})", code))
.unwrap_or_default();
let path = match format {
DiagnosticsFormat::HideSourcePath => String::new(),
DiagnosticsFormat::ShowSourcePath => {
let path = path::get_truncated_path(self.url.path())
.to_string_lossy()
.into_owned();
format!("{}: ", path)
}
};
Spans::from(vec![
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
}
}
fn location_to_file_location(location: &lsp::Location) -> FileLocation {
let path = location.uri.to_file_path().unwrap();
let line = Some((
@ -61,7 +171,14 @@ fn jump_to_location(
return;
}
};
let _id = editor.open(path, action).expect("editor.open failed");
match editor.open(&path, action) {
Ok(_) => (),
Err(err) => {
let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
editor.set_error(err);
return;
}
}
let (view, doc) = current!(editor);
let definition_pos = location.range.start;
// TODO: convert inside server
@ -81,29 +198,14 @@ fn sym_picker(
offset_encoding: OffsetEncoding,
) -> FilePicker<lsp::SymbolInformation> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
let current_path2 = current_path.clone();
FilePicker::new(
symbols,
move |symbol| {
if current_path.as_ref() == Some(&symbol.location.uri) {
symbol.name.as_str().into()
} else {
match symbol.location.uri.to_file_path() {
Ok(path) => {
let relative_path = helix_core::path::get_relative_path(path.as_path())
.to_string_lossy()
.into_owned();
format!("{} ({})", &symbol.name, relative_path).into()
}
Err(_) => format!("{} ({})", &symbol.name, &symbol.location.uri).into(),
}
}
},
current_path.clone(),
move |cx, symbol, action| {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
if current_path2.as_ref() != Some(&symbol.location.uri) {
if current_path.as_ref() != Some(&symbol.location.uri) {
let uri = &symbol.location.uri;
let path = match uri.to_file_path() {
Ok(path) => path,
@ -114,7 +216,7 @@ fn sym_picker(
return;
}
};
if let Err(err) = cx.editor.open(path, action) {
if let Err(err) = cx.editor.open(&path, action) {
let err = format!("failed to open document: {}: {}", uri, err);
log::error!("{}", err);
cx.editor.set_error(err);
@ -138,6 +240,69 @@ fn sym_picker(
.truncate_start(false)
}
#[derive(Copy, Clone, PartialEq)]
enum DiagnosticsFormat {
ShowSourcePath,
HideSourcePath,
}
fn diag_picker(
cx: &Context,
diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
current_path: Option<lsp::Url>,
format: DiagnosticsFormat,
offset_encoding: OffsetEncoding,
) -> FilePicker<PickerDiagnostic> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs
let mut flat_diag = Vec::new();
for (url, diags) in diagnostics {
flat_diag.reserve(diags.len());
for diag in diags {
flat_diag.push(PickerDiagnostic {
url: url.clone(),
diag,
});
}
}
let styles = DiagnosticStyles {
hint: cx.editor.theme.get("hint"),
info: cx.editor.theme.get("info"),
warning: cx.editor.theme.get("warning"),
error: cx.editor.theme.get("error"),
};
FilePicker::new(
flat_diag,
(styles, format),
move |cx, PickerDiagnostic { url, diag }, action| {
if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
} else {
let path = url.to_file_path().unwrap();
cx.editor.open(&path, action).expect("editor.open failed");
}
let (view, doc) = current!(cx.editor);
if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) {
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
align_view(doc, view, Align::Center);
}
},
move |_editor, PickerDiagnostic { url, diag }| {
let location = lsp::Location::new(url.clone(), diag.range);
Some(location_to_file_location(&location))
},
)
.truncate_start(false)
}
pub fn symbol_picker(cx: &mut Context) {
fn nested_to_flat(
list: &mut Vec<lsp::SymbolInformation>,
@ -208,11 +373,50 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
)
}
pub fn diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let language_server = language_server!(cx.editor, doc);
if let Some(current_url) = doc.url() {
let offset_encoding = language_server.offset_encoding();
let diagnostics = cx
.editor
.diagnostics
.get(&current_url)
.cloned()
.unwrap_or_default();
let picker = diag_picker(
cx,
[(current_url.clone(), diagnostics)].into(),
Some(current_url),
DiagnosticsFormat::HideSourcePath,
offset_encoding,
);
cx.push_layer(Box::new(overlayed(picker)));
}
}
pub fn workspace_diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let current_url = doc.url();
let offset_encoding = language_server.offset_encoding();
let diagnostics = cx.editor.diagnostics.clone();
let picker = diag_picker(
cx,
diagnostics,
current_url,
DiagnosticsFormat::ShowSourcePath,
offset_encoding,
);
cx.push_layer(Box::new(overlayed(picker)));
}
impl ui::menu::Item for lsp::CodeActionOrCommand {
fn label(&self) -> &str {
type Data = ();
fn label(&self, _data: &Self::Data) -> Spans {
match self {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
}
}
}
@ -257,7 +461,7 @@ pub fn code_action(cx: &mut Context) {
return;
}
let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
if event != PromptEvent::Validate {
return;
}
@ -287,10 +491,8 @@ pub fn code_action(cx: &mut Context) {
});
picker.move_down(); // pre-select the first item
let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
vertical: 1,
horizontal: 1,
});
let popup =
Popup::new("code-action", picker).margin(helix_view::graphics::Margin::all(1));
compositor.replace_or_push("code-action", popup);
},
)
@ -385,7 +587,7 @@ pub fn apply_workspace_edit(
};
let current_view_id = view!(editor).id;
let doc_id = match editor.open(path, Action::Load) {
let doc_id = match editor.open(&path, Action::Load) {
Ok(doc_id) => doc_id,
Err(err) => {
let err = format!("failed to open document: {}: {}", uri, err);
@ -487,13 +689,14 @@ pub fn apply_workspace_edit(
}
}
}
fn goto_impl(
editor: &mut Editor,
compositor: &mut Compositor,
locations: Vec<lsp::Location>,
offset_encoding: OffsetEncoding,
) {
let cwdir = std::env::current_dir().expect("couldn't determine current directory");
let cwdir = std::env::current_dir().unwrap_or_default();
match locations.as_slice() {
[location] => {
@ -505,26 +708,7 @@ fn goto_impl(
_locations => {
let picker = FilePicker::new(
locations,
move |location| {
let file: Cow<'_, str> = (location.uri.scheme() == "file")
.then(|| {
location
.uri
.to_file_path()
.map(|path| {
// strip root prefix
path.strip_prefix(&cwdir)
.map(|path| path.to_path_buf())
.unwrap_or(path)
})
.map(|path| Cow::from(path.to_string_lossy().into_owned()))
.ok()
})
.flatten()
.unwrap_or_else(|| location.uri.as_str().into());
let line = location.range.start.line;
format!("{}:{}", file, line).into()
},
cwdir,
move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
},
@ -622,34 +806,125 @@ pub fn goto_reference(cx: &mut Context) {
);
}
#[derive(PartialEq)]
pub enum SignatureHelpInvoked {
Manual,
Automatic,
}
pub fn signature_help(cx: &mut Context) {
signature_help_impl(cx, SignatureHelpInvoked::Manual)
}
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if was_manually_invoked {
cx.editor
.set_status("Language server not active for current buffer");
}
return;
}
};
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
Some(f) => f,
None => return,
};
cx.callback(
future,
move |_editor, _compositor, response: Option<lsp::SignatureHelp>| {
if let Some(signature_help) = response {
log::info!("{:?}", signature_help);
// signatures
// active_signature
// active_parameter
// render as:
// signature
// ----------
// doc
// with active param highlighted
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
let config = &editor.config();
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| was_manually_invoked)
{
return;
}
let response = match response {
// According to the spec the response should be None if there
// are no signatures, but some servers don't follow this.
Some(s) if !s.signatures.is_empty() => s,
_ => {
compositor.remove(SignatureHelp::ID);
return;
}
};
let doc = doc!(editor);
let language = doc
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
let signature = match response
.signatures
.get(response.active_signature.unwrap_or(0) as usize)
{
Some(s) => s,
None => return,
};
let mut contents = SignatureHelp::new(
signature.label.clone(),
language.to_string(),
Arc::clone(&editor.syn_loader),
);
let signature_doc = if config.lsp.display_signature_help_docs {
signature.documentation.as_ref().map(|doc| match doc {
lsp::Documentation::String(s) => s.clone(),
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
})
} else {
None
};
contents.set_signature_doc(signature_doc);
let active_param_range = || -> Option<(usize, usize)> {
let param_idx = signature
.active_parameter
.or(response.active_parameter)
.unwrap_or(0) as usize;
let param = signature.parameters.as_ref()?.get(param_idx)?;
match &param.label {
lsp::ParameterLabel::Simple(string) => {
let start = signature.label.find(string.as_str())?;
Some((start, start + string.len()))
}
lsp::ParameterLabel::LabelOffsets([start, end]) => {
// LS sends offsets based on utf-16 based string representation
// but highlighting in helix is done using byte offset.
use helix_core::str_utils::char_to_byte_idx;
let from = char_to_byte_idx(&signature.label, *start as usize);
let to = char_to_byte_idx(&signature.label, *end as usize);
Some((from, to))
}
}
};
contents.set_active_param_range(active_param_range());
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above)
.ignore_escape_key(true);
compositor.replace_or_push(SignatureHelp::ID, popup);
},
);
}
pub fn hover(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
@ -699,10 +974,23 @@ pub fn hover(cx: &mut Context) {
},
);
}
pub fn rename_symbol(cx: &mut Context) {
ui::prompt(
let (view, doc) = current_ref!(cx.editor);
let text = doc.text().slice(..);
let primary_selection = doc.selection(view.id).primary();
let prefill = if primary_selection.len() > 1 {
primary_selection
} else {
use helix_core::textobject::{textobject_word, TextObject};
textobject_word(text, primary_selection, TextObject::Inside, 1, false)
}
.fragment(text)
.into();
ui::prompt_with_input(
cx,
"rename-to:".into(),
prefill,
None,
ui::completers::none,
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
@ -724,3 +1012,44 @@ pub fn rename_symbol(cx: &mut Context) {
},
);
}
pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = language_server.text_document_document_highlight(doc.identifier(), pos, None);
cx.callback(
future,
move |editor, _compositor, response: Option<Vec<lsp::DocumentHighlight>>| {
let document_highlights = match response {
Some(highlights) if !highlights.is_empty() => highlights,
_ => return,
};
let (view, doc) = current!(editor);
let language_server = language_server!(editor, doc);
let offset_encoding = language_server.offset_encoding();
let text = doc.text();
let pos = doc.selection(view.id).primary().head;
// We must find the range that contains our primary cursor to prevent our primary cursor to move
let mut primary_index = 0;
let ranges = document_highlights
.iter()
.filter_map(|highlight| lsp_range_to_range(text, highlight.range, offset_encoding))
.enumerate()
.map(|(i, range)| {
if range.contains(pos) {
primary_index = i;
}
range
})
.collect();
let selection = Selection::new(ranges, primary_index);
doc.set_selection(view.id, selection);
},
);
}

File diff suppressed because it is too large Load Diff

@ -4,7 +4,8 @@
use helix_core::Position;
use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
#[cfg(feature = "integration")]
use tui::backend::TestBackend;
use tui::buffer::Buffer as Surface;
pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
@ -15,9 +16,10 @@ pub enum EventResult {
Consumed(Option<Callback>),
}
use crate::job::Jobs;
use helix_view::Editor;
use crate::job::Jobs;
pub use helix_view::input::Event;
pub struct Context<'a> {
pub editor: &'a mut Editor,
@ -63,11 +65,21 @@ pub trait Component: Any + AnyComponent {
}
}
use anyhow::Error;
use anyhow::Context as AnyhowContext;
use tui::backend::Backend;
#[cfg(not(feature = "integration"))]
use tui::backend::CrosstermBackend;
#[cfg(not(feature = "integration"))]
use std::io::stdout;
use tui::backend::{Backend, CrosstermBackend};
#[cfg(not(feature = "integration"))]
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
#[cfg(feature = "integration")]
type Terminal = tui::terminal::Terminal<TestBackend>;
pub struct Compositor {
layers: Vec<Box<dyn Component>>,
terminal: Terminal,
@ -76,9 +88,14 @@ pub struct Compositor {
}
impl Compositor {
pub fn new() -> Result<Self, Error> {
pub fn new() -> anyhow::Result<Self> {
#[cfg(not(feature = "integration"))]
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
#[cfg(feature = "integration")]
let backend = TestBackend::new(120, 150);
let terminal = Terminal::new(backend).context("build terminal")?;
Ok(Self {
layers: Vec::new(),
terminal,
@ -132,10 +149,18 @@ impl Compositor {
self.layers.pop()
}
pub fn remove(&mut self, id: &'static str) -> Option<Box<dyn Component>> {
let idx = self
.layers
.iter()
.position(|layer| layer.id() == Some(id))?;
Some(self.layers.remove(idx))
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
// If it is a key event and a macro is being recorded, push the key event to the recording.
if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
keys.push(key.into());
keys.push(key);
}
let mut callbacks = Vec::new();

@ -4,6 +4,7 @@ use crossterm::{
};
use helix_core::config::{default_syntax_loader, user_syntax_loader};
use helix_loader::grammar::load_runtime_file;
use helix_view::clipboard::get_clipboard_provider;
use std::io::Write;
#[derive(Copy, Clone)]
@ -52,6 +53,7 @@ pub fn general() -> std::io::Result<()> {
let lang_file = helix_loader::lang_config_file();
let log_file = helix_loader::log_file();
let rt_dir = helix_loader::runtime_dir();
let clipboard_provider = get_clipboard_provider();
if config_file.exists() {
writeln!(stdout, "Config file: {}", config_file.display())?;
@ -76,6 +78,7 @@ pub fn general() -> std::io::Result<()> {
if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) {
writeln!(stdout, "{}", "Runtime directory is empty.".red())?;
}
writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?;
Ok(())
}
@ -139,7 +142,7 @@ pub fn languages_all() -> std::io::Result<()> {
let check_binary = |cmd: Option<String>| match cmd {
Some(cmd) => match which::which(&cmd) {
Ok(_) => column(&format!(" {}", cmd), Color::Green),
Ok(_) => column(&format!(" {}", cmd), Color::Green),
Err(_) => column(&format!("✘ {}", cmd), Color::Red),
},
None => column("None", Color::Yellow),
@ -159,7 +162,7 @@ pub fn languages_all() -> std::io::Result<()> {
for ts_feat in TsFeature::all() {
match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() {
true => column("", Color::Green),
true => column("", Color::Green),
false => column("✘", Color::Red),
}
}
@ -268,7 +271,7 @@ fn probe_treesitter_feature(lang: &str, feature: TsFeature) -> std::io::Result<(
let mut stdout = stdout.lock();
let found = match load_runtime_file(lang, feature.runtime_filename()).is_ok() {
true => "".green(),
true => "".green(),
false => "✘".red(),
};
writeln!(stdout, "{} queries: {}", feature.short_title(), found)?;

@ -2,7 +2,7 @@ use helix_view::Editor;
use crate::compositor::Compositor;
use futures_util::future::{self, BoxFuture, Future, FutureExt};
use futures_util::future::{BoxFuture, Future, FutureExt};
use futures_util::stream::{FuturesUnordered, StreamExt};
pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
@ -93,8 +93,21 @@ impl Jobs {
}
/// Blocks until all the jobs that need to be waited on are done.
pub async fn finish(&mut self) {
let wait_futures = std::mem::take(&mut self.wait_futures);
wait_futures.for_each(|_| future::ready(())).await
pub async fn finish(&mut self) -> anyhow::Result<()> {
log::debug!("waiting on jobs...");
let mut wait_futures = std::mem::take(&mut self.wait_futures);
while let (Some(job), tail) = wait_futures.into_future().await {
match job {
Ok(_) => {
wait_futures = tail;
}
Err(e) => {
self.wait_futures = tail;
return Err(e);
}
}
}
Ok(())
}
}

@ -208,18 +208,17 @@ pub struct Keymap {
root: KeyTrie,
}
/// A map of command names to keybinds that will execute the command.
pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
Keymap { root }
}
pub fn reverse_map(&self) -> HashMap<String, Vec<Vec<KeyEvent>>> {
pub fn reverse_map(&self) -> ReverseKeymap {
// recursively visit all nodes in keymap
fn map_node(
cmd_map: &mut HashMap<String, Vec<Vec<KeyEvent>>>,
node: &KeyTrie,
keys: &mut Vec<KeyEvent>,
) {
fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
match node {
KeyTrie::Leaf(cmd) => match cmd {
MappableCommand::Typable { name, .. } => {

@ -104,6 +104,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"c" => goto_prev_class,
"a" => goto_prev_parameter,
"o" => goto_prev_comment,
"t" => goto_prev_test,
"p" => goto_prev_paragraph,
"space" => add_newline_above,
},
@ -114,6 +115,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"c" => goto_next_class,
"a" => goto_next_parameter,
"o" => goto_next_comment,
"t" => goto_next_test,
"p" => goto_next_paragraph,
"space" => add_newline_below,
},
@ -203,8 +205,11 @@ pub fn default() -> HashMap<Mode, Keymap> {
"f" => file_picker,
"F" => file_picker_in_current_directory,
"b" => buffer_picker,
"j" => jumplist_picker,
"s" => symbol_picker,
"S" => workspace_symbol_picker,
"g" => diagnostics_picker,
"G" => workspace_diagnostics_picker,
"a" => code_action,
"'" => last_picker,
"d" => { "Debug (experimental)" sticky=true
@ -257,6 +262,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"/" => global_search,
"k" => hover,
"r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor,
"?" => command_palette,
"e" => toggle_or_focus_explorer,
"E" => open_explorer_recursion,
@ -270,8 +276,13 @@ pub fn default() -> HashMap<Mode, Keymap> {
"j" | "down" => scroll_down,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
"C-u" => half_page_up,
"C-d" => half_page_down,
"C-u" | "backspace" => half_page_up,
"C-d" | "space" => half_page_down,
"/" => search,
"?" => rsearch,
"n" => search_next,
"N" => search_prev,
},
"Z" => { "View" sticky=true
"z" | "c" => align_view_center,
@ -282,8 +293,13 @@ pub fn default() -> HashMap<Mode, Keymap> {
"j" | "down" => scroll_down,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
"C-u" => half_page_up,
"C-d" => half_page_down,
"C-u" | "backspace" => half_page_up,
"C-d" | "space" => half_page_down,
"/" => search,
"?" => rsearch,
"n" => search_next,
"N" => search_prev,
},
"\"" => select_register,
@ -338,14 +354,13 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-w" => delete_word_backward,
"A-backspace" => delete_word_backward,
"A-d" => delete_word_forward,
"A-del" => delete_word_forward,
"C-s" => commit_undo_checkpoint,
"left" => move_char_left,
"C-b" => move_char_left,
"down" => move_line_down,
"C-n" => move_line_down,
"up" => move_line_up,
"C-p" => move_line_up,
"right" => move_char_right,
"C-f" => move_char_right,
"A-b" => move_prev_word_end,

@ -1,6 +1,8 @@
use anyhow::{Context, Result};
use anyhow::{Context, Error, Result};
use crossterm::event::EventStream;
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::Config;
use std::path::PathBuf;
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@ -62,9 +64,12 @@ FLAGS:
--health [LANG] Checks for potential errors in editor setup
If given, checks for config errors in language LANG
-g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml
-c, --config <file> Specifies a file to use for configuration
-v Increases logging verbosity each use for up to 3 times
(default file: {})
-V, --version Prints version information
--vsplit Splits all given files vertically into different windows
--hsplit Splits all given files horizontally into different windows
",
env!("CARGO_PKG_NAME"),
env!("VERSION_AND_GIT_HASH"),
@ -104,16 +109,37 @@ FLAGS:
}
if args.build_grammars {
helix_loader::grammar::build_grammars()?;
helix_loader::grammar::build_grammars(None)?;
return Ok(0);
}
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
let config_dir = helix_loader::config_dir();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
}
helix_loader::initialize_config_file(args.config_file.clone());
let config = match std::fs::read_to_string(helix_loader::config_file()) {
Ok(config) => toml::from_str(&config)
.map(helix_term::keymap::merge_keys)
.unwrap_or_else(|err| {
eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config");
use std::io::Read;
let _ = std::io::stdin().read(&mut []);
Config::default()
}),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
};
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args).context("unable to create new application")?;
let mut app = Application::new(args, config).context("unable to create new application")?;
let exit_code = app.run().await?;
let exit_code = app.run(&mut EventStream::new()).await?;
Ok(exit_code)
}

@ -1,12 +1,16 @@
use crate::compositor::{Component, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent};
use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::editor::CompleteAction;
use tui::buffer::Buffer as Surface;
use tui::text::Spans;
use std::borrow::Cow;
use helix_core::{Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};
use helix_view::{
graphics::Rect,
input::{KeyCode, KeyEvent},
Document, Editor,
};
use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@ -15,19 +19,25 @@ use helix_lsp::{lsp, util};
use lsp::CompletionItem;
impl menu::Item for CompletionItem {
fn sort_text(&self) -> &str {
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
type Data = ();
fn sort_text(&self, data: &Self::Data) -> Cow<str> {
self.filter_text(data)
}
fn filter_text(&self) -> &str {
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
#[inline]
fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
self.filter_text
.as_ref()
.unwrap_or(&self.label)
.as_str()
.into()
}
fn label(&self) -> &str {
self.label.as_str()
fn label(&self, _data: &Self::Data) -> Spans {
self.label.as_str().into()
}
fn row(&self) -> menu::Row {
fn row(&self, _data: &Self::Data) -> menu::Row {
menu::Row::new(vec![
menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind {
@ -78,6 +88,8 @@ pub struct Completion {
}
impl Completion {
pub const ID: &'static str = "completion";
pub fn new(
editor: &Editor,
items: Vec<CompletionItem>,
@ -85,7 +97,7 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Self {
let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
item: &CompletionItem,
@ -207,7 +219,7 @@ impl Completion {
}
};
});
let popup = Popup::new("completion", menu);
let popup = Popup::new(Self::ID, menu);
let mut completion = Self {
popup,
start_offset,

@ -1,35 +1,35 @@
use crate::{
commands,
compositor::{Component, Context, EventResult},
key,
compositor::{Component, Context, Event, EventResult},
job, key,
keymap::{KeymapResult, Keymaps},
ui::{overlay::Overlay, Completion, Explorer, ProgressSpinners},
};
use helix_core::{
coords_at_pos, encoding,
graphemes::{
ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
},
movement::Direction,
syntax::{self, HighlightEvent},
unicode::segmentation::UnicodeSegmentation,
unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection, Transaction,
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
document::Mode,
editor::{CompleteAction, CursorShapeConfig},
graphics::{CursorKind, Modifier, Rect, Style},
input::KeyEvent,
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
use std::borrow::Cow;
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
use tui::buffer::Buffer as Surface;
use super::lsp::SignatureHelp;
use super::statusline;
pub struct EditorView {
pub keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
@ -115,6 +115,10 @@ impl EditorView {
}
}
if is_focused && editor.config().cursorline {
Self::highlight_cursorline(doc, view, surface, theme);
}
let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
@ -133,7 +137,7 @@ impl EditorView {
surface,
theme,
highlights,
&editor.config().whitespace,
&editor.config(),
);
Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
Self::render_rulers(editor, doc, view, inner, surface, theme);
@ -160,7 +164,11 @@ impl EditorView {
.area
.clip_top(view.area.height.saturating_sub(1))
.clip_bottom(1); // -1 from bottom to remove commandline
self.render_statusline(doc, view, statusline_area, surface, theme, is_focused);
let mut context =
statusline::RenderContext::new(editor, doc, view, is_focused, &self.spinners);
statusline::render(&mut context, statusline_area, surface);
}
pub fn render_rulers(
@ -172,7 +180,9 @@ impl EditorView {
theme: &Theme,
) {
let editor_rulers = &editor.config().rulers;
let ruler_theme = theme.get("ui.virtual.ruler");
let ruler_theme = theme
.try_get("ui.virtual.ruler")
.unwrap_or_else(|| Style::default().bg(Color::Red));
let rulers = doc
.language_config()
@ -373,34 +383,69 @@ impl EditorView {
surface: &mut Surface,
theme: &Theme,
highlights: H,
whitespace: &helix_view::editor::WhitespaceConfig,
config: &helix_view::editor::Config,
) {
let whitespace = &config.whitespace;
use helix_view::editor::WhitespaceRenderValue;
// It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
// of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
let text = doc.text().slice(..);
let characters = &whitespace.characters;
let mut spans = Vec::new();
let mut visual_x = 0u16;
let mut line = 0u16;
let tab_width = doc.tab_width();
let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
(1..tab_width).fold(whitespace.characters.tab.to_string(), |s, _| s + " ")
std::iter::once(characters.tab)
.chain(std::iter::repeat(characters.tabpad).take(tab_width - 1))
.collect()
} else {
" ".repeat(tab_width)
};
let space = whitespace.characters.space.to_string();
let nbsp = whitespace.characters.nbsp.to_string();
let space = characters.space.to_string();
let nbsp = characters.nbsp.to_string();
let newline = if whitespace.render.newline() == WhitespaceRenderValue::All {
whitespace.characters.newline.to_string()
characters.newline.to_string()
} else {
" ".to_string()
};
let indent_guide_char = config.indent_guides.character.to_string();
let text_style = theme.get("ui.text");
let whitespace_style = theme.get("ui.virtual.whitespace");
let mut is_in_indent_area = true;
let mut last_line_indent_level = 0;
// use whitespace style as fallback for indent-guide
let indent_guide_style = text_style.patch(
theme
.try_get("ui.virtual.indent-guide")
.unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
);
let draw_indent_guides = |indent_level, line, surface: &mut Surface| {
if !config.indent_guides.render {
return;
}
let starting_indent = (offset.col / tab_width) as u16;
// TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some
// extra loops if the code is deeply nested.
for i in starting_indent..(indent_level / tab_width as u16) {
surface.set_string(
viewport.x + (i * tab_width as u16) - offset.col as u16,
viewport.y + line,
&indent_guide_char,
indent_guide_style,
);
}
};
'outer: for event in highlights {
match event {
HighlightEvent::HighlightStart(span) => {
@ -453,8 +498,11 @@ impl EditorView {
);
}
draw_indent_guides(last_line_indent_level, line, surface);
visual_x = 0;
line += 1;
is_in_indent_area = true;
// TODO: with proper iter this shouldn't be necessary
if line >= viewport.height {
@ -464,7 +512,7 @@ impl EditorView {
let grapheme = Cow::from(grapheme);
let is_whitespace;
let (grapheme, width) = if grapheme == "\t" {
let (display_grapheme, width) = if grapheme == "\t" {
is_whitespace = true;
// make sure we display tab as appropriate amount of spaces
let visual_tab_width = tab_width - (visual_x as usize % tab_width);
@ -486,12 +534,30 @@ impl EditorView {
(grapheme.as_ref(), width)
};
let cut_off_start = offset.col.saturating_sub(visual_x as usize);
if !out_of_bounds {
// if we're offscreen just keep going until we hit a new line
surface.set_string(
viewport.x + visual_x - offset.col as u16,
viewport.y + line,
grapheme,
display_grapheme,
if is_whitespace {
style.patch(whitespace_style)
} else {
style
},
);
} else if cut_off_start != 0 && cut_off_start < width {
// partially on screen
let rect = Rect::new(
viewport.x as u16,
viewport.y + line,
(width - cut_off_start) as u16,
1,
);
surface.set_style(
rect,
if is_whitespace {
style.patch(whitespace_style)
} else {
@ -500,6 +566,12 @@ impl EditorView {
);
}
if is_in_indent_area && !(grapheme == " " || grapheme == "\t") {
draw_indent_guides(visual_x, line, surface);
is_in_indent_area = false;
last_line_indent_level = visual_x;
}
visual_x = visual_x.saturating_add(width as u16);
}
}
@ -571,7 +643,7 @@ impl EditorView {
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8);
for (constructor, width) in &view.gutters {
for (constructor, width) in view.gutters() {
let gutter = constructor(editor, doc, view, theme, is_focused, *width);
text.reserve(*width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
@ -629,16 +701,17 @@ impl EditorView {
let hint = theme.get("hint");
let mut lines = Vec::new();
let background_style = theme.get("ui.background");
for diagnostic in diagnostics {
let text = Text::styled(
&diagnostic.message,
match diagnostic.severity {
let style = Style::reset()
.patch(background_style)
.patch(match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
});
let text = Text::styled(&diagnostic.message, style);
lines.extend(text.lines);
}
@ -653,149 +726,41 @@ impl EditorView {
);
}
pub fn render_statusline(
&self,
doc: &Document,
view: &View,
viewport: Rect,
surface: &mut Surface,
theme: &Theme,
is_focused: bool,
) {
use tui::text::{Span, Spans};
//-------------------------------
// Left side of the status line.
//-------------------------------
let mode = match doc.mode() {
Mode::Insert => "INS",
Mode::Select => "SEL",
Mode::Normal => "NOR",
};
let progress = doc
.language_server()
.and_then(|srv| {
self.spinners
.get(srv.id())
.and_then(|spinner| spinner.frame())
})
.unwrap_or("");
let base_style = if is_focused {
theme.get("ui.statusline")
} else {
theme.get("ui.statusline.inactive")
};
// statusline
surface.set_style(viewport.with_height(1), base_style);
if is_focused {
surface.set_string(viewport.x + 1, viewport.y, mode, base_style);
}
surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
/// Apply the highlighting on the lines where a cursor is active
pub fn highlight_cursorline(doc: &Document, view: &View, surface: &mut Surface, theme: &Theme) {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
//-------------------------------
// Right side of the status line.
//-------------------------------
let primary_line = doc.selection(view.id).primary().cursor_line(text);
let mut right_side_text = Spans::default();
// The secondary_lines do contain the primary_line, it doesn't matter
// as the else-if clause in the loop later won't test for the
// secondary_lines if primary_line == line.
// It's used inside a loop so the collect isn't needless:
// https://github.com/rust-lang/rust-clippy/issues/6164
#[allow(clippy::needless_collect)]
let secondary_lines: Vec<_> = doc
.selection(view.id)
.iter()
.map(|range| range.cursor_line(text))
.collect();
// Compute the individual info strings and add them to `right_side_text`.
let primary_style = theme.get("ui.cursorline.primary");
let secondary_style = theme.get("ui.cursorline.secondary");
// Diagnostics
let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity;
match diag.severity {
Some(Severity::Warning) => counts.0 += 1,
Some(Severity::Error) | None => counts.1 += 1,
_ => {}
}
counts
});
let (warnings, errors) = diags;
let warning_style = theme.get("warning");
let error_style = theme.get("error");
for i in 0..2 {
let (count, style) = match i {
0 => (warnings, warning_style),
1 => (errors, error_style),
_ => unreachable!(),
};
if count == 0 {
continue;
for line in view.offset.row..(last_line + 1) {
let area = Rect::new(
view.area.x,
view.area.y + (line - view.offset.row) as u16,
view.area.width,
1,
);
if primary_line == line {
surface.set_style(area, primary_style);
} else if secondary_lines.binary_search(&line).is_ok() {
surface.set_style(area, secondary_style);
}
let style = base_style.patch(style);
right_side_text.0.push(Span::styled("●", style));
right_side_text
.0
.push(Span::styled(format!(" {} ", count), base_style));
}
// Selections
let sels_count = doc.selection(view.id).len();
right_side_text.0.push(Span::styled(
format!(
" {} sel{} ",
sels_count,
if sels_count == 1 { "" } else { "s" }
),
base_style,
));
// Position
let pos = coords_at_pos(
doc.text().slice(..),
doc.selection(view.id)
.primary()
.cursor(doc.text().slice(..)),
);
right_side_text.0.push(Span::styled(
format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
base_style,
));
let enc = doc.encoding();
if enc != encoding::UTF_8 {
right_side_text
.0
.push(Span::styled(format!(" {} ", enc.name()), base_style));
}
// Render to the statusline.
surface.set_spans(
viewport.x
+ viewport
.width
.saturating_sub(right_side_text.width() as u16),
viewport.y,
&right_side_text,
right_side_text.width() as u16,
);
//-------------------------------
// Middle / File path / Title
//-------------------------------
let title = {
let rel_path = doc.relative_path();
let path = rel_path
.as_ref()
.map(|p| p.to_string_lossy())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
};
surface.set_string_truncated(
viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
viewport.y,
&title,
viewport
.width
.saturating_sub(6)
.saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
|_| base_style,
true,
true,
);
}
/// Handle events by looking them up in `self.keymaps`. Returns None
@ -977,46 +942,51 @@ impl EditorView {
cxt: &mut commands::Context,
) -> EventResult {
let config = cxt.editor.config();
match event {
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row,
column,
modifiers,
..
} => {
let editor = &mut cxt.editor;
let MouseEvent {
kind,
row,
column,
modifiers,
..
} = event;
let pos_and_view = |editor: &Editor, row, column| {
editor.tree.views().find_map(|(view, _focus)| {
view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
.map(|pos| (pos, view.id))
})
};
let result = editor.tree.views().find_map(|(view, _focus)| {
view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
.map(|pos| (pos, view.id))
});
let gutter_coords_and_view = |editor: &Editor, row, column| {
editor.tree.views().find_map(|(view, _focus)| {
view.gutter_coords_at_screen_coords(row, column)
.map(|coords| (coords, view.id))
})
};
match kind {
MouseEventKind::Down(MouseButton::Left) => {
let editor = &mut cxt.editor;
if let Some((pos, view_id)) = result {
if let Some((pos, view_id)) = pos_and_view(editor, row, column) {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
if modifiers == crossterm::event::KeyModifiers::ALT {
if modifiers == KeyModifiers::ALT {
let selection = doc.selection(view_id).clone();
doc.set_selection(view_id, selection.push(Range::point(pos)));
} else {
doc.set_selection(view_id, Selection::point(pos));
}
editor.tree.focus = view_id;
editor.focus(view_id);
return EventResult::Consumed(None);
}
let result = editor.tree.views().find_map(|(view, _focus)| {
view.gutter_coords_at_screen_coords(row, column)
.map(|coords| (coords, view.id))
});
if let Some((coords, view_id)) = result {
editor.tree.focus = view_id;
if let Some((coords, view_id)) = gutter_coords_and_view(editor, row, column) {
editor.focus(view_id);
let view = editor.tree.get(view_id);
let doc = editor.documents.get_mut(&view.doc).unwrap();
let (view, doc) = current!(cxt.editor);
let path = match doc.path() {
Some(path) => path.clone(),
@ -1033,12 +1003,7 @@ impl EditorView {
EventResult::Ignored(None)
}
MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
row,
column,
..
} => {
MouseEventKind::Drag(MouseButton::Left) => {
let (view, doc) = current!(cxt.editor);
let pos = match view.pos_at_screen_coords(doc, row, column) {
@ -1050,15 +1015,11 @@ impl EditorView {
let primary = selection.primary_mut();
*primary = primary.put_cursor(doc.text().slice(..), pos, true);
doc.set_selection(view.id, selection);
EventResult::Consumed(None)
}
MouseEvent {
kind: MouseEventKind::ScrollUp | MouseEventKind::ScrollDown,
row,
column,
..
} => {
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown => {
let current_view = cxt.editor.tree.focus;
let direction = match event.kind {
@ -1067,17 +1028,12 @@ impl EditorView {
_ => unreachable!(),
};
let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column)
.map(|_| view.id)
});
match result {
Some(view_id) => cxt.editor.tree.focus = view_id,
match pos_and_view(cxt.editor, row, column) {
Some((_, view_id)) => cxt.editor.tree.focus = view_id,
None => return EventResult::Ignored(None),
}
let offset = config.scroll_lines.abs() as usize;
let offset = config.scroll_lines.unsigned_abs();
commands::scroll(cxt, offset, direction);
cxt.editor.tree.focus = current_view;
@ -1085,18 +1041,20 @@ impl EditorView {
EventResult::Consumed(None)
}
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
..
} => {
MouseEventKind::Up(MouseButton::Left) => {
if !config.middle_click_paste {
return EventResult::Ignored(None);
}
let (view, doc) = current!(cxt.editor);
let range = doc.selection(view.id).primary();
if range.to() - range.from() <= 1 {
if doc
.selection(view.id)
.primary()
.slice(doc.text().slice(..))
.len_chars()
<= 1
{
return EventResult::Ignored(None);
}
@ -1105,27 +1063,15 @@ impl EditorView {
EventResult::Consumed(None)
}
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Right),
row,
column,
modifiers,
..
} => {
let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
view.gutter_coords_at_screen_coords(row, column)
.map(|coords| (coords, view.id))
});
if let Some((coords, view_id)) = result {
cxt.editor.tree.focus = view_id;
MouseEventKind::Up(MouseButton::Right) => {
if let Some((coords, view_id)) = gutter_coords_and_view(cxt.editor, row, column) {
cxt.editor.focus(view_id);
let view = cxt.editor.tree.get(view_id);
let doc = cxt.editor.documents.get_mut(&view.doc).unwrap();
let (view, doc) = current!(cxt.editor);
let line = coords.row + view.offset.row;
if let Ok(pos) = doc.text().try_line_to_char(line) {
doc.set_selection(view_id, Selection::point(pos));
if modifiers == crossterm::event::KeyModifiers::ALT {
if modifiers == KeyModifiers::ALT {
commands::MappableCommand::dap_edit_log.execute(cxt);
} else {
commands::MappableCommand::dap_edit_condition.execute(cxt);
@ -1134,38 +1080,29 @@ impl EditorView {
return EventResult::Consumed(None);
}
}
EventResult::Ignored(None)
}
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Middle),
row,
column,
modifiers,
..
} => {
MouseEventKind::Up(MouseButton::Middle) => {
let editor = &mut cxt.editor;
if !config.middle_click_paste {
return EventResult::Ignored(None);
}
if modifiers == crossterm::event::KeyModifiers::ALT {
if modifiers == KeyModifiers::ALT {
commands::MappableCommand::replace_selections_with_primary_clipboard
.execute(cxt);
return EventResult::Consumed(None);
}
let result = editor.tree.views().find_map(|(view, _focus)| {
view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
.map(|pos| (pos, view.id))
});
if let Some((pos, view_id)) = result {
if let Some((pos, view_id)) = pos_and_view(editor, row, column) {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id;
cxt.editor.focus(view_id);
commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
return EventResult::Consumed(None);
}
@ -1203,9 +1140,8 @@ impl Component for EditorView {
// Handling it here but not re-rendering will cause flashing
EventResult::Consumed(None)
}
Event::Key(key) => {
Event::Key(mut key) => {
cx.editor.reset_idle_timer();
let mut key = KeyEvent::from(key);
canonicalize_key(&mut key);
// clear status
@ -1299,10 +1235,21 @@ impl Component for EditorView {
_ => unimplemented!(),
};
self.last_insert.1.clear();
commands::signature_help_impl(
&mut cx,
commands::SignatureHelpInvoked::Automatic,
);
}
(Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion
self.completion = None;
// TODO: Use an on_mode_change hook to remove signature help
context.jobs.callback(async {
let call: job::Callback = Box::new(|_editor, compositor| {
compositor.remove(SignatureHelp::ID);
});
Ok(call)
});
}
_ => (),
}
@ -1311,6 +1258,7 @@ impl Component for EditorView {
}
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::FocusGained | Event::FocusLost => EventResult::Ignored(None),
}
}
@ -1370,12 +1318,7 @@ impl Component for EditorView {
disp.push_str(&count.to_string())
}
for key in self.keymaps.pending() {
let s = key.to_string();
if s.graphemes(true).count() > 1 {
disp.push_str(&format!("<{}>", s));
} else {
disp.push_str(&s);
}
disp.push_str(&key.key_sequence_format());
}
if let Some(pseudo_pending) = &cx.editor.pseudo_pending {
disp.push_str(pseudo_pending.as_str())

@ -4,11 +4,11 @@ use crate::{
ctrl, key, shift, ui,
};
use anyhow::{bail, ensure, Result};
use crossterm::event::{Event, KeyEvent};
use helix_core::Position;
use helix_view::{
editor::Action,
graphics::{CursorKind, Modifier, Rect},
input::{Event, KeyEvent},
Editor,
};
use std::borrow::Cow;
@ -453,7 +453,7 @@ impl Explorer {
}
};
if meta.is_file() {
if let Err(e) = cx.editor.open(item.path.clone(), Action::Replace) {
if let Err(e) = cx.editor.open(&item.path, Action::Replace) {
cx.editor.set_error(format!("{e}"));
}
state.focus = false;

@ -27,10 +27,7 @@ impl Component for Info {
.borders(Borders::ALL)
.border_style(popup_style);
let margin = Margin {
vertical: 0,
horizontal: 1,
};
let margin = Margin::horizontal(1);
let inner = block.inner(area).inner(&margin);
block.render(area, surface);

@ -0,0 +1,133 @@
use std::sync::Arc;
use helix_core::syntax;
use helix_view::graphics::{Margin, Rect, Style};
use tui::buffer::Buffer;
use tui::widgets::{BorderType, Paragraph, Widget, Wrap};
use crate::compositor::{Component, Compositor, Context};
use crate::ui::Markdown;
use super::Popup;
pub struct SignatureHelp {
signature: String,
signature_doc: Option<String>,
/// Part of signature text
active_param_range: Option<(usize, usize)>,
language: String,
config_loader: Arc<syntax::Loader>,
}
impl SignatureHelp {
pub const ID: &'static str = "signature-help";
pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self {
Self {
signature,
signature_doc: None,
active_param_range: None,
language,
config_loader,
}
}
pub fn set_signature_doc(&mut self, signature_doc: Option<String>) {
self.signature_doc = signature_doc;
}
pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) {
self.active_param_range = offset;
}
pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
compositor.find_id::<Popup<Self>>(Self::ID)
}
}
impl Component for SignatureHelp {
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
let margin = Margin::horizontal(1);
let active_param_span = self.active_param_range.map(|(start, end)| {
vec![(
cx.editor.theme.find_scope_index("ui.selection").unwrap(),
start..end,
)]
});
let sig_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(),
&self.language,
Some(&cx.editor.theme),
Arc::clone(&self.config_loader),
active_param_span,
);
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
sig_text_para.render(sig_text_area.inner(&margin), surface);
if self.signature_doc.is_none() {
return;
}
let sep_style = Style::default();
let borders = BorderType::line_symbols(BorderType::Plain);
for x in sig_text_area.left()..sig_text_area.right() {
if let Some(cell) = surface.get_mut(x, sig_text_area.bottom()) {
cell.set_symbol(borders.horizontal).set_style(sep_style);
}
}
let sig_doc = match &self.signature_doc {
None => return,
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
};
let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
let sig_doc_area = area.clip_top(sig_text_area.height + 2);
let sig_doc_para = Paragraph::new(sig_doc)
.wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
sig_doc_para.render(sig_doc_area.inner(&margin), surface);
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
const PADDING: u16 = 2;
const SEPARATOR_HEIGHT: u16 = 1;
if PADDING >= viewport.1 || PADDING >= viewport.0 {
return None;
}
let max_text_width = (viewport.0 - PADDING).min(120);
let signature_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(),
&self.language,
None,
Arc::clone(&self.config_loader),
None,
);
let (sig_width, sig_height) =
crate::ui::text::required_size(&signature_text, max_text_width);
let (width, height) = match self.signature_doc {
Some(ref doc) => {
let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
let doc_text = doc_md.parse(None);
let (doc_width, doc_height) =
crate::ui::text::required_size(&doc_text, max_text_width);
(
sig_width.max(doc_width),
sig_height + SEPARATOR_HEIGHT + doc_height,
)
}
None => (sig_width, sig_height),
};
Some((width + PADDING, height + PADDING))
}
}

@ -144,7 +144,7 @@ impl Markdown {
}
}
fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
pub fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
let spans = std::mem::take(spans);
if !spans.is_empty() {
@ -204,7 +204,7 @@ impl Markdown {
tags.push(Tag::Item);
// get the approriate bullet for the current list
// get the appropriate bullet for the current list
let bullet = list_stack
.last()
.unwrap_or(&None) // use the '- ' bullet in case the list stack would be empty
@ -229,10 +229,7 @@ impl Markdown {
Event::End(tag) => {
tags.pop();
match tag {
Tag::Heading(_, _, _)
| Tag::Paragraph
| Tag::CodeBlock(CodeBlockKind::Fenced(_))
| Tag::Item => {
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) | Tag::Item => {
push_line(&mut spans, &mut lines);
}
_ => (),
@ -240,17 +237,18 @@ impl Markdown {
// whenever heading, code block or paragraph closes, empty line
match tag {
Tag::Heading(_, _, _)
| Tag::Paragraph
| Tag::CodeBlock(CodeBlockKind::Fenced(_)) => {
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) => {
lines.push(Spans::default());
}
_ => (),
}
}
Event::Text(text) => {
// TODO: temp workaround
if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
if let Some(Tag::CodeBlock(kind)) = tags.last() {
let language = match kind {
CodeBlockKind::Fenced(language) => language,
CodeBlockKind::Indented => "",
};
let tui_text = highlighted_code_block(
text.to_string(),
language,
@ -323,10 +321,7 @@ impl Component for Markdown {
.wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
let margin = Margin {
vertical: 1,
horizontal: 1,
};
let margin = Margin::all(1);
par.render(area.inner(&margin), surface);
}

@ -1,9 +1,10 @@
use std::{borrow::Cow, path::PathBuf};
use crate::{
compositor::{Callback, Component, Compositor, Context, EventResult},
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
};
use crossterm::event::Event;
use tui::{buffer::Buffer as Surface, widgets::Table};
use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table};
pub use tui::widgets::{Cell, Row};
@ -14,22 +15,41 @@ use helix_view::{graphics::Rect, Editor};
use tui::layout::Constraint;
pub trait Item {
fn label(&self) -> &str;
/// Additional editor state that is used for label calculation.
type Data;
fn label(&self, data: &Self::Data) -> Spans;
fn sort_text(&self, data: &Self::Data) -> Cow<str> {
let label: String = self.label(data).into();
label.into()
}
fn sort_text(&self) -> &str {
self.label()
fn filter_text(&self, data: &Self::Data) -> Cow<str> {
let label: String = self.label(data).into();
label.into()
}
fn filter_text(&self) -> &str {
self.label()
fn row(&self, data: &Self::Data) -> Row {
Row::new(vec![Cell::from(self.label(data))])
}
}
fn row(&self) -> Row {
Row::new(vec![Cell::from(self.label())])
impl Item for PathBuf {
/// Root prefix to strip.
type Data = PathBuf;
fn label(&self, root_path: &Self::Data) -> Spans {
self.strip_prefix(&root_path)
.unwrap_or(self)
.to_string_lossy()
.into()
}
}
pub struct Menu<T: Item> {
options: Vec<T>,
editor_data: T::Data,
cursor: Option<usize>,
@ -48,14 +68,18 @@ pub struct Menu<T: Item> {
}
impl<T: Item> Menu<T> {
const LEFT_PADDING: usize = 1;
// TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
// rendering)
pub fn new(
options: Vec<T>,
editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let mut menu = Self {
options,
editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
cursor: None,
@ -81,16 +105,17 @@ impl<T: Item> Menu<T> {
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text = option.filter_text();
let text: String = option.filter_text(&self.editor_data).into();
// TODO: using fuzzy_indices could give us the char idx for match highlighting
self.matcher
.fuzzy_match(text, pattern)
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
// matches.sort_unstable_by_key(|(_, score)| -score);
self.matches
.sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
self.matches.sort_unstable_by_key(|(index, _score)| {
self.options[*index].sort_text(&self.editor_data)
});
// reset cursor position
self.cursor = None;
@ -125,10 +150,10 @@ impl<T: Item> Menu<T> {
let n = self
.options
.first()
.map(|option| option.row().cells.len())
.map(|option| option.row(&self.editor_data).cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.row();
let row = option.row(&self.editor_data);
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
@ -150,6 +175,7 @@ impl<T: Item> Menu<T> {
len += 1; // +1: reserve some space for scrollbar
}
len += Self::LEFT_PADDING;
let width = len.min(viewport.0 as usize);
self.widths = max_lens
@ -210,7 +236,7 @@ impl<T: Item + 'static> Component for Menu<T> {
compositor.pop();
}));
match event.into() {
match event {
// esc or ctrl-c aborts the completion and closes the menu
key!(Esc) | ctrl!('c') => {
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
@ -271,6 +297,7 @@ impl<T: Item + 'static> Component for Menu<T> {
.try_get("ui.menu")
.unwrap_or_else(|| theme.get("ui.text"));
let selected = theme.get("ui.menu.selected");
surface.clear_with(area, style);
let scroll = self.scroll;
@ -288,7 +315,7 @@ impl<T: Item + 'static> Component for Menu<T> {
let win_height = area.height as usize;
const fn div_ceil(a: usize, b: usize) -> usize {
(a + b - 1) / a
(a + b - 1) / b
}
let scroll_height = std::cmp::min(div_ceil(win_height.pow(2), len), win_height as usize);
@ -296,7 +323,7 @@ impl<T: Item + 'static> Component for Menu<T> {
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
let rows = options.iter().map(|option| option.row());
let rows = options.iter().map(|option| option.row(&self.editor_data));
let table = Table::new(rows)
.style(style)
.highlight_style(selected)
@ -306,7 +333,7 @@ impl<T: Item + 'static> Component for Menu<T> {
use tui::widgets::TableState;
table.render_table(
area,
area.clip_left(Self::LEFT_PADDING as u16).clip_right(1),
surface,
&mut TableState {
offset: scroll,
@ -314,16 +341,34 @@ impl<T: Item + 'static> Component for Menu<T> {
},
);
if let Some(cursor) = self.cursor {
let offset_from_top = cursor - scroll;
let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
left.set_style(selected);
let right = &mut surface[(
area.right().saturating_sub(1),
area.y + offset_from_top as u16,
)];
right.set_style(selected);
}
let fits = len <= win_height;
let scroll_style = theme.get("ui.menu.scroll");
for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
let cell = &mut surface[(area.x + area.width - 1, area.y + i as u16)];
if !fits {
// Draw scroll track
cell.set_symbol("▐"); // right half block
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
if !fits && is_marked {
let cell = &mut surface[(area.x + area.width - 2, area.y + i as u16)];
cell.set_symbol("▐");
// cell.set_style(selected);
// cell.set_style(if is_marked { selected } else { style });
// Draw scroll thumb
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
}
}
}

@ -2,13 +2,15 @@ mod completion;
pub(crate) mod editor;
mod explore;
mod info;
pub mod lsp;
mod markdown;
pub mod menu;
pub mod overlay;
mod picker;
mod popup;
pub mod popup;
mod prompt;
mod spinner;
mod statusline;
mod text;
mod tree;
@ -37,7 +39,27 @@ pub fn prompt(
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static,
) {
let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn);
show_prompt(
cx,
Prompt::new(prompt, history_register, completion_fn, callback_fn),
);
}
pub fn prompt_with_input(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
input: String,
history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static,
) {
show_prompt(
cx,
Prompt::new(prompt, history_register, completion_fn, callback_fn).with_line(input),
);
}
fn show_prompt(cx: &mut crate::commands::Context, mut prompt: Prompt) {
// Calculate initial completion
prompt.recalculate_completion(cx.editor);
cx.push_layer(Box::new(prompt));
@ -174,12 +196,9 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
FilePicker::new(
files,
move |path: &PathBuf| {
// format_fn
path.strip_prefix(&root).unwrap_or(path).to_string_lossy()
},
root,
move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path.into(), action) {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
} else {
@ -244,6 +263,8 @@ pub mod completers {
));
names.push("default".into());
names.push("base16_default".into());
names.sort();
names.dedup();
let mut names: Vec<_> = names
.into_iter()
@ -259,21 +280,36 @@ pub mod completers {
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
matches.sort_unstable_by(|(name1, score1), (name2, score2)| {
(Reverse(*score1), name1).cmp(&(Reverse(*score2), name2))
});
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
}
/// Recursive function to get all keys from this value and add them to vec
fn get_keys(value: &serde_json::Value, vec: &mut Vec<String>, scope: Option<&str>) {
if let Some(map) = value.as_object() {
for (key, value) in map.iter() {
let key = match scope {
Some(scope) => format!("{}.{}", scope, key),
None => key.clone(),
};
get_keys(value, vec, Some(&key));
if !value.is_object() {
vec.push(key);
}
}
}
}
pub fn setting(_editor: &Editor, input: &str) -> Vec<Completion> {
static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
serde_json::to_value(Config::default())
.unwrap()
.as_object()
.unwrap()
.keys()
.cloned()
.collect()
let mut keys = Vec::new();
let json = serde_json::json!(Config::default());
get_keys(&json, &mut keys, None);
keys
});
let matcher = Matcher::default();
@ -315,7 +351,9 @@ pub mod completers {
})
.collect();
matches.sort_unstable_by_key(|(_language, score)| Reverse(*score));
matches.sort_unstable_by(|(language1, score1), (language2, score2)| {
(Reverse(*score1), language1).cmp(&(Reverse(*score2), language2))
});
matches
.into_iter()
@ -431,13 +469,18 @@ pub mod completers {
let range = (input.len().saturating_sub(file_name.len()))..;
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
(Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
});
files = matches
.into_iter()
.map(|(file, _)| (range.clone(), file))
.collect();
// TODO: complete to longest common match
} else {
files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
}
files

@ -1,4 +1,3 @@
use crossterm::event::Event;
use helix_core::Position;
use helix_view::{
graphics::{CursorKind, Rect},
@ -6,7 +5,7 @@ use helix_view::{
};
use tui::buffer::Buffer;
use crate::compositor::{Component, Context, EventResult};
use crate::compositor::{Component, Context, Event, EventResult};
/// Contains a component placed in the center of the parent component
pub struct Overlay<T> {

@ -1,9 +1,8 @@
use crate::{
compositor::{Component, Compositor, Context, EventResult},
compositor::{Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
ui::{self, EditorView},
};
use crossterm::event::Event;
use tui::{
buffer::Buffer as Surface,
widgets::{Block, BorderType, Borders},
@ -15,7 +14,6 @@ use tui::widgets::Widget;
use std::time::Instant;
use std::{
borrow::Cow,
cmp::Reverse,
collections::HashMap,
io::Read,
@ -30,6 +28,8 @@ use helix_view::{
Document, Editor,
};
use super::menu::Item;
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
@ -37,7 +37,7 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
/// File path and range of lines (used to align and highlight lines)
pub type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> {
pub struct FilePicker<T: Item> {
picker: Picker<T>,
pub truncate_start: bool,
/// Caches paths to documents
@ -84,15 +84,15 @@ impl Preview<'_, '_> {
}
}
impl<T> FilePicker<T> {
impl<T: Item> FilePicker<T> {
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
let truncate_start = true;
let mut picker = Picker::new(options, format_fn, callback_fn);
let mut picker = Picker::new(options, editor_data, callback_fn);
picker.truncate_start = truncate_start;
Self {
@ -163,7 +163,7 @@ impl<T> FilePicker<T> {
}
}
impl<T: 'static> Component for FilePicker<T> {
impl<T: Item + 'static> Component for FilePicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+
// |prompt | |preview |
@ -172,7 +172,7 @@ impl<T: 'static> Component for FilePicker<T> {
// | | | |
// +---------+ +---------+
let render_preview = area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
@ -200,10 +200,7 @@ impl<T: 'static> Component for FilePicker<T> {
// calculate the inner area inside the box
let inner = block.inner(preview_area);
// 1 column gap on either side
let margin = Margin {
vertical: 0,
horizontal: 1,
};
let margin = Margin::horizontal(1);
let inner = inner.inner(&margin);
block.render(preview_area, surface);
@ -240,7 +237,7 @@ impl<T: 'static> Component for FilePicker<T> {
surface,
&cx.editor.theme,
highlights,
&cx.editor.config().whitespace,
&cx.editor.config(),
);
// highlight the line
@ -283,8 +280,9 @@ impl<T: 'static> Component for FilePicker<T> {
}
}
pub struct Picker<T> {
pub struct Picker<T: Item> {
options: Vec<T>,
editor_data: T::Data,
// filter: String,
matcher: Box<Matcher>,
/// (index, score)
@ -301,15 +299,16 @@ pub struct Picker<T> {
previous_pattern: String,
/// Whether to truncate the start (default true)
pub truncate_start: bool,
/// Whether to show the preview panel (default true)
show_preview: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
}
impl<T> Picker<T> {
impl<T: Item> Picker<T> {
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
@ -321,6 +320,7 @@ impl<T> Picker<T> {
let mut picker = Self {
options,
editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
filters: Vec::new(),
@ -328,7 +328,7 @@ impl<T> Picker<T> {
prompt,
previous_pattern: String::new(),
truncate_start: true,
format_fn: Box::new(format_fn),
show_preview: true,
callback_fn: Box::new(callback_fn),
completion_height: 0,
};
@ -374,8 +374,7 @@ impl<T> Picker<T> {
#[allow(unstable_name_collisions)]
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index];
// TODO: maybe using format_fn isn't the best idea here
let text = (self.format_fn)(option);
let text = option.sort_text(&self.editor_data);
match self.matcher.fuzzy_match(&text, pattern) {
Some(s) => {
@ -403,8 +402,7 @@ impl<T> Picker<T> {
self.filters.binary_search(&index).ok()?;
}
// TODO: maybe using format_fn isn't the best idea here
let text = (self.format_fn)(option);
let text = option.filter_text(&self.editor_data);
self.matcher
.fuzzy_match(&text, pattern)
@ -474,6 +472,10 @@ impl<T> Picker<T> {
self.filters.sort_unstable(); // used for binary search later
self.prompt.clear(cx);
}
pub fn toggle_preview(&mut self) {
self.show_preview = !self.show_preview;
}
}
// process:
@ -481,7 +483,7 @@ impl<T> Picker<T> {
// - on input change:
// - score all the names in relation to input
impl<T: 'static> Component for Picker<T> {
impl<T: Item + 'static> Component for Picker<T> {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
self.completion_height = viewport.1.saturating_sub(4);
Some(viewport)
@ -494,12 +496,12 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored(None),
};
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _cx| {
// remove the layer
compositor.last_picker = compositor.pop();
})));
match key_event.into() {
match key_event {
shift!(Tab) | key!(Up) | ctrl!('p') => {
self.move_by(1, Direction::Backward);
}
@ -542,6 +544,9 @@ impl<T: 'static> Component for Picker<T> {
ctrl!(' ') => {
self.save_filter(cx);
}
ctrl!('t') => {
self.toggle_preview();
}
_ => {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
// TODO: recalculate only if pattern changed
@ -611,33 +616,46 @@ impl<T: 'static> Component for Picker<T> {
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
let is_active = i == (self.cursor - offset);
if is_active {
surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
surface.set_string(
inner.x.saturating_sub(3),
inner.y + i as u16,
" > ",
selected,
);
surface.set_style(
Rect::new(inner.x, inner.y + i as u16, inner.width, 1),
selected,
);
}
let formatted = (self.format_fn)(option);
let spans = option.label(&self.editor_data);
let (_score, highlights) = self
.matcher
.fuzzy_indices(&formatted, self.prompt.line())
.fuzzy_indices(&String::from(&spans), self.prompt.line())
.unwrap_or_default();
surface.set_string_truncated(
inner.x,
inner.y + i as u16,
&formatted,
inner.width as usize,
|idx| {
if highlights.contains(&idx) {
highlighted
} else if is_active {
selected
} else {
text_style
}
},
true,
self.truncate_start,
);
spans.0.into_iter().fold(inner, |pos, span| {
let new_x = surface
.set_string_truncated(
pos.x,
pos.y + i as u16,
&span.content,
pos.width as usize,
|idx| {
if highlights.contains(&idx) {
highlighted.patch(span.style)
} else if is_active {
selected.patch(span.style)
} else {
text_style.patch(span.style)
}
},
true,
self.truncate_start,
)
.0;
pos.clip_left(new_x - pos.x)
});
}
}

@ -1,8 +1,8 @@
use crate::{
compositor::{Callback, Component, Context, EventResult},
commands::Open,
compositor::{Callback, Component, Context, Event, EventResult},
ctrl, key,
};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
use helix_core::Position;
@ -17,8 +17,10 @@ pub struct Popup<T: Component> {
margin: Margin,
size: (u16, u16),
child_size: (u16, u16),
position_bias: Open,
scroll: usize,
auto_close: bool,
ignore_escape_key: bool,
id: &'static str,
}
@ -27,20 +29,29 @@ impl<T: Component> Popup<T> {
Self {
contents,
position: None,
margin: Margin {
vertical: 0,
horizontal: 0,
},
margin: Margin::none(),
size: (0, 0),
position_bias: Open::Below,
child_size: (0, 0),
scroll: 0,
auto_close: false,
ignore_escape_key: false,
id,
}
}
pub fn set_position(&mut self, pos: Option<Position>) {
pub fn position(mut self, pos: Option<Position>) -> Self {
self.position = pos;
self
}
pub fn get_position(&self) -> Option<Position> {
self.position
}
pub fn position_bias(mut self, bias: Open) -> Self {
self.position_bias = bias;
self
}
pub fn margin(mut self, margin: Margin) -> Self {
@ -53,6 +64,18 @@ impl<T: Component> Popup<T> {
self
}
/// Ignores an escape keypress event, letting the outer layer
/// (usually the editor) handle it. This is useful for popups
/// in insert mode like completion and signature help where
/// the popup is closed on the mode change from insert to normal
/// which is done with the escape key. Otherwise the popup consumes
/// the escape key event and closes it, and an additional escape
/// would be required to exit insert mode.
pub fn ignore_escape_key(mut self, ignore: bool) -> Self {
self.ignore_escape_key = ignore;
self
}
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
let position = self
.position
@ -71,13 +94,23 @@ impl<T: Component> Popup<T> {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
}
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if viewport.height > rel_y + height {
rel_y += 1 // position below point
} else {
rel_y = rel_y.saturating_sub(height) // position above point
}
let can_put_below = viewport.height > rel_y + height;
let can_put_above = rel_y.checked_sub(height).is_some();
let final_pos = match self.position_bias {
Open::Below => match can_put_below {
true => Open::Below,
false => Open::Above,
},
Open::Above => match can_put_above {
true => Open::Above,
false => Open::Below,
},
};
rel_y = match final_pos {
Open::Above => rel_y.saturating_sub(height),
Open::Below => rel_y + 1,
};
(rel_x, rel_y)
}
@ -115,12 +148,16 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored(None),
};
if key!(Esc) == key && self.ignore_escape_key {
return EventResult::Ignored(None);
}
let close_fn: Callback = Box::new(|compositor, _| {
// remove the layer
compositor.pop();
compositor.remove(self.id.as_ref());
});
match key.into() {
match key {
// esc or ctrl-c aborts the completion and closes the menu
key!(Esc) | ctrl!('c') => {
let _ = self.contents.handle_event(event, cx);
@ -163,8 +200,8 @@ impl<T: Component> Component for Popup<T> {
self.child_size = (width, height);
self.size = (
(width + self.margin.horizontal * 2).min(max_width),
(height + self.margin.vertical * 2).min(max_height),
(width + self.margin.width()).min(max_width),
(height + self.margin.height()).min(max_height),
);
// re-clamp scroll offset

@ -1,6 +1,5 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui};
use crossterm::event::Event;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::{borrow::Cow, ops::RangeFrom};
@ -84,6 +83,13 @@ impl Prompt {
}
}
pub fn with_line(mut self, line: String) -> Self {
let cursor = line.len();
self.line = line;
self.cursor = cursor;
self
}
pub fn line(&self) -> &String {
&self.line
}
@ -430,10 +436,7 @@ impl Prompt {
.borders(Borders::ALL)
.border_style(background);
let inner = block.inner(area).inner(&Margin {
vertical: 0,
horizontal: 1,
});
let inner = block.inner(area).inner(&Margin::horizontal(1));
block.render(area, surface);
text.render(inner, surface, cx);
@ -446,7 +449,7 @@ impl Prompt {
let input: Cow<str> = if self.line.is_empty() {
// latest value in the register list
self.history_register
.and_then(|reg| cx.editor.registers.first(reg).cloned()) // TODO: can we avoid cloning?
.and_then(|reg| cx.editor.registers.last(reg))
.map(|entry| entry.into())
.unwrap_or_else(|| Cow::from(""))
} else {
@ -475,19 +478,19 @@ impl Component for Prompt {
compositor.pop();
})));
match event.into() {
match event {
ctrl!('c') | key!(Esc) => {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn;
}
alt!('b') | alt!(Left) => self.move_cursor(Movement::BackwardWord(1)),
alt!('f') | alt!(Right) => self.move_cursor(Movement::ForwardWord(1)),
alt!('b') | ctrl!(Left) => self.move_cursor(Movement::BackwardWord(1)),
alt!('f') | ctrl!(Right) => self.move_cursor(Movement::ForwardWord(1)),
ctrl!('b') | key!(Left) => self.move_cursor(Movement::BackwardChar(1)),
ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
ctrl!('e') | key!(End) => self.move_end(),
ctrl!('a') | key!(Home) => self.move_start(),
ctrl!('w') => self.delete_word_backwards(cx),
alt!('d') => self.delete_word_forwards(cx),
ctrl!('w') | alt!(Backspace) | ctrl!(Backspace) => self.delete_word_backwards(cx),
alt!('d') | alt!(Delete) | ctrl!(Delete) => self.delete_word_forwards(cx),
ctrl!('k') => self.kill_to_end_of_line(cx),
ctrl!('u') => self.kill_to_start_of_line(cx),
ctrl!('h') | key!(Backspace) => {
@ -525,20 +528,21 @@ impl Component for Prompt {
let input: Cow<str> = if self.line.is_empty() {
// latest value in the register list
self.history_register
.and_then(|reg| cx.editor.registers.first(reg).cloned())
.and_then(|reg| cx.editor.registers.last(reg).cloned())
.map(|entry| entry.into())
.unwrap_or_else(|| Cow::from(""))
} else {
if let Some(register) = self.history_register {
// store in history
let register = cx.editor.registers.get_mut(register);
register.push(self.line.clone());
}
self.line.as_str().into()
};
(self.callback_fn)(cx, &input, PromptEvent::Validate);
if let Some(register) = self.history_register {
// store in history
let register = cx.editor.registers.get_mut(register);
register.push(self.line.clone());
}
return close_fn;
}
}

@ -0,0 +1,375 @@
use helix_core::{coords_at_pos, encoding, Position};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect,
theme::Style,
Document, Editor, View,
};
use crate::ui::ProgressSpinners;
use helix_view::editor::StatusLineElement as StatusLineElementID;
use tui::buffer::Buffer as Surface;
use tui::text::{Span, Spans};
pub struct RenderContext<'a> {
pub editor: &'a Editor,
pub doc: &'a Document,
pub view: &'a View,
pub focused: bool,
pub spinners: &'a ProgressSpinners,
pub parts: RenderBuffer<'a>,
}
impl<'a> RenderContext<'a> {
pub fn new(
editor: &'a Editor,
doc: &'a Document,
view: &'a View,
focused: bool,
spinners: &'a ProgressSpinners,
) -> Self {
RenderContext {
editor,
doc,
view,
focused,
spinners,
parts: RenderBuffer::default(),
}
}
}
#[derive(Default)]
pub struct RenderBuffer<'a> {
pub left: Spans<'a>,
pub center: Spans<'a>,
pub right: Spans<'a>,
}
pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) {
let base_style = if context.focused {
context.editor.theme.get("ui.statusline")
} else {
context.editor.theme.get("ui.statusline.inactive")
};
surface.set_style(viewport.with_height(1), base_style);
let write_left = |context: &mut RenderContext, text, style| {
append(&mut context.parts.left, text, &base_style, style)
};
let write_center = |context: &mut RenderContext, text, style| {
append(&mut context.parts.center, text, &base_style, style)
};
let write_right = |context: &mut RenderContext, text, style| {
append(&mut context.parts.right, text, &base_style, style)
};
// Left side of the status line.
let element_ids = &context.editor.config().statusline.left;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_left));
surface.set_spans(
viewport.x,
viewport.y,
&context.parts.left,
context.parts.left.width() as u16,
);
// Right side of the status line.
let element_ids = &context.editor.config().statusline.right;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_right));
surface.set_spans(
viewport.x
+ viewport
.width
.saturating_sub(context.parts.right.width() as u16),
viewport.y,
&context.parts.right,
context.parts.right.width() as u16,
);
// Center of the status line.
let element_ids = &context.editor.config().statusline.center;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_center));
// Width of the empty space between the left and center area and between the center and right area.
let spacing = 1u16;
let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16;
let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing);
let center_width = center_max_width.min(context.parts.center.width() as u16);
surface.set_spans(
viewport.x + viewport.width / 2 - center_width / 2,
viewport.y,
&context.parts.center,
center_width,
);
}
fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option<Style>) {
buffer.0.push(Span::styled(
text,
style.map_or(*base_style, |s| (*base_style).patch(s)),
));
}
fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
match element_id {
helix_view::editor::StatusLineElement::Mode => render_mode,
helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
helix_view::editor::StatusLineElement::FileName => render_file_name,
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type,
helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics,
helix_view::editor::StatusLineElement::Selections => render_selections,
helix_view::editor::StatusLineElement::Position => render_position,
helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage,
helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer,
}
}
fn render_mode<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let visible = context.focused;
write(
context,
format!(
" {} ",
if visible {
match context.doc.mode() {
Mode::Insert => "INS",
Mode::Select => "SEL",
Mode::Normal => "NOR",
}
} else {
// If not focused, explicitly leave an empty space instead of returning None.
" "
}
),
if visible && context.editor.config().color_modes {
match context.doc.mode() {
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
}
} else {
None
},
);
}
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
write(
context,
context
.doc
.language_server()
.and_then(|srv| {
context
.spinners
.get(srv.id())
.and_then(|spinner| spinner.frame())
})
// Even if there's no spinner; reserve its space to avoid elements frequently shifting.
.unwrap_or(" ")
.to_string(),
None,
);
}
fn render_diagnostics<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let (warnings, errors) = context
.doc
.diagnostics()
.iter()
.fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity;
match diag.severity {
Some(Severity::Warning) => counts.0 += 1,
Some(Severity::Error) | None => counts.1 += 1,
_ => {}
}
counts
});
if warnings > 0 {
write(
context,
"●".to_string(),
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
}
if errors > 0 {
write(
context,
"●".to_string(),
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
}
}
fn render_selections<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let count = context.doc.selection(context.view.id).len();
write(
context,
format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }),
None,
);
}
fn get_position(context: &RenderContext) -> Position {
coords_at_pos(
context.doc.text().slice(..),
context
.doc
.selection(context.view.id)
.primary()
.cursor(context.doc.text().slice(..)),
)
}
fn render_position<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let position = get_position(context);
write(
context,
format!(" {}:{} ", position.row + 1, position.col + 1),
None,
);
}
fn render_position_percentage<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let position = get_position(context);
let maxrows = context.doc.text().len_lines();
write(
context,
format!("{}%", (position.row + 1) * 100 / maxrows),
None,
);
}
fn render_file_encoding<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let enc = context.doc.encoding();
if enc != encoding::UTF_8 {
write(context, format!(" {} ", enc.name()), None);
}
}
fn render_file_line_ending<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
use helix_core::LineEnding::*;
let line_ending = match context.doc.line_ending {
Crlf => "CRLF",
LF => "LF",
#[cfg(feature = "unicode-lines")]
VT => "VT", // U+000B -- VerticalTab
#[cfg(feature = "unicode-lines")]
FF => "FF", // U+000C -- FormFeed
#[cfg(feature = "unicode-lines")]
CR => "CR", // U+000D -- CarriageReturn
#[cfg(feature = "unicode-lines")]
Nel => "NEL", // U+0085 -- NextLine
#[cfg(feature = "unicode-lines")]
LS => "LS", // U+2028 -- Line Separator
#[cfg(feature = "unicode-lines")]
PS => "PS", // U+2029 -- ParagraphSeparator
};
write(context, format!(" {} ", line_ending), None);
}
fn render_file_type<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let file_type = context.doc.language_id().unwrap_or("text");
write(context, format!(" {} ", file_type), None);
}
fn render_file_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = {
let rel_path = context.doc.relative_path();
let path = rel_path
.as_ref()
.map(|p| p.to_string_lossy())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
format!(
" {}{} ",
path,
if context.doc.is_modified() { "[+]" } else { "" }
)
};
write(context, title, None);
}
fn render_separator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let sep = &context.editor.config().statusline.separator;
write(
context,
sep.to_string(),
Some(context.editor.theme.get("ui.statusline.separator")),
);
}
fn render_spacer<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
write(context, String::from(" "), None);
}

@ -7,9 +7,11 @@ use crate::{
compositor::{Context, EventResult},
ctrl, key, shift,
};
use crossterm::event::{Event, KeyEvent};
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::Rect;
use helix_view::{
graphics::Rect,
input::{Event, KeyEvent},
};
use tui::{buffer::Buffer as Surface, text::Spans};
pub trait TreeItem: Sized {

@ -0,0 +1,25 @@
#[cfg(feature = "integration")]
mod test {
mod helpers;
use std::path::PathBuf;
use helix_core::{syntax::AutoPairConfig, Position, Selection};
use helix_term::{args::Args, config::Config};
use indoc::indoc;
use self::helpers::*;
#[tokio::test]
async fn hello_world() -> anyhow::Result<()> {
test(("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#")).await?;
Ok(())
}
mod auto_indent;
mod auto_pairs;
mod commands;
mod movement;
mod write;
}

@ -0,0 +1,26 @@
use super::*;
#[tokio::test]
async fn auto_indent_c() -> anyhow::Result<()> {
test_with_config(
Args {
files: vec![(PathBuf::from("foo.c"), Position::default())],
..Default::default()
},
Config::default(),
// switches to append mode?
(
helpers::platform_line("void foo() {#[|}]#").as_ref(),
"i<ret><esc>",
helpers::platform_line(indoc! {"\
void foo() {
#[|\n]#\
}
"})
.as_ref(),
),
)
.await?;
Ok(())
}

@ -0,0 +1,21 @@
use super::*;
#[tokio::test]
async fn auto_pairs_basic() -> anyhow::Result<()> {
test(("#[\n|]#", "i(<esc>", "(#[|)]#\n")).await?;
test_with_config(
Args::default(),
Config {
editor: helix_view::editor::Config {
auto_pairs: AutoPairConfig::Enable(false),
..Default::default()
},
..Default::default()
},
("#[\n|]#", "i(<esc>", "(#[|\n]#"),
)
.await?;
Ok(())
}

@ -0,0 +1,133 @@
use std::{
io::{Read, Write},
ops::RangeInclusive,
};
use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use super::*;
#[tokio::test]
async fn test_write_quit_fail() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
Some("ihello<esc>:wq<ret>"),
Some(&|app| {
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
}),
false,
)
.await?;
Ok(())
}
#[tokio::test]
#[ignore]
async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
test_key_sequences(
&mut Application::new(Args::default(), Config::default())?,
vec![
(
None,
Some(&|app| {
assert_eq!(1, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
(
Some("ihello<esc>:new<ret>"),
Some(&|app| {
assert_eq!(2, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
(
Some(":buffer<minus>close<ret>"),
Some(&|app| {
assert_eq!(1, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
],
false,
)
.await?;
// verify if writes are queued up, it finishes them before closing the buffer
let mut file = tempfile::NamedTempFile::new()?;
let mut command = String::new();
const RANGE: RangeInclusive<i32> = 1..=1000;
for i in RANGE {
let cmd = format!("%c{}<esc>:w<ret>", i);
command.push_str(&cmd);
}
command.push_str(":buffer<minus>close<ret>");
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
Some(&command),
Some(&|app| {
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
let doc = app.editor.document_by_path(file.path());
assert!(doc.is_none(), "found doc: {:?}", doc);
}),
false,
)
.await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(RANGE.end().to_string(), file_content);
Ok(())
}
#[tokio::test]
async fn test_selection_duplication() -> anyhow::Result<()> {
// Forward
test((
platform_line(indoc! {"\
#[lo|]#rem
ipsum
dolor
"})
.as_str(),
"CC",
platform_line(indoc! {"\
#(lo|)#rem
#(ip|)#sum
#[do|]#lor
"})
.as_str(),
))
.await?;
// Backward
test((
platform_line(indoc! {"\
#[|lo]#rem
ipsum
dolor
"})
.as_str(),
"CC",
platform_line(indoc! {"\
#(|lo)#rem
#(|ip)#sum
#[|do]#lor
"})
.as_str(),
))
.await?;
Ok(())
}

@ -0,0 +1,213 @@
use std::{io::Write, path::PathBuf, time::Duration};
use anyhow::bail;
use crossterm::event::{Event, KeyEvent};
use helix_core::{test, Selection, Transaction};
use helix_term::{application::Application, args::Args, config::Config};
use helix_view::{doc, input::parse_macro};
use tempfile::NamedTempFile;
use tokio_stream::wrappers::UnboundedReceiverStream;
#[derive(Clone, Debug)]
pub struct TestCase {
pub in_text: String,
pub in_selection: Selection,
pub in_keys: String,
pub out_text: String,
pub out_selection: Selection,
}
impl<S: Into<String>> From<(S, S, S)> for TestCase {
fn from((input, keys, output): (S, S, S)) -> Self {
let (in_text, in_selection) = test::print(&input.into());
let (out_text, out_selection) = test::print(&output.into());
TestCase {
in_text,
in_selection,
in_keys: keys.into(),
out_text,
out_selection,
}
}
}
#[inline]
pub async fn test_key_sequence(
app: &mut Application,
in_keys: Option<&str>,
test_fn: Option<&dyn Fn(&Application)>,
should_exit: bool,
) -> anyhow::Result<()> {
test_key_sequences(app, vec![(in_keys, test_fn)], should_exit).await
}
#[allow(clippy::type_complexity)]
pub async fn test_key_sequences(
app: &mut Application,
inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>,
should_exit: bool,
) -> anyhow::Result<()> {
const TIMEOUT: Duration = Duration::from_millis(500);
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut rx_stream = UnboundedReceiverStream::new(rx);
let num_inputs = inputs.len();
for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
if let Some(in_keys) = in_keys {
for key_event in parse_macro(in_keys)?.into_iter() {
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
}
}
let app_exited = !app.event_loop_until_idle(&mut rx_stream).await;
// the app should not exit from any test until the last one
if i < num_inputs - 1 && app_exited {
bail!("application exited before test function could run");
}
// verify if it exited on the last iteration if it should have and
// the inverse
if i == num_inputs - 1 && app_exited != should_exit {
bail!("expected app to exit: {} != {}", app_exited, should_exit);
}
if let Some(test) = test_fn {
test(app);
};
}
if !should_exit {
for key_event in parse_macro("<esc>:q!<ret>")?.into_iter() {
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
}
let event_loop = app.event_loop(&mut rx_stream);
tokio::time::timeout(TIMEOUT, event_loop).await?;
}
app.close().await?;
Ok(())
}
pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
app: Option<Application>,
test_case: T,
test_fn: &dyn Fn(&Application),
should_exit: bool,
) -> anyhow::Result<()> {
let test_case = test_case.into();
let mut app = match app {
Some(app) => app,
None => Application::new(Args::default(), Config::default())?,
};
let (view, doc) = helix_view::current!(app.editor);
let sel = doc.selection(view.id).clone();
// replace the initial text with the input text
doc.apply(
&Transaction::change_by_selection(doc.text(), &sel, |_| {
(0, doc.text().len_chars(), Some((&test_case.in_text).into()))
})
.with_selection(test_case.in_selection.clone()),
view.id,
);
test_key_sequence(
&mut app,
Some(&test_case.in_keys),
Some(test_fn),
should_exit,
)
.await
}
/// Use this for very simple test cases where there is one input
/// document, selection, and sequence of key presses, and you just
/// want to verify the resulting document and selection.
pub async fn test_with_config<T: Into<TestCase>>(
args: Args,
config: Config,
test_case: T,
) -> anyhow::Result<()> {
let test_case = test_case.into();
let app = Application::new(args, config)?;
test_key_sequence_with_input_text(
Some(app),
test_case.clone(),
&|app| {
let doc = doc!(app.editor);
assert_eq!(&test_case.out_text, doc.text());
let mut selections: Vec<_> = doc.selections().values().cloned().collect();
assert_eq!(1, selections.len());
let sel = selections.pop().unwrap();
assert_eq!(test_case.out_selection, sel);
},
false,
)
.await
}
pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
test_with_config(Args::default(), Config::default(), test_case).await
}
pub fn temp_file_with_contents<S: AsRef<str>>(
content: S,
) -> anyhow::Result<tempfile::NamedTempFile> {
let mut temp_file = tempfile::NamedTempFile::new()?;
temp_file
.as_file_mut()
.write_all(content.as_ref().as_bytes())?;
temp_file.flush()?;
temp_file.as_file_mut().sync_all()?;
Ok(temp_file)
}
/// Replaces all LF chars with the system's appropriate line feed
/// character, and if one doesn't exist already, appends the system's
/// appropriate line ending to the end of a string.
pub fn platform_line(input: &str) -> String {
let line_end = helix_core::DEFAULT_LINE_ENDING.as_str();
// we can assume that the source files in this code base will always
// be LF, so indoc strings will always insert LF
let mut output = input.replace('\n', line_end);
if !output.ends_with(line_end) {
output.push_str(line_end);
}
output
}
/// Creates a new temporary file that is set to read only. Useful for
/// testing write failures.
pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
let mut file = tempfile::NamedTempFile::new()?;
let metadata = file.as_file().metadata()?;
let mut perms = metadata.permissions();
perms.set_readonly(true);
file.as_file_mut().set_permissions(perms)?;
Ok(file)
}
/// Creates a new Application with default config that opens the given file
/// path
pub fn app_with_file<P: Into<PathBuf>>(path: P) -> anyhow::Result<Application> {
Application::new(
Args {
files: vec![(path.into(), helix_core::Position::default())],
..Default::default()
},
Config::default(),
)
}

@ -0,0 +1,87 @@
use super::*;
#[tokio::test]
async fn insert_mode_cursor_position() -> anyhow::Result<()> {
test(TestCase {
in_text: String::new(),
in_selection: Selection::single(0, 0),
in_keys: "i".into(),
out_text: String::new(),
out_selection: Selection::single(0, 0),
})
.await?;
test(("#[\n|]#", "i", "#[|\n]#")).await?;
test(("#[\n|]#", "i<esc>", "#[|\n]#")).await?;
test(("#[\n|]#", "i<esc>i", "#[|\n]#")).await?;
Ok(())
}
/// Range direction is preserved when escaping insert mode to normal
#[tokio::test]
async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
test(("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n")).await?;
test((
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
"vll<A-;><esc>",
indoc! {"\
#[|foo]#
#(|bar)#"
},
))
.await?;
test((
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
"a",
indoc! {"\
#[fo|]#o
#(ba|)#r"
},
))
.await?;
test((
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
"a<esc>",
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
))
.await?;
Ok(())
}
/// Ensure the very initial cursor in an opened file is the width of
/// the first grapheme
#[tokio::test]
async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
let file = helpers::temp_file_with_contents(content)?;
let mut app = helpers::app_with_file(file.path())?;
let (view, doc) = helix_view::current!(app.editor);
let sel = doc.selection(view.id).clone();
assert_eq!(expected_sel, sel);
Ok(())
};
test("foo", Selection::single(0, 1))?;
test("👨‍👩‍👧‍👦 foo", Selection::single(0, 7))?;
test("", Selection::single(0, 0))?;
Ok(())
}

@ -0,0 +1,169 @@
use std::{
io::{Read, Write},
ops::RangeInclusive,
};
use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use helix_view::doc;
use super::*;
#[tokio::test]
async fn test_write() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
None,
false,
)
.await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(
helpers::platform_line("the gostak distims the doshes"),
file_content
);
Ok(())
}
#[tokio::test]
async fn test_write_quit() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
None,
true,
)
.await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(
helpers::platform_line("the gostak distims the doshes"),
file_content
);
Ok(())
}
#[tokio::test]
#[ignore]
async fn test_write_concurrent() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut command = String::new();
const RANGE: RangeInclusive<i32> = 1..=5000;
for i in RANGE {
let cmd = format!("%c{}<esc>:w<ret>", i);
command.push_str(&cmd);
}
test_key_sequence(
&mut helpers::app_with_file(file.path())?,
Some(&command),
None,
false,
)
.await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(RANGE.end().to_string(), file_content);
Ok(())
}
#[tokio::test]
#[ignore]
async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
test_key_sequences(
&mut helpers::app_with_file(file.path())?,
vec![
(
None,
Some(&|app| {
let doc = doc!(app.editor);
assert!(!doc.is_modified());
}),
),
(
Some("ihello<esc>"),
Some(&|app| {
let doc = doc!(app.editor);
assert!(doc.is_modified());
}),
),
(
Some(":w<ret>"),
Some(&|app| {
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
let doc = doc!(app.editor);
assert!(doc.is_modified());
}),
),
],
false,
)
.await?;
Ok(())
}
#[tokio::test]
#[ignore]
async fn test_write_fail_new_path() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
test_key_sequences(
&mut Application::new(Args::default(), Config::default())?,
vec![
(
None,
Some(&|app| {
let doc = doc!(app.editor);
assert_ne!(
Some(&Severity::Error),
app.editor.get_status().map(|status| status.1)
);
assert_eq!(None, doc.path());
}),
),
(
Some(&format!(":w {}<ret>", file.path().to_string_lossy())),
Some(&|app| {
let doc = doc!(app.editor);
assert_eq!(
Some(&Severity::Error),
app.editor.get_status().map(|status| status.1)
);
assert_eq!(None, doc.path());
}),
),
],
false,
)
.await?;
Ok(())
}

@ -19,7 +19,7 @@ default = ["crossterm"]
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.9"
crossterm = { version = "0.23", optional = true }
crossterm = { version = "0.25", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.6", path = "../helix-core" }

@ -68,10 +68,7 @@ impl Default for Layout {
fn default() -> Layout {
Layout {
direction: Direction::Vertical,
margin: Margin {
horizontal: 0,
vertical: 0,
},
margin: Margin::none(),
constraints: Vec::new(),
}
}
@ -87,20 +84,19 @@ impl Layout {
}
pub fn margin(mut self, margin: u16) -> Layout {
self.margin = Margin {
horizontal: margin,
vertical: margin,
};
self.margin = Margin::all(margin);
self
}
pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
self.margin.horizontal = horizontal;
self.margin.left = horizontal;
self.margin.right = horizontal;
self
}
pub fn vertical_margin(mut self, vertical: u16) -> Layout {
self.margin.vertical = vertical;
self.margin.top = vertical;
self.margin.bottom = vertical;
self
}

@ -194,6 +194,12 @@ impl<'a> From<&'a str> for Span<'a> {
}
}
impl<'a> From<Cow<'a, str>> for Span<'a> {
fn from(s: Cow<'a, str>) -> Span<'a> {
Span::raw(s)
}
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
@ -229,6 +235,12 @@ impl<'a> From<&'a str> for Spans<'a> {
}
}
impl<'a> From<Cow<'a, str>> for Spans<'a> {
fn from(s: Cow<'a, str>) -> Spans<'a> {
Spans(vec![Span::raw(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
@ -243,10 +255,13 @@ impl<'a> From<Span<'a>> for Spans<'a> {
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
line.0.iter().map(|s| &*s.content).collect()
}
}
impl<'a> From<&Spans<'a>> for String {
fn from(line: &Spans<'a>) -> String {
line.0.iter().map(|s| &*s.content).collect()
}
}
@ -387,6 +402,12 @@ impl<'a> From<&'a str> for Text<'a> {
}
}
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {

@ -77,7 +77,7 @@ impl<'a> Block<'a> {
)]
pub fn title_style(mut self, style: Style) -> Block<'a> {
if let Some(t) = self.title {
let title = String::from(t);
let title = String::from(&t);
self.title = Some(Spans::from(Span::styled(title, style)));
}
self

@ -19,13 +19,13 @@ anyhow = "1"
helix-core = { version = "0.6", path = "../helix-core" }
helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.23", optional = true }
crossterm = { version = "0.25", optional = true }
# Conversion traits
once_cell = "1.12"
once_cell = "1.13"
url = "2"
arc-swap = { version = "1.5.0" }
arc-swap = { version = "1.5.1" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tokio-stream = "0.1"

@ -17,6 +17,10 @@ pub trait ClipboardProvider: std::fmt::Debug {
#[cfg(not(windows))]
macro_rules! command_provider {
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
log::info!(
"Using {} to interact with the system clipboard",
if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() }
);
Box::new(provider::command::Provider {
get_cmd: provider::command::Config {
prg: $get_prg,
@ -36,6 +40,10 @@ macro_rules! command_provider {
primary_paste => $pr_get_prg:literal $( , $pr_get_arg:literal )* ;
primary_copy => $pr_set_prg:literal $( , $pr_set_arg:literal )* ;
) => {{
log::info!(
"Using {} to interact with the system and selection (primary) clipboard",
if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() }
);
Box::new(provider::command::Provider {
get_cmd: provider::command::Config {
prg: $get_prg,
@ -131,7 +139,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
}
}
mod provider {
pub mod provider {
use super::{ClipboardProvider, ClipboardType};
use anyhow::Result;
use std::borrow::Cow;
@ -146,6 +154,9 @@ mod provider {
#[cfg(not(target_os = "windows"))]
impl NopProvider {
pub fn new() -> Self {
log::warn!(
"No clipboard provider found! Yanking and pasting will be internal to Helix"
);
Self {
buf: String::new(),
primary_buf: String::new(),
@ -153,6 +164,13 @@ mod provider {
}
}
#[cfg(not(target_os = "windows"))]
impl Default for NopProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(target_os = "windows"))]
impl ClipboardProvider for NopProvider {
fn name(&self) -> Cow<str> {
@ -184,6 +202,7 @@ mod provider {
#[cfg(target_os = "windows")]
impl ClipboardProvider for WindowsProvider {
fn name(&self) -> Cow<str> {
log::info!("Using clipboard-win to interact with the system clipboard");
Cow::Borrowed("clipboard-win")
}
@ -213,12 +232,11 @@ mod provider {
use super::*;
use anyhow::{bail, Context as _, Result};
#[cfg(not(windows))]
pub fn exists(executable_name: &str) -> bool {
which::which(executable_name).is_ok()
}
#[cfg(not(any(windows, target_os = "macos")))]
#[cfg(not(windows))]
pub fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}

@ -1,5 +1,8 @@
use anyhow::{anyhow, bail, Context, Error};
use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs;
use helix_core::Range;
use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize;
use std::cell::Cell;
@ -19,14 +22,13 @@ use helix_core::{
ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
DEFAULT_LINE_ENDING,
};
use helix_lsp::util::LspFormatting;
use crate::{DocumentId, Editor, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
const DEFAULT_INDENT: IndentStyle = IndentStyle::Tabs;
pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
@ -83,7 +85,7 @@ impl Serialize for Mode {
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
pub(crate) selections: HashMap<ViewId, Selection>,
selections: HashMap<ViewId, Selection>,
path: Option<PathBuf>,
encoding: &'static encoding::Encoding,
@ -396,7 +398,7 @@ impl Document {
/// The same as [`format`], but only returns formatting changes if auto-formatting
/// is configured.
pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
pub fn auto_format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
if self.language_config()?.auto_format {
self.format()
} else {
@ -406,7 +408,56 @@ impl Document {
/// If supported, returns the changes that should be applied to this document in order
/// to format it nicely.
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
// We can't use anyhow::Result here since the output of the future has to be
// clonable to be used as shared future. So use a custom error type.
pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> {
if let Some(formatter) = self.language_config().and_then(|c| c.formatter.clone()) {
use std::process::Stdio;
let text = self.text().clone();
let mut process = tokio::process::Command::new(&formatter.command);
process
.args(&formatter.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let formatting_future = async move {
let mut process = process
.spawn()
.map_err(|e| FormatterError::SpawningFailed {
command: formatter.command.clone(),
error: e.kind(),
})?;
{
let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?;
to_writer(&mut stdin, encoding::UTF_8, &text)
.await
.map_err(|_| FormatterError::BrokenStdin)?;
}
let output = process
.wait_with_output()
.await
.map_err(|_| FormatterError::WaitForOutputFailed)?;
if !output.stderr.is_empty() {
return Err(FormatterError::Stderr(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
if !output.status.success() {
return Err(FormatterError::NonZeroExitStatus);
}
let str = String::from_utf8(output.stdout)
.map_err(|_| FormatterError::InvalidUtf8Output)?;
Ok(helix_core::diff::compare_ropes(&text, &Rope::from(str)))
};
return Some(formatting_future.boxed());
};
let language_server = self.language_server()?;
let text = self.text.clone();
let offset_encoding = language_server.offset_encoding();
@ -426,13 +477,13 @@ impl Document {
log::warn!("LSP formatting failed: {}", e);
Default::default()
});
LspFormatting {
doc: text,
Ok(helix_lsp::util::generate_transaction_from_edits(
&text,
edits,
offset_encoding,
}
))
};
Some(fut)
Some(fut.boxed())
}
pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
@ -441,7 +492,7 @@ impl Document {
pub fn format_and_save(
&mut self,
formatting: Option<impl Future<Output = LspFormatting>>,
formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>,
force: bool,
) -> impl Future<Output = anyhow::Result<()>> {
self.save_impl(formatting, force)
@ -453,7 +504,7 @@ impl Document {
/// at its `path()`.
///
/// If `formatting` is present, it supplies some changes that we apply to the text before saving.
fn save_impl<F: Future<Output = LspFormatting>>(
fn save_impl<F: Future<Output = Result<Transaction, FormatterError>>>(
&mut self,
formatting: Option<F>,
force: bool,
@ -487,7 +538,8 @@ impl Document {
}
if let Some(fmt) = formatting {
let success = Transaction::from(fmt.await).changes().apply(&mut text);
let transaction = fmt.await?;
let success = transaction.changes().apply(&mut text);
if !success {
// This shouldn't happen, because the transaction changes were generated
// from the same text we're saving.
@ -524,9 +576,8 @@ impl Document {
}
/// Detect the indentation used in the file, or otherwise defaults to the language indentation
/// configured in `languages.toml`, with a fallback to 4 space indentation if it isn't
/// specified. Line ending is likewise auto-detected, and will fallback to the default OS
/// line ending.
/// configured in `languages.toml`, with a fallback to tabs if it isn't specified. Line ending
/// is likewise auto-detected, and will fallback to the default OS line ending.
pub fn detect_indent_and_line_ending(&mut self) {
self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
self.language_config()
@ -637,6 +688,37 @@ impl Document {
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
}
/// Find the origin selection of the text in a document, i.e. where
/// a single cursor would go if it were on the first grapheme. If
/// the text is empty, returns (0, 0).
pub fn origin(&self) -> Range {
if self.text().len_chars() == 0 {
return Range::new(0, 0);
}
Range::new(0, 1).grapheme_aligned(self.text().slice(..))
}
/// Reset the view's selection on this document to the
/// [origin](Document::origin) cursor.
pub fn reset_selection(&mut self, view_id: ViewId) {
let origin = self.origin();
self.set_selection(view_id, Selection::single(origin.anchor, origin.head));
}
/// Initializes a new selection for the given view if it does not
/// already have one.
pub fn ensure_view_init(&mut self, view_id: ViewId) {
if self.selections.get(&view_id).is_none() {
self.reset_selection(view_id);
}
}
/// Remove a view's selection from this document.
pub fn remove_view(&mut self, view_id: ViewId) {
self.selections.remove(&view_id);
}
/// Apply a [`Transaction`] to the [`Document`] to change its text.
fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
let old_doc = self.text().clone();
@ -1002,6 +1084,38 @@ impl Default for Document {
}
}
#[derive(Clone, Debug)]
pub enum FormatterError {
SpawningFailed {
command: String,
error: std::io::ErrorKind,
},
BrokenStdin,
WaitForOutputFailed,
Stderr(String),
InvalidUtf8Output,
DiskReloadError(String),
NonZeroExitStatus,
}
impl std::error::Error for FormatterError {}
impl Display for FormatterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SpawningFailed { command, error } => {
write!(f, "Failed to spawn formatter {}: {:?}", command, error)
}
Self::BrokenStdin => write!(f, "Could not write to formatter stdin"),
Self::WaitForOutputFailed => write!(f, "Waiting for formatter output failed"),
Self::Stderr(output) => write!(f, "Formatter error: {}", output),
Self::InvalidUtf8Output => write!(f, "Invalid UTF-8 formatter output"),
Self::DiskReloadError(error) => write!(f, "Error reloading file from disk: {}", error),
Self::NonZeroExitStatus => write!(f, "Formatter exited with non zero exit status:"),
}
}
}
#[cfg(test)]
mod test {
use super::*;

@ -32,13 +32,14 @@ use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
use helix_core::Position;
use helix_core::{
auto_pairs::AutoPairs,
syntax::{self, AutoPairConfig},
Change,
};
use helix_core::{Position, Selection};
use helix_dap as dap;
use helix_lsp::lsp;
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
@ -184,6 +185,8 @@ pub struct Config {
pub shell: Vec<String>,
/// Line number mode.
pub line_number: LineNumber,
/// Highlight the lines cursors are currently on. Defaults to false.
pub cursorline: bool,
/// Gutters. Default ["diagnostics", "line-numbers"]
pub gutters: Vec<GutterType>,
/// Middle click paste support. Defaults to true.
@ -207,6 +210,8 @@ pub struct Config {
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
pub file_picker: FilePickerConfig,
/// Configuration of the statusline elements
pub statusline: StatusLineConfig,
/// Shape for cursor in each mode
pub cursor_shape: CursorShapeConfig,
/// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
@ -215,18 +220,84 @@ pub struct Config {
#[serde(default)]
pub search: SearchConfig,
pub lsp: LspConfig,
pub terminal: Option<TerminalConfig>,
/// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
pub rulers: Vec<u16>,
#[serde(default)]
pub whitespace: WhitespaceConfig,
/// Vertical indent width guides.
pub indent_guides: IndentGuidesConfig,
/// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool,
/// explore config
pub explorer: ExplorerConfig,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct TerminalConfig {
pub command: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
}
#[cfg(windows)]
pub fn get_terminal_provider() -> Option<TerminalConfig> {
use crate::clipboard::provider::command::exists;
if exists("wt") {
return Some(TerminalConfig {
command: "wt".to_string(),
args: vec![
"new-tab".to_string(),
"--title".to_string(),
"DEBUG".to_string(),
"cmd".to_string(),
"/C".to_string(),
],
});
}
return Some(TerminalConfig {
command: "conhost".to_string(),
args: vec!["cmd".to_string(), "/C".to_string()],
});
}
#[cfg(not(any(windows, target_os = "wasm32")))]
pub fn get_terminal_provider() -> Option<TerminalConfig> {
use crate::clipboard::provider::command::{env_var_is_set, exists};
if env_var_is_set("TMUX") && exists("tmux") {
return Some(TerminalConfig {
command: "tmux".to_string(),
args: vec!["split-window".to_string()],
});
}
None
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct LspConfig {
/// Display LSP progress messages below statusline
pub display_messages: bool,
/// Enable automatic pop up of signature help (parameter hints)
pub auto_signature_help: bool,
/// Display docs under signature help popup
pub display_signature_help_docs: bool,
}
impl Default for LspConfig {
fn default() -> Self {
Self {
display_messages: false,
auto_signature_help: true,
display_signature_help_docs: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -238,6 +309,68 @@ pub struct SearchConfig {
pub wrap_around: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct StatusLineConfig {
pub left: Vec<StatusLineElement>,
pub center: Vec<StatusLineElement>,
pub right: Vec<StatusLineElement>,
pub separator: String,
}
impl Default for StatusLineConfig {
fn default() -> Self {
use StatusLineElement as E;
Self {
left: vec![E::Mode, E::Spinner, E::FileName],
center: vec![],
right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding],
separator: String::from("│"),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StatusLineElement {
/// The editor mode (Normal, Insert, Visual/Selection)
Mode,
/// The LSP activity spinner
Spinner,
/// The file nane/path, including a dirty flag if it's unsaved
FileName,
/// The file encoding
FileEncoding,
/// The file line endings (CRLF or LF)
FileLineEnding,
/// The file type (language ID or "text")
FileType,
/// A summary of the number of errors and warnings
Diagnostics,
/// The number of selections (cursors)
Selections,
/// The cursor position
Position,
/// The separator string
Separator,
/// The cursor position as a percent of the total file
PositionPercentage,
/// A single space
Spacer,
}
// Cursor shape is read and used on every rendered frame and so needs
// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
#[derive(Debug, Clone, PartialEq)]
@ -322,6 +455,8 @@ pub enum GutterType {
Diagnostics,
/// Show line numbers
LineNumbers,
/// Show one blank space
Spacer,
}
impl std::str::FromStr for GutterType {
@ -415,6 +550,7 @@ pub struct WhitespaceCharacters {
pub space: char,
pub nbsp: char,
pub tab: char,
pub tabpad: char,
pub newline: char,
}
@ -425,6 +561,23 @@ impl Default for WhitespaceCharacters {
nbsp: '', // U+237D
tab: '', // U+2192
newline: '', // U+23CE
tabpad: ' ',
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct IndentGuidesConfig {
pub render: bool,
pub character: char,
}
impl Default for IndentGuidesConfig {
fn default() -> Self {
Self {
render: false,
character: '',
}
}
}
@ -441,6 +594,7 @@ impl Default for Config {
vec!["sh".to_owned(), "-c".to_owned()]
},
line_number: LineNumber::Absolute,
cursorline: false,
gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
@ -450,12 +604,16 @@ impl Default for Config {
completion_trigger_len: 2,
auto_info: true,
file_picker: FilePickerConfig::default(),
statusline: StatusLineConfig::default(),
cursor_shape: CursorShapeConfig::default(),
true_color: false,
search: SearchConfig::default(),
lsp: LspConfig::default(),
terminal: get_terminal_provider(),
rulers: Vec::new(),
whitespace: WhitespaceConfig::default(),
indent_guides: IndentGuidesConfig::default(),
color_modes: false,
explorer: ExplorerConfig::default(),
}
}
@ -504,8 +662,8 @@ pub struct Editor {
pub registers: Registers,
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub macro_replaying: Vec<char>,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
pub debugger: Option<dap::Client>,
pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@ -515,6 +673,12 @@ pub struct Editor {
pub syn_loader: Arc<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>,
/// last_theme is used for theme previews. We store the current theme here,
/// and if previewing is cancelled, we can return to it.
pub last_theme: Option<Theme>,
/// The currently applied editor theme. While previewing a theme, the previewed theme
/// is set here.
pub theme: Theme,
pub status_msg: Option<(Cow<'static, str>, Severity)>,
pub autoinfo: Option<Info>,
@ -539,6 +703,11 @@ pub enum ConfigEvent {
Update(Box<Config>),
}
enum ThemeAction {
Set,
Preview,
}
#[derive(Debug, Clone)]
pub struct CompleteAction {
pub trigger_offset: usize,
@ -560,7 +729,6 @@ impl Editor {
syn_loader: Arc<syntax::Loader>,
config: Box<dyn DynAccess<Config>>,
) -> Self {
let language_servers = helix_lsp::Registry::new();
let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into();
@ -576,12 +744,14 @@ impl Editor {
macro_recording: None,
macro_replaying: Vec::new(),
theme: theme_loader.default(),
language_servers,
language_servers: helix_lsp::Registry::new(),
diagnostics: BTreeMap::new(),
debugger: None,
debugger_events: SelectAll::new(),
breakpoints: HashMap::new(),
syn_loader,
theme_loader,
last_theme: None,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
@ -637,7 +807,36 @@ impl Editor {
self.status_msg = Some((error.into(), Severity::Error));
}
#[inline]
pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> {
self.status_msg.as_ref().map(|(status, sev)| (status, sev))
}
/// Returns true if the current status is an error
#[inline]
pub fn is_err(&self) -> bool {
self.status_msg
.as_ref()
.map(|(_, sev)| *sev == Severity::Error)
.unwrap_or(false)
}
pub fn unset_theme_preview(&mut self) {
if let Some(last_theme) = self.last_theme.take() {
self.set_theme(last_theme);
}
// None likely occurs when the user types ":theme" and then exits before previewing
}
pub fn set_theme_preview(&mut self, theme: Theme) {
self.set_theme_impl(theme, ThemeAction::Preview);
}
pub fn set_theme(&mut self, theme: Theme) {
self.set_theme_impl(theme, ThemeAction::Set);
}
fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
// `ui.selection` is the only scope required to be able to render a theme.
if theme.find_scope_index("ui.selection").is_none() {
self.set_error("Invalid theme: `ui.selection` required");
@ -647,7 +846,18 @@ impl Editor {
let scopes = theme.scopes();
self.syn_loader.set_scopes(scopes.to_vec());
self.theme = theme;
match preview {
ThemeAction::Preview => {
let last_theme = std::mem::replace(&mut self.theme, theme);
// only insert on first preview: this will be the last theme the user has saved
self.last_theme.get_or_insert(last_theme);
}
ThemeAction::Set => {
self.last_theme = None;
self.theme = theme;
}
}
self._refresh();
}
@ -711,11 +921,8 @@ impl Editor {
view.offset = Position::default();
let doc = self.documents.get_mut(&doc_id).unwrap();
doc.ensure_view_init(view.id);
// initialize selection for view
doc.selections
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = doc
.selection(view.id)
@ -785,13 +992,17 @@ impl Editor {
Action::Load => {
let view_id = view!(self).id;
let doc = self.documents.get_mut(&id).unwrap();
if doc.selections().is_empty() {
doc.set_selection(view_id, Selection::point(0));
}
doc.ensure_view_init(view_id);
return;
}
Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id, self.config().gutters.clone());
// copy the current view, unless there is no view yet
let view = self
.tree
.try_get(self.tree.focus)
.filter(|v| id == v.doc) // Different Document
.cloned()
.unwrap_or_else(|| View::new(id, self.config().gutters.clone()));
let view_id = self.tree.split(
view,
match action {
@ -802,7 +1013,7 @@ impl Editor {
);
// initialize selection for view
let doc = self.documents.get_mut(&id).unwrap();
doc.set_selection(view_id, Selection::point(0));
doc.ensure_view_init(view_id);
}
}
@ -835,8 +1046,9 @@ impl Editor {
Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
}
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?;
// ??? possible use for integration tests
pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(path)?;
let id = self.document_by_path(&path).map(|doc| doc.id);
let id = if let Some(id) = id {
@ -856,12 +1068,7 @@ impl Editor {
pub fn close(&mut self, id: ViewId) {
let view = self.tree.get(self.tree.focus);
// remove selection
self.documents
.get_mut(&view.doc)
.unwrap()
.selections
.remove(&id);
self.documents.get_mut(&view.doc).unwrap().remove_view(id);
self.tree.remove(id);
self._refresh();
}
@ -936,7 +1143,7 @@ impl Editor {
let view = View::new(doc_id, self.config().gutters.clone());
let view_id = self.tree.insert(view);
let doc = self.documents.get_mut(&doc_id).unwrap();
doc.set_selection(view_id, Selection::point(0));
doc.ensure_view_init(view_id);
}
self._refresh();
@ -950,40 +1157,37 @@ impl Editor {
};
}
pub fn focus_next(&mut self) {
self.tree.focus_next();
}
pub fn focus_right(&mut self) {
self.tree.focus_direction(tree::Direction::Right);
}
pub fn focus_left(&mut self) {
self.tree.focus_direction(tree::Direction::Left);
}
pub fn focus_up(&mut self) {
self.tree.focus_direction(tree::Direction::Up);
}
pub fn focus(&mut self, view_id: ViewId) {
let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
pub fn focus_down(&mut self) {
self.tree.focus_direction(tree::Direction::Down);
// if leaving the view: mode should reset
if prev_id != view_id {
let doc_id = self.tree.get(prev_id).doc;
self.documents.get_mut(&doc_id).unwrap().mode = Mode::Normal;
}
}
pub fn swap_right(&mut self) {
self.tree.swap_split_in_direction(tree::Direction::Right);
}
pub fn focus_next(&mut self) {
let prev_id = self.tree.focus;
self.tree.focus_next();
let id = self.tree.focus;
pub fn swap_left(&mut self) {
self.tree.swap_split_in_direction(tree::Direction::Left);
// if leaving the view: mode should reset
if prev_id != id {
let doc_id = self.tree.get(prev_id).doc;
self.documents.get_mut(&doc_id).unwrap().mode = Mode::Normal;
}
}
pub fn swap_up(&mut self) {
self.tree.swap_split_in_direction(tree::Direction::Up);
pub fn focus_direction(&mut self, direction: tree::Direction) {
let current_view = self.tree.focus;
if let Some(id) = self.tree.find_split_in_direction(current_view, direction) {
self.focus(id)
}
}
pub fn swap_down(&mut self) {
self.tree.swap_split_in_direction(tree::Direction::Down);
pub fn swap_split_in_direction(&mut self, direction: tree::Direction) {
self.tree.swap_split_in_direction(direction);
}
pub fn transpose_view(&mut self) {

File diff suppressed because it is too large Load Diff

@ -102,6 +102,17 @@ pub fn line_numbers<'doc>(
})
}
pub fn padding<'doc>(
_editor: &'doc Editor,
_doc: &'doc Document,
_view: &View,
_theme: &Theme,
_is_focused: bool,
_width: usize,
) -> GutterFn<'doc> {
Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
}
#[inline(always)]
const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {

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

Loading…
Cancel
Save