mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
275 Commits
get-lang-c
...
24.03
Author | SHA1 | Date | |
---|---|---|---|
|
2cadec0b11 | ||
|
64c1de1988 | ||
|
787a237f8b | ||
|
381976a04c | ||
|
527cbe656c | ||
|
7ec509e2f9 | ||
|
c69ad7b3bf | ||
|
f21efc7a2f | ||
|
cb286b7a5d | ||
|
a6c6fa4c8e | ||
|
cef839e477 | ||
|
1e50fb8b55 | ||
|
07e21a10f0 | ||
|
a0d6566140 | ||
|
3205bc4132 | ||
|
d4b467090b | ||
|
1d1087822a | ||
|
614a744d24 | ||
|
2d9e336f64 | ||
|
47995bfb0c | ||
|
7bce9530d3 | ||
|
da2dec174a | ||
|
628dcd5c56 | ||
|
2178adfe93 | ||
|
54fab657be | ||
|
8f65bfe089 | ||
|
b46064b8c4 | ||
|
3001f22b31 | ||
|
66b9ff1d2a | ||
|
69e07ab61e | ||
|
68b21578ac | ||
|
b1222f0664 | ||
|
6dd46bfe1c | ||
|
d5c2973cd1 | ||
|
be307a4204 | ||
|
c9e34c556b | ||
|
f5d95de227 | ||
|
c099dde2a7 | ||
|
9ceeea5a83 | ||
|
fdcd461e65 | ||
|
459eb9a4c1 | ||
|
961025433d | ||
|
51739f1290 | ||
|
5ba36fe9b3 | ||
|
7f5fd63835 | ||
|
52a0734120 | ||
|
b8e79c0ef5 | ||
|
bcf7b26393 | ||
|
427dd2f383 | ||
|
d9de809a57 | ||
|
1d1806c85a | ||
|
4b4947639a | ||
|
5b8bfc5476 | ||
|
485c5cf0b8 | ||
|
0b6dea6dc2 | ||
|
6a22d7d1ca | ||
|
4d2282cbcc | ||
|
3d4889ce9a | ||
|
0f5430ab9e | ||
|
e91ec8e880 | ||
|
6ed93b6e49 | ||
|
6607938bf8 | ||
|
13533cb99c | ||
|
df8352ec05 | ||
|
ef797acf0d | ||
|
7ef583bea7 | ||
|
326c078356 | ||
|
8dc50bded9 | ||
|
58022586a0 | ||
|
2e4653ea31 | ||
|
94d210c9bf | ||
|
4ffe993533 | ||
|
38af99f05f | ||
|
476e6baf8f | ||
|
d99b6177c2 | ||
|
eead105f94 | ||
|
64389f97fe | ||
|
f7913c1a3b | ||
|
8457652da1 | ||
|
0301d01e78 | ||
|
3890376a23 | ||
|
e36774c2c8 | ||
|
9ec0271873 | ||
|
61f7d9ce2f | ||
|
761df60077 | ||
|
6fea7876a4 | ||
|
9282f1b8e5 | ||
|
b961acf746 | ||
|
0c51ab16d0 | ||
|
6c4d986c1b | ||
|
b44b627b14 | ||
|
e01a558294 | ||
|
3915b04bd9 | ||
|
2d15acdf60 | ||
|
ab61874efb | ||
|
2e2a1d6f61 | ||
|
2d589e74f0 | ||
|
c145999bff | ||
|
3bd493299f | ||
|
0dc67ff885 | ||
|
e3c6c82828 | ||
|
fd89c3c833 | ||
|
301dfb07cc | ||
|
e27b04735c | ||
|
cb01e52cd8 | ||
|
b93fae9c8b | ||
|
4e5f19df53 | ||
|
7a473c7494 | ||
|
ea0b244644 | ||
|
7d8c86e403 | ||
|
3f98891e79 | ||
|
cc43e3521e | ||
|
9267343830 | ||
|
5bd007266a | ||
|
d769fadde0 | ||
|
f04dafa2e2 | ||
|
5ca6a448e9 | ||
|
1d6db30acf | ||
|
062fb819a2 | ||
|
44db25939c | ||
|
d0bb774471 | ||
|
e51a1e4e2a | ||
|
1143f47954 | ||
|
f03b91d1b7 | ||
|
083a9e775d | ||
|
00653c772e | ||
|
26b3dc29be | ||
|
f46a09ab4f | ||
|
358ac6bc1f | ||
|
1a82aeeae9 | ||
|
ea95c68775 | ||
|
d0f8261141 | ||
|
b43d9aa306 | ||
|
cd02976fa3 | ||
|
c68ec92c5e | ||
|
a876148585 | ||
|
928bf80d9a | ||
|
8141a4a1ab | ||
|
dfa5382c51 | ||
|
6db666fce1 | ||
|
ec9efdef3b | ||
|
38484f33e5 | ||
|
03623f2f40 | ||
|
7100ed4efc | ||
|
b7b6f30084 | ||
|
98ebeeebd8 | ||
|
eca537615a | ||
|
cad0209e20 | ||
|
d2aacb3e36 | ||
|
27335476ed | ||
|
990378a46b | ||
|
cdef4f8a70 | ||
|
787cc36092 | ||
|
ebf155d635 | ||
|
2dc9ce68ec | ||
|
c72426cc87 | ||
|
b950dea003 | ||
|
9ab3f9d01a | ||
|
6432669822 | ||
|
78c34194b5 | ||
|
3e963b3c1b | ||
|
6ffe09e873 | ||
|
76e512f944 | ||
|
59369d99e2 | ||
|
4df08ddbe0 | ||
|
c59f29921d | ||
|
fe869e5dc7 | ||
|
7934ac7714 | ||
|
36ad70e4c7 | ||
|
c55bf72d25 | ||
|
caad87e999 | ||
|
fd7b722dfb | ||
|
661e123152 | ||
|
c111e28a6f | ||
|
d232e7a985 | ||
|
0516337abb | ||
|
bb1e2ddcd8 | ||
|
78ed3aded3 | ||
|
0eec518ff6 | ||
|
45ee568b98 | ||
|
d7c7589fd5 | ||
|
ad7b7bc804 | ||
|
6a90166d0a | ||
|
ac8d1f62a1 | ||
|
d9f7aaacaf | ||
|
7d8ce1a400 | ||
|
204c3707b0 | ||
|
13b9885084 | ||
|
786b5c533e | ||
|
581a1ebf5d | ||
|
d570c29ce3 | ||
|
d137a08231 | ||
|
f8e2d822ba | ||
|
a1272bdb17 | ||
|
bbcc89241f | ||
|
630d91168a | ||
|
72c508de24 | ||
|
c64a0e615b | ||
|
28a39e6efc | ||
|
a37af2dcbf | ||
|
1d87c6a999 | ||
|
0975d9c5e7 | ||
|
5c567f31e2 | ||
|
6e3ed7f0fa | ||
|
75d61d8149 | ||
|
d1054de3ce | ||
|
3f380722fb | ||
|
d545452819 | ||
|
81ae768a4e | ||
|
70cea93bff | ||
|
aa4241c973 | ||
|
5ec9565ddb | ||
|
dd59602809 | ||
|
dbac78bb3c | ||
|
cf4492174d | ||
|
87a720c3a1 | ||
|
f5b67d9acb | ||
|
4ab7029535 | ||
|
eb3c4e9f00 | ||
|
fe44391016 | ||
|
ee68fd09ac | ||
|
0328fa4d02 | ||
|
035b8eabdb | ||
|
8b6565c839 | ||
|
5e0b3cc28b | ||
|
9978d421fe | ||
|
f0be0c6d4a | ||
|
1616021a5a | ||
|
0d09fb4f55 | ||
|
2661e05b34 | ||
|
d8b8d2fda6 | ||
|
cda8ea991e | ||
|
83f09ecbff | ||
|
cb25d13028 | ||
|
6d724a8f33 | ||
|
6bfe1ddc53 | ||
|
b606c05246 | ||
|
299bcce481 | ||
|
2058b3732c | ||
|
7caae13465 | ||
|
cbd8602018 | ||
|
4168864572 | ||
|
ae8042bb83 | ||
|
e85507ccac | ||
|
960cda60ab | ||
|
8e592a151f | ||
|
13ed4f6c47 | ||
|
7d7ace551c | ||
|
9ed3dc52e0 | ||
|
52a43bcdfc | ||
|
f5f08becef | ||
|
9c56afeff3 | ||
|
1bc7aac780 | ||
|
1f916e65cf | ||
|
af8e524a7d | ||
|
c60ba4ba04 | ||
|
6754acd83f | ||
|
dcdecaab22 | ||
|
f41727cc9c | ||
|
6339a8c95a | ||
|
44cb8e5475 | ||
|
eef46b1aed | ||
|
bd9eef1f90 | ||
|
0e7f5d604e | ||
|
fa1d8dfabc | ||
|
2d8d16ff5e | ||
|
8a00620a71 | ||
|
6ce57b7924 | ||
|
eca3ccff76 | ||
|
3011df4f35 | ||
|
445f7a273a | ||
|
2fb7e50b54 | ||
|
3f88a3f4e6 | ||
|
a0b02106c3 | ||
|
054ce3961a |
@@ -1,3 +1,17 @@
|
||||
# we use tokio_unstable to enable runtime::Handle::id so we can separate
|
||||
# globals from multiple parallel tests. If that function ever does get removed
|
||||
# its possible to replace (with some additional overhead and effort)
|
||||
# Annoyingly build.rustflags doesn't work here because it gets overwritten
|
||||
# if people have their own global target.<..> config (for example to enable mold)
|
||||
# specifying flags this way is more robust as they get merged
|
||||
# This still gets overwritten by RUST_FLAGS though, luckily it shouldn't be necessary
|
||||
# to set those most of the time. If downstream does overwrite this its not a huge
|
||||
# deal since it will only break tests anyway
|
||||
[target."cfg(all())"]
|
||||
rustflags = ["--cfg", "tokio_unstable", "-C", "target-feature=-crt-static"]
|
||||
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
integration-test = "test --features integration --profile integration --workspace --test integration"
|
||||
|
||||
|
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -7,6 +7,14 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
tree-sitter:
|
||||
patterns:
|
||||
- "tree-sitter*"
|
||||
rust-dependencies:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
check:
|
||||
name: Check (msrv)
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
@@ -31,6 +32,7 @@ jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
HELIX_LOG_LEVEL: info
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
shared-key: "build"
|
||||
|
||||
- name: Cache test tree-sitter grammar
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: runtime/grammars
|
||||
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
|
||||
@@ -65,6 +67,7 @@ jobs:
|
||||
lints:
|
||||
name: Lints
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
@@ -92,6 +95,7 @@ jobs:
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
4
.github/workflows/cachix.yml
vendored
4
.github/workflows/cachix.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install nix
|
||||
uses: cachix/install-nix-action@v24
|
||||
uses: cachix/install-nix-action@v26
|
||||
|
||||
- name: Authenticate with Cachix
|
||||
uses: cachix/cachix-action@v13
|
||||
uses: cachix/cachix-action@v14
|
||||
with:
|
||||
name: helix
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
2
.ignore
2
.ignore
@@ -1,2 +0,0 @@
|
||||
# Things that we don't want ripgrep to search that we do want in git
|
||||
# https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering
|
253
CHANGELOG.md
253
CHANGELOG.md
@@ -1,3 +1,256 @@
|
||||
# 24.03 (2024-03-30)
|
||||
|
||||
As always, a big thank you to the all of the contributors! This release saw changes from 125 contributors.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `suffix` file-types in the `file-types` key in language configuration have been removed ([#8006](https://github.com/helix-editor/helix/pull/8006))
|
||||
- The `file-types` key in language configuration no longer matches full filenames without a glob pattern ([#8006](https://github.com/helix-editor/helix/pull/8006))
|
||||
|
||||
Features:
|
||||
|
||||
- Open URLs with the `goto_file` command ([#5820](https://github.com/helix-editor/helix/pull/5820))
|
||||
- Support drawing a border around popups and menus ([#4313](https://github.com/helix-editor/helix/pull/4313), [#9508](https://github.com/helix-editor/helix/pull/9508))
|
||||
- Track long lived diagnostic sources like Clippy or `rustc` ([#6447](https://github.com/helix-editor/helix/pull/6447), [#9280](https://github.com/helix-editor/helix/pull/9280))
|
||||
- This improves the handling of diagnostics from sources that only update the diagnostic positions on save.
|
||||
- Add support for LSP `window/showDocument` requests ([#8865](https://github.com/helix-editor/helix/pull/8865))
|
||||
- Refactor ad-hoc hooks to use a new generic event system ([#8021](https://github.com/helix-editor/helix/pull/8021), [#9668](https://github.com/helix-editor/helix/pull/9668), [#9660](https://github.com/helix-editor/helix/pull/9660))
|
||||
- This improves the behavior of autocompletions. For example navigating in insert mode no longer automatically triggers completions.
|
||||
- Allow using globs in the language configuration `file-types` key ([#8006](https://github.com/helix-editor/helix/pull/8006))
|
||||
- Allow specifying required roots for situational LSP activation ([#8696](https://github.com/helix-editor/helix/pull/8696))
|
||||
- Extend selections using mouse clicks in select mode ([#5436](https://github.com/helix-editor/helix/pull/5436))
|
||||
- Toggle block comments ([#4718](https://github.com/helix-editor/helix/pull/4718), [#9894](https://github.com/helix-editor/helix/pull/9894))
|
||||
- Support LSP diagnostic tags ([#9780](https://github.com/helix-editor/helix/pull/9780))
|
||||
- Add a `file-absolute-path` statusline element ([#4535](https://github.com/helix-editor/helix/pull/4535))
|
||||
- Cross injection layers in tree-sitter motions (`A-p`/`A-o`/`A-i`/`A-n`) ([#5176](https://github.com/helix-editor/helix/pull/5176))
|
||||
- Add a Amp-editor-like jumping command ([#8875](https://github.com/helix-editor/helix/pull/8875))
|
||||
|
||||
Commands:
|
||||
|
||||
- `:move` - move buffers with LSP support ([#8584](https://github.com/helix-editor/helix/pull/8584))
|
||||
- Also see [#8949](https://github.com/helix-editor/helix/pull/8949) which made path changes conform to the LSP spec and fixed the behavior of this command.
|
||||
- `page_cursor_up`, `page_cursor_down`, `page_cursor_half_up`, `page_cursor_half_down` - commands for scrolling the cursor and page together ([#8015](https://github.com/helix-editor/helix/pull/8015))
|
||||
- `:yank-diagnostic` - yank the diagnostic(s) under the primary cursor ([#9640](https://github.com/helix-editor/helix/pull/9640))
|
||||
- `select_line_above` / `select_line_below` - extend or shrink a selection based on the direction and anchor ([#9080](https://github.com/helix-editor/helix/pull/9080))
|
||||
|
||||
Usability improvements:
|
||||
|
||||
- Make `roots` key of `[[language]]` entries in `languages.toml` configuration optional ([#8803](https://github.com/helix-editor/helix/pull/8803))
|
||||
- Exit select mode in commands that modify the buffer ([#8689](https://github.com/helix-editor/helix/pull/8689))
|
||||
- Use crossterm cursor when out of focus ([#6858](https://github.com/helix-editor/helix/pull/6858), [#8934](https://github.com/helix-editor/helix/pull/8934))
|
||||
- Join empty lines with only one space in `join_selections` ([#8989](https://github.com/helix-editor/helix/pull/8989))
|
||||
- Introduce a hybrid tree-sitter and contextual indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307))
|
||||
- Allow configuring the indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307))
|
||||
- Check for LSP rename support before showing rename prompt ([#9277](https://github.com/helix-editor/helix/pull/9277))
|
||||
- Normalize `S-<lower-ascii>` keymaps to uppercase ascii ([#9213](https://github.com/helix-editor/helix/pull/9213))
|
||||
- Add formatter status to `--health` output ([#7986](https://github.com/helix-editor/helix/pull/7986))
|
||||
- Change path normalization strategy to not resolve symlinks ([#9330](https://github.com/helix-editor/helix/pull/9330))
|
||||
- Select subtree within injections in `:tree-sitter-subtree` ([#9309](https://github.com/helix-editor/helix/pull/9309))
|
||||
- Use tilde expansion and normalization for `$HELIX_RUNTIME` paths ([1bc7aac](https://github.com/helix-editor/helix/commit/1bc7aac))
|
||||
- Improve failure message for LSP goto references ([#9382](https://github.com/helix-editor/helix/pull/9382))
|
||||
- Use injection syntax trees for bracket matching ([5e0b3cc](https://github.com/helix-editor/helix/commit/5e0b3cc))
|
||||
- Respect injections in `:tree-sitter-highlight-name` ([8b6565c](https://github.com/helix-editor/helix/commit/8b6565c))
|
||||
- Respect injections in `move_parent_node_end` ([035b8ea](https://github.com/helix-editor/helix/commit/035b8ea))
|
||||
- Use `gix` pipeline filter instead of manual CRLF implementation ([#9503](https://github.com/helix-editor/helix/pull/9503))
|
||||
- Follow Neovim's truecolor detection ([#9577](https://github.com/helix-editor/helix/pull/9577))
|
||||
- Reload language configuration with `:reload`, SIGHUP ([#9415](https://github.com/helix-editor/helix/pull/9415))
|
||||
- Allow numbers as bindings ([#8471](https://github.com/helix-editor/helix/pull/8471), [#9887](https://github.com/helix-editor/helix/pull/9887))
|
||||
- Respect undercurl config when terminfo is not available ([#9897](https://github.com/helix-editor/helix/pull/9897))
|
||||
- Ignore `.pijul`, `.hg`, `.jj` in addition to `.git` in file pickers configured to show hidden files ([#9935](https://github.com/helix-editor/helix/pull/9935))
|
||||
- Add completion for registers to `:clear-register` and `:yank-diagnostic` ([#9936](https://github.com/helix-editor/helix/pull/9936))
|
||||
- Repeat last motion for goto next/prev diagnostic ([#9966](https://github.com/helix-editor/helix/pull/9966))
|
||||
- Allow configuring a character to use when rendering narrow no-breaking space ([#9604](https://github.com/helix-editor/helix/pull/9604))
|
||||
- Switch to a streaming regex engine (regex-cursor crate) to significantly speed up regex-based commands and features ([#9422](https://github.com/helix-editor/helix/pull/9422), [#9756](https://github.com/helix-editor/helix/pull/9756), [#9891](https://github.com/helix-editor/helix/pull/9891))
|
||||
|
||||
Fixes:
|
||||
|
||||
- Swap `*` and `+` registers ([#8703](https://github.com/helix-editor/helix/pull/8703), [#8708](https://github.com/helix-editor/helix/pull/8708))
|
||||
- Use terminfo to reset terminal cursor style ([#8591](https://github.com/helix-editor/helix/pull/8591))
|
||||
- Fix precedence of `@align` captures in indentat computation ([#8659](https://github.com/helix-editor/helix/pull/8659))
|
||||
- Only render the preview if a Picker has a preview function ([#8667](https://github.com/helix-editor/helix/pull/8667))
|
||||
- Fix the precedence of `ui.virtual.whitespace` ([#8750](https://github.com/helix-editor/helix/pull/8750), [#8879](https://github.com/helix-editor/helix/pull/8879))
|
||||
- Fix crash in `:indent-style` ([#9087](https://github.com/helix-editor/helix/pull/9087))
|
||||
- Fix `didSave` text inclusion when sync capability is a kind variant ([#9101](https://github.com/helix-editor/helix/pull/9101))
|
||||
- Update the history of newly focused views ([#9271](https://github.com/helix-editor/helix/pull/9271))
|
||||
- Initialize diagnostics when opening a document ([#8873](https://github.com/helix-editor/helix/pull/8873))
|
||||
- Sync views when applying edits to unfocused views ([#9173](https://github.com/helix-editor/helix/pull/9173))
|
||||
- This fixes crashes that could occur from LSP workspace edits or `:write-all`.
|
||||
- Treat non-numeric `+arg`s passed in the CLI args as filenames ([#9333](https://github.com/helix-editor/helix/pull/9333))
|
||||
- Fix crash when using `mm` on an empty plaintext file ([2fb7e50](https://github.com/helix-editor/helix/commit/2fb7e50))
|
||||
- Ignore empty tree-sitter nodes in match bracket ([445f7a2](https://github.com/helix-editor/helix/commit/445f7a2))
|
||||
- Exit a language server if it sends a message with invalid JSON ([#9332](https://github.com/helix-editor/helix/pull/9332))
|
||||
- Handle failures to enable bracketed paste ([#9353](https://github.com/helix-editor/helix/pull/9353))
|
||||
- Gate all captures in a pattern behind `#is-not? local` predicates ([#9390](https://github.com/helix-editor/helix/pull/9390))
|
||||
- Make path changes LSP spec conformant ([#8949](https://github.com/helix-editor/helix/pull/8949))
|
||||
- Use range positions to determine `insert_newline` motion ([#9448](https://github.com/helix-editor/helix/pull/9448))
|
||||
- Fix division by zero when prompt completion area is too small ([#9524](https://github.com/helix-editor/helix/pull/9524))
|
||||
- Add changes to history in clipboard replacement typable commands ([#9625](https://github.com/helix-editor/helix/pull/9625))
|
||||
- Fix a crash in DAP with an unspecified `line` in breakpoints ([#9632](https://github.com/helix-editor/helix/pull/9632))
|
||||
- Fix space handling for filenames in bash completion ([#9702](https://github.com/helix-editor/helix/pull/9702), [#9708](https://github.com/helix-editor/helix/pull/9708))
|
||||
- Key diagnostics off of paths instead of LSP URIs ([#7367](https://github.com/helix-editor/helix/pull/7367))
|
||||
- Fix panic when using `join_selections_space` ([#9783](https://github.com/helix-editor/helix/pull/9783))
|
||||
- Fix panic when using `surround_replace`, `surround_delete` ([#9796](https://github.com/helix-editor/helix/pull/9796))
|
||||
- Fix panic in `surround_replace`, `surround_delete` with nested surrounds and multiple cursors ([#9815](https://github.com/helix-editor/helix/pull/9815))
|
||||
- Fix panic in `select_textobject_around` ([#9832](https://github.com/helix-editor/helix/pull/9832))
|
||||
- Don't stop reloading documents when reloading fails in `:reload-all` ([#9870](https://github.com/helix-editor/helix/pull/9870))
|
||||
- Prevent `shell_keep_pipe` from stopping on nonzero exit status codes ([#9817](https://github.com/helix-editor/helix/pull/9817))
|
||||
|
||||
Themes:
|
||||
|
||||
- Add `gruber-dark` ([#8598](https://github.com/helix-editor/helix/pull/8598))
|
||||
- Update `everblush` ([#8705](https://github.com/helix-editor/helix/pull/8705))
|
||||
- Update `papercolor` ([#8718](https://github.com/helix-editor/helix/pull/8718), [#8827](https://github.com/helix-editor/helix/pull/8827))
|
||||
- Add `polmandres` ([#8759](https://github.com/helix-editor/helix/pull/8759))
|
||||
- Add `starlight` ([#8787](https://github.com/helix-editor/helix/pull/8787))
|
||||
- Update `naysayer` ([#8838](https://github.com/helix-editor/helix/pull/8838))
|
||||
- Add modus operandi themes ([#8728](https://github.com/helix-editor/helix/pull/8728), [#9912](https://github.com/helix-editor/helix/pull/9912))
|
||||
- Update `rose_pine` ([#8946](https://github.com/helix-editor/helix/pull/8946))
|
||||
- Update `darcula` ([#8738](https://github.com/helix-editor/helix/pull/8738), [#9002](https://github.com/helix-editor/helix/pull/9002), [#9449](https://github.com/helix-editor/helix/pull/9449), [#9588](https://github.com/helix-editor/helix/pull/9588))
|
||||
- Add modus vivendi themes ([#8894](https://github.com/helix-editor/helix/pull/8894), [#9912](https://github.com/helix-editor/helix/pull/9912))
|
||||
- Add `horizon-dark` ([#9008](https://github.com/helix-editor/helix/pull/9008), [#9493](https://github.com/helix-editor/helix/pull/9493))
|
||||
- Update `noctis` ([#9123](https://github.com/helix-editor/helix/pull/9123))
|
||||
- Update `nord` ([#9135](https://github.com/helix-editor/helix/pull/9135))
|
||||
- Update monokai pro themes ([#9148](https://github.com/helix-editor/helix/pull/9148))
|
||||
- Update tokyonight themes ([#9099](https://github.com/helix-editor/helix/pull/9099), [#9724](https://github.com/helix-editor/helix/pull/9724), [#9789](https://github.com/helix-editor/helix/pull/9789))
|
||||
- Add `ttox` ([#8524](https://github.com/helix-editor/helix/pull/8524))
|
||||
- Add `voxed` ([#9164](https://github.com/helix-editor/helix/pull/9164))
|
||||
- Update `sonokai` ([#9370](https://github.com/helix-editor/helix/pull/9370), [#9376](https://github.com/helix-editor/helix/pull/9376), [#5379](https://github.com/helix-editor/helix/pull/5379))
|
||||
- Update `onedark`, `onedarker` ([#9397](https://github.com/helix-editor/helix/pull/9397))
|
||||
- Update `cyan_light` ([#9375](https://github.com/helix-editor/helix/pull/9375), [#9688](https://github.com/helix-editor/helix/pull/9688))
|
||||
- Add `gruvbox_light_soft`, `gruvbox_light_hard` ([#9266](https://github.com/helix-editor/helix/pull/9266))
|
||||
- Update GitHub themes ([#9487](https://github.com/helix-editor/helix/pull/9487))
|
||||
- Add `term16_dark`, `term16_light` ([#9477](https://github.com/helix-editor/helix/pull/9477))
|
||||
- Update Zed themes ([#9544](https://github.com/helix-editor/helix/pull/9544), [#9549](https://github.com/helix-editor/helix/pull/9549))
|
||||
- Add `curzon` ([#9553](https://github.com/helix-editor/helix/pull/9553))
|
||||
- Add `monokai_soda` ([#9651](https://github.com/helix-editor/helix/pull/9651))
|
||||
- Update catppuccin themes ([#9859](https://github.com/helix-editor/helix/pull/9859))
|
||||
- Update `rasmus` ([#9939](https://github.com/helix-editor/helix/pull/9939))
|
||||
- Update `dark_plus` ([#9949](https://github.com/helix-editor/helix/pull/9949), [628dcd5](https://github.com/helix-editor/helix/commit/628dcd5))
|
||||
- Update gruvbox themes ([#9960](https://github.com/helix-editor/helix/pull/9960))
|
||||
- Add jump label theming to `dracula` ([#9973](https://github.com/helix-editor/helix/pull/9973))
|
||||
- Add jump label theming to `horizon-dark` ([#9984](https://github.com/helix-editor/helix/pull/9984))
|
||||
- Add jump label theming to catppuccin themes ([2178adf](https://github.com/helix-editor/helix/commit/2178adf), [#9983](https://github.com/helix-editor/helix/pull/9983))
|
||||
- Add jump label theming to `onedark` themes ([da2dec1](https://github.com/helix-editor/helix/commit/da2dec1))
|
||||
- Add jump label theming to rose-pine themes ([#9981](https://github.com/helix-editor/helix/pull/9981))
|
||||
- Add jump label theming to Nord themes ([#10008](https://github.com/helix-editor/helix/pull/10008))
|
||||
- Add jump label theming to Monokai themes ([#10009](https://github.com/helix-editor/helix/pull/10009))
|
||||
- Add jump label theming to gruvbox themes ([#10012](https://github.com/helix-editor/helix/pull/10012))
|
||||
- Add jump label theming to `kanagawa` ([#10030](https://github.com/helix-editor/helix/pull/10030))
|
||||
- Update material themes ([#10043](https://github.com/helix-editor/helix/pull/10043))
|
||||
- Add `jetbrains_dark` ([#9967](https://github.com/helix-editor/helix/pull/9967))
|
||||
|
||||
New languages:
|
||||
|
||||
- Typst ([#7474](https://github.com/helix-editor/helix/pull/7474))
|
||||
- LPF ([#8536](https://github.com/helix-editor/helix/pull/8536))
|
||||
- GN ([#6969](https://github.com/helix-editor/helix/pull/6969))
|
||||
- DBML ([#8860](https://github.com/helix-editor/helix/pull/8860))
|
||||
- log ([#8916](https://github.com/helix-editor/helix/pull/8916))
|
||||
- Janet ([#9081](https://github.com/helix-editor/helix/pull/9081), [#9247](https://github.com/helix-editor/helix/pull/9247))
|
||||
- Agda ([#8285](https://github.com/helix-editor/helix/pull/8285))
|
||||
- Avro ([#9113](https://github.com/helix-editor/helix/pull/9113))
|
||||
- Smali ([#9089](https://github.com/helix-editor/helix/pull/9089))
|
||||
- HOCON ([#9203](https://github.com/helix-editor/helix/pull/9203))
|
||||
- Tact ([#9512](https://github.com/helix-editor/helix/pull/9512))
|
||||
- PKL ([#9515](https://github.com/helix-editor/helix/pull/9515))
|
||||
- CEL ([#9296](https://github.com/helix-editor/helix/pull/9296))
|
||||
- SpiceDB ([#9296](https://github.com/helix-editor/helix/pull/9296))
|
||||
- Hoon ([#9190](https://github.com/helix-editor/helix/pull/9190))
|
||||
- DockerCompose ([#9661](https://github.com/helix-editor/helix/pull/9661), [#9916](https://github.com/helix-editor/helix/pull/9916))
|
||||
- Groovy ([#9350](https://github.com/helix-editor/helix/pull/9350), [#9681](https://github.com/helix-editor/helix/pull/9681), [#9677](https://github.com/helix-editor/helix/pull/9677))
|
||||
- FIDL ([#9713](https://github.com/helix-editor/helix/pull/9713))
|
||||
- Powershell ([#9827](https://github.com/helix-editor/helix/pull/9827))
|
||||
- ld ([#9835](https://github.com/helix-editor/helix/pull/9835))
|
||||
- Hyperland config ([#9899](https://github.com/helix-editor/helix/pull/9899))
|
||||
- JSONC ([#9906](https://github.com/helix-editor/helix/pull/9906))
|
||||
- PHP Blade ([#9513](https://github.com/helix-editor/helix/pull/9513))
|
||||
- SuperCollider ([#9329](https://github.com/helix-editor/helix/pull/9329))
|
||||
- Koka ([#8727](https://github.com/helix-editor/helix/pull/8727))
|
||||
- PKGBUILD ([#9909](https://github.com/helix-editor/helix/pull/9909), [#9943](https://github.com/helix-editor/helix/pull/9943))
|
||||
- Ada ([#9908](https://github.com/helix-editor/helix/pull/9908))
|
||||
- Helm charts ([#9900](https://github.com/helix-editor/helix/pull/9900))
|
||||
- Ember.js templates ([#9902](https://github.com/helix-editor/helix/pull/9902))
|
||||
- Ohm ([#9991](https://github.com/helix-editor/helix/pull/9991))
|
||||
|
||||
Updated languages and queries:
|
||||
|
||||
- Add HTML injection queries for Rust ([#8603](https://github.com/helix-editor/helix/pull/8603))
|
||||
- Switch to tree-sitter-ron for RON ([#8624](https://github.com/helix-editor/helix/pull/8624))
|
||||
- Update and improve comment highlighting ([#8564](https://github.com/helix-editor/helix/pull/8564), [#9253](https://github.com/helix-editor/helix/pull/9253), [#9800](https://github.com/helix-editor/helix/pull/9800), [#10014](https://github.com/helix-editor/helix/pull/10014))
|
||||
- Highlight type parameters in Rust ([#8660](https://github.com/helix-editor/helix/pull/8660))
|
||||
- Change KDL tree-sitter parsers ([#8652](https://github.com/helix-editor/helix/pull/8652))
|
||||
- Update tree-sitter-markdown ([#8721](https://github.com/helix-editor/helix/pull/8721), [#10039](https://github.com/helix-editor/helix/pull/10039))
|
||||
- Update tree-sitter-purescript ([#8712](https://github.com/helix-editor/helix/pull/8712))
|
||||
- Add type parameter highlighting to TypeScript, Go, Haskell, OCaml and Kotlin ([#8718](https://github.com/helix-editor/helix/pull/8718))
|
||||
- Add indentation queries for Scheme and lisps using tree-sitter-scheme ([#8720](https://github.com/helix-editor/helix/pull/8720))
|
||||
- Recognize `meson_options.txt` as Meson ([#8794](https://github.com/helix-editor/helix/pull/8794))
|
||||
- Add language server configuration for Nushell ([#8878](https://github.com/helix-editor/helix/pull/8878))
|
||||
- Recognize `musicxml` as XML ([#8935](https://github.com/helix-editor/helix/pull/8935))
|
||||
- Update tree-sitter-rescript ([#8962](https://github.com/helix-editor/helix/pull/8962))
|
||||
- Update tree-sitter-python ([#8976](https://github.com/helix-editor/helix/pull/8976))
|
||||
- Recognize `.envrc.local` and `.envrc.private` as env ([#8988](https://github.com/helix-editor/helix/pull/8988))
|
||||
- Update tree-sitter-gleam ([#9003](https://github.com/helix-editor/helix/pull/9003), [9ceeea5](https://github.com/helix-editor/helix/commit/9ceeea5))
|
||||
- Update tree-sitter-d ([#9021](https://github.com/helix-editor/helix/pull/9021))
|
||||
- Fix R-markdown language name for LSP detection ([#9012](https://github.com/helix-editor/helix/pull/9012))
|
||||
- Add haskell-language-server LSP configuration ([#9111](https://github.com/helix-editor/helix/pull/9111))
|
||||
- Recognize `glif` as XML ([#9130](https://github.com/helix-editor/helix/pull/9130))
|
||||
- Recognize `.prettierrc` as JSON ([#9214](https://github.com/helix-editor/helix/pull/9214))
|
||||
- Add auto-pairs configuration for scheme ([#9232](https://github.com/helix-editor/helix/pull/9232))
|
||||
- Add textobject queries for Scala ([#9191](https://github.com/helix-editor/helix/pull/9191))
|
||||
- Add textobject queries for Protobuf ([#9184](https://github.com/helix-editor/helix/pull/9184))
|
||||
- Update tree-sitter-wren ([#8544](https://github.com/helix-editor/helix/pull/8544))
|
||||
- Add `spago.yaml` as an LSP root for PureScript ([#9362](https://github.com/helix-editor/helix/pull/9362))
|
||||
- Improve highlight and indent queries for Bash, Make and CSS ([#9393](https://github.com/helix-editor/helix/pull/9393))
|
||||
- Update tree-sitter-scala ([#9348](https://github.com/helix-editor/helix/pull/9348), [#9340](https://github.com/helix-editor/helix/pull/9340), [#9475](https://github.com/helix-editor/helix/pull/9475))
|
||||
- Recognize `.bash_history` as Bash ([#9401](https://github.com/helix-editor/helix/pull/9401))
|
||||
- Recognize Helix ignore files as ignore ([#9447](https://github.com/helix-editor/helix/pull/9447))
|
||||
- Inject SQL into Scala SQL strings ([#9428](https://github.com/helix-editor/helix/pull/9428))
|
||||
- Update gdscript textobjects ([#9288](https://github.com/helix-editor/helix/pull/9288))
|
||||
- Update Go queries ([#9510](https://github.com/helix-editor/helix/pull/9510), [#9525](https://github.com/helix-editor/helix/pull/9525))
|
||||
- Update tree-sitter-nushell ([#9502](https://github.com/helix-editor/helix/pull/9502))
|
||||
- Update tree-sitter-unison, add indent queries ([#9505](https://github.com/helix-editor/helix/pull/9505))
|
||||
- Update tree-sitter-slint ([#9551](https://github.com/helix-editor/helix/pull/9551), [#9698](https://github.com/helix-editor/helix/pull/9698))
|
||||
- Update tree-sitter-swift ([#9586](https://github.com/helix-editor/helix/pull/9586))
|
||||
- Add `fish_indent` as formatter for fish ([78ed3ad](https://github.com/helix-editor/helix/commit/78ed3ad))
|
||||
- Recognize `zon` as Zig ([#9582](https://github.com/helix-editor/helix/pull/9582))
|
||||
- Add a formatter for Odin ([#9537](https://github.com/helix-editor/helix/pull/9537))
|
||||
- Update tree-sitter-erlang ([#9627](https://github.com/helix-editor/helix/pull/9627), [fdcd461](https://github.com/helix-editor/helix/commit/fdcd461))
|
||||
- Capture Rust fields as argument textobjects ([#9637](https://github.com/helix-editor/helix/pull/9637))
|
||||
- Improve Dart textobjects ([#9644](https://github.com/helix-editor/helix/pull/9644))
|
||||
- Recognize `tmux.conf` as a bash file-type ([#9653](https://github.com/helix-editor/helix/pull/9653))
|
||||
- Add textobjects queries for Nix ([#9659](https://github.com/helix-editor/helix/pull/9659))
|
||||
- Add textobjects queries for HCL ([#9658](https://github.com/helix-editor/helix/pull/9658))
|
||||
- Recognize osm and osc extensions as XML ([#9697](https://github.com/helix-editor/helix/pull/9697))
|
||||
- Update tree-sitter-sql ([#9634](https://github.com/helix-editor/helix/pull/9634))
|
||||
- Recognize pde Processing files as Java ([#9741](https://github.com/helix-editor/helix/pull/9741))
|
||||
- Update tree-sitter-lua ([#9727](https://github.com/helix-editor/helix/pull/9727))
|
||||
- Switch tree-sitter-nim parsers ([#9722](https://github.com/helix-editor/helix/pull/9722))
|
||||
- Recognize GTK builder ui files as XML ([#9754](https://github.com/helix-editor/helix/pull/9754))
|
||||
- Add configuration for markdown-oxide language server ([#9758](https://github.com/helix-editor/helix/pull/9758))
|
||||
- Add a shebang for elvish ([#9779](https://github.com/helix-editor/helix/pull/9779))
|
||||
- Fix precedence of Svelte TypeScript injection ([#9777](https://github.com/helix-editor/helix/pull/9777))
|
||||
- Recognize common Dockerfile file types ([#9772](https://github.com/helix-editor/helix/pull/9772))
|
||||
- Recognize NUON files as Nu ([#9839](https://github.com/helix-editor/helix/pull/9839))
|
||||
- Add textobjects for Java native functions and constructors ([#9806](https://github.com/helix-editor/helix/pull/9806))
|
||||
- Fix "braket" typeo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910))
|
||||
- Update tree-sitter-hurl ([#9775](https://github.com/helix-editor/helix/pull/9775))
|
||||
- Add textobjects queries for Vala ([#8541](https://github.com/helix-editor/helix/pull/8541))
|
||||
- Update tree-sitter-git-config ([9610254](https://github.com/helix-editor/helix/commit/9610254))
|
||||
- Recognize 'mmd' as Mermaid ([459eb9a](https://github.com/helix-editor/helix/commit/459eb9a))
|
||||
- Highlight Rust extern crate aliases ([c099dde](https://github.com/helix-editor/helix/commit/c099dde))
|
||||
- Improve parameter highlighting in C++ ([f5d95de](https://github.com/helix-editor/helix/commit/f5d95de))
|
||||
- Recognize 'rclone.conf' as INI ([#9959](https://github.com/helix-editor/helix/pull/9959))
|
||||
- Add injections for GraphQL and ERB in Ruby heredocs ([#10036](https://github.com/helix-editor/helix/pull/10036))
|
||||
- Add `main.odin` to Odin LSP roots ([#9968](https://github.com/helix-editor/helix/pull/9968))
|
||||
|
||||
Packaging:
|
||||
|
||||
- Allow user overlays in Nix grammars build ([#8749](https://github.com/helix-editor/helix/pull/8749))
|
||||
- Set Cargo feature resolver to v2 ([#8917](https://github.com/helix-editor/helix/pull/8917))
|
||||
- Use workspace inheritance for common Cargo metadata ([#8925](https://github.com/helix-editor/helix/pull/8925))
|
||||
- Remove sourcehut-based tree-sitter grammars from default build ([#9316](https://github.com/helix-editor/helix/pull/9316), [#9326](https://github.com/helix-editor/helix/pull/9326))
|
||||
- Add icon to Windows executable ([#9104](https://github.com/helix-editor/helix/pull/9104))
|
||||
|
||||
# 23.10 (2023-10-24)
|
||||
|
||||
A big shout out to all the contributors! We had 118 contributors in this release.
|
||||
|
666
Cargo.lock
generated
666
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ members = [
|
||||
"helix-loader",
|
||||
"helix-vcs",
|
||||
"helix-parsec",
|
||||
"helix-stdx",
|
||||
"xtask",
|
||||
]
|
||||
|
||||
@@ -36,11 +37,11 @@ package.helix-tui.opt-level = 2
|
||||
package.helix-term.opt-level = 2
|
||||
|
||||
[workspace.dependencies]
|
||||
tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
|
||||
tree-sitter = { version = "0.22" }
|
||||
nucleo = "0.2.0"
|
||||
|
||||
[workspace.package]
|
||||
version = "23.10.0"
|
||||
version = "24.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
categories = ["editor"]
|
||||
|
@@ -51,7 +51,8 @@ Its settings will be merged with the configuration directory `config.toml` and t
|
||||
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
|
||||
| `auto-format` | Enable automatic formatting on save | `true` |
|
||||
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
|
||||
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
|
||||
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `250` |
|
||||
| `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |
|
||||
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
|
||||
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
|
||||
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
|
||||
@@ -67,6 +68,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
|
||||
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
|
||||
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
|
||||
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
|
||||
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
|
||||
|
||||
### `[editor.statusline]` Section
|
||||
|
||||
@@ -74,7 +76,7 @@ Allows configuring the statusline at the bottom of the editor.
|
||||
|
||||
The configuration distinguishes between three areas of the status line:
|
||||
|
||||
`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]`
|
||||
`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]`
|
||||
|
||||
Statusline elements can be defined as follows:
|
||||
|
||||
@@ -107,6 +109,7 @@ The following statusline elements can be configured:
|
||||
| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
|
||||
| `spinner` | A progress spinner indicating LSP activity |
|
||||
| `file-name` | The path/name of the opened file |
|
||||
| `file-absolute-path` | The absolute path/name of the opened file |
|
||||
| `file-base-name` | The basename of the opened file |
|
||||
| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
|
||||
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
|
||||
@@ -176,7 +179,7 @@ All git related options are only enabled in a git repository.
|
||||
|`git-ignore` | Enables reading `.gitignore` files | `true`
|
||||
|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true`
|
||||
|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
|
||||
|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
|
||||
|`max-depth` | Set with an integer value for maximum depth to recurse | Unset by default
|
||||
|
||||
Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files.
|
||||
|
||||
@@ -222,7 +225,7 @@ Additionally, this setting can be used in a language config. Unless
|
||||
the editor setting is `false`, this will override the editor config in
|
||||
documents with this language.
|
||||
|
||||
Example `languages.toml` that adds <> and removes ''
|
||||
Example `languages.toml` that adds `<>` and removes `''`
|
||||
|
||||
```toml
|
||||
[[language]]
|
||||
@@ -252,8 +255,8 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r
|
||||
|
||||
| Key | Description | Default |
|
||||
|-----|-------------|---------|
|
||||
| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline` | `"none"` |
|
||||
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below |
|
||||
| `render` | Whether to render whitespace. May either be `all` or `none`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `none` |
|
||||
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below |
|
||||
|
||||
Example
|
||||
|
||||
@@ -264,11 +267,14 @@ render = "all"
|
||||
[editor.whitespace.render]
|
||||
space = "all"
|
||||
tab = "all"
|
||||
nbsp = "none"
|
||||
nnbsp = "none"
|
||||
newline = "none"
|
||||
|
||||
[editor.whitespace.characters]
|
||||
space = "·"
|
||||
nbsp = "⍽"
|
||||
nnbsp = "␣"
|
||||
tab = "→"
|
||||
newline = "⏎"
|
||||
tabpad = "·" # Tabs will look like "→···" (depending on tab width)
|
||||
@@ -339,7 +345,12 @@ Currently unused
|
||||
|
||||
#### `[editor.gutters.diff]` Section
|
||||
|
||||
Currently unused
|
||||
The `diff` gutter option displays colored bars indicating whether a `git` diff represents that a line was added, removed or changed.
|
||||
These colors are controlled by the theme attributes `diff.plus`, `diff.minus` and `diff.delta`.
|
||||
|
||||
Other diff providers will eventually be supported by a future plugin system.
|
||||
|
||||
There are currently no options for this section.
|
||||
|
||||
#### `[editor.gutters.spacer]` Section
|
||||
|
||||
@@ -369,8 +380,25 @@ wrap-indicator = "" # set wrap-indicator to "" to hide it
|
||||
|
||||
### `[editor.smart-tab]` Section
|
||||
|
||||
Options for navigating and editing using tab key.
|
||||
|
||||
| Key | Description | Default |
|
||||
|------------|-------------|---------|
|
||||
| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
|
||||
| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |
|
||||
|
||||
|
||||
Due to lack of support for S-tab in some terminals, the default keybindings don't fully embrace smart-tab editing experience. If you enjoy smart-tab navigation and a terminal that supports the [Enhanced Keyboard protocol](https://github.com/helix-editor/helix/wiki/Terminal-Support#enhanced-keyboard-protocol), consider setting extra keybindings:
|
||||
|
||||
```
|
||||
[keys.normal]
|
||||
tab = "move_parent_node_end"
|
||||
S-tab = "move_parent_node_start"
|
||||
|
||||
[keys.insert]
|
||||
S-tab = "move_parent_node_start"
|
||||
|
||||
[keys.select]
|
||||
tab = "extend_parent_node_end"
|
||||
S-tab = "extend_parent_node_start"
|
||||
```
|
||||
|
@@ -1,5 +1,6 @@
|
||||
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| ada | ✓ | ✓ | | `ada_language_server`, `ada_language_server` |
|
||||
| agda | ✓ | | | |
|
||||
| astro | ✓ | | | |
|
||||
| awk | ✓ | ✓ | | `awk-language-server` |
|
||||
@@ -8,12 +9,14 @@
|
||||
| beancount | ✓ | | | |
|
||||
| bibtex | ✓ | | | `texlab` |
|
||||
| bicep | ✓ | | | `bicep-langserver` |
|
||||
| blade | ✓ | | | |
|
||||
| blueprint | ✓ | | | `blueprint-compiler` |
|
||||
| c | ✓ | ✓ | ✓ | `clangd` |
|
||||
| c-sharp | ✓ | ✓ | | `OmniSharp` |
|
||||
| cabal | | | | `haskell-language-server-wrapper` |
|
||||
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
|
||||
| capnp | ✓ | | ✓ | |
|
||||
| cel | ✓ | | | |
|
||||
| clojure | ✓ | | | `clojure-lsp` |
|
||||
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
|
||||
| comment | ✓ | | | |
|
||||
@@ -21,14 +24,15 @@
|
||||
| cpon | ✓ | | ✓ | |
|
||||
| cpp | ✓ | ✓ | ✓ | `clangd` |
|
||||
| crystal | ✓ | ✓ | | `crystalline` |
|
||||
| css | ✓ | | | `vscode-css-language-server` |
|
||||
| css | ✓ | | ✓ | `vscode-css-language-server` |
|
||||
| cue | ✓ | | | `cuelsp` |
|
||||
| d | ✓ | ✓ | ✓ | `serve-d` |
|
||||
| dart | ✓ | | ✓ | `dart` |
|
||||
| dart | ✓ | ✓ | ✓ | `dart` |
|
||||
| dbml | ✓ | | | |
|
||||
| devicetree | ✓ | | | |
|
||||
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
|
||||
| diff | ✓ | | | |
|
||||
| docker-compose | ✓ | | ✓ | `docker-compose-langserver`, `yaml-language-server` |
|
||||
| dockerfile | ✓ | | | `docker-langserver` |
|
||||
| dot | ✓ | | | `dot-language-server` |
|
||||
| dtd | ✓ | | | |
|
||||
@@ -42,6 +46,7 @@
|
||||
| erb | ✓ | | | |
|
||||
| erlang | ✓ | ✓ | | `erlang_ls` |
|
||||
| esdl | ✓ | | | |
|
||||
| fidl | ✓ | | | |
|
||||
| fish | ✓ | ✓ | ✓ | |
|
||||
| forth | ✓ | | | `forth-lsp` |
|
||||
| fortran | ✓ | | ✓ | `fortls` |
|
||||
@@ -55,6 +60,7 @@
|
||||
| git-ignore | ✓ | | | |
|
||||
| git-rebase | ✓ | | | |
|
||||
| gleam | ✓ | ✓ | | `gleam` |
|
||||
| glimmer | ✓ | | | `ember-language-server` |
|
||||
| glsl | ✓ | ✓ | ✓ | |
|
||||
| gn | ✓ | | | |
|
||||
| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
|
||||
@@ -63,15 +69,19 @@
|
||||
| gotmpl | ✓ | | | `gopls` |
|
||||
| gowork | ✓ | | | `gopls` |
|
||||
| graphql | ✓ | | | `graphql-lsp` |
|
||||
| groovy | ✓ | | | |
|
||||
| hare | ✓ | | | |
|
||||
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
|
||||
| haskell-persistent | ✓ | | | |
|
||||
| hcl | ✓ | | ✓ | `terraform-ls` |
|
||||
| hcl | ✓ | ✓ | ✓ | `terraform-ls` |
|
||||
| heex | ✓ | ✓ | | `elixir-ls` |
|
||||
| helm | ✓ | | | `helm_ls` |
|
||||
| hocon | ✓ | | ✓ | |
|
||||
| hoon | ✓ | | | |
|
||||
| hosts | ✓ | | | |
|
||||
| html | ✓ | | | `vscode-html-language-server` |
|
||||
| hurl | ✓ | | ✓ | |
|
||||
| hyprlang | ✓ | | ✓ | |
|
||||
| idris | | | | `idris2-lsp` |
|
||||
| iex | ✓ | | | |
|
||||
| ini | ✓ | | | |
|
||||
@@ -82,13 +92,16 @@
|
||||
| jsdoc | ✓ | | | |
|
||||
| json | ✓ | | ✓ | `vscode-json-language-server` |
|
||||
| json5 | ✓ | | | |
|
||||
| jsonc | ✓ | | ✓ | `vscode-json-language-server` |
|
||||
| jsonnet | ✓ | | | `jsonnet-language-server` |
|
||||
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| julia | ✓ | ✓ | ✓ | `julia` |
|
||||
| just | ✓ | ✓ | ✓ | |
|
||||
| kdl | ✓ | ✓ | ✓ | |
|
||||
| koka | ✓ | | ✓ | |
|
||||
| kotlin | ✓ | | | `kotlin-language-server` |
|
||||
| latex | ✓ | ✓ | | `texlab` |
|
||||
| ld | ✓ | | ✓ | |
|
||||
| lean | ✓ | | | `lean` |
|
||||
| ledger | ✓ | | | |
|
||||
| llvm | ✓ | ✓ | ✓ | |
|
||||
@@ -97,9 +110,9 @@
|
||||
| log | ✓ | | | |
|
||||
| lpf | ✓ | | | |
|
||||
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
|
||||
| make | ✓ | | | |
|
||||
| make | ✓ | | ✓ | |
|
||||
| markdoc | ✓ | | | `markdoc-ls` |
|
||||
| markdown | ✓ | | | `marksman` |
|
||||
| markdown | ✓ | | | `marksman`, `markdown-oxide` |
|
||||
| markdown.inline | ✓ | | | |
|
||||
| matlab | ✓ | ✓ | ✓ | |
|
||||
| mermaid | ✓ | | | |
|
||||
@@ -109,12 +122,13 @@
|
||||
| nasm | ✓ | ✓ | | |
|
||||
| nickel | ✓ | | ✓ | `nls` |
|
||||
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
|
||||
| nix | ✓ | | | `nil` |
|
||||
| nix | ✓ | ✓ | | `nil` |
|
||||
| nu | ✓ | | | `nu` |
|
||||
| nunjucks | ✓ | | | |
|
||||
| ocaml | ✓ | | ✓ | `ocamllsp` |
|
||||
| ocaml-interface | ✓ | | | `ocamllsp` |
|
||||
| odin | ✓ | | ✓ | `ols` |
|
||||
| ohm | ✓ | ✓ | ✓ | |
|
||||
| opencl | ✓ | ✓ | ✓ | `clangd` |
|
||||
| openscad | ✓ | | | `openscad-lsp` |
|
||||
| org | ✓ | | | |
|
||||
@@ -123,9 +137,13 @@
|
||||
| pem | ✓ | | | |
|
||||
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
|
||||
| php | ✓ | ✓ | ✓ | `intelephense` |
|
||||
| php-only | ✓ | | | |
|
||||
| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` |
|
||||
| pkl | ✓ | | ✓ | |
|
||||
| po | ✓ | ✓ | | |
|
||||
| pod | ✓ | | | |
|
||||
| ponylang | ✓ | ✓ | ✓ | |
|
||||
| powershell | ✓ | | | |
|
||||
| prisma | ✓ | | | `prisma-language-server` |
|
||||
| prolog | | | | `swipl` |
|
||||
| protobuf | ✓ | ✓ | ✓ | `bufls`, `pb` |
|
||||
@@ -148,20 +166,23 @@
|
||||
| scala | ✓ | ✓ | ✓ | `metals` |
|
||||
| scheme | ✓ | | ✓ | |
|
||||
| scss | ✓ | | | `vscode-css-language-server` |
|
||||
| slint | ✓ | | ✓ | `slint-lsp` |
|
||||
| slint | ✓ | ✓ | ✓ | `slint-lsp` |
|
||||
| smali | ✓ | | ✓ | |
|
||||
| smithy | ✓ | | | `cs` |
|
||||
| sml | ✓ | | | |
|
||||
| solidity | ✓ | | | `solc` |
|
||||
| spicedb | ✓ | | | |
|
||||
| sql | ✓ | | | |
|
||||
| sshclientconfig | ✓ | | | |
|
||||
| starlark | ✓ | ✓ | | |
|
||||
| strace | ✓ | | | |
|
||||
| supercollider | ✓ | | | |
|
||||
| svelte | ✓ | | ✓ | `svelteserver` |
|
||||
| sway | ✓ | ✓ | ✓ | `forc` |
|
||||
| swift | ✓ | | | `sourcekit-lsp` |
|
||||
| t32 | ✓ | | | |
|
||||
| tablegen | ✓ | ✓ | ✓ | |
|
||||
| tact | ✓ | ✓ | ✓ | |
|
||||
| task | ✓ | | | |
|
||||
| templ | ✓ | | | `templ` |
|
||||
| tfvars | ✓ | | ✓ | `terraform-ls` |
|
||||
@@ -173,10 +194,10 @@
|
||||
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| typst | ✓ | | | `typst-lsp` |
|
||||
| ungrammar | ✓ | | | |
|
||||
| unison | ✓ | | | |
|
||||
| unison | ✓ | | ✓ | |
|
||||
| uxntal | ✓ | | | |
|
||||
| v | ✓ | ✓ | ✓ | `v-analyzer` |
|
||||
| vala | ✓ | | | `vala-language-server` |
|
||||
| vala | ✓ | ✓ | | `vala-language-server` |
|
||||
| verilog | ✓ | ✓ | | `svlangserver` |
|
||||
| vhdl | ✓ | | | `vhdl_ls` |
|
||||
| vhs | ✓ | | | |
|
||||
|
@@ -86,3 +86,4 @@
|
||||
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
|
||||
| `:redraw` | Clear and re-render the whole UI |
|
||||
| `:move` | Move the current buffer and its corresponding file to a different path |
|
||||
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
|
||||
|
@@ -315,6 +315,10 @@ The first argument (a capture) must/must not be equal to the second argument
|
||||
The first argument (a capture) must/must not match the regex given in the
|
||||
second argument (a string).
|
||||
|
||||
- `#any-of?`/`#not-any-of?`:
|
||||
The first argument (a capture) must/must not be one of the other arguments
|
||||
(strings).
|
||||
|
||||
Additionally, we support some custom predicates for indent queries:
|
||||
|
||||
- `#not-kind-eq?`:
|
||||
|
@@ -54,4 +54,7 @@ The first argument (a capture) must be equal to the second argument
|
||||
The first argument (a capture) must match the regex given in the
|
||||
second argument (a string).
|
||||
|
||||
- `#any-of?` (standard):
|
||||
The first argument (a capture) must be one of the other arguments (strings).
|
||||
|
||||
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
|
||||
|
@@ -44,4 +44,4 @@ doesn't make sense in a navigation context.
|
||||
|
||||
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
|
||||
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
|
||||
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=
|
||||
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+path%3A%2A%2A/textobjects.scm&type=Code&ref=advsearch&l=&l=
|
||||
|
@@ -76,6 +76,15 @@ Releases are available in the `extra` repository:
|
||||
```sh
|
||||
sudo pacman -S helix
|
||||
```
|
||||
|
||||
> 💡 When installed from the `extra` repository, run Helix with `helix` instead of `hx`.
|
||||
>
|
||||
> For example:
|
||||
> ```sh
|
||||
> helix --health
|
||||
> ```
|
||||
> to check health
|
||||
|
||||
Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
|
||||
in the AUR, which builds the master branch.
|
||||
|
||||
@@ -216,12 +225,12 @@ RUSTFLAGS="-C target-feature=-crt-static"
|
||||
|
||||
#### Linux and macOS
|
||||
|
||||
The **runtime** directory is one below the Helix source, so either set a
|
||||
The **runtime** directory is one below the Helix source, so either export a
|
||||
`HELIX_RUNTIME` environment variable to point to that directory and add it to
|
||||
your `~/.bashrc` or equivalent:
|
||||
|
||||
```sh
|
||||
HELIX_RUNTIME=~/src/helix/runtime
|
||||
export HELIX_RUNTIME=~/src/helix/runtime
|
||||
```
|
||||
|
||||
Or, create a symbolic link:
|
||||
|
@@ -12,6 +12,7 @@
|
||||
- [Match mode](#match-mode)
|
||||
- [Window mode](#window-mode)
|
||||
- [Space mode](#space-mode)
|
||||
- [Comment mode](#comment-mode)
|
||||
- [Popup](#popup)
|
||||
- [Unimpaired](#unimpaired)
|
||||
- [Insert mode](#insert-mode)
|
||||
@@ -48,13 +49,13 @@ Normal mode is the default mode when you launch helix. You can return to it from
|
||||
| `T` | Find 'till previous char | `till_prev_char` |
|
||||
| `F` | Find previous char | `find_prev_char` |
|
||||
| `G` | Go to line number `<n>` | `goto_line` |
|
||||
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
|
||||
| `Alt-.` | Repeat last motion (`f`, `t`, `m`, `[` or `]`) | `repeat_last_motion` |
|
||||
| `Home` | Move to the start of the line | `goto_line_start` |
|
||||
| `End` | Move to the end of the line | `goto_line_end` |
|
||||
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
|
||||
| `Ctrl-f`, `PageDown` | Move page down | `page_down` |
|
||||
| `Ctrl-u` | Move half page up | `half_page_up` |
|
||||
| `Ctrl-d` | Move half page down | `half_page_down` |
|
||||
| `Ctrl-u` | Move cursor and page half page up | `page_cursor_half_up` |
|
||||
| `Ctrl-d` | Move cursor and page half page down | `page_cursor_half_down` |
|
||||
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
|
||||
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
|
||||
| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
|
||||
@@ -192,8 +193,8 @@ useful when you're simply looking over text and not actively editing it.
|
||||
| `k`, `up` | Scroll the view upwards | `scroll_up` |
|
||||
| `Ctrl-f`, `PageDown` | Move page down | `page_down` |
|
||||
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
|
||||
| `Ctrl-d` | Move half page down | `half_page_down` |
|
||||
| `Ctrl-u` | Move half page up | `half_page_up` |
|
||||
| `Ctrl-u` | Move cursor and page half page up | `page_cursor_half_up` |
|
||||
| `Ctrl-d` | Move cursor and page half page down | `page_cursor_half_down` |
|
||||
|
||||
#### Goto mode
|
||||
|
||||
@@ -223,6 +224,7 @@ Jumps to various locations.
|
||||
| `.` | Go to last modification in current file | `goto_last_modification` |
|
||||
| `j` | Move down textual (instead of visual) line | `move_line_down` |
|
||||
| `k` | Move up textual (instead of visual) line | `move_line_up` |
|
||||
| `w` | Show labels at each word and select the word that belongs to the entered labels | `goto_word` |
|
||||
|
||||
#### Match mode
|
||||
|
||||
@@ -289,6 +291,9 @@ This layer is a kludge of mappings, mostly pickers.
|
||||
| `h` | Select symbol references (**LSP**) | `select_references_to_symbol_under_cursor` |
|
||||
| `'` | Open last fuzzy picker | `last_picker` |
|
||||
| `w` | Enter [window mode](#window-mode) | N/A |
|
||||
| `c` | Comment/uncomment selections | `toggle_comments` |
|
||||
| `C` | Block comment/uncomment selections | `toggle_block_comments` |
|
||||
| `Alt-c` | Line comment/uncomment selections | `toggle_line_comments` |
|
||||
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
|
||||
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
|
||||
| `y` | Yank selections to clipboard | `yank_to_clipboard` |
|
||||
|
@@ -42,7 +42,7 @@ name = "mylang"
|
||||
scope = "source.mylang"
|
||||
injection-regex = "mylang"
|
||||
file-types = ["mylang", "myl"]
|
||||
comment-token = "#"
|
||||
comment-tokens = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
|
||||
language-servers = [ "mylang-lsp" ]
|
||||
@@ -61,7 +61,8 @@ These configuration keys are available:
|
||||
| `roots` | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
|
||||
| `auto-format` | Whether to autoformat this language when saving |
|
||||
| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
|
||||
| `comment-token` | The token to use as a comment-token |
|
||||
| `comment-tokens` | The tokens to use as a comment token, either a single token `"//"` or an array `["//", "///", "//!"]` (the first token will be used for commenting). Also configurable as `comment-token` for backwards compatibility|
|
||||
| `block-comment-tokens`| The start and end tokens for a multiline comment either an array or single table of `{ start = "/*", end = "*/"}`. The first set of tokens will be used for commenting, any pairs in the array can be uncommented |
|
||||
| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
|
||||
| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
|
||||
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
|
||||
@@ -78,24 +79,26 @@ from the above section. `file-types` is a list of strings or tables, for
|
||||
example:
|
||||
|
||||
```toml
|
||||
file-types = ["Makefile", "toml", { suffix = ".git/config" }]
|
||||
file-types = ["toml", { glob = "Makefile" }, { glob = ".git/config" }, { glob = ".github/workflows/*.yaml" } ]
|
||||
```
|
||||
|
||||
When determining a language configuration to use, Helix searches the file-types
|
||||
with the following priorities:
|
||||
|
||||
1. Exact match: if the filename of a file is an exact match of a string in a
|
||||
`file-types` list, that language wins. In the example above, `"Makefile"`
|
||||
will match against `Makefile` files.
|
||||
2. Extension: if there are no exact matches, any `file-types` string that
|
||||
matches the file extension of a given file wins. In the example above, the
|
||||
`"toml"` matches files like `Cargo.toml` or `languages.toml`.
|
||||
3. Suffix: if there are still no matches, any values in `suffix` tables
|
||||
are checked against the full path of the given file. In the example above,
|
||||
the `{ suffix = ".git/config" }` would match against any `config` files
|
||||
in `.git` directories. Note: `/` is used as the directory separator but is
|
||||
replaced at runtime with the appropriate path separator for the operating
|
||||
system, so this rule would match against `.git\config` files on Windows.
|
||||
1. Glob: values in `glob` tables are checked against the full path of the given
|
||||
file. Globs are standard Unix-style path globs (e.g. the kind you use in Shell)
|
||||
and can be used to match paths for a specific prefix, suffix, directory, etc.
|
||||
In the above example, the `{ glob = "Makefile" }` config would match files
|
||||
with the name `Makefile`, the `{ glob = ".git/config" }` config would match
|
||||
`config` files in `.git` directories, and the `{ glob = ".github/workflows/*.yaml" }`
|
||||
config would match any `yaml` files in `.github/workflow` directories. Note
|
||||
that globs should always use the Unix path separator `/` even on Windows systems;
|
||||
the matcher will automatically take the machine-specific separators into account.
|
||||
If the glob isn't an absolute path or doesn't already start with a glob prefix,
|
||||
`*/` will automatically be added to ensure it matches for any subdirectory.
|
||||
2. Extension: if there are no glob matches, any `file-types` string that matches
|
||||
the file extension of a given file wins. In the example above, the `"toml"`
|
||||
config matches files like `Cargo.toml` or `languages.toml`.
|
||||
|
||||
## Language Server configuration
|
||||
|
||||
@@ -127,6 +130,7 @@ These are the available options for a language server.
|
||||
| `config` | LSP initialization options |
|
||||
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
|
||||
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
|
||||
| `required-root-patterns` | A list of `glob` patterns to look for in the working directory. The language server is started if at least one of them is found. |
|
||||
|
||||
A `format` sub-table within `config` can be used to pass extra formatting options to
|
||||
[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
|
||||
|
@@ -314,6 +314,7 @@ These scopes are used for theming the editor interface:
|
||||
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
|
||||
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
|
||||
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
|
||||
| `ui.virtual.jump-label` | Style for virtual jump labels |
|
||||
| `ui.menu` | Code and command completion menus |
|
||||
| `ui.menu.selected` | Selected autocomplete item |
|
||||
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
|
||||
@@ -333,5 +334,7 @@ These scopes are used for theming the editor interface:
|
||||
| `diagnostic.info` | Diagnostics info (editing area) |
|
||||
| `diagnostic.warning` | Diagnostics warning (editing area) |
|
||||
| `diagnostic.error` | Diagnostics error (editing area) |
|
||||
| `diagnostic.unnecessary` | Diagnostics with unnecessary tag (editing area) |
|
||||
| `diagnostic.deprecated` | Diagnostics with deprecated tag (editing area) |
|
||||
|
||||
[editor-section]: ./configuration.md#editor-section
|
||||
|
@@ -48,6 +48,9 @@
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<releases>
|
||||
<release version="24.03" date="2024-03-30">
|
||||
<url>https://helix-editor.com/news/release-24-03-highlights/</url>
|
||||
</release>
|
||||
<release version="23.10" date="2023-10-24">
|
||||
<url>https://helix-editor.com/news/release-23-10-highlights/</url>
|
||||
</release>
|
||||
|
@@ -5,19 +5,20 @@ _hx() {
|
||||
# $1 command name
|
||||
# $2 word being completed
|
||||
# $3 word preceding
|
||||
COMPREPLY=()
|
||||
|
||||
case "$3" in
|
||||
-g | --grammar)
|
||||
COMPREPLY=($(compgen -W "fetch build" -- $2))
|
||||
COMPREPLY="$(compgen -W 'fetch build' -- $2)"
|
||||
;;
|
||||
--health)
|
||||
local languages=$(hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g')
|
||||
COMPREPLY=($(compgen -W "$languages" -- $2))
|
||||
COMPREPLY="$(compgen -W """$languages""" -- $2)"
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- $2))
|
||||
COMPREPLY="$(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2""")"
|
||||
;;
|
||||
esac
|
||||
} && complete -o filenames -F _hx hx
|
||||
|
||||
local IFS=$'\n'
|
||||
COMPREPLY=($COMPREPLY)
|
||||
} && complete -o filenames -F _hx hx
|
||||
|
BIN
contrib/helix-256p.ico
Normal file
BIN
contrib/helix-256p.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 264 KiB |
24
flake.lock
generated
24
flake.lock
generated
@@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701025348,
|
||||
"narHash": "sha256-42GHmYH+GF7VjwGSt+fVT1CQuNpGanJbNgVHTAZppUM=",
|
||||
"lastModified": 1709610799,
|
||||
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "42afaeb1a0325194a7cdb526332d2cb92fddd07b",
|
||||
"rev": "81c393c776d5379c030607866afef6406ca1be57",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -25,11 +25,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -40,11 +40,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1700794826,
|
||||
"narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=",
|
||||
"lastModified": 1709479366,
|
||||
"narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8",
|
||||
"rev": "b8697e57f10292a6165a20f03d2f42920dfaf973",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -72,11 +72,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701137803,
|
||||
"narHash": "sha256-0LcPAdql5IhQSUXJx3Zna0dYTgdIoYO7zUrsKgiBd04=",
|
||||
"lastModified": 1709604635,
|
||||
"narHash": "sha256-le4fwmWmjGRYWwkho0Gr7mnnZndOOe4XGbLw68OvF40=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "9dd940c967502f844eacea52a61e9596268d4f70",
|
||||
"rev": "e86c0fb5d3a22a5f30d7f64ecad88643fe26449d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
12
grammars.nix
12
grammars.nix
@@ -28,7 +28,17 @@
|
||||
owner = builtins.elemAt match 0;
|
||||
repo = builtins.elemAt match 1;
|
||||
};
|
||||
gitGrammars = builtins.filter isGitGrammar languagesConfig.grammar;
|
||||
# If `use-grammars.only` is set, use only those grammars.
|
||||
# If `use-grammars.except` is set, use all other grammars.
|
||||
# Otherwise use all grammars.
|
||||
useGrammar = grammar:
|
||||
if languagesConfig?use-grammars.only then
|
||||
builtins.elem grammar.name languagesConfig.use-grammars.only
|
||||
else if languagesConfig?use-grammars.except then
|
||||
!(builtins.elem grammar.name languagesConfig.use-grammars.except)
|
||||
else true;
|
||||
grammarsToUse = builtins.filter useGrammar languagesConfig.grammar;
|
||||
gitGrammars = builtins.filter isGitGrammar grammarsToUse;
|
||||
buildGrammar = grammar: let
|
||||
gh = toGitHubFetcher grammar.source.git;
|
||||
sourceGit = builtins.fetchTree {
|
||||
|
@@ -16,12 +16,13 @@ unicode-lines = ["ropey/unicode_lines"]
|
||||
integration = []
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
||||
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
|
||||
smallvec = "1.11"
|
||||
smallvec = "1.13"
|
||||
smartstring = "1.0.1"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-segmentation = "1.11"
|
||||
unicode-width = "0.1"
|
||||
unicode-general-category = "0.6"
|
||||
# slab = "0.4.2"
|
||||
@@ -30,15 +31,15 @@ tree-sitter.workspace = true
|
||||
once_cell = "1.19"
|
||||
arc-swap = "1"
|
||||
regex = "1"
|
||||
bitflags = "2.4"
|
||||
ahash = "0.8.6"
|
||||
bitflags = "2.5"
|
||||
ahash = "0.8.11"
|
||||
hashbrown = { version = "0.14.3", features = ["raw"] }
|
||||
dunce = "1.0"
|
||||
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.7"
|
||||
toml = "0.8"
|
||||
|
||||
imara-diff = "0.1.0"
|
||||
|
||||
@@ -47,11 +48,12 @@ encoding_rs = "0.8"
|
||||
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
|
||||
|
||||
etcetera = "0.8"
|
||||
textwrap = "0.16.0"
|
||||
textwrap = "0.16.1"
|
||||
|
||||
nucleo.workspace = true
|
||||
parking_lot = "0.12"
|
||||
globset = "0.4.14"
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = { version = "1", default-features = false }
|
||||
indoc = "2.0.4"
|
||||
indoc = "2.0.5"
|
||||
|
@@ -1,9 +1,12 @@
|
||||
//! This module contains the functionality toggle comments on lines over the selection
|
||||
//! using the comment character defined in the user's `languages.toml`
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
|
||||
syntax::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril, Transaction,
|
||||
};
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Given text, a comment token, and a set of line indices, returns the following:
|
||||
@@ -22,12 +25,12 @@ fn find_line_comment(
|
||||
) -> (bool, Vec<usize>, usize, usize) {
|
||||
let mut commented = true;
|
||||
let mut to_change = Vec::new();
|
||||
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
|
||||
let mut min = usize::MAX; // minimum col for first_non_whitespace_char
|
||||
let mut margin = 1;
|
||||
let token_len = token.chars().count();
|
||||
for line in lines {
|
||||
let line_slice = text.line(line);
|
||||
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
|
||||
if let Some(pos) = line_slice.first_non_whitespace_char() {
|
||||
let len = line_slice.len_chars();
|
||||
|
||||
if pos < min {
|
||||
@@ -94,6 +97,222 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
|
||||
Transaction::change(doc, changes.into_iter())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CommentChange {
|
||||
Commented {
|
||||
range: Range,
|
||||
start_pos: usize,
|
||||
end_pos: usize,
|
||||
start_margin: bool,
|
||||
end_margin: bool,
|
||||
start_token: String,
|
||||
end_token: String,
|
||||
},
|
||||
Uncommented {
|
||||
range: Range,
|
||||
start_pos: usize,
|
||||
end_pos: usize,
|
||||
start_token: String,
|
||||
end_token: String,
|
||||
},
|
||||
Whitespace {
|
||||
range: Range,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn find_block_comments(
|
||||
tokens: &[BlockCommentToken],
|
||||
text: RopeSlice,
|
||||
selection: &Selection,
|
||||
) -> (bool, Vec<CommentChange>) {
|
||||
let mut commented = true;
|
||||
let mut only_whitespace = true;
|
||||
let mut comment_changes = Vec::with_capacity(selection.len());
|
||||
let default_tokens = tokens.first().cloned().unwrap_or_default();
|
||||
// TODO: check if this can be removed on MSRV bump
|
||||
#[allow(clippy::redundant_clone)]
|
||||
let mut start_token = default_tokens.start.clone();
|
||||
#[allow(clippy::redundant_clone)]
|
||||
let mut end_token = default_tokens.end.clone();
|
||||
|
||||
let mut tokens = tokens.to_vec();
|
||||
// sort the tokens by length, so longer tokens will match first
|
||||
tokens.sort_by(|a, b| {
|
||||
if a.start.len() == b.start.len() {
|
||||
b.end.len().cmp(&a.end.len())
|
||||
} else {
|
||||
b.start.len().cmp(&a.start.len())
|
||||
}
|
||||
});
|
||||
for range in selection {
|
||||
let selection_slice = range.slice(text);
|
||||
if let (Some(start_pos), Some(end_pos)) = (
|
||||
selection_slice.first_non_whitespace_char(),
|
||||
selection_slice.last_non_whitespace_char(),
|
||||
) {
|
||||
let mut line_commented = false;
|
||||
let mut after_start = 0;
|
||||
let mut before_end = 0;
|
||||
let len = (end_pos + 1) - start_pos;
|
||||
|
||||
for BlockCommentToken { start, end } in &tokens {
|
||||
let start_len = start.chars().count();
|
||||
let end_len = end.chars().count();
|
||||
after_start = start_pos + start_len;
|
||||
before_end = end_pos.saturating_sub(end_len);
|
||||
|
||||
if len >= start_len + end_len {
|
||||
let start_fragment = selection_slice.slice(start_pos..after_start);
|
||||
let end_fragment = selection_slice.slice(before_end + 1..end_pos + 1);
|
||||
|
||||
// block commented with these tokens
|
||||
if start_fragment == start.as_str() && end_fragment == end.as_str() {
|
||||
start_token = start.to_string();
|
||||
end_token = end.to_string();
|
||||
line_commented = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !line_commented {
|
||||
comment_changes.push(CommentChange::Uncommented {
|
||||
range: *range,
|
||||
start_pos,
|
||||
end_pos,
|
||||
start_token: default_tokens.start.clone(),
|
||||
end_token: default_tokens.end.clone(),
|
||||
});
|
||||
commented = false;
|
||||
} else {
|
||||
comment_changes.push(CommentChange::Commented {
|
||||
range: *range,
|
||||
start_pos,
|
||||
end_pos,
|
||||
start_margin: selection_slice
|
||||
.get_char(after_start)
|
||||
.map_or(false, |c| c == ' '),
|
||||
end_margin: after_start != before_end
|
||||
&& selection_slice
|
||||
.get_char(before_end)
|
||||
.map_or(false, |c| c == ' '),
|
||||
start_token: start_token.to_string(),
|
||||
end_token: end_token.to_string(),
|
||||
});
|
||||
}
|
||||
only_whitespace = false;
|
||||
} else {
|
||||
comment_changes.push(CommentChange::Whitespace { range: *range });
|
||||
}
|
||||
}
|
||||
if only_whitespace {
|
||||
commented = false;
|
||||
}
|
||||
(commented, comment_changes)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn create_block_comment_transaction(
|
||||
doc: &Rope,
|
||||
selection: &Selection,
|
||||
commented: bool,
|
||||
comment_changes: Vec<CommentChange>,
|
||||
) -> (Transaction, SmallVec<[Range; 1]>) {
|
||||
let mut changes: Vec<Change> = Vec::with_capacity(selection.len() * 2);
|
||||
let mut ranges: SmallVec<[Range; 1]> = SmallVec::with_capacity(selection.len());
|
||||
let mut offs = 0;
|
||||
for change in comment_changes {
|
||||
if commented {
|
||||
if let CommentChange::Commented {
|
||||
range,
|
||||
start_pos,
|
||||
end_pos,
|
||||
start_token,
|
||||
end_token,
|
||||
start_margin,
|
||||
end_margin,
|
||||
} = change
|
||||
{
|
||||
let from = range.from();
|
||||
changes.push((
|
||||
from + start_pos,
|
||||
from + start_pos + start_token.len() + start_margin as usize,
|
||||
None,
|
||||
));
|
||||
changes.push((
|
||||
from + end_pos - end_token.len() - end_margin as usize + 1,
|
||||
from + end_pos + 1,
|
||||
None,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// uncommented so manually map ranges through changes
|
||||
match change {
|
||||
CommentChange::Uncommented {
|
||||
range,
|
||||
start_pos,
|
||||
end_pos,
|
||||
start_token,
|
||||
end_token,
|
||||
} => {
|
||||
let from = range.from();
|
||||
changes.push((
|
||||
from + start_pos,
|
||||
from + start_pos,
|
||||
Some(Tendril::from(format!("{} ", start_token))),
|
||||
));
|
||||
changes.push((
|
||||
from + end_pos + 1,
|
||||
from + end_pos + 1,
|
||||
Some(Tendril::from(format!(" {}", end_token))),
|
||||
));
|
||||
|
||||
let offset = start_token.chars().count() + end_token.chars().count() + 2;
|
||||
ranges.push(
|
||||
Range::new(from + offs, from + offs + end_pos + 1 + offset)
|
||||
.with_direction(range.direction()),
|
||||
);
|
||||
offs += offset;
|
||||
}
|
||||
CommentChange::Commented { range, .. } | CommentChange::Whitespace { range } => {
|
||||
ranges.push(Range::new(range.from() + offs, range.to() + offs));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Transaction::change(doc, changes.into_iter()), ranges)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn toggle_block_comments(
|
||||
doc: &Rope,
|
||||
selection: &Selection,
|
||||
tokens: &[BlockCommentToken],
|
||||
) -> Transaction {
|
||||
let text = doc.slice(..);
|
||||
let (commented, comment_changes) = find_block_comments(tokens, text, selection);
|
||||
let (mut transaction, ranges) =
|
||||
create_block_comment_transaction(doc, selection, commented, comment_changes);
|
||||
if !commented {
|
||||
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
|
||||
}
|
||||
transaction
|
||||
}
|
||||
|
||||
pub fn split_lines_of_selection(text: RopeSlice, selection: &Selection) -> Selection {
|
||||
let mut ranges = SmallVec::new();
|
||||
for range in selection.ranges() {
|
||||
let (line_start, line_end) = range.line_range(text.slice(..));
|
||||
let mut pos = text.line_to_char(line_start);
|
||||
for line in text.slice(pos..text.line_to_char(line_end + 1)).lines() {
|
||||
let start = pos;
|
||||
pos += line.len_chars();
|
||||
ranges.push(Range::new(start, pos));
|
||||
}
|
||||
}
|
||||
Selection::new(ranges, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
@@ -149,4 +368,49 @@ mod test {
|
||||
|
||||
// TODO: account for uncommenting with uneven comment indentation
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_block_comments() {
|
||||
// three lines 5 characters.
|
||||
let mut doc = Rope::from("1\n2\n3");
|
||||
// select whole document
|
||||
let selection = Selection::single(0, doc.len_chars());
|
||||
|
||||
let text = doc.slice(..);
|
||||
|
||||
let res = find_block_comments(&[BlockCommentToken::default()], text, &selection);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
(
|
||||
false,
|
||||
vec![CommentChange::Uncommented {
|
||||
range: Range::new(0, 5),
|
||||
start_pos: 0,
|
||||
end_pos: 4,
|
||||
start_token: "/*".to_string(),
|
||||
end_token: "*/".to_string(),
|
||||
}]
|
||||
)
|
||||
);
|
||||
|
||||
// comment
|
||||
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
|
||||
transaction.apply(&mut doc);
|
||||
|
||||
assert_eq!(doc, "/* 1\n2\n3 */");
|
||||
|
||||
// uncomment
|
||||
let selection = Selection::single(0, doc.len_chars());
|
||||
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
|
||||
transaction.apply(&mut doc);
|
||||
assert_eq!(doc, "1\n2\n3");
|
||||
|
||||
// don't panic when there is just a space in comment
|
||||
doc = Rope::from("/* */");
|
||||
let selection = Selection::single(0, doc.len_chars());
|
||||
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
|
||||
transaction.apply(&mut doc);
|
||||
assert_eq!(doc, "");
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,45 @@
|
||||
/// Syntax configuration loader based on built-in languages.toml.
|
||||
pub fn default_syntax_loader() -> crate::syntax::Configuration {
|
||||
use crate::syntax::{Configuration, Loader, LoaderError};
|
||||
|
||||
/// Language configuration based on built-in languages.toml.
|
||||
pub fn default_lang_config() -> Configuration {
|
||||
helix_loader::config::default_lang_config()
|
||||
.try_into()
|
||||
.expect("Could not serialize built-in languages.toml")
|
||||
.expect("Could not deserialize built-in languages.toml")
|
||||
}
|
||||
/// Syntax configuration loader based on user configured languages.toml.
|
||||
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
|
||||
|
||||
/// Language configuration loader based on built-in languages.toml.
|
||||
pub fn default_lang_loader() -> Loader {
|
||||
Loader::new(default_lang_config()).expect("Could not compile loader for default config")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LanguageLoaderError {
|
||||
DeserializeError(toml::de::Error),
|
||||
LoaderError(LoaderError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LanguageLoaderError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::DeserializeError(err) => write!(f, "Failed to parse language config: {err}"),
|
||||
Self::LoaderError(err) => write!(f, "Failed to compile language config: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LanguageLoaderError {}
|
||||
|
||||
/// Language configuration based on user configured languages.toml.
|
||||
pub fn user_lang_config() -> Result<Configuration, toml::de::Error> {
|
||||
helix_loader::config::user_lang_config()?.try_into()
|
||||
}
|
||||
|
||||
/// Language configuration loader based on user configured languages.toml.
|
||||
pub fn user_lang_loader() -> Result<Loader, LanguageLoaderError> {
|
||||
let config: Configuration = helix_loader::config::user_lang_config()
|
||||
.map_err(LanguageLoaderError::DeserializeError)?
|
||||
.try_into()
|
||||
.map_err(LanguageLoaderError::DeserializeError)?;
|
||||
|
||||
Loader::new(config).map_err(LanguageLoaderError::LoaderError)
|
||||
}
|
||||
|
@@ -116,7 +116,7 @@ impl Default for TextFormat {
|
||||
#[derive(Debug)]
|
||||
pub struct DocumentFormatter<'t> {
|
||||
text_fmt: &'t TextFormat,
|
||||
annotations: &'t TextAnnotations,
|
||||
annotations: &'t TextAnnotations<'t>,
|
||||
|
||||
/// The visual position at the end of the last yielded word boundary
|
||||
visual_pos: Position,
|
||||
|
@@ -1,5 +1,3 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::doc_formatter::{DocumentFormatter, TextFormat};
|
||||
use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
|
||||
|
||||
@@ -105,7 +103,7 @@ fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay
|
||||
DocumentFormatter::new_at_prev_checkpoint(
|
||||
text.into(),
|
||||
&TextFormat::new_test(softwrap),
|
||||
TextAnnotations::default().add_overlay(overlays.into(), None),
|
||||
TextAnnotations::default().add_overlay(overlays, None),
|
||||
char_pos,
|
||||
)
|
||||
.0
|
||||
@@ -142,7 +140,7 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -
|
||||
DocumentFormatter::new_at_prev_checkpoint(
|
||||
text.into(),
|
||||
&TextFormat::new_test(softwrap),
|
||||
TextAnnotations::default().add_inline_annotations(annotations.into(), None),
|
||||
TextAnnotations::default().add_inline_annotations(annotations, None),
|
||||
0,
|
||||
)
|
||||
.0
|
||||
@@ -164,15 +162,24 @@ fn annotation() {
|
||||
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotation_and_overlay() {
|
||||
let annotations = [InlineAnnotation {
|
||||
char_idx: 0,
|
||||
text: "fooo".into(),
|
||||
}];
|
||||
let overlay = [Overlay {
|
||||
char_idx: 0,
|
||||
grapheme: "\t".into(),
|
||||
}];
|
||||
assert_eq!(
|
||||
DocumentFormatter::new_at_prev_checkpoint(
|
||||
"bbar".into(),
|
||||
&TextFormat::new_test(false),
|
||||
TextAnnotations::default()
|
||||
.add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None)
|
||||
.add_overlay(Rc::new([Overlay::new(0, "\t")]), None),
|
||||
.add_inline_annotations(annotations.as_slice(), None)
|
||||
.add_overlay(overlay.as_slice(), None),
|
||||
0,
|
||||
)
|
||||
.0
|
||||
|
@@ -425,6 +425,85 @@ impl<'a> Iterator for RopeGraphemes<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the graphemes of a `RopeSlice` in reverse.
|
||||
#[derive(Clone)]
|
||||
pub struct RevRopeGraphemes<'a> {
|
||||
text: RopeSlice<'a>,
|
||||
chunks: Chunks<'a>,
|
||||
cur_chunk: &'a str,
|
||||
cur_chunk_start: usize,
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for RevRopeGraphemes<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RevRopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RevRopeGraphemes<'a> {
|
||||
#[must_use]
|
||||
pub fn new(slice: RopeSlice) -> RevRopeGraphemes {
|
||||
let (mut chunks, mut cur_chunk_start, _, _) = slice.chunks_at_byte(slice.len_bytes());
|
||||
chunks.reverse();
|
||||
let first_chunk = chunks.next().unwrap_or("");
|
||||
cur_chunk_start -= first_chunk.len();
|
||||
RevRopeGraphemes {
|
||||
text: slice,
|
||||
chunks,
|
||||
cur_chunk: first_chunk,
|
||||
cur_chunk_start,
|
||||
cursor: GraphemeCursor::new(slice.len_bytes(), slice.len_bytes(), true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RevRopeGraphemes<'a> {
|
||||
type Item = RopeSlice<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<RopeSlice<'a>> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
|
||||
{
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
Ok(Some(n)) => {
|
||||
b = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::PrevChunk) => {
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
self.cur_chunk_start -= self.cur_chunk.len();
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a >= self.cur_chunk_start + self.cur_chunk.len() {
|
||||
Some(self.text.byte_slice(b..a))
|
||||
} else {
|
||||
let a2 = a - self.cur_chunk_start;
|
||||
let b2 = b - self.cur_chunk_start;
|
||||
Some((&self.cur_chunk[b2..a2]).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A highly compressed Cow<'a, str> that holds
|
||||
/// atmost u31::MAX bytes and is readonly
|
||||
pub struct GraphemeStr<'a> {
|
||||
|
@@ -27,7 +27,7 @@ pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
|
||||
let date_time = NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?;
|
||||
Some(
|
||||
date_time
|
||||
.checked_add_signed(Duration::minutes(amount))?
|
||||
.checked_add_signed(Duration::try_minutes(amount)?)?
|
||||
.format(format.fmt)
|
||||
.to_string(),
|
||||
)
|
||||
@@ -35,14 +35,15 @@ pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
|
||||
(true, false) => {
|
||||
let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
|
||||
Some(
|
||||
date.checked_add_signed(Duration::days(amount))?
|
||||
date.checked_add_signed(Duration::try_days(amount)?)?
|
||||
.format(format.fmt)
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
(false, true) => {
|
||||
let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
|
||||
let (adjusted_time, _) = time.overflowing_add_signed(Duration::minutes(amount));
|
||||
let (adjusted_time, _) =
|
||||
time.overflowing_add_signed(Duration::try_minutes(amount)?);
|
||||
Some(adjusted_time.format(format.fmt).to_string())
|
||||
}
|
||||
(false, false) => None,
|
||||
|
@@ -1,10 +1,10 @@
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
|
||||
|
||||
use crate::{
|
||||
chars::{char_is_line_ending, char_is_whitespace},
|
||||
find_first_non_whitespace_char,
|
||||
graphemes::{grapheme_width, tab_width_at},
|
||||
syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
|
||||
tree_sitter::Node,
|
||||
@@ -551,7 +551,7 @@ fn query_indents<'a>(
|
||||
// The row/column position of the optional anchor in this query
|
||||
let mut anchor: Option<tree_sitter::Node> = None;
|
||||
for capture in m.captures {
|
||||
let capture_name = query.capture_names()[capture.index as usize].as_str();
|
||||
let capture_name = query.capture_names()[capture.index as usize];
|
||||
let capture_type = match capture_name {
|
||||
"indent" => IndentCaptureType::Indent,
|
||||
"indent.always" => IndentCaptureType::IndentAlways,
|
||||
@@ -970,7 +970,7 @@ pub fn indent_for_newline(
|
||||
let mut num_attempts = 0;
|
||||
for line_idx in (0..=line_before).rev() {
|
||||
let line = text.line(line_idx);
|
||||
let first_non_whitespace_char = match find_first_non_whitespace_char(line) {
|
||||
let first_non_whitespace_char = match line.first_non_whitespace_char() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
continue;
|
||||
|
@@ -17,7 +17,6 @@ pub mod macros;
|
||||
pub mod match_brackets;
|
||||
pub mod movement;
|
||||
pub mod object;
|
||||
pub mod path;
|
||||
mod position;
|
||||
pub mod search;
|
||||
pub mod selection;
|
||||
@@ -38,9 +37,6 @@ pub mod unicode {
|
||||
|
||||
pub use helix_loader::find_workspace;
|
||||
|
||||
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
|
||||
line.chars().position(|ch| !ch.is_whitespace())
|
||||
}
|
||||
mod rope_reader;
|
||||
|
||||
pub use rope_reader::RopeReader;
|
||||
|
@@ -57,10 +57,10 @@ fn find_pair(
|
||||
pos_: usize,
|
||||
traverse_parents: bool,
|
||||
) -> Option<usize> {
|
||||
let tree = syntax.tree();
|
||||
let pos = doc.char_to_byte(pos_);
|
||||
|
||||
let mut node = tree.root_node().descendant_for_byte_range(pos, pos)?;
|
||||
let root = syntax.tree_for_byte_range(pos, pos + 1).root_node();
|
||||
let mut node = root.descendant_for_byte_range(pos, pos + 1)?;
|
||||
|
||||
loop {
|
||||
if node.is_named() {
|
||||
@@ -118,7 +118,7 @@ fn find_pair(
|
||||
};
|
||||
node = parent;
|
||||
}
|
||||
let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
|
||||
let node = root.named_descendant_for_byte_range(pos, pos + 1)?;
|
||||
if node.child_count() != 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -141,7 +141,7 @@ fn find_pair(
|
||||
#[must_use]
|
||||
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
|
||||
// Don't do anything when the cursor is not on top of a bracket.
|
||||
let bracket = doc.char(cursor_pos);
|
||||
let bracket = doc.get_char(cursor_pos)?;
|
||||
if !is_valid_bracket(bracket) {
|
||||
return None;
|
||||
}
|
||||
@@ -265,6 +265,12 @@ fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_matching_bracket_empty_file() {
|
||||
let actual = find_matching_bracket_plaintext("".into(), 0);
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_matching_bracket_current_line_plaintext() {
|
||||
let assert = |input: &str, pos, expected| {
|
||||
|
@@ -573,16 +573,11 @@ pub fn move_parent_node_end(
|
||||
dir: Direction,
|
||||
movement: Movement,
|
||||
) -> Selection {
|
||||
let tree = syntax.tree();
|
||||
|
||||
selection.transform(|range| {
|
||||
let start_from = text.char_to_byte(range.from());
|
||||
let start_to = text.char_to_byte(range.to());
|
||||
|
||||
let mut node = match tree
|
||||
.root_node()
|
||||
.named_descendant_for_byte_range(start_from, start_to)
|
||||
{
|
||||
let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) {
|
||||
Some(node) => node,
|
||||
None => {
|
||||
log::debug!(
|
||||
|
@@ -1,42 +1,52 @@
|
||||
use crate::{Range, RopeSlice, Selection, Syntax};
|
||||
use tree_sitter::Node;
|
||||
use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
|
||||
|
||||
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |mut node, from, to| {
|
||||
while node.start_byte() == from && node.end_byte() == to {
|
||||
node = node.parent()?;
|
||||
let cursor = &mut syntax.walk();
|
||||
|
||||
selection.transform(|range| {
|
||||
let from = text.char_to_byte(range.from());
|
||||
let to = text.char_to_byte(range.to());
|
||||
|
||||
let byte_range = from..to;
|
||||
cursor.reset_to_byte_range(from, to);
|
||||
|
||||
while cursor.node().byte_range() == byte_range {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
Some(node)
|
||||
}
|
||||
|
||||
let node = cursor.node();
|
||||
let from = text.byte_to_char(node.start_byte());
|
||||
let to = text.byte_to_char(node.end_byte());
|
||||
|
||||
Range::new(to, from).with_direction(range.direction())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
|
||||
descendant.child(0).or(Some(descendant))
|
||||
select_node_impl(syntax, text, selection, |cursor| {
|
||||
cursor.goto_first_child();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_sibling<F>(
|
||||
syntax: &Syntax,
|
||||
text: RopeSlice,
|
||||
selection: Selection,
|
||||
sibling_fn: &F,
|
||||
) -> Selection
|
||||
where
|
||||
F: Fn(Node) -> Option<Node>,
|
||||
{
|
||||
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
|
||||
find_sibling_recursive(descendant, sibling_fn)
|
||||
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |cursor| {
|
||||
while !cursor.goto_next_sibling() {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
|
||||
where
|
||||
F: Fn(Node) -> Option<Node>,
|
||||
{
|
||||
sibling_fn(node).or_else(|| {
|
||||
node.parent()
|
||||
.and_then(|node| find_sibling_recursive(node, sibling_fn))
|
||||
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |cursor| {
|
||||
while !cursor.goto_prev_sibling() {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,33 +54,25 @@ fn select_node_impl<F>(
|
||||
syntax: &Syntax,
|
||||
text: RopeSlice,
|
||||
selection: Selection,
|
||||
select_fn: F,
|
||||
motion: F,
|
||||
) -> Selection
|
||||
where
|
||||
F: Fn(Node, usize, usize) -> Option<Node>,
|
||||
F: Fn(&mut TreeCursor),
|
||||
{
|
||||
let tree = syntax.tree();
|
||||
let cursor = &mut syntax.walk();
|
||||
|
||||
selection.transform(|range| {
|
||||
let from = text.char_to_byte(range.from());
|
||||
let to = text.char_to_byte(range.to());
|
||||
|
||||
let node = match tree
|
||||
.root_node()
|
||||
.descendant_for_byte_range(from, to)
|
||||
.and_then(|node| select_fn(node, from, to))
|
||||
{
|
||||
Some(node) => node,
|
||||
None => return range,
|
||||
};
|
||||
cursor.reset_to_byte_range(from, to);
|
||||
|
||||
motion(cursor);
|
||||
|
||||
let node = cursor.node();
|
||||
let from = text.byte_to_char(node.start_byte());
|
||||
let to = text.byte_to_char(node.end_byte());
|
||||
|
||||
if range.head < range.anchor {
|
||||
Range::new(to, from)
|
||||
} else {
|
||||
Range::new(from, to)
|
||||
}
|
||||
Range::new(from, to).with_direction(range.direction())
|
||||
})
|
||||
}
|
||||
|
@@ -1,162 +0,0 @@
|
||||
use etcetera::home_dir;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
/// Replaces users home directory from `path` with tilde `~` if the directory
|
||||
/// is available, otherwise returns the path unchanged.
|
||||
pub fn fold_home_dir(path: &Path) -> PathBuf {
|
||||
if let Ok(home) = home_dir() {
|
||||
if let Ok(stripped) = path.strip_prefix(&home) {
|
||||
return PathBuf::from("~").join(stripped);
|
||||
}
|
||||
}
|
||||
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Expands tilde `~` into users home directory if available, otherwise returns the path
|
||||
/// unchanged. The tilde will only be expanded when present as the first component of the path
|
||||
/// and only slash follows it.
|
||||
pub fn expand_tilde(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
if let Some(Component::Normal(c)) = components.peek() {
|
||||
if c == &"~" {
|
||||
if let Ok(home) = home_dir() {
|
||||
// it's ok to unwrap, the path starts with `~`
|
||||
return home.join(path.strip_prefix("~").unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// 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 get_normalized_path(path: &Path) -> PathBuf {
|
||||
// normalization strategy is to canonicalize first ancestor path that exists (i.e., canonicalize as much as possible),
|
||||
// then run handrolled normalization on the non-existent remainder
|
||||
let (base, path) = path
|
||||
.ancestors()
|
||||
.find_map(|base| {
|
||||
let canonicalized_base = dunce::canonicalize(base).ok()?;
|
||||
let remainder = path.strip_prefix(base).ok()?.into();
|
||||
Some((canonicalized_base, remainder))
|
||||
})
|
||||
.unwrap_or_else(|| (PathBuf::new(), PathBuf::from(path)));
|
||||
|
||||
if path.as_os_str().is_empty() {
|
||||
return base;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
base.join(ret)
|
||||
}
|
||||
|
||||
/// Returns the canonical, absolute form of a path with all intermediate components normalized.
|
||||
///
|
||||
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
|
||||
/// here if the path exists, just normalize it's components.
|
||||
pub fn get_canonicalized_path(path: &Path) -> PathBuf {
|
||||
let path = expand_tilde(path);
|
||||
let path = if path.is_relative() {
|
||||
helix_loader::current_working_dir().join(path)
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
get_normalized_path(path.as_path())
|
||||
}
|
||||
|
||||
pub fn get_relative_path(path: &Path) -> PathBuf {
|
||||
let path = PathBuf::from(path);
|
||||
let path = if path.is_absolute() {
|
||||
let cwdir = get_normalized_path(&helix_loader::current_working_dir());
|
||||
get_normalized_path(&path)
|
||||
.strip_prefix(cwdir)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(path)
|
||||
} else {
|
||||
path
|
||||
};
|
||||
fold_home_dir(&path)
|
||||
}
|
||||
|
||||
/// Returns a truncated filepath where the basepart of the path is reduced to the first
|
||||
/// char of the folder and the whole filename appended.
|
||||
///
|
||||
/// Also strip the current working directory from the beginning of the path.
|
||||
/// Note that this function does not check if the truncated path is unambiguous.
|
||||
///
|
||||
/// ```
|
||||
/// use helix_core::path::get_truncated_path;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("/home/cnorris/documents/jokes.txt").as_path(),
|
||||
/// Path::new("/h/c/d/jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("jokes.txt").as_path(),
|
||||
/// Path::new("jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("/jokes.txt").as_path(),
|
||||
/// Path::new("/jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("/h/c/d/jokes.txt").as_path(),
|
||||
/// Path::new("/h/c/d/jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
|
||||
/// ```
|
||||
///
|
||||
pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
|
||||
let cwd = helix_loader::current_working_dir();
|
||||
let path = path
|
||||
.as_ref()
|
||||
.strip_prefix(cwd)
|
||||
.unwrap_or_else(|_| path.as_ref());
|
||||
let file = path.file_name().unwrap_or_default();
|
||||
let base = path.parent().unwrap_or_else(|| Path::new(""));
|
||||
let mut ret = PathBuf::new();
|
||||
for d in base {
|
||||
ret.push(
|
||||
d.to_string_lossy()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
ret.push(file);
|
||||
ret
|
||||
}
|
@@ -7,9 +7,11 @@ use crate::{
|
||||
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
|
||||
prev_grapheme_boundary,
|
||||
},
|
||||
line_ending::get_line_ending,
|
||||
movement::Direction,
|
||||
Assoc, ChangeSet, RopeGraphemes, RopeSlice,
|
||||
};
|
||||
use helix_stdx::rope::{self, RopeSliceExt};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -703,17 +705,26 @@ impl IntoIterator for Selection {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range> for Selection {
|
||||
fn from(range: Range) -> Self {
|
||||
Self {
|
||||
ranges: smallvec![range],
|
||||
primary_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: checkSelection -> check if valid for doc length && sorted
|
||||
|
||||
pub fn keep_or_remove_matches(
|
||||
text: RopeSlice,
|
||||
selection: &Selection,
|
||||
regex: &crate::regex::Regex,
|
||||
regex: &rope::Regex,
|
||||
remove: bool,
|
||||
) -> Option<Selection> {
|
||||
let result: SmallVec<_> = selection
|
||||
.iter()
|
||||
.filter(|range| regex.is_match(&range.fragment(text)) ^ remove)
|
||||
.filter(|range| regex.is_match(text.regex_input_at(range.from()..range.to())) ^ remove)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
@@ -724,25 +735,20 @@ pub fn keep_or_remove_matches(
|
||||
None
|
||||
}
|
||||
|
||||
// TODO: support to split on capture #N instead of whole match
|
||||
pub fn select_on_matches(
|
||||
text: RopeSlice,
|
||||
selection: &Selection,
|
||||
regex: &crate::regex::Regex,
|
||||
regex: &rope::Regex,
|
||||
) -> Option<Selection> {
|
||||
let mut result = SmallVec::with_capacity(selection.len());
|
||||
|
||||
for sel in selection {
|
||||
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
|
||||
let fragment = sel.fragment(text);
|
||||
|
||||
let sel_start = sel.from();
|
||||
let start_byte = text.char_to_byte(sel_start);
|
||||
|
||||
for mat in regex.find_iter(&fragment) {
|
||||
for mat in regex.find_iter(text.regex_input_at(sel.from()..sel.to())) {
|
||||
// TODO: retain range direction
|
||||
|
||||
let start = text.byte_to_char(start_byte + mat.start());
|
||||
let end = text.byte_to_char(start_byte + mat.end());
|
||||
let start = text.byte_to_char(mat.start());
|
||||
let end = text.byte_to_char(mat.end());
|
||||
|
||||
let range = Range::new(start, end);
|
||||
// Make sure the match is not right outside of the selection.
|
||||
@@ -761,12 +767,7 @@ pub fn select_on_matches(
|
||||
None
|
||||
}
|
||||
|
||||
// TODO: support to split on capture #N instead of whole match
|
||||
pub fn split_on_matches(
|
||||
text: RopeSlice,
|
||||
selection: &Selection,
|
||||
regex: &crate::regex::Regex,
|
||||
) -> Selection {
|
||||
pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection {
|
||||
let mut result = SmallVec::with_capacity(selection.len());
|
||||
|
||||
for sel in selection {
|
||||
@@ -776,21 +777,47 @@ pub fn split_on_matches(
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
|
||||
let fragment = sel.fragment(text);
|
||||
|
||||
let sel_start = sel.from();
|
||||
let sel_end = sel.to();
|
||||
|
||||
let start_byte = text.char_to_byte(sel_start);
|
||||
|
||||
let mut start = sel_start;
|
||||
|
||||
for mat in regex.find_iter(&fragment) {
|
||||
for line in sel.slice(text).lines() {
|
||||
let Some(line_ending) = get_line_ending(&line) else { break };
|
||||
let line_end = start + line.len_chars();
|
||||
// TODO: retain range direction
|
||||
let end = text.byte_to_char(start_byte + mat.start());
|
||||
result.push(Range::new(start, line_end - line_ending.len_chars()));
|
||||
start = line_end;
|
||||
}
|
||||
|
||||
if start < sel_end {
|
||||
result.push(Range::new(start, sel_end));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: figure out a new primary index
|
||||
Selection::new(result, 0)
|
||||
}
|
||||
|
||||
pub fn split_on_matches(text: RopeSlice, selection: &Selection, regex: &rope::Regex) -> Selection {
|
||||
let mut result = SmallVec::with_capacity(selection.len());
|
||||
|
||||
for sel in selection {
|
||||
// Special case: zero-width selection.
|
||||
if sel.from() == sel.to() {
|
||||
result.push(*sel);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sel_start = sel.from();
|
||||
let sel_end = sel.to();
|
||||
let mut start = sel_start;
|
||||
|
||||
for mat in regex.find_iter(text.regex_input_at(sel_start..sel_end)) {
|
||||
// TODO: retain range direction
|
||||
let end = text.byte_to_char(mat.start());
|
||||
result.push(Range::new(start, end));
|
||||
start = text.byte_to_char(start_byte + mat.end());
|
||||
start = text.byte_to_char(mat.end());
|
||||
}
|
||||
|
||||
if start < sel_end {
|
||||
@@ -1021,14 +1048,12 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_select_on_matches() {
|
||||
use crate::regex::{Regex, RegexBuilder};
|
||||
|
||||
let r = Rope::from_str("Nobody expects the Spanish inquisition");
|
||||
let s = r.slice(..);
|
||||
|
||||
let selection = Selection::single(0, r.len_chars());
|
||||
assert_eq!(
|
||||
select_on_matches(s, &selection, &Regex::new(r"[A-Z][a-z]*").unwrap()),
|
||||
select_on_matches(s, &selection, &rope::Regex::new(r"[A-Z][a-z]*").unwrap()),
|
||||
Some(Selection::new(
|
||||
smallvec![Range::new(0, 6), Range::new(19, 26)],
|
||||
0
|
||||
@@ -1038,8 +1063,14 @@ mod test {
|
||||
let r = Rope::from_str("This\nString\n\ncontains multiple\nlines");
|
||||
let s = r.slice(..);
|
||||
|
||||
let start_of_line = RegexBuilder::new(r"^").multi_line(true).build().unwrap();
|
||||
let end_of_line = RegexBuilder::new(r"$").multi_line(true).build().unwrap();
|
||||
let start_of_line = rope::RegexBuilder::new()
|
||||
.syntax(rope::Config::new().multi_line(true))
|
||||
.build(r"^")
|
||||
.unwrap();
|
||||
let end_of_line = rope::RegexBuilder::new()
|
||||
.syntax(rope::Config::new().multi_line(true))
|
||||
.build(r"$")
|
||||
.unwrap();
|
||||
|
||||
// line without ending
|
||||
assert_eq!(
|
||||
@@ -1077,9 +1108,9 @@ mod test {
|
||||
select_on_matches(
|
||||
s,
|
||||
&Selection::single(0, s.len_chars()),
|
||||
&RegexBuilder::new(r"^[a-z ]*$")
|
||||
.multi_line(true)
|
||||
.build()
|
||||
&rope::RegexBuilder::new()
|
||||
.syntax(rope::Config::new().multi_line(true))
|
||||
.build(r"^[a-z ]*$")
|
||||
.unwrap()
|
||||
),
|
||||
Some(Selection::new(
|
||||
@@ -1171,13 +1202,15 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_split_on_matches() {
|
||||
use crate::regex::Regex;
|
||||
|
||||
let text = Rope::from(" abcd efg wrs xyz 123 456");
|
||||
|
||||
let selection = Selection::new(smallvec![Range::new(0, 9), Range::new(11, 20),], 0);
|
||||
|
||||
let result = split_on_matches(text.slice(..), &selection, &Regex::new(r"\s+").unwrap());
|
||||
let result = split_on_matches(
|
||||
text.slice(..),
|
||||
&selection,
|
||||
&rope::Regex::new(r"\s+").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result.ranges(),
|
||||
|
@@ -167,6 +167,10 @@ fn find_nth_open_pair(
|
||||
mut pos: usize,
|
||||
n: usize,
|
||||
) -> Option<usize> {
|
||||
if pos >= text.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut chars = text.chars_at(pos + 1);
|
||||
|
||||
// Adjusts pos for the first iteration, and handles the case of the
|
||||
@@ -260,7 +264,8 @@ pub fn get_surround_pos(
|
||||
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
|
||||
return Err(Error::CursorOverlap);
|
||||
}
|
||||
change_pos.extend_from_slice(&[open_pos, close_pos]);
|
||||
// ensure the positions are always paired in the forward direction
|
||||
change_pos.extend_from_slice(&[open_pos.min(close_pos), close_pos.max(open_pos)]);
|
||||
}
|
||||
Ok(change_pos)
|
||||
}
|
||||
@@ -382,6 +387,21 @@ mod test {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_nth_closest_pairs_pos_index_range_panic() {
|
||||
#[rustfmt::skip]
|
||||
let (doc, selection, _) =
|
||||
rope_with_selections_and_expectations(
|
||||
"(a)c)",
|
||||
"^^^^^"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
find_nth_closest_pairs_pos(doc.slice(..), selection.primary(), 1),
|
||||
Err(Error::PairNotFound)
|
||||
)
|
||||
}
|
||||
|
||||
// Create a Rope and a matching Selection using a specification language.
|
||||
// ^ is a single-point selection.
|
||||
// _ is an expected index. These are returned as a Vec<usize> for use in assertions.
|
||||
|
@@ -1,3 +1,5 @@
|
||||
mod tree_cursor;
|
||||
|
||||
use crate::{
|
||||
auto_pairs::AutoPairs,
|
||||
chars::char_is_line_ending,
|
||||
@@ -10,7 +12,9 @@ use crate::{
|
||||
use ahash::RandomState;
|
||||
use arc_swap::{ArcSwap, Guard};
|
||||
use bitflags::bitflags;
|
||||
use globset::GlobSet;
|
||||
use hashbrown::raw::RawTable;
|
||||
use helix_stdx::rope::{self, RopeSliceExt};
|
||||
use slotmap::{DefaultKey as LayerId, HopSlotMap};
|
||||
|
||||
use std::{
|
||||
@@ -19,7 +23,7 @@ use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
fmt::{self, Display},
|
||||
hash::{Hash, Hasher},
|
||||
mem::{replace, transmute},
|
||||
mem::replace,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
@@ -30,6 +34,8 @@ use serde::{ser::SerializeSeq, Deserialize, Serialize};
|
||||
|
||||
use helix_loader::grammar::{get_language, load_runtime_file};
|
||||
|
||||
pub use tree_cursor::TreeCursor;
|
||||
|
||||
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
@@ -82,12 +88,6 @@ pub struct Configuration {
|
||||
pub language_server: HashMap<String, LanguageServerConfiguration>,
|
||||
}
|
||||
|
||||
impl Default for Configuration {
|
||||
fn default() -> Self {
|
||||
crate::config::default_syntax_loader()
|
||||
}
|
||||
}
|
||||
|
||||
// largely based on tree-sitter/cli/src/loader.rs
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
@@ -103,7 +103,19 @@ pub struct LanguageConfiguration {
|
||||
pub shebangs: Vec<String>, // interpreter(s) associated with language
|
||||
#[serde(default)]
|
||||
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
|
||||
pub comment_token: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing,
|
||||
deserialize_with = "from_comment_tokens",
|
||||
alias = "comment-token"
|
||||
)]
|
||||
pub comment_tokens: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing,
|
||||
deserialize_with = "from_block_comment_tokens"
|
||||
)]
|
||||
pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
|
||||
pub text_width: Option<usize>,
|
||||
pub soft_wrap: Option<SoftWrap>,
|
||||
|
||||
@@ -164,9 +176,11 @@ pub enum FileType {
|
||||
/// The extension of the file, either the `Path::extension` or the full
|
||||
/// filename if the file does not have an extension.
|
||||
Extension(String),
|
||||
/// The suffix of a file. This is compared to a given file's absolute
|
||||
/// path, so it can be used to detect files based on their directories.
|
||||
Suffix(String),
|
||||
/// A Unix-style path glob. This is compared to the file's absolute path, so
|
||||
/// it can be used to detect files based on their directories. If the glob
|
||||
/// is not an absolute path and does not already start with a glob pattern,
|
||||
/// a glob pattern will be prepended to it.
|
||||
Glob(globset::Glob),
|
||||
}
|
||||
|
||||
impl Serialize for FileType {
|
||||
@@ -178,9 +192,9 @@ impl Serialize for FileType {
|
||||
|
||||
match self {
|
||||
FileType::Extension(extension) => serializer.serialize_str(extension),
|
||||
FileType::Suffix(suffix) => {
|
||||
FileType::Glob(glob) => {
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?;
|
||||
map.serialize_entry("glob", glob.glob())?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
@@ -213,9 +227,20 @@ impl<'de> Deserialize<'de> for FileType {
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
match map.next_entry::<String, String>()? {
|
||||
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
|
||||
suffix.replace('/', std::path::MAIN_SEPARATOR_STR)
|
||||
})),
|
||||
Some((key, mut glob)) if key == "glob" => {
|
||||
// If the glob isn't an absolute path or already starts
|
||||
// with a glob pattern, add a leading glob so we
|
||||
// properly match relative paths.
|
||||
if !glob.starts_with('/') && !glob.starts_with("*/") {
|
||||
glob.insert_str(0, "*/");
|
||||
}
|
||||
|
||||
globset::Glob::new(glob.as_str())
|
||||
.map(FileType::Glob)
|
||||
.map_err(|err| {
|
||||
serde::de::Error::custom(format!("invalid `glob` pattern: {}", err))
|
||||
})
|
||||
}
|
||||
Some((key, _value)) => Err(serde::de::Error::custom(format!(
|
||||
"unknown key in `file-types` list: {}",
|
||||
key
|
||||
@@ -231,6 +256,59 @@ impl<'de> Deserialize<'de> for FileType {
|
||||
}
|
||||
}
|
||||
|
||||
fn from_comment_tokens<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum CommentTokens {
|
||||
Multiple(Vec<String>),
|
||||
Single(String),
|
||||
}
|
||||
Ok(
|
||||
Option::<CommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
|
||||
CommentTokens::Single(val) => vec![val],
|
||||
CommentTokens::Multiple(vals) => vals,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BlockCommentToken {
|
||||
pub start: String,
|
||||
pub end: String,
|
||||
}
|
||||
|
||||
impl Default for BlockCommentToken {
|
||||
fn default() -> Self {
|
||||
BlockCommentToken {
|
||||
start: "/*".to_string(),
|
||||
end: "*/".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_block_comment_tokens<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<BlockCommentToken>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum BlockCommentTokens {
|
||||
Multiple(Vec<BlockCommentToken>),
|
||||
Single(BlockCommentToken),
|
||||
}
|
||||
Ok(
|
||||
Option::<BlockCommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
|
||||
BlockCommentTokens::Single(val) => vec![val],
|
||||
BlockCommentTokens::Multiple(vals) => vals,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LanguageServerFeature {
|
||||
@@ -263,7 +341,7 @@ impl Display for LanguageServerFeature {
|
||||
GotoDeclaration => "goto-declaration",
|
||||
GotoDefinition => "goto-definition",
|
||||
GotoTypeDefinition => "goto-type-definition",
|
||||
GotoReference => "goto-type-definition",
|
||||
GotoReference => "goto-reference",
|
||||
GotoImplementation => "goto-implementation",
|
||||
SignatureHelp => "signature-help",
|
||||
Hover => "hover",
|
||||
@@ -358,6 +436,22 @@ where
|
||||
serializer.end()
|
||||
}
|
||||
|
||||
fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let patterns = Vec::<String>::deserialize(deserializer)?;
|
||||
if patterns.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut builder = globset::GlobSetBuilder::new();
|
||||
for pattern in patterns {
|
||||
let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
|
||||
builder.add(glob);
|
||||
}
|
||||
builder.build().map(Some).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageServerConfiguration {
|
||||
@@ -371,6 +465,12 @@ pub struct LanguageServerConfiguration {
|
||||
pub config: Option<serde_json::Value>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u64,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing,
|
||||
deserialize_with = "deserialize_required_root_patterns"
|
||||
)]
|
||||
pub required_root_patterns: Option<GlobSet>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -709,7 +809,7 @@ impl LanguageConfiguration {
|
||||
if query_text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let lang = self.highlight_config.get()?.as_ref()?.language;
|
||||
let lang = &self.highlight_config.get()?.as_ref()?.language;
|
||||
Query::new(lang, &query_text)
|
||||
.map_err(|e| {
|
||||
log::error!(
|
||||
@@ -752,6 +852,47 @@ pub struct SoftWrap {
|
||||
pub wrap_at_text_width: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FileTypeGlob {
|
||||
glob: globset::Glob,
|
||||
language_id: usize,
|
||||
}
|
||||
|
||||
impl FileTypeGlob {
|
||||
fn new(glob: globset::Glob, language_id: usize) -> Self {
|
||||
Self { glob, language_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FileTypeGlobMatcher {
|
||||
matcher: globset::GlobSet,
|
||||
file_types: Vec<FileTypeGlob>,
|
||||
}
|
||||
|
||||
impl FileTypeGlobMatcher {
|
||||
fn new(file_types: Vec<FileTypeGlob>) -> Result<Self, globset::Error> {
|
||||
let mut builder = globset::GlobSetBuilder::new();
|
||||
for file_type in &file_types {
|
||||
builder.add(file_type.glob.clone());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
matcher: builder.build()?,
|
||||
file_types,
|
||||
})
|
||||
}
|
||||
|
||||
fn language_id_for_path(&self, path: &Path) -> Option<&usize> {
|
||||
self.matcher
|
||||
.matches(path)
|
||||
.iter()
|
||||
.filter_map(|idx| self.file_types.get(*idx))
|
||||
.max_by_key(|file_type| file_type.glob.glob().len())
|
||||
.map(|file_type| &file_type.language_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Expose loader as Lazy<> global since it's always static?
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -759,7 +900,7 @@ pub struct Loader {
|
||||
// highlight_names ?
|
||||
language_configs: Vec<Arc<LanguageConfiguration>>,
|
||||
language_config_ids_by_extension: HashMap<String, usize>, // Vec<usize>
|
||||
language_config_ids_by_suffix: HashMap<String, usize>,
|
||||
language_config_ids_glob_matcher: FileTypeGlobMatcher,
|
||||
language_config_ids_by_shebang: HashMap<String, usize>,
|
||||
|
||||
language_server_configs: HashMap<String, LanguageServerConfiguration>,
|
||||
@@ -767,66 +908,57 @@ pub struct Loader {
|
||||
scopes: ArcSwap<Vec<String>>,
|
||||
}
|
||||
|
||||
pub type LoaderError = globset::Error;
|
||||
|
||||
impl Loader {
|
||||
pub fn new(config: Configuration) -> Self {
|
||||
let mut loader = Self {
|
||||
language_configs: Vec::new(),
|
||||
language_server_configs: config.language_server,
|
||||
language_config_ids_by_extension: HashMap::new(),
|
||||
language_config_ids_by_suffix: HashMap::new(),
|
||||
language_config_ids_by_shebang: HashMap::new(),
|
||||
scopes: ArcSwap::from_pointee(Vec::new()),
|
||||
};
|
||||
pub fn new(config: Configuration) -> Result<Self, LoaderError> {
|
||||
let mut language_configs = Vec::new();
|
||||
let mut language_config_ids_by_extension = HashMap::new();
|
||||
let mut language_config_ids_by_shebang = HashMap::new();
|
||||
let mut file_type_globs = Vec::new();
|
||||
|
||||
for config in config.language {
|
||||
// get the next id
|
||||
let language_id = loader.language_configs.len();
|
||||
let language_id = language_configs.len();
|
||||
|
||||
for file_type in &config.file_types {
|
||||
// entry().or_insert(Vec::new).push(language_id);
|
||||
match file_type {
|
||||
FileType::Extension(extension) => loader
|
||||
.language_config_ids_by_extension
|
||||
.insert(extension.clone(), language_id),
|
||||
FileType::Suffix(suffix) => loader
|
||||
.language_config_ids_by_suffix
|
||||
.insert(suffix.clone(), language_id),
|
||||
FileType::Extension(extension) => {
|
||||
language_config_ids_by_extension.insert(extension.clone(), language_id);
|
||||
}
|
||||
FileType::Glob(glob) => {
|
||||
file_type_globs.push(FileTypeGlob::new(glob.to_owned(), language_id));
|
||||
}
|
||||
};
|
||||
}
|
||||
for shebang in &config.shebangs {
|
||||
loader
|
||||
.language_config_ids_by_shebang
|
||||
.insert(shebang.clone(), language_id);
|
||||
language_config_ids_by_shebang.insert(shebang.clone(), language_id);
|
||||
}
|
||||
|
||||
loader.language_configs.push(Arc::new(config));
|
||||
language_configs.push(Arc::new(config));
|
||||
}
|
||||
|
||||
loader
|
||||
Ok(Self {
|
||||
language_configs,
|
||||
language_config_ids_by_extension,
|
||||
language_config_ids_glob_matcher: FileTypeGlobMatcher::new(file_type_globs)?,
|
||||
language_config_ids_by_shebang,
|
||||
language_server_configs: config.language_server,
|
||||
scopes: ArcSwap::from_pointee(Vec::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
|
||||
// Find all the language configurations that match this file name
|
||||
// or a suffix of the file name.
|
||||
let configuration_id = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.and_then(|file_name| self.language_config_ids_by_extension.get(file_name))
|
||||
let configuration_id = self
|
||||
.language_config_ids_glob_matcher
|
||||
.language_id_for_path(path)
|
||||
.or_else(|| {
|
||||
path.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.and_then(|extension| self.language_config_ids_by_extension.get(extension))
|
||||
})
|
||||
.or_else(|| {
|
||||
self.language_config_ids_by_suffix
|
||||
.iter()
|
||||
.find_map(|(file_type, id)| {
|
||||
if path.to_str()?.ends_with(file_type) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
|
||||
@@ -938,7 +1070,7 @@ thread_local! {
|
||||
pub struct Syntax {
|
||||
layers: HopSlotMap<LayerId, LanguageLayer>,
|
||||
root: LayerId,
|
||||
loader: Arc<Loader>,
|
||||
loader: Arc<ArcSwap<Loader>>,
|
||||
}
|
||||
|
||||
fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> {
|
||||
@@ -949,7 +1081,7 @@ impl Syntax {
|
||||
pub fn new(
|
||||
source: RopeSlice,
|
||||
config: Arc<HighlightConfiguration>,
|
||||
loader: Arc<Loader>,
|
||||
loader: Arc<ArcSwap<Loader>>,
|
||||
) -> Option<Self> {
|
||||
let root_layer = LanguageLayer {
|
||||
tree: None,
|
||||
@@ -962,6 +1094,7 @@ impl Syntax {
|
||||
start_point: Point::new(0, 0),
|
||||
end_point: Point::new(usize::MAX, usize::MAX),
|
||||
}],
|
||||
parent: None,
|
||||
};
|
||||
|
||||
// track scope_descriptor: a Vec of scopes for item in tree
|
||||
@@ -993,9 +1126,10 @@ impl Syntax {
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(self.root);
|
||||
|
||||
let scopes = self.loader.scopes.load();
|
||||
let loader = self.loader.load();
|
||||
let scopes = loader.scopes.load();
|
||||
let injection_callback = |language: &InjectionLanguageMarker| {
|
||||
self.loader
|
||||
loader
|
||||
.language_configuration_for_injection_string(language)
|
||||
.and_then(|language_config| language_config.highlight_config(&scopes))
|
||||
};
|
||||
@@ -1231,6 +1365,7 @@ impl Syntax {
|
||||
depth,
|
||||
ranges,
|
||||
flags: LayerUpdateFlags::empty(),
|
||||
parent: Some(layer_id),
|
||||
};
|
||||
|
||||
// Find an identical existing layer
|
||||
@@ -1338,6 +1473,38 @@ impl Syntax {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn tree_for_byte_range(&self, start: usize, end: usize) -> &Tree {
|
||||
let mut container_id = self.root;
|
||||
|
||||
for (layer_id, layer) in self.layers.iter() {
|
||||
if layer.depth > self.layers[container_id].depth
|
||||
&& layer.contains_byte_range(start, end)
|
||||
{
|
||||
container_id = layer_id;
|
||||
}
|
||||
}
|
||||
|
||||
self.layers[container_id].tree()
|
||||
}
|
||||
|
||||
pub fn named_descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
|
||||
self.tree_for_byte_range(start, end)
|
||||
.root_node()
|
||||
.named_descendant_for_byte_range(start, end)
|
||||
}
|
||||
|
||||
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
|
||||
self.tree_for_byte_range(start, end)
|
||||
.root_node()
|
||||
.descendant_for_byte_range(start, end)
|
||||
}
|
||||
|
||||
pub fn walk(&self) -> TreeCursor<'_> {
|
||||
// data structure to find the smallest range that contains a point
|
||||
// when some of the ranges in the structure can overlap.
|
||||
TreeCursor::new(&self.layers, self.root)
|
||||
}
|
||||
|
||||
// Commenting
|
||||
// comment_strings_for_pos
|
||||
// is_commented
|
||||
@@ -1370,6 +1537,7 @@ pub struct LanguageLayer {
|
||||
pub ranges: Vec<Range>,
|
||||
pub depth: u32,
|
||||
flags: LayerUpdateFlags,
|
||||
parent: Option<LayerId>,
|
||||
}
|
||||
|
||||
/// This PartialEq implementation only checks if that
|
||||
@@ -1389,13 +1557,7 @@ impl PartialEq for LanguageLayer {
|
||||
impl Hash for LanguageLayer {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.depth.hash(state);
|
||||
// The transmute is necessary here because tree_sitter::Language does not derive Hash at the moment.
|
||||
// However it does use #[repr] transparent so the transmute here is safe
|
||||
// as `Language` (which `Grammar` is an alias for) is just a newtype wrapper around a (thin) pointer.
|
||||
// This is also compatible with the PartialEq implementation of language
|
||||
// as that is just a pointer comparison.
|
||||
let language: *const () = unsafe { transmute(self.config.language) };
|
||||
language.hash(state);
|
||||
self.config.language.hash(state);
|
||||
self.ranges.hash(state);
|
||||
}
|
||||
}
|
||||
@@ -1412,7 +1574,7 @@ impl LanguageLayer {
|
||||
.map_err(|_| Error::InvalidRanges)?;
|
||||
|
||||
parser
|
||||
.set_language(self.config.language)
|
||||
.set_language(&self.config.language)
|
||||
.map_err(|_| Error::InvalidLanguage)?;
|
||||
|
||||
// unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
|
||||
@@ -1434,6 +1596,32 @@ impl LanguageLayer {
|
||||
self.tree = Some(tree);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the layer contains the given byte range.
|
||||
///
|
||||
/// If the layer has multiple ranges (i.e. combined injections), the
|
||||
/// given range is considered contained if it is within the start and
|
||||
/// end bytes of the first and last ranges **and** if the given range
|
||||
/// starts or ends within any of the layer's ranges.
|
||||
fn contains_byte_range(&self, start: usize, end: usize) -> bool {
|
||||
let layer_start = self
|
||||
.ranges
|
||||
.first()
|
||||
.expect("ranges should not be empty")
|
||||
.start_byte;
|
||||
let layer_end = self
|
||||
.ranges
|
||||
.last()
|
||||
.expect("ranges should not be empty")
|
||||
.end_byte;
|
||||
|
||||
layer_start <= start
|
||||
&& layer_end >= end
|
||||
&& self.ranges.iter().any(|range| {
|
||||
let byte_range = range.start_byte..range.end_byte;
|
||||
byte_range.contains(&start) || byte_range.contains(&end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_edits(
|
||||
@@ -1545,7 +1733,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::{iter, mem, ops, str, usize};
|
||||
use tree_sitter::{
|
||||
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
|
||||
QueryMatch, Range, TextProvider, Tree, TreeCursor,
|
||||
QueryMatch, Range, TextProvider, Tree,
|
||||
};
|
||||
|
||||
const CANCELLATION_CHECK_INTERVAL: usize = 100;
|
||||
@@ -1686,7 +1874,7 @@ impl HighlightConfiguration {
|
||||
|
||||
// Construct a single query by concatenating the three query strings, but record the
|
||||
// range of pattern indices that belong to each individual string.
|
||||
let query = Query::new(language, &query_source)?;
|
||||
let query = Query::new(&language, &query_source)?;
|
||||
let mut highlights_pattern_index = 0;
|
||||
for i in 0..(query.pattern_count()) {
|
||||
let pattern_offset = query.start_byte_for_pattern(i);
|
||||
@@ -1695,7 +1883,7 @@ impl HighlightConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
let injections_query = Query::new(language, injection_query)?;
|
||||
let injections_query = Query::new(&language, injection_query)?;
|
||||
let combined_injections_patterns = (0..injections_query.pattern_count())
|
||||
.filter(|&i| {
|
||||
injections_query
|
||||
@@ -1727,7 +1915,7 @@ impl HighlightConfiguration {
|
||||
let mut local_scope_capture_index = None;
|
||||
for (i, name) in query.capture_names().iter().enumerate() {
|
||||
let i = Some(i as u32);
|
||||
match name.as_str() {
|
||||
match *name {
|
||||
"local.definition" => local_def_capture_index = i,
|
||||
"local.definition-value" => local_def_value_capture_index = i,
|
||||
"local.reference" => local_ref_capture_index = i,
|
||||
@@ -1738,7 +1926,7 @@ impl HighlightConfiguration {
|
||||
|
||||
for (i, name) in injections_query.capture_names().iter().enumerate() {
|
||||
let i = Some(i as u32);
|
||||
match name.as_str() {
|
||||
match *name {
|
||||
"injection.content" => injection_content_capture_index = i,
|
||||
"injection.language" => injection_language_capture_index = i,
|
||||
"injection.filename" => injection_filename_capture_index = i,
|
||||
@@ -1768,7 +1956,7 @@ impl HighlightConfiguration {
|
||||
}
|
||||
|
||||
/// Get a slice containing all of the highlight names used in the configuration.
|
||||
pub fn names(&self) -> &[String] {
|
||||
pub fn names(&self) -> &[&str] {
|
||||
self.query.capture_names()
|
||||
}
|
||||
|
||||
@@ -1846,11 +2034,16 @@ impl HighlightConfiguration {
|
||||
node_slice
|
||||
};
|
||||
|
||||
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(SHEBANG).unwrap());
|
||||
static SHEBANG_REGEX: Lazy<rope::Regex> =
|
||||
Lazy::new(|| rope::Regex::new(SHEBANG).unwrap());
|
||||
|
||||
injection_capture = SHEBANG_REGEX
|
||||
.captures(&Cow::from(lines))
|
||||
.map(|cap| InjectionLanguageMarker::Shebang(cap[1].to_owned()))
|
||||
.captures_iter(lines.regex_input())
|
||||
.map(|cap| {
|
||||
let cap = lines.byte_slice(cap.get_group(1).unwrap().range());
|
||||
InjectionLanguageMarker::Shebang(cap.into())
|
||||
})
|
||||
.next()
|
||||
} else if index == self.injection_content_capture_index {
|
||||
content_node = Some(capture.node);
|
||||
}
|
||||
@@ -2263,6 +2456,7 @@ impl<'a> Iterator for HighlightIter<'a> {
|
||||
// highlighting patterns that are disabled for local variables.
|
||||
if definition_highlight.is_some() || reference_highlight.is_some() {
|
||||
while layer.config.non_local_variable_patterns[match_.pattern_index] {
|
||||
match_.remove();
|
||||
if let Some((next_match, next_capture_index)) = captures.peek() {
|
||||
let next_capture = next_match.captures[*next_capture_index];
|
||||
if next_capture.node == capture.node {
|
||||
@@ -2473,7 +2667,7 @@ pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result
|
||||
|
||||
fn pretty_print_tree_impl<W: fmt::Write>(
|
||||
fmt: &mut W,
|
||||
cursor: &mut TreeCursor,
|
||||
cursor: &mut tree_sitter::TreeCursor,
|
||||
depth: usize,
|
||||
) -> fmt::Result {
|
||||
let node = cursor.node();
|
||||
@@ -2539,15 +2733,21 @@ mod test {
|
||||
let loader = Loader::new(Configuration {
|
||||
language: vec![],
|
||||
language_server: HashMap::new(),
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
let language = get_language("rust").unwrap();
|
||||
|
||||
let query = Query::new(language, query_str).unwrap();
|
||||
let query = Query::new(&language, query_str).unwrap();
|
||||
let textobject = TextObjectQuery { query };
|
||||
let mut cursor = QueryCursor::new();
|
||||
|
||||
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
|
||||
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
|
||||
let syntax = Syntax::new(
|
||||
source.slice(..),
|
||||
Arc::new(config),
|
||||
Arc::new(ArcSwap::from_pointee(loader)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let root = syntax.tree().root_node();
|
||||
let mut test = |capture, range| {
|
||||
@@ -2601,7 +2801,8 @@ mod test {
|
||||
let loader = Loader::new(Configuration {
|
||||
language: vec![],
|
||||
language_server: HashMap::new(),
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let language = get_language("rust").unwrap();
|
||||
let config = HighlightConfiguration::new(
|
||||
@@ -2621,7 +2822,12 @@ mod test {
|
||||
fn main() {}
|
||||
",
|
||||
);
|
||||
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
|
||||
let syntax = Syntax::new(
|
||||
source.slice(..),
|
||||
Arc::new(config),
|
||||
Arc::new(ArcSwap::from_pointee(loader)),
|
||||
)
|
||||
.unwrap();
|
||||
let tree = syntax.tree();
|
||||
let root = tree.root_node();
|
||||
assert_eq!(root.kind(), "source_file");
|
||||
@@ -2707,11 +2913,17 @@ mod test {
|
||||
let loader = Loader::new(Configuration {
|
||||
language: vec![],
|
||||
language_server: HashMap::new(),
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
let language = get_language(language_name).unwrap();
|
||||
|
||||
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
|
||||
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
|
||||
let syntax = Syntax::new(
|
||||
source.slice(..),
|
||||
Arc::new(config),
|
||||
Arc::new(ArcSwap::from_pointee(loader)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let root = syntax
|
||||
.tree()
|
||||
@@ -2765,7 +2977,7 @@ mod test {
|
||||
// rule but `name` and `body` belong to an unnamed helper `_method_rest`.
|
||||
// This can cause a bug with a pretty-printing implementation that
|
||||
// uses `Node::field_name_for_child` to determine field names but is
|
||||
// fixed when using `TreeCursor::field_name`.
|
||||
// fixed when using `tree_sitter::TreeCursor::field_name`.
|
||||
let source = "def self.method_name
|
||||
true
|
||||
end";
|
||||
|
160
helix-core/src/syntax/tree_cursor.rs
Normal file
160
helix-core/src/syntax/tree_cursor.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use std::{cmp::Reverse, ops::Range};
|
||||
|
||||
use super::{LanguageLayer, LayerId};
|
||||
|
||||
use slotmap::HopSlotMap;
|
||||
use tree_sitter::Node;
|
||||
|
||||
/// The byte range of an injection layer.
|
||||
///
|
||||
/// Injection ranges may overlap, but all overlapping parts are subsets of their parent ranges.
|
||||
/// This allows us to sort the ranges ahead of time in order to efficiently find a range that
|
||||
/// contains a point with maximum depth.
|
||||
#[derive(Debug)]
|
||||
struct InjectionRange {
|
||||
start: usize,
|
||||
end: usize,
|
||||
layer_id: LayerId,
|
||||
depth: u32,
|
||||
}
|
||||
|
||||
pub struct TreeCursor<'a> {
|
||||
layers: &'a HopSlotMap<LayerId, LanguageLayer>,
|
||||
root: LayerId,
|
||||
current: LayerId,
|
||||
injection_ranges: Vec<InjectionRange>,
|
||||
// TODO: Ideally this would be a `tree_sitter::TreeCursor<'a>` but
|
||||
// that returns very surprising results in testing.
|
||||
cursor: Node<'a>,
|
||||
}
|
||||
|
||||
impl<'a> TreeCursor<'a> {
|
||||
pub(super) fn new(layers: &'a HopSlotMap<LayerId, LanguageLayer>, root: LayerId) -> Self {
|
||||
let mut injection_ranges = Vec::new();
|
||||
|
||||
for (layer_id, layer) in layers.iter() {
|
||||
// Skip the root layer
|
||||
if layer.parent.is_none() {
|
||||
continue;
|
||||
}
|
||||
for byte_range in layer.ranges.iter() {
|
||||
let range = InjectionRange {
|
||||
start: byte_range.start_byte,
|
||||
end: byte_range.end_byte,
|
||||
layer_id,
|
||||
depth: layer.depth,
|
||||
};
|
||||
injection_ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
injection_ranges.sort_unstable_by_key(|range| (range.end, Reverse(range.depth)));
|
||||
|
||||
let cursor = layers[root].tree().root_node();
|
||||
|
||||
Self {
|
||||
layers,
|
||||
root,
|
||||
current: root,
|
||||
injection_ranges,
|
||||
cursor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node(&self) -> Node<'a> {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
pub fn goto_parent(&mut self) -> bool {
|
||||
if let Some(parent) = self.node().parent() {
|
||||
self.cursor = parent;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we are already on the root layer, we cannot ascend.
|
||||
if self.current == self.root {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ascend to the parent layer.
|
||||
let range = self.node().byte_range();
|
||||
let parent_id = self.layers[self.current]
|
||||
.parent
|
||||
.expect("non-root layers have a parent");
|
||||
self.current = parent_id;
|
||||
let root = self.layers[self.current].tree().root_node();
|
||||
self.cursor = root
|
||||
.descendant_for_byte_range(range.start, range.end)
|
||||
.unwrap_or(root);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Finds the injection layer that has exactly the same range as the given `range`.
|
||||
fn layer_id_of_byte_range(&self, search_range: Range<usize>) -> Option<LayerId> {
|
||||
let start_idx = self
|
||||
.injection_ranges
|
||||
.partition_point(|range| range.end < search_range.end);
|
||||
|
||||
self.injection_ranges[start_idx..]
|
||||
.iter()
|
||||
.take_while(|range| range.end == search_range.end)
|
||||
.find_map(|range| (range.start == search_range.start).then_some(range.layer_id))
|
||||
}
|
||||
|
||||
pub fn goto_first_child(&mut self) -> bool {
|
||||
// Check if the current node's range is an exact injection layer range.
|
||||
if let Some(layer_id) = self
|
||||
.layer_id_of_byte_range(self.node().byte_range())
|
||||
.filter(|&layer_id| layer_id != self.current)
|
||||
{
|
||||
// Switch to the child layer.
|
||||
self.current = layer_id;
|
||||
self.cursor = self.layers[self.current].tree().root_node();
|
||||
true
|
||||
} else if let Some(child) = self.cursor.child(0) {
|
||||
// Otherwise descend in the current tree.
|
||||
self.cursor = child;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn goto_next_sibling(&mut self) -> bool {
|
||||
if let Some(sibling) = self.cursor.next_sibling() {
|
||||
self.cursor = sibling;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn goto_prev_sibling(&mut self) -> bool {
|
||||
if let Some(sibling) = self.cursor.prev_sibling() {
|
||||
self.cursor = sibling;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the injection layer that contains the given start-end range.
|
||||
fn layer_id_containing_byte_range(&self, start: usize, end: usize) -> LayerId {
|
||||
let start_idx = self
|
||||
.injection_ranges
|
||||
.partition_point(|range| range.end < end);
|
||||
|
||||
self.injection_ranges[start_idx..]
|
||||
.iter()
|
||||
.take_while(|range| range.start < end)
|
||||
.find_map(|range| (range.start <= start).then_some(range.layer_id))
|
||||
.unwrap_or(self.root)
|
||||
}
|
||||
|
||||
pub fn reset_to_byte_range(&mut self, start: usize, end: usize) {
|
||||
self.current = self.layer_id_containing_byte_range(start, end);
|
||||
let root = self.layers[self.current].tree().root_node();
|
||||
self.cursor = root.descendant_for_byte_range(start, end).unwrap_or(root);
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
use std::cell::Cell;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::syntax::Highlight;
|
||||
use crate::Tendril;
|
||||
@@ -92,23 +91,23 @@ pub struct LineAnnotation {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Layer<A, M> {
|
||||
annotations: Rc<[A]>,
|
||||
struct Layer<'a, A, M> {
|
||||
annotations: &'a [A],
|
||||
current_index: Cell<usize>,
|
||||
metadata: M,
|
||||
}
|
||||
|
||||
impl<A, M: Clone> Clone for Layer<A, M> {
|
||||
impl<A, M: Clone> Clone for Layer<'_, A, M> {
|
||||
fn clone(&self) -> Self {
|
||||
Layer {
|
||||
annotations: self.annotations.clone(),
|
||||
annotations: self.annotations,
|
||||
current_index: self.current_index.clone(),
|
||||
metadata: self.metadata.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, M> Layer<A, M> {
|
||||
impl<A, M> Layer<'_, A, M> {
|
||||
pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
|
||||
let new_index = self
|
||||
.annotations
|
||||
@@ -128,8 +127,8 @@ impl<A, M> Layer<A, M> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, M> From<(Rc<[A]>, M)> for Layer<A, M> {
|
||||
fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer<A, M> {
|
||||
impl<'a, A, M> From<(&'a [A], M)> for Layer<'a, A, M> {
|
||||
fn from((annotations, metadata): (&'a [A], M)) -> Layer<A, M> {
|
||||
Layer {
|
||||
annotations,
|
||||
current_index: Cell::new(0),
|
||||
@@ -147,13 +146,13 @@ fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> u
|
||||
/// Annotations that change that is displayed when the document is render.
|
||||
/// Also commonly called virtual text.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct TextAnnotations {
|
||||
inline_annotations: Vec<Layer<InlineAnnotation, Option<Highlight>>>,
|
||||
overlays: Vec<Layer<Overlay, Option<Highlight>>>,
|
||||
line_annotations: Vec<Layer<LineAnnotation, ()>>,
|
||||
pub struct TextAnnotations<'a> {
|
||||
inline_annotations: Vec<Layer<'a, InlineAnnotation, Option<Highlight>>>,
|
||||
overlays: Vec<Layer<'a, Overlay, Option<Highlight>>>,
|
||||
line_annotations: Vec<Layer<'a, LineAnnotation, ()>>,
|
||||
}
|
||||
|
||||
impl TextAnnotations {
|
||||
impl<'a> TextAnnotations<'a> {
|
||||
/// Prepare the TextAnnotations for iteration starting at char_idx
|
||||
pub fn reset_pos(&self, char_idx: usize) {
|
||||
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
|
||||
@@ -194,7 +193,7 @@ impl TextAnnotations {
|
||||
/// the annotations that belong to the layers added first will be shown first.
|
||||
pub fn add_inline_annotations(
|
||||
&mut self,
|
||||
layer: Rc<[InlineAnnotation]>,
|
||||
layer: &'a [InlineAnnotation],
|
||||
highlight: Option<Highlight>,
|
||||
) -> &mut Self {
|
||||
self.inline_annotations.push((layer, highlight).into());
|
||||
@@ -211,7 +210,7 @@ impl TextAnnotations {
|
||||
///
|
||||
/// If multiple layers contain overlay at the same position
|
||||
/// the overlay from the layer added last will be show.
|
||||
pub fn add_overlay(&mut self, layer: Rc<[Overlay]>, highlight: Option<Highlight>) -> &mut Self {
|
||||
pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option<Highlight>) -> &mut Self {
|
||||
self.overlays.push((layer, highlight).into());
|
||||
self
|
||||
}
|
||||
@@ -220,7 +219,7 @@ impl TextAnnotations {
|
||||
///
|
||||
/// The line annotations **must be sorted** by their `char_idx`.
|
||||
/// Multiple line annotations with the same `char_idx` **are not allowed**.
|
||||
pub fn add_line_annotation(&mut self, layer: Rc<[LineAnnotation]>) -> &mut Self {
|
||||
pub fn add_line_annotation(&mut self, layer: &'a [LineAnnotation]) -> &mut Self {
|
||||
self.line_annotations.push((layer, ()).into());
|
||||
self
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use helix_core::{
|
||||
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
|
||||
syntax::{Configuration, Loader},
|
||||
Syntax,
|
||||
};
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use ropey::Rope;
|
||||
use std::{ops::Range, path::PathBuf, process::Command};
|
||||
use std::{ops::Range, path::PathBuf, process::Command, sync::Arc};
|
||||
|
||||
#[test]
|
||||
fn test_treesitter_indent_rust() {
|
||||
@@ -186,7 +188,7 @@ fn test_treesitter_indent(
|
||||
lang_scope: &str,
|
||||
ignored_lines: Vec<std::ops::Range<usize>>,
|
||||
) {
|
||||
let loader = Loader::new(indent_tests_config());
|
||||
let loader = Loader::new(indent_tests_config()).unwrap();
|
||||
|
||||
// set runtime path so we can find the queries
|
||||
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
@@ -197,7 +199,12 @@ fn test_treesitter_indent(
|
||||
let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit);
|
||||
let highlight_config = language_config.highlight_config(&[]).unwrap();
|
||||
let text = doc.slice(..);
|
||||
let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
|
||||
let syntax = Syntax::new(
|
||||
text,
|
||||
highlight_config,
|
||||
Arc::new(ArcSwap::from_pointee(loader)),
|
||||
)
|
||||
.unwrap();
|
||||
let indent_query = language_config.indent_query().unwrap();
|
||||
|
||||
for i in 0..doc.len_lines() {
|
||||
@@ -205,7 +212,7 @@ fn test_treesitter_indent(
|
||||
if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
|
||||
continue;
|
||||
}
|
||||
if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
|
||||
if let Some(pos) = line.first_non_whitespace_char() {
|
||||
let tab_width: usize = 4;
|
||||
let suggested_indent = treesitter_indent_for_pos(
|
||||
indent_query,
|
||||
|
@@ -13,6 +13,7 @@ homepage.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
|
||||
anyhow = "1.0"
|
||||
@@ -21,7 +22,6 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
|
||||
which = "5.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
fern = "0.6"
|
||||
|
@@ -113,7 +113,7 @@ impl Client {
|
||||
id: usize,
|
||||
) -> Result<(Self, UnboundedReceiver<Payload>)> {
|
||||
// Resolve path to the binary
|
||||
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
|
||||
let cmd = helix_stdx::env::which(cmd)?;
|
||||
|
||||
let process = Command::new(cmd)
|
||||
.args(args)
|
||||
|
@@ -19,6 +19,8 @@ pub enum Error {
|
||||
#[error("server closed the stream")]
|
||||
StreamClosed,
|
||||
#[error(transparent)]
|
||||
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
@@ -12,5 +12,18 @@ homepage.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
|
||||
parking_lot = { version = "0.12", features = ["send_guard"] }
|
||||
ahash = "0.8.11"
|
||||
hashbrown = "0.14.0"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
|
||||
# the event registry is essentially read only but must be an rwlock so we can
|
||||
# setup new events on initialization, hardware-lock-elision hugely benefits this case
|
||||
# as it essentially makes the lock entirely free as long as there is no writes
|
||||
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
|
||||
once_cell = "1.18"
|
||||
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
futures-executor = "0.3.28"
|
||||
|
||||
[features]
|
||||
integration_test = []
|
||||
|
19
helix-event/src/cancel.rs
Normal file
19
helix-event/src/cancel.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use std::future::Future;
|
||||
|
||||
pub use oneshot::channel as cancelation;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub type CancelTx = oneshot::Sender<()>;
|
||||
pub type CancelRx = oneshot::Receiver<()>;
|
||||
|
||||
pub async fn cancelable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel => {
|
||||
None
|
||||
}
|
||||
res = future => {
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
}
|
67
helix-event/src/debounce.rs
Normal file
67
helix-event/src/debounce.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! Utilities for declaring an async (usually debounced) hook
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_executor::block_on;
|
||||
use tokio::sync::mpsc::{self, error::TrySendError, Sender};
|
||||
use tokio::time::Instant;
|
||||
|
||||
/// Async hooks provide a convenient framework for implementing (debounced)
|
||||
/// async event handlers. Most synchronous event hooks will likely need to
|
||||
/// debounce their events, coordinate multiple different hooks and potentially
|
||||
/// track some state. `AsyncHooks` facilitate these use cases by running as
|
||||
/// a background tokio task that waits for events (usually an enum) to be
|
||||
/// sent through a channel.
|
||||
pub trait AsyncHook: Sync + Send + 'static + Sized {
|
||||
type Event: Sync + Send + 'static;
|
||||
/// Called immediately whenever an event is received, this function can
|
||||
/// consume the event immediately or debounce it. In case of debouncing,
|
||||
/// it can either define a new debounce timeout or continue the current one
|
||||
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;
|
||||
|
||||
/// Called whenever the debounce timeline is reached
|
||||
fn finish_debounce(&mut self);
|
||||
|
||||
fn spawn(self) -> mpsc::Sender<Self::Event> {
|
||||
// the capacity doesn't matter too much here, unless the cpu is totally overwhelmed
|
||||
// the cap will never be reached since we always immediately drain the channel
|
||||
// so it should only be reached in case of total CPU overload.
|
||||
// However, a bounded channel is much more efficient so it's nice to use here
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
tokio::spawn(run(self, rx));
|
||||
tx
|
||||
}
|
||||
}
|
||||
|
||||
async fn run<Hook: AsyncHook>(mut hook: Hook, mut rx: mpsc::Receiver<Hook::Event>) {
|
||||
let mut deadline = None;
|
||||
loop {
|
||||
let event = match deadline {
|
||||
Some(deadline_) => {
|
||||
let res = tokio::time::timeout_at(deadline_, rx.recv()).await;
|
||||
match res {
|
||||
Ok(event) => event,
|
||||
Err(_) => {
|
||||
hook.finish_debounce();
|
||||
deadline = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => rx.recv().await,
|
||||
};
|
||||
let Some(event) = event else {
|
||||
break;
|
||||
};
|
||||
deadline = hook.handle_event(event, deadline);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_blocking<T>(tx: &Sender<T>, data: T) {
|
||||
// block_on has some overhead and in practice the channel should basically
|
||||
// never be full anyway so first try sending without blocking
|
||||
if let Err(TrySendError::Full(data)) = tx.try_send(data) {
|
||||
// set a timeout so that we just drop a message instead of freezing the editor in the worst case
|
||||
let _ = block_on(tx.send_timeout(data, Duration::from_millis(10)));
|
||||
}
|
||||
}
|
91
helix-event/src/hook.rs
Normal file
91
helix-event/src/hook.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! rust dynamic dispatch is extremely limited so we have to build our
|
||||
//! own vtable implementation. Otherwise implementing the event system would not be possible.
|
||||
//! A nice bonus of this approach is that we can optimize the vtable a bit more. Normally
|
||||
//! a dyn Trait fat pointer contains two pointers: A pointer to the data itself and a
|
||||
//! pointer to a global (static) vtable entry which itself contains multiple other pointers
|
||||
//! (the various functions of the trait, drop, size and align). That makes dynamic
|
||||
//! dispatch pretty slow (double pointer indirections). However, we only have a single function
|
||||
//! in the hook trait and don't need a drop implementation (event system is global anyway
|
||||
//! and never dropped) so we can just store the entire vtable inline.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::ptr::{self, NonNull};
|
||||
|
||||
use crate::Event;
|
||||
|
||||
/// Opaque handle type that represents an erased type parameter.
|
||||
///
|
||||
/// If extern types were stable, this could be implemented as `extern { pub type Opaque; }` but
|
||||
/// until then we can use this.
|
||||
///
|
||||
/// Care should be taken that we don't use a concrete instance of this. It should only be used
|
||||
/// through a reference, so we can maintain something else's lifetime.
|
||||
struct Opaque(());
|
||||
|
||||
pub(crate) struct ErasedHook {
|
||||
data: NonNull<Opaque>,
|
||||
call: unsafe fn(NonNull<Opaque>, NonNull<Opaque>, NonNull<Opaque>),
|
||||
}
|
||||
|
||||
impl ErasedHook {
|
||||
pub(crate) fn new_dynamic<H: Fn() -> Result<()> + 'static + Send + Sync>(
|
||||
hook: H,
|
||||
) -> ErasedHook {
|
||||
unsafe fn call<F: Fn() -> Result<()> + 'static + Send + Sync>(
|
||||
hook: NonNull<Opaque>,
|
||||
_event: NonNull<Opaque>,
|
||||
result: NonNull<Opaque>,
|
||||
) {
|
||||
let hook: NonNull<F> = hook.cast();
|
||||
let result: NonNull<Result<()>> = result.cast();
|
||||
let hook: &F = hook.as_ref();
|
||||
let res = hook();
|
||||
ptr::write(result.as_ptr(), res)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ErasedHook {
|
||||
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
|
||||
call: call::<H>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new<E: Event, F: Fn(&mut E) -> Result<()>>(hook: F) -> ErasedHook {
|
||||
unsafe fn call<E: Event, F: Fn(&mut E) -> Result<()>>(
|
||||
hook: NonNull<Opaque>,
|
||||
event: NonNull<Opaque>,
|
||||
result: NonNull<Opaque>,
|
||||
) {
|
||||
let hook: NonNull<F> = hook.cast();
|
||||
let mut event: NonNull<E> = event.cast();
|
||||
let result: NonNull<Result<()>> = result.cast();
|
||||
let hook: &F = hook.as_ref();
|
||||
let res = hook(event.as_mut());
|
||||
ptr::write(result.as_ptr(), res)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ErasedHook {
|
||||
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
|
||||
call: call::<E, F>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn call<E: Event>(&self, event: &mut E) -> Result<()> {
|
||||
let mut res = Ok(());
|
||||
|
||||
unsafe {
|
||||
(self.call)(
|
||||
self.data,
|
||||
NonNull::from(event).cast(),
|
||||
NonNull::from(&mut res).cast(),
|
||||
);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Sync for ErasedHook {}
|
||||
unsafe impl Send for ErasedHook {}
|
@@ -1,8 +1,203 @@
|
||||
//! `helix-event` contains systems that allow (often async) communication between
|
||||
//! different editor components without strongly coupling them. Currently this
|
||||
//! crate only contains some smaller facilities but the intend is to add more
|
||||
//! functionality in the future ( like a generic hook system)
|
||||
//! different editor components without strongly coupling them. Specifically
|
||||
//! it allows defining synchronous hooks that run when certain editor events
|
||||
//! occur.
|
||||
//!
|
||||
//! The core of the event system are hook callbacks and the [`Event`] trait. A
|
||||
//! hook is essentially just a closure `Fn(event: &mut impl Event) -> Result<()>`
|
||||
//! that gets called every time an appropriate event is dispatched. The implementation
|
||||
//! details of the [`Event`] trait are considered private. The [`events`] macro is
|
||||
//! provided which automatically declares event types. Similarly the `register_hook`
|
||||
//! macro should be used to (safely) declare event hooks.
|
||||
//!
|
||||
//! Hooks run synchronously which can be advantageous since they can modify the
|
||||
//! current editor state right away (for example to immediately hide the completion
|
||||
//! popup). However, they can not contain their own state without locking since
|
||||
//! they only receive immutable references. For handler that want to track state, do
|
||||
//! expensive background computations or debouncing an [`AsyncHook`] is preferable.
|
||||
//! Async hooks are based around a channels that receive events specific to
|
||||
//! that `AsyncHook` (usually an enum). These events can be sent by synchronous
|
||||
//! hooks. Due to some limitations around tokio channels the [`send_blocking`]
|
||||
//! function exported in this crate should be used instead of the builtin
|
||||
//! `blocking_send`.
|
||||
//!
|
||||
//! In addition to the core event system, this crate contains some message queues
|
||||
//! that allow transfer of data back to the main event loop from async hooks and
|
||||
//! hooks that may not have access to all application data (for example in helix-view).
|
||||
//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and
|
||||
//! display status messages ([`status`]).
|
||||
//!
|
||||
//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the
|
||||
//! main loop (including access to the compositor). Ideally that queue will be moved
|
||||
//! to helix-view in the future if we manage to detach the compositor from its rendering backend.
|
||||
|
||||
use anyhow::Result;
|
||||
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
|
||||
pub use debounce::{send_blocking, AsyncHook};
|
||||
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
|
||||
pub use registry::Event;
|
||||
|
||||
mod cancel;
|
||||
mod debounce;
|
||||
mod hook;
|
||||
mod redraw;
|
||||
mod registry;
|
||||
#[doc(hidden)]
|
||||
pub mod runtime;
|
||||
pub mod status;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub fn register_event<E: Event + 'static>() {
|
||||
registry::with_mut(|registry| registry.register_event::<E>())
|
||||
}
|
||||
|
||||
/// Registers a hook that will be called when an event of type `E` is dispatched.
|
||||
/// This function should usually not be used directly, use the [`register_hook`]
|
||||
/// macro instead.
|
||||
///
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `hook` must be totally generic over all lifetime parameters of `E`. For
|
||||
/// example if `E` was a known type `Foo<'a, 'b>`, then the correct trait bound
|
||||
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)`, but there is no way to
|
||||
/// express that kind of constraint for a generic type with the Rust type system
|
||||
/// as of this writing.
|
||||
pub unsafe fn register_hook_raw<E: Event>(
|
||||
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
|
||||
) {
|
||||
registry::with_mut(|registry| registry.register_hook(hook))
|
||||
}
|
||||
|
||||
/// Register a hook solely by event name
|
||||
pub fn register_dynamic_hook(
|
||||
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
|
||||
id: &str,
|
||||
) -> Result<()> {
|
||||
registry::with_mut(|reg| reg.register_dynamic_hook(hook, id))
|
||||
}
|
||||
|
||||
pub fn dispatch(e: impl Event) {
|
||||
registry::with(|registry| registry.dispatch(e));
|
||||
}
|
||||
|
||||
/// Macro to declare events
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ``` no-compile
|
||||
/// events! {
|
||||
/// FileWrite(&Path)
|
||||
/// ViewScrolled{ view: View, new_pos: ViewOffset }
|
||||
/// DocumentChanged<'a> { old_doc: &'a Rope, doc: &'a mut Document, changes: &'a ChangeSet }
|
||||
/// }
|
||||
///
|
||||
/// fn init() {
|
||||
/// register_event::<FileWrite>();
|
||||
/// register_event::<ViewScrolled>();
|
||||
/// register_event::<DocumentChanged>();
|
||||
/// }
|
||||
///
|
||||
/// fn save(path: &Path, content: &str){
|
||||
/// std::fs::write(path, content);
|
||||
/// dispatch(FileWrite(path));
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! events {
|
||||
($name: ident<$($lt: lifetime),*> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
|
||||
pub struct $name<$($lt),*> { $(pub $data: $data_ty),* }
|
||||
unsafe impl<$($lt),*> $crate::Event for $name<$($lt),*> {
|
||||
const ID: &'static str = stringify!($name);
|
||||
const LIFETIMES: usize = $crate::events!(@sum $(1, $lt),*);
|
||||
type Static = $crate::events!(@replace_lt $name, $('static, $lt),*);
|
||||
}
|
||||
$crate::events!{ $($rem)* }
|
||||
};
|
||||
($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
|
||||
pub struct $name { $(pub $data: $data_ty),* }
|
||||
unsafe impl $crate::Event for $name {
|
||||
const ID: &'static str = stringify!($name);
|
||||
const LIFETIMES: usize = 0;
|
||||
type Static = Self;
|
||||
}
|
||||
$crate::events!{ $($rem)* }
|
||||
};
|
||||
() => {};
|
||||
(@replace_lt $name: ident, $($lt1: lifetime, $lt2: lifetime),* ) => {$name<$($lt1),*>};
|
||||
(@sum $($val: expr, $lt1: lifetime),* ) => {0 $(+ $val)*};
|
||||
}
|
||||
|
||||
/// Safely register statically typed event hooks
|
||||
#[macro_export]
|
||||
macro_rules! register_hook {
|
||||
// Safety: this is safe because we fully control the type of the event here and
|
||||
// ensure all lifetime arguments are fully generic and the correct number of lifetime arguments
|
||||
// is present
|
||||
(move |$event:ident: &mut $event_ty: ident<$($lt: lifetime),*>| $body: expr) => {
|
||||
let val = move |$event: &mut $event_ty<$($lt),*>| $body;
|
||||
unsafe {
|
||||
// Lifetimes are a bit of a pain. We want to allow events being
|
||||
// non-static. Lifetimes don't actually exist at runtime so its
|
||||
// fine to essentially transmute the lifetimes as long as we can
|
||||
// prove soundness. The hook must therefore accept any combination
|
||||
// of lifetimes. In other words fn(&'_ mut Event<'_, '_>) is ok
|
||||
// but examples like fn(&'_ mut Event<'_, 'static>) or fn<'a>(&'a
|
||||
// mut Event<'a, 'a>) are not. To make this safe we use a macro to
|
||||
// forbid the user from specifying lifetimes manually (all lifetimes
|
||||
// specified are always function generics and passed to the event so
|
||||
// lifetimes can't be used multiple times and using 'static causes a
|
||||
// syntax error).
|
||||
//
|
||||
// There is one soundness hole tough: Type Aliases allow
|
||||
// "accidentally" creating these problems. For example:
|
||||
//
|
||||
// type Event2 = Event<'static>.
|
||||
// type Event2<'a> = Event<'a, a>.
|
||||
//
|
||||
// These cases can be caught by counting the number of lifetimes
|
||||
// parameters at the parameter declaration site and then at the hook
|
||||
// declaration site. By asserting the number of lifetime parameters
|
||||
// are equal we can catch all bad type aliases under one assumption:
|
||||
// There are no unused lifetime parameters. Introducing a static
|
||||
// would reduce the number of arguments of the alias by one in the
|
||||
// above example Event2 has zero lifetime arguments while the original
|
||||
// event has one lifetime argument. Similar logic applies to using
|
||||
// a lifetime argument multiple times. The ASSERT below performs a
|
||||
// a compile time assertion to ensure exactly this property.
|
||||
//
|
||||
// With unused lifetime arguments it is still one way to cause unsound code:
|
||||
//
|
||||
// type Event2<'a, 'b> = Event<'a, 'a>;
|
||||
//
|
||||
// However, this case will always emit a compiler warning/cause CI
|
||||
// failures so a user would have to introduce #[allow(unused)] which
|
||||
// is easily caught in review (and a very theoretical case anyway).
|
||||
// If we want to be pedantic we can simply compile helix with
|
||||
// forbid(unused). All of this is just a safety net to prevent
|
||||
// very theoretical misuse. This won't come up in real code (and is
|
||||
// easily caught in review).
|
||||
#[allow(unused)]
|
||||
const ASSERT: () = {
|
||||
if <$event_ty as $crate::Event>::LIFETIMES != 0 + $crate::events!(@sum $(1, $lt),*){
|
||||
panic!("invalid type alias");
|
||||
}
|
||||
};
|
||||
$crate::register_hook_raw::<$crate::events!(@replace_lt $event_ty, $('static, $lt),*)>(val);
|
||||
}
|
||||
};
|
||||
(move |$event:ident: &mut $event_ty: ident| $body: expr) => {
|
||||
let val = move |$event: &mut $event_ty| $body;
|
||||
unsafe {
|
||||
#[allow(unused)]
|
||||
const ASSERT: () = {
|
||||
if <$event_ty as $crate::Event>::LIFETIMES != 0{
|
||||
panic!("invalid type alias");
|
||||
}
|
||||
};
|
||||
$crate::register_hook_raw::<$event_ty>(val);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -5,16 +5,20 @@ use std::future::Future;
|
||||
use parking_lot::{RwLock, RwLockReadGuard};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::runtime_local;
|
||||
|
||||
runtime_local! {
|
||||
/// A `Notify` instance that can be used to (asynchronously) request
|
||||
/// the editor the render a new frame.
|
||||
/// the editor to render a new frame.
|
||||
static REDRAW_NOTIFY: Notify = Notify::const_new();
|
||||
|
||||
/// A `RwLock` that prevents the next frame from being
|
||||
/// drawn until an exclusive (write) lock can be acquired.
|
||||
/// This allows asynchsonous tasks to acquire `non-exclusive`
|
||||
/// This allows asynchronous tasks to acquire `non-exclusive`
|
||||
/// locks (read) to prevent the next frame from being drawn
|
||||
/// until a certain computation has finished.
|
||||
static RENDER_LOCK: RwLock<()> = RwLock::new(());
|
||||
}
|
||||
|
||||
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
|
||||
|
||||
|
131
helix-event/src/registry.rs
Normal file
131
helix-event/src/registry.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! A global registry where events are registered and can be
|
||||
//! subscribed to by registering hooks. The registry identifies event
|
||||
//! types using their type name so multiple event with the same type name
|
||||
//! may not be registered (will cause a panic to ensure soundness)
|
||||
|
||||
use std::any::TypeId;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use hashbrown::hash_map::Entry;
|
||||
use hashbrown::HashMap;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::hook::ErasedHook;
|
||||
use crate::runtime_local;
|
||||
|
||||
pub struct Registry {
|
||||
events: HashMap<&'static str, TypeId, ahash::RandomState>,
|
||||
handlers: HashMap<&'static str, Vec<ErasedHook>, ahash::RandomState>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn register_event<E: Event + 'static>(&mut self) {
|
||||
let ty = TypeId::of::<E>();
|
||||
assert_eq!(ty, TypeId::of::<E::Static>());
|
||||
match self.events.entry(E::ID) {
|
||||
Entry::Occupied(entry) => {
|
||||
if entry.get() == &ty {
|
||||
// don't warn during tests to avoid log spam
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
panic!("Event {} was registered multiple times", E::ID);
|
||||
} else {
|
||||
panic!("Multiple events with ID {} were registered", E::ID);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(ent) => {
|
||||
ent.insert(ty);
|
||||
self.handlers.insert(E::ID, Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `hook` must be totally generic over all lifetime parameters of `E`. For
|
||||
/// example if `E` was a known type `Foo<'a, 'b> then the correct trait bound
|
||||
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)` but there is no way to
|
||||
/// express that kind of constraint for a generic type with the rust type system
|
||||
/// right now.
|
||||
pub unsafe fn register_hook<E: Event>(
|
||||
&mut self,
|
||||
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
|
||||
) {
|
||||
// ensure event type ids match so we can rely on them always matching
|
||||
let id = E::ID;
|
||||
let Some(&event_id) = self.events.get(id) else {
|
||||
panic!("Tried to register handler for unknown event {id}");
|
||||
};
|
||||
assert!(
|
||||
TypeId::of::<E::Static>() == event_id,
|
||||
"Tried to register invalid hook for event {id}"
|
||||
);
|
||||
let hook = ErasedHook::new(hook);
|
||||
self.handlers.get_mut(id).unwrap().push(hook);
|
||||
}
|
||||
|
||||
pub fn register_dynamic_hook(
|
||||
&mut self,
|
||||
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
|
||||
id: &str,
|
||||
) -> Result<()> {
|
||||
// ensure event type ids match so we can rely on them always matching
|
||||
if self.events.get(id).is_none() {
|
||||
bail!("Tried to register handler for unknown event {id}");
|
||||
};
|
||||
let hook = ErasedHook::new_dynamic(hook);
|
||||
self.handlers.get_mut(id).unwrap().push(hook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dispatch<E: Event>(&self, mut event: E) {
|
||||
let Some(hooks) = self.handlers.get(E::ID) else {
|
||||
log::error!("Dispatched unknown event {}", E::ID);
|
||||
return;
|
||||
};
|
||||
let event_id = self.events[E::ID];
|
||||
|
||||
assert_eq!(
|
||||
TypeId::of::<E::Static>(),
|
||||
event_id,
|
||||
"Tried to dispatch invalid event {}",
|
||||
E::ID
|
||||
);
|
||||
|
||||
for hook in hooks {
|
||||
// safety: event type is the same
|
||||
if let Err(err) = unsafe { hook.call(&mut event) } {
|
||||
log::error!("{} hook failed: {err:#?}", E::ID);
|
||||
crate::status::report_blocking(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime_local! {
|
||||
static REGISTRY: RwLock<Registry> = RwLock::new(Registry {
|
||||
// hardcoded random number is good enough here we don't care about DOS resistance
|
||||
// and avoids the additional complexity of `Option<Registry>`
|
||||
events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)),
|
||||
handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)),
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn with<T>(f: impl FnOnce(&Registry) -> T) -> T {
|
||||
f(®ISTRY.read())
|
||||
}
|
||||
|
||||
pub(crate) fn with_mut<T>(f: impl FnOnce(&mut Registry) -> T) -> T {
|
||||
f(&mut REGISTRY.write())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// The number of specified lifetimes and the static type *must* be correct.
|
||||
/// This is ensured automatically by the [`events`](crate::events)
|
||||
/// macro.
|
||||
pub unsafe trait Event: Sized {
|
||||
/// Globally unique (case sensitive) string that identifies this type.
|
||||
/// A good candidate is the events type name
|
||||
const ID: &'static str;
|
||||
const LIFETIMES: usize;
|
||||
type Static: Event + 'static;
|
||||
}
|
88
helix-event/src/runtime.rs
Normal file
88
helix-event/src/runtime.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! The event system makes use of global to decouple different systems.
|
||||
//! However, this can cause problems for the integration test system because
|
||||
//! it runs multiple helix applications in parallel. Making the globals
|
||||
//! thread-local does not work because a applications can/does have multiple
|
||||
//! runtime threads. Instead this crate implements a similar notion to a thread
|
||||
//! local but instead of being local to a single thread, the statics are local to
|
||||
//! a single tokio-runtime. The implementation requires locking so it's not exactly efficient.
|
||||
//!
|
||||
//! Therefore this function is only enabled during integration tests and behaves like
|
||||
//! a normal static otherwise. I would prefer this module to be fully private and to only
|
||||
//! export the macro but the macro still need to construct these internals so it's marked
|
||||
//! `doc(hidden)` instead
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
pub struct RuntimeLocal<T: 'static> {
|
||||
/// inner API used in the macro, not part of public API
|
||||
#[doc(hidden)]
|
||||
pub __data: T,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
impl<T> Deref for RuntimeLocal<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.__data
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
#[macro_export]
|
||||
macro_rules! runtime_local {
|
||||
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
|
||||
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal {
|
||||
__data: $init
|
||||
};)*
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
pub struct RuntimeLocal<T: 'static> {
|
||||
data:
|
||||
parking_lot::RwLock<hashbrown::HashMap<tokio::runtime::Id, &'static T, ahash::RandomState>>,
|
||||
init: fn() -> T,
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
impl<T> RuntimeLocal<T> {
|
||||
/// inner API used in the macro, not part of public API
|
||||
#[doc(hidden)]
|
||||
pub const fn __new(init: fn() -> T) -> Self {
|
||||
Self {
|
||||
data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher(
|
||||
ahash::RandomState::with_seeds(423, 9978, 38322, 3280080),
|
||||
)),
|
||||
init,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
impl<T> Deref for RuntimeLocal<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
let id = tokio::runtime::Handle::current().id();
|
||||
let guard = self.data.read();
|
||||
match guard.get(&id) {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
drop(guard);
|
||||
let data = Box::leak(Box::new((self.init)()));
|
||||
let mut guard = self.data.write();
|
||||
guard.insert(id, data);
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
#[macro_export]
|
||||
macro_rules! runtime_local {
|
||||
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
|
||||
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal::__new(|| $init);)*
|
||||
};
|
||||
}
|
68
helix-event/src/status.rs
Normal file
68
helix-event/src/status.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! A queue of async messages/errors that will be shown in the editor
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{runtime_local, send_blocking};
|
||||
use once_cell::sync::OnceCell;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
/// Describes the severity level of a [`StatusMessage`].
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub enum Severity {
|
||||
Hint,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct StatusMessage {
|
||||
pub severity: Severity,
|
||||
pub message: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for StatusMessage {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
StatusMessage {
|
||||
severity: Severity::Error,
|
||||
message: err.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for StatusMessage {
|
||||
fn from(msg: &'static str) -> Self {
|
||||
StatusMessage {
|
||||
severity: Severity::Info,
|
||||
message: msg.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime_local! {
|
||||
static MESSAGES: OnceCell<Sender<StatusMessage>> = OnceCell::new();
|
||||
}
|
||||
|
||||
pub async fn report(msg: impl Into<StatusMessage>) {
|
||||
// if the error channel overflows just ignore it
|
||||
let _ = MESSAGES
|
||||
.wait()
|
||||
.send_timeout(msg.into(), Duration::from_millis(10))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub fn report_blocking(msg: impl Into<StatusMessage>) {
|
||||
let messages = MESSAGES.wait();
|
||||
send_blocking(messages, msg.into())
|
||||
}
|
||||
|
||||
/// Must be called once during editor startup exactly once
|
||||
/// before any of the messages in this module can be used
|
||||
///
|
||||
/// # Panics
|
||||
/// If called multiple times
|
||||
pub fn setup() -> Receiver<StatusMessage> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(128);
|
||||
let _ = MESSAGES.set(tx);
|
||||
rx
|
||||
}
|
90
helix-event/src/test.rs
Normal file
90
helix-event/src/test.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{dispatch, events, register_dynamic_hook, register_event, register_hook};
|
||||
#[test]
|
||||
fn smoke_test() {
|
||||
events! {
|
||||
Event1 { content: String }
|
||||
Event2 { content: usize }
|
||||
}
|
||||
register_event::<Event1>();
|
||||
register_event::<Event2>();
|
||||
|
||||
// setup hooks
|
||||
let res1: Arc<Mutex<String>> = Arc::default();
|
||||
let acc = Arc::clone(&res1);
|
||||
register_hook!(move |event: &mut Event1| {
|
||||
acc.lock().push_str(&event.content);
|
||||
Ok(())
|
||||
});
|
||||
let res2: Arc<AtomicUsize> = Arc::default();
|
||||
let acc = Arc::clone(&res2);
|
||||
register_hook!(move |event: &mut Event2| {
|
||||
acc.fetch_add(event.content, Ordering::Relaxed);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// triggers events
|
||||
let thread = std::thread::spawn(|| {
|
||||
for i in 0..1000 {
|
||||
dispatch(Event2 { content: i });
|
||||
}
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
dispatch(Event1 {
|
||||
content: "foo".to_owned(),
|
||||
});
|
||||
dispatch(Event2 { content: 42 });
|
||||
dispatch(Event1 {
|
||||
content: "bar".to_owned(),
|
||||
});
|
||||
dispatch(Event1 {
|
||||
content: "hello world".to_owned(),
|
||||
});
|
||||
thread.join().unwrap();
|
||||
|
||||
// check output
|
||||
assert_eq!(&**res1.lock(), "foobarhello world");
|
||||
assert_eq!(
|
||||
res2.load(Ordering::Relaxed),
|
||||
42 + (0..1000usize).sum::<usize>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic() {
|
||||
events! {
|
||||
Event3 {}
|
||||
Event4 { count: usize }
|
||||
};
|
||||
register_event::<Event3>();
|
||||
register_event::<Event4>();
|
||||
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let count1 = count.clone();
|
||||
let count2 = count.clone();
|
||||
register_dynamic_hook(
|
||||
move || {
|
||||
count1.fetch_add(2, Ordering::Relaxed);
|
||||
Ok(())
|
||||
},
|
||||
"Event3",
|
||||
)
|
||||
.unwrap();
|
||||
register_dynamic_hook(
|
||||
move || {
|
||||
count2.fetch_add(3, Ordering::Relaxed);
|
||||
Ok(())
|
||||
},
|
||||
"Event4",
|
||||
)
|
||||
.unwrap();
|
||||
dispatch(Event3 {});
|
||||
dispatch(Event4 { count: 0 });
|
||||
dispatch(Event3 {});
|
||||
assert_eq!(count.load(Ordering::Relaxed), 7)
|
||||
}
|
@@ -15,21 +15,22 @@ name = "hx-loader"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
|
||||
anyhow = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.7"
|
||||
toml = "0.8"
|
||||
etcetera = "0.8"
|
||||
tree-sitter.workspace = true
|
||||
once_cell = "1.19"
|
||||
log = "0.4"
|
||||
which = "5.0.0"
|
||||
|
||||
# TODO: these two should be on !wasm32 only
|
||||
|
||||
# cloning/compiling tree-sitter grammars
|
||||
cc = { version = "1" }
|
||||
threadpool = { version = "1.0" }
|
||||
tempfile = "3.9.0"
|
||||
tempfile = "3.10.1"
|
||||
dunce = "1.0.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
@@ -86,10 +86,8 @@ pub fn get_language(name: &str) -> Result<Language> {
|
||||
}
|
||||
|
||||
fn ensure_git_is_available() -> Result<()> {
|
||||
match which::which("git") {
|
||||
Ok(_cmd) => Ok(()),
|
||||
Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")),
|
||||
}
|
||||
helix_stdx::env::which("git")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fetch_grammars() -> Result<()> {
|
||||
|
@@ -1,14 +1,13 @@
|
||||
pub mod config;
|
||||
pub mod grammar;
|
||||
|
||||
use helix_stdx::{env::current_working_dir, path};
|
||||
|
||||
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
|
||||
|
||||
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
|
||||
static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
|
||||
once_cell::sync::Lazy::new(prioritize_runtime_dirs);
|
||||
|
||||
@@ -16,31 +15,6 @@ static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCe
|
||||
|
||||
static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
|
||||
|
||||
// Get the current working directory.
|
||||
// This information is managed internally as the call to std::env::current_dir
|
||||
// might fail if the cwd has been deleted.
|
||||
pub fn current_working_dir() -> PathBuf {
|
||||
if let Some(path) = &*CWD.read().unwrap() {
|
||||
return path.clone();
|
||||
}
|
||||
|
||||
let path = std::env::current_dir()
|
||||
.and_then(dunce::canonicalize)
|
||||
.expect("Couldn't determine current working directory");
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path.clone());
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
|
||||
let path = dunce::canonicalize(path)?;
|
||||
std::env::set_current_dir(&path)?;
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initialize_config_file(specified_file: Option<PathBuf>) {
|
||||
let config_file = specified_file.unwrap_or_else(default_config_file);
|
||||
ensure_parent_dir(&config_file);
|
||||
@@ -79,7 +53,8 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
|
||||
rt_dirs.push(conf_rt_dir);
|
||||
|
||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||
rt_dirs.push(dir.into());
|
||||
let dir = path::expand_tilde(Path::new(&dir));
|
||||
rt_dirs.push(path::normalize(dir));
|
||||
}
|
||||
|
||||
// If this variable is set during build time, it will always be included
|
||||
@@ -280,21 +255,9 @@ fn ensure_parent_dir(path: &Path) {
|
||||
mod merge_toml_tests {
|
||||
use std::str;
|
||||
|
||||
use super::{current_working_dir, merge_toml_values, set_current_working_dir};
|
||||
use super::merge_toml_values;
|
||||
use toml::Value;
|
||||
|
||||
#[test]
|
||||
fn current_dir_is_set() {
|
||||
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
|
||||
let cwd = current_working_dir();
|
||||
assert_ne!(cwd, new_path);
|
||||
|
||||
set_current_working_dir(&new_path).expect("Couldn't set new path");
|
||||
|
||||
let cwd = current_working_dir();
|
||||
assert_eq!(cwd, new_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_toml_map_merges() {
|
||||
const USER: &str = r#"
|
||||
|
@@ -13,6 +13,7 @@ homepage.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-parsec = { path = "../helix-parsec" }
|
||||
@@ -26,7 +27,7 @@ lsp-types = { version = "0.95" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
which = "5.0.0"
|
||||
tokio = { version = "1.36", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream = "0.1.15"
|
||||
parking_lot = "0.12.1"
|
||||
arc-swap = "1"
|
||||
|
@@ -1,27 +1,29 @@
|
||||
use crate::{
|
||||
file_operations::FileOperationsInterest,
|
||||
find_lsp_workspace, jsonrpc,
|
||||
transport::{Payload, Transport},
|
||||
Call, Error, OffsetEncoding, Result,
|
||||
};
|
||||
|
||||
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
|
||||
use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope};
|
||||
use helix_loader::{self, VERSION_AND_GIT_HASH};
|
||||
use helix_stdx::path;
|
||||
use lsp::{
|
||||
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
|
||||
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
|
||||
WorkspaceFoldersChangeEvent,
|
||||
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url,
|
||||
WorkspaceFolder, WorkspaceFoldersChangeEvent,
|
||||
};
|
||||
use lsp_types as lsp;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::process::Stdio;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{future::Future, sync::OnceLock};
|
||||
use std::{path::Path, process::Stdio};
|
||||
use tokio::{
|
||||
io::{BufReader, BufWriter},
|
||||
process::{Child, Command},
|
||||
@@ -50,6 +52,7 @@ pub struct Client {
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
request_counter: AtomicU64,
|
||||
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
|
||||
pub(crate) file_operation_interest: OnceLock<FileOperationsInterest>,
|
||||
config: Option<Value>,
|
||||
root_path: std::path::PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
@@ -68,7 +71,7 @@ impl Client {
|
||||
may_support_workspace: bool,
|
||||
) -> bool {
|
||||
let (workspace, workspace_is_cwd) = find_workspace();
|
||||
let workspace = path::get_normalized_path(&workspace);
|
||||
let workspace = path::normalize(workspace);
|
||||
let root = find_lsp_workspace(
|
||||
doc_path
|
||||
.and_then(|x| x.parent().and_then(|x| x.to_str()))
|
||||
@@ -174,15 +177,14 @@ impl Client {
|
||||
args: &[String],
|
||||
config: Option<Value>,
|
||||
server_environment: HashMap<String, String>,
|
||||
root_markers: &[String],
|
||||
manual_roots: &[PathBuf],
|
||||
root_path: PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
id: usize,
|
||||
name: String,
|
||||
req_timeout: u64,
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
||||
// Resolve path to the binary
|
||||
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
|
||||
let cmd = helix_stdx::env::which(cmd)?;
|
||||
|
||||
let process = Command::new(cmd)
|
||||
.envs(server_environment)
|
||||
@@ -203,22 +205,6 @@ impl Client {
|
||||
|
||||
let (server_rx, server_tx, initialize_notify) =
|
||||
Transport::start(reader, writer, stderr, id, name.clone());
|
||||
let (workspace, workspace_is_cwd) = find_workspace();
|
||||
let workspace = path::get_normalized_path(&workspace);
|
||||
let root = find_lsp_workspace(
|
||||
doc_path
|
||||
.and_then(|x| x.parent().and_then(|x| x.to_str()))
|
||||
.unwrap_or("."),
|
||||
root_markers,
|
||||
manual_roots,
|
||||
&workspace,
|
||||
workspace_is_cwd,
|
||||
);
|
||||
|
||||
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
|
||||
// `root_url` can not, use `workspace` as a fallback
|
||||
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
|
||||
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
|
||||
|
||||
let workspace_folders = root_uri
|
||||
.clone()
|
||||
@@ -232,6 +218,7 @@ impl Client {
|
||||
server_tx,
|
||||
request_counter: AtomicU64::new(0),
|
||||
capabilities: OnceCell::new(),
|
||||
file_operation_interest: OnceLock::new(),
|
||||
config,
|
||||
req_timeout,
|
||||
root_path,
|
||||
@@ -277,6 +264,11 @@ impl Client {
|
||||
.expect("language server not yet initialized!")
|
||||
}
|
||||
|
||||
pub(crate) fn file_operations_intests(&self) -> &FileOperationsInterest {
|
||||
self.file_operation_interest
|
||||
.get_or_init(|| FileOperationsInterest::new(self.capabilities()))
|
||||
}
|
||||
|
||||
/// Client has to be initialized otherwise this function panics
|
||||
#[inline]
|
||||
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
|
||||
@@ -639,6 +631,12 @@ impl Client {
|
||||
}),
|
||||
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
|
||||
version_support: Some(true),
|
||||
tag_support: Some(lsp::TagSupport {
|
||||
value_set: vec![
|
||||
lsp::DiagnosticTag::UNNECESSARY,
|
||||
lsp::DiagnosticTag::DEPRECATED,
|
||||
],
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
inlay_hint: Some(lsp::InlayHintClientCapabilities {
|
||||
@@ -716,27 +714,27 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prepare_file_rename(
|
||||
pub fn will_rename(
|
||||
&self,
|
||||
old_uri: &lsp::Url,
|
||||
new_uri: &lsp::Url,
|
||||
old_path: &Path,
|
||||
new_path: &Path,
|
||||
is_dir: bool,
|
||||
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support willRename feature
|
||||
match &capabilities.workspace {
|
||||
Some(workspace) => match &workspace.file_operations {
|
||||
Some(op) => {
|
||||
op.will_rename.as_ref()?;
|
||||
let capabilities = self.file_operations_intests();
|
||||
if !capabilities.will_rename.has_interest(old_path, is_dir) {
|
||||
return None;
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
let url_from_path = |path| {
|
||||
let url = if is_dir {
|
||||
Url::from_directory_path(path)
|
||||
} else {
|
||||
Url::from_file_path(path)
|
||||
};
|
||||
Some(url.ok()?.to_string())
|
||||
};
|
||||
let files = vec![lsp::FileRename {
|
||||
old_uri: old_uri.to_string(),
|
||||
new_uri: new_uri.to_string(),
|
||||
old_uri: url_from_path(old_path)?,
|
||||
new_uri: url_from_path(new_path)?,
|
||||
}];
|
||||
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
|
||||
lsp::RenameFilesParams { files },
|
||||
@@ -750,27 +748,28 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn did_file_rename(
|
||||
pub fn did_rename(
|
||||
&self,
|
||||
old_uri: &lsp::Url,
|
||||
new_uri: &lsp::Url,
|
||||
old_path: &Path,
|
||||
new_path: &Path,
|
||||
is_dir: bool,
|
||||
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support DidRename feature
|
||||
match &capabilities.workspace {
|
||||
Some(workspace) => match &workspace.file_operations {
|
||||
Some(op) => {
|
||||
op.did_rename.as_ref()?;
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
let capabilities = self.file_operations_intests();
|
||||
if !capabilities.did_rename.has_interest(new_path, is_dir) {
|
||||
return None;
|
||||
}
|
||||
let url_from_path = |path| {
|
||||
let url = if is_dir {
|
||||
Url::from_directory_path(path)
|
||||
} else {
|
||||
Url::from_file_path(path)
|
||||
};
|
||||
Some(url.ok()?.to_string())
|
||||
};
|
||||
|
||||
let files = vec![lsp::FileRename {
|
||||
old_uri: old_uri.to_string(),
|
||||
new_uri: new_uri.to_string(),
|
||||
old_uri: url_from_path(old_path)?,
|
||||
new_uri: url_from_path(new_path)?,
|
||||
}];
|
||||
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
|
||||
}
|
||||
@@ -998,6 +997,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
context: lsp::CompletionContext,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
@@ -1009,13 +1009,12 @@ impl Client {
|
||||
text_document,
|
||||
position,
|
||||
},
|
||||
context: Some(context),
|
||||
// TODO: support these tokens by async receiving and updating the choice list
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
partial_result_params: lsp::PartialResultParams {
|
||||
partial_result_token: None,
|
||||
},
|
||||
context: None,
|
||||
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
|
||||
};
|
||||
|
||||
Some(self.call::<lsp::request::Completion>(params))
|
||||
@@ -1024,7 +1023,7 @@ impl Client {
|
||||
pub fn resolve_completion_item(
|
||||
&self,
|
||||
completion_item: lsp::CompletionItem,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<lsp::CompletionItem>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support resolving completion items.
|
||||
@@ -1036,7 +1035,8 @@ impl Client {
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item))
|
||||
let res = self.call::<lsp::request::ResolveCompletionItem>(completion_item);
|
||||
Some(async move { Ok(serde_json::from_value(res.await?)?) })
|
||||
}
|
||||
|
||||
pub fn resolve_code_action(
|
||||
@@ -1062,7 +1062,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<SignatureHelp>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support signature help.
|
||||
@@ -1078,7 +1078,8 @@ impl Client {
|
||||
// lsp::SignatureHelpContext
|
||||
};
|
||||
|
||||
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
|
||||
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
|
||||
Some(async move { Ok(serde_json::from_value(res.await?)?) })
|
||||
}
|
||||
|
||||
pub fn text_document_range_inlay_hints(
|
||||
|
105
helix-lsp/src/file_operations.rs
Normal file
105
helix-lsp/src/file_operations.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use std::path::Path;
|
||||
|
||||
use globset::{GlobBuilder, GlobSet};
|
||||
|
||||
use crate::lsp;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct FileOperationFilter {
|
||||
dir_globs: GlobSet,
|
||||
file_globs: GlobSet,
|
||||
}
|
||||
|
||||
impl FileOperationFilter {
|
||||
fn new(capability: Option<&lsp::FileOperationRegistrationOptions>) -> FileOperationFilter {
|
||||
let Some(cap) = capability else {
|
||||
return FileOperationFilter::default();
|
||||
};
|
||||
let mut dir_globs = GlobSet::builder();
|
||||
let mut file_globs = GlobSet::builder();
|
||||
for filter in &cap.filters {
|
||||
// TODO: support other url schemes
|
||||
let is_non_file_schema = filter
|
||||
.scheme
|
||||
.as_ref()
|
||||
.is_some_and(|schema| schema != "file");
|
||||
if is_non_file_schema {
|
||||
continue;
|
||||
}
|
||||
let ignore_case = filter
|
||||
.pattern
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|opts| opts.ignore_case)
|
||||
.unwrap_or(false);
|
||||
let mut glob_builder = GlobBuilder::new(&filter.pattern.glob);
|
||||
glob_builder.case_insensitive(!ignore_case);
|
||||
let glob = match glob_builder.build() {
|
||||
Ok(glob) => glob,
|
||||
Err(err) => {
|
||||
log::error!("invalid glob send by LS: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match filter.pattern.matches {
|
||||
Some(lsp::FileOperationPatternKind::File) => {
|
||||
file_globs.add(glob);
|
||||
}
|
||||
Some(lsp::FileOperationPatternKind::Folder) => {
|
||||
dir_globs.add(glob);
|
||||
}
|
||||
None => {
|
||||
file_globs.add(glob.clone());
|
||||
dir_globs.add(glob);
|
||||
}
|
||||
};
|
||||
}
|
||||
let file_globs = file_globs.build().unwrap_or_else(|err| {
|
||||
log::error!("invalid globs send by LS: {err}");
|
||||
GlobSet::empty()
|
||||
});
|
||||
let dir_globs = dir_globs.build().unwrap_or_else(|err| {
|
||||
log::error!("invalid globs send by LS: {err}");
|
||||
GlobSet::empty()
|
||||
});
|
||||
FileOperationFilter {
|
||||
dir_globs,
|
||||
file_globs,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_interest(&self, path: &Path, is_dir: bool) -> bool {
|
||||
if is_dir {
|
||||
self.dir_globs.is_match(path)
|
||||
} else {
|
||||
self.file_globs.is_match(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct FileOperationsInterest {
|
||||
// TODO: support other notifications
|
||||
// did_create: FileOperationFilter,
|
||||
// will_create: FileOperationFilter,
|
||||
pub did_rename: FileOperationFilter,
|
||||
pub will_rename: FileOperationFilter,
|
||||
// did_delete: FileOperationFilter,
|
||||
// will_delete: FileOperationFilter,
|
||||
}
|
||||
|
||||
impl FileOperationsInterest {
|
||||
pub fn new(capabilities: &lsp::ServerCapabilities) -> FileOperationsInterest {
|
||||
let capabilities = capabilities
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|capabilities| capabilities.file_operations.as_ref());
|
||||
let Some(capabilities) = capabilities else {
|
||||
return FileOperationsInterest::default();
|
||||
};
|
||||
FileOperationsInterest {
|
||||
did_rename: FileOperationFilter::new(capabilities.did_rename.as_ref()),
|
||||
will_rename: FileOperationFilter::new(capabilities.will_rename.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,9 +1,11 @@
|
||||
mod client;
|
||||
pub mod file_event;
|
||||
mod file_operations;
|
||||
pub mod jsonrpc;
|
||||
pub mod snippet;
|
||||
mod transport;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
pub use client::Client;
|
||||
pub use futures_executor::block_on;
|
||||
pub use jsonrpc::Call;
|
||||
@@ -11,10 +13,10 @@ pub use lsp::{Position, Url};
|
||||
pub use lsp_types as lsp;
|
||||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use helix_core::{
|
||||
path,
|
||||
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
|
||||
use helix_core::syntax::{
|
||||
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
|
||||
};
|
||||
use helix_stdx::path;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
use std::{
|
||||
@@ -44,6 +46,8 @@ pub enum Error {
|
||||
#[error("Unhandled")]
|
||||
Unhandled,
|
||||
#[error(transparent)]
|
||||
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
@@ -549,6 +553,7 @@ pub enum MethodCall {
|
||||
WorkspaceConfiguration(lsp::ConfigurationParams),
|
||||
RegisterCapability(lsp::RegistrationParams),
|
||||
UnregisterCapability(lsp::UnregistrationParams),
|
||||
ShowDocument(lsp::ShowDocumentParams),
|
||||
}
|
||||
|
||||
impl MethodCall {
|
||||
@@ -576,6 +581,10 @@ impl MethodCall {
|
||||
let params: lsp::UnregistrationParams = params.parse()?;
|
||||
Self::UnregisterCapability(params)
|
||||
}
|
||||
lsp::request::ShowDocument::METHOD => {
|
||||
let params: lsp::ShowDocumentParams = params.parse()?;
|
||||
Self::ShowDocument(params)
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::Unhandled);
|
||||
}
|
||||
@@ -632,14 +641,14 @@ impl Notification {
|
||||
#[derive(Debug)]
|
||||
pub struct Registry {
|
||||
inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
|
||||
syn_loader: Arc<helix_core::syntax::Loader>,
|
||||
syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>,
|
||||
counter: usize,
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
|
||||
pub file_event_handler: file_event::Handler,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
|
||||
pub fn new(syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>) -> Self {
|
||||
Self {
|
||||
inner: HashMap::new(),
|
||||
syn_loader,
|
||||
@@ -672,15 +681,15 @@ impl Registry {
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
root_dirs: &[PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> Result<Arc<Client>> {
|
||||
let config = self
|
||||
.syn_loader
|
||||
) -> Result<Option<Arc<Client>>> {
|
||||
let syn_loader = self.syn_loader.load();
|
||||
let config = syn_loader
|
||||
.language_server_configs()
|
||||
.get(&name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
|
||||
let id = self.counter;
|
||||
self.counter += 1;
|
||||
let NewClient(client, incoming) = start_client(
|
||||
if let Some(NewClient(client, incoming)) = start_client(
|
||||
id,
|
||||
name,
|
||||
ls_config,
|
||||
@@ -688,9 +697,12 @@ impl Registry {
|
||||
doc_path,
|
||||
root_dirs,
|
||||
enable_snippets,
|
||||
)?;
|
||||
)? {
|
||||
self.incoming.push(UnboundedReceiverStream::new(incoming));
|
||||
Ok(client)
|
||||
Ok(Some(client))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
|
||||
@@ -715,8 +727,8 @@ impl Registry {
|
||||
root_dirs,
|
||||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => client,
|
||||
error => return Some(error),
|
||||
Ok(client) => client?,
|
||||
Err(error) => return Some(Err(error)),
|
||||
};
|
||||
let old_clients = self
|
||||
.inner
|
||||
@@ -756,13 +768,13 @@ impl Registry {
|
||||
root_dirs: &'a [PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
|
||||
language_config.language_servers.iter().map(
|
||||
language_config.language_servers.iter().filter_map(
|
||||
move |LanguageServerFeatures { name, .. }| {
|
||||
if let Some(clients) = self.inner.get(name) {
|
||||
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
|
||||
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
|
||||
}) {
|
||||
return (name.to_owned(), Ok(client.clone()));
|
||||
return Some((name.to_owned(), Ok(client.clone())));
|
||||
}
|
||||
}
|
||||
match self.start_client(
|
||||
@@ -773,13 +785,14 @@ impl Registry {
|
||||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => {
|
||||
let client = client?;
|
||||
self.inner
|
||||
.entry(name.to_owned())
|
||||
.or_default()
|
||||
.push(client.clone());
|
||||
(name.clone(), Ok(client))
|
||||
Some((name.clone(), Ok(client)))
|
||||
}
|
||||
Err(err) => (name.to_owned(), Err(err)),
|
||||
Err(err) => Some((name.to_owned(), Err(err))),
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -880,18 +893,45 @@ fn start_client(
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
root_dirs: &[PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> Result<NewClient> {
|
||||
) -> Result<Option<NewClient>> {
|
||||
let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
|
||||
let workspace = path::normalize(workspace);
|
||||
let root = find_lsp_workspace(
|
||||
doc_path
|
||||
.and_then(|x| x.parent().and_then(|x| x.to_str()))
|
||||
.unwrap_or("."),
|
||||
&config.roots,
|
||||
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
|
||||
&workspace,
|
||||
workspace_is_cwd,
|
||||
);
|
||||
|
||||
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
|
||||
// `root_url` can not, use `workspace` as a fallback
|
||||
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
|
||||
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
|
||||
|
||||
if let Some(globset) = &ls_config.required_root_patterns {
|
||||
if !root_path
|
||||
.read_dir()?
|
||||
.flatten()
|
||||
.map(|entry| entry.file_name())
|
||||
.any(|entry| globset.is_match(entry))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
let (client, incoming, initialize_notify) = Client::start(
|
||||
&ls_config.command,
|
||||
&ls_config.args,
|
||||
ls_config.config.clone(),
|
||||
ls_config.environment.clone(),
|
||||
&config.roots,
|
||||
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
|
||||
root_path,
|
||||
root_uri,
|
||||
id,
|
||||
name,
|
||||
ls_config.timeout,
|
||||
doc_path,
|
||||
)?;
|
||||
|
||||
let client = Arc::new(client);
|
||||
@@ -930,7 +970,7 @@ fn start_client(
|
||||
initialize_notify.notify_one();
|
||||
});
|
||||
|
||||
Ok(NewClient(client, incoming))
|
||||
Ok(Some(NewClient(client, incoming)))
|
||||
}
|
||||
|
||||
/// Find an LSP workspace of a file using the following mechanism:
|
||||
@@ -953,10 +993,10 @@ pub fn find_lsp_workspace(
|
||||
let mut file = if file.is_absolute() {
|
||||
file.to_path_buf()
|
||||
} else {
|
||||
let current_dir = helix_loader::current_working_dir();
|
||||
let current_dir = helix_stdx::env::current_working_dir();
|
||||
current_dir.join(file)
|
||||
};
|
||||
file = path::get_normalized_path(&file);
|
||||
file = path::normalize(&file);
|
||||
|
||||
if !file.starts_with(workspace) {
|
||||
return None;
|
||||
@@ -973,7 +1013,7 @@ pub fn find_lsp_workspace(
|
||||
|
||||
if root_dirs
|
||||
.iter()
|
||||
.any(|root_dir| path::get_normalized_path(&workspace.join(root_dir)) == ancestor)
|
||||
.any(|root_dir| path::normalize(workspace.join(root_dir)) == ancestor)
|
||||
{
|
||||
// if the worskapce is the cwd do not search any higher for workspaces
|
||||
// but specify
|
||||
|
@@ -270,7 +270,14 @@ impl Transport {
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(Error::StreamClosed) => {
|
||||
Err(err) => {
|
||||
if !matches!(err, Error::StreamClosed) {
|
||||
error!(
|
||||
"Exiting {} after unexpected error: {err:?}",
|
||||
&transport.name
|
||||
);
|
||||
}
|
||||
|
||||
// Close any outstanding requests.
|
||||
for (id, tx) in transport.pending_requests.lock().await.drain() {
|
||||
match tx.send(Err(Error::StreamClosed)).await {
|
||||
@@ -300,10 +307,6 @@ impl Transport {
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{} err: <- {err:?}", transport.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
helix-stdx/Cargo.toml
Normal file
22
helix-stdx/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "helix-stdx"
|
||||
description = "Standard library extensions"
|
||||
include = ["src/**/*", "README.md"]
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
categories.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dunce = "1.0"
|
||||
etcetera = "0.8"
|
||||
ropey = { version = "1.6.1", default-features = false }
|
||||
which = "6.0"
|
||||
regex-cursor = "0.1.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10"
|
81
helix-stdx/src/env.rs
Normal file
81
helix-stdx/src/env.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::RwLock,
|
||||
};
|
||||
|
||||
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
|
||||
// Get the current working directory.
|
||||
// This information is managed internally as the call to std::env::current_dir
|
||||
// might fail if the cwd has been deleted.
|
||||
pub fn current_working_dir() -> PathBuf {
|
||||
if let Some(path) = &*CWD.read().unwrap() {
|
||||
return path.clone();
|
||||
}
|
||||
|
||||
let path = std::env::current_dir()
|
||||
.map(crate::path::normalize)
|
||||
.expect("Couldn't determine current working directory");
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path.clone());
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
|
||||
let path = crate::path::canonicalize(path);
|
||||
std::env::set_current_dir(&path)?;
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn env_var_is_set(env_var_name: &str) -> bool {
|
||||
std::env::var_os(env_var_name).is_some()
|
||||
}
|
||||
|
||||
pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
|
||||
which::which(binary_name).is_ok()
|
||||
}
|
||||
|
||||
pub fn which<T: AsRef<OsStr>>(
|
||||
binary_name: T,
|
||||
) -> Result<std::path::PathBuf, ExecutableNotFoundError> {
|
||||
let binary_name = binary_name.as_ref();
|
||||
which::which(binary_name).map_err(|err| ExecutableNotFoundError {
|
||||
command: binary_name.to_string_lossy().into_owned(),
|
||||
inner: err,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecutableNotFoundError {
|
||||
command: String,
|
||||
inner: which::Error,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExecutableNotFoundError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "command '{}' not found: {}", self.command, self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ExecutableNotFoundError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{current_working_dir, set_current_working_dir};
|
||||
|
||||
#[test]
|
||||
fn current_dir_is_set() {
|
||||
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
|
||||
let cwd = current_working_dir();
|
||||
assert_ne!(cwd, new_path);
|
||||
|
||||
set_current_working_dir(&new_path).expect("Couldn't set new path");
|
||||
|
||||
let cwd = current_working_dir();
|
||||
assert_eq!(cwd, new_path);
|
||||
}
|
||||
}
|
3
helix-stdx/src/lib.rs
Normal file
3
helix-stdx/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod env;
|
||||
pub mod path;
|
||||
pub mod rope;
|
231
helix-stdx/src/path.rs
Normal file
231
helix-stdx/src/path.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
pub use etcetera::home_dir;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
path::{Component, Path, PathBuf, MAIN_SEPARATOR_STR},
|
||||
};
|
||||
|
||||
use crate::env::current_working_dir;
|
||||
|
||||
/// Replaces users home directory from `path` with tilde `~` if the directory
|
||||
/// is available, otherwise returns the path unchanged.
|
||||
pub fn fold_home_dir<'a, P>(path: P) -> Cow<'a, Path>
|
||||
where
|
||||
P: Into<Cow<'a, Path>>,
|
||||
{
|
||||
let path = path.into();
|
||||
if let Ok(home) = home_dir() {
|
||||
if let Ok(stripped) = path.strip_prefix(&home) {
|
||||
let mut path = OsString::with_capacity(2 + stripped.as_os_str().len());
|
||||
path.push("~");
|
||||
path.push(MAIN_SEPARATOR_STR);
|
||||
path.push(stripped);
|
||||
return Cow::Owned(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Expands tilde `~` into users home directory if available, otherwise returns the path
|
||||
/// unchanged. The tilde will only be expanded when present as the first component of the path
|
||||
/// and only slash follows it.
|
||||
pub fn expand_tilde<'a, P>(path: P) -> Cow<'a, Path>
|
||||
where
|
||||
P: Into<Cow<'a, Path>>,
|
||||
{
|
||||
let path = path.into();
|
||||
let mut components = path.components();
|
||||
if let Some(Component::Normal(c)) = components.next() {
|
||||
if c == "~" {
|
||||
if let Ok(mut buf) = home_dir() {
|
||||
buf.push(components);
|
||||
return Cow::Owned(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Normalize a path without resolving symlinks.
|
||||
// Strategy: start from the first component and move up. Cannonicalize previous path,
|
||||
// join component, cannonicalize new path, strip prefix and join to the final result.
|
||||
pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
|
||||
let mut components = path.as_ref().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 => {}
|
||||
#[cfg(not(windows))]
|
||||
Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Component::ParentDir => {
|
||||
if let Some(head) = ret.components().next_back() {
|
||||
match head {
|
||||
Component::Prefix(_) | Component::RootDir => {}
|
||||
Component::CurDir => unreachable!(),
|
||||
// If we left previous component as ".." it means we met a symlink before and we can't pop path.
|
||||
Component::ParentDir => {
|
||||
ret.push("..");
|
||||
}
|
||||
Component::Normal(_) => {
|
||||
if ret.is_symlink() {
|
||||
ret.push("..");
|
||||
} else {
|
||||
ret.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Component::Normal(c) => 'normal: {
|
||||
use std::fs::canonicalize;
|
||||
|
||||
let new_path = ret.join(c);
|
||||
if new_path.is_symlink() {
|
||||
ret = new_path;
|
||||
break 'normal;
|
||||
}
|
||||
let (can_new, can_old) = (canonicalize(&new_path), canonicalize(&ret));
|
||||
match (can_new, can_old) {
|
||||
(Ok(can_new), Ok(can_old)) => {
|
||||
let striped = can_new.strip_prefix(can_old);
|
||||
ret.push(striped.unwrap_or_else(|_| c.as_ref()));
|
||||
}
|
||||
_ => ret.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dunce::simplified(&ret).to_path_buf()
|
||||
}
|
||||
|
||||
/// Returns the canonical, absolute form of a path with all intermediate components normalized.
|
||||
///
|
||||
/// This function is used instead of [`std::fs::canonicalize`] because we don't want to verify
|
||||
/// here if the path exists, just normalize it's components.
|
||||
pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = expand_tilde(path.as_ref());
|
||||
let path = if path.is_relative() {
|
||||
Cow::Owned(current_working_dir().join(path))
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
normalize(path)
|
||||
}
|
||||
|
||||
pub fn get_relative_path<'a, P>(path: P) -> Cow<'a, Path>
|
||||
where
|
||||
P: Into<Cow<'a, Path>>,
|
||||
{
|
||||
let path = path.into();
|
||||
if path.is_absolute() {
|
||||
let cwdir = normalize(current_working_dir());
|
||||
if let Ok(stripped) = normalize(&path).strip_prefix(cwdir) {
|
||||
return Cow::Owned(PathBuf::from(stripped));
|
||||
}
|
||||
|
||||
return fold_home_dir(path);
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Returns a truncated filepath where the basepart of the path is reduced to the first
|
||||
/// char of the folder and the whole filename appended.
|
||||
///
|
||||
/// Also strip the current working directory from the beginning of the path.
|
||||
/// Note that this function does not check if the truncated path is unambiguous.
|
||||
///
|
||||
/// ```
|
||||
/// use helix_stdx::path::get_truncated_path;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("/home/cnorris/documents/jokes.txt").as_path(),
|
||||
/// Path::new("/h/c/d/jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("jokes.txt").as_path(),
|
||||
/// Path::new("jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("/jokes.txt").as_path(),
|
||||
/// Path::new("/jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// get_truncated_path("/h/c/d/jokes.txt").as_path(),
|
||||
/// Path::new("/h/c/d/jokes.txt")
|
||||
/// );
|
||||
/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
|
||||
/// ```
|
||||
///
|
||||
pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
|
||||
let cwd = current_working_dir();
|
||||
let path = path.as_ref();
|
||||
let path = path.strip_prefix(cwd).unwrap_or(path);
|
||||
let file = path.file_name().unwrap_or_default();
|
||||
let base = path.parent().unwrap_or_else(|| Path::new(""));
|
||||
let mut ret = PathBuf::with_capacity(file.len());
|
||||
// A char can't be directly pushed to a PathBuf
|
||||
let mut first_char_buffer = String::new();
|
||||
for d in base {
|
||||
let Some(first_char) = d.to_string_lossy().chars().next() else {
|
||||
break;
|
||||
};
|
||||
first_char_buffer.push(first_char);
|
||||
ret.push(&first_char_buffer);
|
||||
first_char_buffer.clear();
|
||||
}
|
||||
ret.push(file);
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Component, Path},
|
||||
};
|
||||
|
||||
use crate::path;
|
||||
|
||||
#[test]
|
||||
fn expand_tilde() {
|
||||
for path in ["~", "~/foo"] {
|
||||
let expanded = path::expand_tilde(Path::new(path));
|
||||
|
||||
let tilde = Component::Normal(OsStr::new("~"));
|
||||
|
||||
let mut component_count = 0;
|
||||
for component in expanded.components() {
|
||||
// No tilde left.
|
||||
assert_ne!(component, tilde);
|
||||
component_count += 1;
|
||||
}
|
||||
|
||||
// The path was at least expanded to something.
|
||||
assert_ne!(component_count, 0);
|
||||
}
|
||||
}
|
||||
}
|
78
helix-stdx/src/rope.rs
Normal file
78
helix-stdx/src/rope.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
|
||||
pub use regex_cursor::engines::meta::{Builder as RegexBuilder, Regex};
|
||||
pub use regex_cursor::regex_automata::util::syntax::Config;
|
||||
use regex_cursor::{Input as RegexInput, RopeyCursor};
|
||||
use ropey::RopeSlice;
|
||||
|
||||
pub trait RopeSliceExt<'a>: Sized {
|
||||
fn ends_with(self, text: &str) -> bool;
|
||||
fn starts_with(self, text: &str) -> bool;
|
||||
fn regex_input(self) -> RegexInput<RopeyCursor<'a>>;
|
||||
fn regex_input_at_bytes<R: RangeBounds<usize>>(
|
||||
self,
|
||||
byte_range: R,
|
||||
) -> RegexInput<RopeyCursor<'a>>;
|
||||
fn regex_input_at<R: RangeBounds<usize>>(self, char_range: R) -> RegexInput<RopeyCursor<'a>>;
|
||||
fn first_non_whitespace_char(self) -> Option<usize>;
|
||||
fn last_non_whitespace_char(self) -> Option<usize>;
|
||||
}
|
||||
|
||||
impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
|
||||
fn ends_with(self, text: &str) -> bool {
|
||||
let len = self.len_bytes();
|
||||
if len < text.len() {
|
||||
return false;
|
||||
}
|
||||
self.get_byte_slice(len - text.len()..)
|
||||
.map_or(false, |end| end == text)
|
||||
}
|
||||
|
||||
fn starts_with(self, text: &str) -> bool {
|
||||
let len = self.len_bytes();
|
||||
if len < text.len() {
|
||||
return false;
|
||||
}
|
||||
self.get_byte_slice(..len - text.len())
|
||||
.map_or(false, |start| start == text)
|
||||
}
|
||||
|
||||
fn regex_input(self) -> RegexInput<RopeyCursor<'a>> {
|
||||
RegexInput::new(self)
|
||||
}
|
||||
|
||||
fn regex_input_at<R: RangeBounds<usize>>(self, char_range: R) -> RegexInput<RopeyCursor<'a>> {
|
||||
let start_bound = match char_range.start_bound() {
|
||||
Bound::Included(&val) => Bound::Included(self.char_to_byte(val)),
|
||||
Bound::Excluded(&val) => Bound::Excluded(self.char_to_byte(val)),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
let end_bound = match char_range.end_bound() {
|
||||
Bound::Included(&val) => Bound::Included(self.char_to_byte(val)),
|
||||
Bound::Excluded(&val) => Bound::Excluded(self.char_to_byte(val)),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
self.regex_input_at_bytes((start_bound, end_bound))
|
||||
}
|
||||
fn regex_input_at_bytes<R: RangeBounds<usize>>(
|
||||
self,
|
||||
byte_range: R,
|
||||
) -> RegexInput<RopeyCursor<'a>> {
|
||||
let input = match byte_range.start_bound() {
|
||||
Bound::Included(&pos) | Bound::Excluded(&pos) => {
|
||||
RegexInput::new(RopeyCursor::at(self, pos))
|
||||
}
|
||||
Bound::Unbounded => RegexInput::new(self),
|
||||
};
|
||||
input.range(byte_range)
|
||||
}
|
||||
fn first_non_whitespace_char(self) -> Option<usize> {
|
||||
self.chars().position(|ch| !ch.is_whitespace())
|
||||
}
|
||||
fn last_non_whitespace_char(self) -> Option<usize> {
|
||||
self.chars_at(self.len_chars())
|
||||
.reversed()
|
||||
.position(|ch| !ch.is_whitespace())
|
||||
.map(|pos| self.len_chars() - pos - 1)
|
||||
}
|
||||
}
|
124
helix-stdx/tests/path.rs
Normal file
124
helix-stdx/tests/path.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
error::Error,
|
||||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
|
||||
use helix_stdx::path;
|
||||
use tempfile::Builder;
|
||||
|
||||
// Paths on Windows are almost always case-insensitive.
|
||||
// Normalization should return the original path.
|
||||
// E.g. mkdir `CaSe`, normalize(`case`) = `CaSe`.
|
||||
#[test]
|
||||
fn test_case_folding_windows() -> Result<(), Box<dyn Error>> {
|
||||
// tmp/root/case
|
||||
let tmp_prefix = std::env::temp_dir();
|
||||
set_current_dir(&tmp_prefix)?;
|
||||
|
||||
let root = Builder::new().prefix("root-").tempdir()?;
|
||||
let case = Builder::new().prefix("CaSe-").tempdir_in(&root)?;
|
||||
|
||||
let root_without_prefix = root.path().strip_prefix(&tmp_prefix)?;
|
||||
|
||||
let lowercase_case = format!(
|
||||
"case-{}",
|
||||
case.path()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.split_at(5)
|
||||
.1
|
||||
);
|
||||
let test_path = root_without_prefix.join(lowercase_case);
|
||||
assert_eq!(
|
||||
path::normalize(&test_path),
|
||||
case.path().strip_prefix(&tmp_prefix)?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path() -> Result<(), Box<dyn Error>> {
|
||||
/*
|
||||
tmp/root/
|
||||
├── link -> dir1/orig_file
|
||||
├── dir1/
|
||||
│ └── orig_file
|
||||
└── dir2/
|
||||
└── dir_link -> ../dir1/
|
||||
*/
|
||||
|
||||
let tmp_prefix = std::env::temp_dir();
|
||||
set_current_dir(&tmp_prefix)?;
|
||||
|
||||
// Create a tree structure as shown above
|
||||
let root = Builder::new().prefix("root-").tempdir()?;
|
||||
let dir1 = Builder::new().prefix("dir1-").tempdir_in(&root)?;
|
||||
let orig_file = Builder::new().prefix("orig_file-").tempfile_in(&dir1)?;
|
||||
let dir2 = Builder::new().prefix("dir2-").tempdir_in(&root)?;
|
||||
|
||||
// Create path and delete existing file
|
||||
let dir_link = Builder::new()
|
||||
.prefix("dir_link-")
|
||||
.tempfile_in(&dir2)?
|
||||
.path()
|
||||
.to_owned();
|
||||
let link = Builder::new()
|
||||
.prefix("link-")
|
||||
.tempfile_in(&root)?
|
||||
.path()
|
||||
.to_owned();
|
||||
|
||||
use std::os::windows;
|
||||
windows::fs::symlink_dir(&dir1, &dir_link)?;
|
||||
windows::fs::symlink_file(&orig_file, &link)?;
|
||||
|
||||
// root/link
|
||||
let path = link.strip_prefix(&tmp_prefix)?;
|
||||
assert_eq!(
|
||||
path::normalize(path),
|
||||
path,
|
||||
"input {:?} and symlink last component shouldn't be resolved",
|
||||
path
|
||||
);
|
||||
|
||||
// root/dir2/dir_link/orig_file/../..
|
||||
let path = dir_link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(orig_file.path().file_name().unwrap())
|
||||
.join(Component::ParentDir)
|
||||
.join(Component::ParentDir);
|
||||
let expected = dir_link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(Component::ParentDir);
|
||||
assert_eq!(
|
||||
path::normalize(&path),
|
||||
expected,
|
||||
"input {:?} and \"..\" should not erase the simlink that goes ahead",
|
||||
&path
|
||||
);
|
||||
|
||||
// root/link/.././../dir2/../
|
||||
let path = link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(Component::ParentDir)
|
||||
.join(Component::CurDir)
|
||||
.join(Component::ParentDir)
|
||||
.join(dir2.path().file_name().unwrap())
|
||||
.join(Component::ParentDir);
|
||||
let expected = link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(Component::ParentDir)
|
||||
.join(Component::ParentDir);
|
||||
assert_eq!(path::normalize(&path), expected, "input {:?}", &path);
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -15,7 +15,7 @@ homepage.workspace = true
|
||||
[features]
|
||||
default = ["git"]
|
||||
unicode-lines = ["helix-core/unicode-lines"]
|
||||
integration = []
|
||||
integration = ["helix-event/integration_test"]
|
||||
git = ["helix-vcs/git"]
|
||||
|
||||
[[bin]]
|
||||
@@ -23,6 +23,7 @@ name = "hx"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-event = { path = "../helix-event" }
|
||||
helix-view = { path = "../helix-view" }
|
||||
@@ -34,15 +35,14 @@ helix-loader = { path = "../helix-loader" }
|
||||
anyhow = "1"
|
||||
once_cell = "1.19"
|
||||
|
||||
which = "5.0.0"
|
||||
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
|
||||
crossterm = { version = "0.27", features = ["event-stream"] }
|
||||
signal-hook = "0.3"
|
||||
tokio-stream = "0.1"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
arc-swap = { version = "1.6.0" }
|
||||
arc-swap = { version = "1.7.1" }
|
||||
termini = "1"
|
||||
|
||||
# Logging
|
||||
fern = "0.6"
|
||||
@@ -53,16 +53,16 @@ log = "0.4"
|
||||
nucleo.workspace = true
|
||||
ignore = "0.4"
|
||||
# markdown doc rendering
|
||||
pulldown-cmark = { version = "0.9", default-features = false }
|
||||
pulldown-cmark = { version = "0.10", default-features = false }
|
||||
# file type detection
|
||||
content_inspector = "0.2.4"
|
||||
|
||||
# opening URLs
|
||||
open = "5.0.1"
|
||||
open = "5.1.2"
|
||||
url = "2.5.0"
|
||||
|
||||
# config
|
||||
toml = "0.7"
|
||||
toml = "0.8"
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -73,7 +73,7 @@ grep-searcher = "0.1.13"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
|
||||
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
|
||||
libc = "0.2.152"
|
||||
libc = "0.2.153"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
|
||||
@@ -82,6 +82,6 @@ crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
||||
[dev-dependencies]
|
||||
smallvec = "1.11"
|
||||
indoc = "2.0.4"
|
||||
tempfile = "3.9.0"
|
||||
smallvec = "1.13"
|
||||
indoc = "2.0.5"
|
||||
tempfile = "3.10.1"
|
||||
|
@@ -6,4 +6,150 @@ fn main() {
|
||||
build_grammars(Some(std::env::var("TARGET").unwrap()))
|
||||
.expect("Failed to compile tree-sitter grammars");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_rc {
|
||||
use std::io::prelude::Write;
|
||||
use std::{env, io, path::Path, path::PathBuf, process};
|
||||
|
||||
pub(crate) fn link_icon_in_windows_exe(icon_path: &str) {
|
||||
let rc_exe = find_rc_exe().expect("Windows SDK is to be installed along with MSVC");
|
||||
|
||||
let output = env::var("OUT_DIR").expect("Env var OUT_DIR should have been set by compiler");
|
||||
let output_dir = PathBuf::from(output);
|
||||
|
||||
let rc_path = output_dir.join("resource.rc");
|
||||
write_resource_file(&rc_path, icon_path).unwrap();
|
||||
|
||||
let resource_file = PathBuf::from(&output_dir).join("resource.lib");
|
||||
compile_with_toolkit_msvc(rc_exe, resource_file, rc_path);
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", output_dir.display());
|
||||
println!("cargo:rustc-link-lib=dylib=resource");
|
||||
}
|
||||
|
||||
fn compile_with_toolkit_msvc(rc_exe: PathBuf, output: PathBuf, input: PathBuf) {
|
||||
let mut command = process::Command::new(rc_exe);
|
||||
let command = command.arg(format!(
|
||||
"/I{}",
|
||||
env::var("CARGO_MANIFEST_DIR")
|
||||
.expect("CARGO_MANIFEST_DIR should have been set by Cargo")
|
||||
));
|
||||
|
||||
let status = command
|
||||
.arg(format!("/fo{}", output.display()))
|
||||
.arg(format!("{}", input.display()))
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
println!(
|
||||
"RC Output:\n{}\n------",
|
||||
String::from_utf8_lossy(&status.stdout)
|
||||
);
|
||||
println!(
|
||||
"RC Error:\n{}\n------",
|
||||
String::from_utf8_lossy(&status.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn find_rc_exe() -> io::Result<PathBuf> {
|
||||
let find_reg_key = process::Command::new("reg")
|
||||
.arg("query")
|
||||
.arg(r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots")
|
||||
.arg("/reg:32")
|
||||
.arg("/v")
|
||||
.arg("KitsRoot10")
|
||||
.output();
|
||||
|
||||
match find_reg_key {
|
||||
Err(find_reg_key) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to run registry query: {}", find_reg_key),
|
||||
))
|
||||
}
|
||||
Ok(find_reg_key) => {
|
||||
if find_reg_key.status.code().unwrap() != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Can not find Windows SDK",
|
||||
));
|
||||
} else {
|
||||
let lines = String::from_utf8(find_reg_key.stdout)
|
||||
.expect("Should be able to parse the output");
|
||||
let mut lines: Vec<&str> = lines.lines().collect();
|
||||
let mut rc_exe_paths: Vec<PathBuf> = Vec::new();
|
||||
lines.reverse();
|
||||
for line in lines {
|
||||
if line.trim().starts_with("KitsRoot") {
|
||||
let kit: String = line
|
||||
.chars()
|
||||
.skip(line.find("REG_SZ").unwrap() + 6)
|
||||
.skip_while(|c| c.is_whitespace())
|
||||
.collect();
|
||||
|
||||
let p = PathBuf::from(&kit);
|
||||
let rc = if cfg!(target_arch = "x86_64") {
|
||||
p.join(r"bin\x64\rc.exe")
|
||||
} else {
|
||||
p.join(r"bin\x86\rc.exe")
|
||||
};
|
||||
|
||||
if rc.exists() {
|
||||
println!("{:?}", rc);
|
||||
rc_exe_paths.push(rc.to_owned());
|
||||
}
|
||||
|
||||
if let Ok(bin) = p.join("bin").read_dir() {
|
||||
for e in bin.filter_map(|e| e.ok()) {
|
||||
let p = if cfg!(target_arch = "x86_64") {
|
||||
e.path().join(r"x64\rc.exe")
|
||||
} else {
|
||||
e.path().join(r"x86\rc.exe")
|
||||
};
|
||||
if p.exists() {
|
||||
println!("{:?}", p);
|
||||
rc_exe_paths.push(p.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if rc_exe_paths.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Can not find Windows SDK",
|
||||
));
|
||||
}
|
||||
|
||||
println!("{:?}", rc_exe_paths);
|
||||
let rc_path = rc_exe_paths.pop().unwrap();
|
||||
|
||||
let rc_exe = if !rc_path.exists() {
|
||||
if cfg!(target_arch = "x86_64") {
|
||||
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x64\rc.exe")
|
||||
} else {
|
||||
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x86\rc.exe")
|
||||
}
|
||||
} else {
|
||||
rc_path
|
||||
};
|
||||
|
||||
println!("Selected RC path: '{}'", rc_exe.display());
|
||||
Ok(rc_exe)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_resource_file(rc_path: &Path, icon_path: &str) -> io::Result<()> {
|
||||
let mut f = std::fs::File::create(rc_path)?;
|
||||
writeln!(f, "{} ICON \"{}\"", 1, icon_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
use arc_swap::{access::Map, ArcSwap};
|
||||
use futures_util::Stream;
|
||||
use helix_core::{path::get_relative_path, pos_at_coords, syntax, Selection};
|
||||
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
|
||||
use helix_lsp::{
|
||||
lsp::{self, notification::Notification},
|
||||
util::lsp_range_to_range,
|
||||
LspProgressMap,
|
||||
};
|
||||
use helix_stdx::path::get_relative_path;
|
||||
use helix_view::{
|
||||
align_view,
|
||||
document::DocumentSavedEventResult,
|
||||
@@ -19,15 +21,15 @@ use tui::backend::Backend;
|
||||
|
||||
use crate::{
|
||||
args::Args,
|
||||
commands::apply_workspace_edit,
|
||||
compositor::{Compositor, Event},
|
||||
config::Config,
|
||||
handlers,
|
||||
job::Jobs,
|
||||
keymap::Keymaps,
|
||||
ui::{self, overlay::overlaid},
|
||||
};
|
||||
|
||||
use log::{debug, error, warn};
|
||||
use log::{debug, error, info, warn};
|
||||
#[cfg(not(feature = "integration"))]
|
||||
use std::io::stdout;
|
||||
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
|
||||
@@ -64,7 +66,7 @@ pub struct Application {
|
||||
#[allow(dead_code)]
|
||||
theme_loader: Arc<theme::Loader>,
|
||||
#[allow(dead_code)]
|
||||
syn_loader: Arc<syntax::Loader>,
|
||||
syn_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
|
||||
signals: Signals,
|
||||
jobs: Jobs,
|
||||
@@ -94,11 +96,7 @@ fn setup_integration_logging() {
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn new(
|
||||
args: Args,
|
||||
config: Config,
|
||||
syn_loader_conf: syntax::Configuration,
|
||||
) -> Result<Self, Error> {
|
||||
pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Self, Error> {
|
||||
#[cfg(feature = "integration")]
|
||||
setup_integration_logging();
|
||||
|
||||
@@ -124,7 +122,7 @@ impl Application {
|
||||
})
|
||||
.unwrap_or_else(|| theme_loader.default_theme(true_color));
|
||||
|
||||
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
|
||||
let syn_loader = Arc::new(ArcSwap::from_pointee(lang_loader));
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
let backend = CrosstermBackend::new(stdout(), &config.editor);
|
||||
@@ -136,6 +134,7 @@ impl Application {
|
||||
let area = terminal.size().expect("couldn't get terminal size");
|
||||
let mut compositor = Compositor::new(area);
|
||||
let config = Arc::new(ArcSwap::from_pointee(config));
|
||||
let handlers = handlers::setup(config.clone());
|
||||
let mut editor = Editor::new(
|
||||
area,
|
||||
theme_loader.clone(),
|
||||
@@ -143,6 +142,7 @@ impl Application {
|
||||
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.editor
|
||||
})),
|
||||
handlers,
|
||||
);
|
||||
|
||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
@@ -319,10 +319,21 @@ impl Application {
|
||||
Some(event) = input_stream.next() => {
|
||||
self.handle_terminal_events(event).await;
|
||||
}
|
||||
Some(callback) = self.jobs.futures.next() => {
|
||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
||||
Some(callback) = self.jobs.callbacks.recv() => {
|
||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
|
||||
self.render().await;
|
||||
}
|
||||
Some(msg) = self.jobs.status_messages.recv() => {
|
||||
let severity = match msg.severity{
|
||||
helix_event::status::Severity::Hint => Severity::Hint,
|
||||
helix_event::status::Severity::Info => Severity::Info,
|
||||
helix_event::status::Severity::Warning => Severity::Warning,
|
||||
helix_event::status::Severity::Error => Severity::Error,
|
||||
};
|
||||
// TODO: show multiple status messages at once to avoid clobbering
|
||||
self.editor.status_msg = Some((msg.message, severity));
|
||||
helix_event::request_redraw();
|
||||
}
|
||||
Some(callback) = self.jobs.wait_futures.next() => {
|
||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
||||
self.render().await;
|
||||
@@ -379,10 +390,9 @@ impl Application {
|
||||
|
||||
/// refresh language config after config change
|
||||
fn refresh_language_config(&mut self) -> Result<(), Error> {
|
||||
let syntax_config = helix_core::config::user_syntax_loader()
|
||||
.map_err(|err| anyhow::anyhow!("Failed to load language config: {}", err))?;
|
||||
let lang_loader = helix_core::config::user_lang_loader()?;
|
||||
|
||||
self.syn_loader = std::sync::Arc::new(syntax::Loader::new(syntax_config));
|
||||
self.syn_loader.store(Arc::new(lang_loader));
|
||||
self.editor.syn_loader = self.syn_loader.clone();
|
||||
for document in self.editor.documents.values_mut() {
|
||||
document.detect_language(self.syn_loader.clone());
|
||||
@@ -557,26 +567,8 @@ impl Application {
|
||||
let lines = doc_save_event.text.len_lines();
|
||||
let bytes = doc_save_event.text.len_bytes();
|
||||
|
||||
if doc.path() != Some(&doc_save_event.path) {
|
||||
doc.set_path(Some(&doc_save_event.path));
|
||||
|
||||
let loader = self.editor.syn_loader.clone();
|
||||
|
||||
// borrowing the same doc again to get around the borrow checker
|
||||
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
|
||||
let id = doc.id();
|
||||
doc.detect_language(loader);
|
||||
self.editor.refresh_language_servers(id);
|
||||
// and again a borrow checker workaround...
|
||||
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
|
||||
let diagnostics = Editor::doc_diagnostics(
|
||||
&self.editor.language_servers,
|
||||
&self.editor.diagnostics,
|
||||
doc,
|
||||
);
|
||||
doc.replace_diagnostics(diagnostics, &[], None);
|
||||
}
|
||||
|
||||
self.editor
|
||||
.set_doc_path(doc_save_event.doc_id, &doc_save_event.path);
|
||||
// TODO: fix being overwritten by lsp
|
||||
self.editor.set_status(format!(
|
||||
"'{}' written, {}L {}B",
|
||||
@@ -683,9 +675,13 @@ impl Application {
|
||||
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
|
||||
let notification = match Notification::parse(&method, params) {
|
||||
Ok(notification) => notification,
|
||||
Err(helix_lsp::Error::Unhandled) => {
|
||||
info!("Ignoring Unhandled notification from Language Server");
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"received malformed notification from Language Server: {}",
|
||||
error!(
|
||||
"Ignoring unknown notification from Language Server: {}",
|
||||
err
|
||||
);
|
||||
return;
|
||||
@@ -728,7 +724,7 @@ impl Application {
|
||||
}
|
||||
Notification::PublishDiagnostics(mut params) => {
|
||||
let path = match params.uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Ok(path) => helix_stdx::path::normalize(&path),
|
||||
Err(_) => {
|
||||
log::error!("Unsupported file URI: {}", params.uri);
|
||||
return;
|
||||
@@ -757,9 +753,7 @@ impl Application {
|
||||
let lang_conf = doc.language.clone();
|
||||
|
||||
if let Some(lang_conf) = &lang_conf {
|
||||
if let Some(old_diagnostics) =
|
||||
self.editor.diagnostics.get(¶ms.uri)
|
||||
{
|
||||
if let Some(old_diagnostics) = self.editor.diagnostics.get(&path) {
|
||||
if !lang_conf.persistent_diagnostic_sources.is_empty() {
|
||||
// Sort diagnostics first by severity and then by line numbers.
|
||||
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
|
||||
@@ -792,7 +786,7 @@ impl Application {
|
||||
// Insert the original lsp::Diagnostics here because we may have no open document
|
||||
// for diagnosic message and so we can't calculate the exact position.
|
||||
// When using them later in the diagnostics picker, we calculate them on-demand.
|
||||
let diagnostics = match self.editor.diagnostics.entry(params.uri) {
|
||||
let diagnostics = match self.editor.diagnostics.entry(path) {
|
||||
Entry::Occupied(o) => {
|
||||
let current_diagnostics = o.into_mut();
|
||||
// there may entries of other language servers, which is why we can't overwrite the whole entry
|
||||
@@ -991,11 +985,9 @@ impl Application {
|
||||
let language_server = language_server!();
|
||||
if language_server.is_initialized() {
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let res = apply_workspace_edit(
|
||||
&mut self.editor,
|
||||
offset_encoding,
|
||||
¶ms.edit,
|
||||
);
|
||||
let res = self
|
||||
.editor
|
||||
.apply_workspace_edit(offset_encoding, ¶ms.edit);
|
||||
|
||||
Ok(json!(lsp::ApplyWorkspaceEditResponse {
|
||||
applied: res.is_ok(),
|
||||
@@ -1096,6 +1088,13 @@ impl Application {
|
||||
}
|
||||
Ok(serde_json::Value::Null)
|
||||
}
|
||||
Ok(MethodCall::ShowDocument(params)) => {
|
||||
let language_server = language_server!();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
|
||||
let result = self.handle_show_document(params, offset_encoding);
|
||||
Ok(json!(result))
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(language_server!().reply(id, reply));
|
||||
@@ -1104,6 +1103,68 @@ impl Application {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_show_document(
|
||||
&mut self,
|
||||
params: lsp::ShowDocumentParams,
|
||||
offset_encoding: helix_lsp::OffsetEncoding,
|
||||
) -> lsp::ShowDocumentResult {
|
||||
if let lsp::ShowDocumentParams {
|
||||
external: Some(true),
|
||||
uri,
|
||||
..
|
||||
} = params
|
||||
{
|
||||
self.jobs.callback(crate::open_external_url_callback(uri));
|
||||
return lsp::ShowDocumentResult { success: true };
|
||||
};
|
||||
|
||||
let lsp::ShowDocumentParams {
|
||||
uri,
|
||||
selection,
|
||||
take_focus,
|
||||
..
|
||||
} = params;
|
||||
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
log::error!("unsupported file URI: {}: {:?}", uri, err);
|
||||
return lsp::ShowDocumentResult { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
let action = match take_focus {
|
||||
Some(true) => helix_view::editor::Action::Replace,
|
||||
_ => helix_view::editor::Action::VerticalSplit,
|
||||
};
|
||||
|
||||
let doc_id = match self.editor.open(&path, action) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
log::error!("failed to open path: {:?}: {:?}", uri, err);
|
||||
return lsp::ShowDocumentResult { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
let doc = doc_mut!(self.editor, &doc_id);
|
||||
if let Some(range) = selection {
|
||||
// TODO: convert inside server
|
||||
if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding) {
|
||||
let view = view_mut!(self.editor);
|
||||
|
||||
// we flip the range so that the cursor sits on the start of the symbol
|
||||
// (for example start of the function).
|
||||
doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
|
||||
if action.align_view(view, doc.id()) {
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", range);
|
||||
};
|
||||
};
|
||||
lsp::ShowDocumentResult { success: true }
|
||||
}
|
||||
|
||||
async fn claim_term(&mut self) -> std::io::Result<()> {
|
||||
let terminal_config = self.config.load().editor.clone().into();
|
||||
self.terminal.claim(terminal_config)
|
||||
|
@@ -90,10 +90,9 @@ impl Args {
|
||||
}
|
||||
}
|
||||
arg if arg.starts_with('+') => {
|
||||
let arg = &arg[1..];
|
||||
line_number = match arg.parse::<usize>() {
|
||||
Ok(n) => n.saturating_sub(1),
|
||||
_ => anyhow::bail!("bad line number after +"),
|
||||
match arg[1..].parse::<usize>() {
|
||||
Ok(n) => line_number = n.saturating_sub(1),
|
||||
_ => args.files.push(parse_file(arg)),
|
||||
};
|
||||
}
|
||||
arg => args.files.push(parse_file(arg)),
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -78,7 +78,7 @@ fn thread_picker(
|
||||
})
|
||||
.with_preview(move |editor, thread| {
|
||||
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||
let frame = frames.get(0)?;
|
||||
let frame = frames.first()?;
|
||||
let path = frame.source.as_ref()?.path.clone()?;
|
||||
let pos = Some((
|
||||
frame.line.saturating_sub(1),
|
||||
@@ -166,7 +166,7 @@ pub fn dap_start_impl(
|
||||
// TODO: avoid refetching all of this... pass a config in
|
||||
let template = match name {
|
||||
Some(name) => config.templates.iter().find(|t| t.name == name),
|
||||
None => config.templates.get(0),
|
||||
None => config.templates.first(),
|
||||
}
|
||||
.ok_or_else(|| anyhow!("No debug config with given name"))?;
|
||||
|
||||
@@ -217,7 +217,7 @@ pub fn dap_start_impl(
|
||||
}
|
||||
}
|
||||
|
||||
args.insert("cwd", to_value(helix_loader::current_working_dir())?);
|
||||
args.insert("cwd", to_value(helix_stdx::env::current_working_dir())?);
|
||||
|
||||
let args = to_value(args).unwrap();
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
|
||||
use futures_util::{stream::FuturesUnordered, FutureExt};
|
||||
use helix_lsp::{
|
||||
block_on,
|
||||
lsp::{
|
||||
@@ -8,22 +8,21 @@ use helix_lsp::{
|
||||
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
|
||||
Client, OffsetEncoding,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio_stream::StreamExt;
|
||||
use tui::{
|
||||
text::{Span, Spans},
|
||||
widgets::Row,
|
||||
};
|
||||
|
||||
use super::{align_view, push_jump, Align, Context, Editor, Open};
|
||||
use super::{align_view, push_jump, Align, Context, Editor};
|
||||
|
||||
use helix_core::{
|
||||
path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
|
||||
};
|
||||
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
|
||||
use helix_stdx::path;
|
||||
use helix_view::{
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||
editor::Action,
|
||||
graphics::Margin,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
theme::Style,
|
||||
Document, View,
|
||||
};
|
||||
@@ -31,10 +30,7 @@ use helix_view::{
|
||||
use crate::{
|
||||
compositor::{self, Compositor},
|
||||
job::Callback,
|
||||
ui::{
|
||||
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
|
||||
PromptEvent,
|
||||
},
|
||||
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
|
||||
};
|
||||
|
||||
use std::{
|
||||
@@ -42,8 +38,7 @@ use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
fmt::Write,
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Gets the first language server that is attached to a document which supports a specific feature.
|
||||
@@ -139,7 +134,7 @@ struct DiagnosticStyles {
|
||||
}
|
||||
|
||||
struct PickerDiagnostic {
|
||||
url: lsp::Url,
|
||||
path: PathBuf,
|
||||
diag: lsp::Diagnostic,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
@@ -172,8 +167,7 @@ impl ui::menu::Item for PickerDiagnostic {
|
||||
let path = match format {
|
||||
DiagnosticsFormat::HideSourcePath => String::new(),
|
||||
DiagnosticsFormat::ShowSourcePath => {
|
||||
let file_path = self.url.to_file_path().unwrap();
|
||||
let path = path::get_truncated_path(file_path);
|
||||
let path = path::get_truncated_path(&self.path);
|
||||
format!("{}: ", path.to_string_lossy())
|
||||
}
|
||||
};
|
||||
@@ -213,22 +207,31 @@ fn jump_to_location(
|
||||
return;
|
||||
}
|
||||
};
|
||||
jump_to_position(editor, &path, location.range, offset_encoding, action);
|
||||
}
|
||||
|
||||
let doc = match editor.open(&path, action) {
|
||||
fn jump_to_position(
|
||||
editor: &mut Editor,
|
||||
path: &Path,
|
||||
range: lsp::Range,
|
||||
offset_encoding: OffsetEncoding,
|
||||
action: Action,
|
||||
) {
|
||||
let doc = match editor.open(path, action) {
|
||||
Ok(id) => doc_mut!(editor, &id),
|
||||
Err(err) => {
|
||||
let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
|
||||
let err = format!("failed to open path: {:?}: {:?}", path, err);
|
||||
editor.set_error(err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let view = view_mut!(editor);
|
||||
// TODO: convert inside server
|
||||
let new_range =
|
||||
if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) {
|
||||
let new_range = if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding)
|
||||
{
|
||||
new_range
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", location.range);
|
||||
log::warn!("lsp position out of bounds - {:?}", range);
|
||||
return;
|
||||
};
|
||||
// we flip the range so that the cursor sits on the start of the symbol
|
||||
@@ -263,21 +266,20 @@ enum DiagnosticsFormat {
|
||||
|
||||
fn diag_picker(
|
||||
cx: &Context,
|
||||
diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
|
||||
_current_path: Option<lsp::Url>,
|
||||
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, usize)>>,
|
||||
format: DiagnosticsFormat,
|
||||
) -> Picker<PickerDiagnostic> {
|
||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||
|
||||
// flatten the map to a vec of (url, diag) pairs
|
||||
let mut flat_diag = Vec::new();
|
||||
for (url, diags) in diagnostics {
|
||||
for (path, diags) in diagnostics {
|
||||
flat_diag.reserve(diags.len());
|
||||
|
||||
for (diag, ls) in diags {
|
||||
if let Some(ls) = cx.editor.language_server_by_id(ls) {
|
||||
flat_diag.push(PickerDiagnostic {
|
||||
url: url.clone(),
|
||||
path: path.clone(),
|
||||
diag,
|
||||
offset_encoding: ls.offset_encoding(),
|
||||
});
|
||||
@@ -297,22 +299,17 @@ fn diag_picker(
|
||||
(styles, format),
|
||||
move |cx,
|
||||
PickerDiagnostic {
|
||||
url,
|
||||
path,
|
||||
diag,
|
||||
offset_encoding,
|
||||
},
|
||||
action| {
|
||||
jump_to_location(
|
||||
cx.editor,
|
||||
&lsp::Location::new(url.clone(), diag.range),
|
||||
*offset_encoding,
|
||||
action,
|
||||
)
|
||||
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action)
|
||||
},
|
||||
)
|
||||
.with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
|
||||
let location = lsp::Location::new(url.clone(), diag.range);
|
||||
Some(location_to_file_location(&location))
|
||||
.with_preview(move |_editor, PickerDiagnostic { path, diag, .. }| {
|
||||
let line = Some((diag.range.start.line as usize, diag.range.end.line as usize));
|
||||
Some((path.clone().into(), line))
|
||||
})
|
||||
.truncate_start(false)
|
||||
}
|
||||
@@ -475,17 +472,16 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
|
||||
pub fn diagnostics_picker(cx: &mut Context) {
|
||||
let doc = doc!(cx.editor);
|
||||
if let Some(current_url) = doc.url() {
|
||||
if let Some(current_path) = doc.path() {
|
||||
let diagnostics = cx
|
||||
.editor
|
||||
.diagnostics
|
||||
.get(¤t_url)
|
||||
.get(current_path)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let picker = diag_picker(
|
||||
cx,
|
||||
[(current_url.clone(), diagnostics)].into(),
|
||||
Some(current_url),
|
||||
[(current_path.clone(), diagnostics)].into(),
|
||||
DiagnosticsFormat::HideSourcePath,
|
||||
);
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
@@ -493,16 +489,9 @@ pub fn diagnostics_picker(cx: &mut Context) {
|
||||
}
|
||||
|
||||
pub fn workspace_diagnostics_picker(cx: &mut Context) {
|
||||
let doc = doc!(cx.editor);
|
||||
let current_url = doc.url();
|
||||
// TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
|
||||
let diagnostics = cx.editor.diagnostics.clone();
|
||||
let picker = diag_picker(
|
||||
cx,
|
||||
diagnostics,
|
||||
current_url,
|
||||
DiagnosticsFormat::ShowSourcePath,
|
||||
);
|
||||
let picker = diag_picker(cx, diagnostics, DiagnosticsFormat::ShowSourcePath);
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
@@ -731,8 +720,7 @@ pub fn code_action(cx: &mut Context) {
|
||||
resolved_code_action.as_ref().unwrap_or(code_action);
|
||||
|
||||
if let Some(ref workspace_edit) = resolved_code_action.edit {
|
||||
log::debug!("edit: {:?}", workspace_edit);
|
||||
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
|
||||
let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit);
|
||||
}
|
||||
|
||||
// if code action provides both edit and command first the edit
|
||||
@@ -792,63 +780,6 @@ pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd:
|
||||
});
|
||||
}
|
||||
|
||||
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
|
||||
use lsp::ResourceOp;
|
||||
use std::fs;
|
||||
match op {
|
||||
ResourceOp::Create(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
if ignore_if_exists && path.exists() {
|
||||
Ok(())
|
||||
} else {
|
||||
// Create directory if it does not exist
|
||||
if let Some(dir) = path.parent() {
|
||||
if !dir.is_dir() {
|
||||
fs::create_dir_all(dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(&path, [])
|
||||
}
|
||||
}
|
||||
ResourceOp::Delete(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
if path.is_dir() {
|
||||
let recursive = op
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|options| options.recursive)
|
||||
.unwrap_or(false);
|
||||
|
||||
if recursive {
|
||||
fs::remove_dir_all(&path)
|
||||
} else {
|
||||
fs::remove_dir(&path)
|
||||
}
|
||||
} else if path.is_file() {
|
||||
fs::remove_file(&path)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ResourceOp::Rename(op) => {
|
||||
let from = op.old_uri.to_file_path().unwrap();
|
||||
let to = op.new_uri.to_file_path().unwrap();
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
if ignore_if_exists && to.exists() {
|
||||
Ok(())
|
||||
} else {
|
||||
fs::rename(from, &to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyEditError {
|
||||
pub kind: ApplyEditErrorKind,
|
||||
@@ -876,157 +807,20 @@ impl ToString for ApplyEditErrorKind {
|
||||
}
|
||||
}
|
||||
|
||||
///TODO make this transactional (and set failureMode to transactional)
|
||||
pub fn apply_workspace_edit(
|
||||
editor: &mut Editor,
|
||||
offset_encoding: OffsetEncoding,
|
||||
workspace_edit: &lsp::WorkspaceEdit,
|
||||
) -> Result<(), ApplyEditError> {
|
||||
let mut apply_edits = |uri: &helix_lsp::Url,
|
||||
version: Option<i32>,
|
||||
text_edits: Vec<lsp::TextEdit>|
|
||||
-> Result<(), ApplyEditErrorKind> {
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
let err = format!("unable to convert URI to filepath: {}", uri);
|
||||
log::error!("{}", err);
|
||||
editor.set_error(err);
|
||||
return Err(ApplyEditErrorKind::UnknownURISchema);
|
||||
}
|
||||
};
|
||||
|
||||
let doc_id = match editor.open(&path, Action::Load) {
|
||||
Ok(doc_id) => doc_id,
|
||||
Err(err) => {
|
||||
let err = format!("failed to open document: {}: {}", uri, err);
|
||||
log::error!("{}", err);
|
||||
editor.set_error(err);
|
||||
return Err(ApplyEditErrorKind::FileNotFound);
|
||||
}
|
||||
};
|
||||
|
||||
let doc = doc!(editor, &doc_id);
|
||||
if let Some(version) = version {
|
||||
if version != doc.version() {
|
||||
let err = format!("outdated workspace edit for {path:?}");
|
||||
log::error!("{err}, expected {} but got {version}", doc.version());
|
||||
editor.set_error(err);
|
||||
return Err(ApplyEditErrorKind::DocumentChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// Need to determine a view for apply/append_changes_to_history
|
||||
let view_id = editor.get_synced_view_id(doc_id);
|
||||
let doc = doc_mut!(editor, &doc_id);
|
||||
|
||||
let transaction = helix_lsp::util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
text_edits,
|
||||
offset_encoding,
|
||||
);
|
||||
let view = view_mut!(editor, view_id);
|
||||
doc.apply(&transaction, view.id);
|
||||
doc.append_changes_to_history(view);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Some(ref document_changes) = workspace_edit.document_changes {
|
||||
match document_changes {
|
||||
lsp::DocumentChanges::Edits(document_edits) => {
|
||||
for (i, document_edit) in document_edits.iter().enumerate() {
|
||||
let edits = document_edit
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| match edit {
|
||||
lsp::OneOf::Left(text_edit) => text_edit,
|
||||
lsp::OneOf::Right(annotated_text_edit) => {
|
||||
&annotated_text_edit.text_edit
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
apply_edits(
|
||||
&document_edit.text_document.uri,
|
||||
document_edit.text_document.version,
|
||||
edits,
|
||||
)
|
||||
.map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
lsp::DocumentChanges::Operations(operations) => {
|
||||
log::debug!("document changes - operations: {:?}", operations);
|
||||
for (i, operation) in operations.iter().enumerate() {
|
||||
match operation {
|
||||
lsp::DocumentChangeOperation::Op(op) => {
|
||||
apply_document_resource_op(op).map_err(|io| ApplyEditError {
|
||||
kind: ApplyEditErrorKind::IoError(io),
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
|
||||
lsp::DocumentChangeOperation::Edit(document_edit) => {
|
||||
let edits = document_edit
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| match edit {
|
||||
lsp::OneOf::Left(text_edit) => text_edit,
|
||||
lsp::OneOf::Right(annotated_text_edit) => {
|
||||
&annotated_text_edit.text_edit
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
apply_edits(
|
||||
&document_edit.text_document.uri,
|
||||
document_edit.text_document.version,
|
||||
edits,
|
||||
)
|
||||
.map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(ref changes) = workspace_edit.changes {
|
||||
log::debug!("workspace changes: {:?}", changes);
|
||||
for (i, (uri, text_edits)) in changes.iter().enumerate() {
|
||||
let text_edits = text_edits.to_vec();
|
||||
apply_edits(uri, None, text_edits).map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Precondition: `locations` should be non-empty.
|
||||
fn goto_impl(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
locations: Vec<lsp::Location>,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) {
|
||||
let cwdir = helix_loader::current_working_dir();
|
||||
let cwdir = helix_stdx::env::current_working_dir();
|
||||
|
||||
match locations.as_slice() {
|
||||
[location] => {
|
||||
jump_to_location(editor, location, offset_encoding, Action::Replace);
|
||||
}
|
||||
[] => {
|
||||
editor.set_error("No definition found.");
|
||||
}
|
||||
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
||||
_locations => {
|
||||
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
|
||||
jump_to_location(cx.editor, location, offset_encoding, action)
|
||||
@@ -1068,7 +862,11 @@ where
|
||||
future,
|
||||
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
|
||||
let items = to_locations(response);
|
||||
if items.is_empty() {
|
||||
editor.set_error("No definition found.");
|
||||
} else {
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1128,151 +926,19 @@ pub fn goto_reference(cx: &mut Context) {
|
||||
future,
|
||||
move |editor, compositor, response: Option<Vec<lsp::Location>>| {
|
||||
let items = response.unwrap_or_default();
|
||||
if items.is_empty() {
|
||||
editor.set_error("No references found.");
|
||||
} else {
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum SignatureHelpInvoked {
|
||||
Manual,
|
||||
Automatic,
|
||||
}
|
||||
|
||||
pub fn signature_help(cx: &mut Context) {
|
||||
signature_help_impl(cx, SignatureHelpInvoked::Manual)
|
||||
}
|
||||
|
||||
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
|
||||
let future = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||
.find_map(|language_server| {
|
||||
let pos = doc.position(view.id, language_server.offset_encoding());
|
||||
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
||||
});
|
||||
|
||||
let Some(future) = future else {
|
||||
// Do not show the message if signature help was invoked
|
||||
// automatically on backspace, trigger characters, etc.
|
||||
if invoked == SignatureHelpInvoked::Manual {
|
||||
cx.editor
|
||||
.set_error("No configured language server supports signature-help");
|
||||
}
|
||||
return;
|
||||
};
|
||||
signature_help_impl_with_future(cx, future.boxed(), invoked);
|
||||
}
|
||||
|
||||
pub fn signature_help_impl_with_future(
|
||||
cx: &mut Context,
|
||||
future: BoxFuture<'static, helix_lsp::Result<Value>>,
|
||||
invoked: SignatureHelpInvoked,
|
||||
) {
|
||||
cx.callback(
|
||||
future,
|
||||
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
|
||||
let config = &editor.config();
|
||||
|
||||
if !(config.lsp.auto_signature_help
|
||||
|| SignatureHelp::visible_popup(compositor).is_some()
|
||||
|| invoked == SignatureHelpInvoked::Manual)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
|
||||
// it very probably means the server was a little slow to respond and the user has
|
||||
// already moved on to something else, making a signature help popup will just be an
|
||||
// annoyance, see https://github.com/helix-editor/helix/issues/3112
|
||||
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match response {
|
||||
// According to the spec the response should be None if there
|
||||
// are no signatures, but some servers don't follow this.
|
||||
Some(s) if !s.signatures.is_empty() => s,
|
||||
_ => {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let doc = doc!(editor);
|
||||
let language = doc.language_name().unwrap_or("");
|
||||
|
||||
let signature = match response
|
||||
.signatures
|
||||
.get(response.active_signature.unwrap_or(0) as usize)
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
let mut contents = SignatureHelp::new(
|
||||
signature.label.clone(),
|
||||
language.to_string(),
|
||||
Arc::clone(&editor.syn_loader),
|
||||
);
|
||||
|
||||
let signature_doc = if config.lsp.display_signature_help_docs {
|
||||
signature.documentation.as_ref().map(|doc| match doc {
|
||||
lsp::Documentation::String(s) => s.clone(),
|
||||
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
contents.set_signature_doc(signature_doc);
|
||||
|
||||
let active_param_range = || -> Option<(usize, usize)> {
|
||||
let param_idx = signature
|
||||
.active_parameter
|
||||
.or(response.active_parameter)
|
||||
.unwrap_or(0) as usize;
|
||||
let param = signature.parameters.as_ref()?.get(param_idx)?;
|
||||
match ¶m.label {
|
||||
lsp::ParameterLabel::Simple(string) => {
|
||||
let start = signature.label.find(string.as_str())?;
|
||||
Some((start, start + string.len()))
|
||||
}
|
||||
lsp::ParameterLabel::LabelOffsets([start, end]) => {
|
||||
// LS sends offsets based on utf-16 based string representation
|
||||
// but highlighting in helix is done using byte offset.
|
||||
use helix_core::str_utils::char_to_byte_idx;
|
||||
let from = char_to_byte_idx(&signature.label, *start as usize);
|
||||
let to = char_to_byte_idx(&signature.label, *end as usize);
|
||||
Some((from, to))
|
||||
}
|
||||
}
|
||||
};
|
||||
contents.set_active_param_range(active_param_range());
|
||||
|
||||
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
|
||||
let mut popup = Popup::new(SignatureHelp::ID, contents)
|
||||
.position(old_popup.and_then(|p| p.get_position()))
|
||||
.position_bias(Open::Above)
|
||||
.ignore_escape_key(true);
|
||||
|
||||
// Don't create a popup if it intersects the auto-complete menu.
|
||||
let size = compositor.size();
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.as_mut()
|
||||
.map(|completion| completion.area(size, editor))
|
||||
.filter(|area| area.intersects(popup.area(size, editor)))
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
compositor.replace_or_push(SignatureHelp::ID, popup);
|
||||
},
|
||||
);
|
||||
.handlers
|
||||
.trigger_signature_help(SignatureHelpInvoked::Manual, cx.editor)
|
||||
}
|
||||
|
||||
pub fn hover(cx: &mut Context) {
|
||||
@@ -1397,7 +1063,7 @@ pub fn rename_symbol(cx: &mut Context) {
|
||||
|
||||
match block_on(future) {
|
||||
Ok(edits) => {
|
||||
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
|
||||
let _ = cx.editor.apply_workspace_edit(offset_encoding, &edits);
|
||||
}
|
||||
Err(err) => cx.editor.set_error(err.to_string()),
|
||||
}
|
||||
@@ -1649,11 +1315,11 @@ fn compute_inlay_hints_for_view(
|
||||
view_id,
|
||||
DocumentInlayHints {
|
||||
id: new_doc_inlay_hints_id,
|
||||
type_inlay_hints: type_inlay_hints.into(),
|
||||
parameter_inlay_hints: parameter_inlay_hints.into(),
|
||||
other_inlay_hints: other_inlay_hints.into(),
|
||||
padding_before_inlay_hints: padding_before_inlay_hints.into(),
|
||||
padding_after_inlay_hints: padding_after_inlay_hints.into(),
|
||||
type_inlay_hints,
|
||||
parameter_inlay_hints,
|
||||
other_inlay_hints,
|
||||
padding_before_inlay_hints,
|
||||
padding_after_inlay_hints,
|
||||
},
|
||||
);
|
||||
doc.inlay_hints_oudated = false;
|
||||
|
@@ -7,8 +7,7 @@ use super::*;
|
||||
|
||||
use helix_core::fuzzy::fuzzy_match;
|
||||
use helix_core::indent::MAX_INDENT;
|
||||
use helix_core::{encoding, line_ending, path::get_canonicalized_path, shellwords::Shellwords};
|
||||
use helix_lsp::{OffsetEncoding, Url};
|
||||
use helix_core::{encoding, line_ending, shellwords::Shellwords};
|
||||
use helix_view::document::DEFAULT_LANGUAGE_NAME;
|
||||
use helix_view::editor::{Action, CloseError, ConfigEvent};
|
||||
use serde_json::Value;
|
||||
@@ -111,14 +110,14 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
|
||||
ensure!(!args.is_empty(), "wrong argument count");
|
||||
for arg in args {
|
||||
let (path, pos) = args::parse_file(arg);
|
||||
let path = helix_core::path::expand_tilde(&path);
|
||||
let path = helix_stdx::path::expand_tilde(path);
|
||||
// If the path is a directory, open a file picker on that directory and update the status
|
||||
// message
|
||||
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
|
||||
let callback = async move {
|
||||
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
|
||||
move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let picker = ui::file_picker(path, &editor.config());
|
||||
let picker = ui::file_picker(path.into_owned(), &editor.config());
|
||||
compositor.push(Box::new(overlaid(picker)));
|
||||
},
|
||||
));
|
||||
@@ -483,7 +482,7 @@ fn set_indent_style(
|
||||
}
|
||||
|
||||
// Attempt to parse argument as an indent style.
|
||||
let style = match args.get(0) {
|
||||
let style = match args.first() {
|
||||
Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
|
||||
Some(Cow::Borrowed("0")) => Some(Tabs),
|
||||
Some(arg) => arg
|
||||
@@ -535,7 +534,7 @@ fn set_line_ending(
|
||||
}
|
||||
|
||||
let arg = args
|
||||
.get(0)
|
||||
.first()
|
||||
.context("argument missing")?
|
||||
.to_ascii_lowercase();
|
||||
|
||||
@@ -1079,18 +1078,17 @@ fn change_current_directory(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let dir = helix_core::path::expand_tilde(
|
||||
args.first()
|
||||
let dir = args
|
||||
.first()
|
||||
.context("target directory not provided")?
|
||||
.as_ref()
|
||||
.as_ref(),
|
||||
);
|
||||
.as_ref();
|
||||
let dir = helix_stdx::path::expand_tilde(Path::new(dir));
|
||||
|
||||
helix_loader::set_current_working_dir(dir)?;
|
||||
helix_stdx::env::set_current_working_dir(dir)?;
|
||||
|
||||
cx.editor.set_status(format!(
|
||||
"Current working directory is now {}",
|
||||
helix_loader::current_working_dir().display()
|
||||
helix_stdx::env::current_working_dir().display()
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
@@ -1104,7 +1102,7 @@ fn show_current_directory(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let cwd = helix_loader::current_working_dir();
|
||||
let cwd = helix_stdx::env::current_working_dir();
|
||||
let message = format!("Current working directory is {}", cwd.display());
|
||||
|
||||
if cwd.exists() {
|
||||
@@ -1320,7 +1318,11 @@ fn reload_all(
|
||||
// Ensure that the view is synced with the document's history.
|
||||
view.sync_changes(doc);
|
||||
|
||||
doc.reload(view, &cx.editor.diff_providers)?;
|
||||
if let Err(error) = doc.reload(view, &cx.editor.diff_providers) {
|
||||
cx.editor.set_error(format!("{}", error));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(path) = doc.path() {
|
||||
cx.editor
|
||||
.language_servers
|
||||
@@ -1547,10 +1549,7 @@ fn tree_sitter_highlight_name(
|
||||
let text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
let byte = text.char_to_byte(cursor);
|
||||
let node = syntax
|
||||
.tree()
|
||||
.root_node()
|
||||
.descendant_for_byte_range(byte, byte)?;
|
||||
let node = syntax.descendant_for_byte_range(byte, byte)?;
|
||||
// Query the same range as the one used in syntax highlighting.
|
||||
let range = {
|
||||
// Calculate viewport byte ranges:
|
||||
@@ -2078,7 +2077,7 @@ fn reflow(
|
||||
// - The configured text-width for this language in languages.toml
|
||||
// - The configured text-width in the config.toml
|
||||
let text_width: usize = args
|
||||
.get(0)
|
||||
.first()
|
||||
.map(|num| num.parse::<usize>())
|
||||
.transpose()?
|
||||
.or_else(|| doc.language_config().and_then(|config| config.text_width))
|
||||
@@ -2117,11 +2116,7 @@ fn tree_sitter_subtree(
|
||||
let text = doc.text();
|
||||
let from = text.char_to_byte(primary_selection.from());
|
||||
let to = text.char_to_byte(primary_selection.to());
|
||||
if let Some(selected_node) = syntax
|
||||
.tree()
|
||||
.root_node()
|
||||
.descendant_for_byte_range(from, to)
|
||||
{
|
||||
if let Some(selected_node) = syntax.descendant_for_byte_range(from, to) {
|
||||
let mut contents = String::from("```tsq\n");
|
||||
helix_core::syntax::pretty_print_tree(&mut contents, selected_node)?;
|
||||
contents.push_str("\n```");
|
||||
@@ -2266,7 +2261,7 @@ fn run_shell_command(
|
||||
let args = args.join(" ");
|
||||
|
||||
let callback = async move {
|
||||
let (output, success) = shell_impl_async(&shell, &args, None).await?;
|
||||
let output = shell_impl_async(&shell, &args, None).await?;
|
||||
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
||||
move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
if !output.is_empty() {
|
||||
@@ -2279,11 +2274,7 @@ fn run_shell_command(
|
||||
));
|
||||
compositor.replace_or_push("shell", popup);
|
||||
}
|
||||
if success {
|
||||
editor.set_status("Command succeeded");
|
||||
} else {
|
||||
editor.set_error("Command failed");
|
||||
}
|
||||
},
|
||||
));
|
||||
Ok(call)
|
||||
@@ -2412,66 +2403,54 @@ fn move_buffer(
|
||||
|
||||
ensure!(args.len() == 1, format!(":move takes one argument"));
|
||||
let doc = doc!(cx.editor);
|
||||
|
||||
let new_path = get_canonicalized_path(&PathBuf::from(args.first().unwrap().to_string()));
|
||||
let old_path = doc
|
||||
.path()
|
||||
.ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
|
||||
.context("Scratch buffer cannot be moved. Use :write instead")?
|
||||
.clone();
|
||||
let old_path_as_url = doc.url().unwrap();
|
||||
let new_path_as_url = Url::from_file_path(&new_path).unwrap();
|
||||
let new_path = args.first().unwrap().to_string();
|
||||
if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) {
|
||||
bail!("Could not move file: {err}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let edits: Vec<(
|
||||
helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
|
||||
OffsetEncoding,
|
||||
String,
|
||||
)> = doc
|
||||
.language_servers()
|
||||
.map(|lsp| {
|
||||
(
|
||||
lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
|
||||
lsp.offset_encoding(),
|
||||
lsp.name().to_owned(),
|
||||
)
|
||||
})
|
||||
.filter(|(f, _, _)| f.is_some())
|
||||
.map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
|
||||
fn yank_diagnostic(
|
||||
cx: &mut compositor::Context,
|
||||
args: &[Cow<str>],
|
||||
event: PromptEvent,
|
||||
) -> anyhow::Result<()> {
|
||||
if event != PromptEvent::Validate {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reg = match args.first() {
|
||||
Some(s) => {
|
||||
ensure!(s.chars().count() == 1, format!("Invalid register {s}"));
|
||||
s.chars().next().unwrap()
|
||||
}
|
||||
None => '+',
|
||||
};
|
||||
|
||||
let (view, doc) = current_ref!(cx.editor);
|
||||
let primary = doc.selection(view.id).primary();
|
||||
|
||||
// Look only for diagnostics that intersect with the primary selection
|
||||
let diag: Vec<_> = doc
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.filter(|d| primary.overlaps(&helix_core::Range::new(d.range.start, d.range.end)))
|
||||
.map(|d| d.message.clone())
|
||||
.collect();
|
||||
|
||||
for (lsp_reply, encoding, name) in edits {
|
||||
match lsp_reply {
|
||||
Ok(edit) => {
|
||||
if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
|
||||
log::error!(
|
||||
":move command failed to apply edits from lsp {}: {:?}",
|
||||
name,
|
||||
e
|
||||
);
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
|
||||
}
|
||||
};
|
||||
let n = diag.len();
|
||||
if n == 0 {
|
||||
bail!("No diagnostics under primary selection");
|
||||
}
|
||||
|
||||
let doc = doc_mut!(cx.editor);
|
||||
|
||||
doc.set_path(Some(new_path.as_path()));
|
||||
if let Err(e) = std::fs::rename(&old_path, &new_path) {
|
||||
doc.set_path(Some(old_path.as_path()));
|
||||
bail!("Could not move file: {}", e);
|
||||
};
|
||||
|
||||
doc.language_servers().for_each(|lsp| {
|
||||
lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
|
||||
});
|
||||
|
||||
cx.editor
|
||||
.language_servers
|
||||
.file_event_handler
|
||||
.file_changed(new_path);
|
||||
|
||||
cx.editor.registers.write(reg, diag)?;
|
||||
cx.editor.set_status(format!(
|
||||
"Yanked {n} diagnostic{} to register {reg}",
|
||||
if n == 1 { "" } else { "s" }
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3066,7 +3045,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||
aliases: &[],
|
||||
doc: "Clear given register. If no argument is provided, clear all registers.",
|
||||
fun: clear_register,
|
||||
signature: CommandSignature::none(),
|
||||
signature: CommandSignature::all(completers::register),
|
||||
},
|
||||
TypableCommand {
|
||||
name: "redraw",
|
||||
@@ -3082,6 +3061,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||
fun: move_buffer,
|
||||
signature: CommandSignature::positional(&[completers::filename]),
|
||||
},
|
||||
TypableCommand {
|
||||
name: "yank-diagnostic",
|
||||
aliases: &[],
|
||||
doc: "Yank diagnostic(s) under primary cursor to register, or clipboard by default",
|
||||
fun: yank_diagnostic,
|
||||
signature: CommandSignature::all(completers::register),
|
||||
},
|
||||
];
|
||||
|
||||
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
|
||||
|
20
helix-term/src/events.rs
Normal file
20
helix-term/src/events.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use helix_event::{events, register_event};
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::{DocumentDidChange, SelectionDidChange};
|
||||
|
||||
use crate::commands;
|
||||
use crate::keymap::MappableCommand;
|
||||
|
||||
events! {
|
||||
OnModeSwitch<'a, 'cx> { old_mode: Mode, new_mode: Mode, cx: &'a mut commands::Context<'cx> }
|
||||
PostInsertChar<'a, 'cx> { c: char, cx: &'a mut commands::Context<'cx> }
|
||||
PostCommand<'a, 'cx> { command: & 'a MappableCommand, cx: &'a mut commands::Context<'cx> }
|
||||
}
|
||||
|
||||
pub fn register() {
|
||||
register_event::<OnModeSwitch>();
|
||||
register_event::<PostInsertChar>();
|
||||
register_event::<PostCommand>();
|
||||
register_event::<DocumentDidChange>();
|
||||
register_event::<SelectionDidChange>();
|
||||
}
|
29
helix-term/src/handlers.rs
Normal file
29
helix-term/src/handlers.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use helix_event::AsyncHook;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::events;
|
||||
use crate::handlers::completion::CompletionHandler;
|
||||
use crate::handlers::signature_help::SignatureHelpHandler;
|
||||
|
||||
pub use completion::trigger_auto_completion;
|
||||
pub use helix_view::handlers::Handlers;
|
||||
|
||||
mod completion;
|
||||
mod signature_help;
|
||||
|
||||
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
events::register();
|
||||
|
||||
let completions = CompletionHandler::new(config).spawn();
|
||||
let signature_hints = SignatureHelpHandler::new().spawn();
|
||||
let handlers = Handlers {
|
||||
completions,
|
||||
signature_hints,
|
||||
};
|
||||
completion::register_hooks(&handlers);
|
||||
signature_help::register_hooks(&handlers);
|
||||
handlers
|
||||
}
|
473
helix-term/src/handlers/completion.rs
Normal file
473
helix-term/src/handlers/completion.rs
Normal file
@@ -0,0 +1,473 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use helix_core::chars::char_is_word;
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_event::{
|
||||
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||
};
|
||||
use helix_lsp::lsp;
|
||||
use helix_lsp::util::pos_to_lsp_pos;
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::{Mode, SavePoint};
|
||||
use helix_view::handlers::lsp::CompletionEvent;
|
||||
use helix_view::{DocumentId, Editor, ViewId};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::commands;
|
||||
use crate::compositor::Compositor;
|
||||
use crate::config::Config;
|
||||
use crate::events::{OnModeSwitch, PostCommand, PostInsertChar};
|
||||
use crate::job::{dispatch, dispatch_blocking};
|
||||
use crate::keymap::MappableCommand;
|
||||
use crate::ui::editor::InsertEvent;
|
||||
use crate::ui::lsp::SignatureHelp;
|
||||
use crate::ui::{self, CompletionItem, Popup};
|
||||
|
||||
use super::Handlers;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum TriggerKind {
|
||||
Auto,
|
||||
TriggerChar,
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Trigger {
|
||||
pos: usize,
|
||||
view: ViewId,
|
||||
doc: DocumentId,
|
||||
kind: TriggerKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct CompletionHandler {
|
||||
/// currently active trigger which will cause a
|
||||
/// completion request after the timeout
|
||||
trigger: Option<Trigger>,
|
||||
/// A handle for currently active completion request.
|
||||
/// This can be used to determine whether the current
|
||||
/// request is still active (and new triggers should be
|
||||
/// ignored) and can also be used to abort the current
|
||||
/// request (by dropping the handle)
|
||||
request: Option<CancelTx>,
|
||||
config: Arc<ArcSwap<Config>>,
|
||||
}
|
||||
|
||||
impl CompletionHandler {
|
||||
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
|
||||
Self {
|
||||
config,
|
||||
request: None,
|
||||
trigger: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for CompletionHandler {
|
||||
type Event = CompletionEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
_old_timeout: Option<Instant>,
|
||||
) -> Option<Instant> {
|
||||
match event {
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor: trigger_pos,
|
||||
doc,
|
||||
view,
|
||||
} => {
|
||||
// techically it shouldn't be possible to switch views/documents in insert mode
|
||||
// but people may create weird keymaps/use the mouse so lets be extra careful
|
||||
if self
|
||||
.trigger
|
||||
.as_ref()
|
||||
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
|
||||
{
|
||||
self.trigger = Some(Trigger {
|
||||
pos: trigger_pos,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Auto,
|
||||
});
|
||||
}
|
||||
}
|
||||
CompletionEvent::TriggerChar { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.request = None;
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::TriggerChar,
|
||||
});
|
||||
}
|
||||
CompletionEvent::ManualTrigger { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.request = None;
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Manual,
|
||||
});
|
||||
// stop debouncing immediately and request the completion
|
||||
self.finish_debounce();
|
||||
return None;
|
||||
}
|
||||
CompletionEvent::Cancel => {
|
||||
self.trigger = None;
|
||||
self.request = None;
|
||||
}
|
||||
CompletionEvent::DeleteText { cursor } => {
|
||||
// if we deleted the original trigger, abort the completion
|
||||
if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) {
|
||||
self.trigger = None;
|
||||
self.request = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.trigger.map(|trigger| {
|
||||
// if the current request was closed forget about it
|
||||
// otherwise immediately restart the completion request
|
||||
let cancel = self.request.take().map_or(false, |req| !req.is_closed());
|
||||
let timeout = if trigger.kind == TriggerKind::Auto && !cancel {
|
||||
self.config.load().editor.completion_timeout
|
||||
} else {
|
||||
// we want almost instant completions for trigger chars
|
||||
// and restarting completion requests. The small timeout here mainly
|
||||
// serves to better handle cases where the completion handler
|
||||
// may fall behind (so multiple events in the channel) and macros
|
||||
Duration::from_millis(5)
|
||||
};
|
||||
Instant::now() + timeout
|
||||
})
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let trigger = self.trigger.take().expect("debounce always has a trigger");
|
||||
let (tx, rx) = cancelation();
|
||||
self.request = Some(tx);
|
||||
dispatch_blocking(move |editor, compositor| {
|
||||
request_completion(trigger, rx, editor, compositor)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_completion(
|
||||
mut trigger: Trigger,
|
||||
cancel: CancelRx,
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
) {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.is_some()
|
||||
|| editor.mode != Mode::Insert
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let text = doc.text();
|
||||
let cursor = doc.selection(view.id).primary().cursor(text.slice(..));
|
||||
if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos {
|
||||
return;
|
||||
}
|
||||
// this looks odd... Why are we not using the trigger position from
|
||||
// the `trigger` here? Won't that mean that the trigger char doesn't get
|
||||
// send to the LS if we type fast enougn? Yes that is true but it's
|
||||
// not actually a problem. The LSP will resolve the completion to the identifier
|
||||
// anyway (in fact sending the later position is necessary to get the right results
|
||||
// from LSPs that provide incomplete completion list). We rely on trigger offset
|
||||
// and primary cursor matching for multi-cursor completions so this is definitely
|
||||
// necessary from our side too.
|
||||
trigger.pos = cursor;
|
||||
let trigger_text = text.slice(..cursor);
|
||||
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let mut futures: FuturesUnordered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|ls| {
|
||||
let language_server_id = ls.id();
|
||||
let offset_encoding = ls.offset_encoding();
|
||||
let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
|
||||
let doc_id = doc.identifier();
|
||||
let context = if trigger.kind == TriggerKind::Manual {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||
trigger_character: None,
|
||||
}
|
||||
} else {
|
||||
let trigger_char =
|
||||
ls.capabilities()
|
||||
.completion_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| {
|
||||
provider
|
||||
.trigger_characters
|
||||
.as_deref()?
|
||||
.iter()
|
||||
.find(|&trigger| trigger_text.ends_with(trigger))
|
||||
});
|
||||
|
||||
if trigger_char.is_some() {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
trigger_character: trigger_char.cloned(),
|
||||
}
|
||||
} else {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||
trigger_character: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
|
||||
async move {
|
||||
let json = completion_response.await?;
|
||||
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
||||
let items = match response {
|
||||
Some(lsp::CompletionResponse::Array(items)) => items,
|
||||
// TODO: do something with is_incomplete
|
||||
Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: _is_incomplete,
|
||||
items,
|
||||
})) => items,
|
||||
None => Vec::new(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|item| CompletionItem {
|
||||
item,
|
||||
language_server_id,
|
||||
resolved: false,
|
||||
})
|
||||
.collect();
|
||||
anyhow::Ok(items)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let future = async move {
|
||||
let mut items = Vec::new();
|
||||
while let Some(lsp_items) = futures.next().await {
|
||||
match lsp_items {
|
||||
Ok(mut lsp_items) => items.append(&mut lsp_items),
|
||||
Err(err) => {
|
||||
log::debug!("completion request failed: {err:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
items
|
||||
};
|
||||
|
||||
let savepoint = doc.savepoint(view);
|
||||
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
||||
tokio::spawn(async move {
|
||||
let items = cancelable_future(future, cancel).await.unwrap_or_default();
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
dispatch(move |editor, compositor| {
|
||||
show_completion(editor, compositor, items, trigger, savepoint)
|
||||
})
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
fn show_completion(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
items: Vec<CompletionItem>,
|
||||
trigger: Trigger,
|
||||
savepoint: Arc<SavePoint>,
|
||||
) {
|
||||
let (view, doc) = current_ref!(editor);
|
||||
// check if the completion request is stale.
|
||||
//
|
||||
// Completions are completed asynchronously and therefore the user could
|
||||
//switch document/view or leave insert mode. In all of thoise cases the
|
||||
// completion should be discarded
|
||||
if editor.mode != Mode::Insert || view.id != trigger.view || doc.id() != trigger.doc {
|
||||
return;
|
||||
}
|
||||
|
||||
let size = compositor.size();
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
if ui.completion.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
|
||||
let signature_help_area = compositor
|
||||
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
|
||||
.map(|signature_help| signature_help.area(size, editor));
|
||||
// Delete the signature help popup if they intersect.
|
||||
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_auto_completion(
|
||||
tx: &Sender<CompletionEvent>,
|
||||
editor: &Editor,
|
||||
trigger_char_only: bool,
|
||||
) {
|
||||
let config = editor.config.load();
|
||||
if !config.auto_completion {
|
||||
return;
|
||||
}
|
||||
let (view, doc): (&helix_view::View, &helix_view::Document) = current_ref!(editor);
|
||||
let mut text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
text = doc.text().slice(..cursor);
|
||||
|
||||
let is_trigger_char = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.any(|ls| {
|
||||
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(triggers),
|
||||
..
|
||||
}) if triggers.iter().any(|trigger| text.ends_with(trigger)))
|
||||
});
|
||||
if is_trigger_char {
|
||||
send_blocking(
|
||||
tx,
|
||||
CompletionEvent::TriggerChar {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let is_auto_trigger = !trigger_char_only
|
||||
&& doc
|
||||
.text()
|
||||
.chars_at(cursor)
|
||||
.reversed()
|
||||
.take(config.completion_trigger_len as usize)
|
||||
.all(char_is_word);
|
||||
|
||||
if is_auto_trigger {
|
||||
send_blocking(
|
||||
tx,
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_completions(cx: &mut commands::Context, c: Option<char>) {
|
||||
cx.callback.push(Box::new(move |compositor, cx| {
|
||||
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||
if let Some(completion) = &mut editor_view.completion {
|
||||
completion.update_filter(c);
|
||||
if completion.is_empty() {
|
||||
editor_view.clear_completion(cx.editor);
|
||||
// clearing completions might mean we want to immediately rerequest them (usually
|
||||
// this occurs if typing a trigger char)
|
||||
if c.is_some() {
|
||||
trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn clear_completions(cx: &mut commands::Context) {
|
||||
cx.callback.push(Box::new(|compositor, cx| {
|
||||
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||
editor_view.clear_completion(cx.editor);
|
||||
}))
|
||||
}
|
||||
|
||||
fn completion_post_command_hook(
|
||||
tx: &Sender<CompletionEvent>,
|
||||
PostCommand { command, cx }: &mut PostCommand<'_, '_>,
|
||||
) -> anyhow::Result<()> {
|
||||
if cx.editor.mode == Mode::Insert {
|
||||
if cx.editor.last_completion.is_some() {
|
||||
match command {
|
||||
MappableCommand::Static {
|
||||
name: "delete_word_forward" | "delete_char_forward" | "completion",
|
||||
..
|
||||
} => (),
|
||||
MappableCommand::Static {
|
||||
name: "delete_char_backward",
|
||||
..
|
||||
} => update_completions(cx, None),
|
||||
_ => clear_completions(cx),
|
||||
}
|
||||
} else {
|
||||
let event = match command {
|
||||
MappableCommand::Static {
|
||||
name: "delete_char_backward" | "delete_word_forward" | "delete_char_forward",
|
||||
..
|
||||
} => {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let primary_cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
CompletionEvent::DeleteText {
|
||||
cursor: primary_cursor,
|
||||
}
|
||||
}
|
||||
// hacks: some commands are handeled elsewhere and we don't want to
|
||||
// cancel in that case
|
||||
MappableCommand::Static {
|
||||
name: "completion" | "insert_mode" | "append_mode",
|
||||
..
|
||||
} => return Ok(()),
|
||||
_ => CompletionEvent::Cancel,
|
||||
};
|
||||
send_blocking(tx, event);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event));
|
||||
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||
if event.old_mode == Mode::Insert {
|
||||
send_blocking(&tx, CompletionEvent::Cancel);
|
||||
clear_completions(event.cx);
|
||||
} else if event.new_mode == Mode::Insert {
|
||||
trigger_auto_completion(&tx, event.cx.editor, false)
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
|
||||
if event.cx.editor.last_completion.is_some() {
|
||||
update_completions(event.cx, Some(event.c))
|
||||
} else {
|
||||
trigger_auto_completion(&tx, event.cx.editor, false);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
335
helix-term/src/handlers/signature_help.rs
Normal file
335
helix-term/src/handlers/signature_help.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_event::{
|
||||
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||
};
|
||||
use helix_lsp::lsp;
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::{DocumentDidChange, SelectionDidChange};
|
||||
use helix_view::handlers::lsp::{SignatureHelpEvent, SignatureHelpInvoked};
|
||||
use helix_view::Editor;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::commands::Open;
|
||||
use crate::compositor::Compositor;
|
||||
use crate::events::{OnModeSwitch, PostInsertChar};
|
||||
use crate::handlers::Handlers;
|
||||
use crate::ui::lsp::SignatureHelp;
|
||||
use crate::ui::Popup;
|
||||
use crate::{job, ui};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Open,
|
||||
Closed,
|
||||
Pending { request: CancelTx },
|
||||
}
|
||||
|
||||
/// debounce timeout in ms, value taken from VSCode
|
||||
/// TODO: make this configurable?
|
||||
const TIMEOUT: u64 = 120;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct SignatureHelpHandler {
|
||||
trigger: Option<SignatureHelpInvoked>,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl SignatureHelpHandler {
|
||||
pub fn new() -> SignatureHelpHandler {
|
||||
SignatureHelpHandler {
|
||||
trigger: None,
|
||||
state: State::Closed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for SignatureHelpHandler {
|
||||
type Event = SignatureHelpEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<Instant> {
|
||||
match event {
|
||||
SignatureHelpEvent::Invoked => {
|
||||
self.trigger = Some(SignatureHelpInvoked::Manual);
|
||||
self.state = State::Closed;
|
||||
self.finish_debounce();
|
||||
return None;
|
||||
}
|
||||
SignatureHelpEvent::Trigger => {}
|
||||
SignatureHelpEvent::ReTrigger => {
|
||||
// don't retrigger if we aren't open/pending yet
|
||||
if matches!(self.state, State::Closed) {
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
SignatureHelpEvent::Cancel => {
|
||||
self.state = State::Closed;
|
||||
return None;
|
||||
}
|
||||
SignatureHelpEvent::RequestComplete { open } => {
|
||||
// don't cancel rerequest that was already triggered
|
||||
if let State::Pending { request } = &self.state {
|
||||
if !request.is_closed() {
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
self.state = if open { State::Open } else { State::Closed };
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
if self.trigger.is_none() {
|
||||
self.trigger = Some(SignatureHelpInvoked::Automatic)
|
||||
}
|
||||
Some(Instant::now() + Duration::from_millis(TIMEOUT))
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let invocation = self.trigger.take().unwrap();
|
||||
let (tx, rx) = cancelation();
|
||||
self.state = State::Pending { request: tx };
|
||||
job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, rx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_signature_help(
|
||||
editor: &mut Editor,
|
||||
invoked: SignatureHelpInvoked,
|
||||
cancel: CancelRx,
|
||||
) {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
|
||||
let future = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||
.find_map(|language_server| {
|
||||
let pos = doc.position(view.id, language_server.offset_encoding());
|
||||
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
||||
});
|
||||
|
||||
let Some(future) = future else {
|
||||
// Do not show the message if signature help was invoked
|
||||
// automatically on backspace, trigger characters, etc.
|
||||
if invoked == SignatureHelpInvoked::Manual {
|
||||
editor
|
||||
.set_error("No configured language server supports signature-help");
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
match cancelable_future(future, cancel).await {
|
||||
Some(Ok(res)) => {
|
||||
job::dispatch(move |editor, compositor| {
|
||||
show_signature_help(editor, compositor, invoked, res)
|
||||
})
|
||||
.await
|
||||
}
|
||||
Some(Err(err)) => log::error!("signature help request failed: {err}"),
|
||||
None => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn show_signature_help(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
invoked: SignatureHelpInvoked,
|
||||
response: Option<lsp::SignatureHelp>,
|
||||
) {
|
||||
let config = &editor.config();
|
||||
|
||||
if !(config.lsp.auto_signature_help
|
||||
|| SignatureHelp::visible_popup(compositor).is_some()
|
||||
|| invoked == SignatureHelpInvoked::Manual)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
|
||||
// it very probably means the server was a little slow to respond and the user has
|
||||
// already moved on to something else, making a signature help popup will just be an
|
||||
// annoyance, see https://github.com/helix-editor/helix/issues/3112
|
||||
// For the most part this should not be needed as the request gets canceled automatically now
|
||||
// but it's technically possible for the mode change to just preempt this callback so better safe than sorry
|
||||
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match response {
|
||||
// According to the spec the response should be None if there
|
||||
// are no signatures, but some servers don't follow this.
|
||||
Some(s) if !s.signatures.is_empty() => s,
|
||||
_ => {
|
||||
send_blocking(
|
||||
&editor.handlers.signature_hints,
|
||||
SignatureHelpEvent::RequestComplete { open: false },
|
||||
);
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
return;
|
||||
}
|
||||
};
|
||||
send_blocking(
|
||||
&editor.handlers.signature_hints,
|
||||
SignatureHelpEvent::RequestComplete { open: true },
|
||||
);
|
||||
|
||||
let doc = doc!(editor);
|
||||
let language = doc.language_name().unwrap_or("");
|
||||
|
||||
let signature = match response
|
||||
.signatures
|
||||
.get(response.active_signature.unwrap_or(0) as usize)
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
let mut contents = SignatureHelp::new(
|
||||
signature.label.clone(),
|
||||
language.to_string(),
|
||||
Arc::clone(&editor.syn_loader),
|
||||
);
|
||||
|
||||
let signature_doc = if config.lsp.display_signature_help_docs {
|
||||
signature.documentation.as_ref().map(|doc| match doc {
|
||||
lsp::Documentation::String(s) => s.clone(),
|
||||
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
contents.set_signature_doc(signature_doc);
|
||||
|
||||
let active_param_range = || -> Option<(usize, usize)> {
|
||||
let param_idx = signature
|
||||
.active_parameter
|
||||
.or(response.active_parameter)
|
||||
.unwrap_or(0) as usize;
|
||||
let param = signature.parameters.as_ref()?.get(param_idx)?;
|
||||
match ¶m.label {
|
||||
lsp::ParameterLabel::Simple(string) => {
|
||||
let start = signature.label.find(string.as_str())?;
|
||||
Some((start, start + string.len()))
|
||||
}
|
||||
lsp::ParameterLabel::LabelOffsets([start, end]) => {
|
||||
// LS sends offsets based on utf-16 based string representation
|
||||
// but highlighting in helix is done using byte offset.
|
||||
use helix_core::str_utils::char_to_byte_idx;
|
||||
let from = char_to_byte_idx(&signature.label, *start as usize);
|
||||
let to = char_to_byte_idx(&signature.label, *end as usize);
|
||||
Some((from, to))
|
||||
}
|
||||
}
|
||||
};
|
||||
contents.set_active_param_range(active_param_range());
|
||||
|
||||
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
|
||||
let mut popup = Popup::new(SignatureHelp::ID, contents)
|
||||
.position(old_popup.and_then(|p| p.get_position()))
|
||||
.position_bias(Open::Above)
|
||||
.ignore_escape_key(true);
|
||||
|
||||
// Don't create a popup if it intersects the auto-complete menu.
|
||||
let size = compositor.size();
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.as_mut()
|
||||
.map(|completion| completion.area(size, editor))
|
||||
.filter(|area| area.intersects(popup.area(size, editor)))
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
compositor.replace_or_push(SignatureHelp::ID, popup);
|
||||
}
|
||||
|
||||
fn signature_help_post_insert_char_hook(
|
||||
tx: &Sender<SignatureHelpEvent>,
|
||||
PostInsertChar { cx, .. }: &mut PostInsertChar<'_, '_>,
|
||||
) -> anyhow::Result<()> {
|
||||
if !cx.editor.config().lsp.auto_signature_help {
|
||||
return Ok(());
|
||||
}
|
||||
let (view, doc) = current!(cx.editor);
|
||||
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
|
||||
let Some(language_server) = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||
.next()
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let capabilities = language_server.capabilities();
|
||||
|
||||
if let lsp::ServerCapabilities {
|
||||
signature_help_provider:
|
||||
Some(lsp::SignatureHelpOptions {
|
||||
trigger_characters: Some(triggers),
|
||||
// TODO: retrigger_characters
|
||||
..
|
||||
}),
|
||||
..
|
||||
} = capabilities
|
||||
{
|
||||
let mut text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
text = text.slice(..cursor);
|
||||
if triggers.iter().any(|trigger| text.ends_with(trigger)) {
|
||||
send_blocking(tx, SignatureHelpEvent::Trigger)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||
match (event.old_mode, event.new_mode) {
|
||||
(Mode::Insert, _) => {
|
||||
send_blocking(&tx, SignatureHelpEvent::Cancel);
|
||||
event.cx.callback.push(Box::new(|compositor, _| {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
}));
|
||||
}
|
||||
(_, Mode::Insert) => {
|
||||
if event.cx.editor.config().lsp.auto_signature_help {
|
||||
send_blocking(&tx, SignatureHelpEvent::Trigger);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(
|
||||
move |event: &mut PostInsertChar<'_, '_>| signature_help_post_insert_char_hook(&tx, event)
|
||||
);
|
||||
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||
if event.doc.config.load().lsp.auto_signature_help {
|
||||
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(move |event: &mut SelectionDidChange<'_>| {
|
||||
if event.doc.config.load().lsp.auto_signature_help {
|
||||
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
@@ -2,7 +2,7 @@ use crossterm::{
|
||||
style::{Color, Print, Stylize},
|
||||
tty::IsTty,
|
||||
};
|
||||
use helix_core::config::{default_syntax_loader, user_syntax_loader};
|
||||
use helix_core::config::{default_lang_config, user_lang_config};
|
||||
use helix_loader::grammar::load_runtime_file;
|
||||
use helix_view::clipboard::get_clipboard_provider;
|
||||
use std::io::Write;
|
||||
@@ -128,7 +128,7 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
let mut syn_loader_conf = match user_syntax_loader() {
|
||||
let mut syn_loader_conf = match user_lang_config() {
|
||||
Ok(conf) => conf,
|
||||
Err(err) => {
|
||||
let stderr = std::io::stderr();
|
||||
@@ -141,7 +141,7 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
err
|
||||
)?;
|
||||
writeln!(stderr, "{}", "Using default language config".yellow())?;
|
||||
default_syntax_loader()
|
||||
default_lang_config()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -182,7 +182,7 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
.sort_unstable_by_key(|l| l.language_id.clone());
|
||||
|
||||
let check_binary = |cmd: Option<&str>| match cmd {
|
||||
Some(cmd) => match which::which(cmd) {
|
||||
Some(cmd) => match helix_stdx::env::which(cmd) {
|
||||
Ok(_) => column(&format!("✓ {}", cmd), Color::Green),
|
||||
Err(_) => column(&format!("✘ {}", cmd), Color::Red),
|
||||
},
|
||||
@@ -234,7 +234,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
let syn_loader_conf = match user_syntax_loader() {
|
||||
let syn_loader_conf = match user_lang_config() {
|
||||
Ok(conf) => conf,
|
||||
Err(err) => {
|
||||
let stderr = std::io::stderr();
|
||||
@@ -247,7 +247,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
|
||||
err
|
||||
)?;
|
||||
writeln!(stderr, "{}", "Using default language config".yellow())?;
|
||||
default_syntax_loader()
|
||||
default_lang_config()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -322,7 +322,7 @@ fn probe_protocols<'a, I: Iterator<Item = &'a str> + 'a>(
|
||||
writeln!(stdout)?;
|
||||
|
||||
for cmd in server_cmds {
|
||||
let (path, icon) = match which::which(cmd) {
|
||||
let (path, icon) = match helix_stdx::env::which(cmd) {
|
||||
Ok(path) => (path.display().to_string().green(), "✓".green()),
|
||||
Err(_) => (format!("'{}' not found in $PATH", cmd).red(), "✘".red()),
|
||||
};
|
||||
@@ -344,7 +344,7 @@ fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::R
|
||||
writeln!(stdout, "Configured {}: {}", protocol_name, cmd_name)?;
|
||||
|
||||
if let Some(cmd) = server_cmd {
|
||||
let path = match which::which(&cmd) {
|
||||
let path = match helix_stdx::env::which(&cmd) {
|
||||
Ok(path) => path.display().to_string().green(),
|
||||
Err(_) => format!("'{}' not found in $PATH", cmd).red(),
|
||||
};
|
||||
|
@@ -1,13 +1,37 @@
|
||||
use helix_event::status::StatusMessage;
|
||||
use helix_event::{runtime_local, send_blocking};
|
||||
use helix_view::Editor;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::compositor::Compositor;
|
||||
|
||||
use futures_util::future::{BoxFuture, Future, FutureExt};
|
||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||
|
||||
pub type EditorCompositorCallback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
|
||||
pub type EditorCallback = Box<dyn FnOnce(&mut Editor) + Send>;
|
||||
|
||||
runtime_local! {
|
||||
static JOB_QUEUE: OnceCell<Sender<Callback>> = OnceCell::new();
|
||||
}
|
||||
|
||||
pub async fn dispatch_callback(job: Callback) {
|
||||
let _ = JOB_QUEUE.wait().send(job).await;
|
||||
}
|
||||
|
||||
pub async fn dispatch(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
|
||||
let _ = JOB_QUEUE
|
||||
.wait()
|
||||
.send(Callback::EditorCompositor(Box::new(job)))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub fn dispatch_blocking(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
|
||||
let jobs = JOB_QUEUE.wait();
|
||||
send_blocking(jobs, Callback::EditorCompositor(Box::new(job)))
|
||||
}
|
||||
|
||||
pub enum Callback {
|
||||
EditorCompositor(EditorCompositorCallback),
|
||||
Editor(EditorCallback),
|
||||
@@ -21,11 +45,11 @@ pub struct Job {
|
||||
pub wait: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Jobs {
|
||||
pub futures: FuturesUnordered<JobFuture>,
|
||||
/// These are the ones that need to complete before we exit.
|
||||
/// jobs that need to complete before we exit.
|
||||
pub wait_futures: FuturesUnordered<JobFuture>,
|
||||
pub callbacks: Receiver<Callback>,
|
||||
pub status_messages: Receiver<StatusMessage>,
|
||||
}
|
||||
|
||||
impl Job {
|
||||
@@ -52,8 +76,16 @@ impl Job {
|
||||
}
|
||||
|
||||
impl Jobs {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
let (tx, rx) = channel(1024);
|
||||
let _ = JOB_QUEUE.set(tx);
|
||||
let status_messages = helix_event::status::setup();
|
||||
Self {
|
||||
wait_futures: FuturesUnordered::new(),
|
||||
callbacks: rx,
|
||||
status_messages,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
|
||||
@@ -85,18 +117,17 @@ impl Jobs {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
|
||||
tokio::select! {
|
||||
event = self.futures.next() => { event }
|
||||
event = self.wait_futures.next() => { event }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&self, j: Job) {
|
||||
if j.wait {
|
||||
self.wait_futures.push(j.future);
|
||||
} else {
|
||||
self.futures.push(j.future);
|
||||
tokio::spawn(async move {
|
||||
match j.future.await {
|
||||
Ok(Some(cb)) => dispatch_callback(cb).await,
|
||||
Ok(None) => (),
|
||||
Err(err) => helix_event::status::report(err).await,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -303,6 +303,15 @@ impl Keymaps {
|
||||
self.sticky.as_ref()
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, mode: Mode, key: KeyEvent) -> bool {
|
||||
let keymaps = &*self.map();
|
||||
let keymap = &keymaps[&mode];
|
||||
keymap
|
||||
.search(self.pending())
|
||||
.and_then(KeyTrie::node)
|
||||
.is_some_and(|node| node.contains_key(&key))
|
||||
}
|
||||
|
||||
/// Lookup `key` in the keymap to try and find a command to execute. Escape
|
||||
/// key cancels pending keystrokes. If there are no pending keystrokes but a
|
||||
/// sticky node is in use, it will be cleared.
|
||||
@@ -319,7 +328,7 @@ impl Keymaps {
|
||||
self.sticky = None;
|
||||
}
|
||||
|
||||
let first = self.state.get(0).unwrap_or(&key);
|
||||
let first = self.state.first().unwrap_or(&key);
|
||||
let trie_node = match self.sticky {
|
||||
Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())),
|
||||
None => Cow::Borrowed(keymap),
|
||||
|
@@ -58,6 +58,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"k" => move_line_up,
|
||||
"j" => move_line_down,
|
||||
"." => goto_last_modification,
|
||||
"w" => goto_word,
|
||||
},
|
||||
":" => command_mode,
|
||||
|
||||
@@ -178,8 +179,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"esc" => normal_mode,
|
||||
"C-b" | "pageup" => page_up,
|
||||
"C-f" | "pagedown" => page_down,
|
||||
"C-u" => half_page_up,
|
||||
"C-d" => half_page_down,
|
||||
"C-u" => page_cursor_half_up,
|
||||
"C-d" => page_cursor_half_down,
|
||||
|
||||
"C-w" => { "Window"
|
||||
"C-w" | "w" => rotate_view,
|
||||
@@ -276,6 +277,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"k" => hover,
|
||||
"r" => rename_symbol,
|
||||
"h" => select_references_to_symbol_under_cursor,
|
||||
"c" => toggle_comments,
|
||||
"C" => toggle_block_comments,
|
||||
"A-c" => toggle_line_comments,
|
||||
"?" => command_palette,
|
||||
},
|
||||
"z" => { "View"
|
||||
@@ -287,8 +291,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"j" | "down" => scroll_down,
|
||||
"C-b" | "pageup" => page_up,
|
||||
"C-f" | "pagedown" => page_down,
|
||||
"C-u" | "backspace" => half_page_up,
|
||||
"C-d" | "space" => half_page_down,
|
||||
"C-u" | "backspace" => page_cursor_half_up,
|
||||
"C-d" | "space" => page_cursor_half_down,
|
||||
|
||||
"/" => search,
|
||||
"?" => rsearch,
|
||||
@@ -304,8 +308,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"j" | "down" => scroll_down,
|
||||
"C-b" | "pageup" => page_up,
|
||||
"C-f" | "pagedown" => page_down,
|
||||
"C-u" | "backspace" => half_page_up,
|
||||
"C-d" | "space" => half_page_down,
|
||||
"C-u" | "backspace" => page_cursor_half_up,
|
||||
"C-d" | "space" => page_cursor_half_down,
|
||||
|
||||
"/" => search,
|
||||
"?" => rsearch,
|
||||
@@ -357,6 +361,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"g" => { "Goto"
|
||||
"k" => extend_line_up,
|
||||
"j" => extend_line_down,
|
||||
"w" => extend_to_word,
|
||||
},
|
||||
}));
|
||||
let insert = keymap!({ "Insert mode"
|
||||
|
@@ -6,31 +6,53 @@ pub mod args;
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
pub mod health;
|
||||
pub mod job;
|
||||
pub mod keymap;
|
||||
pub mod ui;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use ignore::DirEntry;
|
||||
use futures_util::Future;
|
||||
mod handlers;
|
||||
|
||||
use ignore::DirEntry;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn true_color() -> bool {
|
||||
std::env::var("COLORTERM")
|
||||
.map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
fn true_color() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn true_color() -> bool {
|
||||
if matches!(
|
||||
std::env::var("COLORTERM").map(|v| matches!(v.as_str(), "truecolor" | "24bit")),
|
||||
Ok(true)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match termini::TermInfo::from_env() {
|
||||
Ok(t) => {
|
||||
t.extended_cap("RGB").is_some()
|
||||
|| t.extended_cap("Tc").is_some()
|
||||
|| (t.extended_cap("setrgbf").is_some() && t.extended_cap("setrgbb").is_some())
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Function used for filtering dir entries in the various file pickers.
|
||||
fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> bool {
|
||||
// We always want to ignore the .git directory, otherwise if
|
||||
// We always want to ignore popular VCS directories, otherwise if
|
||||
// `ignore` is turned off, we end up with a lot of noise
|
||||
// in our picker.
|
||||
if entry.file_name() == ".git" {
|
||||
if matches!(
|
||||
entry.file_name().to_str(),
|
||||
Some(".git" | ".pijul" | ".jj" | ".hg")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -46,3 +68,22 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Opens URL in external program.
|
||||
fn open_external_url_callback(
|
||||
url: Url,
|
||||
) -> impl Future<Output = Result<job::Callback, anyhow::Error>> + Send + 'static {
|
||||
let commands = open::commands(url.as_str());
|
||||
async {
|
||||
for cmd in commands {
|
||||
let mut command = tokio::process::Command::new(cmd.get_program());
|
||||
command.args(cmd.get_args());
|
||||
if command.output().await.is_ok() {
|
||||
return Ok(job::Callback::Editor(Box::new(|_| {})));
|
||||
}
|
||||
}
|
||||
Ok(job::Callback::Editor(Box::new(move |editor| {
|
||||
editor.set_error("Opening URL in external program failed")
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
@@ -118,16 +118,16 @@ FLAGS:
|
||||
|
||||
// Before setting the working directory, resolve all the paths in args.files
|
||||
for (path, _) in args.files.iter_mut() {
|
||||
*path = helix_core::path::get_canonicalized_path(path);
|
||||
*path = helix_stdx::path::canonicalize(&path);
|
||||
}
|
||||
|
||||
// NOTE: Set the working directory early so the correct configuration is loaded. Be aware that
|
||||
// Application::new() depends on this logic so it must be updated if this changes.
|
||||
if let Some(path) = &args.working_directory {
|
||||
helix_loader::set_current_working_dir(path)?;
|
||||
helix_stdx::env::set_current_working_dir(path)?;
|
||||
} else if let Some((path, _)) = args.files.first().filter(|p| p.0.is_dir()) {
|
||||
// If the first file is a directory, it will be the working directory unless -w was specified
|
||||
helix_loader::set_current_working_dir(path)?;
|
||||
helix_stdx::env::set_current_working_dir(path)?;
|
||||
}
|
||||
|
||||
let config = match Config::load_default() {
|
||||
@@ -145,18 +145,18 @@ FLAGS:
|
||||
}
|
||||
};
|
||||
|
||||
let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
|
||||
eprintln!("Bad language config: {}", err);
|
||||
let lang_loader = helix_core::config::user_lang_loader().unwrap_or_else(|err| {
|
||||
eprintln!("{}", err);
|
||||
eprintln!("Press <ENTER> to continue with default language config");
|
||||
use std::io::Read;
|
||||
// This waits for an enter press.
|
||||
let _ = std::io::stdin().read(&mut []);
|
||||
helix_core::config::default_syntax_loader()
|
||||
helix_core::config::default_lang_loader()
|
||||
});
|
||||
|
||||
// TODO: use the thread local executor to spawn the application task separately from the work pool
|
||||
let mut app = Application::new(args, config, syn_loader_conf)
|
||||
.context("unable to create new application")?;
|
||||
let mut app =
|
||||
Application::new(args, config, lang_loader).context("unable to create new application")?;
|
||||
|
||||
let exit_code = app.run(&mut EventStream::new()).await?;
|
||||
|
||||
|
@@ -1,19 +1,25 @@
|
||||
use crate::compositor::{Component, Context, Event, EventResult};
|
||||
use crate::{
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
handlers::trigger_auto_completion,
|
||||
job,
|
||||
};
|
||||
use helix_event::AsyncHook;
|
||||
use helix_view::{
|
||||
document::SavePoint,
|
||||
editor::CompleteAction,
|
||||
graphics::Margin,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
theme::{Modifier, Style},
|
||||
ViewId,
|
||||
};
|
||||
use tokio::time::Instant;
|
||||
use tui::{buffer::Buffer as Surface, text::Span};
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use std::{borrow::Cow, sync::Arc, time::Duration};
|
||||
|
||||
use helix_core::{Change, Transaction};
|
||||
use helix_core::{chars, Change, Transaction};
|
||||
use helix_view::{graphics::Rect, Document, Editor};
|
||||
|
||||
use crate::commands;
|
||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
|
||||
use helix_lsp::{lsp, util, OffsetEncoding};
|
||||
@@ -95,10 +101,10 @@ pub struct CompletionItem {
|
||||
/// Wraps a Menu.
|
||||
pub struct Completion {
|
||||
popup: Popup<Menu<CompletionItem>>,
|
||||
start_offset: usize,
|
||||
#[allow(dead_code)]
|
||||
trigger_offset: usize,
|
||||
// TODO: maintain a completioncontext with trigger kind & trigger char
|
||||
filter: String,
|
||||
resolve_handler: tokio::sync::mpsc::Sender<CompletionItem>,
|
||||
}
|
||||
|
||||
impl Completion {
|
||||
@@ -108,7 +114,6 @@ impl Completion {
|
||||
editor: &Editor,
|
||||
savepoint: Arc<SavePoint>,
|
||||
mut items: Vec<CompletionItem>,
|
||||
start_offset: usize,
|
||||
trigger_offset: usize,
|
||||
) -> Self {
|
||||
let preview_completion_insert = editor.config().preview_completion_insert;
|
||||
@@ -246,7 +251,7 @@ impl Completion {
|
||||
// (also without sending the transaction to the LS) *before any further transaction is applied*.
|
||||
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
|
||||
// is applied to).
|
||||
if editor.last_completion.is_none() {
|
||||
if matches!(editor.last_completion, Some(CompleteAction::Triggered)) {
|
||||
editor.last_completion = Some(CompleteAction::Selected {
|
||||
savepoint: doc.savepoint(view),
|
||||
})
|
||||
@@ -324,8 +329,18 @@ impl Completion {
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
}
|
||||
// we could have just inserted a trigger char (like a `crate::` completion for rust
|
||||
// so we want to retrigger immediately when accepting a completion.
|
||||
trigger_auto_completion(&editor.handlers.completions, editor, true);
|
||||
}
|
||||
};
|
||||
|
||||
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
|
||||
if event != PromptEvent::Update {
|
||||
editor
|
||||
.handlers
|
||||
.trigger_signature_help(SignatureHelpInvoked::Automatic, editor);
|
||||
}
|
||||
});
|
||||
|
||||
let margin = if editor.menu_border() {
|
||||
@@ -339,18 +354,37 @@ impl Completion {
|
||||
.ignore_escape_key(true)
|
||||
.margin(margin);
|
||||
|
||||
let (view, doc) = current_ref!(editor);
|
||||
let text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
let offset = text
|
||||
.chars_at(cursor)
|
||||
.reversed()
|
||||
.take_while(|ch| chars::char_is_word(*ch))
|
||||
.count();
|
||||
let start_offset = cursor.saturating_sub(offset);
|
||||
|
||||
let fragment = doc.text().slice(start_offset..cursor);
|
||||
let mut completion = Self {
|
||||
popup,
|
||||
start_offset,
|
||||
trigger_offset,
|
||||
// TODO: expand nucleo api to allow moving straight to a Utf32String here
|
||||
// and avoid allocation during matching
|
||||
filter: String::from(fragment),
|
||||
resolve_handler: ResolveHandler::default().spawn(),
|
||||
};
|
||||
|
||||
// need to recompute immediately in case start_offset != trigger_offset
|
||||
completion.recompute_filter(editor);
|
||||
completion
|
||||
.popup
|
||||
.contents_mut()
|
||||
.score(&completion.filter, false);
|
||||
|
||||
completion
|
||||
}
|
||||
|
||||
/// Synchronously resolve the given completion item. This is used when
|
||||
/// accepting a completion.
|
||||
fn resolve_completion_item(
|
||||
language_server: &helix_lsp::Client,
|
||||
completion_item: lsp::CompletionItem,
|
||||
@@ -358,7 +392,7 @@ impl Completion {
|
||||
let future = language_server.resolve_completion_item(completion_item)?;
|
||||
let response = helix_lsp::block_on(future);
|
||||
match response {
|
||||
Ok(value) => serde_json::from_value(value).ok(),
|
||||
Ok(item) => Some(item),
|
||||
Err(err) => {
|
||||
log::error!("Failed to resolve completion item: {}", err);
|
||||
None
|
||||
@@ -366,39 +400,22 @@ impl Completion {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recompute_filter(&mut self, editor: &Editor) {
|
||||
/// Appends (`c: Some(c)`) or removes (`c: None`) a character to/from the filter
|
||||
/// this should be called whenever the user types or deletes a character in insert mode.
|
||||
pub fn update_filter(&mut self, c: Option<char>) {
|
||||
// recompute menu based on matches
|
||||
let menu = self.popup.contents_mut();
|
||||
let (view, doc) = current_ref!(editor);
|
||||
|
||||
// cx.hooks()
|
||||
// cx.add_hook(enum type, ||)
|
||||
// cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
|
||||
// callback with editor & compositor
|
||||
//
|
||||
// trigger_hook sends event into channel, that's consumed in the global loop and
|
||||
// triggers all registered callbacks
|
||||
// TODO: hooks should get processed immediately so maybe do it after select!(), before
|
||||
// looping?
|
||||
|
||||
let cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
if self.trigger_offset <= cursor {
|
||||
let fragment = doc.text().slice(self.start_offset..cursor);
|
||||
let text = Cow::from(fragment);
|
||||
// TODO: logic is same as ui/picker
|
||||
menu.score(&text);
|
||||
} else {
|
||||
// we backspaced before the start offset, clear the menu
|
||||
// this will cause the editor to remove the completion popup
|
||||
match c {
|
||||
Some(c) => self.filter.push(c),
|
||||
None => {
|
||||
self.filter.pop();
|
||||
if self.filter.is_empty() {
|
||||
menu.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, cx: &mut commands::Context) {
|
||||
self.recompute_filter(cx.editor)
|
||||
}
|
||||
menu.score(&self.filter, c.is_some());
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -409,62 +426,6 @@ impl Completion {
|
||||
self.popup.contents_mut().replace_option(old_item, new_item);
|
||||
}
|
||||
|
||||
/// Asynchronously requests that the currently selection completion item is
|
||||
/// resolved through LSP `completionItem/resolve`.
|
||||
pub fn ensure_item_resolved(&mut self, cx: &mut commands::Context) -> bool {
|
||||
// > If computing full completion items is expensive, servers can additionally provide a
|
||||
// > handler for the completion item resolve request. ...
|
||||
// > A typical use case is for example: the `textDocument/completion` request doesn't fill
|
||||
// > in the `documentation` property for returned completion items since it is expensive
|
||||
// > to compute. When the item is selected in the user interface then a
|
||||
// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
||||
// > The returned completion item should have the documentation property filled in.
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
|
||||
let current_item = match self.popup.contents().selection() {
|
||||
Some(item) if !item.resolved => item.clone(),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let Some(language_server) = cx
|
||||
.editor
|
||||
.language_server_by_id(current_item.language_server_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// This method should not block the compositor so we handle the response asynchronously.
|
||||
let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
cx.callback(
|
||||
future,
|
||||
move |_editor, compositor, response: Option<lsp::CompletionItem>| {
|
||||
let resolved_item = match response {
|
||||
Some(item) => item,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if let Some(completion) = &mut compositor
|
||||
.find::<crate::ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
{
|
||||
let resolved_item = CompletionItem {
|
||||
item: resolved_item,
|
||||
language_server_id: current_item.language_server_id,
|
||||
resolved: true,
|
||||
};
|
||||
|
||||
completion.replace_item(current_item, resolved_item);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
|
||||
self.popup.area(viewport, editor)
|
||||
}
|
||||
@@ -487,6 +448,9 @@ impl Component for Completion {
|
||||
Some(option) => option,
|
||||
None => return,
|
||||
};
|
||||
if !option.resolved {
|
||||
helix_event::send_blocking(&self.resolve_handler, option.clone());
|
||||
}
|
||||
// need to render:
|
||||
// option.detail
|
||||
// ---
|
||||
@@ -588,3 +552,88 @@ impl Component for Completion {
|
||||
markdown_doc.render(doc_area, surface, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// A hook for resolving incomplete completion items.
|
||||
///
|
||||
/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion):
|
||||
///
|
||||
/// > If computing full completion items is expensive, servers can additionally provide a
|
||||
/// > handler for the completion item resolve request. ...
|
||||
/// > A typical use case is for example: the `textDocument/completion` request doesn't fill
|
||||
/// > in the `documentation` property for returned completion items since it is expensive
|
||||
/// > to compute. When the item is selected in the user interface then a
|
||||
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
||||
/// > The returned completion item should have the documentation property filled in.
|
||||
#[derive(Debug, Default)]
|
||||
struct ResolveHandler {
|
||||
trigger: Option<CompletionItem>,
|
||||
request: Option<helix_event::CancelTx>,
|
||||
}
|
||||
|
||||
impl AsyncHook for ResolveHandler {
|
||||
type Event = CompletionItem;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
item: Self::Event,
|
||||
timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
if self
|
||||
.trigger
|
||||
.as_ref()
|
||||
.is_some_and(|trigger| trigger == &item)
|
||||
{
|
||||
timeout
|
||||
} else {
|
||||
self.trigger = Some(item);
|
||||
self.request = None;
|
||||
Some(Instant::now() + Duration::from_millis(150))
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let Some(item) = self.trigger.take() else { return };
|
||||
let (tx, rx) = helix_event::cancelation();
|
||||
self.request = Some(tx);
|
||||
job::dispatch_blocking(move |editor, _| resolve_completion_item(editor, item, rx))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_completion_item(
|
||||
editor: &mut Editor,
|
||||
item: CompletionItem,
|
||||
cancel: helix_event::CancelRx,
|
||||
) {
|
||||
let Some(language_server) = editor.language_server_by_id(item.language_server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(future) = language_server.resolve_completion_item(item.item.clone()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
match helix_event::cancelable_future(future, cancel).await {
|
||||
Some(Ok(resolved_item)) => {
|
||||
job::dispatch(move |_, compositor| {
|
||||
if let Some(completion) = &mut compositor
|
||||
.find::<crate::ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
{
|
||||
let resolved_item = CompletionItem {
|
||||
item: resolved_item,
|
||||
language_server_id: item.language_server_id,
|
||||
resolved: true,
|
||||
};
|
||||
|
||||
completion.replace_item(item, resolved_item);
|
||||
};
|
||||
})
|
||||
.await
|
||||
}
|
||||
Some(Err(err)) => log::error!("completion resolve request failed: {err}"),
|
||||
None => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -341,6 +341,7 @@ pub struct TextRenderer<'a> {
|
||||
pub indent_guide_style: Style,
|
||||
pub newline: String,
|
||||
pub nbsp: String,
|
||||
pub nnbsp: String,
|
||||
pub space: String,
|
||||
pub tab: String,
|
||||
pub virtual_tab: String,
|
||||
@@ -395,6 +396,11 @@ impl<'a> TextRenderer<'a> {
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
let nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All {
|
||||
ws_chars.nnbsp.into()
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
|
||||
let text_style = theme.get("ui.text");
|
||||
|
||||
@@ -405,6 +411,7 @@ impl<'a> TextRenderer<'a> {
|
||||
indent_guide_char: editor_config.indent_guides.character.into(),
|
||||
newline,
|
||||
nbsp,
|
||||
nnbsp,
|
||||
space,
|
||||
tab,
|
||||
virtual_tab,
|
||||
@@ -448,6 +455,7 @@ impl<'a> TextRenderer<'a> {
|
||||
let width = grapheme.width();
|
||||
let space = if is_virtual { " " } else { &self.space };
|
||||
let nbsp = if is_virtual { " " } else { &self.nbsp };
|
||||
let nnbsp = if is_virtual { " " } else { &self.nnbsp };
|
||||
let tab = if is_virtual {
|
||||
&self.virtual_tab
|
||||
} else {
|
||||
@@ -461,6 +469,7 @@ impl<'a> TextRenderer<'a> {
|
||||
// TODO special rendering for other whitespaces?
|
||||
Grapheme::Other { ref g } if g == " " => space,
|
||||
Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp,
|
||||
Grapheme::Other { ref g } if g == "\u{202F}" => nnbsp,
|
||||
Grapheme::Other { ref g } => g,
|
||||
Grapheme::Newline => &self.newline,
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
commands::{self, OnKeyCallback},
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
job::{self, Callback},
|
||||
events::{OnModeSwitch, PostCommand},
|
||||
key,
|
||||
keymap::{KeymapResult, Keymaps},
|
||||
ui::{
|
||||
@@ -33,8 +33,8 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
|
||||
|
||||
use tui::{buffer::Buffer as Surface, text::Span};
|
||||
|
||||
use super::document::LineDecoration;
|
||||
use super::{completion::CompletionItem, statusline};
|
||||
use super::{document::LineDecoration, lsp::SignatureHelp};
|
||||
|
||||
pub struct EditorView {
|
||||
pub keymaps: Keymaps,
|
||||
@@ -360,7 +360,7 @@ impl EditorView {
|
||||
doc: &Document,
|
||||
theme: &Theme,
|
||||
) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
|
||||
use helix_core::diagnostic::Severity;
|
||||
use helix_core::diagnostic::{DiagnosticTag, Severity};
|
||||
let get_scope_of = |scope| {
|
||||
theme
|
||||
.find_scope_index_exact(scope)
|
||||
@@ -380,6 +380,10 @@ impl EditorView {
|
||||
let error = get_scope_of("diagnostic.error");
|
||||
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
|
||||
|
||||
// Diagnostic tags
|
||||
let unnecessary = theme.find_scope_index_exact("diagnostic.unnecessary");
|
||||
let deprecated = theme.find_scope_index_exact("diagnostic.deprecated");
|
||||
|
||||
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
|
||||
let mut info_vec = Vec::new();
|
||||
let mut hint_vec = Vec::new();
|
||||
@@ -396,6 +400,15 @@ impl EditorView {
|
||||
_ => (&mut default_vec, r#default),
|
||||
};
|
||||
|
||||
let scope = diagnostic
|
||||
.tags
|
||||
.first()
|
||||
.and_then(|tag| match tag {
|
||||
DiagnosticTag::Unnecessary => unnecessary,
|
||||
DiagnosticTag::Deprecated => deprecated,
|
||||
})
|
||||
.unwrap_or(scope);
|
||||
|
||||
// If any diagnostic overlaps ranges with the prior diagnostic,
|
||||
// merge the two together. Otherwise push a new span.
|
||||
match vec.last_mut() {
|
||||
@@ -716,7 +729,8 @@ impl EditorView {
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(lines)
|
||||
let text = Text::from(lines);
|
||||
let paragraph = Paragraph::new(&text)
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
let width = 100.min(viewport.width);
|
||||
@@ -835,35 +849,26 @@ impl EditorView {
|
||||
|
||||
let mut execute_command = |command: &commands::MappableCommand| {
|
||||
command.execute(cxt);
|
||||
helix_event::dispatch(PostCommand { command, cx: cxt });
|
||||
|
||||
let current_mode = cxt.editor.mode();
|
||||
match (last_mode, current_mode) {
|
||||
(Mode::Normal, Mode::Insert) => {
|
||||
if current_mode != last_mode {
|
||||
helix_event::dispatch(OnModeSwitch {
|
||||
old_mode: last_mode,
|
||||
new_mode: current_mode,
|
||||
cx: cxt,
|
||||
});
|
||||
|
||||
// HAXX: if we just entered insert mode from normal, clear key buf
|
||||
// and record the command that got us into this mode.
|
||||
|
||||
if current_mode == Mode::Insert {
|
||||
// how we entered insert mode is important, and we should track that so
|
||||
// we can repeat the side effect.
|
||||
self.last_insert.0 = command.clone();
|
||||
self.last_insert.1.clear();
|
||||
}
|
||||
}
|
||||
|
||||
commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic);
|
||||
}
|
||||
(Mode::Insert, Mode::Normal) => {
|
||||
// if exiting insert mode, remove completion
|
||||
self.clear_completion(cxt.editor);
|
||||
cxt.editor.completion_request_handle = None;
|
||||
|
||||
// TODO: Use an on_mode_change hook to remove signature help
|
||||
cxt.jobs.callback(async {
|
||||
let call: job::Callback =
|
||||
Callback::EditorCompositor(Box::new(|_editor, compositor| {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
}));
|
||||
Ok(call)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
last_mode = current_mode;
|
||||
};
|
||||
|
||||
@@ -911,11 +916,15 @@ impl EditorView {
|
||||
|
||||
fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
|
||||
match (event, cxt.editor.count) {
|
||||
// count handling
|
||||
(key!(i @ '0'), Some(_)) | (key!(i @ '1'..='9'), _) => {
|
||||
// If the count is already started and the input is a number, always continue the count.
|
||||
(key!(i @ '0'..='9'), Some(count)) => {
|
||||
let i = i.to_digit(10).unwrap() as usize;
|
||||
cxt.editor.count =
|
||||
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
|
||||
cxt.editor.count = NonZeroUsize::new(count.get() * 10 + i);
|
||||
}
|
||||
// A non-zero digit will start the count if that number isn't used by a keymap.
|
||||
(key!(i @ '1'..='9'), None) if !self.keymaps.contains_key(mode, event) => {
|
||||
let i = i.to_digit(10).unwrap() as usize;
|
||||
cxt.editor.count = NonZeroUsize::new(i);
|
||||
}
|
||||
// special handling for repeat operator
|
||||
(key!('.'), _) if self.keymaps.pending().is_empty() => {
|
||||
@@ -991,12 +1000,10 @@ impl EditorView {
|
||||
editor: &mut Editor,
|
||||
savepoint: Arc<SavePoint>,
|
||||
items: Vec<CompletionItem>,
|
||||
start_offset: usize,
|
||||
trigger_offset: usize,
|
||||
size: Rect,
|
||||
) -> Option<Rect> {
|
||||
let mut completion =
|
||||
Completion::new(editor, savepoint, items, start_offset, trigger_offset);
|
||||
let mut completion = Completion::new(editor, savepoint, items, trigger_offset);
|
||||
|
||||
if completion.is_empty() {
|
||||
// skip if we got no completion results
|
||||
@@ -1004,7 +1011,7 @@ impl EditorView {
|
||||
}
|
||||
|
||||
let area = completion.area(size, editor);
|
||||
editor.last_completion = None;
|
||||
editor.last_completion = Some(CompleteAction::Triggered);
|
||||
self.last_insert.1.push(InsertEvent::TriggerCompletion);
|
||||
|
||||
// TODO : propagate required size on resize to completion too
|
||||
@@ -1017,6 +1024,7 @@ impl EditorView {
|
||||
self.completion = None;
|
||||
if let Some(last_completion) = editor.last_completion.take() {
|
||||
match last_completion {
|
||||
CompleteAction::Triggered => (),
|
||||
CompleteAction::Applied {
|
||||
trigger_offset,
|
||||
changes,
|
||||
@@ -1030,40 +1038,43 @@ impl EditorView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any savepoints
|
||||
editor.clear_idle_timer(); // don't retrigger
|
||||
}
|
||||
|
||||
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
||||
commands::compute_inlay_hints_for_all_views(cx.editor, cx.jobs);
|
||||
|
||||
if let Some(completion) = &mut self.completion {
|
||||
return if completion.ensure_item_resolved(cx) {
|
||||
EventResult::Consumed(None)
|
||||
} else {
|
||||
EventResult::Ignored(None)
|
||||
};
|
||||
}
|
||||
|
||||
if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion {
|
||||
return EventResult::Ignored(None);
|
||||
}
|
||||
|
||||
crate::commands::insert::idle_completion(cx);
|
||||
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
/// must be called whenever the editor processed input that
|
||||
/// is not a `KeyEvent`. In these cases any pending keys/on next
|
||||
/// key callbacks must be canceled.
|
||||
fn handle_non_key_input(&mut self, cxt: &mut commands::Context) {
|
||||
cxt.editor.status_msg = None;
|
||||
cxt.editor.reset_idle_timer();
|
||||
// HACKS: create a fake key event that will never trigger any actual map
|
||||
// and therefore simply acts as "dismiss"
|
||||
let null_key_event = KeyEvent {
|
||||
code: KeyCode::Null,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
};
|
||||
// dismiss any pending keys
|
||||
if let Some(on_next_key) = self.on_next_key.take() {
|
||||
on_next_key(cxt, null_key_event);
|
||||
}
|
||||
self.handle_keymap_event(cxt.editor.mode, cxt, null_key_event);
|
||||
self.pseudo_pending.clear();
|
||||
}
|
||||
|
||||
fn handle_mouse_event(
|
||||
&mut self,
|
||||
event: &MouseEvent,
|
||||
cxt: &mut commands::Context,
|
||||
) -> EventResult {
|
||||
if event.kind != MouseEventKind::Moved {
|
||||
cxt.editor.reset_idle_timer();
|
||||
self.handle_non_key_input(cxt)
|
||||
}
|
||||
|
||||
let config = cxt.editor.config();
|
||||
@@ -1105,6 +1116,15 @@ impl EditorView {
|
||||
if modifiers == KeyModifiers::ALT {
|
||||
let selection = doc.selection(view_id).clone();
|
||||
doc.set_selection(view_id, selection.push(Range::point(pos)));
|
||||
} else if editor.mode == Mode::Select {
|
||||
// Discards non-primary selections for consistent UX with normal mode
|
||||
let primary = doc.selection(view_id).primary().put_cursor(
|
||||
doc.text().slice(..),
|
||||
pos,
|
||||
true,
|
||||
);
|
||||
editor.mouse_down_range = Some(primary);
|
||||
doc.set_selection(view_id, Selection::single(primary.anchor, primary.head));
|
||||
} else {
|
||||
doc.set_selection(view_id, Selection::point(pos));
|
||||
}
|
||||
@@ -1173,7 +1193,7 @@ impl EditorView {
|
||||
}
|
||||
|
||||
let offset = config.scroll_lines.unsigned_abs();
|
||||
commands::scroll(cxt, offset, direction);
|
||||
commands::scroll(cxt, offset, direction, false);
|
||||
|
||||
cxt.editor.tree.focus = current_view;
|
||||
cxt.editor.ensure_cursor_in_view(current_view);
|
||||
@@ -1188,19 +1208,26 @@ impl EditorView {
|
||||
|
||||
let (view, doc) = current!(cxt.editor);
|
||||
|
||||
if doc
|
||||
.selection(view.id)
|
||||
let should_yank = match cxt.editor.mouse_down_range.take() {
|
||||
Some(down_range) => doc.selection(view.id).primary() != down_range,
|
||||
None => {
|
||||
// This should not happen under normal cases. We fall back to the original
|
||||
// behavior of yanking on non-single-char selections.
|
||||
doc.selection(view.id)
|
||||
.primary()
|
||||
.slice(doc.text().slice(..))
|
||||
.len_chars()
|
||||
<= 1
|
||||
{
|
||||
return EventResult::Ignored(None);
|
||||
> 1
|
||||
}
|
||||
};
|
||||
|
||||
commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
|
||||
|
||||
if should_yank {
|
||||
commands::MappableCommand::yank_main_selection_to_primary_clipboard
|
||||
.execute(cxt);
|
||||
EventResult::Consumed(None)
|
||||
} else {
|
||||
EventResult::Ignored(None)
|
||||
}
|
||||
}
|
||||
|
||||
MouseEventKind::Up(MouseButton::Right) => {
|
||||
@@ -1265,13 +1292,14 @@ impl Component for EditorView {
|
||||
editor: context.editor,
|
||||
count: None,
|
||||
register: None,
|
||||
callback: None,
|
||||
callback: Vec::new(),
|
||||
on_next_key_callback: None,
|
||||
jobs: context.jobs,
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Paste(contents) => {
|
||||
self.handle_non_key_input(&mut cx);
|
||||
cx.count = cx.editor.count;
|
||||
commands::paste_bracketed_value(&mut cx, contents.clone());
|
||||
cx.editor.count = None;
|
||||
@@ -1338,12 +1366,6 @@ impl Component for EditorView {
|
||||
if callback.is_some() {
|
||||
// assume close_fn
|
||||
self.clear_completion(cx.editor);
|
||||
|
||||
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
|
||||
commands::signature_help_impl(
|
||||
&mut cx,
|
||||
commands::SignatureHelpInvoked::Automatic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1354,14 +1376,6 @@ impl Component for EditorView {
|
||||
|
||||
// record last_insert key
|
||||
self.last_insert.1.push(InsertEvent::Key(key));
|
||||
|
||||
// lastly we recalculate completion
|
||||
if let Some(completion) = &mut self.completion {
|
||||
completion.update(&mut cx);
|
||||
if completion.is_empty() {
|
||||
self.clear_completion(cx.editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mode => self.command_mode(mode, &mut cx, key),
|
||||
@@ -1375,7 +1389,7 @@ impl Component for EditorView {
|
||||
}
|
||||
|
||||
// appease borrowck
|
||||
let callback = cx.callback.take();
|
||||
let callbacks = take(&mut cx.callback);
|
||||
|
||||
// if the command consumed the last view, skip the render.
|
||||
// on the next loop cycle the Application will then terminate.
|
||||
@@ -1394,6 +1408,16 @@ impl Component for EditorView {
|
||||
if mode != Mode::Insert {
|
||||
doc.append_changes_to_history(view);
|
||||
}
|
||||
let callback = if callbacks.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let callback: crate::compositor::Callback = Box::new(move |compositor, cx| {
|
||||
for callback in callbacks {
|
||||
callback(compositor, cx)
|
||||
}
|
||||
});
|
||||
Some(callback)
|
||||
};
|
||||
|
||||
EventResult::Consumed(callback)
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context};
|
||||
use helix_view::graphics::{Margin, Rect};
|
||||
use helix_view::info::Info;
|
||||
use tui::buffer::Buffer as Surface;
|
||||
use tui::text::Text;
|
||||
use tui::widgets::{Block, Borders, Paragraph, Widget};
|
||||
|
||||
impl Component for Info {
|
||||
@@ -31,7 +32,7 @@ impl Component for Info {
|
||||
let inner = block.inner(area).inner(&margin);
|
||||
block.render(area, surface);
|
||||
|
||||
Paragraph::new(self.text.as_str())
|
||||
Paragraph::new(&Text::from(self.text.as_str()))
|
||||
.style(text_style)
|
||||
.render(inner, surface);
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use helix_core::syntax;
|
||||
use helix_view::graphics::{Margin, Rect, Style};
|
||||
use tui::buffer::Buffer;
|
||||
@@ -18,13 +19,17 @@ pub struct SignatureHelp {
|
||||
active_param_range: Option<(usize, usize)>,
|
||||
|
||||
language: String,
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
config_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
}
|
||||
|
||||
impl SignatureHelp {
|
||||
pub const ID: &'static str = "signature-help";
|
||||
|
||||
pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self {
|
||||
pub fn new(
|
||||
signature: String,
|
||||
language: String,
|
||||
config_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
signature,
|
||||
signature_doc: None,
|
||||
@@ -72,7 +77,7 @@ impl Component for SignatureHelp {
|
||||
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
|
||||
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
|
||||
let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area);
|
||||
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
|
||||
let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false });
|
||||
sig_text_para.render(sig_text_area, surface);
|
||||
|
||||
if self.signature_doc.is_none() {
|
||||
@@ -95,7 +100,7 @@ impl Component for SignatureHelp {
|
||||
let sig_doc_area = area
|
||||
.clip_top(sig_text_area.height + 2)
|
||||
.clip_bottom(u16::from(cx.editor.popup_border()));
|
||||
let sig_doc_para = Paragraph::new(sig_doc)
|
||||
let sig_doc_para = Paragraph::new(&sig_doc)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
|
||||
sig_doc_para.render(sig_doc_area.inner(&margin), surface);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::compositor::{Component, Context};
|
||||
use arc_swap::ArcSwap;
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
text::{Span, Spans, Text},
|
||||
@@ -6,7 +7,7 @@ use tui::{
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||
|
||||
use helix_core::{
|
||||
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
|
||||
@@ -31,7 +32,7 @@ pub fn highlighted_code_block<'a>(
|
||||
text: &str,
|
||||
language: &str,
|
||||
theme: Option<&Theme>,
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
config_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
additional_highlight_spans: Option<Vec<(usize, std::ops::Range<usize>)>>,
|
||||
) -> Text<'a> {
|
||||
let mut spans = Vec::new();
|
||||
@@ -48,6 +49,7 @@ pub fn highlighted_code_block<'a>(
|
||||
|
||||
let ropeslice = RopeSlice::from(text);
|
||||
let syntax = config_loader
|
||||
.load()
|
||||
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
|
||||
language.into(),
|
||||
))
|
||||
@@ -121,7 +123,7 @@ pub fn highlighted_code_block<'a>(
|
||||
pub struct Markdown {
|
||||
contents: String,
|
||||
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
config_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
}
|
||||
|
||||
// TODO: pre-render and self reference via Pin
|
||||
@@ -140,7 +142,7 @@ impl Markdown {
|
||||
];
|
||||
const INDENT: &'static str = " ";
|
||||
|
||||
pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
|
||||
pub fn new(contents: String, config_loader: Arc<ArcSwap<syntax::Loader>>) -> Self {
|
||||
Self {
|
||||
contents,
|
||||
config_loader,
|
||||
@@ -209,7 +211,7 @@ impl Markdown {
|
||||
|
||||
list_stack.push(list);
|
||||
}
|
||||
Event::End(Tag::List(_)) => {
|
||||
Event::End(TagEnd::List(_)) => {
|
||||
list_stack.pop();
|
||||
|
||||
// whenever top-level list closes, empty line
|
||||
@@ -249,7 +251,10 @@ impl Markdown {
|
||||
Event::End(tag) => {
|
||||
tags.pop();
|
||||
match tag {
|
||||
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) | Tag::Item => {
|
||||
TagEnd::Heading(_)
|
||||
| TagEnd::Paragraph
|
||||
| TagEnd::CodeBlock
|
||||
| TagEnd::Item => {
|
||||
push_line(&mut spans, &mut lines);
|
||||
}
|
||||
_ => (),
|
||||
@@ -257,7 +262,7 @@ impl Markdown {
|
||||
|
||||
// whenever heading, code block or paragraph closes, empty line
|
||||
match tag {
|
||||
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) => {
|
||||
TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::CodeBlock => {
|
||||
lines.push(Spans::default());
|
||||
}
|
||||
_ => (),
|
||||
@@ -279,7 +284,7 @@ impl Markdown {
|
||||
lines.extend(tui_text.lines.into_iter());
|
||||
} else {
|
||||
let style = match tags.last() {
|
||||
Some(Tag::Heading(level, ..)) => match level {
|
||||
Some(Tag::Heading { level, .. }) => match level {
|
||||
HeadingLevel::H1 => heading_styles[0],
|
||||
HeadingLevel::H2 => heading_styles[1],
|
||||
HeadingLevel::H3 => heading_styles[2],
|
||||
@@ -341,7 +346,7 @@ impl Component for Markdown {
|
||||
|
||||
let text = self.parse(Some(&cx.editor.theme));
|
||||
|
||||
let par = Paragraph::new(text)
|
||||
let par = Paragraph::new(&text)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
|
||||
|
||||
|
@@ -96,13 +96,26 @@ impl<T: Item> Menu<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score(&mut self, pattern: &str) {
|
||||
// reuse the matches allocation
|
||||
self.matches.clear();
|
||||
pub fn score(&mut self, pattern: &str, incremental: bool) {
|
||||
let mut matcher = MATCHER.lock();
|
||||
matcher.config = Config::DEFAULT;
|
||||
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
|
||||
let mut buf = Vec::new();
|
||||
if incremental {
|
||||
self.matches.retain_mut(|(index, score)| {
|
||||
let option = &self.options[*index as usize];
|
||||
let text = option.filter_text(&self.editor_data);
|
||||
let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher);
|
||||
match new_score {
|
||||
Some(new_score) => {
|
||||
*score = new_score as u32;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.matches.clear();
|
||||
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
|
||||
let text = option.filter_text(&self.editor_data);
|
||||
pattern
|
||||
@@ -110,6 +123,7 @@ impl<T: Item> Menu<T> {
|
||||
.map(|score| (i as u32, score as u32))
|
||||
});
|
||||
self.matches.extend(matches);
|
||||
}
|
||||
self.matches
|
||||
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
|
||||
|
||||
@@ -413,6 +427,7 @@ impl<T: Item + 'static> Component for Menu<T> {
|
||||
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
} else if !render_borders {
|
||||
// Draw scroll track
|
||||
cell.set_symbol(half_block);
|
||||
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ use crate::filter_picker_entry;
|
||||
use crate::job::{self, Callback};
|
||||
pub use completion::{Completion, CompletionItem};
|
||||
pub use editor::EditorView;
|
||||
use helix_stdx::rope;
|
||||
pub use markdown::Markdown;
|
||||
pub use menu::Menu;
|
||||
pub use picker::{DynamicPicker, FileLocation, Picker};
|
||||
@@ -26,8 +27,6 @@ pub use prompt::{Prompt, PromptEvent};
|
||||
pub use spinner::{ProgressSpinners, Spinner};
|
||||
pub use text::Text;
|
||||
|
||||
use helix_core::regex::Regex;
|
||||
use helix_core::regex::RegexBuilder;
|
||||
use helix_view::Editor;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -63,7 +62,22 @@ pub fn regex_prompt(
|
||||
prompt: std::borrow::Cow<'static, str>,
|
||||
history_register: Option<char>,
|
||||
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
|
||||
fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
|
||||
fun: impl Fn(&mut crate::compositor::Context, rope::Regex, PromptEvent) + 'static,
|
||||
) {
|
||||
raw_regex_prompt(
|
||||
cx,
|
||||
prompt,
|
||||
history_register,
|
||||
completion_fn,
|
||||
move |cx, regex, _, event| fun(cx, regex, event),
|
||||
);
|
||||
}
|
||||
pub fn raw_regex_prompt(
|
||||
cx: &mut crate::commands::Context,
|
||||
prompt: std::borrow::Cow<'static, str>,
|
||||
history_register: Option<char>,
|
||||
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
|
||||
fun: impl Fn(&mut crate::compositor::Context, rope::Regex, &str, PromptEvent) + 'static,
|
||||
) {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let doc_id = view.doc;
|
||||
@@ -94,10 +108,13 @@ pub fn regex_prompt(
|
||||
false
|
||||
};
|
||||
|
||||
match RegexBuilder::new(input)
|
||||
match rope::RegexBuilder::new()
|
||||
.syntax(
|
||||
rope::Config::new()
|
||||
.case_insensitive(case_insensitive)
|
||||
.multi_line(true)
|
||||
.build()
|
||||
.multi_line(true),
|
||||
)
|
||||
.build(input)
|
||||
{
|
||||
Ok(regex) => {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
@@ -110,7 +127,7 @@ pub fn regex_prompt(
|
||||
view.jumps.push((doc_id, snapshot.clone()));
|
||||
}
|
||||
|
||||
fun(cx, regex, event);
|
||||
fun(cx, regex, input, event);
|
||||
|
||||
let (view, doc) = current!(cx.editor);
|
||||
view.ensure_cursor_in_view(doc, config.scrolloff);
|
||||
@@ -336,8 +353,8 @@ pub mod completers {
|
||||
pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
|
||||
let text: String = "text".into();
|
||||
|
||||
let language_ids = editor
|
||||
.syn_loader
|
||||
let loader = editor.syn_loader.load();
|
||||
let language_ids = loader
|
||||
.language_configs()
|
||||
.map(|config| &config.language_id)
|
||||
.chain(std::iter::once(&text));
|
||||
@@ -409,7 +426,7 @@ pub mod completers {
|
||||
use std::path::Path;
|
||||
|
||||
let is_tilde = input == "~";
|
||||
let path = helix_core::path::expand_tilde(Path::new(input));
|
||||
let path = helix_stdx::path::expand_tilde(Path::new(input));
|
||||
|
||||
let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
|
||||
(path, None)
|
||||
@@ -428,9 +445,9 @@ pub mod completers {
|
||||
path
|
||||
} else {
|
||||
match path.parent() {
|
||||
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
|
||||
Some(path) if !path.as_os_str().is_empty() => Cow::Borrowed(path),
|
||||
// Path::new("h")'s parent is Some("")...
|
||||
_ => helix_loader::current_working_dir(),
|
||||
_ => Cow::Owned(helix_stdx::env::current_working_dir()),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -492,4 +509,18 @@ pub mod completers {
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(editor: &Editor, input: &str) -> Vec<Completion> {
|
||||
let iter = editor
|
||||
.registers
|
||||
.iter_preview()
|
||||
// Exclude special registers that shouldn't be written to
|
||||
.filter(|(ch, _)| !matches!(ch, '%' | '#' | '.'))
|
||||
.map(|(ch, _)| ch.to_string());
|
||||
|
||||
fuzzy_match(input, iter, false)
|
||||
.into_iter()
|
||||
.map(|(name, _)| ((0..), name.into()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
@@ -63,7 +63,7 @@ impl PathOrId {
|
||||
fn get_canonicalized(self) -> Self {
|
||||
use PathOrId::*;
|
||||
match self {
|
||||
Path(path) => Path(helix_core::path::get_canonicalized_path(&path)),
|
||||
Path(path) => Path(helix_stdx::path::canonicalize(path)),
|
||||
Id(id) => Id(id),
|
||||
}
|
||||
}
|
||||
@@ -461,14 +461,17 @@ impl<T: Item + 'static> Picker<T> {
|
||||
|
||||
// Then attempt to highlight it if it has no language set
|
||||
if doc.language_config().is_none() {
|
||||
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
|
||||
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader.load())
|
||||
{
|
||||
doc.language = Some(language_config.clone());
|
||||
let text = doc.text().clone();
|
||||
let loader = cx.editor.syn_loader.clone();
|
||||
let job = tokio::task::spawn_blocking(move || {
|
||||
let syntax = language_config.highlight_config(&loader.scopes()).and_then(
|
||||
|highlight_config| Syntax::new(text.slice(..), highlight_config, loader),
|
||||
);
|
||||
let syntax = language_config
|
||||
.highlight_config(&loader.load().scopes())
|
||||
.and_then(|highlight_config| {
|
||||
Syntax::new(text.slice(..), highlight_config, loader)
|
||||
});
|
||||
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let Some(syntax) = syntax else {
|
||||
log::info!("highlighting picker item failed");
|
||||
|
@@ -303,6 +303,7 @@ impl<T: Component> Component for Popup<T> {
|
||||
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
} else if !render_borders {
|
||||
// Draw scroll track
|
||||
cell.set_symbol(half_block);
|
||||
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
use crate::compositor::{Component, Compositor, Context, Event, EventResult};
|
||||
use crate::{alt, ctrl, key, shift, ui};
|
||||
use arc_swap::ArcSwap;
|
||||
use helix_core::syntax;
|
||||
use helix_view::input::KeyEvent;
|
||||
use helix_view::keyboard::KeyCode;
|
||||
@@ -34,7 +35,7 @@ pub struct Prompt {
|
||||
callback_fn: CallbackFn,
|
||||
pub doc_fn: DocFn,
|
||||
next_char_handler: Option<PromptCharHandler>,
|
||||
language: Option<(&'static str, Arc<syntax::Loader>)>,
|
||||
language: Option<(&'static str, Arc<ArcSwap<syntax::Loader>>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -98,7 +99,11 @@ impl Prompt {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_language(mut self, language: &'static str, loader: Arc<syntax::Loader>) -> Self {
|
||||
pub fn with_language(
|
||||
mut self,
|
||||
language: &'static str,
|
||||
loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
) -> Self {
|
||||
self.language = Some((language, loader));
|
||||
self
|
||||
}
|
||||
@@ -393,7 +398,7 @@ impl Prompt {
|
||||
height,
|
||||
);
|
||||
|
||||
if !self.completion.is_empty() {
|
||||
if completion_area.height > 0 && !self.completion.is_empty() {
|
||||
let area = completion_area;
|
||||
let background = theme.get("ui.menu");
|
||||
|
||||
|
@@ -11,7 +11,7 @@ impl ProgressSpinners {
|
||||
}
|
||||
|
||||
pub fn get_or_create(&mut self, id: usize) -> &mut Spinner {
|
||||
self.inner.entry(id).or_insert_with(Spinner::default)
|
||||
self.inner.entry(id).or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,6 @@ use helix_view::document::DEFAULT_LANGUAGE_NAME;
|
||||
use helix_view::{
|
||||
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||
graphics::Rect,
|
||||
theme::Style,
|
||||
Document, Editor, View,
|
||||
};
|
||||
|
||||
@@ -20,7 +19,6 @@ pub struct RenderContext<'a> {
|
||||
pub view: &'a View,
|
||||
pub focused: bool,
|
||||
pub spinners: &'a ProgressSpinners,
|
||||
pub parts: RenderBuffer<'a>,
|
||||
}
|
||||
|
||||
impl<'a> RenderContext<'a> {
|
||||
@@ -37,18 +35,10 @@ impl<'a> RenderContext<'a> {
|
||||
view,
|
||||
focused,
|
||||
spinners,
|
||||
parts: RenderBuffer::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RenderBuffer<'a> {
|
||||
pub left: Spans<'a>,
|
||||
pub center: Spans<'a>,
|
||||
pub right: Spans<'a>,
|
||||
}
|
||||
|
||||
pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) {
|
||||
let base_style = if context.focused {
|
||||
context.editor.theme.get("ui.statusline")
|
||||
@@ -58,90 +48,93 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface
|
||||
|
||||
surface.set_style(viewport.with_height(1), base_style);
|
||||
|
||||
let write_left = |context: &mut RenderContext, text, style| {
|
||||
append(&mut context.parts.left, text, &base_style, style)
|
||||
};
|
||||
let write_center = |context: &mut RenderContext, text, style| {
|
||||
append(&mut context.parts.center, text, &base_style, style)
|
||||
};
|
||||
let write_right = |context: &mut RenderContext, text, style| {
|
||||
append(&mut context.parts.right, text, &base_style, style)
|
||||
};
|
||||
|
||||
// Left side of the status line.
|
||||
|
||||
let config = context.editor.config();
|
||||
|
||||
let element_ids = &config.statusline.left;
|
||||
element_ids
|
||||
.iter()
|
||||
.map(|element_id| get_render_function(*element_id))
|
||||
.for_each(|render| render(context, write_left));
|
||||
let statusline = render_statusline(context, viewport.width as usize);
|
||||
|
||||
surface.set_spans(
|
||||
viewport.x,
|
||||
viewport.y,
|
||||
&context.parts.left,
|
||||
context.parts.left.width() as u16,
|
||||
&statusline,
|
||||
statusline.width() as u16,
|
||||
);
|
||||
}
|
||||
|
||||
// Right side of the status line.
|
||||
pub fn render_statusline<'a>(context: &mut RenderContext, width: usize) -> Spans<'a> {
|
||||
let config = context.editor.config();
|
||||
|
||||
let element_ids = &config.statusline.right;
|
||||
element_ids
|
||||
let element_ids = &config.statusline.left;
|
||||
let mut left = element_ids
|
||||
.iter()
|
||||
.map(|element_id| get_render_function(*element_id))
|
||||
.for_each(|render| render(context, write_right));
|
||||
|
||||
surface.set_spans(
|
||||
viewport.x
|
||||
+ viewport
|
||||
.width
|
||||
.saturating_sub(context.parts.right.width() as u16),
|
||||
viewport.y,
|
||||
&context.parts.right,
|
||||
context.parts.right.width() as u16,
|
||||
);
|
||||
|
||||
// Center of the status line.
|
||||
.flat_map(|render| render(context).0)
|
||||
.collect::<Vec<Span>>();
|
||||
|
||||
let element_ids = &config.statusline.center;
|
||||
element_ids
|
||||
let mut center = element_ids
|
||||
.iter()
|
||||
.map(|element_id| get_render_function(*element_id))
|
||||
.for_each(|render| render(context, write_center));
|
||||
.flat_map(|render| render(context).0)
|
||||
.collect::<Vec<Span>>();
|
||||
|
||||
// Width of the empty space between the left and center area and between the center and right area.
|
||||
let spacing = 1u16;
|
||||
let element_ids = &config.statusline.right;
|
||||
let mut right = element_ids
|
||||
.iter()
|
||||
.map(|element_id| get_render_function(*element_id))
|
||||
.flat_map(|render| render(context).0)
|
||||
.collect::<Vec<Span>>();
|
||||
|
||||
let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16;
|
||||
let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing);
|
||||
let center_width = center_max_width.min(context.parts.center.width() as u16);
|
||||
let left_area_width: usize = left.iter().map(|s| s.width()).sum();
|
||||
let center_area_width: usize = center.iter().map(|s| s.width()).sum();
|
||||
let right_area_width: usize = right.iter().map(|s| s.width()).sum();
|
||||
|
||||
surface.set_spans(
|
||||
viewport.x + viewport.width / 2 - center_width / 2,
|
||||
viewport.y,
|
||||
&context.parts.center,
|
||||
center_width,
|
||||
);
|
||||
let min_spacing_between_areas = 1usize;
|
||||
let sides_space_required = left_area_width + right_area_width + min_spacing_between_areas;
|
||||
let total_space_required = sides_space_required + center_area_width + min_spacing_between_areas;
|
||||
|
||||
let mut statusline: Vec<Span> = vec![];
|
||||
|
||||
if center_area_width > 0 && total_space_required <= width {
|
||||
// SAFETY: this subtraction cannot underflow because `left_area_width + center_area_width + right_area_width`
|
||||
// is smaller than `total_space_required`, which is smaller than `width` in this branch.
|
||||
let total_spacers = width - (left_area_width + center_area_width + right_area_width);
|
||||
// This is how much padding space it would take on either side to align the center area to the middle.
|
||||
let center_margin = (width - center_area_width) / 2;
|
||||
let left_spacers = if left_area_width < center_margin && right_area_width < center_margin {
|
||||
// Align the center area to the middle if there is enough space on both sides.
|
||||
center_margin - left_area_width
|
||||
} else {
|
||||
// Otherwise split the available space evenly and use it as margin.
|
||||
// The center element won't be aligned to the middle but it will be evenly
|
||||
// spaced between the left and right areas.
|
||||
total_spacers / 2
|
||||
};
|
||||
let right_spacers = total_spacers - left_spacers;
|
||||
|
||||
statusline.append(&mut left);
|
||||
statusline.push(" ".repeat(left_spacers).into());
|
||||
statusline.append(&mut center);
|
||||
statusline.push(" ".repeat(right_spacers).into());
|
||||
statusline.append(&mut right);
|
||||
} else if right_area_width > 0 && sides_space_required <= width {
|
||||
let side_areas_width = left_area_width + right_area_width;
|
||||
statusline.append(&mut left);
|
||||
statusline.push(" ".repeat(width - side_areas_width).into());
|
||||
statusline.append(&mut right);
|
||||
} else if left_area_width <= width {
|
||||
statusline.append(&mut left);
|
||||
}
|
||||
|
||||
fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option<Style>) {
|
||||
buffer.0.push(Span::styled(
|
||||
text,
|
||||
style.map_or(*base_style, |s| (*base_style).patch(s)),
|
||||
));
|
||||
statusline.into()
|
||||
}
|
||||
|
||||
fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn get_render_function<'a>(
|
||||
element_id: StatusLineElementID,
|
||||
) -> impl Fn(&RenderContext) -> Spans<'a> {
|
||||
match element_id {
|
||||
helix_view::editor::StatusLineElement::Mode => render_mode,
|
||||
helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
|
||||
helix_view::editor::StatusLineElement::FileBaseName => render_file_base_name,
|
||||
helix_view::editor::StatusLineElement::FileName => render_file_name,
|
||||
helix_view::editor::StatusLineElement::FileAbsolutePath => render_file_absolute_path,
|
||||
helix_view::editor::StatusLineElement::FileModificationIndicator => {
|
||||
render_file_modification_indicator
|
||||
}
|
||||
@@ -165,48 +158,40 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn render_mode<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_mode<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let visible = context.focused;
|
||||
let config = context.editor.config();
|
||||
let modenames = &config.statusline.mode;
|
||||
write(
|
||||
context,
|
||||
format!(
|
||||
" {} ",
|
||||
if visible {
|
||||
let modename = if visible {
|
||||
match context.editor.mode() {
|
||||
Mode::Insert => &modenames.insert,
|
||||
Mode::Select => &modenames.select,
|
||||
Mode::Normal => &modenames.normal,
|
||||
Mode::Insert => modenames.insert.clone(),
|
||||
Mode::Select => modenames.select.clone(),
|
||||
Mode::Normal => modenames.normal.clone(),
|
||||
}
|
||||
} else {
|
||||
// If not focused, explicitly leave an empty space instead of returning None.
|
||||
" "
|
||||
}
|
||||
),
|
||||
// If not focused, explicitly leave an empty space.
|
||||
" ".into()
|
||||
};
|
||||
let modename = format!(" {} ", modename);
|
||||
if visible && config.color_modes {
|
||||
Span::styled(
|
||||
modename,
|
||||
match context.editor.mode() {
|
||||
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
|
||||
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
|
||||
Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
Mode::Insert => context.editor.theme.get("ui.statusline.insert"),
|
||||
Mode::Select => context.editor.theme.get("ui.statusline.select"),
|
||||
Mode::Normal => context.editor.theme.get("ui.statusline.normal"),
|
||||
},
|
||||
);
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
Span::raw(modename).into()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO think about handling multiple language servers
|
||||
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_lsp_spinner<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let language_server = context.doc.language_servers().next();
|
||||
write(
|
||||
context,
|
||||
Span::raw(
|
||||
language_server
|
||||
.and_then(|srv| {
|
||||
context
|
||||
@@ -217,14 +202,11 @@ where
|
||||
// Even if there's no spinner; reserve its space to avoid elements frequently shifting.
|
||||
.unwrap_or(" ")
|
||||
.to_string(),
|
||||
None,
|
||||
);
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_diagnostics<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let (warnings, errors) = context
|
||||
.doc
|
||||
.diagnostics()
|
||||
@@ -239,29 +221,28 @@ where
|
||||
counts
|
||||
});
|
||||
|
||||
let mut output = Spans::default();
|
||||
|
||||
if warnings > 0 {
|
||||
write(
|
||||
context,
|
||||
output.0.push(Span::styled(
|
||||
"●".to_string(),
|
||||
Some(context.editor.theme.get("warning")),
|
||||
);
|
||||
write(context, format!(" {} ", warnings), None);
|
||||
context.editor.theme.get("warning"),
|
||||
));
|
||||
output.0.push(Span::raw(format!(" {} ", warnings)));
|
||||
}
|
||||
|
||||
if errors > 0 {
|
||||
write(
|
||||
context,
|
||||
output.0.push(Span::styled(
|
||||
"●".to_string(),
|
||||
Some(context.editor.theme.get("error")),
|
||||
);
|
||||
write(context, format!(" {} ", errors), None);
|
||||
}
|
||||
context.editor.theme.get("error"),
|
||||
));
|
||||
output.0.push(Span::raw(format!(" {} ", errors)));
|
||||
}
|
||||
|
||||
fn render_workspace_diagnostics<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
output
|
||||
}
|
||||
|
||||
fn render_workspace_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let (warnings, errors) =
|
||||
context
|
||||
.editor
|
||||
@@ -277,51 +258,49 @@ where
|
||||
counts
|
||||
});
|
||||
|
||||
let mut output = Spans::default();
|
||||
|
||||
if warnings > 0 || errors > 0 {
|
||||
write(context, " W ".into(), None);
|
||||
output.0.push(Span::raw(" W "));
|
||||
}
|
||||
|
||||
if warnings > 0 {
|
||||
write(
|
||||
context,
|
||||
output.0.push(Span::styled(
|
||||
"●".to_string(),
|
||||
Some(context.editor.theme.get("warning")),
|
||||
);
|
||||
write(context, format!(" {} ", warnings), None);
|
||||
context.editor.theme.get("warning"),
|
||||
));
|
||||
output.0.push(Span::raw(format!(" {} ", warnings)));
|
||||
}
|
||||
|
||||
if errors > 0 {
|
||||
write(
|
||||
context,
|
||||
output.0.push(Span::styled(
|
||||
"●".to_string(),
|
||||
Some(context.editor.theme.get("error")),
|
||||
);
|
||||
write(context, format!(" {} ", errors), None);
|
||||
}
|
||||
context.editor.theme.get("error"),
|
||||
));
|
||||
output.0.push(Span::raw(format!(" {} ", errors)));
|
||||
}
|
||||
|
||||
fn render_selections<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
output
|
||||
}
|
||||
|
||||
fn render_selections<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let count = context.doc.selection(context.view.id).len();
|
||||
write(
|
||||
context,
|
||||
format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }),
|
||||
None,
|
||||
);
|
||||
Span::raw(format!(
|
||||
" {} sel{} ",
|
||||
count,
|
||||
if count == 1 { "" } else { "s" }
|
||||
))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_primary_selection_length<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_primary_selection_length<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let tot_sel = context.doc.selection(context.view.id).primary().len();
|
||||
write(
|
||||
context,
|
||||
format!(" {} char{} ", tot_sel, if tot_sel == 1 { "" } else { "s" }),
|
||||
None,
|
||||
);
|
||||
Span::raw(format!(
|
||||
" {} char{} ",
|
||||
tot_sel,
|
||||
if tot_sel == 1 { "" } else { "s" }
|
||||
))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn get_position(context: &RenderContext) -> Position {
|
||||
@@ -335,55 +314,33 @@ fn get_position(context: &RenderContext) -> Position {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_position<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_position<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let position = get_position(context);
|
||||
write(
|
||||
context,
|
||||
format!(" {}:{} ", position.row + 1, position.col + 1),
|
||||
None,
|
||||
);
|
||||
Span::raw(format!(" {}:{} ", position.row + 1, position.col + 1)).into()
|
||||
}
|
||||
|
||||
fn render_total_line_numbers<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_total_line_numbers<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let total_line_numbers = context.doc.text().len_lines();
|
||||
|
||||
write(context, format!(" {} ", total_line_numbers), None);
|
||||
Span::raw(format!(" {} ", total_line_numbers)).into()
|
||||
}
|
||||
|
||||
fn render_position_percentage<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_position_percentage<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let position = get_position(context);
|
||||
let maxrows = context.doc.text().len_lines();
|
||||
write(
|
||||
context,
|
||||
format!("{}%", (position.row + 1) * 100 / maxrows),
|
||||
None,
|
||||
);
|
||||
Span::raw(format!("{}%", (position.row + 1) * 100 / maxrows)).into()
|
||||
}
|
||||
|
||||
fn render_file_encoding<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_file_encoding<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let enc = context.doc.encoding();
|
||||
|
||||
if enc != encoding::UTF_8 {
|
||||
write(context, format!(" {} ", enc.name()), None);
|
||||
Span::raw(format!(" {} ", enc.name())).into()
|
||||
} else {
|
||||
Spans::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_file_line_ending<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_file_line_ending<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
use helix_core::LineEnding::*;
|
||||
let line_ending = match context.doc.line_ending {
|
||||
Crlf => "CRLF",
|
||||
@@ -402,22 +359,16 @@ where
|
||||
PS => "PS", // U+2029 -- ParagraphSeparator
|
||||
};
|
||||
|
||||
write(context, format!(" {} ", line_ending), None);
|
||||
Span::raw(format!(" {} ", line_ending)).into()
|
||||
}
|
||||
|
||||
fn render_file_type<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_file_type<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let file_type = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
|
||||
|
||||
write(context, format!(" {} ", file_type), None);
|
||||
Span::raw(format!(" {} ", file_type)).into()
|
||||
}
|
||||
|
||||
fn render_file_name<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_file_name<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let title = {
|
||||
let rel_path = context.doc.relative_path();
|
||||
let path = rel_path
|
||||
@@ -427,13 +378,23 @@ where
|
||||
format!(" {} ", path)
|
||||
};
|
||||
|
||||
write(context, title, None);
|
||||
Span::raw(title).into()
|
||||
}
|
||||
|
||||
fn render_file_modification_indicator<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_file_absolute_path<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let title = {
|
||||
let path = context.doc.path();
|
||||
let path = path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy())
|
||||
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
|
||||
format!(" {} ", path)
|
||||
};
|
||||
|
||||
Span::raw(title).into()
|
||||
}
|
||||
|
||||
fn render_file_modification_indicator<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let title = (if context.doc.is_modified() {
|
||||
"[+]"
|
||||
} else {
|
||||
@@ -441,76 +402,60 @@ where
|
||||
})
|
||||
.to_string();
|
||||
|
||||
write(context, title, None);
|
||||
Span::raw(title).into()
|
||||
}
|
||||
|
||||
fn render_read_only_indicator<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_read_only_indicator<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let title = if context.doc.readonly {
|
||||
" [readonly] "
|
||||
} else {
|
||||
""
|
||||
}
|
||||
.to_string();
|
||||
write(context, title, None);
|
||||
Span::raw(title).into()
|
||||
}
|
||||
|
||||
fn render_file_base_name<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_file_base_name<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let title = {
|
||||
let rel_path = context.doc.relative_path();
|
||||
let path = rel_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.as_path().file_name().map(|s| s.to_string_lossy()))
|
||||
.and_then(|p| p.file_name().map(|s| s.to_string_lossy()))
|
||||
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
|
||||
format!(" {} ", path)
|
||||
};
|
||||
|
||||
write(context, title, None);
|
||||
Span::raw(title).into()
|
||||
}
|
||||
|
||||
fn render_separator<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_separator<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let sep = &context.editor.config().statusline.separator;
|
||||
|
||||
write(
|
||||
context,
|
||||
Span::styled(
|
||||
sep.to_string(),
|
||||
Some(context.editor.theme.get("ui.statusline.separator")),
|
||||
);
|
||||
context.editor.theme.get("ui.statusline.separator"),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_spacer<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
write(context, String::from(" "), None);
|
||||
fn render_spacer<'a>(_context: &RenderContext) -> Spans<'a> {
|
||||
Span::raw(" ").into()
|
||||
}
|
||||
|
||||
fn render_version_control<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_version_control<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
let head = context
|
||||
.doc
|
||||
.version_control_head()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
write(context, head, None);
|
||||
Span::raw(head).into()
|
||||
}
|
||||
|
||||
fn render_register<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
fn render_register<'a>(context: &RenderContext) -> Spans<'a> {
|
||||
if let Some(reg) = context.editor.selected_register {
|
||||
write(context, format!(" reg={} ", reg), None)
|
||||
Span::raw(format!(" reg={} ", reg)).into()
|
||||
} else {
|
||||
Spans::default()
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user