mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
179 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
f1dc25a774 | ||
|
4f335fabc8 | ||
|
f366b97bce | ||
|
9c24f1ec0e | ||
|
f99a683991 | ||
|
9edae7e1f8 | ||
|
51d1d43289 | ||
|
5a245b83a0 | ||
|
2100f5a2c0 | ||
|
8f6f329057 | ||
|
8949347e2c | ||
|
54de768915 | ||
|
6e03019a2c | ||
|
31d41080ed | ||
|
5e6b46e7c5 | ||
|
354b822d21 | ||
|
fae2127a11 | ||
|
0e5b421646 | ||
|
4a9d1163e0 |
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
|
||||
|
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -82,3 +82,7 @@
|
||||
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
|
||||
|
154
Cargo.lock
generated
154
Cargo.lock
generated
@@ -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,21 +77,6 @@ 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"
|
||||
@@ -244,9 +229,9 @@ 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",
|
||||
@@ -272,25 +257,20 @@ 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.0.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"etcetera",
|
||||
"helix-syntax",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"ropey",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"tendril",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
@@ -298,7 +278,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-lsp"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-executor",
|
||||
@@ -320,7 +300,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-syntax"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"serde",
|
||||
@@ -330,11 +310,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-term"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"dirs-next",
|
||||
"fern",
|
||||
@@ -357,7 +336,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-tui"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
@@ -369,13 +348,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-view"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossterm",
|
||||
"helix-core",
|
||||
"helix-lsp",
|
||||
"helix-tui",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"slotmap",
|
||||
@@ -422,16 +402,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"
|
||||
@@ -476,9 +446,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"
|
||||
@@ -500,9 +470,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.89.0"
|
||||
version = "0.89.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07731ecd4ee0654728359a5b95e2a254c857876c04b85225496a35d60345daa7"
|
||||
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"serde",
|
||||
@@ -601,12 +571,6 @@ version = "1.7.2"
|
||||
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"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
@@ -658,9 +622,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",
|
||||
]
|
||||
@@ -730,6 +694,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"
|
||||
@@ -753,18 +750,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.125"
|
||||
version = "1.0.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
||||
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.125"
|
||||
version = "1.0.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
||||
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -856,29 +853,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",
|
||||
@@ -920,9 +908,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.5.0"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5"
|
||||
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -940,9 +928,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57"
|
||||
checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -951,9 +939,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0"
|
||||
checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -971,9 +959,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",
|
||||
@@ -1043,12 +1031,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"
|
||||
|
38
README.md
38
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 the Rust syntax has 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.
|
||||
@@ -41,6 +45,31 @@ 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.
|
||||
|
||||
> NOTE: You should set this to <path to repository>/runtime in development (if
|
||||
> running via cargo).
|
||||
>
|
||||
> `export HELIX_RUNTIME=$PWD/runtime`
|
||||
|
||||
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.**
|
||||
@@ -58,5 +87,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).
|
||||
|
8
TODO.md
8
TODO.md
@@ -1,8 +1,9 @@
|
||||
- Refactor tree-sitter-highlight to work like the atom one, recomputing partial tree updates.
|
||||
- syntax errors highlight query
|
||||
|
||||
------
|
||||
|
||||
as you type completion!
|
||||
|
||||
- tree sitter:
|
||||
- lua
|
||||
- markdown
|
||||
@@ -19,15 +20,14 @@
|
||||
- [ ] document.on_type provider triggers
|
||||
- [ ] completion isIncomplete support
|
||||
|
||||
- [ ] extract indentation calculation queries so we can support other languages.
|
||||
- [ ] scroll wheel support
|
||||
- [ ] matching bracket highlight
|
||||
|
||||
1
|
||||
- [ ] :format/:fmt that formats the buffer
|
||||
- [ ] respect view fullscreen flag
|
||||
- [ ] Implement marks (superset of Selection/Range)
|
||||
|
||||
- [ ] nixos packaging
|
||||
- [ ] CI binary builds
|
||||
|
||||
- [ ] = for auto indent line/selection
|
||||
- [ ] :x for closing buffers
|
||||
|
@@ -4,3 +4,6 @@ language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
theme = "colibri"
|
||||
|
||||
[output.html]
|
||||
cname = "docs.helix-editor.com"
|
||||
|
@@ -1 +1,87 @@
|
||||
# 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 | |
|
||||
| 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 |
|
||||
| 0 | move to the start of the line |
|
||||
| $ | move to the end of the line |
|
||||
| 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 |
|
||||
|
||||
@@ -86,6 +86,18 @@ in reverse, or searching via smartcase.
|
||||
| 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
|
||||
|
||||
I'm still pondering whether to keep this mode or not. It changes movement
|
||||
@@ -118,8 +130,13 @@ 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -127,6 +144,17 @@ Jumps to various locations.
|
||||
|
||||
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 +163,5 @@ 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 |
|
||||
| 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)!
|
46
contrib/themes/ingrid.toml
Normal file
46
contrib/themes/ingrid.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
# 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.menu.selected" = { fg = "#D74E50", bg = "#F3EAE9" }
|
||||
|
||||
"warning" = "#D4A520"
|
||||
"error" = "#D74E50"
|
||||
"info" = "#839A53"
|
||||
"hint" = "#A6B6CE"
|
@@ -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
|
||||
|
26
flake.lock
generated
26
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1619345332,
|
||||
"narHash": "sha256-qHnQkEp1uklKTpx3MvKtY6xzgcqXDsz5nLilbbuL+3A=",
|
||||
"lastModified": 1620759905,
|
||||
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "2ebf2558e5bf978c7fb8ea927dfaed8fefab2e28",
|
||||
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -50,10 +50,10 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1619775165,
|
||||
"narHash": "sha256-2qaBErjxuWpTIq6Yee5GJmhr84hmzBotLQ0ayg1VXg8=",
|
||||
"path": "/nix/store/gs997rgx3pvdgcb54wd3fi9wbnznd9g4-source",
|
||||
"rev": "849b29b4f76d66ec7aeeeed699b7e27ef3db7c02",
|
||||
"lastModified": 1622059058,
|
||||
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=",
|
||||
"path": "/nix/store/2gam4i1fa1v19k3n5rc9vgvqac1c2xj5-source",
|
||||
"rev": "84aa23742f6c72501f9cc209f29c438766f5352d",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
@@ -63,11 +63,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1620252427,
|
||||
"narHash": "sha256-U1Q5QceuT4chJTJ1UOt7bZOn9Y2o5/7w27RISjqXoQw=",
|
||||
"lastModified": 1622194753,
|
||||
"narHash": "sha256-76qtvFp/vFEz46lz5iZMJ0mnsWQYmuGYlb0fHgKqqMg=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d3ba49889a76539ea0f7d7285b203e7f81326ded",
|
||||
"rev": "540dccb2aeaffa9dc69bfdc41c55abd7ccc6baa3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -106,11 +106,11 @@
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1620355527,
|
||||
"narHash": "sha256-mUTnUODiAtxH83gbv7uuvCbqZ/BNkYYk/wa3MkwrskE=",
|
||||
"lastModified": 1622257069,
|
||||
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "d8efe70dc561c4bea0b7bf440d36ce98c497e054",
|
||||
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@@ -1,17 +1,20 @@
|
||||
[package]
|
||||
name = "helix-core"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
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"
|
||||
@@ -22,5 +25,7 @@ once_cell = "1.4"
|
||||
regex = "1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.5"
|
||||
|
||||
etcetera = "0.3"
|
||||
rust-embed = { version = "5.9.0", optional = true }
|
||||
|
@@ -65,14 +65,20 @@ fn handle_open(
|
||||
) -> Transaction {
|
||||
let mut ranges = SmallVec::with_capacity(selection.len());
|
||||
|
||||
let mut offs = 0;
|
||||
|
||||
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let pos = range.head;
|
||||
let next = next_char(doc, pos);
|
||||
|
||||
let head = pos + open.len_utf8();
|
||||
let head = pos + offs + open.len_utf8();
|
||||
// if selection, retain anchor, if cursor, move over
|
||||
ranges.push(Range::new(
|
||||
if range.is_empty() { head } else { range.anchor },
|
||||
if range.is_empty() {
|
||||
head
|
||||
} else {
|
||||
range.anchor + offs
|
||||
},
|
||||
head,
|
||||
));
|
||||
|
||||
@@ -88,6 +94,8 @@ fn handle_open(
|
||||
pair.push_char(open);
|
||||
pair.push_char(close);
|
||||
|
||||
offs += 2;
|
||||
|
||||
(pos, pos, Some(pair))
|
||||
}
|
||||
}
|
||||
@@ -99,14 +107,20 @@ fn handle_open(
|
||||
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
|
||||
let mut ranges = SmallVec::with_capacity(selection.len());
|
||||
|
||||
let mut offs = 0;
|
||||
|
||||
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let pos = range.head;
|
||||
let next = next_char(doc, pos);
|
||||
|
||||
let head = pos + close.len_utf8();
|
||||
let head = pos + offs + close.len_utf8();
|
||||
// if selection, retain anchor, if cursor, move over
|
||||
ranges.push(Range::new(
|
||||
if range.is_empty() { head } else { range.anchor },
|
||||
if range.is_empty() {
|
||||
head
|
||||
} else {
|
||||
range.anchor + offs
|
||||
},
|
||||
head,
|
||||
));
|
||||
|
||||
@@ -114,6 +128,8 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
|
||||
// return transaction that moves past close
|
||||
(pos, pos, None) // no-op
|
||||
} else {
|
||||
offs += close.len_utf8();
|
||||
|
||||
// TODO: else return (use default handler that inserts close)
|
||||
(pos, pos, Some(Tendril::from_char(close)))
|
||||
}
|
||||
|
@@ -65,9 +65,7 @@ impl History {
|
||||
self.cursor == 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;
|
||||
@@ -77,17 +75,17 @@ impl History {
|
||||
|
||||
self.cursor = current_revision.parent;
|
||||
|
||||
Some(current_revision.revert.clone())
|
||||
Some(¤t_revision.revert)
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<Transaction> {
|
||||
pub fn redo(&mut self) -> Option<&Transaction> {
|
||||
let current_revision = &self.revisions[self.cursor];
|
||||
|
||||
// for now, simply pick the latest child (linear undo / redo)
|
||||
if let Some((index, transaction)) = current_revision.children.last() {
|
||||
self.cursor = *index;
|
||||
|
||||
return Some(transaction.clone());
|
||||
return Some(&transaction);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
find_first_non_whitespace_char,
|
||||
syntax::Syntax,
|
||||
syntax::{IndentQuery, LanguageConfiguration, Syntax},
|
||||
tree_sitter::{Node, Tree},
|
||||
Rope, RopeSlice,
|
||||
};
|
||||
@@ -43,41 +43,12 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
|
||||
Some(node)
|
||||
}
|
||||
|
||||
fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
|
||||
fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
|
||||
// NOTE: can't use contains() on query because of comparing Vec<String> and &str
|
||||
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
|
||||
|
||||
let mut increment: i32 = 0;
|
||||
|
||||
// Hardcoded for rust for now
|
||||
let indent_scopes = &[
|
||||
"while_expression",
|
||||
"for_expression",
|
||||
"loop_expression",
|
||||
"if_expression",
|
||||
"if_let_expression",
|
||||
// "match_expression",
|
||||
// "match_arm",
|
||||
"tuple_expression",
|
||||
"array_expression",
|
||||
// indent_except_first_scopes
|
||||
"use_list",
|
||||
"block",
|
||||
"match_block",
|
||||
"arguments",
|
||||
"parameters",
|
||||
"declaration_list",
|
||||
"field_declaration_list",
|
||||
"field_initializer_list",
|
||||
"struct_pattern",
|
||||
"tuple_pattern",
|
||||
"enum_variant_list",
|
||||
// "function_item",
|
||||
// "closure_expression",
|
||||
"binary_expression",
|
||||
"field_expression",
|
||||
"where_clause",
|
||||
];
|
||||
|
||||
let outdent = &["where", "}", "]", ")"];
|
||||
|
||||
let mut node = match node {
|
||||
Some(node) => node,
|
||||
None => return 0,
|
||||
@@ -88,7 +59,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
|
||||
// if we're calculating indentation for a brand new line then the current node will become the
|
||||
// parent node. We need to take it's indentation level into account too.
|
||||
let node_kind = node.kind();
|
||||
if newline && indent_scopes.contains(&node_kind) {
|
||||
if newline && query.indent.contains(node_kind) {
|
||||
increment += 1;
|
||||
}
|
||||
|
||||
@@ -102,14 +73,14 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
|
||||
// }) <-- }) is two scopes
|
||||
let starts_same_line = start == prev_start;
|
||||
|
||||
if outdent.contains(&node.kind()) && !starts_same_line {
|
||||
if query.outdent.contains(node.kind()) && !starts_same_line {
|
||||
// we outdent by skipping the rules for the current level and jumping up
|
||||
// node = parent;
|
||||
increment -= 1;
|
||||
// continue;
|
||||
}
|
||||
|
||||
if indent_scopes.contains(&parent_kind) // && not_first_or_last_sibling
|
||||
if query.indent.contains(parent_kind) // && not_first_or_last_sibling
|
||||
&& !starts_same_line
|
||||
{
|
||||
// println!("is_scope {}", parent_kind);
|
||||
@@ -128,6 +99,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
|
||||
}
|
||||
|
||||
fn suggested_indent_for_line(
|
||||
language_config: &LanguageConfiguration,
|
||||
syntax: Option<&Syntax>,
|
||||
text: RopeSlice,
|
||||
line_num: usize,
|
||||
@@ -137,7 +109,7 @@ fn suggested_indent_for_line(
|
||||
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(syntax, text, start, false);
|
||||
return suggested_indent_for_pos(Some(language_config), syntax, text, start, false);
|
||||
};
|
||||
|
||||
// if the line is blank, indent should be zero
|
||||
@@ -148,18 +120,24 @@ fn suggested_indent_for_line(
|
||||
// - it should return 0 when mass indenting stuff
|
||||
// - it should look up the wrapper node and count it too when we press o/O
|
||||
pub fn suggested_indent_for_pos(
|
||||
language_config: Option<&LanguageConfiguration>,
|
||||
syntax: Option<&Syntax>,
|
||||
text: RopeSlice,
|
||||
pos: usize,
|
||||
new_line: bool,
|
||||
) -> usize {
|
||||
if let Some(syntax) = syntax {
|
||||
if let (Some(query), Some(syntax)) = (
|
||||
language_config.and_then(|config| config.indent_query()),
|
||||
syntax,
|
||||
) {
|
||||
let byte_start = text.char_to_byte(pos);
|
||||
let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
|
||||
|
||||
// let config = load indentation query config from Syntax(should contain language_config)
|
||||
|
||||
// TODO: special case for comments
|
||||
// TODO: if preserve_leading_whitespace
|
||||
calculate_indentation(node, new_line)
|
||||
calculate_indentation(query, node, new_line)
|
||||
} else {
|
||||
// TODO: heuristics for non-tree sitter grammars
|
||||
0
|
||||
@@ -273,21 +251,25 @@ 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(" "),
|
||||
}),
|
||||
}],
|
||||
});
|
||||
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(),
|
||||
}],
|
||||
},
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
// set runtime path so we can find the queries
|
||||
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
@@ -304,7 +286,7 @@ where
|
||||
let line = text.line(i);
|
||||
let indent = indent_level_for_line(line, tab_width);
|
||||
assert_eq!(
|
||||
suggested_indent_for_line(Some(&syntax), text, i, tab_width),
|
||||
suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
|
||||
indent,
|
||||
"line {}: {}",
|
||||
i,
|
||||
|
@@ -13,9 +13,10 @@ mod position;
|
||||
pub mod register;
|
||||
pub mod search;
|
||||
pub mod selection;
|
||||
pub mod state;
|
||||
mod state;
|
||||
pub mod syntax;
|
||||
mod transaction;
|
||||
pub mod words;
|
||||
|
||||
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
|
||||
// find first non-whitespace char
|
||||
@@ -44,6 +45,7 @@ pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(embed_runtime))]
|
||||
pub fn runtime_dir() -> std::path::PathBuf {
|
||||
// runtime env var || dir where binary is located
|
||||
std::env::var("HELIX_RUNTIME")
|
||||
@@ -58,13 +60,22 @@ pub fn runtime_dir() -> std::path::PathBuf {
|
||||
|
||||
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;
|
||||
|
@@ -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
|
||||
|
@@ -25,8 +25,7 @@ pub fn move_horizontally(
|
||||
}
|
||||
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);
|
||||
let end = text.line_to_char(line + 1).saturating_sub(1);
|
||||
nth_next_grapheme_boundary(text, pos, count).min(end)
|
||||
}
|
||||
};
|
||||
@@ -46,7 +45,10 @@ pub fn move_vertically(
|
||||
|
||||
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),
|
||||
text.len_lines().saturating_sub(2),
|
||||
),
|
||||
};
|
||||
|
||||
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
|
||||
@@ -77,8 +79,9 @@ pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) ->
|
||||
begin += 1;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_next(slice, &mut begin, |ch| ch == '\n');
|
||||
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
|
||||
return None;
|
||||
};
|
||||
ch = slice.char(begin);
|
||||
|
||||
end = begin + 1;
|
||||
@@ -135,7 +138,7 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
|
||||
let mut end = begin;
|
||||
|
||||
for _ in 0..count {
|
||||
if begin + 1 == slice.len_chars() {
|
||||
if begin + 2 >= slice.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -146,8 +149,9 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
|
||||
begin += 1;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_next(slice, &mut begin, |ch| ch == '\n');
|
||||
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
|
||||
return None;
|
||||
};
|
||||
|
||||
end = begin;
|
||||
|
||||
@@ -170,48 +174,52 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
|
||||
|
||||
// used for by-word movement
|
||||
|
||||
fn is_word(ch: char) -> bool {
|
||||
pub(crate) fn is_word(ch: char) -> bool {
|
||||
ch.is_alphanumeric() || ch == '_'
|
||||
}
|
||||
|
||||
fn is_horiz_blank(ch: char) -> bool {
|
||||
pub(crate) fn is_horiz_blank(ch: char) -> bool {
|
||||
matches!(ch, ' ' | '\t')
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum Category {
|
||||
pub(crate) enum Category {
|
||||
Whitespace,
|
||||
Eol,
|
||||
Word,
|
||||
Punctuation,
|
||||
Unknown,
|
||||
}
|
||||
fn categorize(ch: char) -> Category {
|
||||
|
||||
pub(crate) fn categorize(ch: char) -> Category {
|
||||
if ch == '\n' {
|
||||
Category::Eol
|
||||
} else if ch.is_ascii_whitespace() {
|
||||
Category::Whitespace
|
||||
} else if is_word(ch) {
|
||||
Category::Word
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
Category::Punctuation
|
||||
} else if ch.is_ascii_alphanumeric() {
|
||||
Category::Word
|
||||
} else {
|
||||
unreachable!()
|
||||
Category::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
|
||||
/// Returns true if there are more characters left after the new position.
|
||||
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
let mut chars = slice.chars_at(*pos);
|
||||
|
||||
for ch in chars {
|
||||
while let Some(ch) = chars.next() {
|
||||
if !fun(ch) {
|
||||
break;
|
||||
}
|
||||
*pos += 1;
|
||||
}
|
||||
chars.next().is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@@ -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)
|
||||
|
@@ -383,7 +383,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());
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ pub use helix_syntax::{get_language, get_language_name, Lang};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -41,6 +41,9 @@ pub struct LanguageConfiguration {
|
||||
pub language_server: Option<LanguageServerConfiguration>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indent: Option<IndentationConfiguration>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -59,16 +62,57 @@ pub struct IndentationConfiguration {
|
||||
pub unit: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IndentQuery {
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
pub indent: HashSet<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
pub outdent: HashSet<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "embed_runtime"))]
|
||||
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||
let root = crate::runtime_dir();
|
||||
let path = root.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
|
||||
@@ -127,6 +171,17 @@ impl LanguageConfiguration {
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn indent_query(&self) -> Option<&IndentQuery> {
|
||||
self.indent_query
|
||||
.get_or_init(|| {
|
||||
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
||||
|
||||
let toml = load_runtime_file(&language, "indents.toml").ok()?;
|
||||
toml::from_slice(&toml.as_bytes()).ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> &str {
|
||||
&self.scope
|
||||
}
|
||||
@@ -138,13 +193,15 @@ 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 {
|
||||
@@ -164,6 +221,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.
|
||||
@@ -717,7 +778,7 @@ struct LocalScope<'a> {
|
||||
local_defs: Vec<LocalDef<'a>>,
|
||||
}
|
||||
|
||||
struct HighlightIter<'a, F>
|
||||
struct HighlightIter<'a, 'tree: 'a, F>
|
||||
where
|
||||
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
|
||||
{
|
||||
@@ -725,16 +786,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>>,
|
||||
@@ -901,7 +962,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
|
||||
@@ -1165,7 +1226,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,
|
||||
{
|
||||
@@ -1216,7 +1277,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() {
|
||||
@@ -1235,7 +1296,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,
|
||||
{
|
||||
@@ -1672,3 +1733,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());
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
65
helix-core/src/words.rs
Normal file
65
helix-core/src/words.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::movement::{categorize, is_horiz_blank, is_word, skip_over_prev};
|
||||
use ropey::RopeSlice;
|
||||
|
||||
#[must_use]
|
||||
pub fn nth_prev_word_boundary(slice: RopeSlice, mut char_idx: usize, count: usize) -> usize {
|
||||
let mut with_end = false;
|
||||
|
||||
for _ in 0..count {
|
||||
if char_idx == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_prev(slice, &mut char_idx, |ch| ch == '\n');
|
||||
|
||||
with_end = skip_over_prev(slice, &mut char_idx, is_horiz_blank);
|
||||
|
||||
// refetch
|
||||
let ch = slice.char(char_idx);
|
||||
|
||||
if is_word(ch) {
|
||||
with_end = skip_over_prev(slice, &mut char_idx, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
with_end = skip_over_prev(slice, &mut char_idx, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
}
|
||||
|
||||
if with_end {
|
||||
char_idx
|
||||
} else {
|
||||
char_idx + 1
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_prev_word_boundary() {
|
||||
use ropey::Rope;
|
||||
let t = |x, y| {
|
||||
let text = Rope::from(x);
|
||||
let out = nth_prev_word_boundary(text.slice(..), text.len_chars() - 1, 1);
|
||||
assert_eq!(text.slice(..out), y, r#"from "{}""#, x);
|
||||
};
|
||||
t("abcd\nefg\nwrs", "abcd\nefg\n");
|
||||
t("abcd\nefg\n", "abcd\n");
|
||||
t("abcd\n", "");
|
||||
t("hello, world!", "hello, world");
|
||||
t("hello, world", "hello, ");
|
||||
t("hello, ", "hello");
|
||||
t("hello", "");
|
||||
t("こんにちは、世界!", "こんにちは、世界!"); // TODO: punctuation
|
||||
t("こんにちは、世界", "こんにちは、");
|
||||
t("こんにちは、", "こんにちは、"); // what?
|
||||
t("こんにちは", "");
|
||||
t("この世界。", "この世界。"); // what?
|
||||
t("この世界", "");
|
||||
t("お前はもう死んでいる", "");
|
||||
t("その300円です", ""); // TODO: should stop at 300
|
||||
t("唱k", ""); // TODO: should stop at 唱
|
||||
t("1 + 1 = 2", "1 + 1 = ");
|
||||
t("1 + 1 =", "1 + 1 ");
|
||||
t("1 + 1", "1 + ");
|
||||
t("1 + ", "1 ");
|
||||
t("1 ", "");
|
||||
t("1+1=2", "1+1=");
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-lsp"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
@@ -1,5 +1,4 @@
|
||||
mod client;
|
||||
mod select_all;
|
||||
mod transport;
|
||||
|
||||
pub use jsonrpc_core as jsonrpc;
|
||||
@@ -171,7 +170,7 @@ pub use jsonrpc::Call;
|
||||
|
||||
type LanguageId = String;
|
||||
|
||||
use crate::select_all::SelectAll;
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
|
||||
pub struct Registry {
|
||||
inner: HashMap<LanguageId, Option<Arc<Client>>>,
|
||||
@@ -198,7 +197,7 @@ impl Registry {
|
||||
if let Some(config) = &language_config.language_server {
|
||||
// avoid borrow issues
|
||||
let inner = &mut self.inner;
|
||||
let s_incoming = &self.incoming;
|
||||
let s_incoming = &mut 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
|
||||
|
@@ -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,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-syntax"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
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
@@ -72,6 +72,7 @@ mk_langs!(
|
||||
(CSharp, tree_sitter_c_sharp),
|
||||
(Cpp, tree_sitter_cpp),
|
||||
(Css, tree_sitter_css),
|
||||
(Elixir, tree_sitter_elixir),
|
||||
(Go, tree_sitter_go),
|
||||
// (Haskell, tree_sitter_haskell),
|
||||
(Html, tree_sitter_html),
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-term"
|
||||
version = "0.1.0"
|
||||
version = "0.0.10"
|
||||
description = "A post-modern text editor."
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
@@ -8,6 +8,9 @@ license = "MPL-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
embed_runtime = ["helix-core/embed_runtime"]
|
||||
|
||||
[[bin]]
|
||||
name = "hx"
|
||||
path = "src/main.rs"
|
||||
@@ -24,7 +27,6 @@ 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"] }
|
||||
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
|
||||
|
@@ -1,8 +1,6 @@
|
||||
use clap::ArgMatches as Args;
|
||||
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
|
||||
use crate::{compositor::Compositor, ui};
|
||||
use crate::{compositor::Compositor, ui, Args};
|
||||
|
||||
use log::{error, info};
|
||||
|
||||
@@ -47,16 +45,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,
|
||||
@@ -207,7 +217,7 @@ impl Application {
|
||||
})
|
||||
.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();
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ use helix_core::{
|
||||
movement::{self, Direction},
|
||||
object, pos_at_coords,
|
||||
regex::{self, Regex},
|
||||
register, search, selection, Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection,
|
||||
SmallVec, Tendril, Transaction,
|
||||
register, search, selection, words, Change, ChangeSet, Position, Range, Rope, RopeSlice,
|
||||
Selection, SmallVec, Tendril, Transaction,
|
||||
};
|
||||
|
||||
use helix_view::{
|
||||
@@ -37,7 +37,6 @@ use once_cell::sync::Lazy;
|
||||
pub struct Context<'a> {
|
||||
pub count: usize,
|
||||
pub editor: &'a mut Editor,
|
||||
pub view_id: ViewId,
|
||||
|
||||
pub callback: Option<crate::compositor::Callback>,
|
||||
pub on_next_key_callback: Option<Box<dyn FnOnce(&mut Context, KeyEvent)>>,
|
||||
@@ -187,37 +186,31 @@ pub fn move_line_down(cx: &mut Context) {
|
||||
|
||||
pub fn move_line_end(cx: &mut Context) {
|
||||
let (view, doc) = cx.current();
|
||||
let lines = selection_lines(doc.text(), doc.selection(view.id));
|
||||
|
||||
let positions = lines
|
||||
.into_iter()
|
||||
.map(|index| {
|
||||
// adjust all positions to the end of the line.
|
||||
let selection = doc.selection(view.id).transform(|range| {
|
||||
let text = doc.text();
|
||||
let line = text.char_to_line(range.head);
|
||||
|
||||
// Line end is pos at the start of next line - 1
|
||||
// subtract another 1 because the line ends with \n
|
||||
doc.text().line_to_char(index + 1).saturating_sub(2)
|
||||
})
|
||||
.map(|pos| Range::new(pos, pos));
|
||||
|
||||
let selection = Selection::new(positions.collect(), 0);
|
||||
// Line end is pos at the start of next line - 1
|
||||
// subtract another 1 because the line ends with \n
|
||||
let pos = text.line_to_char(line + 1).saturating_sub(2);
|
||||
Range::new(pos, pos)
|
||||
});
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
pub fn move_line_start(cx: &mut Context) {
|
||||
let (view, doc) = cx.current();
|
||||
let lines = selection_lines(doc.text(), doc.selection(view.id));
|
||||
|
||||
let positions = lines
|
||||
.into_iter()
|
||||
.map(|index| {
|
||||
// adjust all positions to the start of the line.
|
||||
doc.text().line_to_char(index)
|
||||
})
|
||||
.map(|pos| Range::new(pos, pos));
|
||||
let selection = doc.selection(view.id).transform(|range| {
|
||||
let text = doc.text();
|
||||
let line = text.char_to_line(range.head);
|
||||
|
||||
let selection = Selection::new(positions.collect(), 0);
|
||||
// adjust to start of the line
|
||||
let pos = text.line_to_char(line);
|
||||
Range::new(pos, pos)
|
||||
});
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
@@ -322,7 +315,7 @@ fn _find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
|
||||
where
|
||||
// TODO: make an options struct for and abstract this Fn into a searcher type
|
||||
// use the definition for w/b/e too
|
||||
F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize>,
|
||||
F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
|
||||
{
|
||||
// TODO: count is reset to 1 before next key so we move it into the closure here.
|
||||
// Would be nice to carry over.
|
||||
@@ -339,7 +332,7 @@ where
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
let selection = doc.selection(view.id).transform(|mut range| {
|
||||
search::find_nth_next(text, ch, range.head, count, inclusive).map_or(range, |pos| {
|
||||
search_fn(text, ch, range.head, count, inclusive).map_or(range, |pos| {
|
||||
if extend {
|
||||
Range::new(range.anchor, pos)
|
||||
} else {
|
||||
@@ -480,10 +473,10 @@ fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
|
||||
let last_line = view.last_line(doc);
|
||||
|
||||
// clamp into viewport
|
||||
let line = cursor.row.clamp(
|
||||
view.first_line + scrolloff,
|
||||
last_line.saturating_sub(scrolloff),
|
||||
);
|
||||
let line = cursor
|
||||
.row
|
||||
.max(view.first_line + scrolloff)
|
||||
.min(last_line.saturating_sub(scrolloff));
|
||||
|
||||
let text = doc.text().slice(..);
|
||||
let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
|
||||
@@ -580,6 +573,37 @@ pub fn extend_line_down(cx: &mut Context) {
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
pub fn extend_line_end(cx: &mut Context) {
|
||||
let (view, doc) = cx.current();
|
||||
|
||||
let selection = doc.selection(view.id).transform(|range| {
|
||||
let text = doc.text();
|
||||
let line = text.char_to_line(range.head);
|
||||
|
||||
// Line end is pos at the start of next line - 1
|
||||
// subtract another 1 because the line ends with \n
|
||||
let pos = text.line_to_char(line + 1).saturating_sub(2);
|
||||
Range::new(range.anchor, pos)
|
||||
});
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
pub fn extend_line_start(cx: &mut Context) {
|
||||
let (view, doc) = cx.current();
|
||||
|
||||
let selection = doc.selection(view.id).transform(|range| {
|
||||
let text = doc.text();
|
||||
let line = text.char_to_line(range.head);
|
||||
|
||||
// adjust to start of the line
|
||||
let pos = text.line_to_char(line);
|
||||
Range::new(range.anchor, pos)
|
||||
});
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
pub fn select_all(cx: &mut Context) {
|
||||
let (view, doc) = cx.current();
|
||||
|
||||
@@ -640,7 +664,12 @@ fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, e
|
||||
let start = text.byte_to_char(mat.start());
|
||||
let end = text.byte_to_char(mat.end());
|
||||
|
||||
let head = end - 1;
|
||||
if end == 0 {
|
||||
// skip empty matches that don't make sense
|
||||
return;
|
||||
}
|
||||
|
||||
let head = end;
|
||||
|
||||
let selection = if extend {
|
||||
selection.clone().push(Range::new(start, head))
|
||||
@@ -656,7 +685,7 @@ fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, e
|
||||
|
||||
// TODO: use one function for search vs extend
|
||||
pub fn search(cx: &mut Context) {
|
||||
let doc = cx.doc();
|
||||
let (view, doc) = cx.current();
|
||||
|
||||
// TODO: could probably share with select_on_matches?
|
||||
|
||||
@@ -664,7 +693,7 @@ pub fn search(cx: &mut Context) {
|
||||
// feed chunks into the regex yet
|
||||
let contents = doc.text().slice(..).to_string();
|
||||
|
||||
let view_id = cx.view_id;
|
||||
let view_id = view.id;
|
||||
let prompt = ui::regex_prompt(cx, "search:".to_string(), move |view, doc, regex| {
|
||||
let text = doc.text();
|
||||
let start = doc.selection(view.id).cursor();
|
||||
@@ -676,6 +705,7 @@ pub fn search(cx: &mut Context) {
|
||||
|
||||
cx.push_layer(Box::new(prompt));
|
||||
}
|
||||
// can't search next for ""compose"" for some reason
|
||||
|
||||
pub fn _search_next(cx: &mut Context, extend: bool) {
|
||||
if let Some(query) = register::get('\\') {
|
||||
@@ -719,7 +749,9 @@ pub fn select_line(cx: &mut Context) {
|
||||
|
||||
let line = text.char_to_line(pos.head);
|
||||
let start = text.line_to_char(line);
|
||||
let end = text.line_to_char(line + count).saturating_sub(1);
|
||||
let end = text
|
||||
.line_to_char(std::cmp::min(doc.text().len_lines(), line + count))
|
||||
.saturating_sub(1);
|
||||
|
||||
doc.set_selection(view.id, Selection::single(start, end));
|
||||
}
|
||||
@@ -760,7 +792,9 @@ fn _delete_selection(doc: &mut Document, view_id: ViewId) {
|
||||
// then delete
|
||||
let transaction =
|
||||
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
|
||||
(range.from(), range.to() + 1, None)
|
||||
let max_to = doc.text().len_chars().saturating_sub(1);
|
||||
let to = std::cmp::min(max_to, range.to() + 1);
|
||||
(range.from(), to, None)
|
||||
});
|
||||
doc.apply(&transaction, view_id);
|
||||
}
|
||||
@@ -770,6 +804,9 @@ pub fn delete_selection(cx: &mut Context) {
|
||||
_delete_selection(doc, view.id);
|
||||
|
||||
doc.append_changes_to_history(view.id);
|
||||
|
||||
// exit select mode, if currently in select mode
|
||||
exit_select_mode(cx);
|
||||
}
|
||||
|
||||
pub fn change_selection(cx: &mut Context) {
|
||||
@@ -824,6 +861,17 @@ pub fn append_mode(cx: &mut Context) {
|
||||
graphemes::next_grapheme_boundary(text, range.to()), // to() + next char
|
||||
)
|
||||
});
|
||||
|
||||
let end = text.len_chars();
|
||||
|
||||
if selection.iter().any(|range| range.head == end) {
|
||||
let transaction = Transaction::change(
|
||||
doc.text(),
|
||||
std::array::IntoIter::new([(end, end, Some(Tendril::from_char('\n')))]),
|
||||
);
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
@@ -875,17 +923,30 @@ mod cmd {
|
||||
}
|
||||
|
||||
fn open(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||
let path = args[0];
|
||||
editor.open(path.into(), Action::Replace);
|
||||
match args.get(0) {
|
||||
Some(path) => {
|
||||
// TODO: handle error
|
||||
editor.open(path.into(), Action::Replace);
|
||||
}
|
||||
None => {
|
||||
editor.set_error("wrong argument count".to_string());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn write(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||
let id = editor.view().doc;
|
||||
let doc = &mut editor.documents[id];
|
||||
let (view, doc) = editor.current();
|
||||
if let Some(path) = args.get(0) {
|
||||
if let Err(err) = doc.set_path(Path::new(path)) {
|
||||
editor.set_error(format!("invalid filepath: {}", err));
|
||||
return;
|
||||
};
|
||||
}
|
||||
if doc.path().is_none() {
|
||||
editor.set_error("cannot write a buffer without a filename".to_string());
|
||||
return;
|
||||
}
|
||||
doc.format(view.id); // TODO: merge into save
|
||||
tokio::spawn(doc.save());
|
||||
}
|
||||
|
||||
@@ -896,24 +957,7 @@ mod cmd {
|
||||
fn format(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
if let Some(language_server) = doc.language_server() {
|
||||
// TODO: await, no blocking
|
||||
let transaction = helix_lsp::block_on(
|
||||
language_server
|
||||
.text_document_formatting(doc.identifier(), lsp::FormattingOptions::default()),
|
||||
)
|
||||
.map(|edits| {
|
||||
helix_lsp::util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
edits,
|
||||
language_server.offset_encoding(),
|
||||
)
|
||||
});
|
||||
|
||||
if let Ok(transaction) = transaction {
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
}
|
||||
doc.format(view.id)
|
||||
}
|
||||
|
||||
pub const COMMAND_LIST: &[Command] = &[
|
||||
@@ -941,7 +985,7 @@ mod cmd {
|
||||
Command {
|
||||
name: "write",
|
||||
alias: Some("w"),
|
||||
doc: "Write changes to disk.",
|
||||
doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
|
||||
fun: write,
|
||||
completer: Some(completers::filename),
|
||||
},
|
||||
@@ -1025,6 +1069,9 @@ pub fn command_mode(cx: &mut Context) {
|
||||
}
|
||||
|
||||
let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
|
||||
if parts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
|
||||
(cmd.fun)(editor, &parts[1..], event);
|
||||
@@ -1107,19 +1154,6 @@ pub fn buffer_picker(cx: &mut Context) {
|
||||
cx.push_layer(Box::new(picker));
|
||||
}
|
||||
|
||||
// calculate line numbers for each selection range
|
||||
fn selection_lines(doc: &Rope, selection: &Selection) -> Vec<usize> {
|
||||
let mut lines = selection
|
||||
.iter()
|
||||
.map(|range| doc.char_to_line(range.head))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
lines.sort_unstable(); // sorting by usize so _unstable is preferred
|
||||
lines.dedup();
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
// I inserts at the start of each line with a selection
|
||||
pub fn prepend_to_line(cx: &mut Context) {
|
||||
move_line_start(cx);
|
||||
@@ -1129,15 +1163,14 @@ pub fn prepend_to_line(cx: &mut Context) {
|
||||
|
||||
// A inserts at the end of each line with a selection
|
||||
pub fn append_to_line(cx: &mut Context) {
|
||||
move_line_end(cx);
|
||||
|
||||
let (view, doc) = cx.current();
|
||||
enter_insert_mode(doc);
|
||||
|
||||
// offset by another 1 char since move_line_end will position on the last char, we want to
|
||||
// append past that
|
||||
let selection = doc.selection(view.id).transform(|range| {
|
||||
let pos = range.head + 1;
|
||||
let text = doc.text();
|
||||
let line = text.char_to_line(range.head);
|
||||
// we can't use line_to_char(line + 1) - 2 because the last line might not contain \n
|
||||
let pos = (text.line_to_char(line) + text.line(line).len_chars()).saturating_sub(1);
|
||||
Range::new(pos, pos)
|
||||
});
|
||||
doc.set_selection(view.id, selection);
|
||||
@@ -1154,13 +1187,15 @@ fn open(cx: &mut Context, open: Open) {
|
||||
enter_insert_mode(doc);
|
||||
|
||||
let text = doc.text().slice(..);
|
||||
let lines = selection_lines(doc.text(), doc.selection(view.id));
|
||||
let selection = doc.selection(view.id);
|
||||
|
||||
let mut ranges = SmallVec::with_capacity(lines.len());
|
||||
let mut ranges = SmallVec::with_capacity(selection.len());
|
||||
|
||||
let changes: Vec<Change> = selection
|
||||
.iter()
|
||||
.map(|range| {
|
||||
let line = text.char_to_line(range.head);
|
||||
|
||||
let changes: Vec<Change> = lines
|
||||
.into_iter()
|
||||
.map(|line| {
|
||||
let line = match open {
|
||||
// adjust position to the end of the line (next line - 1)
|
||||
Open::Below => line + 1,
|
||||
@@ -1171,7 +1206,13 @@ fn open(cx: &mut Context, open: Open) {
|
||||
let index = doc.text().line_to_char(line).saturating_sub(1);
|
||||
|
||||
// TODO: share logic with insert_newline for indentation
|
||||
let indent_level = indent::suggested_indent_for_pos(doc.syntax(), text, index, true);
|
||||
let indent_level = indent::suggested_indent_for_pos(
|
||||
doc.language_config(),
|
||||
doc.syntax(),
|
||||
text,
|
||||
index,
|
||||
true,
|
||||
);
|
||||
let indent = doc.indent_unit().repeat(indent_level);
|
||||
let mut text = String::with_capacity(1 + indent.len());
|
||||
text.push('\n');
|
||||
@@ -1179,7 +1220,7 @@ fn open(cx: &mut Context, open: Open) {
|
||||
let text = text.repeat(count);
|
||||
|
||||
// calculate new selection range
|
||||
let pos = index + text.len();
|
||||
let pos = index + text.chars().count();
|
||||
ranges.push(Range::new(pos, pos));
|
||||
|
||||
(index, index, Some(text.into()))
|
||||
@@ -1245,7 +1286,8 @@ pub fn goto_mode(cx: &mut Context) {
|
||||
// TODO: can't go to line 1 since we can't distinguish between g and 1g, g gets converted
|
||||
// to 1g
|
||||
let (view, doc) = cx.current();
|
||||
let pos = doc.text().line_to_char(count - 1);
|
||||
let line_idx = std::cmp::min(count - 1, doc.text().len_lines().saturating_sub(2));
|
||||
let pos = doc.text().line_to_char(line_idx);
|
||||
doc.set_selection(view.id, Selection::point(pos));
|
||||
return;
|
||||
}
|
||||
@@ -1260,10 +1302,35 @@ pub fn goto_mode(cx: &mut Context) {
|
||||
match ch {
|
||||
'g' => move_file_start(cx),
|
||||
'e' => move_file_end(cx),
|
||||
'h' => move_line_start(cx),
|
||||
'l' => move_line_end(cx),
|
||||
'd' => goto_definition(cx),
|
||||
't' => goto_type_definition(cx),
|
||||
'y' => goto_type_definition(cx),
|
||||
'r' => goto_reference(cx),
|
||||
'i' => goto_implementation(cx),
|
||||
|
||||
't' | 'm' | 'b' => {
|
||||
let (view, doc) = cx.current();
|
||||
|
||||
let pos = doc.selection(view.id).cursor();
|
||||
let line = doc.text().char_to_line(pos);
|
||||
|
||||
let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref
|
||||
|
||||
let last_line = view.last_line(doc);
|
||||
|
||||
let line = match ch {
|
||||
't' => (view.first_line + scrolloff),
|
||||
'm' => (view.first_line + (view.area.height as usize / 2)),
|
||||
'b' => last_line.saturating_sub(scrolloff),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.min(last_line.saturating_sub(scrolloff));
|
||||
|
||||
let pos = doc.text().line_to_char(line);
|
||||
|
||||
doc.set_selection(view.id, Selection::point(pos));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -1469,6 +1536,86 @@ pub fn goto_reference(cx: &mut Context) {
|
||||
);
|
||||
}
|
||||
|
||||
fn goto_pos(editor: &mut Editor, pos: usize) {
|
||||
push_jump(editor);
|
||||
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
doc.set_selection(view.id, Selection::point(pos));
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
|
||||
pub fn goto_first_diag(cx: &mut Context) {
|
||||
let editor = &mut cx.editor;
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
let cursor_pos = doc.selection(view.id).cursor();
|
||||
let diag = if let Some(diag) = doc.diagnostics().first() {
|
||||
diag.range.start
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
goto_pos(editor, diag);
|
||||
}
|
||||
|
||||
pub fn goto_last_diag(cx: &mut Context) {
|
||||
let editor = &mut cx.editor;
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
let cursor_pos = doc.selection(view.id).cursor();
|
||||
let diag = if let Some(diag) = doc.diagnostics().last() {
|
||||
diag.range.start
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
goto_pos(editor, diag);
|
||||
}
|
||||
|
||||
pub fn goto_next_diag(cx: &mut Context) {
|
||||
let editor = &mut cx.editor;
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
let cursor_pos = doc.selection(view.id).cursor();
|
||||
let diag = if let Some(diag) = doc
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.map(|diag| diag.range.start)
|
||||
.find(|&pos| pos > cursor_pos)
|
||||
{
|
||||
diag
|
||||
} else if let Some(diag) = doc.diagnostics().first() {
|
||||
diag.range.start
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
goto_pos(editor, diag);
|
||||
}
|
||||
|
||||
pub fn goto_prev_diag(cx: &mut Context) {
|
||||
let editor = &mut cx.editor;
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
let cursor_pos = doc.selection(view.id).cursor();
|
||||
let diag = if let Some(diag) = doc
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|diag| diag.range.start)
|
||||
.find(|&pos| pos < cursor_pos)
|
||||
{
|
||||
diag
|
||||
} else if let Some(diag) = doc.diagnostics().last() {
|
||||
diag.range.start
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
goto_pos(editor, diag);
|
||||
}
|
||||
|
||||
pub fn signature_help(cx: &mut Context) {
|
||||
let (view, doc) = cx.current();
|
||||
|
||||
@@ -1638,6 +1785,9 @@ pub mod insert {
|
||||
let selection = doc.selection(view.id);
|
||||
let mut ranges = SmallVec::with_capacity(selection.len());
|
||||
|
||||
// TODO: this is annoying, but we need to do it to properly calculate pos after edits
|
||||
let mut offs = 0;
|
||||
|
||||
let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
|
||||
let pos = range.head;
|
||||
|
||||
@@ -1649,17 +1799,29 @@ pub mod insert {
|
||||
let curr = contents.char(pos);
|
||||
|
||||
// TODO: offset range.head by 1? when calculating?
|
||||
let indent_level =
|
||||
indent::suggested_indent_for_pos(doc.syntax(), text, pos.saturating_sub(1), true);
|
||||
let indent_level = indent::suggested_indent_for_pos(
|
||||
doc.language_config(),
|
||||
doc.syntax(),
|
||||
text,
|
||||
pos.saturating_sub(1),
|
||||
true,
|
||||
);
|
||||
let indent = doc.indent_unit().repeat(indent_level);
|
||||
let mut text = String::with_capacity(1 + indent.len());
|
||||
text.push('\n');
|
||||
text.push_str(&indent);
|
||||
|
||||
let head = pos + text.len();
|
||||
let head = pos + offs + text.chars().count();
|
||||
|
||||
// TODO: range replace or extend
|
||||
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
|
||||
// can be used with cx.mode to do replace or extend on most changes
|
||||
ranges.push(Range::new(
|
||||
if range.is_empty() { head } else { range.anchor },
|
||||
if range.is_empty() {
|
||||
head
|
||||
} else {
|
||||
range.anchor + offs
|
||||
},
|
||||
head,
|
||||
));
|
||||
|
||||
@@ -1669,11 +1831,11 @@ pub mod insert {
|
||||
let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
|
||||
text.push('\n');
|
||||
text.push_str(&indent);
|
||||
|
||||
(pos, pos, Some(text.into()))
|
||||
} else {
|
||||
(pos, pos, Some(text.into()))
|
||||
}
|
||||
|
||||
offs += text.chars().count();
|
||||
|
||||
(pos, pos, Some(text.into()))
|
||||
});
|
||||
|
||||
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
|
||||
@@ -1700,7 +1862,6 @@ pub mod insert {
|
||||
|
||||
pub fn delete_char_forward(cx: &mut Context) {
|
||||
let count = cx.count;
|
||||
let doc = cx.doc();
|
||||
let (view, doc) = cx.current();
|
||||
let text = doc.text().slice(..);
|
||||
let transaction =
|
||||
@@ -1713,6 +1874,21 @@ pub mod insert {
|
||||
});
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
|
||||
pub fn delete_word_backward(cx: &mut Context) {
|
||||
let count = cx.count;
|
||||
let (view, doc) = cx.current();
|
||||
let text = doc.text().slice(..);
|
||||
let transaction =
|
||||
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
|
||||
(
|
||||
words::nth_prev_word_boundary(text, range.head, count),
|
||||
range.head,
|
||||
None,
|
||||
)
|
||||
});
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Undo / Redo
|
||||
@@ -1721,12 +1897,12 @@ pub mod insert {
|
||||
// storing it?
|
||||
|
||||
pub fn undo(cx: &mut Context) {
|
||||
let view_id = cx.view_id;
|
||||
let view_id = cx.view().id;
|
||||
cx.doc().undo(view_id);
|
||||
}
|
||||
|
||||
pub fn redo(cx: &mut Context) {
|
||||
let view_id = cx.view_id;
|
||||
let view_id = cx.view().id;
|
||||
cx.doc().redo(view_id);
|
||||
}
|
||||
|
||||
@@ -1839,11 +2015,12 @@ fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
|
||||
}
|
||||
|
||||
pub fn indent(cx: &mut Context) {
|
||||
let count = cx.count;
|
||||
let (view, doc) = cx.current();
|
||||
let lines = get_lines(doc, view.id);
|
||||
|
||||
// Indent by one level
|
||||
let indent = Tendril::from(doc.indent_unit());
|
||||
let indent = Tendril::from(doc.indent_unit().repeat(count));
|
||||
|
||||
let transaction = Transaction::change(
|
||||
doc.text(),
|
||||
@@ -1857,14 +2034,17 @@ pub fn indent(cx: &mut Context) {
|
||||
}
|
||||
|
||||
pub fn unindent(cx: &mut Context) {
|
||||
let count = cx.count;
|
||||
let (view, doc) = cx.current();
|
||||
let lines = get_lines(doc, view.id);
|
||||
let mut changes = Vec::with_capacity(lines.len());
|
||||
let tab_width = doc.tab_width();
|
||||
let indent_width = count * tab_width;
|
||||
|
||||
for line_idx in lines {
|
||||
let line = doc.text().line(line_idx);
|
||||
let mut width = 0;
|
||||
let mut pos = 0;
|
||||
|
||||
for ch in line.chars() {
|
||||
match ch {
|
||||
@@ -1873,14 +2053,17 @@ pub fn unindent(cx: &mut Context) {
|
||||
_ => break,
|
||||
}
|
||||
|
||||
if width >= tab_width {
|
||||
pos += 1;
|
||||
|
||||
if width >= indent_width {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if width > 0 {
|
||||
// now delete from start to first non-blank
|
||||
if pos > 0 {
|
||||
let start = doc.text().line_to_char(line_idx);
|
||||
changes.push((start, start + width, None))
|
||||
changes.push((start, start + pos, None))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2151,11 +2334,6 @@ pub fn hover(cx: &mut Context) {
|
||||
);
|
||||
}
|
||||
|
||||
// view movements
|
||||
pub fn next_view(cx: &mut Context) {
|
||||
cx.editor.focus_next()
|
||||
}
|
||||
|
||||
// comments
|
||||
pub fn toggle_comments(cx: &mut Context) {
|
||||
let (view, doc) = cx.current();
|
||||
@@ -2219,16 +2397,38 @@ pub fn jump_backward(cx: &mut Context) {
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
pub fn window_mode(cx: &mut Context) {
|
||||
cx.on_next_key(move |cx, event| {
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char(ch),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match ch {
|
||||
'w' => rotate_view(cx),
|
||||
'h' => hsplit(cx),
|
||||
'v' => vsplit(cx),
|
||||
'q' => wclose(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn vsplit(cx: &mut Context) {
|
||||
pub fn rotate_view(cx: &mut Context) {
|
||||
cx.editor.focus_next()
|
||||
}
|
||||
|
||||
// split helper, clear it later
|
||||
use helix_view::editor::Action;
|
||||
fn split(cx: &mut Context, action: Action) {
|
||||
use helix_view::editor::Action;
|
||||
let (view, doc) = cx.current();
|
||||
let id = doc.id();
|
||||
let selection = doc.selection(view.id).clone();
|
||||
let first_line = view.first_line;
|
||||
|
||||
cx.editor.switch(id, Action::VerticalSplit);
|
||||
cx.editor.switch(id, action);
|
||||
|
||||
// match the selection in the previous view
|
||||
let (view, doc) = cx.current();
|
||||
@@ -2236,6 +2436,20 @@ pub fn vsplit(cx: &mut Context) {
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
pub fn hsplit(cx: &mut Context) {
|
||||
split(cx, Action::HorizontalSplit);
|
||||
}
|
||||
|
||||
pub fn vsplit(cx: &mut Context) {
|
||||
split(cx, Action::VerticalSplit);
|
||||
}
|
||||
|
||||
pub fn wclose(cx: &mut Context) {
|
||||
let view_id = cx.view().id;
|
||||
// close current split
|
||||
cx.editor.close(view_id, /* close_buffer */ false);
|
||||
}
|
||||
|
||||
pub fn space_mode(cx: &mut Context) {
|
||||
cx.on_next_key(move |cx, event| {
|
||||
if let KeyEvent {
|
||||
@@ -2247,16 +2461,7 @@ pub fn space_mode(cx: &mut Context) {
|
||||
match ch {
|
||||
'f' => file_picker(cx),
|
||||
'b' => buffer_picker(cx),
|
||||
'v' => vsplit(cx),
|
||||
'w' => {
|
||||
// save current buffer
|
||||
let doc = cx.doc();
|
||||
tokio::spawn(doc.save());
|
||||
}
|
||||
'c' => {
|
||||
// close current split
|
||||
cx.editor.close(cx.view_id, /* close_buffer */ false);
|
||||
}
|
||||
'w' => window_mode(cx),
|
||||
// ' ' => toggle_alternate_buffer(cx),
|
||||
// TODO: temporary since space mode took it's old key
|
||||
' ' => keep_primary_selection(cx),
|
||||
@@ -2297,7 +2502,7 @@ pub fn view_mode(cx: &mut Context) {
|
||||
let pos = coords_at_pos(doc.text().slice(..), pos);
|
||||
|
||||
const OFFSET: usize = 7; // gutters
|
||||
view.first_col = pos.col.saturating_sub((view.area.width as usize - OFFSET) / 2);
|
||||
view.first_col = pos.col.saturating_sub(((view.area.width as usize).saturating_sub(OFFSET)) / 2);
|
||||
},
|
||||
'h' => (),
|
||||
'j' => scroll(cx, 1, Direction::Forward),
|
||||
@@ -2308,3 +2513,35 @@ pub fn view_mode(cx: &mut Context) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn left_bracket_mode(cx: &mut Context) {
|
||||
cx.on_next_key(move |cx, event| {
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char(ch),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match ch {
|
||||
'd' => goto_prev_diag(cx),
|
||||
'D' => goto_first_diag(cx),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn right_bracket_mode(cx: &mut Context) {
|
||||
cx.on_next_key(move |cx, event| {
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char(ch),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match ch {
|
||||
'd' => goto_next_diag(cx),
|
||||
'D' => goto_last_diag(cx),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -35,10 +35,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 +61,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 +85,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")]
|
||||
@@ -103,15 +107,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 +132,40 @@ pub fn default() -> Keymaps {
|
||||
key!('k') => commands::move_line_up,
|
||||
key!('l') => commands::move_char_right,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_char_left,
|
||||
KeyEvent {
|
||||
code: KeyCode::Down,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_down,
|
||||
KeyEvent {
|
||||
code: KeyCode::Up,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_up,
|
||||
KeyEvent {
|
||||
code: KeyCode::Right,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => 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!('0') => commands::move_line_start,
|
||||
key!('$') => commands::move_line_end,
|
||||
KeyEvent {
|
||||
code: KeyCode::Home,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_start,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::End,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_end,
|
||||
|
||||
key!('w') => commands::move_next_word_start,
|
||||
key!('b') => commands::move_prev_word_start,
|
||||
@@ -157,11 +176,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 +193,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 +213,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
|
||||
@@ -240,21 +261,20 @@ pub fn default() -> Keymaps {
|
||||
code: KeyCode::PageUp,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::page_up,
|
||||
ctrl!('b') => commands::page_up,
|
||||
KeyEvent {
|
||||
code: KeyCode::PageDown,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::page_down,
|
||||
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
|
||||
|
||||
@@ -276,19 +296,44 @@ pub fn default() -> Keymaps {
|
||||
key!('k') => commands::extend_line_up,
|
||||
key!('l') => commands::extend_char_right,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_char_left,
|
||||
KeyEvent {
|
||||
code: KeyCode::Down,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_down,
|
||||
KeyEvent {
|
||||
code: KeyCode::Up,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_up,
|
||||
KeyEvent {
|
||||
code: KeyCode::Right,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => 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,
|
||||
|
||||
key!('T') => commands::extend_till_prev_char,
|
||||
key!('F') => commands::extend_prev_char,
|
||||
KeyEvent {
|
||||
code: KeyCode::Home,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_start,
|
||||
KeyEvent {
|
||||
code: KeyCode::End,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_end,
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::exit_select_mode as Command,
|
||||
} => commands::exit_select_mode,
|
||||
)
|
||||
.into_iter(),
|
||||
);
|
||||
@@ -321,6 +366,7 @@ pub fn default() -> Keymaps {
|
||||
} => commands::insert::insert_tab,
|
||||
|
||||
ctrl!('x') => commands::completion,
|
||||
ctrl!('w') => commands::insert::delete_word_backward,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@@ -8,12 +8,11 @@ mod ui;
|
||||
|
||||
use application::Application;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Error;
|
||||
use anyhow::{Context, Error, 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 +27,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 +38,123 @@ 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();
|
||||
pub struct Args {
|
||||
display_help: bool,
|
||||
display_version: bool,
|
||||
verbosity: u64,
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
let verbosity: u64 = args.occurrences_of("verbose");
|
||||
fn parse_args(mut args: Args) -> Result<Args> {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let mut iter = argv.iter();
|
||||
|
||||
setup_logging(verbosity).expect("failed to initialize logging.");
|
||||
iter.next(); // skip the program, we don't care about that
|
||||
|
||||
// initialize language registry
|
||||
use helix_core::config_dir;
|
||||
use helix_core::syntax::{Loader, LOADER};
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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"));
|
||||
// push the remaining args, if any to the files
|
||||
for filename in iter {
|
||||
args.files.push(PathBuf::from(filename));
|
||||
}
|
||||
|
||||
LOADER.get_or_init(|| {
|
||||
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
|
||||
Loader::new(config)
|
||||
});
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cache_dir = helix_core::cache_dir();
|
||||
if !cache_dir.exists() {
|
||||
std::fs::create_dir(&cache_dir);
|
||||
}
|
||||
|
||||
let logpath = cache_dir.join("helix.log");
|
||||
let help = format!(
|
||||
"\
|
||||
{} {}
|
||||
{}
|
||||
{}
|
||||
|
||||
USAGE:
|
||||
hx [FLAGS] [files]...
|
||||
|
||||
ARGS:
|
||||
<files>... Sets the input file to use
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
let mut args: Args = Args {
|
||||
display_help: false,
|
||||
display_version: false,
|
||||
verbosity: 0,
|
||||
files: [].to_vec(),
|
||||
};
|
||||
|
||||
args = parse_args(args).context("could not parse arguments")?;
|
||||
|
||||
// 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(&conf_dir);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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,89 +80,83 @@ 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();
|
||||
}
|
||||
PromptEvent::Validate => {
|
||||
let (view, doc) = editor.current();
|
||||
// revert state to what it was before the last update
|
||||
// doc.state = snapshot.clone();
|
||||
|
||||
// revert state to what it was before the last update
|
||||
// doc.state = snapshot.clone();
|
||||
// extract as fn(doc, item):
|
||||
|
||||
// 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
|
||||
|
||||
// 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();
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
} 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.
|
||||
};
|
||||
|
||||
// TODO: merge edit with additional_text_edits
|
||||
if let Some(additional_edits) = &item.additional_text_edits {
|
||||
if !additional_edits.is_empty() {
|
||||
unimplemented!(
|
||||
"completion: additional_text_edits: {:?}",
|
||||
additional_edits
|
||||
);
|
||||
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)
|
||||
}
|
||||
}
|
||||
} 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(
|
||||
// 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![edit],
|
||||
offset_encoding, // TODO: should probably transcode in Client
|
||||
vec![(trigger_offset, cursor, None)].into_iter(),
|
||||
);
|
||||
doc.apply(&transaction, view.id);
|
||||
doc.apply(&remove, view.id);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
use helix_lsp::OffsetEncoding;
|
||||
let transaction = util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
vec![edit],
|
||||
offset_encoding, // TODO: should probably transcode in Client
|
||||
);
|
||||
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,
|
||||
@@ -164,6 +207,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)
|
||||
}
|
||||
|
||||
@@ -180,32 +230,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -148,6 +148,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 +195,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
|
||||
});
|
||||
@@ -254,7 +261,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 +281,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 +299,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 +320,29 @@ 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 {
|
||||
// this only prevents panic due to painting selection too far
|
||||
// TODO: prevent painting when scroll past x or in gutter
|
||||
// TODO: use a more correct width check
|
||||
if (pos.col as u16) < viewport.width {
|
||||
let style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
surface
|
||||
.get_mut(
|
||||
viewport.x + pos.col as u16,
|
||||
viewport.y + pos.row as u16,
|
||||
)
|
||||
.set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,9 +355,9 @@ impl EditorView {
|
||||
let info: Style = theme.get("info");
|
||||
let hint: Style = theme.get("hint");
|
||||
|
||||
for (i, line) in (view.first_line..last_line).enumerate() {
|
||||
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 +401,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 +483,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,
|
||||
);
|
||||
@@ -522,7 +566,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,7 +575,6 @@ impl Component for EditorView {
|
||||
let mode = doc.mode();
|
||||
|
||||
let mut cxt = commands::Context {
|
||||
view_id: view.id,
|
||||
editor: &mut cx.editor,
|
||||
count: 1,
|
||||
callback: None,
|
||||
@@ -578,7 +622,6 @@ impl Component for EditorView {
|
||||
if completion.is_empty() {
|
||||
self.completion = None;
|
||||
}
|
||||
// TODO: if exiting InsertMode, remove completion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,16 +642,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)
|
||||
}
|
||||
@@ -620,6 +671,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);
|
||||
@@ -659,3 +714,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,
|
||||
@@ -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,7 +85,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
||||
Err(_err) => None,
|
||||
});
|
||||
|
||||
const MAX: usize = 1024;
|
||||
const MAX: usize = 2048;
|
||||
|
||||
Picker::new(
|
||||
files.take(MAX).collect(),
|
||||
|
@@ -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 {
|
||||
@@ -191,15 +206,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 +246,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 +262,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)
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,11 @@ pub enum PromptEvent {
|
||||
Abort,
|
||||
}
|
||||
|
||||
pub enum CompletionDirection {
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
pub fn new(
|
||||
prompt: String,
|
||||
@@ -80,11 +85,18 @@ impl Prompt {
|
||||
self.exit_selection();
|
||||
}
|
||||
|
||||
pub fn change_completion_selection(&mut self) {
|
||||
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 +104,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,9 +123,10 @@ 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 max_col = std::cmp::max(1, area.width / BASE_WIDTH);
|
||||
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
|
||||
let completion_area = Rect::new(
|
||||
area.x,
|
||||
@@ -133,7 +146,8 @@ impl Prompt {
|
||||
|
||||
for (i, (_range, completion)) in self.completion.iter().enumerate() {
|
||||
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
|
||||
};
|
||||
@@ -158,14 +172,15 @@ impl Prompt {
|
||||
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;
|
||||
@@ -250,12 +265,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 +295,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.0.10"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
|
@@ -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.0.10"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -28,3 +28,4 @@ slotmap = "1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.5"
|
||||
log = "~0.4"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
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::{
|
||||
@@ -40,11 +41,14 @@ 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>>,
|
||||
}
|
||||
|
||||
@@ -64,6 +68,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 +133,53 @@ 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))?;
|
||||
Rope::from_reader(BufReader::new(file))?
|
||||
};
|
||||
|
||||
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 +195,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 +227,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 +268,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);
|
||||
}
|
||||
@@ -262,26 +360,36 @@ impl Document {
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, view_id: ViewId) -> bool {
|
||||
if let Some(transaction) = self.history.undo() {
|
||||
let success = self._apply(&transaction, view_id);
|
||||
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
|
||||
|
||||
success
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, view_id: ViewId) -> bool {
|
||||
if let Some(transaction) = self.history.redo() {
|
||||
let success = self._apply(&transaction, view_id);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -300,7 +408,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 +420,11 @@ impl Document {
|
||||
|
||||
#[inline]
|
||||
pub fn is_modified(&self) -> bool {
|
||||
let history = self.history.take();
|
||||
let current_revision = history.current_revision();
|
||||
self.history.set(history);
|
||||
self.path.is_some()
|
||||
&& (self.history.current_revision() != self.last_saved_revision
|
||||
|| !self.changes.is_empty())
|
||||
&& (current_revision != self.last_saved_revision || !self.changes.is_empty())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -328,6 +440,11 @@ impl Document {
|
||||
.map(|language| language.scope.as_str())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn language_config(&self) -> Option<&LanguageConfiguration> {
|
||||
self.language.as_deref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Current document version, incremented at each change.
|
||||
pub fn version(&self) -> i32 {
|
||||
@@ -402,6 +519,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)]
|
||||
|
@@ -36,6 +36,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
|
||||
@@ -81,11 +93,18 @@ impl Editor {
|
||||
view.jumps.push(jump);
|
||||
view.doc = id;
|
||||
view.first_line = 0;
|
||||
let view_id = view.id;
|
||||
|
||||
let (view, doc) = self.current();
|
||||
|
||||
// initialize selection for view
|
||||
let doc = &mut self.documents[id];
|
||||
doc.selections.insert(view_id, Selection::point(0));
|
||||
let selection = doc
|
||||
.selections
|
||||
.entry(view.id)
|
||||
.or_insert_with(|| Selection::point(0));
|
||||
// TODO: reuse align_view
|
||||
let pos = selection.cursor();
|
||||
let line = doc.text().char_to_line(pos);
|
||||
view.first_line = line.saturating_sub(view.area.height as usize / 2);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +137,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()
|
||||
@@ -128,7 +147,7 @@ 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
|
||||
@@ -187,8 +206,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,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)
|
||||
);
|
||||
}
|
||||
|
@@ -293,9 +293,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) {
|
||||
|
@@ -106,7 +106,7 @@ impl View {
|
||||
/// Calculates the last visible line on screen
|
||||
#[inline]
|
||||
pub fn last_line(&self, doc: &Document) -> usize {
|
||||
let height = self.area.height.saturating_sub(1); // - 1 for statusline
|
||||
let height = self.area.height.saturating_sub(2); // - 2 for statusline
|
||||
std::cmp::min(
|
||||
self.first_line + height as usize,
|
||||
doc.text().len_lines() - 1,
|
||||
@@ -143,8 +143,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))
|
||||
}
|
||||
|
@@ -17,6 +17,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 +42,7 @@ injection-regex = "c"
|
||||
file-types = ["c"] # TODO: ["h"]
|
||||
roots = []
|
||||
|
||||
language-server = { command = "clangd" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
@@ -42,6 +52,7 @@ injection-regex = "cpp"
|
||||
file-types = ["cc", "cpp", "hpp", "h"]
|
||||
roots = []
|
||||
|
||||
language-server = { command = "clangd" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
@@ -65,6 +76,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"
|
||||
@@ -122,3 +144,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 = " " }
|
||||
|
146
runtime/queries/elixir/highlights.scm
Normal file
146
runtime/queries/elixir/highlights.scm
Normal file
@@ -0,0 +1,146 @@
|
||||
["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) @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)$")
|
||||
(#match? @variable.parameter "^[^_]"))
|
||||
|
||||
(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)))))]))
|
||||
(#match? @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
|
||||
|
||||
[(identifier) @function.special
|
||||
(#match? @function.special "^__.+__$")]
|
||||
|
||||
[(remote_identifier) @function.special
|
||||
(#match? @function.special "^__.+__$")]
|
||||
|
||||
[(identifier) @comment
|
||||
(#match? @comment "^_")]
|
||||
|
||||
(ERROR) @warning
|
26
runtime/queries/go/indents.toml
Normal file
26
runtime/queries/go/indents.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
indent = [
|
||||
"import_declaration",
|
||||
"const_declaration",
|
||||
"var_declaration",
|
||||
"type_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",
|
||||
]
|
||||
|
||||
outdent = [
|
||||
"case",
|
||||
"}",
|
||||
"]",
|
||||
")"
|
||||
]
|
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 = [
|
||||
"}",
|
||||
"]",
|
||||
")"
|
||||
]
|
@@ -1,10 +1,28 @@
|
||||
; Identifier conventions
|
||||
|
||||
|
||||
; Assume all-caps names are constants
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z\\d_]+$'"))
|
||||
|
||||
; Assume other uppercase names are enum constructors
|
||||
((identifier) @constructor
|
||||
(#match? @constructor "^[A-Z]"))
|
||||
|
||||
; Assume that uppercase names in paths are types
|
||||
(mod_item
|
||||
name: (identifier) @namespace)
|
||||
(scoped_identifier
|
||||
path: (identifier) @namespace)
|
||||
(scoped_identifier
|
||||
(scoped_identifier
|
||||
name: (identifier) @namespace))
|
||||
(scoped_type_identifier
|
||||
path: (identifier) @namespace)
|
||||
(scoped_type_identifier
|
||||
(scoped_identifier
|
||||
name: (identifier) @namespace))
|
||||
|
||||
((scoped_identifier
|
||||
path: (identifier) @type)
|
||||
(#match? @type "^[A-Z]"))
|
||||
@@ -13,9 +31,18 @@
|
||||
name: (identifier) @type))
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
; Assume other uppercase names are enum constructors
|
||||
((identifier) @constructor
|
||||
(#match? @constructor "^[A-Z]"))
|
||||
; Namespaces
|
||||
|
||||
(crate) @namespace
|
||||
(extern_crate_declaration
|
||||
(crate)
|
||||
name: (identifier) @namespace)
|
||||
(scoped_use_list
|
||||
path: (identifier) @namespace)
|
||||
(scoped_use_list
|
||||
path: (scoped_identifier
|
||||
(identifier) @namespace))
|
||||
(use_list (scoped_identifier (identifier) @namespace . (_)))
|
||||
|
||||
; Function calls
|
||||
|
||||
@@ -41,6 +68,14 @@
|
||||
(macro_invocation
|
||||
macro: (identifier) @function.macro
|
||||
"!" @function.macro)
|
||||
(macro_invocation
|
||||
macro: (scoped_identifier
|
||||
(identifier) @function.macro .))
|
||||
|
||||
; (metavariable) @variable
|
||||
(metavariable) @function.macro
|
||||
|
||||
"$" @function.macro
|
||||
|
||||
; Function definitions
|
||||
|
||||
@@ -73,9 +108,11 @@
|
||||
";" @punctuation.delimiter
|
||||
|
||||
(parameter (identifier) @variable.parameter)
|
||||
(closure_parameters (_) @variable.parameter)
|
||||
|
||||
(lifetime (identifier) @label)
|
||||
|
||||
"async" @keyword
|
||||
"break" @keyword
|
||||
"const" @keyword
|
||||
"continue" @keyword
|
||||
@@ -109,14 +146,14 @@
|
||||
"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)
|
||||
(super) @keyword
|
||||
"as" @keyword
|
||||
|
||||
(self) @variable.builtin
|
||||
(metavariable) @variable
|
||||
|
||||
[
|
||||
(char_literal)
|
||||
@@ -133,7 +170,44 @@
|
||||
(attribute_item) @attribute
|
||||
(inner_attribute_item) @attribute
|
||||
|
||||
"as" @operator
|
||||
"*" @operator
|
||||
"&" @operator
|
||||
"'" @operator
|
||||
[
|
||||
"*"
|
||||
"'"
|
||||
"->"
|
||||
"=>"
|
||||
"<="
|
||||
"="
|
||||
"=="
|
||||
"!"
|
||||
"!="
|
||||
"%"
|
||||
"%="
|
||||
"&"
|
||||
"&="
|
||||
"&&"
|
||||
"|"
|
||||
"|="
|
||||
"||"
|
||||
"^"
|
||||
"^="
|
||||
"*"
|
||||
"*="
|
||||
"-"
|
||||
"-="
|
||||
"+"
|
||||
"+="
|
||||
"/"
|
||||
"/="
|
||||
">"
|
||||
"<"
|
||||
">="
|
||||
">>"
|
||||
"<<"
|
||||
">>="
|
||||
"@"
|
||||
".."
|
||||
"..="
|
||||
"'"
|
||||
] @operator
|
||||
|
||||
"?" @special
|
||||
|
31
runtime/queries/rust/indents.toml
Normal file
31
runtime/queries/rust/indents.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
indent = [
|
||||
"while_expression",
|
||||
"for_expression",
|
||||
"loop_expression",
|
||||
"if_expression",
|
||||
"if_let_expression",
|
||||
"tuple_expression",
|
||||
"array_expression",
|
||||
"use_list",
|
||||
"block",
|
||||
"match_block",
|
||||
"arguments",
|
||||
"parameters",
|
||||
"declaration_list",
|
||||
"field_declaration_list",
|
||||
"field_initializer_list",
|
||||
"struct_pattern",
|
||||
"tuple_pattern",
|
||||
"enum_variant_list",
|
||||
"binary_expression",
|
||||
"field_expression",
|
||||
"where_clause",
|
||||
"macro_invocation"
|
||||
]
|
||||
|
||||
outdent = [
|
||||
"where",
|
||||
"}",
|
||||
"]",
|
||||
")"
|
||||
]
|
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
|
@@ -4,6 +4,8 @@ pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
(rust-bin.stable.latest.default.override { extensions = ["rust-src"]; })
|
||||
lld_10
|
||||
lldb
|
||||
# pythonPackages.six
|
||||
stdenv.cc.cc.lib
|
||||
# pkg-config
|
||||
];
|
||||
@@ -12,6 +14,7 @@ pkgs.mkShell {
|
||||
# https://github.com/rust-lang/rust/issues/55979
|
||||
LD_LIBRARY_PATH="${stdenv.cc.cc.lib}/lib64:$LD_LIBRARY_PATH";
|
||||
|
||||
HELIX_RUNTIME=./runtime;
|
||||
shellHook = ''
|
||||
export HELIX_RUNTIME=$PWD/runtime
|
||||
'';
|
||||
}
|
||||
|
||||
|
10
theme.toml
10
theme.toml
@@ -1,9 +1,11 @@
|
||||
"attribute" = "#dbbfef" # lilac
|
||||
"keyword" = "#eccdba" # almond
|
||||
"keyword.directive" = "#dbbfef" # lilac -- preprocessor comments (#if in C)
|
||||
"namespace" = "#dbbfef" # lilac
|
||||
"punctuation" = "#a4a0e8" # lavender
|
||||
"punctuation.delimiter" = "#a4a0e8" # lavender
|
||||
"operator" = "#dbbfef" # lilac
|
||||
"special" = "#efba5d" # honey
|
||||
# "property" = "#a4a0e8" # lavender
|
||||
"property" = "#ffffff" # white
|
||||
"variable" = "#a4a0e8" # lavender
|
||||
@@ -31,7 +33,6 @@
|
||||
# TODO: variable as lilac
|
||||
# TODO: mod/use statements as white
|
||||
# TODO: mod stuff as chamois
|
||||
# TODO: add "(scoped_identifier) @path" for std::mem::
|
||||
#
|
||||
# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors
|
||||
|
||||
@@ -42,10 +43,15 @@
|
||||
"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.menu.selected" = { fg = "#281733", bg = "#ffffff" } # revolver
|
||||
|
||||
"warning" = "#ffcd1c"
|
||||
"error" = "#f47868"
|
||||
"info" = "#6F44F0"
|
||||
|
Reference in New Issue
Block a user