Compare commits

..

72 Commits

Author SHA1 Message Date
Blaž Hrastnik
cbb3ebafdc Support ctrl-f and ctrl-b to page up/down, fixes #41 2021-06-02 13:20:36 +09:00
Blaž Hrastnik
0851110d10 f/t: Check if at bounds before searching, refs #43, closes #37 2021-06-02 13:20:27 +09:00
Blaž Hrastnik
3ace581191 Fix panics when triggering w or e on the last char of the line
Closes #32
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
c0264b9f7f fix: Don't allow moving past last line, fixes #30, #24
Off by 1 error
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
22dad592b8 Merge pull request #40 from data0x200/fix-empty-command
Fix empty command cause panic
2021-06-02 13:06:57 +09:00
Blaž Hrastnik
67b1cd32c7 Update install notes 2021-06-02 11:14:46 +09:00
Daichi Takamiya
4d12c7c3cf Fix empty command cause panic 2021-06-02 10:55:32 +09:00
Blaž Hrastnik
4f56a8e248 book: Always generate the CNAME file 2021-06-02 10:24:00 +09:00
Blaž Hrastnik
dbc392d92c Run fmt 2021-06-02 09:56:50 +09:00
Blaž Hrastnik
7967d312c0 Merge pull request #38 from nathom/master
Add .DS_Store to ignored directories
2021-06-02 09:37:09 +09:00
Blaž Hrastnik
db48d22384 Merge pull request #19 from wojciechkepka/archinstall
Add Arch Linux installation instructions to README
2021-06-02 09:30:41 +09:00
Blaž Hrastnik
533ff61d0e Merge pull request #34 from DanySpin97/improve-error
Improve errors handling in main by adding context
2021-06-02 09:30:16 +09:00
nathom
b1ce969d80 Add .DS_Store to ignored directories 2021-06-01 17:29:37 -07:00
Blaž Hrastnik
01bf363446 Merge pull request #31 from wullewutz/patch-1
Fixed c/p error in keymap doc
2021-06-02 09:26:29 +09:00
Blaž Hrastnik
cc323f7665 Merge pull request #36 from swdunlop/patch-1
Make HELIX_RUNTIME depend on pwd, not speed's HOME
2021-06-02 09:26:04 +09:00
Scott Dunlop
60caaf7fc4 Make HELIX_RUNTIME depend on pwd, not speed's HOME 2021-06-01 15:03:57 -07:00
Danilo Spinella
ea824ed05d Improve errors handling in main by adding context
Return a anyhow::Result in main function so that Context can be used
there too.
2021-06-01 23:27:16 +02:00
wullewutz
cfae07e7ba Fixed c/p error in keymap doc
Go to definition mapping is "gd" not "ge"
2021-06-01 22:36:42 +02:00
wojciechkepka
56dbc60840 Add Arch Linux installation instructions to README 2021-06-01 21:08:09 +02:00
Blaž Hrastnik
c2e6b9f506 Add typescript support & ts/js indentation queries 2021-06-01 17:55:11 +09:00
Blaž Hrastnik
8fd8006043 Golang indent queries 2021-06-01 17:26:10 +09:00
Blaž Hrastnik
ce25aa951e Allow setting a filepath on :write 2021-06-01 17:26:03 +09:00
Blaž Hrastnik
a2147fc7d5 Change help prompt styling 2021-06-01 12:00:25 +09:00
Blaž Hrastnik
d8e16554bf Don't crash if no filename specified on open 2021-06-01 11:59:59 +09:00
Blaž Hrastnik
2cc30cd07c Categorize _ as a word char, not punctuation 2021-05-31 21:09:17 +09:00
Blaž Hrastnik
0dde5f2cae Fix badge 2021-05-31 21:09:07 +09:00
Blaž Hrastnik
b8d6e6ad28 Allow setting verbosity to info again 2021-05-31 17:14:49 +09:00
Blaž Hrastnik
5825bce0e4 Link to install/keymap docs, fix build status badge 2021-05-31 17:12:09 +09:00
Blaž Hrastnik
aeabfc55a8 Adjust golang indents yet again 2021-05-31 17:09:19 +09:00
Blaž Hrastnik
17e9386388 Allow moving to EOL byte, also fixes #15 2021-05-31 17:08:19 +09:00
Blaž Hrastnik
138787f76e Drop clap for pico-args
We barely have any flags so it's not worth the compilation time or
binary size to use clap.
2021-05-31 17:07:43 +09:00
Blaž Hrastnik
1132c5122f Update mdbook styling, add link to AUR 2021-05-31 00:32:21 +09:00
Blaž Hrastnik
4a8053e832 Merge pull request #13 from helix-editor/dependabot/cargo/tokio-1.6.1
Bump tokio from 1.6.0 to 1.6.1
2021-05-30 18:02:52 +09:00
Blaž Hrastnik
e033a4b8ac Merge pull request #11 from helix-editor/dependabot/github_actions/actions/upload-artifact-2.2.3
Bump actions/upload-artifact from 1 to 2.2.3
2021-05-30 17:58:43 +09:00
Blaž Hrastnik
acbcd758bd Merge pull request #12 from helix-editor/dependabot/github_actions/actions/cache-2.1.6
Bump actions/cache from 1 to 2.1.6
2021-05-30 17:58:21 +09:00
dependabot[bot]
76eed4caad Bump tokio from 1.6.0 to 1.6.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.6.0...tokio-1.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:43 +00:00
dependabot[bot]
3170c49be8 Bump actions/cache from 1 to 2.1.6
Bumps [actions/cache](https://github.com/actions/cache) from 1 to 2.1.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v1...v2.1.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:19 +00:00
dependabot[bot]
0327d66653 Bump actions/upload-artifact from 1 to 2.2.3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 2.2.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v2.2.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:15 +00:00
Blaž Hrastnik
c67e31830d Add dependabot config 2021-05-30 17:52:56 +09:00
Blaž Hrastnik
6460501a44 Update architecture.md 2021-05-30 17:52:46 +09:00
Blaž Hrastnik
67b037050f Adjust rust indents 2021-05-30 17:13:32 +09:00
Blaž Hrastnik
87d0617f3b Completion: Format docs tabs & highlight in the doc's native language 2021-05-30 17:13:02 +09:00
Blaž Hrastnik
668f735232 Update the README's matrix link 2021-05-30 17:12:43 +09:00
Blaž Hrastnik
a3a9502596 Add a github pages auto-build action. 2021-05-30 17:12:29 +09:00
Blaž Hrastnik
3810650a6b Completion: Render non-markdown docs too 2021-05-30 10:36:58 +09:00
Blaž Hrastnik
2c48d65b15 Format document on save 2021-05-30 00:00:15 +09:00
Blaž Hrastnik
d5466eddf5 Update flake deps 2021-05-29 23:59:30 +09:00
Blaž Hrastnik
d54ae09d3b ESC should exit both completion and insert mode 2021-05-29 10:37:47 +09:00
Blaž Hrastnik
a28eaa81a0 Golang indent adjustment 2021-05-29 00:06:38 +09:00
Blaž Hrastnik
d708efe275 Fix cursor positioning for prompts 2021-05-29 00:06:23 +09:00
Blaž Hrastnik
3336023614 ui: Menu rendering adjustments 2021-05-28 00:01:17 +09:00
Blaž Hrastnik
094203c74e Update deps, introduce the new tree-sitter lifetimes 2021-05-28 00:00:51 +09:00
Blaž Hrastnik
b114cfa119 Display more data in completion popups. 2021-05-22 17:33:42 +09:00
Blaž Hrastnik
f1dc25a774 Support count for indent too 2021-05-19 00:37:01 +09:00
Blaž Hrastnik
4f335fabc8 Fix unindent to work with tabs, take a count 2021-05-19 00:35:33 +09:00
Blaž Hrastnik
f366b97bce Update dependencies 2021-05-18 18:30:48 +09:00
Blaž Hrastnik
9c24f1ec0e Drop selection_lines completely, change move_line_start binding 2021-05-18 18:28:32 +09:00
Blaž Hrastnik
f99a683991 Fix crash if appending at end of line on the last line of the file 2021-05-18 18:17:14 +09:00
Blaž Hrastnik
9edae7e1f8 syntax: golang: Indent type declarations 2021-05-18 17:54:18 +09:00
Blaž Hrastnik
51d1d43289 Double the UI picker file limit. 2021-05-18 17:53:58 +09:00
Blaž Hrastnik
5a245b83a0 Append :fmt as a separate history state 2021-05-18 17:53:00 +09:00
Blaž Hrastnik
2100f5a2c0 Address clippy lint. 2021-05-17 23:01:45 +09:00
Blaž Hrastnik
8f6f329057 If switching to a previously open buffer in the same view, keep it's old offset 2021-05-17 16:36:13 +09:00
Blaž Hrastnik
8949347e2c Completion: apply additionalTextEdits.
Used for adding imports to the file when completing.
2021-05-17 16:35:34 +09:00
Blaž Hrastnik
54de768915 Fix crash if typing | (regex or) into the prompt.
Zero-width matches at the start of the file make no sense to us.
2021-05-16 18:58:43 +09:00
Blaž Hrastnik
6e03019a2c Adjust highlighting for rust. 2021-05-16 18:58:27 +09:00
Blaž Hrastnik
31d41080ed Add indentation queries for golang. 2021-05-15 17:17:26 +09:00
Blaž Hrastnik
5e6b46e7c5 Use array::IntoIter. 2021-05-15 10:52:07 +09:00
Blaž Hrastnik
354b822d21 Fix crash on xa<Enter> if we were on the last line. 2021-05-15 10:50:36 +09:00
Blaž Hrastnik
fae2127a11 Drop cx.view_id, it was used before we had cx.current. 2021-05-15 10:50:36 +09:00
Blaž Hrastnik
0e5b421646 When calculating a new selection, we need to take newly inserted text into account. 2021-05-15 10:50:36 +09:00
Blaž Hrastnik
4a9d1163e0 Hacky way to specify indent scopes per language via toml configs.
Can't do it via a scm query nicely because it returns an iterator over
all the matches, whereas we want to traverse the tree ourselves.

Can't extract the pattern data from a parsed query either.

Oh well, toml files for now.
2021-05-14 19:21:46 +09:00
49 changed files with 1112 additions and 552 deletions

14
.github/dependabot.yml vendored Normal file
View 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"

View File

@@ -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') }}
@@ -64,19 +64,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') }}
@@ -104,19 +104,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
View 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

View File

@@ -96,7 +96,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

114
Cargo.lock generated
View File

@@ -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,17 +257,10 @@ 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"
dependencies = [
"anyhow",
"etcetera",
"helix-syntax",
"once_cell",
@@ -291,6 +269,7 @@ dependencies = [
"serde",
"smallvec",
"tendril",
"toml",
"tree-sitter",
"unicode-segmentation",
"unicode-width",
@@ -334,7 +313,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"crossterm",
"dirs-next",
"fern",
@@ -348,6 +326,7 @@ dependencies = [
"log",
"num_cpus",
"once_cell",
"pico-args",
"pulldown-cmark",
"serde",
"serde_json",
@@ -422,16 +401,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 +445,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 +469,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 +570,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"
@@ -644,6 +607,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pico-args"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7afeb98c5a10e0bffcc7fc16e105b04d06729fac5fd6384aebf7ff5cb5a67d"
[[package]]
name = "pin-project-lite"
version = "0.2.6"
@@ -658,9 +627,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",
]
@@ -753,18 +722,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 +825,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 +880,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 +900,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 +911,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 +931,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 +1003,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"

View File

@@ -1,7 +1,7 @@
# Helix
[![Build status](https://github.com/helix-editor/helix/workflows/ci/badge.svg)](https://github.com/helix-editor/helix/actions)
[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)
![Screenshot](./screenshot.png)
@@ -10,7 +10,8 @@ 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/).
# Features
@@ -24,7 +25,7 @@ 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 Rust and Golang have indentation definitions at the moment.
We provide packaging for various distributions, but here's a quick method to
build from source.
@@ -41,6 +42,14 @@ 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).
## 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
# Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
@@ -58,5 +67,5 @@ 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 channel](https://matrix.to/#/#helix-community:matrix.org).

View File

@@ -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

View File

@@ -4,3 +4,6 @@ language = "en"
multilingual = false
src = "src"
theme = "colibri"
[output.html]
cname = "docs.helix-editor.com"

View File

@@ -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

View File

@@ -17,7 +17,7 @@
| f | find next char |
| T | find 'till previous char |
| F | find previous char |
| 0 | move to the start of the line |
| ^ | move to the start of the line |
| $ | move to the end of the line |
| m | Jump to matching bracket |
| PageUp | Move page up |
@@ -118,7 +118,7 @@ Jumps to various locations.
|-----|-----------|
| g | Go to the start of the file |
| e | Go to the end of the file |
| e | Go to definition |
| d | Go to definition |
| t | Go to type definition |
| r | Go to references |
| i | Go to implementation |

View File

@@ -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;
}

View File

@@ -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%);

View File

@@ -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
View File

@@ -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": {

View File

@@ -11,7 +11,6 @@ license = "MPL-2.0"
helix-syntax = { path = "../helix-syntax" }
ropey = "1.2"
anyhow = "1"
smallvec = "1.4"
tendril = "0.4.2"
unicode-segmentation = "1.6"
@@ -22,5 +21,6 @@ once_cell = "1.4"
regex = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
etcetera = "0.3"

View File

@@ -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)))
}

View File

@@ -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
@@ -286,6 +264,7 @@ where
tab_width: 4,
unit: String::from(" "),
}),
indent_query: OnceCell::new(),
}],
});
@@ -304,7 +283,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,

View File

@@ -13,7 +13,7 @@ mod position;
pub mod register;
pub mod search;
pub mod selection;
pub mod state;
mod state;
pub mod syntax;
mod transaction;

View File

@@ -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,7 @@ 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() - 2),
};
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
@@ -65,7 +64,7 @@ pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) ->
let mut end = begin;
for _ in 0..count {
if begin + 1 == slice.len_chars() {
if begin + 2 > slice.len_chars() {
return None;
}
@@ -135,7 +134,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;
}
@@ -190,10 +189,10 @@ fn categorize(ch: char) -> Category {
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!()
}

View File

@@ -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);

View File

@@ -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,6 +62,17 @@ 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>,
}
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());
@@ -127,6 +141,20 @@ 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 root = crate::runtime_dir();
let path = root.join("queries").join(language).join("indents.toml");
let toml = std::fs::read(&path).ok()?;
toml::from_slice(&toml).ok()
})
.as_ref()
}
pub fn scope(&self) -> &str {
&self.scope
}
@@ -717,7 +745,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 +753,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 +929,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 +1193,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 +1244,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 +1263,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,
{

View File

@@ -107,7 +107,10 @@ 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(),
".DS_Store".to_string(),
];
let dirs = collect_tree_sitter_dirs(&ignore);
let mut n_jobs = 0;

View File

@@ -24,7 +24,7 @@ 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"] }
pico-args = "0.4"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }

View File

@@ -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,8 +45,8 @@ 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 {
if !args.files.is_empty() {
for file in args.files {
editor.open(file, Action::VerticalSplit)?;
}
} else {

View File

@@ -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);
}
@@ -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
.min(view.first_line + scrolloff)
.max(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
@@ -640,6 +633,11 @@ 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());
if end == 0 {
// skip empty matches that don't make sense
return;
}
let head = end - 1;
let selection = if extend {
@@ -656,7 +654,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 +662,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 +674,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('\\') {
@@ -824,6 +823,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 +885,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 +919,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 +947,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 +1031,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 +1116,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 +1125,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 +1149,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 +1168,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');
@@ -1638,6 +1641,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 +1655,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.len();
// 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 +1687,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.len();
(pos, pos, Some(text.into()))
});
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
@@ -1721,12 +1739,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 +1857,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 +1876,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 +1895,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))
}
}
@@ -2250,12 +2275,14 @@ pub fn space_mode(cx: &mut Context) {
'v' => vsplit(cx),
'w' => {
// save current buffer
let doc = cx.doc();
let (view, doc) = cx.current();
doc.format(view.id); // TODO: merge into save
tokio::spawn(doc.save());
}
'c' => {
let view_id = cx.view().id;
// close current split
cx.editor.close(cx.view_id, /* close_buffer */ false);
cx.editor.close(view_id, /* close_buffer */ false);
}
// ' ' => toggle_alternate_buffer(cx),
// TODO: temporary since space mode took it's old key

View File

@@ -145,7 +145,7 @@ pub fn default() -> Keymaps {
//
key!('r') => commands::replace,
key!('0') => commands::move_line_start,
key!('^') => commands::move_line_start,
key!('$') => commands::move_line_end,
key!('w') => commands::move_next_word_start,
@@ -240,10 +240,12 @@ 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,

View File

@@ -8,12 +8,11 @@ mod ui;
use application::Application;
use clap::{App, Arg};
use std::path::PathBuf;
use anyhow::Error;
use anyhow::{Context, Result};
fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
fn setup_logging(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,7 +27,7 @@ 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");
let home = dirs_next::home_dir().context("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()
@@ -48,27 +47,53 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
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 {
files: Vec<PathBuf>,
}
let verbosity: u64 = args.occurrences_of("verbose");
fn main() -> Result<()> {
let help = format!(
"\
{} {}
{}
{}
setup_logging(verbosity).expect("failed to initialize logging.");
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
-V, --version Prints version information
",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
);
let mut pargs = pico_args::Arguments::from_env();
// Help has a higher priority and should be handled separately.
if pargs.contains(["-h", "--help"]) {
print!("{}", help);
std::process::exit(0);
}
let mut verbosity: u64 = 0;
if pargs.contains("-v") {
verbosity = 1;
}
setup_logging(verbosity).context("failed to initialize logging")?;
let args = Args {
files: pargs.finish().into_iter().map(|arg| arg.into()).collect(),
};
// initialize language registry
use helix_core::config_dir;
@@ -80,17 +105,16 @@ fn main() {
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
LOADER.get_or_init(|| {
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
Loader::new(config)
});
let config = toml::from_slice(toml).context("Could not parse languages.toml")?;
LOADER.get_or_init(|| Loader::new(config));
let runtime = tokio::runtime::Runtime::new().unwrap();
let runtime = tokio::runtime::Runtime::new().context("unable to start tokio runtime")?;
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args).context("unable to create new appliction")?;
runtime.block_on(async move {
let mut app = Application::new(args).unwrap();
app.run().await;
});
Ok(())
}

View File

@@ -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);
}
}
}

View File

@@ -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"),
@@ -530,7 +537,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 +584,6 @@ impl Component for EditorView {
if completion.is_empty() {
self.completion = None;
}
// TODO: if exiting InsertMode, remove completion
}
}
}
@@ -599,16 +604,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)
}

View File

@@ -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
}

View File

@@ -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 });
}
}

View File

@@ -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(),

View File

@@ -118,6 +118,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 +203,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:
@@ -260,6 +264,15 @@ impl<T: 'static> Component for Picker<T> {
}
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)
}
}

View File

@@ -111,6 +111,7 @@ 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;
@@ -133,7 +134,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 +160,9 @@ impl Prompt {
if let Some(doc) = (self.doc_fn)(&self.line) {
let text = ui::Text::new(doc.to_string());
let area = Rect::new(
completion_area.x,
completion_area.y - 3,
completion_area.width,
3,
);
let area = Rect::new(completion_area.x, completion_area.y - 3, 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;
@@ -271,8 +268,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,
))
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -1,6 +1,6 @@
use anyhow::{Context, Error};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use helix_core::{
@@ -64,6 +64,42 @@ 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
}
use helix_lsp::lsp;
use url::Url;
@@ -116,6 +152,29 @@ impl Document {
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>> {
@@ -153,6 +212,20 @@ impl Document {
}
}
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
// canonicalize path to absolute value
let current_dir = std::env::current_dir()?;
let path = normalize_path(&current_dir.join(path));
if let Some(parent) = path.parent() {
// TODO: return error as necessary
if parent.exists() {
self.path = Some(path);
}
}
Ok(())
}
pub fn set_language(
&mut self,
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
@@ -328,6 +401,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 {

View File

@@ -81,11 +81,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;
}

View File

@@ -65,6 +65,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"

View 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",
"}",
"]",
")"
]

View 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 = [
"}",
"]",
")"
]

View File

@@ -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,15 @@
name: (identifier) @type))
(#match? @type "^[A-Z]"))
; Assume other uppercase names are enum constructors
((identifier) @constructor
(#match? @constructor "^[A-Z]"))
; Namespaces
(crate) @namespace
(scoped_use_list
path: (identifier) @namespace)
(scoped_use_list
path: (scoped_identifier
(identifier) @namespace))
(use_list (scoped_identifier (identifier) @namespace . (_)))
; Function calls
@@ -38,9 +62,19 @@
function: (field_expression
field: (field_identifier) @function.method))
; (macro_invocation
; macro: (identifier) @function.macro
; "!" @function.macro)
(macro_invocation
macro: (identifier) @function.macro
"!" @function.macro)
macro: (identifier) @function.macro)
(macro_invocation
macro: (scoped_identifier
(identifier) @function.macro .))
; (metavariable) @variable
(metavariable) @function.macro
"$" @function.macro
; Function definitions
@@ -73,6 +107,7 @@
";" @punctuation.delimiter
(parameter (identifier) @variable.parameter)
(closure_parameters (_) @variable.parameter)
(lifetime (identifier) @label)
@@ -114,9 +149,9 @@
(scoped_use_list (self) @keyword)
(scoped_identifier (self) @keyword)
(super) @keyword
"as" @keyword
(self) @variable.builtin
(metavariable) @variable
[
(char_literal)
@@ -133,7 +168,44 @@
(attribute_item) @attribute
(inner_attribute_item) @attribute
"as" @operator
"*" @operator
"&" @operator
"'" @operator
[
"*"
"'"
"->"
"=>"
"<="
"="
"=="
"!"
"!="
"%"
"%="
"&"
"&="
"&&"
"|"
"|="
"||"
"^"
"^="
"*"
"*="
"-"
"-="
"+"
"+="
"/"
"/="
">"
"<"
">="
">>"
"<<"
">>="
"@"
".."
"..="
"'"
] @operator
"?" @special

View 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",
"}",
"]",
")"
]

View 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

View File

@@ -0,0 +1 @@
../javascript/indents.toml

View File

@@ -0,0 +1,2 @@
(required_parameter (identifier) @local.definition)
(optional_parameter (identifier) @local.definition)

View 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

View File

@@ -12,6 +12,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
'';
}

View File

@@ -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"