mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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"
|
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -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
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
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
114
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,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"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# Helix
|
||||
|
||||
|
||||
[](https://github.com/helix-editor/helix/actions)
|
||||
[](https://github.com/helix-editor/helix/actions)
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
@@ -58,5 +59,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).
|
||||
|
||||
|
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
|
||||
|
@@ -21,7 +21,7 @@ shell for working on Helix.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
TODO: AUR
|
||||
A binary package is available on AUR as [helix-bin](https://aur.archlinux.org/packages/helix-bin/).
|
||||
|
||||
## Build from source
|
||||
|
||||
|
@@ -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 |
|
||||
|
@@ -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%);
|
||||
|
@@ -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": {
|
||||
|
@@ -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"
|
||||
|
@@ -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)))
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
};
|
||||
@@ -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!()
|
||||
}
|
||||
|
@@ -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,
|
||||
{
|
||||
|
@@ -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 }
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
@@ -1107,19 +1113,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 +1122,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 +1146,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 +1165,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 +1638,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 +1652,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 +1684,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 +1736,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 +1854,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 +1873,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 +1892,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 +2272,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
|
||||
|
@@ -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,
|
||||
|
@@ -8,7 +8,6 @@ mod ui;
|
||||
|
||||
use application::Application;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Error;
|
||||
@@ -48,28 +47,54 @@ 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() {
|
||||
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
|
||||
-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).expect("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;
|
||||
use helix_core::syntax::{Loader, LOADER};
|
||||
|
@@ -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"),
|
||||
@@ -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)
|
||||
}
|
||||
|
@@ -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(),
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
))
|
||||
}
|
||||
|
@@ -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,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(¤t_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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
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",
|
||||
"}",
|
||||
"]",
|
||||
")"
|
||||
]
|
@@ -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
|
||||
|
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",
|
||||
"}",
|
||||
"]",
|
||||
")"
|
||||
]
|
@@ -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;
|
||||
# HELIX_RUNTIME=./runtime;
|
||||
HELIX_RUNTIME="/home/speed/src/helix/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