mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
253 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1bda454149 | ||
|
e819121f6e | ||
|
f33aaba53f | ||
|
9cfa163370 | ||
|
6b8c6ed535 | ||
|
e4b3a666d2 | ||
|
43e3173231 | ||
|
8c1edd22af | ||
|
9b352ceefd | ||
|
f882ea92b6 | ||
|
98485524c8 | ||
|
f47da891db | ||
|
5d23667a26 | ||
|
b6e363ef0e | ||
|
ca02024199 | ||
|
ae5ecfdf66 | ||
|
d545e61644 | ||
|
df217f71c1 | ||
|
d008e86037 | ||
|
b9100fbd44 | ||
|
52d3c29244 | ||
|
17c9a8499e | ||
|
62e6232a32 | ||
|
d8b5d1181f | ||
|
b500a2a138 | ||
|
a3f01503e2 | ||
|
9640ed1425 | ||
|
9baf1ecc90 | ||
|
44cc0d8eb0 | ||
|
1953588873 | ||
|
45793d7c09 | ||
|
4b6aff8c66 | ||
|
4a40e935de | ||
|
716067ba05 | ||
|
c754df12b3 | ||
|
1bf5b103b0 | ||
|
1665bac1b6 | ||
|
278361a086 | ||
|
69fe46a122 | ||
|
86af55c379 | ||
|
0c2b99327a | ||
|
a8a5bcd13d | ||
|
098806ce2a | ||
|
c0d32707d0 | ||
|
d8df10f295 | ||
|
38073fd64c | ||
|
01760c3845 | ||
|
8590f6a912 | ||
|
69378382c3 | ||
|
1a774d61bb | ||
|
1b14e9a19a | ||
|
e46346c907 | ||
|
9887b1275a | ||
|
7cc13fefe9 | ||
|
1a3a924634 | ||
|
aebdef8257 | ||
|
6b3c9d8ed3 | ||
|
4dbc23ff1c | ||
|
b20e4a108c | ||
|
1bb9977faf | ||
|
29962a5bd9 | ||
|
7ef0e2cab6 | ||
|
35feb614b6 | ||
|
5e2ba28e0e | ||
|
83723957fe | ||
|
ae51065213 | ||
|
4e3a343602 | ||
|
81e02e1ba4 | ||
|
c349ceb61f | ||
|
2e4a338944 | ||
|
9c83a98469 | ||
|
1bffb34350 | ||
|
c978d811d9 | ||
|
48df05b16d | ||
|
b873fb9897 | ||
|
8f1eb7b2b0 | ||
|
82fdfdc38e | ||
|
ea6667070f | ||
|
960bc9f134 | ||
|
08f50310bd | ||
|
4bec87ad18 | ||
|
c65b4dea09 | ||
|
6fc0e0b5fb | ||
|
0201ef9205 | ||
|
037f45f24e | ||
|
9821beb5c4 | ||
|
3cee0bf200 | ||
|
4fd38f82a3 | ||
|
b5682f984b | ||
|
68affa3c59 | ||
|
d5de9183ef | ||
|
8d6fad4cac | ||
|
14830e75ff | ||
|
59f94d13b8 | ||
|
b3eeac7bbf | ||
|
f48a60b8e2 | ||
|
4f561e93b8 | ||
|
01b1bd15a1 | ||
|
ff8a031cb2 | ||
|
d9b2f6feac | ||
|
582f1ee9d8 | ||
|
e2d780f993 | ||
|
843c2cdebd | ||
|
8a29086c1a | ||
|
16b1cfa3be | ||
|
2066e866c7 | ||
|
3494bb8ef0 | ||
|
a4ff8cdd8a | ||
|
145bc1970a | ||
|
54f3548d54 | ||
|
3280510d5b | ||
|
df80f3c966 | ||
|
40744ce835 | ||
|
aa8a8baeeb | ||
|
bcb1afeb4c | ||
|
de946d2357 | ||
|
14f511da93 | ||
|
392631b21d | ||
|
ce99ecc7a2 | ||
|
2ac496f919 | ||
|
5463a436a8 | ||
|
e09b0f4eff | ||
|
f3db12e240 | ||
|
676719b361 | ||
|
ae105812d6 | ||
|
255598a2cb | ||
|
62d181de78 | ||
|
8c2fa12ffc | ||
|
212f6bc372 | ||
|
c5c3ec07f4 | ||
|
444cd0b068 | ||
|
f6a900fee1 | ||
|
6254720f53 | ||
|
407b37c327 | ||
|
2bb71a829e | ||
|
c17dcb8633 | ||
|
5a344a3ae5 | ||
|
a1f4b8f92b | ||
|
72eaaaac99 | ||
|
8f78c0c612 | ||
|
01dd7b570a | ||
|
f3a243c6cb | ||
|
adcfcf9044 | ||
|
4f0e3aa948 | ||
|
f2e554d761 | ||
|
bd4552cd2b | ||
|
06d8d3f55f | ||
|
8afd4e1bc2 | ||
|
43b92b24d2 | ||
|
b2b2d430ae | ||
|
8af5a9a5cf | ||
|
f76f44c8af | ||
|
d55419604c | ||
|
29b9eed33c | ||
|
fdb5bfafae | ||
|
e6132f0acd | ||
|
3071339cbc | ||
|
27aee705e0 | ||
|
f0fe558f38 | ||
|
09a7db637e | ||
|
31ed4db153 | ||
|
3c5dfb0633 | ||
|
6cbc0aea92 | ||
|
c1c3750d38 | ||
|
daad8ebe12 | ||
|
68abc67ec6 | ||
|
712f25c2b9 | ||
|
abe8a83d8e | ||
|
a05fb95769 | ||
|
74e4ac8d49 | ||
|
0e6f007028 | ||
|
c3a98b6a3e | ||
|
4fe654cf9a | ||
|
661dbdca57 | ||
|
5773bd6a40 | ||
|
d664d1dec0 | ||
|
7e8603247d | ||
|
7140908f6e | ||
|
6dba1e7ec7 | ||
|
c0332bd935 | ||
|
3c7729906c | ||
|
1b67fae9f4 | ||
|
f0018280cb | ||
|
7202953e69 | ||
|
7761c88d61 | ||
|
68f5031dcc | ||
|
83031564db | ||
|
eab6e53511 | ||
|
f5f46b1fed | ||
|
5f49bafbe8 | ||
|
2719a35123 | ||
|
0a6672c626 | ||
|
b51111a364 | ||
|
78980f575b | ||
|
0bb375bafa | ||
|
c960bcfc24 | ||
|
e88383d990 | ||
|
312b29f712 | ||
|
f4560cb68a | ||
|
cbb3ebafdc | ||
|
0851110d10 | ||
|
3ace581191 | ||
|
c0264b9f7f | ||
|
22dad592b8 | ||
|
ca042a4bde | ||
|
67b1cd32c7 | ||
|
4d12c7c3cf | ||
|
4f56a8e248 | ||
|
dbc392d92c | ||
|
7967d312c0 | ||
|
db48d22384 | ||
|
533ff61d0e | ||
|
b1ce969d80 | ||
|
01bf363446 | ||
|
cc323f7665 | ||
|
60caaf7fc4 | ||
|
ea824ed05d | ||
|
cfae07e7ba | ||
|
56dbc60840 | ||
|
c2e6b9f506 | ||
|
8fd8006043 | ||
|
ce25aa951e | ||
|
a2147fc7d5 | ||
|
d8e16554bf | ||
|
2cc30cd07c | ||
|
0dde5f2cae | ||
|
b8d6e6ad28 | ||
|
5825bce0e4 | ||
|
aeabfc55a8 | ||
|
17e9386388 | ||
|
138787f76e | ||
|
1132c5122f | ||
|
4a8053e832 | ||
|
e033a4b8ac | ||
|
acbcd758bd | ||
|
76eed4caad | ||
|
3170c49be8 | ||
|
0327d66653 | ||
|
c67e31830d | ||
|
6460501a44 | ||
|
67b037050f | ||
|
87d0617f3b | ||
|
668f735232 | ||
|
a3a9502596 | ||
|
3810650a6b | ||
|
2c48d65b15 | ||
|
d5466eddf5 | ||
|
d54ae09d3b | ||
|
a28eaa81a0 | ||
|
d708efe275 | ||
|
3336023614 | ||
|
094203c74e | ||
|
b114cfa119 |
5
.envrc
5
.envrc
@@ -1,2 +1,5 @@
|
||||
watch_file shell.nix
|
||||
use flake
|
||||
watch_file flake.lock
|
||||
|
||||
# try to use flakes, if it fails use normal nix (ie. shell.nix)
|
||||
use flake || use nix
|
||||
|
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: C-bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Your issue may already be reported!
|
||||
Please search on the issue tracker before creating one. -->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
<!-- Ideally provide a key sequence and/or asciinema.org recording. -->
|
||||
|
||||
### Environment
|
||||
|
||||
- Platform: <!-- macOS / Windows / Linux -->
|
||||
- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
|
||||
|
||||
<details><summary>~/.cache/helix/helix.log</summary>
|
||||
|
||||
```
|
||||
please provide a copy of `~/.cache/helix/helix.log` here if possible, you may need to redact some of the lines
|
||||
```
|
||||
|
||||
</details>
|
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: Build
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '00 01 * * *'
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -25,19 +25,19 @@ jobs:
|
||||
override: true
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo target dir
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
@@ -60,23 +60,23 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo target dir
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
@@ -86,6 +86,11 @@ jobs:
|
||||
with:
|
||||
command: test
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
rust: [stable]
|
||||
|
||||
lints:
|
||||
name: Lints
|
||||
runs-on: ubuntu-latest
|
||||
@@ -104,19 +109,19 @@ jobs:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo target dir
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
27
.github/workflows/gh-pages.yml
vendored
Normal file
27
.github/workflows/gh-pages.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Github Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v1
|
||||
with:
|
||||
mdbook-version: 'latest'
|
||||
# mdbook-version: '0.4.8'
|
||||
|
||||
- run: mdbook build book
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: github.ref == 'refs/heads/master'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./book/book
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -37,6 +37,10 @@ jobs:
|
||||
rust: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
cross: false
|
||||
# - build: aarch64-macos
|
||||
# os: macos-latest
|
||||
# rust: stable
|
||||
# target: aarch64-apple-darwin
|
||||
# - build: x86_64-win-gnu
|
||||
# os: windows-2019
|
||||
# rust: stable-x86_64-gnu
|
||||
@@ -96,7 +100,7 @@ jobs:
|
||||
cp "target/${{ matrix.target }}/release/hx" "dist/"
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v1
|
||||
- uses: actions/upload-artifact@v2.2.3
|
||||
with:
|
||||
name: bins-${{ matrix.build }}
|
||||
path: dist
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ target
|
||||
.direnv
|
||||
helix-term/rustfmt.toml
|
||||
helix-syntax/languages/
|
||||
result
|
||||
|
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -82,3 +82,10 @@
|
||||
path = helix-syntax/languages/tree-sitter-toml
|
||||
url = https://github.com/ikatyang/tree-sitter-toml
|
||||
shallow = true
|
||||
[submodule "helix-syntax/languages/tree-sitter-elixir"]
|
||||
path = helix-syntax/languages/tree-sitter-elixir
|
||||
url = https://github.com/IceDragon200/tree-sitter-elixir
|
||||
shallow = true
|
||||
[submodule "helix-syntax/languages/tree-sitter-nix"]
|
||||
path = helix-syntax/languages/tree-sitter-nix
|
||||
url = https://github.com/cstrahan/tree-sitter-nix
|
||||
|
222
Cargo.lock
generated
222
Cargo.lock
generated
@@ -13,9 +13,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.40"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
|
||||
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -52,9 +52,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.67"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
|
||||
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
@@ -77,54 +77,38 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.0.0-beta.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"os_str_bytes",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
|
||||
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
|
||||
checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"futures-core",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9"
|
||||
checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
@@ -244,26 +228,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
|
||||
checksum = "f0fc1b9fa0e64ffb1aa5b95daa0f0f167734fd528b7c02eabc581d9d843649b1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
@@ -272,56 +250,47 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||
|
||||
[[package]]
|
||||
name = "helix-core"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"etcetera",
|
||||
"helix-syntax",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"ropey",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"tendril",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"unicode-general-category",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "helix-lsp"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"glob",
|
||||
"helix-core",
|
||||
"jsonrpc-core",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"once_cell",
|
||||
"pathdiff",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "helix-syntax"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"serde",
|
||||
@@ -331,11 +300,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-term"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"dirs-next",
|
||||
"fern",
|
||||
@@ -358,7 +326,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-tui"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
@@ -370,13 +338,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-view"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossterm",
|
||||
"helix-core",
|
||||
"helix-lsp",
|
||||
"helix-tui",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"slotmap",
|
||||
@@ -407,9 +376,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.17"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
|
||||
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"globset",
|
||||
@@ -423,16 +392,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.9"
|
||||
@@ -460,7 +419,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "jsonrpc-core"
|
||||
version = "17.1.0"
|
||||
source = "git+https://github.com/paritytech/jsonrpc#609d7a6cc160742d035510fa89fb424ccf077660"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4467ab6dfa369b69e52bd0692e480c4d117410538526a57a304a0f2250fd95e"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -477,9 +437,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.94"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
|
||||
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -501,9 +461,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.89.0"
|
||||
version = "0.89.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07731ecd4ee0654728359a5b95e2a254c857876c04b85225496a35d60345daa7"
|
||||
checksum = "852e0dedfd52cc32325598b2631e0eba31b7b708959676a9f837042f276b09a2"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"serde",
|
||||
@@ -598,15 +558,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.7.2"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -633,12 +587,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
@@ -659,9 +607,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.26"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
||||
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
@@ -731,6 +679,39 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "5.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "5.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "5.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d"
|
||||
dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
@@ -796,20 +777,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.17"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
|
||||
checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.3.0"
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
|
||||
checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -857,29 +848,20 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.24"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
|
||||
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.24"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
|
||||
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -921,9 +903,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.6.0"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37"
|
||||
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -972,9 +954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.19.3"
|
||||
version = "0.19.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f41201fed3db3b520405a9c01c61773a250d4c3f43e9861c14b2bb232c981ab"
|
||||
checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -999,10 +981,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.17"
|
||||
name = "unicode-general-category"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
|
||||
checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
@@ -1044,12 +1032,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.3"
|
||||
|
45
README.md
45
README.md
@@ -1,7 +1,7 @@
|
||||
# Helix
|
||||
|
||||
|
||||
[](https://github.com/helix-editor/helix/actions)
|
||||
[](https://github.com/helix-editor/helix/actions)
|
||||
|
||||

|
||||
|
||||
@@ -10,7 +10,10 @@ A kakoune / neovim inspired editor, written in Rust.
|
||||
The editing model is very heavily based on kakoune; during development I found
|
||||
myself agreeing with most of kakoune's design decisions.
|
||||
|
||||
For more information, see the [website](https://helix-editor.com).
|
||||
For more information, see the [website](https://helix-editor.com) or
|
||||
[documentation](https://docs.helix-editor.com/).
|
||||
|
||||
All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html)
|
||||
|
||||
# Features
|
||||
|
||||
@@ -24,7 +27,8 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
|
||||
|
||||
# Installation
|
||||
|
||||
Note: Only Rust and Golang have indentation definitions at the moment.
|
||||
Note: Only certain languages have indentation definitions at the moment. Check
|
||||
`runtime/<lang>/` for `indents.toml`.
|
||||
|
||||
We provide packaging for various distributions, but here's a quick method to
|
||||
build from source.
|
||||
@@ -37,10 +41,33 @@ cargo install --path helix-term
|
||||
|
||||
This will install the `hx` binary to `$HOME/.cargo/bin`.
|
||||
|
||||
Now copy the `runtime/` directory somewhere. Helix will by default look for the
|
||||
runtime inside the same folder as the executable, but that can be overriden via
|
||||
the `HELIX_RUNTIME` environment variable.
|
||||
Now copy the `runtime/` directory somewhere. Helix will by default look for the runtime
|
||||
inside the config directory or the same directory as executable, but that can be overriden
|
||||
via the `HELIX_RUNTIME` environment variable.
|
||||
|
||||
> NOTE: running via cargo doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
|
||||
> detect the `runtime` directory in the project root.
|
||||
|
||||
If you want to embed the `runtime/` directory into the Helix binary you can build
|
||||
it with:
|
||||
|
||||
```
|
||||
cargo install --path helix-term --features "embed_runtime"
|
||||
```
|
||||
|
||||
## Arch Linux
|
||||
There are two packages available from AUR:
|
||||
- `helix-bin`: contains prebuilt binary from GitHub releases
|
||||
- `helix-git`: builds the master branch of this repository
|
||||
|
||||
## MacOS
|
||||
Helix can be installed on MacOS through homebrew via:
|
||||
|
||||
```
|
||||
brew tap helix-editor/helix
|
||||
brew install helix
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
||||
@@ -49,6 +76,9 @@ Some suggestions to get started:
|
||||
|
||||
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/good%20first%20issue) label on the issue tracker.
|
||||
- Help with packaging on various distributions needed!
|
||||
- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
|
||||
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
|
||||
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
|
||||
- If your preferred language is missing, integrating a tree-sitter grammar for
|
||||
it and defining syntax highlight queries for it is straight forward and
|
||||
doesn't require much knowledge of the internals.
|
||||
@@ -58,5 +88,4 @@ a good overview of the internals.
|
||||
|
||||
# Getting help
|
||||
|
||||
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-editor:matrix.org).
|
||||
|
||||
Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet).
|
||||
|
5
TODO.md
5
TODO.md
@@ -2,6 +2,8 @@
|
||||
|
||||
------
|
||||
|
||||
as you type completion!
|
||||
|
||||
- tree sitter:
|
||||
- lua
|
||||
- markdown
|
||||
@@ -18,6 +20,9 @@
|
||||
- [ ] document.on_type provider triggers
|
||||
- [ ] completion isIncomplete support
|
||||
|
||||
- [ ] scroll wheel support
|
||||
- [ ] matching bracket highlight
|
||||
|
||||
1
|
||||
- [ ] respect view fullscreen flag
|
||||
- [ ] Implement marks (superset of Selection/Range)
|
||||
|
@@ -4,3 +4,6 @@ language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
theme = "colibri"
|
||||
|
||||
[output.html]
|
||||
cname = "docs.helix-editor.com"
|
||||
|
@@ -1 +1,88 @@
|
||||
# Configuration
|
||||
|
||||
## Theme
|
||||
|
||||
Use a custom theme by placing a theme.toml in your config directory (i.e ~/.config/helix/theme.toml). The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/contrib/themes).
|
||||
|
||||
Styles in theme.toml are specified of in the form:
|
||||
|
||||
```toml
|
||||
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
|
||||
```
|
||||
|
||||
where `name` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
|
||||
|
||||
To specify only the foreground color:
|
||||
|
||||
```toml
|
||||
key = "#ffffff"
|
||||
```
|
||||
|
||||
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
|
||||
|
||||
```toml
|
||||
"key.key" = "#ffffff"
|
||||
```
|
||||
|
||||
Possible modifiers:
|
||||
|
||||
| modifier |
|
||||
| --- |
|
||||
| bold |
|
||||
| dim |
|
||||
| italic |
|
||||
| underlined |
|
||||
| slow\_blink |
|
||||
| rapid\_blink |
|
||||
| reversed |
|
||||
| hidden |
|
||||
| crossed\_out |
|
||||
|
||||
Possible keys:
|
||||
|
||||
| key | notes |
|
||||
| --- | --- |
|
||||
| attribute | |
|
||||
| keyword | |
|
||||
| keyword.directive | preprocessor directives (\#if in C) |
|
||||
| namespace | |
|
||||
| punctuation | |
|
||||
| punctuation.delimiter | |
|
||||
| operator | |
|
||||
| special | |
|
||||
| property | |
|
||||
| variable | |
|
||||
| variable.parameter | |
|
||||
| type | |
|
||||
| type.builtin | |
|
||||
| constructor | |
|
||||
| function | |
|
||||
| function.macro | |
|
||||
| function.builtin | |
|
||||
| comment | |
|
||||
| variable.builtin | |
|
||||
| constant | |
|
||||
| constant.builtin | |
|
||||
| string | |
|
||||
| number | |
|
||||
| escape | escaped characters |
|
||||
| label | used for lifetimes |
|
||||
| module | |
|
||||
| ui.background | |
|
||||
| ui.linenr | |
|
||||
| ui.statusline | |
|
||||
| ui.popup | |
|
||||
| ui.window | |
|
||||
| ui.help | |
|
||||
| ui.text | |
|
||||
| ui.text.focus | |
|
||||
| ui.menu.selected | |
|
||||
| ui.selection | for selections in the editing area |
|
||||
| warning | LSP warning |
|
||||
| error | LSP error |
|
||||
| info | LSP info |
|
||||
| hint | LSP hint |
|
||||
|
||||
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
|
||||
|
||||
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.
|
||||
|
@@ -6,10 +6,7 @@ We provide pre-built binaries on the [GitHub Releases page](https://github.com/h
|
||||
|
||||
TODO: brew tap
|
||||
|
||||
```
|
||||
$ brew tap helix-editor/helix
|
||||
$ brew install helix
|
||||
```
|
||||
Please use a pre-built binary release for the time being.
|
||||
|
||||
## Linux
|
||||
|
||||
@@ -21,7 +18,9 @@ shell for working on Helix.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
TODO: AUR
|
||||
Binary packages are available on AUR:
|
||||
- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
|
||||
- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
|
||||
|
||||
## Build from source
|
||||
|
||||
@@ -36,3 +35,10 @@ This will install the `hx` binary to `$HOME/.cargo/bin`.
|
||||
Now copy the `runtime/` directory somewhere. Helix will by default look for the
|
||||
runtime inside the same folder as the executable, but that can be overriden via
|
||||
the `HELIX_RUNTIME` environment variable.
|
||||
|
||||
If you want to embed the `runtime/` directory into the Helix binary you can build
|
||||
it with:
|
||||
|
||||
```
|
||||
cargo install --path helix-term --features "embed_runtime"
|
||||
```
|
||||
|
@@ -6,10 +6,10 @@
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| h | move left |
|
||||
| j | move down |
|
||||
| k | move up |
|
||||
| l | move right |
|
||||
| h, Left | move left |
|
||||
| j, Down | move down |
|
||||
| k, Up | move up |
|
||||
| l, Right | move right |
|
||||
| w | move next word start |
|
||||
| b | move previous word start |
|
||||
| e | move next word end |
|
||||
@@ -17,20 +17,20 @@
|
||||
| f | find next char |
|
||||
| T | find 'till previous char |
|
||||
| F | find previous char |
|
||||
| ^ | move to the start of the line |
|
||||
| $ | move to the end of the line |
|
||||
| m | Jump to matching bracket |
|
||||
| Home | move to the start of the line |
|
||||
| End | move to the end of the line |
|
||||
| m | Jump to matching bracket |
|
||||
| PageUp | Move page up |
|
||||
| PageDown | Move page down |
|
||||
| ctrl-u | Move half page up |
|
||||
| ctrl-d | Move half page down |
|
||||
| Tab | Switch to next view |
|
||||
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab |
|
||||
| ctrl-o | Jump backward on the jumplist |
|
||||
| v | Enter select (extend) mode |
|
||||
| g | Enter goto mode |
|
||||
| : | Enter command mode |
|
||||
| z | Enter view mode |
|
||||
| ctrl-w | Enter window mode (maybe will be remove for spc w w later) |
|
||||
| space | Enter space mode |
|
||||
| K | Show documentation for the item under the cursor |
|
||||
|
||||
@@ -38,13 +38,14 @@
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| r | replace (single character change) |
|
||||
| r | replace with a character |
|
||||
| R | replace with yanked text |
|
||||
| i | Insert before selection |
|
||||
| a | Insert after selection (append) |
|
||||
| I | Insert at the start of the line |
|
||||
| A | Insert at the end of the line |
|
||||
| o | Open new line below selection |
|
||||
| o | Open new line above selection |
|
||||
| I | Insert at the start of the line |
|
||||
| A | Insert at the end of the line |
|
||||
| o | Open new line below selection |
|
||||
| o | Open new line above selection |
|
||||
| u | Undo change |
|
||||
| U | Redo change |
|
||||
| y | Yank selection |
|
||||
@@ -53,26 +54,26 @@
|
||||
| > | Indent selection |
|
||||
| < | Unindent selection |
|
||||
| = | Format selection |
|
||||
| d | Delete selection |
|
||||
| c | Change selection (delete and enter insert mode) |
|
||||
| d | Delete selection |
|
||||
| c | Change selection (delete and enter insert mode) |
|
||||
|
||||
### Selection manipulation
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| s | Select all regex matches inside selections |
|
||||
| S | Split selection into subselections on regex matches |
|
||||
| alt-s | Split selection on newlines |
|
||||
| ; | Collapse selection onto a single cursor |
|
||||
| alt-; | Flip selection cursor and anchor |
|
||||
| % | Select entire file |
|
||||
| x | Select current line |
|
||||
| X | Extend to next line |
|
||||
| [ | Expand selection to parent syntax node TODO: pick a key |
|
||||
| s | Select all regex matches inside selections |
|
||||
| S | Split selection into subselections on regex matches |
|
||||
| alt-s | Split selection on newlines |
|
||||
| ; | Collapse selection onto a single cursor |
|
||||
| alt-; | Flip selection cursor and anchor |
|
||||
| % | Select entire file |
|
||||
| x | Select current line |
|
||||
| X | Extend to next line |
|
||||
| [ | Expand selection to parent syntax node TODO: pick a key |
|
||||
| J | join lines inside selection |
|
||||
| K | keep selections matching the regex TODO: overlapped by hover help |
|
||||
| space | keep only the primary selection TODO: overlapped by space mode |
|
||||
| ctrl-c | Comment/uncomment the selections |
|
||||
| ctrl-c | Comment/uncomment the selections |
|
||||
|
||||
### Search
|
||||
|
||||
@@ -81,10 +82,22 @@ in reverse, or searching via smartcase.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| / | Search for regex pattern |
|
||||
| n | Select next search match |
|
||||
| N | Add next search match to selection |
|
||||
| * | Use current selection as the search pattern |
|
||||
| / | Search for regex pattern |
|
||||
| n | Select next search match |
|
||||
| N | Add next search match to selection |
|
||||
| * | Use current selection as the search pattern |
|
||||
|
||||
### Diagnostics
|
||||
|
||||
> NOTE: `[` and `]` will likely contain more pair mappings in the style of
|
||||
> [vim-unimpaired](https://github.com/tpope/vim-unimpaired)
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| [d | Go to previous diagnostic |
|
||||
| ]d | Go to next diagnostic |
|
||||
| [D | Go to first diagnostic in document |
|
||||
| ]D | Go to last diagnostic in document |
|
||||
|
||||
## Select / extend mode
|
||||
|
||||
@@ -118,15 +131,33 @@ Jumps to various locations.
|
||||
|-----|-----------|
|
||||
| g | Go to the start of the file |
|
||||
| e | Go to the end of the file |
|
||||
| e | Go to definition |
|
||||
| t | Go to type definition |
|
||||
| h | Go to the start of the line |
|
||||
| l | Go to the end of the line |
|
||||
| s | Go to first non-whitespace character of the line |
|
||||
| t | Go to the top of the screen |
|
||||
| m | Go to the middle of the screen |
|
||||
| b | Go to the bottom of the screen |
|
||||
| d | Go to definition |
|
||||
| y | Go to type definition |
|
||||
| r | Go to references |
|
||||
| i | Go to implementation |
|
||||
| a | Go to the last accessed/alternate file |
|
||||
|
||||
## Object mode
|
||||
|
||||
TODO: Mappings for selecting syntax nodes (a superset of `[`).
|
||||
|
||||
## Window mode
|
||||
|
||||
This layer is similar to vim keybindings as kakoune does not support window.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| w, ctrl-w | Switch to next window |
|
||||
| v, ctrl-v | Vertical right split |
|
||||
| h, ctrl-h | Horizontal bottom split |
|
||||
| q, ctrl-q | Close current window |
|
||||
|
||||
## Space mode
|
||||
|
||||
This layer is a kludge of mappings I had under leader key in neovim.
|
||||
@@ -135,7 +166,6 @@ This layer is a kludge of mappings I had under leader key in neovim.
|
||||
|-----|-----------|
|
||||
| f | Open file picker |
|
||||
| b | Open buffer picker |
|
||||
| v | Open a new vertical split into the current file |
|
||||
| w | Save changes to file |
|
||||
| c | Close the current split |
|
||||
| s | Open symbol picker (current document)|
|
||||
| w | Enter window mode |
|
||||
| space | Keep primary selection TODO: it's here because space mode replaced it |
|
||||
|
@@ -109,7 +109,7 @@ h6:target::before {
|
||||
margin-top: 1.275em;
|
||||
margin-bottom: .875em;
|
||||
}
|
||||
.content p, .content ol, .content ul, .content table, .content blockquote {
|
||||
.content p, .content ol, .content ul, .content table {
|
||||
margin-top: 0;
|
||||
margin-bottom: .875em;
|
||||
}
|
||||
@@ -123,8 +123,7 @@ h6:target::before {
|
||||
.content .header:link,
|
||||
.content .header:visited {
|
||||
color: var(--fg);
|
||||
/* color: white; */
|
||||
color: #281733;
|
||||
color: var(--heading-fg);
|
||||
}
|
||||
.content .header:link,
|
||||
.content .header:visited:hover {
|
||||
@@ -168,12 +167,15 @@ table tbody tr:nth-child(2n) {
|
||||
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem 1.5rem;
|
||||
color: var(--fg);
|
||||
opacity: .9;
|
||||
background-color: var(--quote-bg);
|
||||
border-top: .1em solid var(--quote-border);
|
||||
border-bottom: .1em solid var(--quote-border);
|
||||
border-left: 4px solid var(--quote-border);
|
||||
}
|
||||
blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -13,6 +13,7 @@
|
||||
.ayu {
|
||||
--bg: hsl(210, 25%, 8%);
|
||||
--fg: #c5c5c5;
|
||||
--heading-fg: #c5c5c5;
|
||||
|
||||
--sidebar-bg: #14191f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
@@ -53,6 +54,7 @@
|
||||
.coal {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
--heading-fg: #98a3ad;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
@@ -93,6 +95,7 @@
|
||||
.light {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
--heading-fg: hsl(0, 0%, 0%);
|
||||
|
||||
--sidebar-bg: #fafafa;
|
||||
--sidebar-fg: hsl(0, 0%, 0%);
|
||||
@@ -133,6 +136,7 @@
|
||||
.navy {
|
||||
--bg: hsl(226, 23%, 11%);
|
||||
--fg: #bcbdd0;
|
||||
--heading-fg: #bcbdd0;
|
||||
|
||||
--sidebar-bg: #282d3f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
@@ -173,6 +177,7 @@
|
||||
.rust {
|
||||
--bg: hsl(60, 9%, 87%);
|
||||
--fg: #262625;
|
||||
--heading-fg: #262625;
|
||||
|
||||
--sidebar-bg: #3b2e2a;
|
||||
--sidebar-fg: #c8c9db;
|
||||
@@ -214,6 +219,7 @@
|
||||
.light.no-js {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #ebeafa;
|
||||
--heading-fg: #ebeafa;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
@@ -297,6 +303,7 @@
|
||||
--bg: #ffffff;
|
||||
--fg: #452859;
|
||||
--fg: #5a5977;
|
||||
--heading-fg: #281733;
|
||||
|
||||
--sidebar-bg: #281733;
|
||||
--sidebar-fg: #c8c9db;
|
||||
@@ -317,8 +324,8 @@
|
||||
--theme-popup-border: #737480;
|
||||
--theme-hover: rgba(0,0,0, .2);
|
||||
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
--quote-bg: rgba(0, 0, 0, 0);
|
||||
--quote-border: hsl(226, 15%, 75%);
|
||||
|
||||
--table-border-color: #5a5977;
|
||||
--table-border-color: hsl(201deg 10% 67%);
|
||||
|
9
contrib/themes/README.md
Normal file
9
contrib/themes/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# User submitted themes
|
||||
|
||||
If you submit a theme, please include a comment at the top with your name and email address:
|
||||
|
||||
```toml
|
||||
# Author : Name <email@my.domain>
|
||||
```
|
||||
|
||||
We have a preview page for themes on our [wiki](https://github.com/helix-editor/helix/wiki/Themes)!
|
47
contrib/themes/bogster.toml
Normal file
47
contrib/themes/bogster.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Author : Wojciech Kępka <wojciech@wkepka.dev>
|
||||
|
||||
"attribute" = "#dc7759"
|
||||
"keyword" = { fg = "#dcb659", modifiers = ["bold"] }
|
||||
"keyword.directive" = "#dcb659"
|
||||
"namespace" = "#d32c5d"
|
||||
"punctuation" = "#dc7759"
|
||||
"punctuation.delimiter" = "#dc7759"
|
||||
"operator" = { fg = "#dc7759", modifiers = ["bold"] }
|
||||
"special" = "#7fdc59"
|
||||
"property" = "#c6b8ad"
|
||||
"variable" = "#c6b8ad"
|
||||
"variable.parameter" = "#c6b8ad"
|
||||
"type" = "#dc597f"
|
||||
"type.builtin" = { fg = "#d32c5d", modifiers = ["bold"] }
|
||||
"constructor" = "#dc597f"
|
||||
"function" = "#59dcd8"
|
||||
"function.macro" = { fg = "#dc7759", modifiers = ["bold"] }
|
||||
"function.builtin" = { fg = "#59dcd8", modifiers = ["bold"] }
|
||||
"comment" = "#627d9d"
|
||||
"variable.builtin" = "#c6b8ad"
|
||||
"constant" = "#59dcb7"
|
||||
"constant.builtin" = "#59dcb7"
|
||||
"string" = "#59dcb7"
|
||||
"number" = "#59c0dc"
|
||||
"escape" = { fg = "#7fdc59", modifiers = ["bold"] }
|
||||
"label" = "#59c0dc"
|
||||
|
||||
"module" = "#d32c5d"
|
||||
|
||||
"ui.background" = { bg = "#161c23" }
|
||||
"ui.linenr" = { fg = "#415367" }
|
||||
"ui.statusline" = { bg = "#232d38" }
|
||||
"ui.popup" = { bg = "#232d38" }
|
||||
"ui.window" = { bg = "#232d38" }
|
||||
"ui.help" = { bg = "#232d38", fg = "#e5ded6" }
|
||||
|
||||
"ui.text" = { fg = "#e5ded6" }
|
||||
"ui.text.focus" = { fg = "#e5ded6", modifiers= ["bold"] }
|
||||
|
||||
"ui.selection" = { bg = "#540099" }
|
||||
"ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" }
|
||||
|
||||
"warning" = "#dc7759"
|
||||
"error" = "#dc597f"
|
||||
"info" = "#59dcb7"
|
||||
"hint" = "#59c0dc"
|
47
contrib/themes/ingrid.toml
Normal file
47
contrib/themes/ingrid.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Author : Ingrid Rebecca Abraham <git@ingrids.email>
|
||||
|
||||
"attribute" = "#839A53"
|
||||
"keyword" = { fg = "#D74E50", modifiers = ["bold"] }
|
||||
"keyword.directive" = "#6F873E"
|
||||
"namespace" = "#839A53"
|
||||
"punctuation" = "#C97270"
|
||||
"punctuation.delimiter" = "#C97270"
|
||||
"operator" = { fg = "#D74E50", modifiers = ["bold"] }
|
||||
"special" = "#D68482"
|
||||
"property" = "#89BEB7"
|
||||
"variable" = "#A6B6CE"
|
||||
"variable.parameter" = "#89BEB7"
|
||||
"type" = { fg = "#A6B6CE", modifiers = ["bold"] }
|
||||
"type.builtin" = "#839A53"
|
||||
"constructor" = { fg = "#839A53", modifiers = ["bold"] }
|
||||
"function" = { fg = "#89BEB7", modifiers = ["bold"] }
|
||||
"function.macro" = { fg = "#D4A520", modifiers = ["bold"] }
|
||||
"function.builtin" = "#89BEB7"
|
||||
"comment" = "#A6B6CE"
|
||||
"variable.builtin" = "#D4A520"
|
||||
"constant" = "#D4A520"
|
||||
"constant.builtin" = "#D4A520"
|
||||
"string" = "#D74E50"
|
||||
"number" = "#D74E50"
|
||||
"escape" = { fg = "#D74E50", modifiers = ["bold"] }
|
||||
"label" = "#D68482"
|
||||
|
||||
"module" = "#839A53"
|
||||
|
||||
"ui.background" = { bg = "#FFFCFD" }
|
||||
"ui.linenr" = { fg = "#bbbbbb" }
|
||||
"ui.statusline" = { bg = "#F3EAE9" }
|
||||
"ui.popup" = { bg = "#F3EAE9" }
|
||||
"ui.window" = { bg = "#D8B8B3" }
|
||||
"ui.help" = { bg = "#D8B8B3", fg = "#250E07" }
|
||||
|
||||
"ui.text" = { fg = "#7B91B3" }
|
||||
"ui.text.focus" = { fg = "#250E07", modifiers= ["bold"] }
|
||||
|
||||
"ui.selection" = { bg = "#540099" }
|
||||
"ui.menu.selected" = { fg = "#D74E50", bg = "#F3EAE9" }
|
||||
|
||||
"warning" = "#D4A520"
|
||||
"error" = "#D74E50"
|
||||
"info" = "#839A53"
|
||||
"hint" = "#A6B6CE"
|
41
contrib/themes/onedark.toml
Normal file
41
contrib/themes/onedark.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
# Author : Gokul Soumya <gokulps15@gmail.com>
|
||||
|
||||
"attribute" = { fg = "#E5C07B" }
|
||||
"comment" = { fg = "#5C6370", modifiers = ['italic'] }
|
||||
"constant" = { fg = "#56B6C2" }
|
||||
"constant.builtin" = { fg = "#61AFEF" }
|
||||
"constructor" = { fg = "#61AFEF" }
|
||||
"escape" = { fg = "#D19A66" }
|
||||
"function" = { fg = "#61AFEF" }
|
||||
"function.builtin" = { fg = "#61AFEF" }
|
||||
"function.macro" = { fg = "#C678DD" }
|
||||
"keyword" = { fg = "#E06C75" }
|
||||
"keyword.directive" = { fg = "#C678DD" }
|
||||
"label" = { fg = "#C678DD" }
|
||||
"namespace" = { fg = "#61AFEF" }
|
||||
"number" = { fg = "#D19A66" }
|
||||
"operator" = { fg = "#C678DD" }
|
||||
"property" = { fg = "#E06C75" }
|
||||
"special" = { fg = "#61AFEF" }
|
||||
"string" = { fg = "#98C379" }
|
||||
"type" = { fg = "#E5C07B" }
|
||||
"type.builtin" = { fg = "#E5C07B" }
|
||||
"variable" = { fg = "#61AFEF" }
|
||||
"variable.builtin" = { fg = "#61AFEF" }
|
||||
"variable.parameter" = { fg = "#E06C75" }
|
||||
|
||||
"info" = { fg = "#61afef", modifiers = ['bold'] }
|
||||
"hint" = { fg = "#98c379", modifiers = ['bold'] }
|
||||
"warning" = { fg = "#e5c07b", modifiers = ['bold'] }
|
||||
"error" = { fg = "#e06c75", modifiers = ['bold'] }
|
||||
|
||||
"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
|
||||
"ui.background" = { fg = "#ABB2BF", bg = "#282C34" }
|
||||
"ui.help" = { bg = "#3E4452" }
|
||||
"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] }
|
||||
"ui.popup" = { bg = "#3E4452" }
|
||||
"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
|
||||
"ui.selection" = { bg = "#3E4452" }
|
||||
"ui.text" = { fg = "#ABB2BF", bg = "#282C34" }
|
||||
"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
|
||||
"ui.window" = { bg = "#3E4452" }
|
@@ -8,68 +8,78 @@
|
||||
| helix-term | Terminal UI |
|
||||
| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
|
||||
|
||||
# Notes
|
||||
|
||||
- server-client architecture via gRPC, UI separate from core
|
||||
- multi cursor based editing and slicing
|
||||
- WASM based plugins (builtin LSP & fuzzy file finder)
|
||||
This document contains a high-level overview of Helix internals.
|
||||
|
||||
Structure similar to codemirror:
|
||||
> NOTE: Use `cargo doc --open` for API documentation as well as dependency
|
||||
> documentation.
|
||||
|
||||
- text (ropes)
|
||||
- transactions
|
||||
- changes
|
||||
- invert changes (generates a revert)
|
||||
- annotations (time changed etc)
|
||||
- state effects
|
||||
- additional editor state as facets
|
||||
- snapshots as an async view into current state
|
||||
- selections { anchor (nonmoving), head (moving) from/to } -> SelectionSet with a primary
|
||||
- cursor is just a single range selection
|
||||
- markers
|
||||
track a position inside text that synchronizes with edits
|
||||
- { doc, selection, update(), splice, changes(), facets, tabSize, identUnit, lineSeparator, changeFilter/transactionFilter to modify stuff before }
|
||||
- view (actual UI)
|
||||
- viewport(Lines) -> what's actually visible
|
||||
- extend the view via Decorations (inline styling) or Components (UI)
|
||||
- mark / wieget / line / replace decoration
|
||||
- commands (transform state)
|
||||
- movement
|
||||
- selection extension
|
||||
- deletion
|
||||
- indentation
|
||||
- keymap (maps keys to commands)
|
||||
- history (undo tree via immutable ropes)
|
||||
- undoes transactions via reverts
|
||||
- (collab mode)
|
||||
- gutter (line numbers, diagnostic marker, etc) -> ties into UI components
|
||||
- rangeset/span -> mappable over changes (can be a marker primitive?)
|
||||
- syntax (treesitter)
|
||||
- fold
|
||||
- selections (select mode/multiselect)
|
||||
- matchbrackets
|
||||
- closebrackets
|
||||
- special-chars (shows dots etc for specials)
|
||||
- panel (for UI: file pickers, search dialogs, etc)
|
||||
- tooltip (for UI)
|
||||
- search (regex)
|
||||
- lint (async linters)
|
||||
- lsp
|
||||
- highlight
|
||||
- stream-syntax
|
||||
- autocomplete
|
||||
- comment (gc, etc for auto commenting)
|
||||
- snippets
|
||||
- terminal mode?
|
||||
## Core
|
||||
|
||||
- plugins can contain more commands/ui abstractions to use elsewhere
|
||||
- languageData as presets for each language (syntax, indent, comment, etc)
|
||||
The core contains basic building blocks used to construct the editor. It is
|
||||
heavily based on [CodeMirror 6](https://codemirror.net/6/docs/). The primitives
|
||||
are functional: most operations won't modify data in place but instead return
|
||||
a new copy.
|
||||
|
||||
Vim stuff:
|
||||
- motions/operators/text objects
|
||||
- full visual mode
|
||||
- macros
|
||||
- jump lists
|
||||
- marks
|
||||
- yank/paste
|
||||
- conceal for markdown markers, etc
|
||||
The main data structure used for representing buffers is a `Rope`. We re-export
|
||||
the excellent [ropey](https://github.com/cessen/ropey) library. Ropes are cheap
|
||||
to clone, and allow us to easily make snapshots of a text state.
|
||||
|
||||
Multiple selections are a core editing primitive. Document selections are
|
||||
represented by a `Selection`. Each `Range` in the selection consists of a moving
|
||||
`head` and an immovable `anchor`. A single cursor in the editor is simply
|
||||
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
|
||||
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
|
||||
applying the transaction.
|
||||
|
||||
> NOTE: `Transaction::change`/`Transaction::change_by_selection` is the main
|
||||
> interface used to generate text edits.
|
||||
|
||||
`Syntax` is the interface used to interact with tree-sitter ASTs for syntax
|
||||
highling and other features.
|
||||
|
||||
## View
|
||||
|
||||
The `view` layer was supposed to be a frontend-agnostic imperative library that
|
||||
would build on top of `core` to provide the common editor logic. Currently it's
|
||||
tied to the terminal UI.
|
||||
|
||||
A `Document` ties together the `Rope`, `Selection`(s), `Syntax`, document
|
||||
`History`, language server (etc.) into a comprehensive representation of an open
|
||||
file.
|
||||
|
||||
A `View` represents an open split in the UI. It holds the currently open
|
||||
document ID and other related state.
|
||||
|
||||
> NOTE: Multiple views are able to display the same document, so the document
|
||||
> contains selections for each view. To retrieve, `document.selection()` takes
|
||||
> a `ViewId`.
|
||||
|
||||
The `Editor` holds the global state: all the open documents, a tree
|
||||
representation of all the view splits, and a registry of language servers. To
|
||||
open or close files, interact with the editor.
|
||||
|
||||
## LSP
|
||||
|
||||
A language server protocol client.
|
||||
|
||||
## Term
|
||||
|
||||
The terminal frontend.
|
||||
|
||||
The `main` function sets up a new `Application` that runs the event loop.
|
||||
|
||||
`commands.rs` is probably the most interesting file. It contains all commands
|
||||
(actions tied to keybindings).
|
||||
|
||||
`keymap.rs` links commands to key combinations.
|
||||
|
||||
|
||||
## TUI / Term
|
||||
|
||||
TODO: document Component and rendering related stuff
|
||||
|
124
flake.lock
generated
124
flake.lock
generated
@@ -1,73 +1,83 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"devshell": {
|
||||
"locked": {
|
||||
"lastModified": 1619345332,
|
||||
"narHash": "sha256-qHnQkEp1uklKTpx3MvKtY6xzgcqXDsz5nLilbbuL+3A=",
|
||||
"lastModified": 1622711433,
|
||||
"narHash": "sha256-rGjXz7FA7HImAT3TtoqwecByLO5yhVPSwPdaYPBFRQw=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "2ebf2558e5bf978c7fb8ea927dfaed8fefab2e28",
|
||||
"repo": "devshell",
|
||||
"rev": "1f4fb67b662b65fa7cfe696fc003fcc1e8f7cc36",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"flakeCompat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1614513358,
|
||||
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
|
||||
"lastModified": 1606424373,
|
||||
"narHash": "sha256-oq8d4//CJOrVj+EcOaSXvMebvuTkmBJuT5tzlfewUnQ=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "99f1c2157fba4bfe6211a321fd0ee43199025dbf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"helix": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1623545930,
|
||||
"narHash": "sha256-14ASoYbxXHU/qPGctiUymb4fMRCoih9c7YujjxqEkdU=",
|
||||
"ref": "master",
|
||||
"rev": "9640ed1425f2db904fb42cd0c54dc6fbc05ca292",
|
||||
"revCount": 821,
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/helix-editor/helix.git"
|
||||
},
|
||||
"original": {
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/helix-editor/helix.git"
|
||||
}
|
||||
},
|
||||
"nixCargoIntegration": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"devshell": "devshell",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rustOverlay": "rustOverlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1620316130,
|
||||
"narHash": "sha256-sU0VS5oJS1FsHsZsLELAXc7G2eIelVuucRw+q5B1x9k=",
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"rev": "a3f40fe42cc6d267ff7518fa3199e99ff1444ac4",
|
||||
"lastModified": 1623591988,
|
||||
"narHash": "sha256-a8E5LYKxYjHmBWZsFxKnCBVGsWHFEWrKjeAJkplWrfI=",
|
||||
"owner": "yusdacra",
|
||||
"repo": "nix-cargo-integration",
|
||||
"rev": "8254b71eddd4e85173eddc189174b873fad85360",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"owner": "yusdacra",
|
||||
"repo": "nix-cargo-integration",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1619775165,
|
||||
"narHash": "sha256-2qaBErjxuWpTIq6Yee5GJmhr84hmzBotLQ0ayg1VXg8=",
|
||||
"path": "/nix/store/gs997rgx3pvdgcb54wd3fi9wbnznd9g4-source",
|
||||
"rev": "849b29b4f76d66ec7aeeeed699b7e27ef3db7c02",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1620252427,
|
||||
"narHash": "sha256-U1Q5QceuT4chJTJ1UOt7bZOn9Y2o5/7w27RISjqXoQw=",
|
||||
"lastModified": 1623324058,
|
||||
"narHash": "sha256-Jm9GUTXdjXz56gWDKy++EpFfjrBaxqXlLvTLfgEi8lo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d3ba49889a76539ea0f7d7285b203e7f81326ded",
|
||||
"rev": "432fc2d9a67f92e05438dff5fdc2b39d33f77997",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,40 +87,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1617325113,
|
||||
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-overlay": "rust-overlay"
|
||||
"flakeCompat": "flakeCompat",
|
||||
"helix": "helix",
|
||||
"nixCargoIntegration": "nixCargoIntegration",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"rustOverlay": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1620355527,
|
||||
"narHash": "sha256-mUTnUODiAtxH83gbv7uuvCbqZ/BNkYYk/wa3MkwrskE=",
|
||||
"lastModified": 1623550815,
|
||||
"narHash": "sha256-RumRrkE6OTJDndHV4qZNZv8kUGnzwRHZQSyzx29r6/g=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "d8efe70dc561c4bea0b7bf440d36ce98c497e054",
|
||||
"rev": "9824f142cbd7bc3e2a92eefbb79addfff8704cd3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
69
flake.nix
69
flake.nix
@@ -3,31 +3,54 @@
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
naersk.url = "github:nmattia/naersk";
|
||||
nixCargoIntegration = {
|
||||
url = "github:yusdacra/nix-cargo-integration";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flakeCompat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
helix = {
|
||||
url = "https://github.com/helix-editor/helix.git";
|
||||
type = "git";
|
||||
flake = false;
|
||||
submodules = true;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ self, nixpkgs, naersk, rust-overlay, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlay ]; };
|
||||
rust = (pkgs.rustChannelOf {
|
||||
date = "2021-05-01";
|
||||
channel = "nightly";
|
||||
}).minimal; # cargo, rustc and rust-std
|
||||
naerskLib = naersk.lib."${system}".override {
|
||||
# naersk can't build with stable?!
|
||||
# inherit (pkgs.rust-bin.stable.latest) rustc cargo;
|
||||
rustc = rust;
|
||||
cargo = rust;
|
||||
outputs = inputs@{ nixCargoIntegration, helix, ... }:
|
||||
nixCargoIntegration.lib.makeOutputs {
|
||||
root = ./.;
|
||||
buildPlatform = "crate2nix";
|
||||
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 = {
|
||||
crateOverrides = common: _: {
|
||||
helix-term = prev: { buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ]; };
|
||||
# link runtime since helix-core expects it because of embed_runtime feature
|
||||
helix-core = _: { preConfigure = "ln -s ${common.root + "/runtime"} ../runtime"; };
|
||||
# link languages and theme toml files since helix-view expects them
|
||||
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
|
||||
helix-syntax = prev: {
|
||||
src = common.pkgs.runCommand prev.src.name { } ''
|
||||
mkdir -p $out
|
||||
ln -s ${prev.src}/* $out
|
||||
ln -sf ${helix}/helix-syntax/languages $out
|
||||
'';
|
||||
};
|
||||
};
|
||||
in rec {
|
||||
packages.helix = naerskLib.buildPackage {
|
||||
pname = "helix";
|
||||
root = ./.;
|
||||
shell = common: prev: {
|
||||
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb ]);
|
||||
env = prev.env ++ [
|
||||
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
|
||||
{ name = "RUST_BACKTRACE"; value = "1"; }
|
||||
{ name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
|
||||
];
|
||||
};
|
||||
defaultPackage = packages.helix;
|
||||
devShell = pkgs.callPackage ./shell.nix {};
|
||||
});
|
||||
build = _: prev: { rootFeatures = prev.rootFeatures ++ [ "embed_runtime" ]; };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@@ -1,27 +1,32 @@
|
||||
[package]
|
||||
name = "helix-core"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
[features]
|
||||
embed_runtime = ["rust-embed"]
|
||||
|
||||
[dependencies]
|
||||
helix-syntax = { path = "../helix-syntax" }
|
||||
|
||||
ropey = "1.2"
|
||||
anyhow = "1"
|
||||
smallvec = "1.4"
|
||||
tendril = "0.4.2"
|
||||
unicode-segmentation = "1.6"
|
||||
unicode-segmentation = "1.7.1"
|
||||
unicode-width = "0.1"
|
||||
unicode-general-category = "0.4.0"
|
||||
# slab = "0.4.2"
|
||||
tree-sitter = "0.19"
|
||||
once_cell = "1.4"
|
||||
once_cell = "1.8"
|
||||
regex = "1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.5"
|
||||
|
||||
etcetera = "0.3"
|
||||
rust-embed = { version = "5.9.0", optional = true }
|
||||
|
@@ -67,7 +67,7 @@ fn handle_open(
|
||||
|
||||
let mut offs = 0;
|
||||
|
||||
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let pos = range.head;
|
||||
let next = next_char(doc, pos);
|
||||
|
||||
@@ -109,7 +109,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
|
||||
|
||||
let mut offs = 0;
|
||||
|
||||
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let pos = range.head;
|
||||
let next = next_char(doc, pos);
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
find_first_non_whitespace_char2, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
|
||||
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
|
||||
};
|
||||
use core::ops::Range;
|
||||
use std::borrow::Cow;
|
||||
@@ -14,7 +14,7 @@ fn find_line_comment(
|
||||
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
|
||||
for line in lines {
|
||||
let line_slice = text.line(line);
|
||||
if let Some(pos) = find_first_non_whitespace_char2(line_slice) {
|
||||
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
|
||||
let len = line_slice.len_chars();
|
||||
|
||||
if pos < min {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#[derive(Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
Warning,
|
||||
@@ -6,10 +6,13 @@ pub enum Severity {
|
||||
Hint,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Range {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Diagnostic {
|
||||
pub range: Range,
|
||||
pub line: usize,
|
||||
|
@@ -3,6 +3,8 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
|
||||
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[must_use]
|
||||
pub fn grapheme_width(g: &str) -> usize {
|
||||
if g.as_bytes()[0] <= 127 {
|
||||
@@ -156,6 +158,18 @@ pub struct RopeGraphemes<'a> {
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for RopeGraphemes<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RopeGraphemes<'a> {
|
||||
#[must_use]
|
||||
pub fn new(slice: RopeSlice) -> RopeGraphemes {
|
||||
|
@@ -1,19 +1,61 @@
|
||||
use crate::{ChangeSet, Rope, State, Transaction};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Undo-tree style history store.
|
||||
// Stores the history of changes to a buffer.
|
||||
//
|
||||
// Currently the history is represented as a vector of revisions. The vector
|
||||
// always has at least one element: the empty root revision. Each revision
|
||||
// with the exception of the root has a parent revision, a [Transaction]
|
||||
// that can be applied to its parent to transition from the parent to itself,
|
||||
// and an inversion of that transaction to transition from the parent to its
|
||||
// latest child.
|
||||
//
|
||||
// When using `u` to undo a change, an inverse of the stored transaction will
|
||||
// be applied which will transition the buffer to the parent state.
|
||||
//
|
||||
// Each revision with the exception of the last in the vector also has a
|
||||
// last child revision. When using `U` to redo a change, the last child transaction
|
||||
// will be applied to the current state of the buffer.
|
||||
//
|
||||
// The current revision is the one currently displayed in the buffer.
|
||||
//
|
||||
// Commiting a new revision to the history will update the last child of the
|
||||
// current revision, and push a new revision to the end of the vector.
|
||||
//
|
||||
// Revisions are commited with a timestamp. :earlier and :later can be used
|
||||
// to jump to the closest revision to a moment in time relative to the timestamp
|
||||
// of the current revision plus (:later) or minus (:earlier) the duration
|
||||
// given to the command. If a single integer is given, the editor will instead
|
||||
// jump the given number of revisions in the vector.
|
||||
//
|
||||
// Limitations:
|
||||
// * Changes in selections currently don't commit history changes. The selection
|
||||
// will only be updated to the state after a commited buffer change.
|
||||
// * The vector of history revisions is currently unbounded. This might
|
||||
// cause the memory consumption to grow significantly large during long
|
||||
// editing sessions.
|
||||
// * Because delete transactions currently don't store the text that they
|
||||
// delete, we also store an inversion of the transaction.
|
||||
#[derive(Debug)]
|
||||
pub struct History {
|
||||
revisions: Vec<Revision>,
|
||||
cursor: usize,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
// A single point in history. See [History] for more information.
|
||||
#[derive(Debug)]
|
||||
struct Revision {
|
||||
parent: usize,
|
||||
children: SmallVec<[(usize, Transaction); 1]>,
|
||||
/// The transaction to revert to previous state.
|
||||
revert: Transaction,
|
||||
// selection before, selection after?
|
||||
last_child: Option<NonZeroUsize>,
|
||||
transaction: Transaction,
|
||||
// We need an inversion for undos because delete transactions don't store
|
||||
// the deleted text.
|
||||
inversion: Transaction,
|
||||
timestamp: Instant,
|
||||
}
|
||||
|
||||
impl Default for History {
|
||||
@@ -22,74 +64,253 @@ impl Default for History {
|
||||
Self {
|
||||
revisions: vec![Revision {
|
||||
parent: 0,
|
||||
children: SmallVec::new(),
|
||||
revert: Transaction::from(ChangeSet::new(&Rope::new())),
|
||||
last_child: None,
|
||||
transaction: Transaction::from(ChangeSet::new(&Rope::new())),
|
||||
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
|
||||
timestamp: Instant::now(),
|
||||
}],
|
||||
cursor: 0,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) {
|
||||
// TODO: could store a single transaction, if deletes also stored the text they delete
|
||||
let revert = transaction
|
||||
self.commit_revision_at_timestamp(transaction, original, Instant::now());
|
||||
}
|
||||
|
||||
pub fn commit_revision_at_timestamp(
|
||||
&mut self,
|
||||
transaction: &Transaction,
|
||||
original: &State,
|
||||
timestamp: Instant,
|
||||
) {
|
||||
let inversion = transaction
|
||||
.invert(&original.doc)
|
||||
// Store the current cursor position
|
||||
.with_selection(original.selection.clone());
|
||||
|
||||
let new_cursor = self.revisions.len();
|
||||
let new_current = self.revisions.len();
|
||||
self.revisions[self.current].last_child = NonZeroUsize::new(new_current);
|
||||
self.revisions.push(Revision {
|
||||
parent: self.cursor,
|
||||
children: SmallVec::new(),
|
||||
revert,
|
||||
parent: self.current,
|
||||
last_child: None,
|
||||
transaction: transaction.clone(),
|
||||
inversion,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
// add a reference to the parent
|
||||
self.revisions
|
||||
.get_mut(self.cursor)
|
||||
.unwrap() // TODO: get_unchecked_mut
|
||||
.children
|
||||
.push((new_cursor, transaction.clone()));
|
||||
|
||||
self.cursor = new_cursor;
|
||||
self.current = new_current;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_revision(&self) -> usize {
|
||||
self.cursor
|
||||
self.current
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn at_root(&self) -> bool {
|
||||
self.cursor == 0
|
||||
self.current == 0
|
||||
}
|
||||
|
||||
// TODO: I'd like to pass Transaction by reference but it fights with the borrowck
|
||||
|
||||
pub fn undo(&mut self) -> Option<Transaction> {
|
||||
pub fn undo(&mut self) -> Option<&Transaction> {
|
||||
if self.at_root() {
|
||||
// We're at the root of undo, nothing to do.
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_revision = &self.revisions[self.cursor];
|
||||
|
||||
self.cursor = current_revision.parent;
|
||||
|
||||
Some(current_revision.revert.clone())
|
||||
let current_revision = &self.revisions[self.current];
|
||||
self.current = current_revision.parent;
|
||||
Some(¤t_revision.inversion)
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<Transaction> {
|
||||
let current_revision = &self.revisions[self.cursor];
|
||||
pub fn redo(&mut self) -> Option<&Transaction> {
|
||||
let current_revision = &self.revisions[self.current];
|
||||
let last_child = current_revision.last_child?;
|
||||
self.current = last_child.get();
|
||||
|
||||
// for now, simply pick the latest child (linear undo / redo)
|
||||
if let Some((index, transaction)) = current_revision.children.last() {
|
||||
self.cursor = *index;
|
||||
let last_child_revision = &self.revisions[last_child.get()];
|
||||
Some(&self.revisions[last_child.get()].transaction)
|
||||
}
|
||||
|
||||
return Some(transaction.clone());
|
||||
fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
|
||||
use std::collections::HashSet;
|
||||
let mut a_path_set = HashSet::new();
|
||||
let mut b_path_set = HashSet::new();
|
||||
loop {
|
||||
a_path_set.insert(a);
|
||||
b_path_set.insert(b);
|
||||
if a_path_set.contains(&b) {
|
||||
return b;
|
||||
}
|
||||
if b_path_set.contains(&a) {
|
||||
return a;
|
||||
}
|
||||
a = self.revisions[a].parent; // Relies on the parent of 0 being 0.
|
||||
b = self.revisions[b].parent; // Same as above.
|
||||
}
|
||||
}
|
||||
|
||||
// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
|
||||
// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
|
||||
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
|
||||
let mut path = Vec::new();
|
||||
while n != a {
|
||||
path.push(n);
|
||||
n = self.revisions[n].parent;
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
|
||||
let lca = self.lowest_common_ancestor(self.current, to);
|
||||
let up = self.path_up(self.current, lca);
|
||||
let down = self.path_up(to, lca);
|
||||
self.current = to;
|
||||
let up_txns = up.iter().map(|&n| self.revisions[n].inversion.clone());
|
||||
let down_txns = down
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&n| self.revisions[n].transaction.clone());
|
||||
up_txns.chain(down_txns).collect()
|
||||
}
|
||||
|
||||
fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
|
||||
self.jump_to(self.current.saturating_sub(delta))
|
||||
}
|
||||
|
||||
fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
|
||||
self.jump_to(
|
||||
self.current
|
||||
.saturating_add(delta)
|
||||
.min(self.revisions.len() - 1),
|
||||
)
|
||||
}
|
||||
|
||||
// Helper for a binary search case below.
|
||||
fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
|
||||
let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
|
||||
let dur_i = self.revisions[i].timestamp.duration_since(instant);
|
||||
use std::cmp::Ordering::*;
|
||||
match dur_im1.cmp(&dur_i) {
|
||||
Less => i - 1,
|
||||
Equal | Greater => i,
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
|
||||
let search_result = self
|
||||
.revisions
|
||||
.binary_search_by(|rev| rev.timestamp.cmp(&instant));
|
||||
let revision = match search_result {
|
||||
Ok(revision) => revision,
|
||||
Err(insert_point) => match insert_point {
|
||||
0 => 0,
|
||||
n if n == self.revisions.len() => n - 1,
|
||||
i => self.revision_closer_to_instant(i, instant),
|
||||
},
|
||||
};
|
||||
self.jump_to(revision)
|
||||
}
|
||||
|
||||
fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
match self.revisions[self.current].timestamp.checked_sub(duration) {
|
||||
Some(instant) => self.jump_instant(instant),
|
||||
None => self.jump_to(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
match self.revisions[self.current].timestamp.checked_add(duration) {
|
||||
Some(instant) => self.jump_instant(instant),
|
||||
None => self.jump_to(self.revisions.len() - 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
use UndoKind::*;
|
||||
match uk {
|
||||
Steps(n) => self.jump_backward(n),
|
||||
TimePeriod(d) => self.jump_duration_backward(d),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
use UndoKind::*;
|
||||
match uk {
|
||||
Steps(n) => self.jump_forward(n),
|
||||
TimePeriod(d) => self.jump_duration_forward(d),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum UndoKind {
|
||||
Steps(usize),
|
||||
TimePeriod(std::time::Duration),
|
||||
}
|
||||
|
||||
// A subset of sytemd.time time span syntax units.
|
||||
const TIME_UNITS: &[(&[&str], &str, u64)] = &[
|
||||
(&["seconds", "second", "sec", "s"], "seconds", 1),
|
||||
(&["minutes", "minute", "min", "m"], "minutes", 60),
|
||||
(&["hours", "hour", "hr", "h"], "hours", 60 * 60),
|
||||
(&["days", "day", "d"], "days", 24 * 60 * 60),
|
||||
];
|
||||
|
||||
static DURATION_VALIDATION_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
|
||||
|
||||
static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
|
||||
|
||||
fn parse_human_duration(s: &str) -> Result<Duration, String> {
|
||||
if !DURATION_VALIDATION_REGEX.is_match(s) {
|
||||
return Err("duration should be composed \
|
||||
of positive integers followed by time units"
|
||||
.to_string());
|
||||
}
|
||||
|
||||
let mut specified = [false; TIME_UNITS.len()];
|
||||
let mut seconds = 0u64;
|
||||
for cap in NUMBER_UNIT_REGEX.captures_iter(s) {
|
||||
let (n, unit_str) = (&cap[1], &cap[2]);
|
||||
|
||||
let n: u64 = n.parse().map_err(|_| format!("integer too large: {}", n))?;
|
||||
|
||||
let time_unit = TIME_UNITS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, (forms, _, _))| forms.iter().any(|f| f == &unit_str));
|
||||
|
||||
if let Some((i, (_, unit, mul))) = time_unit {
|
||||
if specified[i] {
|
||||
return Err(format!("{} specified more than once", unit));
|
||||
}
|
||||
specified[i] = true;
|
||||
|
||||
let new_seconds = n.checked_mul(*mul).and_then(|s| seconds.checked_add(s));
|
||||
match new_seconds {
|
||||
Some(ns) => seconds = ns,
|
||||
None => return Err("duration too large".to_string()),
|
||||
}
|
||||
} else {
|
||||
return Err(format!("incorrect time unit: {}", unit_str));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Duration::from_secs(seconds))
|
||||
}
|
||||
|
||||
impl std::str::FromStr for UndoKind {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
Ok(Self::Steps(1usize))
|
||||
} else if let Ok(n) = s.parse::<usize>() {
|
||||
Ok(UndoKind::Steps(n))
|
||||
} else {
|
||||
Ok(Self::TimePeriod(parse_human_duration(s)?))
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,4 +366,191 @@ mod test {
|
||||
undo(&mut history, &mut state);
|
||||
assert_eq!("hello", state.doc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_earlier_later() {
|
||||
let mut history = History::default();
|
||||
let doc = Rope::from("a\n");
|
||||
let mut state = State::new(doc);
|
||||
|
||||
fn undo(history: &mut History, state: &mut State) {
|
||||
if let Some(transaction) = history.undo() {
|
||||
transaction.apply(&mut state.doc);
|
||||
}
|
||||
};
|
||||
|
||||
fn earlier(history: &mut History, state: &mut State, uk: UndoKind) {
|
||||
let txns = history.earlier(uk);
|
||||
for txn in txns {
|
||||
txn.apply(&mut state.doc);
|
||||
}
|
||||
};
|
||||
|
||||
fn later(history: &mut History, state: &mut State, uk: UndoKind) {
|
||||
let txns = history.later(uk);
|
||||
for txn in txns {
|
||||
txn.apply(&mut state.doc);
|
||||
}
|
||||
};
|
||||
|
||||
fn commit_change(
|
||||
history: &mut History,
|
||||
state: &mut State,
|
||||
change: crate::transaction::Change,
|
||||
instant: Instant,
|
||||
) {
|
||||
let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter());
|
||||
history.commit_revision_at_timestamp(&txn, &state, instant);
|
||||
txn.apply(&mut state.doc);
|
||||
};
|
||||
|
||||
let t0 = Instant::now();
|
||||
let t = |n| t0.checked_add(Duration::from_secs(n)).unwrap();
|
||||
|
||||
commit_change(&mut history, &mut state, (1, 1, Some(" b".into())), t(0));
|
||||
assert_eq!("a b\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (3, 3, Some(" c".into())), t(10));
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (5, 5, Some(" d".into())), t(20));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
undo(&mut history, &mut state);
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (5, 5, Some(" e".into())), t(30));
|
||||
assert_eq!("a b c e\n", state.doc);
|
||||
|
||||
undo(&mut history, &mut state);
|
||||
undo(&mut history, &mut state);
|
||||
assert_eq!("a b\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (1, 3, None), t(40));
|
||||
assert_eq!("a\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (1, 1, Some(" f".into())), t(50));
|
||||
assert_eq!("a f\n", state.doc);
|
||||
|
||||
use UndoKind::*;
|
||||
|
||||
earlier(&mut history, &mut state, Steps(3));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(20, 0)));
|
||||
assert_eq!("a\n", state.doc);
|
||||
|
||||
earlier(&mut history, &mut state, TimePeriod(Duration::new(19, 0)));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
earlier(
|
||||
&mut history,
|
||||
&mut state,
|
||||
TimePeriod(Duration::new(10000, 0)),
|
||||
);
|
||||
assert_eq!("a\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, Steps(50));
|
||||
assert_eq!("a f\n", state.doc);
|
||||
|
||||
earlier(&mut history, &mut state, Steps(4));
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(1, 0)));
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(5, 0)));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(6, 0)));
|
||||
assert_eq!("a b c e\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, Steps(1));
|
||||
assert_eq!("a\n", state.doc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_undo_kind() {
|
||||
use UndoKind::*;
|
||||
|
||||
// Default is one step.
|
||||
assert_eq!("".parse(), Ok(Steps(1)));
|
||||
|
||||
// An integer means the number of steps.
|
||||
assert_eq!("1".parse(), Ok(Steps(1)));
|
||||
assert_eq!(" 16 ".parse(), Ok(Steps(16)));
|
||||
|
||||
// Duration has a strict format.
|
||||
let validation_err = Err("duration should be composed \
|
||||
of positive integers followed by time units"
|
||||
.to_string());
|
||||
assert_eq!(" 16 33".parse::<UndoKind>(), validation_err);
|
||||
assert_eq!(" seconds 22 ".parse::<UndoKind>(), validation_err);
|
||||
assert_eq!(" -4 m".parse::<UndoKind>(), validation_err);
|
||||
assert_eq!("5s 3".parse::<UndoKind>(), validation_err);
|
||||
|
||||
// Units are u64.
|
||||
assert_eq!(
|
||||
"18446744073709551616minutes".parse::<UndoKind>(),
|
||||
Err("integer too large: 18446744073709551616".to_string())
|
||||
);
|
||||
|
||||
// Units are validated.
|
||||
assert_eq!(
|
||||
"1 millenium".parse::<UndoKind>(),
|
||||
Err("incorrect time unit: millenium".to_string())
|
||||
);
|
||||
|
||||
// Units can't be specified twice.
|
||||
assert_eq!(
|
||||
"2 seconds 6s".parse::<UndoKind>(),
|
||||
Err("seconds specified more than once".to_string())
|
||||
);
|
||||
|
||||
// Various formats are correctly handled.
|
||||
assert_eq!(
|
||||
"4s".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(4)))
|
||||
);
|
||||
assert_eq!(
|
||||
"2m".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(120)))
|
||||
);
|
||||
assert_eq!(
|
||||
"5h".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(5 * 60 * 60)))
|
||||
);
|
||||
assert_eq!(
|
||||
"3d".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(3 * 24 * 60 * 60)))
|
||||
);
|
||||
assert_eq!(
|
||||
"1m30s".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(90)))
|
||||
);
|
||||
assert_eq!(
|
||||
"1m 20 seconds".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(80)))
|
||||
);
|
||||
assert_eq!(
|
||||
" 2 minute 1day".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(24 * 60 * 60 + 2 * 60)))
|
||||
);
|
||||
assert_eq!(
|
||||
"3 d 2hour 5 minutes 30sec".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(
|
||||
3 * 24 * 60 * 60 + 2 * 60 * 60 + 5 * 60 + 30
|
||||
)))
|
||||
);
|
||||
|
||||
// Sum overflow is handled.
|
||||
assert_eq!(
|
||||
"18446744073709551615minutes".parse::<UndoKind>(),
|
||||
Err("duration too large".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
"1 minute 18446744073709551615 seconds".parse::<UndoKind>(),
|
||||
Err("duration too large".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -105,11 +105,14 @@ fn suggested_indent_for_line(
|
||||
line_num: usize,
|
||||
tab_width: usize,
|
||||
) -> usize {
|
||||
let line = text.line(line_num);
|
||||
let current = indent_level_for_line(line, tab_width);
|
||||
|
||||
if let Some(start) = find_first_non_whitespace_char(text, line_num) {
|
||||
return suggested_indent_for_pos(Some(language_config), syntax, text, start, false);
|
||||
if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
|
||||
return suggested_indent_for_pos(
|
||||
Some(language_config),
|
||||
syntax,
|
||||
text,
|
||||
start + text.line_to_char(line_num),
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
// if the line is blank, indent should be zero
|
||||
@@ -251,22 +254,26 @@ where
|
||||
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
let loader = Loader::new(Configuration {
|
||||
language: vec![LanguageConfiguration {
|
||||
scope: "source.rust".to_string(),
|
||||
file_types: vec!["rs".to_string()],
|
||||
language_id: Lang::Rust,
|
||||
highlight_config: OnceCell::new(),
|
||||
//
|
||||
roots: vec![],
|
||||
language_server: None,
|
||||
indent: Some(IndentationConfiguration {
|
||||
tab_width: 4,
|
||||
unit: String::from(" "),
|
||||
}),
|
||||
indent_query: OnceCell::new(),
|
||||
}],
|
||||
});
|
||||
let loader = Loader::new(
|
||||
Configuration {
|
||||
language: vec![LanguageConfiguration {
|
||||
scope: "source.rust".to_string(),
|
||||
file_types: vec!["rs".to_string()],
|
||||
language_id: Lang::Rust,
|
||||
highlight_config: OnceCell::new(),
|
||||
//
|
||||
roots: vec![],
|
||||
auto_format: false,
|
||||
language_server: None,
|
||||
indent: Some(IndentationConfiguration {
|
||||
tab_width: 4,
|
||||
unit: String::from(" "),
|
||||
}),
|
||||
indent_query: OnceCell::new(),
|
||||
}],
|
||||
},
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
// set runtime path so we can find the queries
|
||||
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
@@ -3,7 +3,7 @@ pub mod auto_pairs;
|
||||
pub mod comment;
|
||||
pub mod diagnostic;
|
||||
pub mod graphemes;
|
||||
mod history;
|
||||
pub mod history;
|
||||
pub mod indent;
|
||||
pub mod macros;
|
||||
pub mod match_brackets;
|
||||
@@ -13,62 +13,89 @@ mod position;
|
||||
pub mod register;
|
||||
pub mod search;
|
||||
pub mod selection;
|
||||
pub mod state;
|
||||
mod state;
|
||||
pub mod syntax;
|
||||
mod transaction;
|
||||
|
||||
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
|
||||
// find first non-whitespace char
|
||||
for (start, ch) in line.chars().enumerate() {
|
||||
// TODO: could use memchr with chunks?
|
||||
if ch != ' ' && ch != '\t' && ch != '\n' {
|
||||
return Some(start);
|
||||
}
|
||||
}
|
||||
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
|
||||
once_cell::sync::Lazy::new(runtime_dir);
|
||||
|
||||
None
|
||||
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
|
||||
line.chars().position(|ch| !ch.is_whitespace())
|
||||
}
|
||||
pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -> Option<usize> {
|
||||
let line = text.line(line_num);
|
||||
let mut start = text.line_to_char(line_num);
|
||||
|
||||
// find first non-whitespace char
|
||||
for ch in line.chars() {
|
||||
// TODO: could use memchr with chunks?
|
||||
if ch != ' ' && ch != '\t' && ch != '\n' {
|
||||
return Some(start);
|
||||
pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
|
||||
let current_dir = std::env::current_dir().expect("unable to determine current directory");
|
||||
|
||||
let root = match root {
|
||||
Some(root) => {
|
||||
let root = std::path::Path::new(root);
|
||||
if root.is_absolute() {
|
||||
root.to_path_buf()
|
||||
} else {
|
||||
current_dir.join(root)
|
||||
}
|
||||
}
|
||||
start += 1;
|
||||
}
|
||||
None => current_dir,
|
||||
};
|
||||
|
||||
for ancestor in root.ancestors() {
|
||||
// TODO: also use defined roots if git isn't found
|
||||
if ancestor.join(".git").is_dir() {
|
||||
return Some(ancestor.to_path_buf());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn runtime_dir() -> std::path::PathBuf {
|
||||
// runtime env var || dir where binary is located
|
||||
std::env::var("HELIX_RUNTIME")
|
||||
.map(|path| path.into())
|
||||
.unwrap_or_else(|_| {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
|
||||
.unwrap()
|
||||
})
|
||||
#[cfg(not(embed_runtime))]
|
||||
fn runtime_dir() -> std::path::PathBuf {
|
||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||
return dir.into();
|
||||
}
|
||||
|
||||
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()
|
||||
.and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn config_dir() -> std::path::PathBuf {
|
||||
// TODO: allow env var override
|
||||
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
||||
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
||||
let mut path = strategy.config_dir();
|
||||
path.push("helix");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn cache_dir() -> std::path::PathBuf {
|
||||
// TODO: allow env var override
|
||||
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
||||
let mut path = strategy.cache_dir();
|
||||
path.push("helix");
|
||||
path
|
||||
}
|
||||
|
||||
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
||||
|
||||
pub use ropey::{Rope, RopeSlice};
|
||||
|
||||
pub use tendril::StrTendril as Tendril;
|
||||
|
||||
pub use unicode_general_category::get_general_category;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use {regex, tree_sitter};
|
||||
|
||||
@@ -78,7 +105,6 @@ pub use smallvec::SmallVec;
|
||||
pub use syntax::Syntax;
|
||||
|
||||
pub use diagnostic::Diagnostic;
|
||||
pub use history::History;
|
||||
pub use state::State;
|
||||
|
||||
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use crate::{Range, Rope, Selection, Syntax};
|
||||
|
||||
// const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']')];
|
||||
const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
|
||||
// limit matching pairs to only ( ) { } [ ] < >
|
||||
|
||||
#[must_use]
|
||||
@@ -20,15 +20,27 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let start_byte = node.start_byte();
|
||||
let end_byte = node.end_byte() - 1; // it's end exclusive
|
||||
|
||||
if start_byte == byte_pos {
|
||||
return Some(doc.byte_to_char(end_byte));
|
||||
if node.is_error() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if end_byte == byte_pos {
|
||||
return Some(doc.byte_to_char(start_byte));
|
||||
let start_byte = node.start_byte();
|
||||
let len = doc.len_bytes();
|
||||
if start_byte >= len {
|
||||
return None;
|
||||
}
|
||||
let end_byte = node.end_byte() - 1; // it's end exclusive
|
||||
let start_char = doc.byte_to_char(start_byte);
|
||||
let end_char = doc.byte_to_char(end_byte);
|
||||
|
||||
if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
|
||||
if start_byte == byte_pos {
|
||||
return Some(end_char);
|
||||
}
|
||||
|
||||
if end_byte == byte_pos {
|
||||
return Some(start_char);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
@@ -1,241 +1,323 @@
|
||||
use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
|
||||
use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
|
||||
use std::iter::{self, from_fn, Peekable, SkipWhile};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
use ropey::iter::Chars;
|
||||
|
||||
use crate::{
|
||||
coords_at_pos,
|
||||
graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
|
||||
pos_at_coords, Position, Range, RopeSlice,
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Movement {
|
||||
Extend,
|
||||
Move,
|
||||
}
|
||||
|
||||
pub fn move_horizontally(
|
||||
text: RopeSlice,
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
dir: Direction,
|
||||
count: usize,
|
||||
extend: bool,
|
||||
behaviour: Movement,
|
||||
) -> Range {
|
||||
let pos = range.head;
|
||||
let line = text.char_to_line(pos);
|
||||
let line = slice.char_to_line(pos);
|
||||
// TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
|
||||
// we stop calculating past start/end of line.
|
||||
let pos = match dir {
|
||||
Direction::Backward => {
|
||||
let start = text.line_to_char(line);
|
||||
nth_prev_grapheme_boundary(text, pos, count).max(start)
|
||||
let start = slice.line_to_char(line);
|
||||
nth_prev_grapheme_boundary(slice, pos, count).max(start)
|
||||
}
|
||||
Direction::Forward => {
|
||||
// Line end is pos at the start of next line - 1
|
||||
// subtract another 1 because the line ends with \n
|
||||
let end = text.line_to_char(line + 1).saturating_sub(2);
|
||||
nth_next_grapheme_boundary(text, pos, count).min(end)
|
||||
let end = slice.line_to_char(line + 1).saturating_sub(1);
|
||||
nth_next_grapheme_boundary(slice, pos, count).min(end)
|
||||
}
|
||||
};
|
||||
Range::new(if extend { range.anchor } else { pos }, pos)
|
||||
let anchor = match behaviour {
|
||||
Movement::Extend => range.anchor,
|
||||
Movement::Move => pos,
|
||||
};
|
||||
Range::new(anchor, pos)
|
||||
}
|
||||
|
||||
pub fn move_vertically(
|
||||
text: RopeSlice,
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
dir: Direction,
|
||||
count: usize,
|
||||
extend: bool,
|
||||
behaviour: Movement,
|
||||
) -> Range {
|
||||
let Position { row, col } = coords_at_pos(text, range.head);
|
||||
let Position { row, col } = coords_at_pos(slice, range.head);
|
||||
|
||||
let horiz = range.horiz.unwrap_or(col as u32);
|
||||
|
||||
let new_line = match dir {
|
||||
Direction::Backward => row.saturating_sub(count),
|
||||
Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 1),
|
||||
Direction::Forward => std::cmp::min(
|
||||
row.saturating_add(count),
|
||||
slice.len_lines().saturating_sub(2),
|
||||
),
|
||||
};
|
||||
|
||||
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
|
||||
let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
|
||||
let new_line_len = slice.line(new_line).len_chars().saturating_sub(2);
|
||||
|
||||
let new_col = std::cmp::min(horiz as usize, new_line_len);
|
||||
|
||||
let pos = pos_at_coords(text, Position::new(new_line, new_col));
|
||||
let pos = pos_at_coords(slice, Position::new(new_line, new_col));
|
||||
|
||||
let mut range = Range::new(if extend { range.anchor } else { pos }, pos);
|
||||
let anchor = match behaviour {
|
||||
Movement::Extend => range.anchor,
|
||||
Movement::Move => pos,
|
||||
};
|
||||
|
||||
let mut range = Range::new(anchor, pos);
|
||||
range.horiz = Some(horiz);
|
||||
range
|
||||
}
|
||||
|
||||
pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
|
||||
let mut end = begin;
|
||||
|
||||
for _ in 0..count {
|
||||
if begin + 1 == slice.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut ch = slice.char(begin);
|
||||
let next = slice.char(begin + 1);
|
||||
|
||||
// if we're at the end of a word, or on whitespce right before new one
|
||||
if categorize(ch) != categorize(next) {
|
||||
begin += 1;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_next(slice, &mut begin, |ch| ch == '\n');
|
||||
ch = slice.char(begin);
|
||||
|
||||
end = begin + 1;
|
||||
|
||||
if is_word(ch) {
|
||||
skip_over_next(slice, &mut end, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
|
||||
skip_over_next(slice, &mut end, is_horiz_blank);
|
||||
}
|
||||
|
||||
Some(Range::new(begin, end - 1))
|
||||
pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
|
||||
word_move(slice, range, count, WordMotionTarget::NextWordStart)
|
||||
}
|
||||
|
||||
pub fn move_prev_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
|
||||
let mut with_end = false;
|
||||
let mut end = begin;
|
||||
|
||||
for _ in 0..count {
|
||||
if begin == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ch = slice.char(begin);
|
||||
let prev = slice.char(begin - 1);
|
||||
|
||||
if categorize(ch) != categorize(prev) {
|
||||
begin -= 1;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_prev(slice, &mut begin, |ch| ch == '\n');
|
||||
|
||||
end = begin;
|
||||
|
||||
with_end = skip_over_prev(slice, &mut end, is_horiz_blank);
|
||||
|
||||
// refetch
|
||||
let ch = slice.char(end);
|
||||
|
||||
if is_word(ch) {
|
||||
with_end = skip_over_prev(slice, &mut end, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
with_end = skip_over_prev(slice, &mut end, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
}
|
||||
|
||||
Some(Range::new(begin, if with_end { end } else { end + 1 }))
|
||||
pub fn move_next_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
|
||||
word_move(slice, range, count, WordMotionTarget::NextWordEnd)
|
||||
}
|
||||
|
||||
pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
|
||||
let mut end = begin;
|
||||
pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
|
||||
word_move(slice, range, count, WordMotionTarget::PrevWordStart)
|
||||
}
|
||||
|
||||
for _ in 0..count {
|
||||
if begin + 1 == slice.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ch = slice.char(begin);
|
||||
let next = slice.char(begin + 1);
|
||||
|
||||
if categorize(ch) != categorize(next) {
|
||||
begin += 1;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_next(slice, &mut begin, |ch| ch == '\n');
|
||||
|
||||
end = begin;
|
||||
|
||||
skip_over_next(slice, &mut end, is_horiz_blank);
|
||||
|
||||
// refetch
|
||||
let ch = slice.char(end);
|
||||
|
||||
if is_word(ch) {
|
||||
skip_over_next(slice, &mut end, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
}
|
||||
|
||||
Some(Range::new(begin, end - 1))
|
||||
fn word_move(slice: RopeSlice, mut range: Range, count: usize, target: WordMotionTarget) -> Range {
|
||||
(0..count).fold(range, |range, _| {
|
||||
slice.chars_at(range.head).range_to_target(target, range)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- util ------------
|
||||
|
||||
// used for by-word movement
|
||||
|
||||
fn is_word(ch: char) -> bool {
|
||||
#[inline]
|
||||
pub(crate) fn is_word(ch: char) -> bool {
|
||||
ch.is_alphanumeric() || ch == '_'
|
||||
}
|
||||
|
||||
fn is_horiz_blank(ch: char) -> bool {
|
||||
matches!(ch, ' ' | '\t')
|
||||
#[inline]
|
||||
pub(crate) fn is_end_of_line(ch: char) -> bool {
|
||||
ch == '\n'
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// Whitespace, but not end of line
|
||||
pub(crate) fn is_strict_whitespace(ch: char) -> bool {
|
||||
ch.is_whitespace() && !is_end_of_line(ch)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_punctuation(ch: char) -> bool {
|
||||
use unicode_general_category::{get_general_category, GeneralCategory};
|
||||
|
||||
matches!(
|
||||
get_general_category(ch),
|
||||
GeneralCategory::OtherPunctuation
|
||||
| GeneralCategory::OpenPunctuation
|
||||
| GeneralCategory::ClosePunctuation
|
||||
| GeneralCategory::InitialPunctuation
|
||||
| GeneralCategory::FinalPunctuation
|
||||
| GeneralCategory::ConnectorPunctuation
|
||||
| GeneralCategory::DashPunctuation
|
||||
| GeneralCategory::MathSymbol
|
||||
| GeneralCategory::CurrencySymbol
|
||||
| GeneralCategory::ModifierSymbol
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum Category {
|
||||
pub enum Category {
|
||||
Whitespace,
|
||||
Eol,
|
||||
Word,
|
||||
Punctuation,
|
||||
Unknown,
|
||||
}
|
||||
fn categorize(ch: char) -> Category {
|
||||
if ch == '\n' {
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn categorize(ch: char) -> Category {
|
||||
if is_end_of_line(ch) {
|
||||
Category::Eol
|
||||
} else if ch.is_ascii_whitespace() {
|
||||
} else if ch.is_whitespace() {
|
||||
Category::Whitespace
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
Category::Punctuation
|
||||
} else if ch.is_ascii_alphanumeric() {
|
||||
} else if is_word(ch) {
|
||||
Category::Word
|
||||
} else if is_punctuation(ch) {
|
||||
Category::Punctuation
|
||||
} else {
|
||||
unreachable!()
|
||||
Category::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
|
||||
/// Returns first index that doesn't satisfy a given predicate when
|
||||
/// advancing the character index.
|
||||
///
|
||||
/// Returns none if all characters satisfy the predicate.
|
||||
pub fn skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
let mut chars = slice.chars_at(*pos);
|
||||
|
||||
for ch in chars {
|
||||
if !fun(ch) {
|
||||
break;
|
||||
}
|
||||
*pos += 1;
|
||||
}
|
||||
let mut chars = slice.chars_at(pos).enumerate();
|
||||
chars.find_map(|(i, c)| if !fun(c) { Some(pos + i) } else { None })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns true if the final pos matches the predicate.
|
||||
pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
|
||||
/// Returns first index that doesn't satisfy a given predicate when
|
||||
/// retreating the character index, saturating if all elements satisfy
|
||||
/// the condition.
|
||||
pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
// need to +1 so that prev() includes current char
|
||||
let mut chars = slice.chars_at(*pos + 1);
|
||||
|
||||
while let Some(ch) = chars.prev() {
|
||||
if !fun(ch) {
|
||||
break;
|
||||
let mut chars_starting_from_next = slice.chars_at(pos + 1);
|
||||
let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
|
||||
backwards.find_map(|(i, c)| {
|
||||
if !fun(c) {
|
||||
Some(pos.saturating_sub(i))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Possible targets of a word motion
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum WordMotionTarget {
|
||||
NextWordStart,
|
||||
NextWordEnd,
|
||||
PrevWordStart,
|
||||
}
|
||||
|
||||
pub trait CharHelpers {
|
||||
fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range;
|
||||
}
|
||||
|
||||
enum WordMotionPhase {
|
||||
Start,
|
||||
SkipNewlines,
|
||||
ReachTarget,
|
||||
}
|
||||
|
||||
impl CharHelpers for Chars<'_> {
|
||||
fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
|
||||
let range = origin;
|
||||
// Characters are iterated forward or backwards depending on the motion direction.
|
||||
let characters: Box<dyn Iterator<Item = char>> = match target {
|
||||
WordMotionTarget::PrevWordStart => {
|
||||
self.next();
|
||||
Box::new(from_fn(|| self.prev()))
|
||||
}
|
||||
_ => Box::new(self),
|
||||
};
|
||||
|
||||
// Index advancement also depends on the direction.
|
||||
let advance: &dyn Fn(&mut usize) = match target {
|
||||
WordMotionTarget::PrevWordStart => &|u| *u = u.saturating_sub(1),
|
||||
_ => &|u| *u += 1,
|
||||
};
|
||||
|
||||
let mut characters = characters.peekable();
|
||||
let mut phase = WordMotionPhase::Start;
|
||||
let mut head = origin.head;
|
||||
let mut anchor: Option<usize> = None;
|
||||
let is_boundary = |a: char, b: Option<char>| categorize(a) != categorize(b.unwrap_or(a));
|
||||
while let Some(peek) = characters.peek().copied() {
|
||||
phase = match phase {
|
||||
WordMotionPhase::Start => {
|
||||
characters.next();
|
||||
if characters.peek().is_none() {
|
||||
break; // We're at the end, so there's nothing to do.
|
||||
}
|
||||
// Anchor may remain here if the head wasn't at a boundary
|
||||
if !is_boundary(peek, characters.peek().copied()) && !is_end_of_line(peek) {
|
||||
anchor = Some(head);
|
||||
}
|
||||
// First character is always skipped by the head
|
||||
advance(&mut head);
|
||||
WordMotionPhase::SkipNewlines
|
||||
}
|
||||
WordMotionPhase::SkipNewlines => {
|
||||
if is_end_of_line(peek) {
|
||||
characters.next();
|
||||
if characters.peek().is_some() {
|
||||
advance(&mut head);
|
||||
}
|
||||
WordMotionPhase::SkipNewlines
|
||||
} else {
|
||||
WordMotionPhase::ReachTarget
|
||||
}
|
||||
}
|
||||
WordMotionPhase::ReachTarget => {
|
||||
characters.next();
|
||||
anchor = anchor.or(Some(head));
|
||||
if reached_target(target, peek, characters.peek()) {
|
||||
break;
|
||||
} else {
|
||||
advance(&mut head);
|
||||
}
|
||||
WordMotionPhase::ReachTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
Range::new(anchor.unwrap_or(origin.anchor), head)
|
||||
}
|
||||
}
|
||||
|
||||
fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>) -> bool {
|
||||
let next_peek = match next_peek {
|
||||
Some(next_peek) => next_peek,
|
||||
None => return true,
|
||||
};
|
||||
|
||||
match target {
|
||||
WordMotionTarget::NextWordStart => {
|
||||
((categorize(peek) != categorize(*next_peek))
|
||||
&& (is_end_of_line(*next_peek) || !next_peek.is_whitespace()))
|
||||
}
|
||||
WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
|
||||
((categorize(peek) != categorize(*next_peek))
|
||||
&& (!peek.is_whitespace() || is_end_of_line(*next_peek)))
|
||||
}
|
||||
*pos = pos.saturating_sub(1);
|
||||
}
|
||||
fun(slice.char(*pos))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::array::{self, IntoIter};
|
||||
|
||||
use ropey::Rope;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SINGLE_LINE_SAMPLE: &str = "This is a simple alphabetic line";
|
||||
const MULTILINE_SAMPLE: &str = "\
|
||||
Multiline\n\
|
||||
text sample\n\
|
||||
which\n\
|
||||
is merely alphabetic\n\
|
||||
and whitespaced\n\
|
||||
";
|
||||
|
||||
const MULTIBYTE_CHARACTER_SAMPLE: &str = "\
|
||||
パーティーへ行かないか\n\
|
||||
The text above is Japanese\n\
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn test_vertical_move() {
|
||||
let text = Rope::from("abcd\nefg\nwrs");
|
||||
@@ -246,9 +328,477 @@ mod test {
|
||||
assert_eq!(
|
||||
coords_at_pos(
|
||||
slice,
|
||||
move_vertically(slice, range, Direction::Forward, 1, false).head
|
||||
move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
|
||||
),
|
||||
(1, 2).into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_moves_through_single_line_in_single_line_text() {
|
||||
let text = Rope::from(SINGLE_LINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
|
||||
let mut range = Range::point(position);
|
||||
|
||||
let moves_and_expected_coordinates = [
|
||||
((Direction::Forward, 1usize), (0, 1)),
|
||||
((Direction::Forward, 2usize), (0, 3)),
|
||||
((Direction::Forward, 0usize), (0, 3)),
|
||||
((Direction::Forward, 999usize), (0, 31)),
|
||||
((Direction::Forward, 999usize), (0, 31)),
|
||||
((Direction::Backward, 999usize), (0, 0)),
|
||||
];
|
||||
|
||||
for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
|
||||
range = move_horizontally(slice, range, direction, amount, Movement::Move);
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_moves_through_single_line_in_multiline_text() {
|
||||
let text = Rope::from(MULTILINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
|
||||
let mut range = Range::point(position);
|
||||
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
((Direction::Forward, 1usize), (0, 1)), // M_ltiline
|
||||
((Direction::Forward, 2usize), (0, 3)), // Mul_iline
|
||||
((Direction::Backward, 6usize), (0, 0)), // _ultiline
|
||||
((Direction::Backward, 999usize), (0, 0)), // _ultiline
|
||||
((Direction::Forward, 3usize), (0, 3)), // Mul_iline
|
||||
((Direction::Forward, 0usize), (0, 3)), // Mul_iline
|
||||
((Direction::Backward, 0usize), (0, 3)), // Mul_iline
|
||||
((Direction::Forward, 999usize), (0, 9)), // Multilin_
|
||||
((Direction::Forward, 999usize), (0, 9)), // Multilin_
|
||||
]);
|
||||
|
||||
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
|
||||
range = move_horizontally(slice, range, direction, amount, Movement::Move);
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_extending_moves_in_single_line_text() {
|
||||
let text = Rope::from(SINGLE_LINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
|
||||
let mut range = Range::point(position);
|
||||
let original_anchor = range.anchor;
|
||||
|
||||
let moves = IntoIter::new([
|
||||
(Direction::Forward, 1usize),
|
||||
(Direction::Forward, 5usize),
|
||||
(Direction::Backward, 3usize),
|
||||
]);
|
||||
|
||||
for (direction, amount) in moves {
|
||||
range = move_horizontally(slice, range, direction, amount, Movement::Extend);
|
||||
assert_eq!(range.anchor, original_anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_moves_in_single_column() {
|
||||
let text = Rope::from(MULTILINE_SAMPLE);
|
||||
let slice = dbg!(&text).slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
let mut range = Range::point(position);
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
((Direction::Forward, 1usize), (1, 0)),
|
||||
((Direction::Forward, 2usize), (3, 0)),
|
||||
((Direction::Backward, 999usize), (0, 0)),
|
||||
((Direction::Forward, 3usize), (3, 0)),
|
||||
((Direction::Forward, 0usize), (3, 0)),
|
||||
((Direction::Backward, 0usize), (3, 0)),
|
||||
((Direction::Forward, 5), (4, 0)),
|
||||
((Direction::Forward, 999usize), (4, 0)),
|
||||
]);
|
||||
|
||||
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
|
||||
range = move_vertically(slice, range, direction, amount, Movement::Move);
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_moves_jumping_column() {
|
||||
let text = Rope::from(MULTILINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
let mut range = Range::point(position);
|
||||
|
||||
enum Axis {
|
||||
H,
|
||||
V,
|
||||
}
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
// Places cursor at the end of line
|
||||
((Axis::H, Direction::Forward, 8usize), (0, 8)),
|
||||
// First descent preserves column as the target line is wider
|
||||
((Axis::V, Direction::Forward, 1usize), (1, 8)),
|
||||
// Second descent clamps column as the target line is shorter
|
||||
((Axis::V, Direction::Forward, 1usize), (2, 4)),
|
||||
// Third descent restores the original column
|
||||
((Axis::V, Direction::Forward, 1usize), (3, 8)),
|
||||
// Behaviour is preserved even through long jumps
|
||||
((Axis::V, Direction::Backward, 999usize), (0, 8)),
|
||||
((Axis::V, Direction::Forward, 999usize), (4, 8)),
|
||||
]);
|
||||
|
||||
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),
|
||||
};
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multibyte_character_column_jumps() {
|
||||
let text = Rope::from(MULTIBYTE_CHARACTER_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
let mut range = Range::point(position);
|
||||
|
||||
// FIXME: The behaviour captured in this test diverges from both Kakoune and Vim. These
|
||||
// will attempt to preserve the horizontal position of the cursor, rather than
|
||||
// placing it at the same character index.
|
||||
enum Axis {
|
||||
H,
|
||||
V,
|
||||
}
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
// Places cursor at the fourth kana
|
||||
((Axis::H, Direction::Forward, 4), (0, 4)),
|
||||
// Descent places cursor at the fourth character.
|
||||
((Axis::V, Direction::Forward, 1usize), (1, 4)),
|
||||
]);
|
||||
|
||||
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),
|
||||
};
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn nonsensical_ranges_panic_on_forward_movement_attempt_in_debug_mode() {
|
||||
move_next_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn nonsensical_ranges_panic_on_forward_to_end_movement_attempt_in_debug_mode() {
|
||||
move_next_word_end(Rope::from("Sample").slice(..), Range::point(99999999), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn nonsensical_ranges_panic_on_backwards_movement_attempt_in_debug_mode() {
|
||||
move_prev_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_behaviour_when_moving_to_start_of_next_words() {
|
||||
let tests = array::IntoIter::new([
|
||||
("Basic forward motion stops at the first space",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 5))]),
|
||||
(" Starting from a boundary advances the anchor",
|
||||
vec![(1, Range::new(0, 0), Range::new(1, 9))]),
|
||||
("Long whitespace gap is bridged by the head",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 10))]),
|
||||
("Previous anchor is irrelevant for forward motions",
|
||||
vec![(1, Range::new(12, 0), Range::new(0, 8))]),
|
||||
(" Starting from whitespace moves to last space in sequence",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 3))]),
|
||||
("Starting from mid-word leaves anchor at start position and moves head",
|
||||
vec![(1, Range::new(3, 3), Range::new(3, 8))]),
|
||||
("Identifiers_with_underscores are considered a single word",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 28))]),
|
||||
("Jumping\n into starting whitespace selects the spaces before 'into'",
|
||||
vec![(1, Range::new(0, 6), Range::new(8, 11))]),
|
||||
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 11)),
|
||||
(1, Range::new(0, 11), Range::new(12, 14)),
|
||||
(1, Range::new(12, 14), Range::new(15, 17))
|
||||
]),
|
||||
("... ... punctuation and spaces behave as expected",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 5)),
|
||||
(1, Range::new(0, 5), Range::new(6, 9)),
|
||||
]),
|
||||
(".._.._ punctuation is not joined by underscores into a single block",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 1))]),
|
||||
("Newlines\n\nare bridged seamlessly.",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 7)),
|
||||
(1, Range::new(0, 7), Range::new(10, 13)),
|
||||
]),
|
||||
("Jumping\n\n\n\n\n\n from newlines to whitespace selects whitespace.",
|
||||
vec![
|
||||
(1, Range::new(0, 8), Range::new(13, 15)),
|
||||
]),
|
||||
("A failed motion does not modify the range",
|
||||
vec![
|
||||
(3, Range::new(37, 41), Range::new(37, 41)),
|
||||
]),
|
||||
("oh oh oh two character words!",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 2)),
|
||||
(1, Range::new(0, 2), Range::new(3, 5)),
|
||||
(1, Range::new(0, 1), Range::new(2, 2)),
|
||||
]),
|
||||
("Multiple motions at once resolve correctly",
|
||||
vec![
|
||||
(3, Range::new(0, 0), Range::new(17, 19)),
|
||||
]),
|
||||
("Excessive motions are performed partially",
|
||||
vec![
|
||||
(999, Range::new(0, 0), Range::new(32, 40)),
|
||||
]),
|
||||
("", // Edge case of moving forward in empty string
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
("\n\n\n\n\n", // Edge case of moving forward in all newlines
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 4)),
|
||||
]),
|
||||
("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(1, 3)),
|
||||
(1, Range::new(1, 3), Range::new(5, 7)),
|
||||
]),
|
||||
("ヒーリクス multibyte characters behave as normal characters",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 5)),
|
||||
]),
|
||||
]);
|
||||
|
||||
for (sample, scenario) in tests {
|
||||
for (count, begin, expected_end) in scenario.into_iter() {
|
||||
let range = move_next_word_start(Rope::from(sample).slice(..), begin, count);
|
||||
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_behaviour_when_moving_to_start_of_previous_words() {
|
||||
let tests = array::IntoIter::new([
|
||||
("Basic backward motion from the middle of a word",
|
||||
vec![(1, Range::new(3, 3), Range::new(3, 0))]),
|
||||
("Starting from after boundary retreats the anchor",
|
||||
vec![(1, Range::new(0, 8), Range::new(7, 0))]),
|
||||
(" Jump to start of a word preceded by whitespace",
|
||||
vec![(1, Range::new(5, 5), Range::new(5, 4))]),
|
||||
(" Jump to start of line from start of word preceded by whitespace",
|
||||
vec![(1, Range::new(4, 4), Range::new(3, 0))]),
|
||||
("Previous anchor is irrelevant for backward motions",
|
||||
vec![(1, Range::new(12, 5), Range::new(5, 0))]),
|
||||
(" Starting from whitespace moves to first space in sequence",
|
||||
vec![(1, Range::new(0, 3), Range::new(3, 0))]),
|
||||
("Identifiers_with_underscores are considered a single word",
|
||||
vec![(1, Range::new(0, 20), Range::new(20, 0))]),
|
||||
("Jumping\n \nback through a newline selects whitespace",
|
||||
vec![(1, Range::new(0, 13), Range::new(11, 8))]),
|
||||
("Jumping to start of word from the end selects the word",
|
||||
vec![(1, Range::new(6, 6), Range::new(6, 0))]),
|
||||
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
|
||||
vec![
|
||||
(1, Range::new(30, 30), Range::new(30, 21)),
|
||||
(1, Range::new(30, 21), Range::new(20, 18)),
|
||||
(1, Range::new(20, 18), Range::new(17, 15))
|
||||
]),
|
||||
|
||||
("... ... punctuation and spaces behave as expected",
|
||||
vec![
|
||||
(1, Range::new(0, 10), Range::new(9, 6)),
|
||||
(1, Range::new(9, 6), Range::new(5, 0)),
|
||||
]),
|
||||
(".._.._ punctuation is not joined by underscores into a single block",
|
||||
vec![(1, Range::new(0, 5), Range::new(4, 3))]),
|
||||
("Newlines\n\nare bridged seamlessly.",
|
||||
vec![
|
||||
(1, Range::new(0, 10), Range::new(7, 0)),
|
||||
]),
|
||||
("Jumping \n\n\n\n\nback from within a newline group selects previous block",
|
||||
vec![
|
||||
(1, Range::new(0, 13), Range::new(10, 0)),
|
||||
]),
|
||||
("Failed motions do not modify the range",
|
||||
vec![
|
||||
(0, Range::new(3, 0), Range::new(3, 0)),
|
||||
]),
|
||||
("Multiple motions at once resolve correctly",
|
||||
vec![
|
||||
(3, Range::new(18, 18), Range::new(8, 0)),
|
||||
]),
|
||||
("Excessive motions are performed partially",
|
||||
vec![
|
||||
(999, Range::new(40, 40), Range::new(9, 0)),
|
||||
]),
|
||||
("", // Edge case of moving backwards in empty string
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
("\n\n\n\n\n", // Edge case of moving backwards in all newlines
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
(" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
|
||||
vec![
|
||||
(1, Range::new(0, 7), Range::new(6, 4)),
|
||||
(1, Range::new(6, 4), Range::new(2, 0)),
|
||||
]),
|
||||
("ヒーリクス multibyte characters behave as normal characters",
|
||||
vec![
|
||||
(1, Range::new(0, 5), Range::new(4, 0)),
|
||||
]),
|
||||
]);
|
||||
|
||||
for (sample, scenario) in tests {
|
||||
for (count, begin, expected_end) in scenario.into_iter() {
|
||||
let range = move_prev_word_start(Rope::from(sample).slice(..), begin, count);
|
||||
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_behaviour_when_moving_to_end_of_next_words() {
|
||||
let tests = array::IntoIter::new([
|
||||
("Basic forward motion from the start of a word to the end of it",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 4))]),
|
||||
("Basic forward motion from the end of a word to the end of the next",
|
||||
vec![(1, Range::new(0, 4), Range::new(5, 12))]),
|
||||
("Basic forward motion from the middle of a word to the end of it",
|
||||
vec![(1, Range::new(2, 2), Range::new(2, 4))]),
|
||||
(" Jumping to end of a word preceded by whitespace",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 10))]),
|
||||
(" Starting from a boundary advances the anchor",
|
||||
vec![(1, Range::new(0, 0), Range::new(1, 8))]),
|
||||
("Previous anchor is irrelevant for end of word motion",
|
||||
vec![(1, Range::new(12, 2), Range::new(2, 7))]),
|
||||
("Identifiers_with_underscores are considered a single word",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 27))]),
|
||||
("Jumping\n into starting whitespace selects up to the end of next word",
|
||||
vec![(1, Range::new(0, 6), Range::new(8, 15))]),
|
||||
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 11)),
|
||||
(1, Range::new(0, 11), Range::new(12, 14)),
|
||||
(1, Range::new(12, 14), Range::new(15, 17))
|
||||
]),
|
||||
("... ... punctuation and spaces behave as expected",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 2)),
|
||||
(1, Range::new(0, 2), Range::new(3, 8)),
|
||||
]),
|
||||
(".._.._ punctuation is not joined by underscores into a single block",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 1))]),
|
||||
("Newlines\n\nare bridged seamlessly.",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 7)),
|
||||
(1, Range::new(0, 7), Range::new(10, 12)),
|
||||
]),
|
||||
("Jumping\n\n\n\n\n\n from newlines to whitespace selects to end of next word.",
|
||||
vec![
|
||||
(1, Range::new(0, 8), Range::new(13, 19)),
|
||||
]),
|
||||
("A failed motion does not modify the range",
|
||||
vec![
|
||||
(3, Range::new(37, 41), Range::new(37, 41)),
|
||||
]),
|
||||
("Multiple motions at once resolve correctly",
|
||||
vec![
|
||||
(3, Range::new(0, 0), Range::new(16, 18)),
|
||||
]),
|
||||
("Excessive motions are performed partially",
|
||||
vec![
|
||||
(999, Range::new(0, 0), Range::new(31, 40)),
|
||||
]),
|
||||
("", // Edge case of moving forward in empty string
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
("\n\n\n\n\n", // Edge case of moving forward in all newlines
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 4)),
|
||||
]),
|
||||
("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(1, 3)),
|
||||
(1, Range::new(1, 3), Range::new(5, 7)),
|
||||
]),
|
||||
("ヒーリクス multibyte characters behave as normal characters",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 4)),
|
||||
]),
|
||||
]);
|
||||
|
||||
for (sample, scenario) in tests {
|
||||
for (count, begin, expected_end) in scenario.into_iter() {
|
||||
let range = move_next_word_end(Rope::from(sample).slice(..), begin, count);
|
||||
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_categorize() {
|
||||
const WORD_TEST_CASE: &'static str =
|
||||
"_hello_world_あいうえおー12345678901234567890";
|
||||
const PUNCTUATION_TEST_CASE: &'static str =
|
||||
"!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
|
||||
const WHITESPACE_TEST_CASE: &'static str = " ";
|
||||
|
||||
assert_eq!(Category::Eol, categorize('\n'));
|
||||
|
||||
for ch in WHITESPACE_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
Category::Whitespace,
|
||||
categorize(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Whitespace`",
|
||||
ch,
|
||||
categorize(ch)
|
||||
);
|
||||
}
|
||||
|
||||
for ch in WORD_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
Category::Word,
|
||||
categorize(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Word`",
|
||||
ch,
|
||||
categorize(ch)
|
||||
);
|
||||
}
|
||||
|
||||
for ch in PUNCTUATION_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
Category::Punctuation,
|
||||
categorize(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Punctuation`",
|
||||
ch,
|
||||
categorize(ch)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,16 +6,15 @@ use std::{collections::HashMap, sync::RwLock};
|
||||
static REGISTRY: Lazy<RwLock<HashMap<char, Vec<String>>>> =
|
||||
Lazy::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
pub fn get(register: char) -> Option<Vec<String>> {
|
||||
/// Read register values.
|
||||
pub fn get(register_name: char) -> Option<Vec<String>> {
|
||||
let registry = REGISTRY.read().unwrap();
|
||||
|
||||
// TODO: no cloning
|
||||
registry.get(®ister).cloned()
|
||||
registry.get(®ister_name).cloned() // TODO: no cloning
|
||||
}
|
||||
|
||||
/// Read register values.
|
||||
// restoring: bool
|
||||
pub fn set(register: char, values: Vec<String>) {
|
||||
pub fn set(register_name: char, values: Vec<String>) {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
|
||||
registry.insert(register, values);
|
||||
registry.insert(register_name, values);
|
||||
}
|
||||
|
@@ -7,6 +7,10 @@ pub fn find_nth_next(
|
||||
n: usize,
|
||||
inclusive: bool,
|
||||
) -> Option<usize> {
|
||||
if pos >= text.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// start searching right after pos
|
||||
let mut chars = text.chars_at(pos + 1);
|
||||
|
||||
@@ -37,7 +41,7 @@ pub fn find_nth_prev(
|
||||
inclusive: bool,
|
||||
) -> Option<usize> {
|
||||
// start searching right before pos
|
||||
let mut chars = text.chars_at(pos.saturating_sub(1));
|
||||
let mut chars = text.chars_at(pos);
|
||||
|
||||
for _ in 0..n {
|
||||
loop {
|
||||
@@ -52,7 +56,7 @@ pub fn find_nth_prev(
|
||||
}
|
||||
|
||||
if !inclusive {
|
||||
pos -= 1;
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
Some(pos)
|
||||
|
@@ -35,6 +35,10 @@ impl Range {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn point(head: usize) -> Self {
|
||||
Self::new(head, head)
|
||||
}
|
||||
|
||||
/// Start of the range.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
@@ -383,7 +387,7 @@ pub fn split_on_matches(
|
||||
// TODO: retain range direction
|
||||
|
||||
let end = text.byte_to_char(start_byte + mat.start());
|
||||
result.push(Range::new(start, end - 1));
|
||||
result.push(Range::new(start, end.saturating_sub(1)));
|
||||
start = text.byte_to_char(start_byte + mat.end());
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use crate::{Rope, Selection};
|
||||
|
||||
/// A state represents the current editor state of a single buffer.
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub doc: Rope,
|
||||
pub selection: Selection,
|
||||
|
@@ -5,6 +5,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -12,13 +13,13 @@ use std::{
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Configuration {
|
||||
pub language: Vec<LanguageConfiguration>,
|
||||
}
|
||||
|
||||
// largely based on tree-sitter/cli/src/loader.rs
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageConfiguration {
|
||||
#[serde(rename = "name")]
|
||||
@@ -27,8 +28,8 @@ pub struct LanguageConfiguration {
|
||||
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
|
||||
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
|
||||
|
||||
// pub path: PathBuf,
|
||||
// root_path for tree-sitter (^)
|
||||
#[serde(default)]
|
||||
pub auto_format: bool,
|
||||
|
||||
// content_regex
|
||||
// injection_regex
|
||||
@@ -46,7 +47,7 @@ pub struct LanguageConfiguration {
|
||||
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageServerConfiguration {
|
||||
pub command: String,
|
||||
@@ -55,14 +56,14 @@ pub struct LanguageServerConfiguration {
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IndentationConfiguration {
|
||||
pub tab_width: usize,
|
||||
pub unit: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IndentQuery {
|
||||
#[serde(default)]
|
||||
@@ -73,16 +74,48 @@ pub struct IndentQuery {
|
||||
pub outdent: HashSet<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "embed_runtime"))]
|
||||
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||
let path = crate::RUNTIME_DIR
|
||||
.join("queries")
|
||||
.join(language)
|
||||
.join(filename);
|
||||
std::fs::read_to_string(&path)
|
||||
}
|
||||
|
||||
#[cfg(feature = "embed_runtime")]
|
||||
fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
use std::fmt;
|
||||
|
||||
#[derive(rust_embed::RustEmbed)]
|
||||
#[folder = "../runtime/"]
|
||||
struct Runtime;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EmbeddedFileNotFoundError {
|
||||
path: PathBuf,
|
||||
}
|
||||
impl std::error::Error for EmbeddedFileNotFoundError {}
|
||||
impl fmt::Display for EmbeddedFileNotFoundError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "failed to load embedded file {}", self.path.display())
|
||||
}
|
||||
}
|
||||
|
||||
let path = PathBuf::from("queries").join(language).join(filename);
|
||||
|
||||
if let Some(query_bytes) = Runtime::get(&path.display().to_string()) {
|
||||
String::from_utf8(query_bytes.to_vec()).map_err(|err| err.into())
|
||||
} else {
|
||||
Err(Box::new(EmbeddedFileNotFoundError { path }))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_query(language: &str, filename: &str) -> String {
|
||||
static INHERITS_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
|
||||
|
||||
let root = crate::runtime_dir();
|
||||
// let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
let path = root.join("queries").join(language).join(filename);
|
||||
|
||||
let query = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
let query = load_runtime_file(language, filename).unwrap_or_default();
|
||||
|
||||
// TODO: the collect() is not ideal
|
||||
let inherits = INHERITS_REGEX
|
||||
@@ -146,11 +179,8 @@ impl LanguageConfiguration {
|
||||
.get_or_init(|| {
|
||||
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
||||
|
||||
let root = crate::runtime_dir();
|
||||
let path = root.join("queries").join(language).join("indents.toml");
|
||||
|
||||
let toml = std::fs::read(&path).ok()?;
|
||||
toml::from_slice(&toml).ok()
|
||||
let toml = load_runtime_file(&language, "indents.toml").ok()?;
|
||||
toml::from_slice(&toml.as_bytes()).ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
@@ -162,17 +192,20 @@ impl LanguageConfiguration {
|
||||
|
||||
pub static LOADER: OnceCell<Loader> = OnceCell::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Loader {
|
||||
// highlight_names ?
|
||||
language_configs: Vec<Arc<LanguageConfiguration>>,
|
||||
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
|
||||
scopes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn new(config: Configuration) -> Self {
|
||||
pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
|
||||
let mut loader = Self {
|
||||
language_configs: Vec::new(),
|
||||
language_config_ids_by_file_type: HashMap::new(),
|
||||
scopes,
|
||||
};
|
||||
|
||||
for config in config.language {
|
||||
@@ -192,6 +225,10 @@ impl Loader {
|
||||
loader
|
||||
}
|
||||
|
||||
pub fn scopes(&self) -> &[String] {
|
||||
&self.scopes
|
||||
}
|
||||
|
||||
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
|
||||
// Find all the language configurations that match this file name
|
||||
// or a suffix of the file name.
|
||||
@@ -223,6 +260,12 @@ pub struct TsParser {
|
||||
cursors: Vec<QueryCursor>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for TsParser {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TsParser").finish()
|
||||
}
|
||||
}
|
||||
|
||||
// could also just use a pool, or a single instance?
|
||||
thread_local! {
|
||||
pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
|
||||
@@ -231,6 +274,7 @@ thread_local! {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Syntax {
|
||||
config: Arc<HighlightConfiguration>,
|
||||
|
||||
@@ -333,7 +377,11 @@ impl Syntax {
|
||||
// prevents them from being moved. But both of these values are really just
|
||||
// pointers, so it's actually ok to move them.
|
||||
|
||||
let mut cursor = QueryCursor::new(); // reuse a pool
|
||||
// reuse a cursor from the pool if possible
|
||||
let mut cursor = PARSER.with(|ts_parser| {
|
||||
let highlighter = &mut ts_parser.borrow_mut();
|
||||
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
|
||||
});
|
||||
let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
|
||||
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
|
||||
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
|
||||
@@ -407,6 +455,7 @@ impl Syntax {
|
||||
// buffer_range_for_scope_at_pos
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LanguageLayer {
|
||||
// mode
|
||||
// grammar
|
||||
@@ -715,6 +764,7 @@ pub enum HighlightEvent {
|
||||
/// Contains the data neeeded to higlight code written in a particular language.
|
||||
///
|
||||
/// This struct is immutable and can be shared between threads.
|
||||
#[derive(Debug)]
|
||||
pub struct HighlightConfiguration {
|
||||
pub language: Grammar,
|
||||
pub query: Query,
|
||||
@@ -745,7 +795,8 @@ struct LocalScope<'a> {
|
||||
local_defs: Vec<LocalDef<'a>>,
|
||||
}
|
||||
|
||||
struct HighlightIter<'a, F>
|
||||
#[derive(Debug)]
|
||||
struct HighlightIter<'a, 'tree: 'a, F>
|
||||
where
|
||||
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
|
||||
{
|
||||
@@ -753,16 +804,16 @@ where
|
||||
byte_offset: usize,
|
||||
injection_callback: F,
|
||||
cancellation_flag: Option<&'a AtomicUsize>,
|
||||
layers: Vec<HighlightIterLayer<'a>>,
|
||||
layers: Vec<HighlightIterLayer<'a, 'tree>>,
|
||||
iter_count: usize,
|
||||
next_event: Option<HighlightEvent>,
|
||||
last_highlight_range: Option<(usize, usize, usize)>,
|
||||
}
|
||||
|
||||
struct HighlightIterLayer<'a> {
|
||||
struct HighlightIterLayer<'a, 'tree: 'a> {
|
||||
_tree: Option<Tree>,
|
||||
cursor: QueryCursor,
|
||||
captures: iter::Peekable<QueryCaptures<'a, Cow<'a, [u8]>>>,
|
||||
captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>,
|
||||
config: &'a HighlightConfiguration,
|
||||
highlight_end_stack: Vec<usize>,
|
||||
scope_stack: Vec<LocalScope<'a>>,
|
||||
@@ -770,6 +821,12 @@ struct HighlightIterLayer<'a> {
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HighlightIterLayer").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightConfiguration {
|
||||
/// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting
|
||||
/// queries.
|
||||
@@ -929,7 +986,7 @@ impl HighlightConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> HighlightIterLayer<'a> {
|
||||
impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
|
||||
/// Create a new 'layer' of highlighting for this document.
|
||||
///
|
||||
/// In the even that the new layer contains "combined injections" (injections where multiple
|
||||
@@ -1193,7 +1250,7 @@ impl<'a> HighlightIterLayer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F> HighlightIter<'a, F>
|
||||
impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F>
|
||||
where
|
||||
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
|
||||
{
|
||||
@@ -1244,7 +1301,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
|
||||
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) {
|
||||
if let Some(sort_key) = layer.sort_key() {
|
||||
let mut i = 1;
|
||||
while i < self.layers.len() {
|
||||
@@ -1263,7 +1320,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F> Iterator for HighlightIter<'a, F>
|
||||
impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F>
|
||||
where
|
||||
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
|
||||
{
|
||||
@@ -1700,3 +1757,13 @@ fn test_input_edits() {
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_runtime_file() {
|
||||
// Test to make sure we can load some data from the runtime directory.
|
||||
let contents = load_runtime_file("rust", "indents.toml").unwrap();
|
||||
assert!(!contents.is_empty());
|
||||
|
||||
let results = load_runtime_file("rust", "does-not-exist");
|
||||
assert!(results.is_err());
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ pub enum Operation {
|
||||
Insert(Tendril),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Assoc {
|
||||
Before,
|
||||
After,
|
||||
@@ -90,7 +90,8 @@ impl ChangeSet {
|
||||
return;
|
||||
}
|
||||
|
||||
self.len_after += fragment.len();
|
||||
// Avoiding std::str::len() to account for UTF-8 characters.
|
||||
self.len_after += fragment.chars().count();
|
||||
|
||||
let new_last = match self.changes.as_mut_slice() {
|
||||
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
|
||||
@@ -415,7 +416,7 @@ impl ChangeSet {
|
||||
|
||||
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
|
||||
/// a single transaction.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Transaction {
|
||||
changes: ChangeSet,
|
||||
selection: Option<Selection>,
|
||||
@@ -754,4 +755,21 @@ mod test {
|
||||
use Operation::*;
|
||||
assert_eq!(changes.changes, &[Insert("a".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_with_utf8() {
|
||||
const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
|
||||
|
||||
let empty = Rope::from("");
|
||||
let mut a = ChangeSet::new(&empty);
|
||||
|
||||
let mut b = ChangeSet::new(&empty);
|
||||
b.insert(TEST_CASE.into());
|
||||
|
||||
let changes = a.compose(b);
|
||||
|
||||
use Operation::*;
|
||||
assert_eq!(changes.changes, &[Insert(TEST_CASE.into())]);
|
||||
assert_eq!(changes.len_after, TEST_CASE.chars().count());
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-lsp"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -10,20 +10,14 @@ license = "MPL-2.0"
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
|
||||
once_cell = "1.4"
|
||||
|
||||
lsp-types = { version = "0.89", features = ["proposed"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1.5"
|
||||
futures-executor = { version = "0.3" }
|
||||
url = "2"
|
||||
pathdiff = "0.2"
|
||||
glob = "0.3"
|
||||
anyhow = "1"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
# jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
|
||||
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc", default-features = false } # don't pull in all of futures
|
||||
anyhow = "1.0"
|
||||
futures-executor = "0.3"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
|
||||
log = "0.4"
|
||||
lsp-types = { version = "0.89", features = ["proposed"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
log = "~0.4"
|
||||
tokio = { version = "1.6", features = ["full"] }
|
||||
tokio-stream = "0.1.6"
|
@@ -3,31 +3,24 @@ use crate::{
|
||||
Call, Error, OffsetEncoding, Result,
|
||||
};
|
||||
|
||||
use helix_core::{ChangeSet, Rope};
|
||||
|
||||
// use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use helix_core::{find_root, ChangeSet, Rope};
|
||||
use jsonrpc_core as jsonrpc;
|
||||
use lsp_types as lsp;
|
||||
use serde_json::Value;
|
||||
|
||||
use std::future::Future;
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use tokio::{
|
||||
io::{BufReader, BufWriter},
|
||||
// prelude::*,
|
||||
process::{Child, Command},
|
||||
sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
_process: Child,
|
||||
|
||||
outgoing: UnboundedSender<Payload>,
|
||||
// pub incoming: Receiver<Call>,
|
||||
pub request_counter: AtomicU64,
|
||||
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
request_counter: AtomicU64,
|
||||
capabilities: Option<lsp::ServerCapabilities>,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
@@ -43,40 +36,27 @@ impl Client {
|
||||
.kill_on_drop(true)
|
||||
.spawn();
|
||||
|
||||
// use std::io::ErrorKind;
|
||||
let mut process = match process {
|
||||
Ok(process) => process,
|
||||
Err(err) => match err.kind() {
|
||||
// ErrorKind::NotFound | ErrorKind::PermissionDenied => {
|
||||
// return Err(Error::Other(err.into()))
|
||||
// }
|
||||
_kind => return Err(Error::Other(err.into())),
|
||||
},
|
||||
};
|
||||
let mut process = process?;
|
||||
|
||||
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
|
||||
let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
|
||||
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
|
||||
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
|
||||
|
||||
let (incoming, outgoing) = Transport::start(reader, writer, stderr);
|
||||
let (server_rx, server_tx) = Transport::start(reader, writer, stderr);
|
||||
|
||||
let client = Self {
|
||||
_process: process,
|
||||
|
||||
outgoing,
|
||||
// incoming,
|
||||
server_tx,
|
||||
request_counter: AtomicU64::new(0),
|
||||
|
||||
capabilities: None,
|
||||
// diagnostics: HashMap::new(),
|
||||
offset_encoding: OffsetEncoding::Utf8,
|
||||
};
|
||||
|
||||
// TODO: async client.initialize()
|
||||
// maybe use an arc<atomic> flag
|
||||
|
||||
Ok((client, incoming))
|
||||
Ok((client, server_rx))
|
||||
}
|
||||
|
||||
fn next_request_id(&self) -> jsonrpc::Id {
|
||||
@@ -106,7 +86,7 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Execute a RPC request on the language server.
|
||||
pub async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
|
||||
async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
R::Result: core::fmt::Debug, // TODO: temporary
|
||||
@@ -118,17 +98,20 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Execute a RPC request on the language server.
|
||||
pub fn call<R: lsp::request::Request>(
|
||||
fn call<R: lsp::request::Request>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
let outgoing = self.outgoing.clone();
|
||||
let server_tx = self.server_tx.clone();
|
||||
let id = self.next_request_id();
|
||||
|
||||
async move {
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
let params = serde_json::to_value(params)?;
|
||||
|
||||
let request = jsonrpc::MethodCall {
|
||||
@@ -140,32 +123,29 @@ impl Client {
|
||||
|
||||
let (tx, mut rx) = channel::<Result<Value>>(1);
|
||||
|
||||
outgoing
|
||||
server_tx
|
||||
.send(Payload::Request {
|
||||
chan: tx,
|
||||
value: request,
|
||||
})
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
timeout(Duration::from_secs(2), rx.recv())
|
||||
.await
|
||||
.map_err(|_| Error::Timeout)? // return Timeout
|
||||
.unwrap() // TODO: None if channel closed
|
||||
.ok_or(Error::StreamClosed)?
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a RPC notification to the language server.
|
||||
pub fn notify<R: lsp::notification::Notification>(
|
||||
fn notify<R: lsp::notification::Notification>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
) -> impl Future<Output = Result<()>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
let outgoing = self.outgoing.clone();
|
||||
let server_tx = self.server_tx.clone();
|
||||
|
||||
async move {
|
||||
let params = serde_json::to_value(params)?;
|
||||
@@ -176,7 +156,7 @@ impl Client {
|
||||
params: Self::value_into_params(params),
|
||||
};
|
||||
|
||||
outgoing
|
||||
server_tx
|
||||
.send(Payload::Notification(notification))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
@@ -205,7 +185,7 @@ impl Client {
|
||||
}),
|
||||
};
|
||||
|
||||
self.outgoing
|
||||
self.server_tx
|
||||
.send(Payload::Response(output))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
@@ -216,15 +196,16 @@ impl Client {
|
||||
// General messages
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
pub async fn initialize(&mut self) -> Result<()> {
|
||||
pub(crate) async fn initialize(&mut self) -> Result<()> {
|
||||
// TODO: delay any requests that are triggered prior to initialize
|
||||
let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
|
||||
|
||||
#[allow(deprecated)]
|
||||
let params = lsp::InitializeParams {
|
||||
process_id: Some(std::process::id()),
|
||||
// root_path is obsolete, use root_uri
|
||||
root_path: None,
|
||||
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
|
||||
root_uri: None, // set to project root in the future
|
||||
root_uri: root,
|
||||
initialization_options: None,
|
||||
capabilities: lsp::ClientCapabilities {
|
||||
text_document: Some(lsp::TextDocumentClientCapabilities {
|
||||
@@ -247,6 +228,11 @@ impl Client {
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
window: Some(lsp::WindowClientCapabilities {
|
||||
// TODO: temporarily disabled until we implement handling for window/workDoneProgress/create
|
||||
// work_done_progress: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
trace: None,
|
||||
@@ -674,4 +660,17 @@ impl Client {
|
||||
|
||||
self.call::<lsp::request::References>(params)
|
||||
}
|
||||
|
||||
pub fn document_symbols(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
let params = lsp::DocumentSymbolParams {
|
||||
text_document,
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
|
||||
partial_result_params: lsp::PartialResultParams::default(),
|
||||
};
|
||||
|
||||
self.call::<lsp::request::DocumentSymbolRequest>(params)
|
||||
}
|
||||
}
|
||||
|
@@ -1,26 +1,27 @@
|
||||
mod client;
|
||||
mod select_all;
|
||||
mod transport;
|
||||
|
||||
pub use client::Client;
|
||||
pub use futures_executor::block_on;
|
||||
pub use jsonrpc::Call;
|
||||
pub use jsonrpc_core as jsonrpc;
|
||||
pub use lsp::{Position, Url};
|
||||
pub use lsp_types as lsp;
|
||||
|
||||
pub use client::Client;
|
||||
pub use lsp::{Position, Url};
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use helix_core::syntax::LanguageConfiguration;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
pub use futures_executor::block_on;
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
type LanguageId = String;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
@@ -28,8 +29,14 @@ pub enum Error {
|
||||
Rpc(#[from] jsonrpc::Error),
|
||||
#[error("failed to parse: {0}")]
|
||||
Parse(#[from] serde_json::Error),
|
||||
#[error("IO Error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error("request timed out")]
|
||||
Timeout,
|
||||
#[error("server closed the stream")]
|
||||
StreamClosed,
|
||||
#[error("LSP not defined")]
|
||||
LspNotDefined,
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -48,23 +55,54 @@ pub mod util {
|
||||
use super::*;
|
||||
use helix_core::{Range, Rope, Transaction};
|
||||
|
||||
/// Converts [`lsp::Position`] to a position in the document.
|
||||
///
|
||||
/// Returns `None` if position exceeds document length or an operation overflows.
|
||||
pub fn lsp_pos_to_pos(
|
||||
doc: &Rope,
|
||||
pos: lsp::Position,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) -> usize {
|
||||
) -> Option<usize> {
|
||||
let max_line = doc.lines().count().saturating_sub(1);
|
||||
let pos_line = pos.line as usize;
|
||||
let pos_line = if pos_line > max_line {
|
||||
return None;
|
||||
} else {
|
||||
pos_line
|
||||
};
|
||||
match offset_encoding {
|
||||
OffsetEncoding::Utf8 => {
|
||||
let line = doc.line_to_char(pos.line as usize);
|
||||
line + pos.character as usize
|
||||
let max_char = doc
|
||||
.line_to_char(max_line)
|
||||
.checked_add(doc.line(max_line).len_chars())?;
|
||||
let line = doc.line_to_char(pos_line);
|
||||
let pos = line.checked_add(pos.character as usize)?;
|
||||
if pos <= max_char {
|
||||
Some(pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OffsetEncoding::Utf16 => {
|
||||
let line = doc.line_to_char(pos.line as usize);
|
||||
let max_char = doc
|
||||
.line_to_char(max_line)
|
||||
.checked_add(doc.line(max_line).len_chars())?;
|
||||
let max_cu = doc.char_to_utf16_cu(max_char);
|
||||
let line = doc.line_to_char(pos_line);
|
||||
let line_start = doc.char_to_utf16_cu(line);
|
||||
doc.utf16_cu_to_char(line_start + pos.character as usize)
|
||||
let pos = line_start.checked_add(pos.character as usize)?;
|
||||
if pos <= max_cu {
|
||||
Some(doc.utf16_cu_to_char(pos))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts position in the document to [`lsp::Position`].
|
||||
///
|
||||
/// Panics when `pos` is out of `doc` bounds or operation overflows.
|
||||
pub fn pos_to_lsp_pos(
|
||||
doc: &Rope,
|
||||
pos: usize,
|
||||
@@ -88,6 +126,7 @@ pub mod util {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a range in the document to [`lsp::Range`].
|
||||
pub fn range_to_lsp_range(
|
||||
doc: &Rope,
|
||||
range: Range,
|
||||
@@ -99,6 +138,17 @@ pub mod util {
|
||||
lsp::Range::new(start, end)
|
||||
}
|
||||
|
||||
pub fn lsp_range_to_range(
|
||||
doc: &Rope,
|
||||
range: lsp::Range,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) -> Option<Range> {
|
||||
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
|
||||
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
|
||||
|
||||
Some(Range::new(start, end))
|
||||
}
|
||||
|
||||
pub fn generate_transaction_from_edits(
|
||||
doc: &Rope,
|
||||
edits: Vec<lsp::TextEdit>,
|
||||
@@ -114,14 +164,21 @@ pub mod util {
|
||||
None
|
||||
};
|
||||
|
||||
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding);
|
||||
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding);
|
||||
let start =
|
||||
if let Some(start) = lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
|
||||
start
|
||||
} else {
|
||||
return (0, 0, None);
|
||||
};
|
||||
let end = if let Some(end) = lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
|
||||
end
|
||||
} else {
|
||||
return (0, 0, None);
|
||||
};
|
||||
(start, end, replacement)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// apply_insert_replace_edit
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@@ -129,6 +186,7 @@ pub enum Notification {
|
||||
PublishDiagnostics(lsp::PublishDiagnosticsParams),
|
||||
ShowMessage(lsp::ShowMessageParams),
|
||||
LogMessage(lsp::LogMessageParams),
|
||||
ProgressMessage(lsp::ProgressParams),
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
@@ -146,17 +204,20 @@ impl Notification {
|
||||
}
|
||||
|
||||
lsp::notification::ShowMessage::METHOD => {
|
||||
let params: lsp::ShowMessageParams =
|
||||
params.parse().expect("Failed to parse ShowMessage params");
|
||||
let params: lsp::ShowMessageParams = params.parse().ok()?;
|
||||
|
||||
Self::ShowMessage(params)
|
||||
}
|
||||
lsp::notification::LogMessage::METHOD => {
|
||||
let params: lsp::LogMessageParams =
|
||||
params.parse().expect("Failed to parse ShowMessage params");
|
||||
let params: lsp::LogMessageParams = params.parse().ok()?;
|
||||
|
||||
Self::LogMessage(params)
|
||||
}
|
||||
lsp::notification::Progress::METHOD => {
|
||||
let params: lsp::ProgressParams = params.parse().ok()?;
|
||||
|
||||
Self::ProgressMessage(params)
|
||||
}
|
||||
_ => {
|
||||
log::error!("unhandled LSP notification: {}", method);
|
||||
return None;
|
||||
@@ -167,14 +228,9 @@ impl Notification {
|
||||
}
|
||||
}
|
||||
|
||||
pub use jsonrpc::Call;
|
||||
|
||||
type LanguageId = String;
|
||||
|
||||
use crate::select_all::SelectAll;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Registry {
|
||||
inner: HashMap<LanguageId, Option<Arc<Client>>>,
|
||||
inner: HashMap<LanguageId, Arc<Client>>,
|
||||
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<Call>>,
|
||||
}
|
||||
@@ -193,35 +249,29 @@ impl Registry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Option<Arc<Client>> {
|
||||
// TODO: propagate the error
|
||||
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
|
||||
if let Some(config) = &language_config.language_server {
|
||||
// avoid borrow issues
|
||||
let inner = &mut self.inner;
|
||||
let s_incoming = &self.incoming;
|
||||
|
||||
let language_server = inner
|
||||
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769
|
||||
.or_insert_with(|| {
|
||||
// TODO: lookup defaults for id (name, args)
|
||||
let s_incoming = &mut self.incoming;
|
||||
|
||||
match inner.entry(language_config.scope.clone()) {
|
||||
Entry::Occupied(language_server) => Ok(language_server.get().clone()),
|
||||
Entry::Vacant(entry) => {
|
||||
// initialize a new client
|
||||
let (mut client, incoming) =
|
||||
Client::start(&config.command, &config.args).ok()?;
|
||||
|
||||
let (mut client, incoming) = Client::start(&config.command, &config.args)?;
|
||||
// TODO: run this async without blocking
|
||||
futures_executor::block_on(client.initialize()).unwrap();
|
||||
|
||||
futures_executor::block_on(client.initialize())?;
|
||||
s_incoming.push(UnboundedReceiverStream::new(incoming));
|
||||
let client = Arc::new(client);
|
||||
|
||||
Some(Arc::new(client))
|
||||
})
|
||||
.clone();
|
||||
|
||||
return language_server;
|
||||
entry.insert(client.clone());
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(Error::LspNotDefined)
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,3 +300,34 @@ impl Registry {
|
||||
// there needs to be a way to process incoming lsp messages from all clients.
|
||||
// -> notifications need to be dispatched to wherever
|
||||
// -> requests need to generate a reply and travel back to the same lsp!
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{lsp, util::*, OffsetEncoding};
|
||||
use helix_core::Rope;
|
||||
|
||||
#[test]
|
||||
fn converts_lsp_pos_to_pos() {
|
||||
macro_rules! test_case {
|
||||
($doc:expr, ($x:expr, $y:expr) => $want:expr) => {
|
||||
let doc = Rope::from($doc);
|
||||
let pos = lsp::Position::new($x, $y);
|
||||
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf16));
|
||||
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf8))
|
||||
};
|
||||
}
|
||||
|
||||
test_case!("", (0, 0) => Some(0));
|
||||
test_case!("", (0, 1) => None);
|
||||
test_case!("", (1, 0) => None);
|
||||
test_case!("\n\n", (0, 0) => Some(0));
|
||||
test_case!("\n\n", (1, 0) => Some(1));
|
||||
test_case!("\n\n", (1, 1) => Some(2));
|
||||
test_case!("\n\n", (2, 0) => Some(2));
|
||||
test_case!("\n\n", (3, 0) => None);
|
||||
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
|
||||
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
|
||||
test_case!("test\n\n\n\ncase", (4, 5) => None);
|
||||
test_case!("", (u32::MAX, u32::MAX) => None);
|
||||
}
|
||||
}
|
||||
|
@@ -1,143 +0,0 @@
|
||||
//! An unbounded set of streams
|
||||
|
||||
use core::{
|
||||
fmt::{self, Debug},
|
||||
iter::FromIterator,
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use futures_util::stream::{FusedStream, FuturesUnordered, StreamExt, StreamFuture};
|
||||
use futures_util::{ready, stream::Stream};
|
||||
|
||||
/// An unbounded set of streams
|
||||
///
|
||||
/// This "combinator" provides the ability to maintain a set of streams
|
||||
/// and drive them all to completion.
|
||||
///
|
||||
/// Streams are pushed into this set and their realized values are
|
||||
/// yielded as they become ready. Streams will only be polled when they
|
||||
/// generate notifications. This allows to coordinate a large number of streams.
|
||||
///
|
||||
/// Note that you can create a ready-made `SelectAll` via the
|
||||
/// `select_all` function in the `stream` module, or you can start with an
|
||||
/// empty set with the `SelectAll::new` constructor.
|
||||
#[must_use = "streams do nothing unless polled"]
|
||||
pub struct SelectAll<St> {
|
||||
inner: FuturesUnordered<StreamFuture<St>>,
|
||||
}
|
||||
|
||||
impl<St: Debug> Debug for SelectAll<St> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "SelectAll {{ ... }}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<St: Stream + Unpin> SelectAll<St> {
|
||||
/// Constructs a new, empty `SelectAll`
|
||||
///
|
||||
/// The returned `SelectAll` does not contain any streams and, in this
|
||||
/// state, `SelectAll::poll` will return `Poll::Ready(None)`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: FuturesUnordered::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of streams contained in the set.
|
||||
///
|
||||
/// This represents the total number of in-flight streams.
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the set contains no streams
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Push a stream into the set.
|
||||
///
|
||||
/// This function submits the given stream to the set for managing. This
|
||||
/// function will not call `poll` on the submitted stream. The caller must
|
||||
/// ensure that `SelectAll::poll` is called in order to receive task
|
||||
/// notifications.
|
||||
pub fn push(&self, stream: St) {
|
||||
self.inner.push(stream.into_future());
|
||||
}
|
||||
}
|
||||
|
||||
impl<St: Stream + Unpin> Default for SelectAll<St> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<St: Stream + Unpin> Stream for SelectAll<St> {
|
||||
type Item = St::Item;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
match ready!(self.inner.poll_next_unpin(cx)) {
|
||||
Some((Some(item), remaining)) => {
|
||||
self.push(remaining);
|
||||
return Poll::Ready(Some(item));
|
||||
}
|
||||
Some((None, _)) => {
|
||||
// `FuturesUnordered` thinks it isn't terminated
|
||||
// because it yielded a Some.
|
||||
// We do not return, but poll `FuturesUnordered`
|
||||
// in the next loop iteration.
|
||||
}
|
||||
None => return Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<St: Stream + Unpin> FusedStream for SelectAll<St> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
self.inner.is_terminated()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a list of streams into a `Stream` of results from the streams.
|
||||
///
|
||||
/// This essentially takes a list of streams (e.g. a vector, an iterator, etc.)
|
||||
/// and bundles them together into a single stream.
|
||||
/// The stream will yield items as they become available on the underlying
|
||||
/// streams internally, in the order they become available.
|
||||
///
|
||||
/// Note that the returned set can also be used to dynamically push more
|
||||
/// futures into the set as they become available.
|
||||
///
|
||||
/// This function is only available when the `std` or `alloc` feature of this
|
||||
/// library is activated, and it is activated by default.
|
||||
pub fn select_all<I>(streams: I) -> SelectAll<I::Item>
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Stream + Unpin,
|
||||
{
|
||||
let set = SelectAll::new();
|
||||
|
||||
for stream in streams {
|
||||
set.push(stream);
|
||||
}
|
||||
|
||||
set
|
||||
}
|
||||
|
||||
impl<St: Stream + Unpin> FromIterator<St> for SelectAll<St> {
|
||||
fn from_iter<T: IntoIterator<Item = St>>(iter: T) -> Self {
|
||||
select_all(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<St: Stream + Unpin> Extend<St> for SelectAll<St> {
|
||||
fn extend<T: IntoIterator<Item = St>>(&mut self, iter: T) {
|
||||
for st in iter {
|
||||
self.push(st)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,15 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
|
||||
use log::{error, info};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
use crate::Result;
|
||||
use jsonrpc_core as jsonrpc;
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use tokio::{
|
||||
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||
process::{ChildStderr, ChildStdin, ChildStdout},
|
||||
@@ -26,47 +20,45 @@ pub enum Payload {
|
||||
Response(jsonrpc::Output),
|
||||
}
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
/// A type representing all possible values sent from the server to the client.
|
||||
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(untagged)]
|
||||
enum Message {
|
||||
enum ServerMessage {
|
||||
/// A regular JSON-RPC request output (single response).
|
||||
Output(jsonrpc::Output),
|
||||
/// A JSON-RPC request or notification.
|
||||
Call(jsonrpc::Call),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
incoming: UnboundedSender<jsonrpc::Call>,
|
||||
outgoing: UnboundedReceiver<Payload>,
|
||||
client_tx: UnboundedSender<jsonrpc::Call>,
|
||||
client_rx: UnboundedReceiver<Payload>,
|
||||
|
||||
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
|
||||
headers: HashMap<String, String>,
|
||||
|
||||
writer: BufWriter<ChildStdin>,
|
||||
reader: BufReader<ChildStdout>,
|
||||
stderr: BufReader<ChildStderr>,
|
||||
server_stdin: BufWriter<ChildStdin>,
|
||||
server_stdout: BufReader<ChildStdout>,
|
||||
server_stderr: BufReader<ChildStderr>,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn start(
|
||||
reader: BufReader<ChildStdout>,
|
||||
writer: BufWriter<ChildStdin>,
|
||||
stderr: BufReader<ChildStderr>,
|
||||
server_stdout: BufReader<ChildStdout>,
|
||||
server_stdin: BufWriter<ChildStdin>,
|
||||
server_stderr: BufReader<ChildStderr>,
|
||||
) -> (UnboundedReceiver<jsonrpc::Call>, UnboundedSender<Payload>) {
|
||||
let (incoming, rx) = unbounded_channel();
|
||||
let (tx, outgoing) = unbounded_channel();
|
||||
let (client_tx, rx) = unbounded_channel();
|
||||
let (tx, client_rx) = unbounded_channel();
|
||||
|
||||
let transport = Self {
|
||||
reader,
|
||||
writer,
|
||||
stderr,
|
||||
incoming,
|
||||
outgoing,
|
||||
server_stdout,
|
||||
server_stdin,
|
||||
server_stderr,
|
||||
client_tx,
|
||||
client_rx,
|
||||
pending_requests: HashMap::default(),
|
||||
headers: HashMap::default(),
|
||||
};
|
||||
|
||||
tokio::spawn(transport.duplex());
|
||||
@@ -74,105 +66,104 @@ impl Transport {
|
||||
(rx, tx)
|
||||
}
|
||||
|
||||
async fn recv(
|
||||
async fn recv_server_message(
|
||||
reader: &mut (impl AsyncBufRead + Unpin + Send),
|
||||
headers: &mut HashMap<String, String>,
|
||||
) -> core::result::Result<Message, std::io::Error> {
|
||||
// read headers
|
||||
buffer: &mut String,
|
||||
) -> Result<ServerMessage> {
|
||||
let mut content_length = None;
|
||||
loop {
|
||||
let mut header = String::new();
|
||||
// detect pipe closed if 0
|
||||
reader.read_line(&mut header).await?;
|
||||
let header = header.trim();
|
||||
buffer.truncate(0);
|
||||
reader.read_line(buffer).await?;
|
||||
let header = buffer.trim();
|
||||
|
||||
if header.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = header.split(": ").collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to parse header",
|
||||
));
|
||||
let mut parts = header.split(": ");
|
||||
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some("Content-Length"), Some(value), None) => {
|
||||
content_length = Some(value.parse().unwrap());
|
||||
}
|
||||
(Some(_), Some(_), None) => {}
|
||||
_ => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to parse header",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
headers.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
|
||||
// find content-length
|
||||
let content_length = headers.get("Content-Length").unwrap().parse().unwrap();
|
||||
let content_length = content_length.unwrap();
|
||||
|
||||
//TODO: reuse vector
|
||||
let mut content = vec![0; content_length];
|
||||
reader.read_exact(&mut content).await?;
|
||||
let msg = String::from_utf8(content).unwrap();
|
||||
|
||||
// read data
|
||||
|
||||
info!("<- {}", msg);
|
||||
|
||||
// try parsing as output (server response) or call (server request)
|
||||
let output: serde_json::Result<Message> = serde_json::from_str(&msg);
|
||||
let output: serde_json::Result<ServerMessage> = serde_json::from_str(&msg);
|
||||
|
||||
Ok(output?)
|
||||
}
|
||||
|
||||
async fn err(
|
||||
async fn recv_server_error(
|
||||
err: &mut (impl AsyncBufRead + Unpin + Send),
|
||||
) -> core::result::Result<(), std::io::Error> {
|
||||
let mut line = String::new();
|
||||
err.read_line(&mut line).await?;
|
||||
error!("err <- {}", line);
|
||||
buffer: &mut String,
|
||||
) -> Result<()> {
|
||||
buffer.truncate(0);
|
||||
err.read_line(buffer).await?;
|
||||
error!("err <- {}", buffer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_payload(&mut self, payload: Payload) -> io::Result<()> {
|
||||
match payload {
|
||||
async fn send_payload_to_server(&mut self, payload: Payload) -> Result<()> {
|
||||
//TODO: reuse string
|
||||
let json = match payload {
|
||||
Payload::Request { chan, value } => {
|
||||
self.pending_requests.insert(value.id.clone(), chan);
|
||||
|
||||
let json = serde_json::to_string(&value)?;
|
||||
self.send(json).await
|
||||
serde_json::to_string(&value)?
|
||||
}
|
||||
Payload::Notification(value) => {
|
||||
let json = serde_json::to_string(&value)?;
|
||||
self.send(json).await
|
||||
}
|
||||
Payload::Response(error) => {
|
||||
let json = serde_json::to_string(&error)?;
|
||||
self.send(json).await
|
||||
}
|
||||
}
|
||||
Payload::Notification(value) => serde_json::to_string(&value)?,
|
||||
Payload::Response(error) => serde_json::to_string(&error)?,
|
||||
};
|
||||
self.send_string_to_server(json).await
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, request: String) -> io::Result<()> {
|
||||
async fn send_string_to_server(&mut self, request: String) -> Result<()> {
|
||||
info!("-> {}", request);
|
||||
|
||||
// send the headers
|
||||
self.writer
|
||||
self.server_stdin
|
||||
.write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes())
|
||||
.await?;
|
||||
|
||||
// send the body
|
||||
self.writer.write_all(request.as_bytes()).await?;
|
||||
self.server_stdin.write_all(request.as_bytes()).await?;
|
||||
|
||||
self.writer.flush().await?;
|
||||
self.server_stdin.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
|
||||
async fn process_server_message(&mut self, msg: ServerMessage) -> Result<()> {
|
||||
match msg {
|
||||
Message::Output(output) => self.recv_response(output).await?,
|
||||
Message::Call(call) => {
|
||||
self.incoming.send(call).unwrap();
|
||||
ServerMessage::Output(output) => self.process_request_response(output).await?,
|
||||
ServerMessage::Call(call) => {
|
||||
self.client_tx.send(call).unwrap();
|
||||
// let notification = Notification::parse(&method, params);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_response(&mut self, output: jsonrpc::Output) -> io::Result<()> {
|
||||
async fn process_request_response(&mut self, output: jsonrpc::Output) -> Result<()> {
|
||||
let (id, result) = match output {
|
||||
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
|
||||
info!("<- {}", result);
|
||||
@@ -200,29 +191,33 @@ impl Transport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn duplex(mut self) {
|
||||
async fn duplex(mut self) {
|
||||
let mut recv_buffer = String::new();
|
||||
let mut err_buffer = String::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
// client -> server
|
||||
msg = self.outgoing.recv() => {
|
||||
if msg.is_none() {
|
||||
break;
|
||||
msg = self.client_rx.recv() => {
|
||||
match msg {
|
||||
Some(msg) => {
|
||||
self.send_payload_to_server(msg).await.unwrap()
|
||||
},
|
||||
None => break
|
||||
}
|
||||
let msg = msg.unwrap();
|
||||
|
||||
self.send_payload(msg).await.unwrap();
|
||||
}
|
||||
// server <- client
|
||||
msg = Self::recv(&mut self.reader, &mut self.headers) => {
|
||||
if msg.is_err() {
|
||||
error!("err: <- {:?}", msg);
|
||||
break;
|
||||
// server -> client
|
||||
msg = Self::recv_server_message(&mut self.server_stdout, &mut recv_buffer) => {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
self.process_server_message(msg).await.unwrap();
|
||||
}
|
||||
Err(_) => {
|
||||
error!("err: <- {:?}", msg);
|
||||
break;
|
||||
},
|
||||
}
|
||||
let msg = msg.unwrap();
|
||||
|
||||
self.recv_msg(msg).await.unwrap();
|
||||
}
|
||||
_msg = Self::err(&mut self.stderr) => {}
|
||||
_msg = Self::recv_server_error(&mut self.server_stderr, &mut err_buffer) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-syntax"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
@@ -1,16 +1,8 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
fn get_opt_level() -> u32 {
|
||||
env::var("OPT_LEVEL").unwrap().parse::<u32>().unwrap()
|
||||
}
|
||||
|
||||
fn get_debug() -> bool {
|
||||
env::var("DEBUG").unwrap() == "true"
|
||||
}
|
||||
|
||||
fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> {
|
||||
let mut dirs = Vec::new();
|
||||
for entry in fs::read_dir("languages").unwrap().flatten() {
|
||||
@@ -58,25 +50,28 @@ fn build_c(files: Vec<String>, language: &str) {
|
||||
.file(&file)
|
||||
.include(PathBuf::from(file).parent().unwrap())
|
||||
.pic(true)
|
||||
.opt_level(get_opt_level())
|
||||
.debug(get_debug())
|
||||
.warnings(false)
|
||||
.flag_if_supported("-std=c99");
|
||||
.warnings(false);
|
||||
}
|
||||
build.compile(&format!("tree-sitter-{}-c", language));
|
||||
}
|
||||
|
||||
fn build_cpp(files: Vec<String>, language: &str) {
|
||||
let mut build = cc::Build::new();
|
||||
|
||||
let flag = if build.get_compiler().is_like_msvc() {
|
||||
"/std:c++17"
|
||||
} else {
|
||||
"-std=c++14"
|
||||
};
|
||||
|
||||
for file in files {
|
||||
build
|
||||
.file(&file)
|
||||
.include(PathBuf::from(file).parent().unwrap())
|
||||
.pic(true)
|
||||
.opt_level(get_opt_level())
|
||||
.debug(get_debug())
|
||||
.warnings(false)
|
||||
.cpp(true);
|
||||
.cpp(true)
|
||||
.flag_if_supported(flag);
|
||||
}
|
||||
build.compile(&format!("tree-sitter-{}-cpp", language));
|
||||
}
|
||||
@@ -107,7 +102,11 @@ fn build_dir(dir: &str, language: &str) {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let ignore = vec!["tree-sitter-typescript".to_string()];
|
||||
let ignore = vec![
|
||||
"tree-sitter-typescript".to_string(),
|
||||
"tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34
|
||||
".DS_Store".to_string(),
|
||||
];
|
||||
let dirs = collect_tree_sitter_dirs(&ignore);
|
||||
|
||||
let mut n_jobs = 0;
|
||||
|
1
helix-syntax/languages/tree-sitter-elixir
Submodule
1
helix-syntax/languages/tree-sitter-elixir
Submodule
Submodule helix-syntax/languages/tree-sitter-elixir added at 295e62a43b
1
helix-syntax/languages/tree-sitter-haskell
Submodule
1
helix-syntax/languages/tree-sitter-haskell
Submodule
Submodule helix-syntax/languages/tree-sitter-haskell added at 237f4eb441
1
helix-syntax/languages/tree-sitter-nix
Submodule
1
helix-syntax/languages/tree-sitter-nix
Submodule
Submodule helix-syntax/languages/tree-sitter-nix added at 50f38ceab6
@@ -68,17 +68,19 @@ mk_langs!(
|
||||
// 2) tree-sitter function to call to get a Language
|
||||
(Agda, tree_sitter_agda),
|
||||
(Bash, tree_sitter_bash),
|
||||
(C, tree_sitter_c),
|
||||
(CSharp, tree_sitter_c_sharp),
|
||||
(Cpp, tree_sitter_cpp),
|
||||
(CSharp, tree_sitter_c_sharp),
|
||||
(Css, tree_sitter_css),
|
||||
(C, tree_sitter_c),
|
||||
(Elixir, tree_sitter_elixir),
|
||||
(Go, tree_sitter_go),
|
||||
// (Haskell, tree_sitter_haskell),
|
||||
(Html, tree_sitter_html),
|
||||
(Java, tree_sitter_java),
|
||||
(Javascript, tree_sitter_javascript),
|
||||
(Java, tree_sitter_java),
|
||||
(Json, tree_sitter_json),
|
||||
(Julia, tree_sitter_julia),
|
||||
(Nix, tree_sitter_nix),
|
||||
(Php, tree_sitter_php),
|
||||
(Python, tree_sitter_python),
|
||||
(Ruby, tree_sitter_ruby),
|
||||
|
@@ -1,12 +1,17 @@
|
||||
[package]
|
||||
name = "helix-term"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "A post-modern text editor."
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[package.metadata.nix]
|
||||
build = true
|
||||
app = true
|
||||
|
||||
[features]
|
||||
embed_runtime = ["helix-core/embed_runtime"]
|
||||
|
||||
[[bin]]
|
||||
name = "hx"
|
||||
@@ -18,13 +23,12 @@ helix-view = { path = "../helix-view", features = ["term"]}
|
||||
helix-lsp = { path = "../helix-lsp"}
|
||||
|
||||
anyhow = "1"
|
||||
once_cell = "1.4"
|
||||
once_cell = "1.8"
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
num_cpus = "1"
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
|
||||
crossterm = { version = "0.19", features = ["event-stream"] }
|
||||
clap = { version = "3.0.0-beta.2 ", default-features = false, features = ["std", "cargo"] }
|
||||
crossterm = { version = "0.20", features = ["event-stream"] }
|
||||
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
|
||||
|
@@ -1,8 +1,7 @@
|
||||
use clap::ArgMatches as Args;
|
||||
|
||||
use helix_lsp::lsp;
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
|
||||
use crate::{compositor::Compositor, ui};
|
||||
use crate::{args::Args, compositor::Compositor, ui};
|
||||
|
||||
use log::{error, info};
|
||||
|
||||
@@ -47,16 +46,28 @@ impl Application {
|
||||
let size = compositor.size();
|
||||
let mut editor = Editor::new(size);
|
||||
|
||||
if let Ok(files) = args.values_of_t::<PathBuf>("files") {
|
||||
for file in files {
|
||||
editor.open(file, Action::VerticalSplit)?;
|
||||
compositor.push(Box::new(ui::EditorView::new()));
|
||||
|
||||
if !args.files.is_empty() {
|
||||
let first = &args.files[0]; // we know it's not empty
|
||||
if first.is_dir() {
|
||||
editor.new_file(Action::VerticalSplit);
|
||||
compositor.push(Box::new(ui::file_picker(first.clone())));
|
||||
} else {
|
||||
for file in args.files {
|
||||
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 {
|
||||
editor.open(file, Action::VerticalSplit)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
editor.new_file(Action::VerticalSplit);
|
||||
}
|
||||
|
||||
compositor.push(Box::new(ui::EditorView::new()));
|
||||
|
||||
let mut app = Self {
|
||||
compositor,
|
||||
editor,
|
||||
@@ -167,7 +178,7 @@ impl Application {
|
||||
let diagnostics = params
|
||||
.diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| {
|
||||
.filter_map(|diagnostic| {
|
||||
use helix_core::{
|
||||
diagnostic::{Range, Severity, Severity::*},
|
||||
Diagnostic,
|
||||
@@ -178,18 +189,29 @@ impl Application {
|
||||
let language_server = doc.language_server().unwrap();
|
||||
|
||||
// TODO: convert inside server
|
||||
let start = lsp_pos_to_pos(
|
||||
let start = if let Some(start) = lsp_pos_to_pos(
|
||||
text,
|
||||
diagnostic.range.start,
|
||||
language_server.offset_encoding(),
|
||||
);
|
||||
let end = lsp_pos_to_pos(
|
||||
) {
|
||||
start
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
|
||||
let end = if let Some(end) = lsp_pos_to_pos(
|
||||
text,
|
||||
diagnostic.range.end,
|
||||
language_server.offset_encoding(),
|
||||
);
|
||||
) {
|
||||
end
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
|
||||
Diagnostic {
|
||||
Some(Diagnostic {
|
||||
range: Range { start, end },
|
||||
line: diagnostic.range.start.line as usize,
|
||||
message: diagnostic.message,
|
||||
@@ -203,11 +225,11 @@ impl Application {
|
||||
),
|
||||
// code
|
||||
// source
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
doc.diagnostics = diagnostics;
|
||||
doc.set_diagnostics(diagnostics);
|
||||
// TODO: we want to process all the events in queue, then render. publishDiagnostic tends to send a whole bunch of events
|
||||
self.render();
|
||||
}
|
||||
@@ -218,6 +240,59 @@ impl Application {
|
||||
Notification::LogMessage(params) => {
|
||||
log::warn!("unhandled window/logMessage: {:?}", params);
|
||||
}
|
||||
Notification::ProgressMessage(params) => {
|
||||
let token = match params.token {
|
||||
lsp::NumberOrString::Number(n) => n.to_string(),
|
||||
lsp::NumberOrString::String(s) => s,
|
||||
};
|
||||
let msg = {
|
||||
let lsp::ProgressParamsValue::WorkDone(work) = params.value;
|
||||
let parts = match work {
|
||||
lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
|
||||
title,
|
||||
message,
|
||||
percentage,
|
||||
..
|
||||
}) => (Some(title), message, percentage.map(|n| n.to_string())),
|
||||
lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
|
||||
message,
|
||||
percentage,
|
||||
..
|
||||
}) => (None, message, percentage.map(|n| n.to_string())),
|
||||
lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd {
|
||||
message,
|
||||
}) => {
|
||||
if let Some(message) = message {
|
||||
(None, Some(message), None)
|
||||
} else {
|
||||
self.editor.clear_status();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
match parts {
|
||||
(Some(title), Some(message), Some(percentage)) => {
|
||||
format!("{}% {} - {}", percentage, title, message)
|
||||
}
|
||||
(Some(title), None, Some(percentage)) => {
|
||||
format!("{}% {}", percentage, title)
|
||||
}
|
||||
(Some(title), Some(message), None) => {
|
||||
format!("{} - {}", title, message)
|
||||
}
|
||||
(None, Some(message), Some(percentage)) => {
|
||||
format!("{}% {}", percentage, message)
|
||||
}
|
||||
(Some(title), None, None) => title,
|
||||
(None, Some(message), None) => message,
|
||||
(None, None, Some(percentage)) => format!("{}%", percentage),
|
||||
(None, None, None) => "".into(),
|
||||
}
|
||||
};
|
||||
let status = format!("[{}] {}", token, msg);
|
||||
self.editor.set_status(status);
|
||||
self.render();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
53
helix-term/src/args.rs
Normal file
53
helix-term/src/args.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use anyhow::{Error, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Args {
|
||||
pub display_help: bool,
|
||||
pub display_version: bool,
|
||||
pub verbosity: u64,
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn parse_args() -> Result<Args> {
|
||||
let mut args = Args::default();
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let mut iter = argv.iter();
|
||||
|
||||
iter.next(); // skip the program, we don't care about that
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
"--" => break, // stop parsing at this point treat the remaining as files
|
||||
"--version" => args.display_version = true,
|
||||
"--help" => args.display_help = true,
|
||||
arg if arg.starts_with("--") => {
|
||||
return Err(Error::msg(format!(
|
||||
"unexpected double dash argument: {}",
|
||||
arg
|
||||
)))
|
||||
}
|
||||
arg if arg.starts_with('-') => {
|
||||
let arg = arg.get(1..).unwrap().chars();
|
||||
for chr in arg {
|
||||
match chr {
|
||||
'v' => args.verbosity += 1,
|
||||
'V' => args.display_version = true,
|
||||
'h' => args.display_help = true,
|
||||
_ => return Err(Error::msg(format!("unexpected short arg {}", chr))),
|
||||
}
|
||||
}
|
||||
}
|
||||
arg => args.files.push(PathBuf::from(arg)),
|
||||
}
|
||||
}
|
||||
|
||||
// push the remaining args, if any to the files
|
||||
for filename in iter {
|
||||
args.files.push(PathBuf::from(filename));
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -122,9 +122,17 @@ impl Compositor {
|
||||
}
|
||||
|
||||
pub fn render(&mut self, cx: &mut Context) {
|
||||
let area = self.size();
|
||||
let area = self
|
||||
.terminal
|
||||
.autoresize()
|
||||
.expect("Unable to determine terminal size");
|
||||
|
||||
// TODO: need to recalculate view tree if necessary
|
||||
|
||||
let surface = self.terminal.current_buffer_mut();
|
||||
|
||||
let area = *surface.area();
|
||||
|
||||
for layer in &self.layers {
|
||||
layer.render(area, surface, cx)
|
||||
}
|
||||
@@ -174,10 +182,8 @@ pub trait AnyComponent {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use cursive_core::views::TextComponent;
|
||||
/// # use cursive_core::view::Component;
|
||||
/// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||
/// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||
/// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||
/// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||
/// ```
|
||||
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
|
||||
}
|
||||
|
@@ -11,7 +11,8 @@ use std::collections::HashMap;
|
||||
// W = next WORD
|
||||
// e = end of word
|
||||
// E = end of WORD
|
||||
// r =
|
||||
// r = replace
|
||||
// R = replace with yanked
|
||||
// t = 'till char
|
||||
// y = yank
|
||||
// u = undo
|
||||
@@ -35,10 +36,10 @@ use std::collections::HashMap;
|
||||
// f = find_char()
|
||||
// g = goto (gg, G, gc, gd, etc)
|
||||
//
|
||||
// h = move_char_left(n)
|
||||
// j = move_line_down(n)
|
||||
// k = move_line_up(n)
|
||||
// l = move_char_right(n)
|
||||
// h = move_char_left(n) || arrow-left = move_char_left(n)
|
||||
// j = move_line_down(n) || arrow-down = move_line_down(n)
|
||||
// k = move_line_up(n) || arrow_up = move_line_up(n)
|
||||
// l = move_char_right(n) || arrow-right = move_char_right(n)
|
||||
// : = command line
|
||||
// ; = collapse selection to cursor
|
||||
// " = use register
|
||||
@@ -61,8 +62,8 @@ use std::collections::HashMap;
|
||||
// in kakoune these are alt-h alt-l / gh gl
|
||||
// select from curs to begin end / move curs to begin end
|
||||
// 0 = start of line
|
||||
// ^ = start of line (first non blank char)
|
||||
// $ = end of line
|
||||
// ^ = start of line(first non blank char) || Home = start of line(first non blank char)
|
||||
// $ = end of line || End = end of line
|
||||
//
|
||||
// z = save selections
|
||||
// Z = restore selections
|
||||
@@ -85,6 +86,10 @@ use std::collections::HashMap;
|
||||
//
|
||||
// gd = goto definition
|
||||
// gr = goto reference
|
||||
// [d = previous diagnostic
|
||||
// d] = next diagnostic
|
||||
// [D = first diagnostic
|
||||
// D] = last diagnostic
|
||||
// }
|
||||
|
||||
// #[cfg(feature = "term")]
|
||||
@@ -95,6 +100,12 @@ pub type Keymaps = HashMap<Mode, Keymap>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! key {
|
||||
($key:ident) => {
|
||||
KeyEvent {
|
||||
code: KeyCode::$key,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}
|
||||
};
|
||||
($($ch:tt)*) => {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char($($ch)*),
|
||||
@@ -103,15 +114,6 @@ macro_rules! key {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! shift {
|
||||
($($ch:tt)*) => {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char($($ch)*),
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ctrl {
|
||||
($($ch:tt)*) => {
|
||||
KeyEvent {
|
||||
@@ -137,16 +139,22 @@ pub fn default() -> Keymaps {
|
||||
key!('k') => commands::move_line_up,
|
||||
key!('l') => commands::move_char_right,
|
||||
|
||||
key!(Left) => commands::move_char_left,
|
||||
key!(Down) => commands::move_line_down,
|
||||
key!(Up) => commands::move_line_up,
|
||||
key!(Right) => commands::move_char_right,
|
||||
|
||||
key!('t') => commands::find_till_char,
|
||||
key!('f') => commands::find_next_char,
|
||||
shift!('T') => commands::till_prev_char,
|
||||
shift!('F') => commands::find_prev_char,
|
||||
key!('T') => commands::till_prev_char,
|
||||
key!('F') => commands::find_prev_char,
|
||||
// and matching set for select mode (extend)
|
||||
//
|
||||
key!('r') => commands::replace,
|
||||
key!('R') => commands::replace_with_yanked,
|
||||
|
||||
key!('^') => commands::move_line_start,
|
||||
key!('$') => commands::move_line_end,
|
||||
key!(Home) => commands::move_line_start,
|
||||
key!(End) => commands::move_line_end,
|
||||
|
||||
key!('w') => commands::move_next_word_start,
|
||||
key!('b') => commands::move_prev_word_start,
|
||||
@@ -157,11 +165,11 @@ pub fn default() -> Keymaps {
|
||||
key!(':') => commands::command_mode,
|
||||
|
||||
key!('i') => commands::insert_mode,
|
||||
shift!('I') => commands::prepend_to_line,
|
||||
key!('I') => commands::prepend_to_line,
|
||||
key!('a') => commands::append_mode,
|
||||
shift!('A') => commands::append_to_line,
|
||||
key!('A') => commands::append_to_line,
|
||||
key!('o') => commands::open_below,
|
||||
shift!('O') => commands::open_above,
|
||||
key!('O') => commands::open_above,
|
||||
// [<space> ]<space> equivalents too (add blank new line, no edit)
|
||||
|
||||
|
||||
@@ -174,12 +182,12 @@ pub fn default() -> Keymaps {
|
||||
|
||||
key!('s') => commands::select_regex,
|
||||
alt!('s') => commands::split_selection_on_newline,
|
||||
shift!('S') => commands::split_selection,
|
||||
key!('S') => commands::split_selection,
|
||||
key!(';') => commands::collapse_selection,
|
||||
alt!(';') => commands::flip_selections,
|
||||
key!('%') => commands::select_all,
|
||||
key!('x') => commands::select_line,
|
||||
shift!('X') => commands::extend_line,
|
||||
key!('X') => commands::extend_line,
|
||||
// or select mode X?
|
||||
// extend_to_whole_line, crop_to_whole_line
|
||||
|
||||
@@ -194,30 +202,32 @@ pub fn default() -> Keymaps {
|
||||
// repeat_select
|
||||
|
||||
// TODO: figure out what key to use
|
||||
key!('[') => commands::expand_selection,
|
||||
// key!('[') => commands::expand_selection, ??
|
||||
key!('[') => commands::left_bracket_mode,
|
||||
key!(']') => commands::right_bracket_mode,
|
||||
|
||||
key!('/') => commands::search,
|
||||
// ? for search_reverse
|
||||
key!('n') => commands::search_next,
|
||||
shift!('N') => commands::extend_search_next,
|
||||
key!('N') => commands::extend_search_next,
|
||||
// N for search_prev
|
||||
key!('*') => commands::search_selection,
|
||||
|
||||
key!('u') => commands::undo,
|
||||
shift!('U') => commands::redo,
|
||||
key!('U') => commands::redo,
|
||||
|
||||
key!('y') => commands::yank,
|
||||
// yank_all
|
||||
key!('p') => commands::paste_after,
|
||||
// paste_all
|
||||
shift!('P') => commands::paste_before,
|
||||
key!('P') => commands::paste_before,
|
||||
|
||||
key!('>') => commands::indent,
|
||||
key!('<') => commands::unindent,
|
||||
key!('=') => commands::format_selections,
|
||||
shift!('J') => commands::join_selections,
|
||||
key!('J') => commands::join_selections,
|
||||
// TODO: conflicts hover/doc
|
||||
shift!('K') => commands::keep_selections,
|
||||
key!('K') => commands::keep_selections,
|
||||
// TODO: and another method for inverse
|
||||
|
||||
// TODO: clashes with space mode
|
||||
@@ -232,38 +242,31 @@ pub fn default() -> Keymaps {
|
||||
|
||||
// C / altC = copy (repeat) selections on prev/next lines
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::normal_mode,
|
||||
KeyEvent {
|
||||
code: KeyCode::PageUp,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::page_up,
|
||||
KeyEvent {
|
||||
code: KeyCode::PageDown,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::page_down,
|
||||
key!(Esc) => commands::normal_mode,
|
||||
key!(PageUp) => commands::page_up,
|
||||
key!(PageDown) => commands::page_down,
|
||||
ctrl!('b') => commands::page_up,
|
||||
ctrl!('f') => commands::page_down,
|
||||
ctrl!('u') => commands::half_page_up,
|
||||
ctrl!('d') => commands::half_page_down,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::next_view,
|
||||
ctrl!('w') => commands::window_mode,
|
||||
|
||||
// move under <space>c
|
||||
ctrl!('c') => commands::toggle_comments,
|
||||
shift!('K') => commands::hover,
|
||||
key!('K') => commands::hover,
|
||||
|
||||
// z family for save/restore/combine from/to sels from register
|
||||
|
||||
ctrl!('i') => commands::jump_forward, // TODO: ctrl-i conflicts tab
|
||||
// supposedly ctrl!('i') but did not work
|
||||
key!(Tab) => commands::jump_forward,
|
||||
ctrl!('o') => commands::jump_backward,
|
||||
// ctrl!('s') => commands::save_selection,
|
||||
|
||||
key!(' ') => commands::space_mode,
|
||||
key!('z') => commands::view_mode,
|
||||
|
||||
key!('"') => commands::select_register,
|
||||
);
|
||||
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
|
||||
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
|
||||
@@ -276,19 +279,23 @@ pub fn default() -> Keymaps {
|
||||
key!('k') => commands::extend_line_up,
|
||||
key!('l') => commands::extend_char_right,
|
||||
|
||||
key!(Left) => commands::extend_char_left,
|
||||
key!(Down) => commands::extend_line_down,
|
||||
key!(Up) => commands::extend_line_up,
|
||||
key!(Right) => commands::extend_char_right,
|
||||
|
||||
key!('w') => commands::extend_next_word_start,
|
||||
key!('b') => commands::extend_prev_word_start,
|
||||
key!('e') => commands::extend_next_word_end,
|
||||
|
||||
key!('t') => commands::extend_till_char,
|
||||
key!('f') => commands::extend_next_char,
|
||||
shift!('T') => commands::extend_till_prev_char,
|
||||
shift!('F') => commands::extend_prev_char,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::exit_select_mode as Command,
|
||||
key!('T') => commands::extend_till_prev_char,
|
||||
key!('F') => commands::extend_prev_char,
|
||||
key!(Home) => commands::extend_line_start,
|
||||
key!(End) => commands::extend_line_end,
|
||||
key!(Esc) => commands::exit_select_mode,
|
||||
)
|
||||
.into_iter(),
|
||||
);
|
||||
@@ -299,28 +306,14 @@ pub fn default() -> Keymaps {
|
||||
Mode::Normal => normal,
|
||||
Mode::Select => select,
|
||||
Mode::Insert => hashmap!(
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::normal_mode as Command,
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::delete_char_backward,
|
||||
KeyEvent {
|
||||
code: KeyCode::Delete,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::delete_char_forward,
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::insert_newline,
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::insert_tab,
|
||||
key!(Esc) => commands::normal_mode as Command,
|
||||
key!(Backspace) => commands::insert::delete_char_backward,
|
||||
key!(Delete) => commands::insert::delete_char_forward,
|
||||
key!(Enter) => commands::insert::insert_newline,
|
||||
key!(Tab) => commands::insert::insert_tab,
|
||||
|
||||
ctrl!('x') => commands::completion,
|
||||
ctrl!('w') => commands::insert::delete_word_backward,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
8
helix-term/src/lib.rs
Normal file
8
helix-term/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#![allow(unused)]
|
||||
|
||||
pub mod application;
|
||||
pub mod args;
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod keymap;
|
||||
pub mod ui;
|
@@ -1,19 +1,11 @@
|
||||
#![allow(unused)]
|
||||
use helix_term::application::Application;
|
||||
use helix_term::args::Args;
|
||||
|
||||
mod application;
|
||||
mod commands;
|
||||
mod compositor;
|
||||
mod keymap;
|
||||
mod ui;
|
||||
|
||||
use application::Application;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Error;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
|
||||
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
let mut base_config = fern::Dispatch::new();
|
||||
|
||||
// Let's say we depend on something which whose "info" level messages are too
|
||||
@@ -28,8 +20,6 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
|
||||
_3_or_more => base_config.level(log::LevelFilter::Trace),
|
||||
};
|
||||
|
||||
let home = dirs_next::home_dir().expect("can't find the home directory");
|
||||
|
||||
// Separate file config so we can include year, month and day in file logs
|
||||
let file_config = fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
@@ -41,56 +31,69 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
|
||||
message
|
||||
))
|
||||
})
|
||||
.chain(fern::log_file(home.join("helix.log"))?);
|
||||
.chain(fern::log_file(logpath)?);
|
||||
|
||||
base_config.chain(file_config).apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = clap::app_from_crate!()
|
||||
.arg(
|
||||
Arg::new("files")
|
||||
.about("Sets the input file to use")
|
||||
.required(false)
|
||||
.multiple(true)
|
||||
.index(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("verbose")
|
||||
.about("Increases logging verbosity each use for up to 3 times")
|
||||
.short('v')
|
||||
.takes_value(false)
|
||||
.multiple_occurrences(true),
|
||||
)
|
||||
.get_matches();
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cache_dir = helix_core::cache_dir();
|
||||
if !cache_dir.exists() {
|
||||
std::fs::create_dir_all(&cache_dir).ok();
|
||||
}
|
||||
|
||||
let verbosity: u64 = args.occurrences_of("verbose");
|
||||
let logpath = cache_dir.join("helix.log");
|
||||
let help = format!(
|
||||
"\
|
||||
{} {}
|
||||
{}
|
||||
{}
|
||||
|
||||
setup_logging(verbosity).expect("failed to initialize logging.");
|
||||
USAGE:
|
||||
hx [FLAGS] [files]...
|
||||
|
||||
// initialize language registry
|
||||
use helix_core::config_dir;
|
||||
use helix_core::syntax::{Loader, LOADER};
|
||||
ARGS:
|
||||
<files>... Sets the input file to use
|
||||
|
||||
// load $HOME/.config/helix/languages.toml, fallback to default config
|
||||
let config = std::fs::read(config_dir().join("languages.toml"));
|
||||
let toml = config
|
||||
.as_deref()
|
||||
.unwrap_or(include_bytes!("../../languages.toml"));
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-v Increases logging verbosity each use for up to 3 times
|
||||
(default file: {})
|
||||
-V, --version Prints version information
|
||||
",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("CARGO_PKG_AUTHORS"),
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
logpath.display(),
|
||||
);
|
||||
|
||||
LOADER.get_or_init(|| {
|
||||
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
|
||||
Loader::new(config)
|
||||
});
|
||||
let args = Args::parse_args().context("could not parse arguments")?;
|
||||
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
// Help has a higher priority and should be handled separately.
|
||||
if args.display_help {
|
||||
print!("{}", help);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if args.display_version {
|
||||
println!("helix {}", env!("CARGO_PKG_VERSION"));
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let conf_dir = helix_core::config_dir();
|
||||
if !conf_dir.exists() {
|
||||
std::fs::create_dir_all(&conf_dir).ok();
|
||||
}
|
||||
|
||||
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
||||
|
||||
// TODO: use the thread local executor to spawn the application task separately from the work pool
|
||||
runtime.block_on(async move {
|
||||
let mut app = Application::new(args).unwrap();
|
||||
let mut app = Application::new(args).context("unable to create new appliction")?;
|
||||
app.run().await.unwrap();
|
||||
|
||||
app.run().await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -12,11 +12,60 @@ use helix_core::{Position, Transaction};
|
||||
use helix_view::Editor;
|
||||
|
||||
use crate::commands;
|
||||
use crate::ui::{Markdown, Menu, Popup, PromptEvent};
|
||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
|
||||
use helix_lsp::lsp;
|
||||
use lsp::CompletionItem;
|
||||
|
||||
impl menu::Item for CompletionItem {
|
||||
fn filter_text(&self) -> &str {
|
||||
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
|
||||
}
|
||||
|
||||
fn label(&self) -> &str {
|
||||
self.label.as_str()
|
||||
}
|
||||
|
||||
fn row(&self) -> menu::Row {
|
||||
menu::Row::new(vec![
|
||||
menu::Cell::from(self.label.as_str()),
|
||||
menu::Cell::from(match self.kind {
|
||||
Some(lsp::CompletionItemKind::Text) => "text",
|
||||
Some(lsp::CompletionItemKind::Method) => "method",
|
||||
Some(lsp::CompletionItemKind::Function) => "function",
|
||||
Some(lsp::CompletionItemKind::Constructor) => "constructor",
|
||||
Some(lsp::CompletionItemKind::Field) => "field",
|
||||
Some(lsp::CompletionItemKind::Variable) => "variable",
|
||||
Some(lsp::CompletionItemKind::Class) => "class",
|
||||
Some(lsp::CompletionItemKind::Interface) => "interface",
|
||||
Some(lsp::CompletionItemKind::Module) => "module",
|
||||
Some(lsp::CompletionItemKind::Property) => "property",
|
||||
Some(lsp::CompletionItemKind::Unit) => "unit",
|
||||
Some(lsp::CompletionItemKind::Value) => "value",
|
||||
Some(lsp::CompletionItemKind::Enum) => "enum",
|
||||
Some(lsp::CompletionItemKind::Keyword) => "keyword",
|
||||
Some(lsp::CompletionItemKind::Snippet) => "snippet",
|
||||
Some(lsp::CompletionItemKind::Color) => "color",
|
||||
Some(lsp::CompletionItemKind::File) => "file",
|
||||
Some(lsp::CompletionItemKind::Reference) => "reference",
|
||||
Some(lsp::CompletionItemKind::Folder) => "folder",
|
||||
Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
|
||||
Some(lsp::CompletionItemKind::Constant) => "constant",
|
||||
Some(lsp::CompletionItemKind::Struct) => "struct",
|
||||
Some(lsp::CompletionItemKind::Event) => "event",
|
||||
Some(lsp::CompletionItemKind::Operator) => "operator",
|
||||
Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
|
||||
None => "",
|
||||
}),
|
||||
// self.detail.as_deref().unwrap_or("")
|
||||
// self.label_details
|
||||
// .as_ref()
|
||||
// .or(self.detail())
|
||||
// .as_str(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a Menu.
|
||||
pub struct Completion {
|
||||
popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
|
||||
@@ -31,92 +80,85 @@ impl Completion {
|
||||
trigger_offset: usize,
|
||||
) -> Self {
|
||||
// let items: Vec<CompletionItem> = Vec::new();
|
||||
let mut menu = Menu::new(
|
||||
items,
|
||||
|item| {
|
||||
// format_fn
|
||||
item.label.as_str().into()
|
||||
let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| {
|
||||
match event {
|
||||
PromptEvent::Abort => {
|
||||
// revert state
|
||||
// let id = editor.view().doc;
|
||||
// let doc = &mut editor.documents[id];
|
||||
// doc.state = snapshot.clone();
|
||||
}
|
||||
PromptEvent::Validate => {
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
// TODO: use item.filter_text for filtering
|
||||
},
|
||||
move |editor: &mut Editor, item, event| {
|
||||
match event {
|
||||
PromptEvent::Abort => {
|
||||
// revert state
|
||||
// let id = editor.view().doc;
|
||||
// let doc = &mut editor.documents[id];
|
||||
// doc.state = snapshot.clone();
|
||||
// revert state to what it was before the last update
|
||||
// doc.state = snapshot.clone();
|
||||
|
||||
// extract as fn(doc, item):
|
||||
|
||||
// TODO: need to apply without composing state...
|
||||
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
|
||||
// the final state?
|
||||
// -> on update simply update the snapshot, then on accept redo the call,
|
||||
// finally updating doc.changes + notifying lsp.
|
||||
//
|
||||
// or we could simply use doc.undo + apply when changing between options
|
||||
|
||||
// always present here
|
||||
let item = item.unwrap();
|
||||
|
||||
use helix_lsp::{lsp, util};
|
||||
|
||||
// if more text was entered, remove it
|
||||
let cursor = doc.selection(view.id).cursor();
|
||||
if trigger_offset < cursor {
|
||||
let remove = Transaction::change(
|
||||
doc.text(),
|
||||
vec![(trigger_offset, cursor, None)].into_iter(),
|
||||
);
|
||||
doc.apply(&remove, view.id);
|
||||
}
|
||||
PromptEvent::Validate => {
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
// revert state to what it was before the last update
|
||||
// doc.state = snapshot.clone();
|
||||
|
||||
// extract as fn(doc, item):
|
||||
|
||||
// TODO: need to apply without composing state...
|
||||
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
|
||||
// the final state?
|
||||
// -> on update simply update the snapshot, then on accept redo the call,
|
||||
// finally updating doc.changes + notifying lsp.
|
||||
//
|
||||
// or we could simply use doc.undo + apply when changing between options
|
||||
|
||||
// always present here
|
||||
let item = item.unwrap();
|
||||
|
||||
use helix_lsp::{lsp, util};
|
||||
// determine what to insert: text_edit | insert_text | label
|
||||
let edit = if let Some(edit) = &item.text_edit {
|
||||
match edit {
|
||||
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
||||
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
||||
unimplemented!("completion: insert_and_replace {:?}", item)
|
||||
}
|
||||
use helix_lsp::OffsetEncoding;
|
||||
let transaction = if let Some(edit) = &item.text_edit {
|
||||
let edit = match edit {
|
||||
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
||||
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
||||
unimplemented!("completion: insert_and_replace {:?}", item)
|
||||
}
|
||||
} else {
|
||||
item.insert_text.as_ref().unwrap_or(&item.label);
|
||||
unimplemented!();
|
||||
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
|
||||
// and we insert at position.
|
||||
};
|
||||
|
||||
// if more text was entered, remove it
|
||||
let cursor = doc.selection(view.id).cursor();
|
||||
if trigger_offset < cursor {
|
||||
let remove = Transaction::change(
|
||||
doc.text(),
|
||||
vec![(trigger_offset, cursor, None)].into_iter(),
|
||||
);
|
||||
doc.apply(&remove, view.id);
|
||||
}
|
||||
|
||||
use helix_lsp::OffsetEncoding;
|
||||
let transaction = util::generate_transaction_from_edits(
|
||||
util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
vec![edit],
|
||||
offset_encoding, // TODO: should probably transcode in Client
|
||||
);
|
||||
doc.apply(&transaction, view.id);
|
||||
)
|
||||
} else {
|
||||
let text = item.insert_text.as_ref().unwrap_or(&item.label);
|
||||
let cursor = doc.selection(view.id).cursor();
|
||||
Transaction::change(
|
||||
doc.text(),
|
||||
vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
|
||||
)
|
||||
};
|
||||
|
||||
// TODO: merge edit with additional_text_edits
|
||||
if let Some(additional_edits) = &item.additional_text_edits {
|
||||
// gopls uses this to add extra imports
|
||||
if !additional_edits.is_empty() {
|
||||
let transaction = util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
additional_edits.clone(),
|
||||
offset_encoding, // TODO: should probably transcode in Client
|
||||
);
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
doc.apply(&transaction, view.id);
|
||||
|
||||
// TODO: merge edit with additional_text_edits
|
||||
if let Some(additional_edits) = &item.additional_text_edits {
|
||||
// gopls uses this to add extra imports
|
||||
if !additional_edits.is_empty() {
|
||||
let transaction = util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
additional_edits.clone(),
|
||||
offset_encoding, // TODO: should probably transcode in Client
|
||||
);
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
});
|
||||
let popup = Popup::new(menu);
|
||||
Self {
|
||||
popup,
|
||||
@@ -167,6 +209,13 @@ impl Completion {
|
||||
|
||||
impl Component for Completion {
|
||||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
// let the Editor handle Esc instead
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
}) = event
|
||||
{
|
||||
return EventResult::Ignored;
|
||||
}
|
||||
self.popup.handle_event(event, cx)
|
||||
}
|
||||
|
||||
@@ -183,32 +232,61 @@ impl Component for Completion {
|
||||
// option.detail
|
||||
// ---
|
||||
// option.documentation
|
||||
match &option.documentation {
|
||||
Some(lsp::Documentation::String(s))
|
||||
|
||||
let (view, doc) = cx.editor.current();
|
||||
let language = doc
|
||||
.language()
|
||||
.and_then(|scope| scope.strip_prefix("source."))
|
||||
.unwrap_or("");
|
||||
|
||||
let doc = match &option.documentation {
|
||||
Some(lsp::Documentation::String(contents))
|
||||
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::PlainText,
|
||||
value: s,
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: convert to wrapped text
|
||||
let doc = s;
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
))
|
||||
}
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: contents,
|
||||
})) => {
|
||||
let doc = Markdown::new(contents.clone());
|
||||
let half = area.height / 2;
|
||||
let height = 15.min(half);
|
||||
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
|
||||
let area = Rect::new(0, area.height - height - 2, area.width, height);
|
||||
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.popup");
|
||||
surface.clear_with(area, background);
|
||||
doc.render(area, surface, cx);
|
||||
// TODO: set language based on doc scope
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
))
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
None if option.detail.is_some() => {
|
||||
// TODO: copied from above
|
||||
|
||||
// TODO: set language based on doc scope
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
let half = area.height / 2;
|
||||
let height = 15.min(half);
|
||||
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
|
||||
let area = Rect::new(0, area.height - height - 2, area.width, height);
|
||||
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.popup");
|
||||
surface.clear_with(area, background);
|
||||
doc.render(area, surface, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -34,6 +34,12 @@ pub struct EditorView {
|
||||
|
||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
|
||||
impl Default for EditorView {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -148,6 +154,13 @@ impl EditorView {
|
||||
// TODO: scope matching: biggest union match? [string] & [html, string], [string, html] & [ string, html]
|
||||
// can do this by sorting our theme matches based on array len (longest first) then stopping at the
|
||||
// first rule that matches (rule.all(|scope| scopes.contains(scope)))
|
||||
// log::info!(
|
||||
// "scopes: {:?}",
|
||||
// spans
|
||||
// .iter()
|
||||
// .map(|span| theme.scopes()[span.0].as_str())
|
||||
// .collect::<Vec<_>>()
|
||||
// );
|
||||
let style = match spans.first() {
|
||||
Some(span) => theme.get(theme.scopes()[span.0].as_str()),
|
||||
None => theme.get("ui.text"),
|
||||
@@ -188,7 +201,7 @@ impl EditorView {
|
||||
}
|
||||
|
||||
// ugh,interleave highlight spans with diagnostic spans
|
||||
let is_diagnostic = doc.diagnostics.iter().any(|diagnostic| {
|
||||
let is_diagnostic = doc.diagnostics().iter().any(|diagnostic| {
|
||||
diagnostic.range.start <= char_index
|
||||
&& diagnostic.range.end > char_index
|
||||
});
|
||||
@@ -227,8 +240,7 @@ impl EditorView {
|
||||
// .bg(Color::Rgb(255, 255, 255))
|
||||
.add_modifier(Modifier::REVERSED);
|
||||
|
||||
// let selection_style = Style::default().bg(Color::Rgb(94, 0, 128));
|
||||
let selection_style = Style::default().bg(Color::Rgb(84, 0, 153));
|
||||
let selection_style = theme.get("ui.selection");
|
||||
|
||||
for selection in doc
|
||||
.selection(view.id)
|
||||
@@ -254,7 +266,16 @@ impl EditorView {
|
||||
Rect::new(
|
||||
viewport.x + start.col as u16,
|
||||
viewport.y + start.row as u16,
|
||||
(end.col - start.col) as u16 + 1,
|
||||
// .min is important, because set_style does a
|
||||
// for i in area.left()..area.right() and
|
||||
// area.right = x + width !!! which shouldn't be > then surface.area.right()
|
||||
// This is checked by a debug_assert! in Buffer::index_of
|
||||
((end.col - start.col) as u16 + 1).min(
|
||||
surface
|
||||
.area
|
||||
.width
|
||||
.saturating_sub(viewport.x + start.col as u16),
|
||||
),
|
||||
1,
|
||||
),
|
||||
selection_style,
|
||||
@@ -265,7 +286,7 @@ impl EditorView {
|
||||
viewport.x + start.col as u16,
|
||||
viewport.y + start.row as u16,
|
||||
// text.line(view.first_line).len_chars() as u16 - start.col as u16,
|
||||
viewport.width - start.col as u16,
|
||||
viewport.width.saturating_sub(start.col as u16),
|
||||
1,
|
||||
),
|
||||
selection_style,
|
||||
@@ -283,7 +304,12 @@ impl EditorView {
|
||||
);
|
||||
}
|
||||
surface.set_style(
|
||||
Rect::new(viewport.x, viewport.y + end.row as u16, end.col as u16, 1),
|
||||
Rect::new(
|
||||
viewport.x,
|
||||
viewport.y + end.row as u16,
|
||||
(end.col as u16).min(viewport.width),
|
||||
1,
|
||||
),
|
||||
selection_style,
|
||||
);
|
||||
}
|
||||
@@ -299,6 +325,31 @@ impl EditorView {
|
||||
),
|
||||
cursor_style,
|
||||
);
|
||||
// TODO: set cursor position for IME
|
||||
if let Some(syntax) = doc.syntax() {
|
||||
use helix_core::match_brackets;
|
||||
let pos = doc.selection(view.id).cursor();
|
||||
let pos = match_brackets::find(syntax, doc.text(), pos);
|
||||
if let Some(pos) = pos {
|
||||
let pos = view.screen_coords_at_pos(doc, text, pos);
|
||||
if let Some(pos) = pos {
|
||||
if (pos.col as u16) < viewport.width + view.first_col as u16
|
||||
&& pos.col >= view.first_col
|
||||
{
|
||||
let style = Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.add_modifier(Modifier::DIM);
|
||||
|
||||
surface
|
||||
.get_mut(
|
||||
viewport.x + pos.col as u16,
|
||||
viewport.y + pos.row as u16,
|
||||
)
|
||||
.set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,7 +364,7 @@ impl EditorView {
|
||||
|
||||
for (i, line) in (view.first_line..last_line).enumerate() {
|
||||
use helix_core::diagnostic::Severity;
|
||||
if let Some(diagnostic) = doc.diagnostics.iter().find(|d| d.line == line) {
|
||||
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
|
||||
surface.set_stringn(
|
||||
viewport.x - OFFSET,
|
||||
viewport.y + i as u16,
|
||||
@@ -357,7 +408,7 @@ impl EditorView {
|
||||
let cursor = doc.selection(view.id).cursor();
|
||||
let line = doc.text().char_to_line(cursor);
|
||||
|
||||
let diagnostics = doc.diagnostics.iter().filter(|diagnostic| {
|
||||
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
|
||||
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
|
||||
});
|
||||
|
||||
@@ -439,7 +490,7 @@ impl EditorView {
|
||||
surface.set_stringn(
|
||||
viewport.x + viewport.width.saturating_sub(15),
|
||||
viewport.y,
|
||||
format!("{}", doc.diagnostics.len()),
|
||||
format!("{}", doc.diagnostics().len()),
|
||||
4,
|
||||
text_color,
|
||||
);
|
||||
@@ -475,7 +526,8 @@ impl EditorView {
|
||||
// count handling
|
||||
key!(i @ '0'..='9') => {
|
||||
let i = i.to_digit(10).unwrap() as usize;
|
||||
cxt.editor.count = Some(cxt.editor.count.map_or(i, |c| c * 10 + i));
|
||||
cxt.editor.count =
|
||||
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
|
||||
}
|
||||
// special handling for repeat operator
|
||||
key!('.') => {
|
||||
@@ -488,11 +540,14 @@ impl EditorView {
|
||||
}
|
||||
_ => {
|
||||
// set the count
|
||||
cxt.count = cxt.editor.count.take().unwrap_or(1);
|
||||
cxt._count = cxt.editor.count.take();
|
||||
// TODO: edge case: 0j -> reset to 1
|
||||
// if this fails, count was Some(0)
|
||||
// debug_assert!(cxt.count != 0);
|
||||
|
||||
// set the register
|
||||
cxt.register = cxt.editor.register.take();
|
||||
|
||||
if let Some(command) = self.keymap[&mode].get(&event) {
|
||||
command(cxt);
|
||||
}
|
||||
@@ -522,7 +577,8 @@ impl Component for EditorView {
|
||||
cx.editor.resize(Rect::new(0, 0, width, height - 1));
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(key) => {
|
||||
Event::Key(mut key) => {
|
||||
canonicalize_key(&mut key);
|
||||
// clear status
|
||||
cx.editor.status_msg = None;
|
||||
|
||||
@@ -530,11 +586,12 @@ impl Component for EditorView {
|
||||
let mode = doc.mode();
|
||||
|
||||
let mut cxt = commands::Context {
|
||||
register: helix_view::RegisterSelection::default(),
|
||||
editor: &mut cx.editor,
|
||||
count: 1,
|
||||
_count: None,
|
||||
callback: None,
|
||||
callbacks: cx.callbacks,
|
||||
on_next_key_callback: None,
|
||||
callbacks: cx.callbacks,
|
||||
};
|
||||
|
||||
if let Some(on_next_key) = self.on_next_key.take() {
|
||||
@@ -577,7 +634,6 @@ impl Component for EditorView {
|
||||
if completion.is_empty() {
|
||||
self.completion = None;
|
||||
}
|
||||
// TODO: if exiting InsertMode, remove completion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,16 +654,24 @@ impl Component for EditorView {
|
||||
let (view, doc) = cx.editor.current();
|
||||
view.ensure_cursor_in_view(doc);
|
||||
|
||||
if mode == Mode::Normal && doc.mode() == Mode::Insert {
|
||||
// HAXX: if we just entered insert mode from normal, clear key buf
|
||||
// and record the command that got us into this mode.
|
||||
// mode transitions
|
||||
match (mode, doc.mode()) {
|
||||
(Mode::Normal, Mode::Insert) => {
|
||||
// HAXX: if we just entered insert mode from normal, clear key buf
|
||||
// and record the command that got us into this mode.
|
||||
|
||||
// how we entered insert mode is important, and we should track that so
|
||||
// we can repeat the side effect.
|
||||
// how we entered insert mode is important, and we should track that so
|
||||
// we can repeat the side effect.
|
||||
|
||||
self.last_insert.0 = self.keymap[&mode][&key];
|
||||
self.last_insert.1.clear();
|
||||
};
|
||||
self.last_insert.0 = self.keymap[&mode][&key];
|
||||
self.last_insert.1.clear();
|
||||
}
|
||||
(Mode::Insert, Mode::Normal) => {
|
||||
// if exiting insert mode, remove completion
|
||||
self.completion = None;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
EventResult::Consumed(callback)
|
||||
}
|
||||
@@ -619,6 +683,10 @@ impl Component for EditorView {
|
||||
// clear with background color
|
||||
surface.set_style(area, cx.editor.theme.get("ui.background"));
|
||||
|
||||
// if the terminal size suddenly changed, we need to trigger a resize
|
||||
cx.editor
|
||||
.resize(Rect::new(area.x, area.y, area.width, area.height - 1)); // - 1 to account for commandline
|
||||
|
||||
for (view, is_focused) in cx.editor.tree.views() {
|
||||
let doc = cx.editor.document(view.doc).unwrap();
|
||||
self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused);
|
||||
@@ -658,3 +726,13 @@ impl Component for EditorView {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize_key(key: &mut KeyEvent) {
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char(_),
|
||||
modifiers: _,
|
||||
} = key
|
||||
{
|
||||
key.modifiers.remove(KeyModifiers::SHIFT)
|
||||
}
|
||||
}
|
||||
|
@@ -107,11 +107,14 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
None => text_style,
|
||||
};
|
||||
|
||||
// TODO: replace tabs with indentation
|
||||
|
||||
let mut slice = &text[start..end];
|
||||
while let Some(end) = slice.find('\n') {
|
||||
// emit span up to newline
|
||||
let text = &slice[..end];
|
||||
let span = Span::styled(text.to_owned(), style);
|
||||
let text = text.replace('\t', " "); // replace tabs
|
||||
let span = Span::styled(text, style);
|
||||
spans.push(span);
|
||||
|
||||
// truncate slice to after newline
|
||||
@@ -124,7 +127,8 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
|
||||
// if there's anything left, emit it too
|
||||
if !slice.is_empty() {
|
||||
let span = Span::styled(slice.to_owned(), style);
|
||||
let span =
|
||||
Span::styled(slice.replace('\t', " "), style);
|
||||
spans.push(span);
|
||||
}
|
||||
}
|
||||
@@ -153,6 +157,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
}
|
||||
}
|
||||
Event::Code(text) | Event::Html(text) => {
|
||||
log::warn!("code {:?}", text);
|
||||
let mut span = to_span(text);
|
||||
span.style = code_style;
|
||||
spans.push(span);
|
||||
@@ -167,7 +172,9 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
lines.push(Spans::default());
|
||||
}
|
||||
// TaskListMarker(bool) true if checked
|
||||
_ => (),
|
||||
_ => {
|
||||
log::warn!("unhandled markdown event {:?}", event);
|
||||
}
|
||||
}
|
||||
// build up a vec of Paragraph tui widgets
|
||||
}
|
||||
|
@@ -4,8 +4,11 @@ use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::Table,
|
||||
};
|
||||
|
||||
pub use tui::widgets::{Cell, Row};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
@@ -14,7 +17,15 @@ use fuzzy_matcher::FuzzyMatcher;
|
||||
use helix_core::Position;
|
||||
use helix_view::Editor;
|
||||
|
||||
pub struct Menu<T> {
|
||||
pub trait Item {
|
||||
// TODO: sort_text
|
||||
fn filter_text(&self) -> &str;
|
||||
|
||||
fn label(&self) -> &str;
|
||||
fn row(&self) -> Row;
|
||||
}
|
||||
|
||||
pub struct Menu<T: Item> {
|
||||
options: Vec<T>,
|
||||
|
||||
cursor: Option<usize>,
|
||||
@@ -23,19 +34,17 @@ pub struct Menu<T> {
|
||||
/// (index, score)
|
||||
matches: Vec<(usize, i64)>,
|
||||
|
||||
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
|
||||
callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
|
||||
|
||||
scroll: usize,
|
||||
size: (u16, u16),
|
||||
}
|
||||
|
||||
impl<T> Menu<T> {
|
||||
impl<T: Item> Menu<T> {
|
||||
// TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
|
||||
// rendering)
|
||||
pub fn new(
|
||||
options: Vec<T>,
|
||||
format_fn: impl Fn(&T) -> Cow<str> + 'static,
|
||||
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
|
||||
) -> Self {
|
||||
let mut menu = Self {
|
||||
@@ -43,7 +52,6 @@ impl<T> Menu<T> {
|
||||
matcher: Box::new(Matcher::default()),
|
||||
matches: Vec::new(),
|
||||
cursor: None,
|
||||
format_fn: Box::new(format_fn),
|
||||
callback_fn: Box::new(callback_fn),
|
||||
scroll: 0,
|
||||
size: (0, 0),
|
||||
@@ -61,7 +69,6 @@ impl<T> Menu<T> {
|
||||
ref mut options,
|
||||
ref mut matcher,
|
||||
ref mut matches,
|
||||
ref format_fn,
|
||||
..
|
||||
} = *self;
|
||||
|
||||
@@ -72,8 +79,7 @@ impl<T> Menu<T> {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, option)| {
|
||||
// TODO: maybe using format_fn isn't the best idea here
|
||||
let text = (format_fn)(option);
|
||||
let text = option.filter_text();
|
||||
// TODO: using fuzzy_indices could give us the char idx for match highlighting
|
||||
matcher
|
||||
.fuzzy_match(&text, pattern)
|
||||
@@ -134,7 +140,7 @@ impl<T> Menu<T> {
|
||||
|
||||
use super::PromptEvent as MenuEvent;
|
||||
|
||||
impl<T: 'static> Component for Menu<T> {
|
||||
impl<T: Item + 'static> Component for Menu<T> {
|
||||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
let event = match event {
|
||||
Event::Key(event) => event,
|
||||
@@ -160,8 +166,8 @@ impl<T: 'static> Component for Menu<T> {
|
||||
}
|
||||
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Up, ..
|
||||
@@ -224,7 +230,7 @@ impl<T: 'static> Component for Menu<T> {
|
||||
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||
let width = std::cmp::min(30, viewport.0);
|
||||
|
||||
const MAX: usize = 5;
|
||||
const MAX: usize = 10;
|
||||
let height = std::cmp::min(self.options.len(), MAX);
|
||||
let height = std::cmp::min(height, viewport.1 as usize);
|
||||
|
||||
@@ -236,9 +242,11 @@ impl<T: 'static> Component for Menu<T> {
|
||||
Some(self.size)
|
||||
}
|
||||
|
||||
// TODO: required size should re-trigger when we filter items so we can draw a smaller menu
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let style = cx.editor.theme.get("ui.text");
|
||||
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
|
||||
let selected = cx.editor.theme.get("ui.menu.selected");
|
||||
|
||||
let scroll = self.scroll;
|
||||
|
||||
@@ -264,26 +272,40 @@ impl<T: 'static> Component for Menu<T> {
|
||||
let scroll_line = (win_height - scroll_height) * scroll
|
||||
/ std::cmp::max(1, len.saturating_sub(win_height));
|
||||
|
||||
for (i, option) in options[scroll..(scroll + win_height).min(len)]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let line = Some(i + scroll);
|
||||
// TODO: set bg for the whole row if selected
|
||||
surface.set_stringn(
|
||||
area.x,
|
||||
area.y + i as u16,
|
||||
(self.format_fn)(option),
|
||||
area.width as usize - 1,
|
||||
if line == self.cursor { selected } else { style },
|
||||
);
|
||||
use tui::layout::Constraint;
|
||||
let rows = options.iter().map(|option| option.row());
|
||||
let table = Table::new(rows)
|
||||
.style(style)
|
||||
.highlight_style(selected)
|
||||
.column_spacing(1)
|
||||
.widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
|
||||
use tui::widgets::TableState;
|
||||
|
||||
table.render_table(
|
||||
area,
|
||||
surface,
|
||||
&mut TableState {
|
||||
offset: scroll,
|
||||
selected: self.cursor,
|
||||
},
|
||||
);
|
||||
|
||||
// // TODO: set bg for the whole row if selected
|
||||
// if line == self.cursor {
|
||||
// surface.set_style(
|
||||
// Rect::new(area.x, area.y + i as u16, area.width - 1, 1),
|
||||
// selected,
|
||||
// )
|
||||
// }
|
||||
|
||||
for (i, option) in (scroll..(scroll + win_height).min(len)).enumerate() {
|
||||
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
|
||||
|
||||
if is_marked {
|
||||
let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16);
|
||||
cell.set_symbol("▐ ");
|
||||
cell.set_style(selected);
|
||||
// cell.set_style(selected);
|
||||
// cell.set_style(if is_marked { selected } else { style });
|
||||
}
|
||||
}
|
||||
|
@@ -85,10 +85,15 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
||||
Err(_err) => None,
|
||||
});
|
||||
|
||||
const MAX: usize = 2048;
|
||||
let files = if root.join(".git").is_dir() {
|
||||
files.collect()
|
||||
} else {
|
||||
const MAX: usize = 8192;
|
||||
files.take(MAX).collect()
|
||||
};
|
||||
|
||||
Picker::new(
|
||||
files.take(MAX).collect(),
|
||||
files,
|
||||
move |path: &PathBuf| {
|
||||
// format_fn
|
||||
path.strip_prefix(&root)
|
||||
|
@@ -100,8 +100,11 @@ impl<T> Picker<T> {
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
// TODO: len - 1
|
||||
if self.cursor < self.options.len() {
|
||||
if self.matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.cursor < self.matches.len() - 1 {
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
@@ -118,6 +121,18 @@ impl<T> Picker<T> {
|
||||
// - on input change:
|
||||
// - score all the names in relation to input
|
||||
|
||||
fn inner_rect(area: Rect) -> Rect {
|
||||
let padding_vertical = area.height * 20 / 100;
|
||||
let padding_horizontal = area.width * 20 / 100;
|
||||
|
||||
Rect::new(
|
||||
area.x + padding_horizontal,
|
||||
area.y + padding_vertical,
|
||||
area.width - padding_horizontal * 2,
|
||||
area.height - padding_vertical * 2,
|
||||
)
|
||||
}
|
||||
|
||||
impl<T: 'static> Component for Picker<T> {
|
||||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
let key_event = match event {
|
||||
@@ -136,7 +151,11 @@ impl<T: 'static> Component for Picker<T> {
|
||||
code: KeyCode::Up, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('k'),
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_up(),
|
||||
KeyEvent {
|
||||
@@ -144,11 +163,18 @@ impl<T: 'static> Component for Picker<T> {
|
||||
..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('j'),
|
||||
code: KeyCode::Tab, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_down(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
return close_fn;
|
||||
}
|
||||
@@ -162,7 +188,7 @@ impl<T: 'static> Component for Picker<T> {
|
||||
return close_fn;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('x'),
|
||||
code: KeyCode::Char('h'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
if let Some(option) = self.selection() {
|
||||
@@ -191,15 +217,7 @@ impl<T: 'static> Component for Picker<T> {
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let padding_vertical = area.height * 20 / 100;
|
||||
let padding_horizontal = area.width * 20 / 100;
|
||||
|
||||
let area = Rect::new(
|
||||
area.x + padding_horizontal,
|
||||
area.y + padding_vertical,
|
||||
area.width - padding_horizontal * 2,
|
||||
area.height - padding_vertical * 2,
|
||||
);
|
||||
let area = inner_rect(area);
|
||||
|
||||
// -- Render the frame:
|
||||
|
||||
@@ -239,13 +257,14 @@ impl<T: 'static> Component for Picker<T> {
|
||||
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
|
||||
|
||||
let rows = inner.height - 2; // -1 for search bar
|
||||
let offset = self.cursor / (rows as usize) * (rows as usize);
|
||||
|
||||
let files = self.matches.iter().map(|(index, _score)| {
|
||||
let files = self.matches.iter().skip(offset).map(|(index, _score)| {
|
||||
(index, self.options.get(*index).unwrap()) // get_unchecked
|
||||
});
|
||||
|
||||
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
|
||||
if i == self.cursor {
|
||||
if i == (self.cursor - offset) {
|
||||
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
|
||||
}
|
||||
|
||||
@@ -254,12 +273,25 @@ impl<T: 'static> Component for Picker<T> {
|
||||
inner.y + 2 + i as u16,
|
||||
(self.format_fn)(option),
|
||||
inner.width as usize - 1,
|
||||
if i == self.cursor { selected } else { style },
|
||||
if i == (self.cursor - offset) {
|
||||
selected
|
||||
} else {
|
||||
style
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
|
||||
// TODO: this is mostly duplicate code
|
||||
let area = inner_rect(area);
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
// calculate the inner area inside the box
|
||||
let inner = block.inner(area);
|
||||
|
||||
// prompt area
|
||||
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
|
||||
|
||||
self.prompt.cursor_position(area, editor)
|
||||
}
|
||||
}
|
||||
|
@@ -125,13 +125,13 @@ impl<T: Component> Component for Popup<T> {
|
||||
let mut rel_x = position.col as u16;
|
||||
let mut rel_y = position.row as u16;
|
||||
if viewport.width <= rel_x + width {
|
||||
rel_x -= ((rel_x + width) - viewport.width)
|
||||
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 height <= rel_y {
|
||||
rel_y -= height // position above point
|
||||
rel_y = rel_y.saturating_sub(height) // position above point
|
||||
} else {
|
||||
rel_y += 1 // position below point
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ pub struct Prompt {
|
||||
pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum PromptEvent {
|
||||
/// The prompt input has been updated.
|
||||
Update,
|
||||
@@ -28,6 +28,11 @@ pub enum PromptEvent {
|
||||
Abort,
|
||||
}
|
||||
|
||||
pub enum CompletionDirection {
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
pub fn new(
|
||||
prompt: String,
|
||||
@@ -80,11 +85,39 @@ impl Prompt {
|
||||
self.exit_selection();
|
||||
}
|
||||
|
||||
pub fn change_completion_selection(&mut self) {
|
||||
pub fn delete_word_backwards(&mut self) {
|
||||
use helix_core::get_general_category;
|
||||
let mut chars = self.line.char_indices().rev();
|
||||
// TODO add skipping whitespace logic here
|
||||
let (mut i, cat) = match chars.next() {
|
||||
Some((i, c)) => (i, get_general_category(c)),
|
||||
None => return,
|
||||
};
|
||||
self.cursor -= 1;
|
||||
for (nn, nc) in chars {
|
||||
if get_general_category(nc) != cat {
|
||||
break;
|
||||
}
|
||||
i = nn;
|
||||
self.cursor -= 1;
|
||||
}
|
||||
self.line.drain(i..);
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
self.exit_selection();
|
||||
}
|
||||
|
||||
pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
|
||||
if self.completion.is_empty() {
|
||||
return;
|
||||
}
|
||||
let index = self.selection.map_or(0, |i| i + 1) % self.completion.len();
|
||||
|
||||
let index = match direction {
|
||||
CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1),
|
||||
CompletionDirection::Backward => {
|
||||
self.selection.unwrap_or(0) + self.completion.len() - 1
|
||||
}
|
||||
} % self.completion.len();
|
||||
|
||||
self.selection = Some(index);
|
||||
|
||||
let (range, item) = &self.completion[index];
|
||||
@@ -92,8 +125,8 @@ impl Prompt {
|
||||
self.line.replace_range(range.clone(), item);
|
||||
|
||||
self.move_end();
|
||||
// TODO: recalculate completion when completion item is accepted, (Enter)
|
||||
}
|
||||
|
||||
pub fn exit_selection(&mut self) {
|
||||
self.selection = None;
|
||||
}
|
||||
@@ -111,10 +144,24 @@ impl Prompt {
|
||||
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let theme = &cx.editor.theme;
|
||||
let text_color = theme.get("ui.text.focus");
|
||||
let selected_color = theme.get("ui.menu.selected");
|
||||
// completion
|
||||
|
||||
let max_col = area.width / BASE_WIDTH;
|
||||
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
|
||||
let max_len = self
|
||||
.completion
|
||||
.iter()
|
||||
.map(|(_, completion)| completion.len() as u16)
|
||||
.max()
|
||||
.unwrap_or(BASE_WIDTH)
|
||||
.max(BASE_WIDTH);
|
||||
|
||||
let cols = std::cmp::max(1, area.width / max_len);
|
||||
let col_width = (area.width - (cols)) / cols;
|
||||
|
||||
let height = ((self.completion.len() as u16 + cols - 1) / cols)
|
||||
.min(10) // at most 10 rows (or less)
|
||||
.min(area.height);
|
||||
|
||||
let completion_area = Rect::new(
|
||||
area.x,
|
||||
(area.height - height).saturating_sub(1),
|
||||
@@ -131,17 +178,24 @@ impl Prompt {
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
|
||||
for (i, (_range, completion)) in self.completion.iter().enumerate() {
|
||||
// TODO: paginate
|
||||
for (i, (_range, completion)) in self
|
||||
.completion
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(height as usize * cols as usize)
|
||||
{
|
||||
let color = if Some(i) == self.selection {
|
||||
Style::default().bg(Color::Rgb(104, 60, 232))
|
||||
// Style::default().bg(Color::Rgb(104, 60, 232))
|
||||
selected_color // TODO: just invert bg
|
||||
} else {
|
||||
text_color
|
||||
};
|
||||
surface.set_stringn(
|
||||
area.x + 1 + col * BASE_WIDTH,
|
||||
area.x + col * (1 + col_width),
|
||||
area.y + row,
|
||||
&completion,
|
||||
BASE_WIDTH as usize - 1,
|
||||
col_width.saturating_sub(1) as usize,
|
||||
color,
|
||||
);
|
||||
row += 1;
|
||||
@@ -149,23 +203,21 @@ impl Prompt {
|
||||
row = 0;
|
||||
col += 1;
|
||||
}
|
||||
if col > max_col {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(doc) = (self.doc_fn)(&self.line) {
|
||||
let text = ui::Text::new(doc.to_string());
|
||||
|
||||
let area = Rect::new(
|
||||
let viewport = area;
|
||||
let area = viewport.intersection(Rect::new(
|
||||
completion_area.x,
|
||||
completion_area.y - 3,
|
||||
completion_area.width,
|
||||
BASE_WIDTH * 3,
|
||||
3,
|
||||
);
|
||||
));
|
||||
|
||||
let background = theme.get("ui.window");
|
||||
let background = theme.get("ui.help");
|
||||
surface.clear_with(area, background);
|
||||
|
||||
use tui::layout::Margin;
|
||||
@@ -239,6 +291,10 @@ impl Component for Prompt {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_start(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.delete_word_backwards(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -250,12 +306,21 @@ impl Component for Prompt {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
} => {
|
||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
|
||||
return close_fn;
|
||||
if self.line.ends_with('/') {
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
self.exit_selection();
|
||||
} else {
|
||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
|
||||
return close_fn;
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab, ..
|
||||
} => self.change_completion_selection(),
|
||||
} => self.change_completion_selection(CompletionDirection::Forward),
|
||||
KeyEvent {
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
} => self.change_completion_selection(CompletionDirection::Backward),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('q'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
@@ -271,8 +336,9 @@ impl Component for Prompt {
|
||||
}
|
||||
|
||||
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
|
||||
let line = area.height as usize - 1;
|
||||
Some(Position::new(
|
||||
area.height as usize,
|
||||
area.y as usize + line,
|
||||
area.x as usize + self.prompt.len() + self.cursor,
|
||||
))
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-tui"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
@@ -16,5 +16,5 @@ bitflags = "1.0"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
crossterm = { version = "0.19", optional = true }
|
||||
crossterm = { version = "0.20", optional = true }
|
||||
serde = { version = "1", "optional" = true, features = ["derive"]}
|
||||
|
@@ -137,14 +137,12 @@ where
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
if self.viewport.resize_behavior == ResizeBehavior::Auto {
|
||||
let size = self.size()?;
|
||||
if size != self.viewport.area {
|
||||
self.resize(size)?;
|
||||
}
|
||||
pub fn autoresize(&mut self) -> io::Result<Rect> {
|
||||
let size = self.size()?;
|
||||
if size != self.viewport.area {
|
||||
self.resize(size)?;
|
||||
};
|
||||
Ok(())
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
|
@@ -13,12 +13,12 @@ mod block;
|
||||
// mod list;
|
||||
mod paragraph;
|
||||
mod reflow;
|
||||
// mod table;
|
||||
mod table;
|
||||
|
||||
pub use self::block::{Block, BorderType};
|
||||
// pub use self::list::{List, ListItem, ListState};
|
||||
pub use self::paragraph::{Paragraph, Wrap};
|
||||
// pub use self::table::{Cell, Row, Table, TableState};
|
||||
pub use self::table::{Cell, Row, Table, TableState};
|
||||
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
use bitflags::bitflags;
|
||||
|
@@ -3,7 +3,7 @@ use crate::{
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use cassowary::{
|
||||
strength::{MEDIUM, REQUIRED, WEAK},
|
||||
@@ -368,8 +368,8 @@ impl<'a> Table<'a> {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
pub offset: usize,
|
||||
pub selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for TableState {
|
||||
@@ -394,10 +394,11 @@ impl TableState {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for Table<'a> {
|
||||
type State = TableState;
|
||||
// impl<'a> StatefulWidget for Table<'a> {
|
||||
impl<'a> Table<'a> {
|
||||
// type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -522,7 +523,7 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
|
||||
impl<'a> Widget for Table<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
Table::render_table(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,7 @@
|
||||
use helix_tui::{
|
||||
backend::{Backend, TestBackend},
|
||||
layout::Rect,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
use std::error::Error;
|
||||
|
||||
#[test]
|
||||
fn terminal_buffer_size_should_be_limited() {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-view"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -18,8 +18,8 @@ helix-lsp = { path = "../helix-lsp"}
|
||||
|
||||
# Conversion traits
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
|
||||
crossterm = { version = "0.19", features = ["event-stream"], optional = true }
|
||||
once_cell = "1.4"
|
||||
crossterm = { version = "0.20", features = ["event-stream"], optional = true }
|
||||
once_cell = "1.8"
|
||||
url = "2"
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -28,3 +28,4 @@ slotmap = "1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.5"
|
||||
log = "~0.4"
|
||||
|
@@ -1,18 +1,20 @@
|
||||
use anyhow::{Context, Error};
|
||||
use std::cell::Cell;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use helix_core::{
|
||||
history::History,
|
||||
syntax::{LanguageConfiguration, LOADER},
|
||||
ChangeSet, Diagnostic, History, Rope, Selection, State, Syntax, Transaction,
|
||||
ChangeSet, Diagnostic, Rope, Selection, State, Syntax, Transaction,
|
||||
};
|
||||
|
||||
use crate::{DocumentId, ViewId};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Select,
|
||||
@@ -40,14 +42,40 @@ pub struct Document {
|
||||
/// State at last commit. Used for calculating reverts.
|
||||
old_state: Option<State>,
|
||||
/// Undo tree.
|
||||
history: History,
|
||||
// It can be used as a cell where we will take it out to get some parts of the history and put
|
||||
// it back as it separated from the edits. We could split out the parts manually but that will
|
||||
// be more troublesome.
|
||||
history: Cell<History>,
|
||||
last_saved_revision: usize,
|
||||
version: i32, // should be usize?
|
||||
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
language_server: Option<Arc<helix_lsp::Client>>,
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
impl fmt::Debug for Document {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Document")
|
||||
.field("id", &self.id)
|
||||
.field("text", &self.text)
|
||||
.field("selections", &self.selections)
|
||||
.field("path", &self.path)
|
||||
.field("mode", &self.mode)
|
||||
.field("restore_cursor", &self.restore_cursor)
|
||||
.field("syntax", &self.syntax)
|
||||
.field("language", &self.language)
|
||||
.field("changes", &self.changes)
|
||||
.field("old_state", &self.old_state)
|
||||
// .field("history", &self.history)
|
||||
.field("last_saved_revision", &self.last_saved_revision)
|
||||
.field("version", &self.version)
|
||||
.field("diagnostics", &self.diagnostics)
|
||||
// .field("language_server", &self.language_server)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Like std::mem::replace() except it allows the replacement value to be mapped from the
|
||||
/// original value.
|
||||
fn take_with<T, F>(mut_ref: &mut T, closure: F)
|
||||
@@ -64,6 +92,50 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize a path, removing things like `.` and `..`.
|
||||
///
|
||||
/// CAUTION: This does not resolve symlinks (unlike
|
||||
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
|
||||
/// behavior at times. This should be used carefully. Unfortunately,
|
||||
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
|
||||
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
|
||||
/// needs to improve on.
|
||||
/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
components.next();
|
||||
PathBuf::from(c.as_os_str())
|
||||
} else {
|
||||
PathBuf::new()
|
||||
};
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
Component::Prefix(..) => unreachable!(),
|
||||
Component::RootDir => {
|
||||
ret.push(component.as_os_str());
|
||||
}
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
// Returns the canonical, absolute form of a path with all intermediate components normalized.
|
||||
//
|
||||
// This function is used instead of `std::fs::canonicalize` because we don't want to verify
|
||||
// here if the path exists, just normalize it's components.
|
||||
pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
|
||||
std::env::current_dir().map(|current_dir| normalize_path(¤t_dir.join(path)))
|
||||
}
|
||||
|
||||
use helix_lsp::lsp;
|
||||
use url::Url;
|
||||
|
||||
@@ -85,37 +157,58 @@ impl Document {
|
||||
old_state,
|
||||
diagnostics: Vec::new(),
|
||||
version: 0,
|
||||
history: History::default(),
|
||||
history: Cell::new(History::default()),
|
||||
last_saved_revision: 0,
|
||||
language_server: None,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: passing scopes here is awkward
|
||||
// TODO: async fn?
|
||||
pub fn load(path: PathBuf, scopes: &[String]) -> Result<Self, Error> {
|
||||
use std::{env, fs::File, io::BufReader};
|
||||
let _current_dir = env::current_dir()?;
|
||||
pub fn load(path: PathBuf) -> Result<Self, Error> {
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
||||
let file = File::open(path.clone()).context(format!("unable to open {:?}", path))?;
|
||||
let doc = Rope::from_reader(BufReader::new(file))?;
|
||||
|
||||
// TODO: create if not found
|
||||
let doc = if !path.exists() {
|
||||
Rope::from("\n")
|
||||
} else {
|
||||
let file = File::open(&path).context(format!("unable to open {:?}", path))?;
|
||||
let mut doc = Rope::from_reader(BufReader::new(file))?;
|
||||
// add missing newline at the end of file
|
||||
if doc.len_bytes() == 0 || doc.byte(doc.len_bytes() - 1) != b'\n' {
|
||||
doc.insert_char(doc.len_chars(), '\n');
|
||||
}
|
||||
doc
|
||||
};
|
||||
|
||||
let mut doc = Self::new(doc);
|
||||
|
||||
let language_config = LOADER
|
||||
.get()
|
||||
.unwrap()
|
||||
.language_config_for_file_name(path.as_path());
|
||||
doc.set_language(language_config, scopes);
|
||||
|
||||
// canonicalize path to absolute value
|
||||
doc.path = Some(std::fs::canonicalize(path)?);
|
||||
// set the path and try detecting the language
|
||||
doc.set_path(&path)?;
|
||||
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
// TODO: remove view_id dependency here
|
||||
pub fn format(&mut self, view_id: ViewId) {
|
||||
if let Some(language_server) = self.language_server() {
|
||||
// TODO: await, no blocking
|
||||
let transaction = helix_lsp::block_on(
|
||||
language_server
|
||||
.text_document_formatting(self.identifier(), lsp::FormattingOptions::default()),
|
||||
)
|
||||
.map(|edits| {
|
||||
helix_lsp::util::generate_transaction_from_edits(
|
||||
self.text(),
|
||||
edits,
|
||||
language_server.offset_encoding(),
|
||||
)
|
||||
});
|
||||
|
||||
if let Ok(transaction) = transaction {
|
||||
self.apply(&transaction, view_id);
|
||||
self.append_changes_to_history(view_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
|
||||
// or is that handled by the OS/async layer
|
||||
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
|
||||
@@ -131,10 +224,20 @@ impl Document {
|
||||
let language_server = self.language_server.clone();
|
||||
|
||||
// reset the modified flag
|
||||
self.last_saved_revision = self.history.current_revision();
|
||||
let history = self.history.take();
|
||||
self.last_saved_revision = history.current_revision();
|
||||
self.history.set(history);
|
||||
|
||||
async move {
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
if let Some(parent) = path.parent() {
|
||||
// TODO: display a prompt asking the user if the directories should be created
|
||||
if !parent.exists() {
|
||||
return Err(Error::msg(
|
||||
"can't save file, parent directory does not exist",
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut file = File::create(path).await?;
|
||||
|
||||
// write all the rope chunks to file
|
||||
@@ -153,6 +256,28 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_language(&mut self) {
|
||||
if let Some(path) = self.path() {
|
||||
let loader = LOADER.get().unwrap();
|
||||
let language_config = loader.language_config_for_file_name(path);
|
||||
let scopes = loader.scopes();
|
||||
self.set_language(language_config, scopes);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
|
||||
let path = canonicalize_path(path)?;
|
||||
|
||||
// if parent doesn't exist we still want to open the document
|
||||
// and error out when document is saved
|
||||
self.path = Some(path);
|
||||
|
||||
// try detecting the language based on filepath
|
||||
self.detect_language();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_language(
|
||||
&mut self,
|
||||
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
|
||||
@@ -172,8 +297,10 @@ impl Document {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_language2(&mut self, scope: &str, scopes: &[String]) {
|
||||
let language_config = LOADER.get().unwrap().language_config_for_scope(scope);
|
||||
pub fn set_language2(&mut self, scope: &str) {
|
||||
let loader = LOADER.get().unwrap();
|
||||
let language_config = loader.language_config_for_scope(scope);
|
||||
let scopes = loader.scopes();
|
||||
|
||||
self.set_language(language_config, scopes);
|
||||
}
|
||||
@@ -261,28 +388,48 @@ impl Document {
|
||||
success
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, view_id: ViewId) -> bool {
|
||||
if let Some(transaction) = self.history.undo() {
|
||||
let success = self._apply(&transaction, view_id);
|
||||
pub fn undo(&mut self, view_id: ViewId) {
|
||||
let mut history = self.history.take();
|
||||
let success = if let Some(transaction) = history.undo() {
|
||||
self._apply(&transaction, view_id)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
self.history.set(history);
|
||||
|
||||
if success {
|
||||
// reset changeset to fix len
|
||||
self.changes = ChangeSet::new(self.text());
|
||||
|
||||
return success;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, view_id: ViewId) -> bool {
|
||||
if let Some(transaction) = self.history.redo() {
|
||||
let success = self._apply(&transaction, view_id);
|
||||
pub fn redo(&mut self, view_id: ViewId) {
|
||||
let mut history = self.history.take();
|
||||
let success = if let Some(transaction) = history.redo() {
|
||||
self._apply(&transaction, view_id)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
self.history.set(history);
|
||||
|
||||
if success {
|
||||
// reset changeset to fix len
|
||||
self.changes = ChangeSet::new(self.text());
|
||||
|
||||
return success;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
|
||||
let txns = self.history.get_mut().earlier(uk);
|
||||
for txn in txns {
|
||||
self._apply(&txn, view_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
|
||||
let txns = self.history.get_mut().later(uk);
|
||||
for txn in txns {
|
||||
self._apply(&txn, view_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_changes_to_history(&mut self, view_id: ViewId) {
|
||||
@@ -300,7 +447,9 @@ impl Document {
|
||||
// HAXX: we need to reconstruct the state as it was before the changes..
|
||||
let old_state = self.old_state.take().expect("no old_state available");
|
||||
|
||||
self.history.commit_revision(&transaction, &old_state);
|
||||
let mut history = self.history.take();
|
||||
history.commit_revision(&transaction, &old_state);
|
||||
self.history.set(history);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -310,9 +459,10 @@ impl Document {
|
||||
|
||||
#[inline]
|
||||
pub fn is_modified(&self) -> bool {
|
||||
self.path.is_some()
|
||||
&& (self.history.current_revision() != self.last_saved_revision
|
||||
|| !self.changes.is_empty())
|
||||
let history = self.history.take();
|
||||
let current_revision = history.current_revision();
|
||||
self.history.set(history);
|
||||
current_revision != self.last_saved_revision || !self.changes.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -407,6 +557,14 @@ impl Document {
|
||||
pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier {
|
||||
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
|
||||
}
|
||||
|
||||
pub fn diagnostics(&self) -> &[Diagnostic] {
|
||||
&self.diagnostics
|
||||
}
|
||||
|
||||
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
|
||||
self.diagnostics = diagnostics;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use crate::{theme::Theme, tree::Tree, Document, DocumentId, View, ViewId};
|
||||
use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -9,17 +9,19 @@ use anyhow::Error;
|
||||
|
||||
pub use helix_core::diagnostic::Severity;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Editor {
|
||||
pub tree: Tree,
|
||||
pub documents: SlotMap<DocumentId, Document>,
|
||||
pub count: Option<usize>,
|
||||
pub count: Option<std::num::NonZeroUsize>,
|
||||
pub register: RegisterSelection,
|
||||
pub theme: Theme,
|
||||
pub language_servers: helix_lsp::Registry,
|
||||
|
||||
pub status_msg: Option<(String, Severity)>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Action {
|
||||
Replace,
|
||||
HorizontalSplit,
|
||||
@@ -36,6 +38,18 @@ impl Editor {
|
||||
.unwrap_or(include_bytes!("../../theme.toml"));
|
||||
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
|
||||
|
||||
// initialize language registry
|
||||
use helix_core::syntax::{Loader, LOADER};
|
||||
|
||||
// load $HOME/.config/helix/languages.toml, fallback to default config
|
||||
let config = std::fs::read(helix_core::config_dir().join("languages.toml"));
|
||||
let toml = config
|
||||
.as_deref()
|
||||
.unwrap_or(include_bytes!("../../languages.toml"));
|
||||
|
||||
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
|
||||
LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec()));
|
||||
|
||||
let language_servers = helix_lsp::Registry::new();
|
||||
|
||||
// HAXX: offset the render area height by 1 to account for prompt/commandline
|
||||
@@ -45,12 +59,17 @@ impl Editor {
|
||||
tree: Tree::new(area),
|
||||
documents: SlotMap::with_key(),
|
||||
count: None,
|
||||
register: RegisterSelection::default(),
|
||||
theme,
|
||||
language_servers,
|
||||
status_msg: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_status(&mut self) {
|
||||
self.status_msg = None;
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: String) {
|
||||
self.status_msg = Some((status, Severity::Info));
|
||||
}
|
||||
@@ -69,6 +88,12 @@ impl Editor {
|
||||
pub fn switch(&mut self, id: DocumentId, action: Action) {
|
||||
use crate::tree::Layout;
|
||||
use helix_core::Selection;
|
||||
|
||||
if !self.documents.contains_key(id) {
|
||||
log::error!("cannot switch to document that does not exist (anymore)");
|
||||
return;
|
||||
}
|
||||
|
||||
match action {
|
||||
Action::Replace => {
|
||||
let view = self.view();
|
||||
@@ -79,6 +104,7 @@ impl Editor {
|
||||
|
||||
let view = self.view_mut();
|
||||
view.jumps.push(jump);
|
||||
view.last_accessed_doc = Some(view.doc);
|
||||
view.doc = id;
|
||||
view.first_line = 0;
|
||||
|
||||
@@ -125,7 +151,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
|
||||
let path = std::fs::canonicalize(path)?;
|
||||
let path = crate::document::canonicalize_path(&path)?;
|
||||
|
||||
let id = self
|
||||
.documents()
|
||||
@@ -135,13 +161,13 @@ impl Editor {
|
||||
let id = if let Some(id) = id {
|
||||
id
|
||||
} else {
|
||||
let mut doc = Document::load(path, self.theme.scopes())?;
|
||||
let mut doc = Document::load(path)?;
|
||||
|
||||
// try to find a language server based on the language name
|
||||
let language_server = doc
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|language| self.language_servers.get(language));
|
||||
.and_then(|language| self.language_servers.get(language).ok());
|
||||
|
||||
if let Some(language_server) = language_server {
|
||||
doc.set_language_server(Some(language_server.clone()));
|
||||
@@ -182,7 +208,7 @@ impl Editor {
|
||||
let language_server = doc
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|language| language_servers.get(language));
|
||||
.and_then(|language| language_servers.get(language).ok());
|
||||
if let Some(language_server) = language_server {
|
||||
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
|
||||
}
|
||||
@@ -194,8 +220,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, area: Rect) {
|
||||
self.tree.resize(area);
|
||||
self._refresh();
|
||||
if self.tree.resize(area) {
|
||||
self._refresh();
|
||||
};
|
||||
}
|
||||
|
||||
pub fn focus_next(&mut self) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
pub mod register_selection;
|
||||
pub mod theme;
|
||||
pub mod tree;
|
||||
pub mod view;
|
||||
@@ -10,5 +11,6 @@ new_key_type! { pub struct ViewId; }
|
||||
|
||||
pub use document::Document;
|
||||
pub use editor::Editor;
|
||||
pub use register_selection::RegisterSelection;
|
||||
pub use theme::Theme;
|
||||
pub use view::View;
|
||||
|
48
helix-view/src/register_selection.rs
Normal file
48
helix-view/src/register_selection.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
/// Register selection and configuration
|
||||
///
|
||||
/// This is a kind a of specialized `Option<char>` for register selection.
|
||||
/// Point is to keep whether the register selection has been explicitely
|
||||
/// set or not while being convenient by knowing the default register name.
|
||||
#[derive(Debug)]
|
||||
pub struct RegisterSelection {
|
||||
selected: char,
|
||||
default_name: char,
|
||||
}
|
||||
|
||||
impl RegisterSelection {
|
||||
pub fn new(default_name: char) -> Self {
|
||||
Self {
|
||||
selected: default_name,
|
||||
default_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&mut self, name: char) {
|
||||
self.selected = name;
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> Self {
|
||||
Self {
|
||||
selected: std::mem::replace(&mut self.selected, self.default_name),
|
||||
default_name: self.default_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.selected == self.default_name
|
||||
}
|
||||
|
||||
pub fn name(&self) -> char {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegisterSelection {
|
||||
fn default() -> Self {
|
||||
let default_name = '"';
|
||||
Self {
|
||||
selected: default_name,
|
||||
default_name,
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,10 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use toml::Value;
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
pub use tui::style::{Color, Style};
|
||||
pub use tui::style::{Color, Modifier, Style};
|
||||
|
||||
// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||
// pub struct Color {
|
||||
@@ -115,6 +116,7 @@ impl<'de> Deserialize<'de> for Theme {
|
||||
}
|
||||
|
||||
fn parse_style(style: &mut Style, value: Value) {
|
||||
//TODO: alert user of parsing failures
|
||||
if let Value::Table(entries) = value {
|
||||
for (name, value) in entries {
|
||||
match name.as_str() {
|
||||
@@ -128,6 +130,13 @@ fn parse_style(style: &mut Style, value: Value) {
|
||||
*style = style.bg(color);
|
||||
}
|
||||
}
|
||||
"modifiers" => {
|
||||
if let Value::Array(arr) = value {
|
||||
for modifier in arr.iter().filter_map(parse_modifier) {
|
||||
*style = style.add_modifier(modifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -157,9 +166,34 @@ fn parse_color(value: Value) -> Option<Color> {
|
||||
if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
|
||||
Some(Color::Rgb(red, green, blue))
|
||||
} else {
|
||||
warn!("malformed hexcode in theme: {}", s);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
warn!("unrecognized value in theme: {}", value);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_modifier(value: &Value) -> Option<Modifier> {
|
||||
if let Value::String(s) = value {
|
||||
match s.as_str() {
|
||||
"bold" => Some(Modifier::BOLD),
|
||||
"dim" => Some(Modifier::DIM),
|
||||
"italic" => Some(Modifier::ITALIC),
|
||||
"underlined" => Some(Modifier::UNDERLINED),
|
||||
"slow_blink" => Some(Modifier::SLOW_BLINK),
|
||||
"rapid_blink" => Some(Modifier::RAPID_BLINK),
|
||||
"reversed" => Some(Modifier::REVERSED),
|
||||
"hidden" => Some(Modifier::HIDDEN),
|
||||
"crossed_out" => Some(Modifier::CROSSED_OUT),
|
||||
_ => {
|
||||
warn!("unrecognized modifier in theme: {}", s);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("unrecognized modifier in theme: {}", value);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -177,3 +211,39 @@ impl Theme {
|
||||
&self.scopes
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_string() {
|
||||
let fg = Value::String("#ffffff".to_string());
|
||||
|
||||
let mut style = Style::default();
|
||||
parse_style(&mut style, fg);
|
||||
|
||||
assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_table() {
|
||||
let table = toml::toml! {
|
||||
"keyword" = {
|
||||
fg = "#ffffff",
|
||||
bg = "#000000",
|
||||
modifiers = ["bold"],
|
||||
}
|
||||
};
|
||||
|
||||
let mut style = Style::default();
|
||||
if let Value::Table(entries) = table {
|
||||
for (_name, value) in entries {
|
||||
parse_style(&mut style, value);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
style,
|
||||
Style::default()
|
||||
.fg(Color::Rgb(255, 255, 255))
|
||||
.bg(Color::Rgb(0, 0, 0))
|
||||
.add_modifier(Modifier::BOLD)
|
||||
);
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ use tui::layout::Rect;
|
||||
|
||||
// the dimensions are recomputed on windo resize/tree change.
|
||||
//
|
||||
#[derive(Debug)]
|
||||
pub struct Tree {
|
||||
root: ViewId,
|
||||
// (container, index inside the container)
|
||||
@@ -17,11 +18,13 @@ pub struct Tree {
|
||||
stack: Vec<(ViewId, Rect)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Node {
|
||||
parent: ViewId,
|
||||
content: Content,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Content {
|
||||
View(Box<View>),
|
||||
Container(Box<Container>),
|
||||
@@ -45,13 +48,14 @@ impl Node {
|
||||
|
||||
// TODO: screen coord to container + container coordinate helpers
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
// could explore stacked/tabbed
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Container {
|
||||
layout: Layout,
|
||||
children: Vec<ViewId>,
|
||||
@@ -293,9 +297,13 @@ impl Tree {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, area: Rect) {
|
||||
self.area = area;
|
||||
self.recalculate();
|
||||
pub fn resize(&mut self, area: Rect) -> bool {
|
||||
if self.area != area {
|
||||
self.area = area;
|
||||
self.recalculate();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn recalculate(&mut self) {
|
||||
@@ -428,6 +436,7 @@ impl Tree {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Traverse<'a> {
|
||||
tree: &'a Tree,
|
||||
stack: Vec<ViewId>, // TODO: reuse the one we use on update
|
||||
|
@@ -12,6 +12,7 @@ pub const PADDING: usize = 5;
|
||||
|
||||
type Jump = (DocumentId, Selection);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JumpList {
|
||||
jumps: Vec<Jump>,
|
||||
current: usize,
|
||||
@@ -37,20 +38,28 @@ impl JumpList {
|
||||
pub fn forward(&mut self, count: usize) -> Option<&Jump> {
|
||||
if self.current + count < self.jumps.len() {
|
||||
self.current += count;
|
||||
return self.jumps.get(self.current);
|
||||
self.jumps.get(self.current)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn backward(&mut self, count: usize) -> Option<&Jump> {
|
||||
if self.current.checked_sub(count).is_some() {
|
||||
self.current -= count;
|
||||
return self.jumps.get(self.current);
|
||||
// Taking view and doc to prevent unnecessary cloning when jump is not required.
|
||||
pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) -> Option<&Jump> {
|
||||
if let Some(current) = self.current.checked_sub(count) {
|
||||
if self.current == self.jumps.len() {
|
||||
let jump = (doc.id(), doc.selection(view_id).clone());
|
||||
self.push(jump);
|
||||
}
|
||||
self.current = current;
|
||||
self.jumps.get(self.current)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct View {
|
||||
pub id: ViewId,
|
||||
pub doc: DocumentId,
|
||||
@@ -58,6 +67,8 @@ pub struct View {
|
||||
pub first_col: usize,
|
||||
pub area: Rect,
|
||||
pub jumps: JumpList,
|
||||
/// the last accessed file before the current one
|
||||
pub last_accessed_doc: Option<DocumentId>,
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -69,6 +80,7 @@ impl View {
|
||||
first_col: 0,
|
||||
area: Rect::default(), // will get calculated upon inserting into tree
|
||||
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
|
||||
last_accessed_doc: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +155,9 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
let row = line - self.first_line as usize;
|
||||
let col = col - self.first_col as usize;
|
||||
// It is possible for underflow to occur if the buffer length is larger than the terminal width.
|
||||
let row = line.saturating_sub(self.first_line);
|
||||
let col = col.saturating_sub(self.first_col);
|
||||
|
||||
Some(Position::new(row, col))
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ scope = "source.rust"
|
||||
injection-regex = "rust"
|
||||
file-types = ["rs"]
|
||||
roots = []
|
||||
auto-format = true
|
||||
|
||||
language-server = { command = "rust-analyzer" }
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
@@ -17,6 +18,15 @@ roots = []
|
||||
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "elixir"
|
||||
scope = "source.elixir"
|
||||
injection-regex = "elixir"
|
||||
file-types = ["ex", "exs"]
|
||||
roots = []
|
||||
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "json"
|
||||
scope = "source.json"
|
||||
@@ -33,6 +43,7 @@ injection-regex = "c"
|
||||
file-types = ["c"] # TODO: ["h"]
|
||||
roots = []
|
||||
|
||||
language-server = { command = "clangd" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
@@ -42,6 +53,7 @@ injection-regex = "cpp"
|
||||
file-types = ["cc", "cpp", "hpp", "h"]
|
||||
roots = []
|
||||
|
||||
language-server = { command = "clangd" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
@@ -50,6 +62,7 @@ scope = "source.go"
|
||||
injection-regex = "go"
|
||||
file-types = ["go"]
|
||||
roots = ["Gopkg.toml", "go.mod"]
|
||||
auto-format = true
|
||||
|
||||
language-server = { command = "gopls" }
|
||||
# TODO: gopls needs utf-8 offsets?
|
||||
@@ -65,6 +78,17 @@ roots = []
|
||||
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "typescript"
|
||||
scope = "source.ts"
|
||||
injection-regex = "^(ts|typescript)$"
|
||||
file-types = ["ts"]
|
||||
roots = []
|
||||
# TODO: highlights-jsx, highlights-params
|
||||
|
||||
language-server = { command = "typescript-language-server", args = ["--stdio"] }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "css"
|
||||
scope = "source.css"
|
||||
@@ -94,6 +118,15 @@ language-server = { command = "pyls" }
|
||||
# TODO: pyls needs utf-8 offsets
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "nix"
|
||||
scope = "source.nix"
|
||||
injection-regex = "nix"
|
||||
file-types = ["nix"]
|
||||
roots = []
|
||||
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "ruby"
|
||||
scope = "source.ruby"
|
||||
@@ -122,3 +155,12 @@ file-types = ["php"]
|
||||
roots = []
|
||||
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
# [[language]]
|
||||
# name = "haskell"
|
||||
# scope = "source.haskell"
|
||||
# injection-regex = "haskell"
|
||||
# file-types = ["hs"]
|
||||
# roots = []
|
||||
#
|
||||
# indent = { tab-width = 2, unit = " " }
|
||||
|
138
runtime/queries/elixir/highlights.scm
Normal file
138
runtime/queries/elixir/highlights.scm
Normal file
@@ -0,0 +1,138 @@
|
||||
["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
|
||||
|
||||
[(true) (false) (nil)] @constant.builtin
|
||||
|
||||
(keyword
|
||||
[(keyword_literal)
|
||||
":"] @tag)
|
||||
|
||||
(keyword
|
||||
(keyword_string
|
||||
[(string_start)
|
||||
(string_content)
|
||||
(string_end)] @tag))
|
||||
|
||||
[(atom_literal)
|
||||
(atom_start)
|
||||
(atom_content)
|
||||
(atom_end)] @tag
|
||||
|
||||
[(comment)
|
||||
(unused_identifier)] @comment
|
||||
|
||||
(escape_sequence) @escape
|
||||
|
||||
(call function: (function_identifier) @keyword
|
||||
(#match? @keyword "^(defmodule|defexception|defp|def|with|case|cond|raise|import|require|use|defmacrop|defmacro|defguardp|defguard|defdelegate|defstruct|alias|defimpl|defprotocol|defoverridable|receive|if|for|try|throw|unless|reraise|super|quote|unquote|unquote_splicing)$"))
|
||||
|
||||
(call function: (function_identifier) @keyword
|
||||
[(call
|
||||
function: (function_identifier) @function
|
||||
(arguments
|
||||
[(identifier) @variable.parameter
|
||||
(_ (identifier) @variable.parameter)
|
||||
(_ (_ (identifier) @variable.parameter))
|
||||
(_ (_ (_ (identifier) @variable.parameter)))
|
||||
(_ (_ (_ (_ (identifier) @variable.parameter))))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
|
||||
(binary_op
|
||||
left:
|
||||
(call
|
||||
function: (function_identifier) @function
|
||||
(arguments
|
||||
[(identifier) @variable.parameter
|
||||
(_ (identifier) @variable.parameter)
|
||||
(_ (_ (identifier) @variable.parameter))
|
||||
(_ (_ (_ (identifier) @variable.parameter)))
|
||||
(_ (_ (_ (_ (identifier) @variable.parameter))))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
|
||||
operator: "when")
|
||||
(binary_op
|
||||
left: (identifier) @variable.parameter
|
||||
operator: _ @function
|
||||
right: (identifier) @variable.parameter)]
|
||||
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
|
||||
|
||||
(call (function_identifier) @keyword
|
||||
[(call
|
||||
function: (function_identifier) @function)
|
||||
(identifier) @function
|
||||
(binary_op
|
||||
left:
|
||||
[(call
|
||||
function: (function_identifier) @function)
|
||||
(identifier) @function]
|
||||
operator: "when")]
|
||||
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
|
||||
|
||||
(anonymous_function
|
||||
(stab_expression
|
||||
left: (bare_arguments
|
||||
[(identifier) @variable.parameter
|
||||
(_ (identifier) @variable.parameter)
|
||||
(_ (_ (identifier) @variable.parameter))
|
||||
(_ (_ (_ (identifier) @variable.parameter)))
|
||||
(_ (_ (_ (_ (identifier) @variable.parameter))))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])))
|
||||
|
||||
(unary_op
|
||||
operator: "@"
|
||||
(call (identifier) @attribute
|
||||
(heredoc
|
||||
[(heredoc_start)
|
||||
(heredoc_content)
|
||||
(heredoc_end)] @doc))
|
||||
(#match? @attribute "^(doc|moduledoc)$"))
|
||||
|
||||
(module) @type
|
||||
|
||||
(unary_op
|
||||
operator: "@" @attribute
|
||||
[(call
|
||||
function: (function_identifier) @attribute)
|
||||
(identifier) @attribute])
|
||||
|
||||
(unary_op
|
||||
operator: _ @operator)
|
||||
|
||||
(binary_op
|
||||
operator: _ @operator)
|
||||
|
||||
(heredoc
|
||||
[(heredoc_start)
|
||||
(heredoc_content)
|
||||
(heredoc_end)] @string)
|
||||
|
||||
(string
|
||||
[(string_start)
|
||||
(string_content)
|
||||
(string_end)] @string)
|
||||
|
||||
(sigil_start) @string.special
|
||||
(sigil_content) @string
|
||||
(sigil_end) @string.special
|
||||
|
||||
(interpolation
|
||||
"#{" @punctuation.special
|
||||
"}" @punctuation.special)
|
||||
|
||||
[
|
||||
","
|
||||
"->"
|
||||
"."
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
"<<"
|
||||
">>"
|
||||
] @punctuation.bracket
|
||||
|
||||
(special_identifier) @function.special
|
||||
|
||||
(ERROR) @warning
|
@@ -3,15 +3,19 @@ indent = [
|
||||
"const_declaration",
|
||||
"var_declaration",
|
||||
"type_declaration",
|
||||
"function_declaration",
|
||||
"method_declaration",
|
||||
"type_spec",
|
||||
# simply block should be enough
|
||||
# "function_declaration",
|
||||
# "method_declaration",
|
||||
"composite_literal",
|
||||
"func_literal",
|
||||
"literal_value",
|
||||
"expression_case",
|
||||
"default_case",
|
||||
"type_case",
|
||||
"communication_case",
|
||||
"argument_list",
|
||||
"block"
|
||||
"block",
|
||||
]
|
||||
|
||||
outdent = [
|
||||
|
43
runtime/queries/haskell/highlights.scm
Normal file
43
runtime/queries/haskell/highlights.scm
Normal file
@@ -0,0 +1,43 @@
|
||||
(variable) @variable
|
||||
(operator) @operator
|
||||
(exp_name (constructor) @constructor)
|
||||
(constructor_operator) @operator
|
||||
(module) @module_name
|
||||
(type) @type
|
||||
(type) @class
|
||||
(constructor) @constructor
|
||||
(pragma) @pragma
|
||||
(comment) @comment
|
||||
(signature name: (variable) @fun_type_name)
|
||||
(function name: (variable) @fun_name)
|
||||
(constraint class: (class_name (type)) @class)
|
||||
(class (class_head class: (class_name (type)) @class))
|
||||
(instance (instance_head class: (class_name (type)) @class))
|
||||
(integer) @literal
|
||||
(exp_literal (float)) @literal
|
||||
(char) @literal
|
||||
(con_unit) @literal
|
||||
(con_list) @literal
|
||||
(tycon_arrow) @operator
|
||||
(where) @keyword
|
||||
"module" @keyword
|
||||
"let" @keyword
|
||||
"in" @keyword
|
||||
"class" @keyword
|
||||
"instance" @keyword
|
||||
"data" @keyword
|
||||
"newtype" @keyword
|
||||
"family" @keyword
|
||||
"type" @keyword
|
||||
"import" @keyword
|
||||
"qualified" @keyword
|
||||
"as" @keyword
|
||||
"deriving" @keyword
|
||||
"via" @keyword
|
||||
"stock" @keyword
|
||||
"anyclass" @keyword
|
||||
"do" @keyword
|
||||
"mdo" @keyword
|
||||
"rec" @keyword
|
||||
"(" @paren
|
||||
")" @paren
|
4
runtime/queries/haskell/locals.scm
Normal file
4
runtime/queries/haskell/locals.scm
Normal file
@@ -0,0 +1,4 @@
|
||||
(signature name: (variable)) @local.definition
|
||||
(function name: (variable)) @local.definition
|
||||
(pat_name (variable)) @local.definition
|
||||
(exp_name (variable)) @local.reference
|
28
runtime/queries/javascript/indents.toml
Normal file
28
runtime/queries/javascript/indents.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
indent = [
|
||||
"array",
|
||||
"object",
|
||||
"arguments",
|
||||
"formal_parameters",
|
||||
|
||||
"statement_block",
|
||||
"object_pattern",
|
||||
"class_body",
|
||||
"named_imports",
|
||||
|
||||
"binary_expression",
|
||||
"return_statement",
|
||||
"template_substitution",
|
||||
# (expression_statement (call_expression))
|
||||
"export_clause",
|
||||
|
||||
# typescript
|
||||
"enum_declaration",
|
||||
"interface_declaration",
|
||||
"object_type",
|
||||
]
|
||||
|
||||
outdent = [
|
||||
"}",
|
||||
"]",
|
||||
")"
|
||||
]
|
87
runtime/queries/nix/highlights.scm
Normal file
87
runtime/queries/nix/highlights.scm
Normal file
@@ -0,0 +1,87 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"let"
|
||||
"inherit"
|
||||
"in"
|
||||
"rec"
|
||||
"with"
|
||||
"assert"
|
||||
] @keyword
|
||||
|
||||
((identifier) @variable.builtin
|
||||
(#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$")
|
||||
(#is-not? local))
|
||||
|
||||
((identifier) @function.builtin
|
||||
(#match? @function.builtin "^(__add|__addErrorContext|__all|__any|__appendContext|__attrNames|__attrValues|__bitAnd|__bitOr|__bitXor|__catAttrs|__compareVersions|__concatLists|__concatMap|__concatStringsSep|__deepSeq|__div|__elem|__elemAt|__fetchurl|__filter|__filterSource|__findFile|__foldl'|__fromJSON|__functionArgs|__genList|__genericClosure|__getAttr|__getContext|__getEnv|__hasAttr|__hasContext|__hashFile|__hashString|__head|__intersectAttrs|__isAttrs|__isBool|__isFloat|__isFunction|__isInt|__isList|__isPath|__isString|__langVersion|__length|__lessThan|__listToAttrs|__mapAttrs|__match|__mul|__parseDrvName|__partition|__path|__pathExists|__readDir|__readFile|__replaceStrings|__seq|__sort|__split|__splitVersion|__storePath|__stringLength|__sub|__substring|__tail|__toFile|__toJSON|__toPath|__toXML|__trace|__tryEval|__typeOf|__unsafeDiscardOutputDependency|__unsafeDiscardStringContext|__unsafeGetAttrPos|__valueSize|abort|baseNameOf|derivation|derivationStrict|dirOf|fetchGit|fetchMercurial|fetchTarball|fromTOML|import|isNull|map|placeholder|removeAttrs|scopedImport|throw|toString)$")
|
||||
(#is-not? local))
|
||||
|
||||
[
|
||||
(string)
|
||||
(indented_string)
|
||||
] @string
|
||||
|
||||
[
|
||||
(path)
|
||||
(hpath)
|
||||
(spath)
|
||||
] @string.special.path
|
||||
|
||||
(uri) @string.special.uri
|
||||
|
||||
[
|
||||
(integer)
|
||||
(float)
|
||||
] @number
|
||||
|
||||
(interpolation
|
||||
"${" @punctuation.special
|
||||
"}" @punctuation.special) @embedded
|
||||
|
||||
(escape_sequence) @escape
|
||||
|
||||
(function
|
||||
universal: (identifier) @variable.parameter
|
||||
)
|
||||
|
||||
(formal
|
||||
name: (identifier) @variable.parameter
|
||||
"?"? @punctuation.delimiter)
|
||||
|
||||
(app
|
||||
function: [
|
||||
(identifier) @function
|
||||
(select
|
||||
attrpath: (attrpath
|
||||
attr: (attr_identifier) @function .))])
|
||||
|
||||
|
||||
(unary
|
||||
operator: _ @operator)
|
||||
|
||||
(binary
|
||||
operator: _ @operator)
|
||||
|
||||
(attr_identifier) @property
|
||||
(inherit attrs: (attrs_inherited (identifier) @property) )
|
||||
|
||||
[
|
||||
";"
|
||||
"."
|
||||
","
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
(identifier) @variable
|
9
runtime/queries/nix/indents.toml
Normal file
9
runtime/queries/nix/indents.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
indent = [
|
||||
"if",
|
||||
"let",
|
||||
"function",
|
||||
"attrset",
|
||||
"list",
|
||||
"indented_string",
|
||||
"parenthesized"
|
||||
]
|
@@ -34,6 +34,9 @@
|
||||
; Namespaces
|
||||
|
||||
(crate) @namespace
|
||||
(extern_crate_declaration
|
||||
(crate)
|
||||
name: (identifier) @namespace)
|
||||
(scoped_use_list
|
||||
path: (identifier) @namespace)
|
||||
(scoped_use_list
|
||||
@@ -62,11 +65,9 @@
|
||||
function: (field_expression
|
||||
field: (field_identifier) @function.method))
|
||||
|
||||
; (macro_invocation
|
||||
; macro: (identifier) @function.macro
|
||||
; "!" @function.macro)
|
||||
(macro_invocation
|
||||
macro: (identifier) @function.macro)
|
||||
macro: (identifier) @function.macro
|
||||
"!" @function.macro)
|
||||
(macro_invocation
|
||||
macro: (scoped_identifier
|
||||
(identifier) @function.macro .))
|
||||
@@ -111,6 +112,7 @@
|
||||
|
||||
(lifetime (identifier) @label)
|
||||
|
||||
"async" @keyword
|
||||
"break" @keyword
|
||||
"const" @keyword
|
||||
"continue" @keyword
|
||||
@@ -144,7 +146,7 @@
|
||||
"use" @keyword
|
||||
"where" @keyword
|
||||
"while" @keyword
|
||||
(mutable_specifier) @keyword
|
||||
(mutable_specifier) @keyword.mut
|
||||
(use_list (self) @keyword)
|
||||
(scoped_use_list (self) @keyword)
|
||||
(scoped_identifier (self) @keyword)
|
||||
|
@@ -19,7 +19,8 @@ indent = [
|
||||
"enum_variant_list",
|
||||
"binary_expression",
|
||||
"field_expression",
|
||||
"where_clause"
|
||||
"where_clause",
|
||||
"macro_invocation"
|
||||
]
|
||||
|
||||
outdent = [
|
||||
|
36
runtime/queries/typescript/highlights.scm
Normal file
36
runtime/queries/typescript/highlights.scm
Normal file
@@ -0,0 +1,36 @@
|
||||
; inherits: javascript
|
||||
|
||||
; Types
|
||||
|
||||
(type_identifier) @type
|
||||
(predefined_type) @type.builtin
|
||||
|
||||
((identifier) @type
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
(type_arguments
|
||||
"<" @punctuation.bracket
|
||||
">" @punctuation.bracket)
|
||||
|
||||
; Variables
|
||||
|
||||
(required_parameter (identifier) @variable.parameter)
|
||||
(optional_parameter (identifier) @variable.parameter)
|
||||
|
||||
; Keywords
|
||||
|
||||
[
|
||||
"abstract"
|
||||
"declare"
|
||||
"enum"
|
||||
"export"
|
||||
"implements"
|
||||
"interface"
|
||||
"keyof"
|
||||
"namespace"
|
||||
"private"
|
||||
"protected"
|
||||
"public"
|
||||
"type"
|
||||
"readonly"
|
||||
] @keyword
|
1
runtime/queries/typescript/indents.toml
Symbolic link
1
runtime/queries/typescript/indents.toml
Symbolic link
@@ -0,0 +1 @@
|
||||
../javascript/indents.toml
|
2
runtime/queries/typescript/locals.scm
Normal file
2
runtime/queries/typescript/locals.scm
Normal file
@@ -0,0 +1,2 @@
|
||||
(required_parameter (identifier) @local.definition)
|
||||
(optional_parameter (identifier) @local.definition)
|
23
runtime/queries/typescript/tags.scm
Normal file
23
runtime/queries/typescript/tags.scm
Normal file
@@ -0,0 +1,23 @@
|
||||
(function_signature
|
||||
name: (identifier) @name) @definition.function
|
||||
|
||||
(method_signature
|
||||
name: (property_identifier) @name) @definition.method
|
||||
|
||||
(abstract_method_signature
|
||||
name: (property_identifier) @name) @definition.method
|
||||
|
||||
(abstract_class_declaration
|
||||
name: (type_identifier) @name) @definition.class
|
||||
|
||||
(module
|
||||
name: (identifier) @name) @definition.module
|
||||
|
||||
(interface_declaration
|
||||
name: (type_identifier) @name) @definition.interface
|
||||
|
||||
(type_annotation
|
||||
(type_identifier) @name) @reference.type
|
||||
|
||||
(new_expression
|
||||
constructor: (identifier) @name) @reference.class
|
23
shell.nix
23
shell.nix
@@ -1,17 +1,6 @@
|
||||
{ stdenv, pkgs }:
|
||||
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
(rust-bin.stable.latest.default.override { extensions = ["rust-src"]; })
|
||||
lld_10
|
||||
stdenv.cc.cc.lib
|
||||
# pkg-config
|
||||
];
|
||||
RUSTFLAGS = "-C link-arg=-fuse-ld=lld -C target-cpu=native";
|
||||
RUST_BACKTRACE = "1";
|
||||
# https://github.com/rust-lang/rust/issues/55979
|
||||
LD_LIBRARY_PATH="${stdenv.cc.cc.lib}/lib64:$LD_LIBRARY_PATH";
|
||||
|
||||
HELIX_RUNTIME=./runtime;
|
||||
}
|
||||
|
||||
# Flake's devShell for non-flake-enabled nix instances
|
||||
let
|
||||
src = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.flakeCompat.locked;
|
||||
compat = fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/${src.rev}.tar.gz"; sha256 = src.narHash; };
|
||||
in
|
||||
(import compat { src = ./.; }).shellNix.default
|
||||
|
@@ -43,10 +43,16 @@
|
||||
"ui.statusline" = { bg = "#281733" } # revolver
|
||||
"ui.popup" = { bg = "#281733" } # revolver
|
||||
"ui.window" = { bg = "#452859" } # bossa nova
|
||||
"ui.window" = { bg = "#452859" } # bossa nova
|
||||
"ui.help" = { bg = "#6F44F0", fg = "#a4a0e8" }
|
||||
"ui.help" = { bg = "#7958DC", fg = "#171452" }
|
||||
|
||||
"ui.text" = { fg = "#a4a0e8"} # lavender
|
||||
"ui.text" = { fg = "#a4a0e8" } # lavender
|
||||
"ui.text.focus" = { fg = "#dbbfef"} # lilac
|
||||
|
||||
"ui.selection" = { bg = "#540099" }
|
||||
"ui.menu.selected" = { fg = "#281733", bg = "#ffffff" } # revolver
|
||||
|
||||
"warning" = "#ffcd1c"
|
||||
"error" = "#f47868"
|
||||
"info" = "#6F44F0"
|
||||
|
Reference in New Issue
Block a user