mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 00:13:28 +02:00
Compare commits
151 Commits
default-in
...
f59dc9e48f
Author | SHA1 | Date | |
---|---|---|---|
|
f59dc9e48f | ||
|
d63c2d2fea | ||
|
0a4207be32 | ||
|
3adc021c06 | ||
|
d1750a7502 | ||
|
c5f0a4bc22 | ||
|
4967229e85 | ||
|
68f11f9324 | ||
|
af74a61ad4 | ||
|
cfb5158cd1 | ||
|
e3fafb6bad | ||
|
6e9939a2d1 | ||
|
b08aba8e8e | ||
|
83abbe56df | ||
|
9cc912a63e | ||
|
fe1393cec8 | ||
|
392e444ff9 | ||
|
0ea5d87985 | ||
|
6b73c3c550 | ||
|
b309d72688 | ||
|
d546a799e5 | ||
|
7c37e8acea | ||
|
d4c91daa5e | ||
|
dc7c2acc08 | ||
|
99cea8c284 | ||
|
077c901be9 | ||
|
a5bf7c0d5e | ||
|
8ab20720da | ||
|
feeaec097a | ||
|
4f5eaa4186 | ||
|
7a5b618fe5 | ||
|
77ff51caa4 | ||
|
7e4e556f84 | ||
|
96c60198ec | ||
|
3dadd82c89 | ||
|
5a8fb732f2 | ||
|
8671882ee2 | ||
|
1d3e65fdbc | ||
|
f81b59fc15 | ||
|
cc8e890906 | ||
|
aa14cd38fc | ||
|
22a3b10dd8 | ||
|
535e6ee77b | ||
|
4b40b45527 | ||
|
95c378a764 | ||
|
74bb02ffe7 | ||
|
b81ee02db4 | ||
|
9ec07cf1f6 | ||
|
9f34f8b5ff | ||
|
3fb1443162 | ||
|
207c0e3899 | ||
|
f9f5fe6b12 | ||
|
e5e7fe43ce | ||
|
da4ede9535 | ||
|
6f26a257d5 | ||
|
b1eb9c09f4 | ||
|
b6ccbddc77 | ||
|
a4a2b50a50 | ||
|
e5d1f6c517 | ||
|
050e1d9b47 | ||
|
2b9cc20d23 | ||
|
9e3b510dc7 | ||
|
6b93c77033 | ||
|
fdaec3f972 | ||
|
8a898c88bc | ||
|
e0544d01f1 | ||
|
001efa801e | ||
|
00dbca93c4 | ||
|
6726c1f41c | ||
|
7747d3b93e | ||
|
4d0466d30c | ||
|
ed2807ae07 | ||
|
327f3852f4 | ||
|
155fde5178 | ||
|
f5a399c7f9 | ||
|
cb7188d5cc | ||
|
a44695e4e8 | ||
|
ef3a49d03c | ||
|
56fa9bf7c1 | ||
|
b5a9c34e14 | ||
|
e8e36a6a8e | ||
|
18572973e6 | ||
|
4a25f63169 | ||
|
0345400c41 | ||
|
43990ed0c8 | ||
|
178c55708a | ||
|
6c0d598183 | ||
|
5b5f6daab3 | ||
|
601c904e50 | ||
|
93cf3b1baf | ||
|
fdfc6df122 | ||
|
d2595930fa | ||
|
758f80a4fc | ||
|
e58b08d22a | ||
|
62f3cd3f5a | ||
|
39cccc23e5 | ||
|
f0be627dcb | ||
|
1bdd8ae784 | ||
|
2d5a19f081 | ||
|
d9fe4798fa | ||
|
39eec87284 | ||
|
285a7440a3 | ||
|
2f7cc9d0ae | ||
|
ca4f638dfd | ||
|
4480da752c | ||
|
1f7b593857 | ||
|
6807e32ec1 | ||
|
54e748b0ce | ||
|
c8224bcf4e | ||
|
1941f0b639 | ||
|
e17b80a5a2 | ||
|
6dc4722665 | ||
|
6ea3677b9f | ||
|
fe2291a59b | ||
|
a789ec7f4b | ||
|
6b511964bb | ||
|
ddbac29d14 | ||
|
f4557d0bff | ||
|
6479f74a57 | ||
|
27c90b7fff | ||
|
9ea190b729 | ||
|
c2782568f1 | ||
|
8dbc664a30 | ||
|
f72b6f758b | ||
|
4fd4588482 | ||
|
94c96cfe0e | ||
|
44b5413716 | ||
|
e15134beac | ||
|
22e60d6a71 | ||
|
8297d60ca0 | ||
|
4281228da3 | ||
|
395a71bf53 | ||
|
1e4bf6704a | ||
|
b01fbb4a22 | ||
|
f75a26cb9b | ||
|
21ae1c98fb | ||
|
7b8a4b7a51 | ||
|
715d4ae2d5 | ||
|
22b184b570 | ||
|
665ee4da22 | ||
|
ecd18e3eb2 | ||
|
e7f95ca6b2 | ||
|
4418e338e8 | ||
|
6c71fc00b2 | ||
|
727758e068 | ||
|
63eb1b870c | ||
|
2d5826d194 | ||
|
9f4ef2fc3d | ||
|
fd8aacc1a4 | ||
|
2ee11a0a9d | ||
|
9512cb9472 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install MSRV toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
HELIX_LOG_LEVEL: info
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install MSRV toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install MSRV toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install MSRV toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
|
2
.github/workflows/cachix.yml
vendored
2
.github/workflows/cachix.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
2
.github/workflows/gh-pages.yml
vendored
2
.github/workflows/gh-pages.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
|
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -58,23 +58,21 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false # don't fail other jobs if one fails
|
||||
matrix:
|
||||
build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
|
||||
build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows, aarch64-macos] #, x86_64-win-gnu, win32-msvc
|
||||
include:
|
||||
- build: x86_64-linux
|
||||
os: ubuntu-24.04
|
||||
# WARN: When changing this to a newer version, make sure that the GLIBC isnt too new, as this can cause issues
|
||||
# with portablity on older systems that dont follow ubuntus more rapid release cadence.
|
||||
os: ubuntu-22.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
cross: false
|
||||
- build: aarch64-linux
|
||||
os: ubuntu-24.04-arm
|
||||
# Version should be kept in lockstep with the x86_64 version
|
||||
os: ubuntu-22.04-arm
|
||||
rust: stable
|
||||
target: aarch64-unknown-linux-gnu
|
||||
cross: false
|
||||
# - build: riscv64-linux
|
||||
# os: ubuntu-22.04
|
||||
# rust: stable
|
||||
# target: riscv64gc-unknown-linux-gnu
|
||||
# cross: true
|
||||
- build: x86_64-macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
@@ -85,13 +83,16 @@ jobs:
|
||||
rust: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
cross: false
|
||||
# 23.03: build issues
|
||||
- build: aarch64-macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: aarch64-apple-darwin
|
||||
cross: false
|
||||
skip_tests: true # x86_64 host can't run aarch64 code
|
||||
# - build: riscv64-linux
|
||||
# os: ubuntu-22.04
|
||||
# rust: stable
|
||||
# target: riscv64gc-unknown-linux-gnu
|
||||
# cross: true
|
||||
# - build: x86_64-win-gnu
|
||||
# os: windows-2019
|
||||
# rust: stable-x86_64-gnu
|
||||
@@ -103,16 +104,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download grammars
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
|
||||
- name: Move grammars under runtime
|
||||
if: "!startsWith(matrix.os, 'windows')"
|
||||
run: |
|
||||
mkdir -p runtime/grammars/sources
|
||||
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
|
||||
tar xJf grammars.tar.xz -C runtime/grammars/sources
|
||||
|
||||
# The rust-toolchain action ignores rust-toolchain.toml files.
|
||||
# Removing this before building with cargo ensures that the rust-toolchain
|
||||
@@ -213,7 +214,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p dist
|
||||
if [ "${{ matrix.os }}" = "windows-2019" ]; then
|
||||
if [ "${{ matrix.os }}" = "windows-latest" ]; then
|
||||
cp "target/${{ matrix.target }}/opt/hx.exe" "dist/"
|
||||
else
|
||||
cp "target/${{ matrix.target }}/opt/hx" "dist/"
|
||||
@@ -234,9 +235,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
|
||||
- name: Build archive
|
||||
shell: bash
|
||||
@@ -291,7 +292,7 @@ jobs:
|
||||
file_glob: true
|
||||
tag: ${{ github.ref_name }}
|
||||
overwrite: true
|
||||
|
||||
|
||||
- name: Upload binaries as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: env.preview == 'true'
|
||||
|
@@ -20,6 +20,10 @@ Updated languages and queries:
|
||||
Packaging:
|
||||
-->
|
||||
|
||||
# 25.07.1 (2025-07-18)
|
||||
|
||||
This is a patch release which lowers the GLIBC requirements of the release artifacts published to GitHub ([#13983](https://github.com/helix-editor/helix/pull/13983))
|
||||
|
||||
# 25.07 (2025-07-15)
|
||||
|
||||
As always, a big thank you to all of the contributors! This release saw changes from 195 contributors.
|
||||
|
593
Cargo.lock
generated
593
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,7 @@ tree-house = { version = "0.3.0", default-features = false }
|
||||
nucleo = "0.5.0"
|
||||
slotmap = "1.0.7"
|
||||
thiserror = "2.0"
|
||||
tempfile = "3.20.0"
|
||||
tempfile = "3.21.0"
|
||||
bitflags = "2.9"
|
||||
unicode-segmentation = "1.2"
|
||||
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
|
||||
@@ -51,9 +51,10 @@ futures-executor = "0.3"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
tokio-stream = "0.1.17"
|
||||
toml = "0.9"
|
||||
termina = "0.1.0"
|
||||
|
||||
[workspace.package]
|
||||
version = "25.7.0"
|
||||
version = "25.7.1"
|
||||
edition = "2021"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
categories = ["editor"]
|
||||
|
@@ -11,6 +11,7 @@
|
||||
- [Textobjects](./textobjects.md)
|
||||
- [Syntax aware motions](./syntax-aware-motions.md)
|
||||
- [Pickers](./pickers.md)
|
||||
- [Jumplist](./jumplist.md)
|
||||
- [Keymap](./keymap.md)
|
||||
- [Command line](./command-line.md)
|
||||
- [Commands](./commands.md)
|
||||
@@ -28,3 +29,5 @@
|
||||
- [Adding textobject queries](./guides/textobject.md)
|
||||
- [Adding indent queries](./guides/indent.md)
|
||||
- [Adding injection queries](./guides/injection.md)
|
||||
- [Adding tags queries](./guides/tags.md)
|
||||
- [Adding rainbow bracket queries](./guides/rainbow_bracket_queries.md)
|
||||
|
@@ -42,7 +42,7 @@ RUSTFLAGS="-C target-feature=-crt-static"
|
||||
# Optimized
|
||||
cargo install \
|
||||
--profile opt \
|
||||
--config 'build.rustflags="-C target-cpu=native"' \
|
||||
--config 'build.rustflags=["-C", "target-cpu=native"]' \
|
||||
--path helix-term \
|
||||
--locked
|
||||
```
|
||||
|
@@ -47,6 +47,8 @@ The following variables are supported:
|
||||
| `cursor_column` | The column number of the primary cursor in the currently focused document, starting at 1. This is counted as the number of grapheme clusters from the start of the line rather than bytes or codepoints. |
|
||||
| `buffer_name` | The relative path of the currently focused document. `[scratch]` is expanded instead for scratch buffers. |
|
||||
| `line_ending` | A string containing the line ending of the currently focused document. For example on Unix systems this is usually a line-feed character (`\n`) but on Windows systems this may be a carriage-return plus a line-feed (`\r\n`). The line ending kind of the currently focused document can be inspected with the `:line-ending` command. |
|
||||
| `current_working_directory` | Current working directory |
|
||||
| `workspace_directory` | Nearest ancestor directory of the current working directory that contains `.git`, `.svn`, `jj` or `.helix` |
|
||||
| `language` | A string containing the language name of the currently focused document.|
|
||||
| `selection` | A string containing the contents of the primary selection of the currently focused document. |
|
||||
| `selection_line_start` | The line number of the start of the primary selection in the currently focused document, starting at 1. |
|
||||
|
@@ -19,6 +19,7 @@
|
||||
- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
|
||||
- [`[editor.smart-tab]` Section](#editorsmart-tab-section)
|
||||
- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section)
|
||||
- [`[editor.word-completion]` Section](#editorword-completion-section)
|
||||
|
||||
### `[editor]` Section
|
||||
|
||||
@@ -59,9 +60,10 @@
|
||||
| `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"`
|
||||
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
|
||||
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | `"hint"`
|
||||
| `clipboard-provider` | Which API to use for clipboard interaction. One of `pasteboard` (MacOS), `wayland`, `x-clip`, `x-sel`, `win-32-yank`, `termux`, `tmux`, `windows`, `termcode`, `none`, or a custom command set. | Platform and environment specific. |
|
||||
| `editor-config` | Whether to read settings from [EditorConfig](https://editorconfig.org) files | `true` |
|
||||
| `rainbow-brackets` | Whether to render rainbow colors for matching brackets. Requires tree-sitter `rainbows.scm` queries for the language. | `false` |
|
||||
|
||||
### `[editor.clipboard-provider]` Section
|
||||
|
||||
@@ -131,6 +133,7 @@ The following statusline elements can be configured:
|
||||
| `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 |
|
||||
| `current-working-directory` | The current working directory |
|
||||
| `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 |
|
||||
| `file-line-ending` | The file line endings (CRLF or LF) |
|
||||
@@ -450,7 +453,7 @@ fn main() {
|
||||
|
||||
| Key | Description | Default |
|
||||
|------------|-------------|---------|
|
||||
| `cursor-line` | The minimum severity that a diagnostic must have to be shown inline on the line that contains the primary cursor. Set to `disable` to not show any diagnostics inline. This option does not have any effect when in insert-mode and will only take effect 350ms after moving the cursor to a different line. | `"disable"` |
|
||||
| `cursor-line` | The minimum severity that a diagnostic must have to be shown inline on the line that contains the primary cursor. Set to `disable` to not show any diagnostics inline. This option does not have any effect when in insert-mode and will only take effect 350ms after moving the cursor to a different line. | `"warning"` |
|
||||
| `other-lines` | The minimum severity that a diagnostic must have to be shown inline on a line that does not contain the cursor-line. Set to `disable` to not show any diagnostics inline. | `"disable"` |
|
||||
| `prefix-len` | How many horizontal bars `─` are rendered before the diagnostic text. | `1` |
|
||||
| `max-wrap` | Equivalent of the `editor.soft-wrap.max-wrap` option for diagnostics. | `20` |
|
||||
@@ -468,12 +471,20 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
### `[editor.word-completion]` Section
|
||||
|
||||
The new diagnostic rendering is not yet enabled by default. As soon as end of line or inline diagnostics are enabled the old diagnostics rendering is automatically disabled. The recommended default setting are:
|
||||
Options for controlling completion of words from open buffers.
|
||||
|
||||
| Key | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `enable` | Whether word completion is enabled | `true` |
|
||||
| `trigger-length` | Number of word characters to type before triggering completion | `7` |
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
[editor]
|
||||
end-of-line-diagnostics = "hint"
|
||||
[editor.inline-diagnostics]
|
||||
cursor-line = "warning" # show warnings and errors on the cursorline inline
|
||||
[editor.word-completion]
|
||||
enable = true
|
||||
# Set the trigger length lower so that words are completed more often
|
||||
trigger-length = 4
|
||||
```
|
||||
|
@@ -1,282 +1,297 @@
|
||||
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default language servers |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| ada | ✓ | ✓ | | `ada_language_server` |
|
||||
| adl | ✓ | ✓ | ✓ | |
|
||||
| agda | ✓ | | | |
|
||||
| alloy | ✓ | | | |
|
||||
| amber | ✓ | | | `amber-lsp` |
|
||||
| astro | ✓ | | | `astro-ls` |
|
||||
| awk | ✓ | ✓ | | `awk-language-server` |
|
||||
| bash | ✓ | ✓ | ✓ | `bash-language-server` |
|
||||
| bass | ✓ | | | `bass` |
|
||||
| beancount | ✓ | | | `beancount-language-server` |
|
||||
| bibtex | ✓ | | | `texlab` |
|
||||
| bicep | ✓ | | | `bicep-langserver` |
|
||||
| bitbake | ✓ | | | `bitbake-language-server` |
|
||||
| blade | ✓ | | | |
|
||||
| blueprint | ✓ | | | `blueprint-compiler` |
|
||||
| c | ✓ | ✓ | ✓ | `clangd` |
|
||||
| c-sharp | ✓ | ✓ | | `OmniSharp` |
|
||||
| cabal | | | | `haskell-language-server-wrapper` |
|
||||
| caddyfile | ✓ | ✓ | ✓ | |
|
||||
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
|
||||
| capnp | ✓ | | ✓ | |
|
||||
| cel | ✓ | | | |
|
||||
| circom | ✓ | | | `circom-lsp` |
|
||||
| clarity | ✓ | | | `clarinet` |
|
||||
| clojure | ✓ | | | `clojure-lsp` |
|
||||
| cmake | ✓ | ✓ | ✓ | `neocmakelsp`, `cmake-language-server` |
|
||||
| codeql | ✓ | ✓ | | `codeql` |
|
||||
| comment | ✓ | | | |
|
||||
| common-lisp | ✓ | | ✓ | `cl-lsp` |
|
||||
| cpon | ✓ | | ✓ | |
|
||||
| cpp | ✓ | ✓ | ✓ | `clangd` |
|
||||
| crystal | ✓ | ✓ | ✓ | `crystalline`, `ameba-ls` |
|
||||
| css | ✓ | | ✓ | `vscode-css-language-server` |
|
||||
| csv | ✓ | | | |
|
||||
| cue | ✓ | | | `cuelsp` |
|
||||
| cylc | ✓ | ✓ | ✓ | |
|
||||
| d | ✓ | ✓ | ✓ | `serve-d` |
|
||||
| dart | ✓ | ✓ | ✓ | `dart` |
|
||||
| dbml | ✓ | | | |
|
||||
| debian | ✓ | | | |
|
||||
| devicetree | ✓ | | | `dts-lsp` |
|
||||
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
|
||||
| diff | ✓ | | | |
|
||||
| djot | ✓ | | | |
|
||||
| docker-compose | ✓ | ✓ | ✓ | `docker-compose-langserver`, `yaml-language-server` |
|
||||
| dockerfile | ✓ | ✓ | | `docker-langserver` |
|
||||
| dot | ✓ | | | `dot-language-server` |
|
||||
| dtd | ✓ | | | |
|
||||
| dune | ✓ | | | |
|
||||
| dunstrc | ✓ | | | |
|
||||
| earthfile | ✓ | ✓ | ✓ | `earthlyls` |
|
||||
| edoc | ✓ | | | |
|
||||
| eex | ✓ | | | |
|
||||
| ejs | ✓ | | | |
|
||||
| elisp | ✓ | | | |
|
||||
| elixir | ✓ | ✓ | ✓ | `elixir-ls` |
|
||||
| elm | ✓ | ✓ | | `elm-language-server` |
|
||||
| elvish | ✓ | | | `elvish` |
|
||||
| env | ✓ | ✓ | | |
|
||||
| erb | ✓ | | | |
|
||||
| erlang | ✓ | ✓ | | `erlang_ls`, `elp` |
|
||||
| esdl | ✓ | | | |
|
||||
| fennel | ✓ | | | `fennel-ls` |
|
||||
| fga | ✓ | ✓ | ✓ | |
|
||||
| fidl | ✓ | | | |
|
||||
| fish | ✓ | ✓ | ✓ | `fish-lsp` |
|
||||
| forth | ✓ | | | `forth-lsp` |
|
||||
| fortran | ✓ | | ✓ | `fortls` |
|
||||
| fsharp | ✓ | | | `fsautocomplete` |
|
||||
| gas | ✓ | ✓ | | `asm-lsp` |
|
||||
| gdscript | ✓ | ✓ | ✓ | |
|
||||
| gemini | ✓ | | | |
|
||||
| gherkin | ✓ | | | |
|
||||
| ghostty | ✓ | | | |
|
||||
| git-attributes | ✓ | | | |
|
||||
| git-commit | ✓ | ✓ | | |
|
||||
| git-config | ✓ | ✓ | | |
|
||||
| git-ignore | ✓ | | | |
|
||||
| git-notes | ✓ | | | |
|
||||
| git-rebase | ✓ | | | |
|
||||
| gjs | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
|
||||
| gleam | ✓ | ✓ | | `gleam` |
|
||||
| glimmer | ✓ | | | `ember-language-server` |
|
||||
| glsl | ✓ | ✓ | ✓ | `glsl_analyzer` |
|
||||
| gn | ✓ | | | |
|
||||
| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
|
||||
| godot-resource | ✓ | ✓ | | |
|
||||
| gomod | ✓ | | | `gopls` |
|
||||
| gotmpl | ✓ | | | `gopls` |
|
||||
| gowork | ✓ | | | `gopls` |
|
||||
| gpr | ✓ | | | `ada_language_server` |
|
||||
| graphql | ✓ | ✓ | | `graphql-lsp` |
|
||||
| gren | ✓ | ✓ | | |
|
||||
| groovy | ✓ | | | |
|
||||
| gts | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
|
||||
| hare | ✓ | | | |
|
||||
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
|
||||
| haskell-persistent | ✓ | | | |
|
||||
| hcl | ✓ | ✓ | ✓ | `terraform-ls` |
|
||||
| heex | ✓ | ✓ | | `elixir-ls` |
|
||||
| helm | ✓ | | | `helm_ls` |
|
||||
| hocon | ✓ | ✓ | ✓ | |
|
||||
| hoon | ✓ | | | |
|
||||
| hosts | ✓ | | | |
|
||||
| html | ✓ | | | `vscode-html-language-server`, `superhtml` |
|
||||
| htmldjango | ✓ | | | `djlsp`, `vscode-html-language-server`, `superhtml` |
|
||||
| hurl | ✓ | ✓ | ✓ | |
|
||||
| hyprlang | ✓ | | ✓ | `hyprls` |
|
||||
| idris | | | | `idris2-lsp` |
|
||||
| iex | ✓ | | | |
|
||||
| ini | ✓ | | | |
|
||||
| ink | ✓ | | | |
|
||||
| inko | ✓ | ✓ | ✓ | |
|
||||
| janet | ✓ | | | |
|
||||
| java | ✓ | ✓ | ✓ | `jdtls` |
|
||||
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| jinja | ✓ | | | |
|
||||
| jjconfig | ✓ | ✓ | ✓ | `taplo`, `tombi` |
|
||||
| jjdescription | ✓ | | | |
|
||||
| jjrevset | ✓ | | | |
|
||||
| jjtemplate | ✓ | | | |
|
||||
| jq | ✓ | ✓ | | `jq-lsp` |
|
||||
| jsdoc | ✓ | | | |
|
||||
| json | ✓ | ✓ | ✓ | `vscode-json-language-server` |
|
||||
| json-ld | ✓ | ✓ | ✓ | `vscode-json-language-server` |
|
||||
| json5 | ✓ | | | |
|
||||
| jsonc | ✓ | | ✓ | `vscode-json-language-server` |
|
||||
| jsonnet | ✓ | | | `jsonnet-language-server` |
|
||||
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| julia | ✓ | ✓ | ✓ | `julia` |
|
||||
| just | ✓ | ✓ | ✓ | `just-lsp` |
|
||||
| kdl | ✓ | ✓ | ✓ | |
|
||||
| koka | ✓ | | ✓ | `koka` |
|
||||
| kotlin | ✓ | ✓ | ✓ | `kotlin-language-server` |
|
||||
| koto | ✓ | ✓ | ✓ | `koto-ls` |
|
||||
| latex | ✓ | ✓ | | `texlab` |
|
||||
| ld | ✓ | | ✓ | |
|
||||
| ldif | ✓ | | | |
|
||||
| lean | ✓ | | | `lean` |
|
||||
| ledger | ✓ | | | |
|
||||
| llvm | ✓ | ✓ | ✓ | |
|
||||
| llvm-mir | ✓ | ✓ | ✓ | |
|
||||
| llvm-mir-yaml | ✓ | | ✓ | |
|
||||
| log | ✓ | | | |
|
||||
| lpf | ✓ | | | |
|
||||
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
|
||||
| luau | ✓ | ✓ | ✓ | `luau-lsp` |
|
||||
| mail | ✓ | ✓ | | |
|
||||
| make | ✓ | | ✓ | |
|
||||
| markdoc | ✓ | | | `markdoc-ls` |
|
||||
| markdown | ✓ | | | `marksman`, `markdown-oxide` |
|
||||
| markdown-rustdoc | ✓ | | | |
|
||||
| markdown.inline | ✓ | | | |
|
||||
| matlab | ✓ | ✓ | ✓ | |
|
||||
| mermaid | ✓ | | | |
|
||||
| meson | ✓ | | ✓ | `mesonlsp` |
|
||||
| mint | | | | `mint` |
|
||||
| mojo | ✓ | ✓ | ✓ | `pixi` |
|
||||
| move | ✓ | | | |
|
||||
| msbuild | ✓ | | ✓ | |
|
||||
| nasm | ✓ | ✓ | | `asm-lsp` |
|
||||
| nestedtext | ✓ | ✓ | ✓ | |
|
||||
| nginx | ✓ | | | |
|
||||
| nickel | ✓ | | ✓ | `nls` |
|
||||
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
|
||||
| nix | ✓ | ✓ | ✓ | `nil`, `nixd` |
|
||||
| nu | ✓ | | | `nu` |
|
||||
| nunjucks | ✓ | | | |
|
||||
| ocaml | ✓ | | ✓ | `ocamllsp` |
|
||||
| ocaml-interface | ✓ | | | `ocamllsp` |
|
||||
| odin | ✓ | ✓ | ✓ | `ols` |
|
||||
| ohm | ✓ | ✓ | ✓ | |
|
||||
| opencl | ✓ | ✓ | ✓ | `clangd` |
|
||||
| openscad | ✓ | | | `openscad-lsp` |
|
||||
| org | ✓ | | | |
|
||||
| pascal | ✓ | ✓ | | `pasls` |
|
||||
| passwd | ✓ | | | |
|
||||
| pem | ✓ | | | |
|
||||
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
|
||||
| pest | ✓ | ✓ | ✓ | `pest-language-server` |
|
||||
| php | ✓ | ✓ | ✓ | `intelephense` |
|
||||
| php-only | ✓ | | | |
|
||||
| pkgbuild | ✓ | ✓ | ✓ | `termux-language-server`, `bash-language-server` |
|
||||
| pkl | ✓ | | ✓ | `pkl-lsp` |
|
||||
| po | ✓ | ✓ | | |
|
||||
| pod | ✓ | | | |
|
||||
| ponylang | ✓ | ✓ | ✓ | |
|
||||
| powershell | ✓ | | | |
|
||||
| prisma | ✓ | ✓ | | `prisma-language-server` |
|
||||
| prolog | ✓ | | ✓ | `swipl` |
|
||||
| properties | ✓ | ✓ | | |
|
||||
| protobuf | ✓ | ✓ | ✓ | `buf`, `pb`, `protols` |
|
||||
| prql | ✓ | | | |
|
||||
| pug | ✓ | | | |
|
||||
| purescript | ✓ | ✓ | | `purescript-language-server` |
|
||||
| python | ✓ | ✓ | ✓ | `ty`, `ruff`, `jedi-language-server`, `pylsp` |
|
||||
| qml | ✓ | ✓ | ✓ | `qmlls` |
|
||||
| quarto | ✓ | | ✓ | |
|
||||
| quint | ✓ | | | `quint-language-server` |
|
||||
| r | ✓ | | | `R` |
|
||||
| racket | ✓ | | ✓ | `racket` |
|
||||
| regex | ✓ | | | |
|
||||
| rego | ✓ | | | `regols` |
|
||||
| rescript | ✓ | ✓ | | `rescript-language-server` |
|
||||
| rmarkdown | ✓ | | ✓ | `R` |
|
||||
| robot | ✓ | | | `robotframework_ls` |
|
||||
| ron | ✓ | | ✓ | |
|
||||
| rst | ✓ | | | |
|
||||
| ruby | ✓ | ✓ | ✓ | `ruby-lsp`, `solargraph` |
|
||||
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
|
||||
| rust-format-args | ✓ | | | |
|
||||
| rust-format-args-macro | ✓ | ✓ | ✓ | |
|
||||
| sage | ✓ | ✓ | | |
|
||||
| scala | ✓ | ✓ | ✓ | `metals` |
|
||||
| scheme | ✓ | | ✓ | |
|
||||
| scss | ✓ | | | `vscode-css-language-server` |
|
||||
| slang | ✓ | ✓ | ✓ | `slangd` |
|
||||
| slint | ✓ | ✓ | ✓ | `slint-lsp` |
|
||||
| smali | ✓ | | ✓ | |
|
||||
| smithy | ✓ | | | `cs` |
|
||||
| sml | ✓ | | | |
|
||||
| snakemake | ✓ | | ✓ | `pylsp` |
|
||||
| solidity | ✓ | ✓ | | `solc` |
|
||||
| sourcepawn | ✓ | ✓ | | `sourcepawn-studio` |
|
||||
| spade | ✓ | | ✓ | `spade-language-server` |
|
||||
| spicedb | ✓ | | | |
|
||||
| sql | ✓ | ✓ | | |
|
||||
| sshclientconfig | ✓ | | | |
|
||||
| starlark | ✓ | ✓ | ✓ | `starpls` |
|
||||
| strace | ✓ | | | |
|
||||
| supercollider | ✓ | | | |
|
||||
| svelte | ✓ | | ✓ | `svelteserver` |
|
||||
| sway | ✓ | ✓ | ✓ | `forc` |
|
||||
| swift | ✓ | ✓ | | `sourcekit-lsp` |
|
||||
| systemd | ✓ | | | `systemd-lsp` |
|
||||
| t32 | ✓ | | | |
|
||||
| tablegen | ✓ | ✓ | ✓ | |
|
||||
| tact | ✓ | ✓ | ✓ | |
|
||||
| task | ✓ | | | |
|
||||
| tcl | ✓ | | ✓ | |
|
||||
| teal | ✓ | | | `teal-language-server` |
|
||||
| templ | ✓ | | | `templ` |
|
||||
| tera | ✓ | | | |
|
||||
| textproto | ✓ | ✓ | ✓ | |
|
||||
| tfvars | ✓ | | ✓ | `terraform-ls` |
|
||||
| thrift | ✓ | | | |
|
||||
| tlaplus | ✓ | | | |
|
||||
| todotxt | ✓ | | | |
|
||||
| toml | ✓ | ✓ | | `taplo`, `tombi` |
|
||||
| tsq | ✓ | | | `ts_query_ls` |
|
||||
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| twig | ✓ | | | |
|
||||
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| typespec | ✓ | ✓ | ✓ | `tsp-server` |
|
||||
| typst | ✓ | | | `tinymist` |
|
||||
| ungrammar | ✓ | | | |
|
||||
| unison | ✓ | ✓ | ✓ | |
|
||||
| uxntal | ✓ | | | |
|
||||
| v | ✓ | ✓ | ✓ | `v-analyzer` |
|
||||
| vala | ✓ | ✓ | | `vala-language-server` |
|
||||
| vento | ✓ | | | |
|
||||
| verilog | ✓ | ✓ | | `svlangserver` |
|
||||
| vhdl | ✓ | | | `vhdl_ls` |
|
||||
| vhs | ✓ | | | |
|
||||
| vue | ✓ | | | `vue-language-server` |
|
||||
| wast | ✓ | | | |
|
||||
| wat | ✓ | | | `wat_server` |
|
||||
| webc | ✓ | | | |
|
||||
| werk | ✓ | | | |
|
||||
| wesl | ✓ | ✓ | | |
|
||||
| wgsl | ✓ | | | `wgsl-analyzer` |
|
||||
| wit | ✓ | | ✓ | |
|
||||
| wren | ✓ | ✓ | ✓ | |
|
||||
| xit | ✓ | | | |
|
||||
| xml | ✓ | | ✓ | |
|
||||
| xtc | ✓ | | | |
|
||||
| yaml | ✓ | ✓ | ✓ | `yaml-language-server`, `ansible-language-server` |
|
||||
| yara | ✓ | | | `yls` |
|
||||
| yuck | ✓ | | | |
|
||||
| zig | ✓ | ✓ | ✓ | `zls` |
|
||||
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Code Navigation Tags | Rainbow Brackets | Default language servers |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ada | ✓ | ✓ | | | | `ada_language_server` |
|
||||
| adl | ✓ | ✓ | ✓ | | | |
|
||||
| agda | ✓ | | | | | |
|
||||
| alloy | ✓ | | | | | |
|
||||
| amber | ✓ | | | | | `amber-lsp` |
|
||||
| astro | ✓ | | | | | `astro-ls` |
|
||||
| awk | ✓ | ✓ | | | | `awk-language-server` |
|
||||
| bash | ✓ | ✓ | ✓ | ✓ | ✓ | `bash-language-server` |
|
||||
| bass | ✓ | | | | | `bass` |
|
||||
| beancount | ✓ | | | | | `beancount-language-server` |
|
||||
| bibtex | ✓ | | | | | `texlab` |
|
||||
| bicep | ✓ | | | | | `bicep-langserver` |
|
||||
| bitbake | ✓ | | | | | `bitbake-language-server` |
|
||||
| blade | ✓ | ✓ | | | ✓ | |
|
||||
| blueprint | ✓ | | | | | `blueprint-compiler` |
|
||||
| c | ✓ | ✓ | ✓ | ✓ | ✓ | `clangd` |
|
||||
| c-sharp | ✓ | ✓ | | ✓ | | `OmniSharp` |
|
||||
| cabal | | | | | | `haskell-language-server-wrapper` |
|
||||
| caddyfile | ✓ | ✓ | ✓ | | | |
|
||||
| cairo | ✓ | ✓ | ✓ | | | `cairo-language-server` |
|
||||
| capnp | ✓ | | ✓ | | | |
|
||||
| cel | ✓ | | | | | |
|
||||
| circom | ✓ | | | | | `circom-lsp` |
|
||||
| clarity | ✓ | | | | | `clarinet` |
|
||||
| clojure | ✓ | | | | ✓ | `clojure-lsp` |
|
||||
| cmake | ✓ | ✓ | ✓ | | | `neocmakelsp`, `cmake-language-server` |
|
||||
| codeql | ✓ | ✓ | | | | `codeql` |
|
||||
| comment | ✓ | | | | | |
|
||||
| common-lisp | ✓ | | ✓ | | ✓ | `cl-lsp` |
|
||||
| cpon | ✓ | | ✓ | | | |
|
||||
| cpp | ✓ | ✓ | ✓ | ✓ | ✓ | `clangd` |
|
||||
| cross-config | ✓ | ✓ | | | ✓ | `taplo`, `tombi` |
|
||||
| crystal | ✓ | ✓ | ✓ | ✓ | | `crystalline`, `ameba-ls` |
|
||||
| css | ✓ | | ✓ | | ✓ | `vscode-css-language-server` |
|
||||
| csv | ✓ | | | | | |
|
||||
| cue | ✓ | | | | | `cuelsp` |
|
||||
| cylc | ✓ | ✓ | ✓ | | | |
|
||||
| cython | ✓ | | ✓ | ✓ | | |
|
||||
| d | ✓ | ✓ | ✓ | | | `serve-d` |
|
||||
| dart | ✓ | ✓ | ✓ | | | `dart` |
|
||||
| dbml | ✓ | | | | | |
|
||||
| debian | ✓ | | | | | |
|
||||
| devicetree | ✓ | | | | | `dts-lsp` |
|
||||
| dhall | ✓ | ✓ | | | | `dhall-lsp-server` |
|
||||
| diff | ✓ | | | | | |
|
||||
| djot | ✓ | | | | | |
|
||||
| docker-compose | ✓ | ✓ | ✓ | | | `docker-compose-langserver`, `yaml-language-server` |
|
||||
| dockerfile | ✓ | ✓ | | | | `docker-langserver` |
|
||||
| dot | ✓ | | | | | `dot-language-server` |
|
||||
| doxyfile | ✓ | ✓ | ✓ | ✓ | | |
|
||||
| dtd | ✓ | | | | | |
|
||||
| dune | ✓ | | | | | |
|
||||
| dunstrc | ✓ | | | | | |
|
||||
| earthfile | ✓ | ✓ | ✓ | | | `earthlyls` |
|
||||
| edoc | ✓ | | | | | |
|
||||
| eex | ✓ | | | | | |
|
||||
| ejs | ✓ | | | | | |
|
||||
| elisp | ✓ | | | ✓ | | |
|
||||
| elixir | ✓ | ✓ | ✓ | ✓ | ✓ | `elixir-ls` |
|
||||
| elm | ✓ | ✓ | | ✓ | | `elm-language-server` |
|
||||
| elvish | ✓ | | | | | `elvish` |
|
||||
| env | ✓ | ✓ | | | | |
|
||||
| erb | ✓ | | | | | |
|
||||
| erlang | ✓ | ✓ | | ✓ | ✓ | `erlang_ls`, `elp` |
|
||||
| esdl | ✓ | | | | | |
|
||||
| fennel | ✓ | | | | | `fennel-ls` |
|
||||
| fga | ✓ | ✓ | ✓ | | | |
|
||||
| fidl | ✓ | | | | | |
|
||||
| fish | ✓ | ✓ | ✓ | | | `fish-lsp` |
|
||||
| flatbuffers | ✓ | | | | | |
|
||||
| forth | ✓ | | | | | `forth-lsp` |
|
||||
| fortran | ✓ | | ✓ | | | `fortls` |
|
||||
| fsharp | ✓ | | | | | `fsautocomplete` |
|
||||
| gas | ✓ | ✓ | | | | `asm-lsp` |
|
||||
| gdscript | ✓ | ✓ | ✓ | ✓ | | |
|
||||
| gemini | ✓ | | | | | |
|
||||
| gherkin | ✓ | | | | | |
|
||||
| ghostty | ✓ | | | | | |
|
||||
| git-attributes | ✓ | | | | | |
|
||||
| git-cliff-config | ✓ | ✓ | | | ✓ | `taplo`, `tombi` |
|
||||
| git-commit | ✓ | ✓ | | | | |
|
||||
| git-config | ✓ | ✓ | | | | |
|
||||
| git-ignore | ✓ | | | | | |
|
||||
| git-notes | ✓ | | | | | |
|
||||
| git-rebase | ✓ | | | | | |
|
||||
| gjs | ✓ | ✓ | ✓ | ✓ | | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
|
||||
| gleam | ✓ | ✓ | | | ✓ | `gleam` |
|
||||
| glimmer | ✓ | | | | | `ember-language-server` |
|
||||
| glsl | ✓ | ✓ | ✓ | | | `glsl_analyzer` |
|
||||
| gn | ✓ | | | | | |
|
||||
| go | ✓ | ✓ | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
|
||||
| go-format-string | ✓ | | | | ✓ | |
|
||||
| godot-resource | ✓ | ✓ | | | | |
|
||||
| gomod | ✓ | | | | | `gopls` |
|
||||
| gotmpl | ✓ | | | | | `gopls` |
|
||||
| gowork | ✓ | | | | | `gopls` |
|
||||
| gpr | ✓ | | | | | `ada_language_server` |
|
||||
| graphql | ✓ | ✓ | | | | `graphql-lsp` |
|
||||
| gren | ✓ | ✓ | | | | |
|
||||
| groovy | ✓ | | | | | |
|
||||
| gts | ✓ | ✓ | ✓ | ✓ | | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
|
||||
| hare | ✓ | | | | | |
|
||||
| haskell | ✓ | ✓ | | | | `haskell-language-server-wrapper` |
|
||||
| haskell-persistent | ✓ | | | | | |
|
||||
| hcl | ✓ | ✓ | ✓ | | | `terraform-ls` |
|
||||
| hdl | ✓ | | | | | |
|
||||
| heex | ✓ | ✓ | | | | `elixir-ls` |
|
||||
| helm | ✓ | | | | | `helm_ls` |
|
||||
| hocon | ✓ | ✓ | ✓ | | | |
|
||||
| hoon | ✓ | | | | | |
|
||||
| hosts | ✓ | | | | | |
|
||||
| html | ✓ | ✓ | | | ✓ | `vscode-html-language-server`, `superhtml` |
|
||||
| htmldjango | ✓ | | | | | `djlsp`, `vscode-html-language-server`, `superhtml` |
|
||||
| hurl | ✓ | ✓ | ✓ | | | |
|
||||
| hyprlang | ✓ | | ✓ | | | `hyprls` |
|
||||
| idris | | | | | | `idris2-lsp` |
|
||||
| iex | ✓ | | | | | |
|
||||
| ini | ✓ | | | | | |
|
||||
| ink | ✓ | | | | | |
|
||||
| inko | ✓ | ✓ | ✓ | ✓ | | |
|
||||
| janet | ✓ | | ✓ | | ✓ | |
|
||||
| java | ✓ | ✓ | ✓ | ✓ | ✓ | `jdtls` |
|
||||
| javascript | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| jinja | ✓ | | | | | |
|
||||
| jjconfig | ✓ | ✓ | ✓ | | | `taplo`, `tombi` |
|
||||
| jjdescription | ✓ | | | | | |
|
||||
| jjrevset | ✓ | | | | | |
|
||||
| jjtemplate | ✓ | | | | | |
|
||||
| jq | ✓ | ✓ | | | | `jq-lsp` |
|
||||
| jsdoc | ✓ | | | | | |
|
||||
| json | ✓ | ✓ | ✓ | | ✓ | `vscode-json-language-server` |
|
||||
| json-ld | ✓ | ✓ | ✓ | | | `vscode-json-language-server` |
|
||||
| json5 | ✓ | | | | | |
|
||||
| jsonc | ✓ | | ✓ | | | `vscode-json-language-server` |
|
||||
| jsonnet | ✓ | | | | | `jsonnet-language-server` |
|
||||
| jsx | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| julia | ✓ | ✓ | ✓ | | | `julia` |
|
||||
| just | ✓ | ✓ | ✓ | ✓ | | `just-lsp` |
|
||||
| kconfig | ✓ | | ✓ | | | |
|
||||
| kdl | ✓ | ✓ | ✓ | | | |
|
||||
| koka | ✓ | | ✓ | | | `koka` |
|
||||
| kotlin | ✓ | ✓ | ✓ | ✓ | | `kotlin-language-server` |
|
||||
| koto | ✓ | ✓ | ✓ | | | `koto-ls` |
|
||||
| latex | ✓ | ✓ | | | | `texlab` |
|
||||
| ld | ✓ | | ✓ | | | |
|
||||
| ldif | ✓ | | | | | |
|
||||
| lean | ✓ | | | | | `lean` |
|
||||
| ledger | ✓ | | | | | |
|
||||
| llvm | ✓ | ✓ | ✓ | | | |
|
||||
| llvm-mir | ✓ | ✓ | ✓ | | | |
|
||||
| llvm-mir-yaml | ✓ | | ✓ | | | |
|
||||
| log | ✓ | | | | | |
|
||||
| lpf | ✓ | | | | | |
|
||||
| lua | ✓ | ✓ | ✓ | | ✓ | `lua-language-server` |
|
||||
| luap | ✓ | | | | | |
|
||||
| luau | ✓ | ✓ | ✓ | | | `luau-lsp` |
|
||||
| mail | ✓ | ✓ | | | | |
|
||||
| make | ✓ | | ✓ | | | |
|
||||
| markdoc | ✓ | | | | | `markdoc-ls` |
|
||||
| markdown | ✓ | | | ✓ | | `marksman`, `markdown-oxide` |
|
||||
| markdown-rustdoc | ✓ | | | | | |
|
||||
| markdown.inline | ✓ | | | | | |
|
||||
| matlab | ✓ | ✓ | ✓ | | | |
|
||||
| mermaid | ✓ | | | | | |
|
||||
| meson | ✓ | | ✓ | | | `mesonlsp` |
|
||||
| mint | | | | | | `mint` |
|
||||
| mojo | ✓ | ✓ | ✓ | | | `pixi` |
|
||||
| move | ✓ | | | | | |
|
||||
| msbuild | ✓ | | ✓ | | | |
|
||||
| nasm | ✓ | ✓ | | | | `asm-lsp` |
|
||||
| nestedtext | ✓ | ✓ | ✓ | | | |
|
||||
| nginx | ✓ | | | | | |
|
||||
| nickel | ✓ | | ✓ | | | `nls` |
|
||||
| nim | ✓ | ✓ | ✓ | | | `nimlangserver` |
|
||||
| nix | ✓ | ✓ | ✓ | | ✓ | `nil`, `nixd` |
|
||||
| nu | ✓ | | | | | `nu` |
|
||||
| nunjucks | ✓ | | | | | |
|
||||
| ocaml | ✓ | | ✓ | | | `ocamllsp` |
|
||||
| ocaml-interface | ✓ | | | | | `ocamllsp` |
|
||||
| odin | ✓ | ✓ | ✓ | | | `ols` |
|
||||
| ohm | ✓ | ✓ | ✓ | | | |
|
||||
| opencl | ✓ | ✓ | ✓ | | | `clangd` |
|
||||
| openscad | ✓ | | | | | `openscad-lsp` |
|
||||
| org | ✓ | | | | | |
|
||||
| pascal | ✓ | ✓ | | | | `pasls` |
|
||||
| passwd | ✓ | | | | | |
|
||||
| pem | ✓ | | | | | |
|
||||
| perl | ✓ | ✓ | ✓ | | | `perlnavigator` |
|
||||
| pest | ✓ | ✓ | ✓ | | | `pest-language-server` |
|
||||
| php | ✓ | ✓ | ✓ | ✓ | ✓ | `intelephense` |
|
||||
| php-only | ✓ | | | ✓ | | |
|
||||
| pip-requirements | ✓ | | | | | |
|
||||
| pkgbuild | ✓ | ✓ | ✓ | | | `termux-language-server`, `bash-language-server` |
|
||||
| pkl | ✓ | | ✓ | | | `pkl-lsp` |
|
||||
| po | ✓ | ✓ | | | | |
|
||||
| pod | ✓ | | | | | |
|
||||
| ponylang | ✓ | ✓ | ✓ | | | |
|
||||
| powershell | ✓ | | | | | |
|
||||
| prisma | ✓ | ✓ | | | | `prisma-language-server` |
|
||||
| prolog | ✓ | | ✓ | | | `swipl` |
|
||||
| properties | ✓ | ✓ | | | | |
|
||||
| protobuf | ✓ | ✓ | ✓ | ✓ | | `buf`, `pb`, `protols` |
|
||||
| prql | ✓ | | | | | |
|
||||
| pug | ✓ | | | | | |
|
||||
| purescript | ✓ | ✓ | | | | `purescript-language-server` |
|
||||
| python | ✓ | ✓ | ✓ | ✓ | ✓ | `ty`, `ruff`, `jedi-language-server`, `pylsp` |
|
||||
| qml | ✓ | ✓ | ✓ | | | `qmlls` |
|
||||
| quarto | ✓ | | ✓ | | | |
|
||||
| quint | ✓ | | | | | `quint-language-server` |
|
||||
| r | ✓ | | | | | `R` |
|
||||
| racket | ✓ | | ✓ | | ✓ | `racket` |
|
||||
| regex | ✓ | | | | ✓ | |
|
||||
| rego | ✓ | | | | | `regols` |
|
||||
| rescript | ✓ | ✓ | | | | `rescript-language-server` |
|
||||
| rmarkdown | ✓ | | ✓ | | | `R` |
|
||||
| robot | ✓ | | | | | `robotframework_ls` |
|
||||
| robots.txt | ✓ | ✓ | | ✓ | | |
|
||||
| ron | ✓ | | ✓ | ✓ | ✓ | |
|
||||
| rst | ✓ | | | | | |
|
||||
| ruby | ✓ | ✓ | ✓ | ✓ | ✓ | `ruby-lsp`, `solargraph` |
|
||||
| rust | ✓ | ✓ | ✓ | ✓ | ✓ | `rust-analyzer` |
|
||||
| rust-format-args | ✓ | | | | | |
|
||||
| rust-format-args-macro | ✓ | ✓ | ✓ | | ✓ | |
|
||||
| sage | ✓ | ✓ | | | | |
|
||||
| scala | ✓ | ✓ | ✓ | | | `metals` |
|
||||
| scheme | ✓ | | ✓ | | ✓ | |
|
||||
| scss | ✓ | | | | ✓ | `vscode-css-language-server` |
|
||||
| shellcheckrc | ✓ | ✓ | | | | |
|
||||
| slang | ✓ | ✓ | ✓ | | | `slangd` |
|
||||
| slint | ✓ | ✓ | ✓ | | | `slint-lsp` |
|
||||
| smali | ✓ | | ✓ | | | |
|
||||
| smithy | ✓ | | | | | `cs` |
|
||||
| sml | ✓ | | | | | |
|
||||
| snakemake | ✓ | | ✓ | | | `pylsp` |
|
||||
| solidity | ✓ | ✓ | | | | `solc` |
|
||||
| sourcepawn | ✓ | ✓ | | | | `sourcepawn-studio` |
|
||||
| spade | ✓ | | ✓ | | | `spade-language-server` |
|
||||
| spicedb | ✓ | | | ✓ | | |
|
||||
| sql | ✓ | ✓ | | | | |
|
||||
| sshclientconfig | ✓ | | | | | |
|
||||
| starlark | ✓ | ✓ | ✓ | | ✓ | `starpls` |
|
||||
| strace | ✓ | | | | | |
|
||||
| strictdoc | ✓ | | | ✓ | | |
|
||||
| supercollider | ✓ | | | | | |
|
||||
| svelte | ✓ | | ✓ | | | `svelteserver` |
|
||||
| sway | ✓ | ✓ | ✓ | | | `forc` |
|
||||
| swift | ✓ | ✓ | | | ✓ | `sourcekit-lsp` |
|
||||
| systemd | ✓ | | | | | `systemd-lsp` |
|
||||
| systemverilog | ✓ | | | | | |
|
||||
| t32 | ✓ | | | | | |
|
||||
| tablegen | ✓ | ✓ | ✓ | | | |
|
||||
| tact | ✓ | ✓ | ✓ | | | |
|
||||
| task | ✓ | | | | | |
|
||||
| tcl | ✓ | | ✓ | | | |
|
||||
| teal | ✓ | | | | | `teal-language-server` |
|
||||
| templ | ✓ | | | | | `templ` |
|
||||
| tera | ✓ | | | | | |
|
||||
| textproto | ✓ | ✓ | ✓ | | | |
|
||||
| tfvars | ✓ | | ✓ | | | `terraform-ls` |
|
||||
| thrift | ✓ | | | | | |
|
||||
| tlaplus | ✓ | | | | | |
|
||||
| todotxt | ✓ | | | | | |
|
||||
| toml | ✓ | ✓ | | | ✓ | `taplo`, `tombi` |
|
||||
| tsq | ✓ | | | | ✓ | `ts_query_ls` |
|
||||
| tsx | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| twig | ✓ | | | | | |
|
||||
| typescript | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| typespec | ✓ | ✓ | ✓ | | | `tsp-server` |
|
||||
| typst | ✓ | | | ✓ | | `tinymist` |
|
||||
| ungrammar | ✓ | | | | | |
|
||||
| unison | ✓ | ✓ | ✓ | | | |
|
||||
| uxntal | ✓ | | | | | |
|
||||
| v | ✓ | ✓ | ✓ | | | `v-analyzer` |
|
||||
| vala | ✓ | ✓ | | | | `vala-language-server` |
|
||||
| vento | ✓ | | | | | |
|
||||
| verilog | ✓ | ✓ | | | | `svlangserver` |
|
||||
| vhdl | ✓ | | | | | `vhdl_ls` |
|
||||
| vhs | ✓ | | | | | |
|
||||
| vim | ✓ | | | | | |
|
||||
| vue | ✓ | | | | | `vue-language-server` |
|
||||
| wast | ✓ | | | | | |
|
||||
| wat | ✓ | | | | | `wat_server` |
|
||||
| webc | ✓ | | | | | |
|
||||
| werk | ✓ | | | | | |
|
||||
| wesl | ✓ | ✓ | | | | |
|
||||
| wgsl | ✓ | ✓ | ✓ | ✓ | ✓ | `wgsl-analyzer` |
|
||||
| wit | ✓ | | ✓ | | | |
|
||||
| wren | ✓ | ✓ | ✓ | | | |
|
||||
| xit | ✓ | | | | | |
|
||||
| xml | ✓ | ✓ | ✓ | | ✓ | |
|
||||
| xtc | ✓ | | | | | |
|
||||
| yaml | ✓ | ✓ | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
|
||||
| yara | ✓ | | | | | `yls` |
|
||||
| yuck | ✓ | | | | | |
|
||||
| zig | ✓ | ✓ | ✓ | | | `zls` |
|
||||
|
@@ -106,10 +106,14 @@
|
||||
| `code_action` | Perform code action | normal: `` <space>a ``, select: `` <space>a `` |
|
||||
| `buffer_picker` | Open buffer picker | normal: `` <space>b ``, select: `` <space>b `` |
|
||||
| `jumplist_picker` | Open jumplist picker | normal: `` <space>j ``, select: `` <space>j `` |
|
||||
| `symbol_picker` | Open symbol picker | normal: `` <space>s ``, select: `` <space>s `` |
|
||||
| `symbol_picker` | Open symbol picker | |
|
||||
| `syntax_symbol_picker` | Open symbol picker from syntax information | |
|
||||
| `lsp_or_syntax_symbol_picker` | Open symbol picker from LSP or syntax information | normal: `` <space>s ``, select: `` <space>s `` |
|
||||
| `changed_file_picker` | Open changed file picker | normal: `` <space>g ``, select: `` <space>g `` |
|
||||
| `select_references_to_symbol_under_cursor` | Select symbol references | normal: `` <space>h ``, select: `` <space>h `` |
|
||||
| `workspace_symbol_picker` | Open workspace symbol picker | normal: `` <space>S ``, select: `` <space>S `` |
|
||||
| `workspace_symbol_picker` | Open workspace symbol picker | |
|
||||
| `syntax_workspace_symbol_picker` | Open workspace symbol picker from syntax information | |
|
||||
| `lsp_or_syntax_workspace_symbol_picker` | Open workspace symbol picker from LSP or syntax information | normal: `` <space>S ``, select: `` <space>S `` |
|
||||
| `diagnostics_picker` | Open diagnostic picker | normal: `` <space>d ``, select: `` <space>d `` |
|
||||
| `workspace_diagnostics_picker` | Open workspace diagnostic picker | normal: `` <space>D ``, select: `` <space>D `` |
|
||||
| `last_picker` | Open last picker | normal: `` <space>' ``, select: `` <space>' `` |
|
||||
@@ -168,6 +172,8 @@
|
||||
| `smart_tab` | Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command. | insert: `` <tab> `` |
|
||||
| `insert_tab` | Insert tab char | insert: `` <S-tab> `` |
|
||||
| `insert_newline` | Insert newline char | insert: `` <C-j> ``, `` <ret> `` |
|
||||
| `insert_char_interactive` | Insert an interactively-chosen char | |
|
||||
| `append_char_interactive` | Append an interactively-chosen char | |
|
||||
| `delete_char_backward` | Delete previous char | insert: `` <C-h> ``, `` <backspace> ``, `` <S-backspace> `` |
|
||||
| `delete_char_forward` | Delete next char | insert: `` <C-d> ``, `` <del> `` |
|
||||
| `delete_word_backward` | Delete previous word | insert: `` <C-w> ``, `` <A-backspace> `` |
|
||||
@@ -267,6 +273,8 @@
|
||||
| `goto_prev_comment` | Goto previous comment | normal: `` [c ``, select: `` [c `` |
|
||||
| `goto_next_test` | Goto next test | normal: `` ]T ``, select: `` ]T `` |
|
||||
| `goto_prev_test` | Goto previous test | normal: `` [T ``, select: `` [T `` |
|
||||
| `goto_next_xml_element` | Goto next (X)HTML element | normal: `` ]x ``, select: `` ]x `` |
|
||||
| `goto_prev_xml_element` | Goto previous (X)HTML element | normal: `` [x ``, select: `` [x `` |
|
||||
| `goto_next_entry` | Goto next pairing | normal: `` ]e ``, select: `` ]e `` |
|
||||
| `goto_prev_entry` | Goto previous pairing | normal: `` [e ``, select: `` [e `` |
|
||||
| `goto_next_paragraph` | Goto next paragraph | normal: `` ]p ``, select: `` ]p `` |
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# Guides
|
||||
|
||||
This section contains guides for adding new language server configurations,
|
||||
tree-sitter grammars, textobject queries, and other similar items.
|
||||
tree-sitter grammars, textobject and rainbow bracket queries, and other similar items.
|
||||
|
132
book/src/guides/rainbow_bracket_queries.md
Normal file
132
book/src/guides/rainbow_bracket_queries.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Adding Rainbow Bracket Queries
|
||||
|
||||
Helix uses `rainbows.scm` tree-sitter query files to provide rainbow bracket
|
||||
functionality.
|
||||
|
||||
Tree-sitter queries are documented in the tree-sitter online documentation.
|
||||
If you're writing queries for the first time, be sure to check out the section
|
||||
on [syntax highlighting queries] and on [query syntax].
|
||||
|
||||
Rainbow queries have two captures: `@rainbow.scope` and `@rainbow.bracket`.
|
||||
`@rainbow.scope` should capture any node that increases the nesting level
|
||||
while `@rainbow.bracket` should capture any bracket nodes. Put another way:
|
||||
`@rainbow.scope` switches to the next rainbow color for all nodes in the tree
|
||||
under it while `@rainbow.bracket` paints captured nodes with the current
|
||||
rainbow color.
|
||||
|
||||
For an example, let's add rainbow queries for the tree-sitter query (TSQ)
|
||||
language itself. These queries will go into a
|
||||
`runtime/queries/tsq/rainbows.scm` file in the repository root.
|
||||
|
||||
First we'll add the `@rainbow.bracket` captures. TSQ only has parentheses and
|
||||
square brackets:
|
||||
|
||||
```tsq
|
||||
["(" ")" "[" "]"] @rainbow.bracket
|
||||
```
|
||||
|
||||
The ordering of the nodes within the alternation (square brackets) is not
|
||||
taken into consideration.
|
||||
|
||||
> Note: Why are these nodes quoted? Most syntax highlights capture text
|
||||
> surrounded by parentheses. These are _named nodes_ and correspond to the
|
||||
> names of rules in the grammar. Brackets are usually written in tree-sitter
|
||||
> grammars as literal strings, for example:
|
||||
>
|
||||
> ```js
|
||||
> {
|
||||
> // ...
|
||||
> arguments: seq("(", repeat($.argument), ")"),
|
||||
> // ...
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Nodes written as literal strings in tree-sitter grammars may be captured
|
||||
> in queries with those same literal strings.
|
||||
|
||||
Then we'll add `@rainbow.scope` captures. The easiest way to do this is to
|
||||
view the `grammar.js` file in the tree-sitter grammar's repository. For TSQ,
|
||||
that file is [here][tsq grammar.js]. As we scroll down the `grammar.js`, we
|
||||
see that the `(alternation)`, (L36) `(group)` (L57), `(named_node)` (L59),
|
||||
`(predicate)` (L87) and `(wildcard_node)` (L97) nodes all contain literal
|
||||
parentheses or square brackets in their definitions. These nodes are all
|
||||
direct parents of brackets and happen to also be the nodes we want to change
|
||||
to the next rainbow color, so we capture them as `@rainbow.scope`.
|
||||
|
||||
```tsq
|
||||
[
|
||||
(group)
|
||||
(named_node)
|
||||
(wildcard_node)
|
||||
(predicate)
|
||||
(alternation)
|
||||
] @rainbow.scope
|
||||
```
|
||||
|
||||
This strategy works as a rule of thumb for most programming and configuration
|
||||
languages. Markup languages can be trickier and may take additional
|
||||
experimentation to find the correct nodes to use for scopes and brackets.
|
||||
|
||||
The `:tree-sitter-subtree` command shows the syntax tree under the primary
|
||||
selection in S-expression format and can be a useful tool for determining how
|
||||
to write a query.
|
||||
|
||||
### Properties
|
||||
|
||||
The `rainbow.include-children` property may be applied to `@rainbow.scope`
|
||||
captures. By default, all `@rainbow.bracket` captures must be direct descendant
|
||||
of a node captured with `@rainbow.scope` in a syntax tree in order to be
|
||||
highlighted. The `rainbow.include-children` property disables that check and
|
||||
allows `@rainbow.bracket` captures to be highlighted if they are direct or
|
||||
indirect descendants of some node captured with `@rainbow.scope`.
|
||||
|
||||
For example, this property is used in the HTML rainbow queries.
|
||||
|
||||
For a document like `<a>link</a>`, the syntax tree is:
|
||||
|
||||
```tsq
|
||||
(element ; <a>link</a>
|
||||
(start_tag ; <a>
|
||||
(tag_name)) ; a
|
||||
(text) ; link
|
||||
(end_tag ; </a>
|
||||
(tag_name))) ; a
|
||||
```
|
||||
|
||||
If we want to highlight the `<`, `>` and `</` nodes with rainbow colors, we
|
||||
capture them as `@rainbow.bracket`:
|
||||
|
||||
```tsq
|
||||
["<" ">" "</"] @rainbow.bracket
|
||||
```
|
||||
|
||||
And we capture `(element)` as `@rainbow.scope` because `(element)` nodes nest
|
||||
within each other: they increment the nesting level and switch to the next
|
||||
color in the rainbow.
|
||||
|
||||
```tsq
|
||||
(element) @rainbow.scope
|
||||
```
|
||||
|
||||
But this combination of `@rainbow.scope` and `@rainbow.bracket` will not
|
||||
highlight any nodes. `<`, `>` and `</` are children of the `(start_tag)` and
|
||||
`(end_tag)` nodes. We can't capture `(start_tag)` and `(end_tag)` as
|
||||
`@rainbow.scope` because they don't nest other elements. We can fix this case
|
||||
by removing the requirement that `<`, `>` and `</` are direct descendants of
|
||||
`(element)` using the `rainbow.include-children` property.
|
||||
|
||||
```tsq
|
||||
((element) @rainbow.scope
|
||||
(#set! rainbow.include-children))
|
||||
```
|
||||
|
||||
With this property set, `<`, `>`, and `</` will highlight with rainbow colors
|
||||
even though they aren't direct descendents of the `(element)` node.
|
||||
|
||||
`rainbow.include-children` is not necessary for the vast majority of programming
|
||||
languages. It is only necessary when the node that increments the nesting level
|
||||
(changes rainbow color) is not the direct parent of the bracket node.
|
||||
|
||||
[syntax highlighting queries]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#highlights
|
||||
[query syntax]: https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries
|
||||
[tsq grammar.js]: https://github.com/the-mikedavis/tree-sitter-tsq/blob/48b5e9f82ae0a4727201626f33a17f69f8e0ff86/grammar.js
|
34
book/src/guides/tags.md
Normal file
34
book/src/guides/tags.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## Adding tags queries
|
||||
|
||||
See tree-sitter's documentation on [Code Navigation Systems] for more
|
||||
background on tags queries.
|
||||
|
||||
Helix provides LSP-like features such as document and workspace symbol pickers
|
||||
out-of-the-box for languages with `tags.scm` queries based on syntax trees. To
|
||||
be analyzed a language must have a tree-sitter grammar and a `tags.scm` query
|
||||
file which pattern matches interesting nodes from syntax trees.
|
||||
|
||||
Query files should be placed in `runtime/queries/{language}/tags.scm`
|
||||
when contributing to Helix. You may place these under your local runtime
|
||||
directory (`~/.config/helix/runtime` in Linux for example) for the sake of
|
||||
testing.
|
||||
|
||||
The following [captures][tree-sitter-captures] are recognized:
|
||||
|
||||
| Capture name |
|
||||
|--- |
|
||||
| `definition.class` |
|
||||
| `definition.constant` |
|
||||
| `definition.function` |
|
||||
| `definition.interface` |
|
||||
| `definition.macro` |
|
||||
| `definition.module` |
|
||||
| `definition.struct` |
|
||||
| `definition.type` |
|
||||
|
||||
[Example query files][example-queries] can be found in the Helix GitHub
|
||||
repository.
|
||||
|
||||
[Code Navigation Systems]: https://tree-sitter.github.io/tree-sitter/4-code-navigation.html
|
||||
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers/queries/index.html
|
||||
[example-queries]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+path%3A%2A%2A/tags.scm&type=Code
|
@@ -28,6 +28,8 @@ The following [captures][tree-sitter-captures] are recognized:
|
||||
| `comment.around` |
|
||||
| `entry.inside` |
|
||||
| `entry.around` |
|
||||
| `xml-element.inside` |
|
||||
| `xml-element.around` |
|
||||
|
||||
[Example query files][textobject-examples] can be found in the helix GitHub repository.
|
||||
|
||||
|
36
book/src/jumplist.md
Normal file
36
book/src/jumplist.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## Using the jumplist
|
||||
|
||||
To help with quick navigation, Helix maintains a list of "jumps" called the jumplist.
|
||||
Whenever you make a significant movement (see next section), Helix stores your selections from before the move as a jump.
|
||||
A jump serves as a kind of checkpoint, allowing you to jump to a separate location, make edits, and return to where you were with your previous selections.
|
||||
This way, the jumplist tracks both your previous location and your selections.
|
||||
You can manually save a jump by using `Ctrl-s`.
|
||||
To jump backward in the jumplist, use `Ctrl-o`; to go forward, use `Ctrl-i`. To view and select from the full jumplist, use `Space-j` to open the jumplist picker.
|
||||
|
||||
### What makes a jump
|
||||
The following is a non-exhaustive list of which actions add a jump to the jumplist:
|
||||
- Switching buffers
|
||||
- Using the buffer picker, going to the next/previous buffer
|
||||
- Going to the last accessed/modified file
|
||||
- Making a new file (`:new FILE`)
|
||||
- Opening a file (`:open FILE`)
|
||||
- Includes `:log-open`, `:config-open`, `:config-open-workspace`, `:tutor`
|
||||
- Navigating by pickers, global search, or the file explorer
|
||||
- `goto_file` (`gf`)
|
||||
- Big in-file movements
|
||||
- `select_regex` (`s`)
|
||||
- `split_regex` (`S`)
|
||||
- `search` (`/`)
|
||||
- `keep_selections` and `remove_selections` (`K` and `<A-K>`)
|
||||
- `goto_file_start` (`gg`)
|
||||
- `goto_file_end`
|
||||
- `goto_last_line` (`ge`)
|
||||
- `:goto 123` / `:123` / `123G`
|
||||
- `goto_definition` (`gd`)
|
||||
- `goto_declaration` (`gD`)
|
||||
- `goto_type_definition` (`gy`)
|
||||
- `goto_reference` (`gr`)
|
||||
- Other
|
||||
- `Ctrl-s` manually creates a jump
|
||||
- Trying to close a modified buffer can switch you to that buffer and create a jump
|
||||
- The debugger can create jumps as you jump stack frames
|
@@ -35,6 +35,8 @@ Normal mode is the default mode when you launch helix. You can return to it from
|
||||
|
||||
> NOTE: Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line.
|
||||
|
||||
> Hereafter, `<n>` represents an integer by typing a sequence of digits.
|
||||
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `h`, `Left` | Move left | `move_char_left` |
|
||||
@@ -51,7 +53,7 @@ Normal mode is the default mode when you launch helix. You can return to it from
|
||||
| `f` | Find next char | `find_next_char` |
|
||||
| `T` | Find till previous char | `till_prev_char` |
|
||||
| `F` | Find previous char | `find_prev_char` |
|
||||
| `G` | Go to line number `<n>` | `goto_line` |
|
||||
| `<n>G`, `<n>gg` | Go to line number `<n>` | `goto_line` |
|
||||
| `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` |
|
||||
@@ -212,8 +214,10 @@ Jumps to various locations.
|
||||
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `g` | Go to line number `<n>` else start of file | `goto_file_start` |
|
||||
| <code>|</code> | Go to column number `<n>` else start of line | `goto_column` |
|
||||
| `<n>g`| Go to line number `<n>` | `goto_file_start` |
|
||||
| `g` | Go to the start of the file | `goto_file_start` |
|
||||
| <code><n>|</code> | Go to column number `<n>` | `goto_column` |
|
||||
| <code>|</code> | Go to the start of line | `goto_column` |
|
||||
| `e` | Go to the end of the file | `goto_last_line` |
|
||||
| `f` | Go to files in the selections | `goto_file` |
|
||||
| `h` | Go to the start of the line | `goto_line_start` |
|
||||
@@ -348,30 +352,32 @@ Displays the signature of the selected completion item. Remapping currently not
|
||||
|
||||
These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
|
||||
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
|
||||
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
|
||||
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
|
||||
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
|
||||
| `]f` | Go to next function (**TS**) | `goto_next_function` |
|
||||
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
|
||||
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
|
||||
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
|
||||
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
|
||||
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
|
||||
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
|
||||
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
|
||||
| `]T` | Go to next test (**TS**) | `goto_next_test` |
|
||||
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
|
||||
| `]p` | Go to next paragraph | `goto_next_paragraph` |
|
||||
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
|
||||
| `]g` | Go to next change | `goto_next_change` |
|
||||
| `[g` | Go to previous change | `goto_prev_change` |
|
||||
| `]G` | Go to last change | `goto_last_change` |
|
||||
| `[G` | Go to first change | `goto_first_change` |
|
||||
| `]Space` | Add newline below | `add_newline_below` |
|
||||
| `[Space` | Add newline above | `add_newline_above` |
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
|
||||
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
|
||||
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
|
||||
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
|
||||
| `]f` | Go to next function (**TS**) | `goto_next_function` |
|
||||
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
|
||||
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
|
||||
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
|
||||
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
|
||||
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
|
||||
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
|
||||
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
|
||||
| `]T` | Go to next test (**TS**) | `goto_next_test` |
|
||||
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
|
||||
| `]p` | Go to next paragraph | `goto_next_paragraph` |
|
||||
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
|
||||
| `]g` | Go to next change | `goto_next_change` |
|
||||
| `[g` | Go to previous change | `goto_prev_change` |
|
||||
| `]G` | Go to last change | `goto_last_change` |
|
||||
| `[G` | Go to first change | `goto_first_change` |
|
||||
| `[x` | Go to next (X)HTML element | `goto_next_xml_element` |
|
||||
| `]x` | Go to previous (X)HTML element | `goto_prev_xml_element` |
|
||||
| `]Space` | Add newline below | `add_newline_below` |
|
||||
| `[Space` | Add newline above | `add_newline_above` |
|
||||
|
||||
## Insert mode
|
||||
|
||||
|
@@ -71,8 +71,10 @@ These configuration keys are available:
|
||||
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
|
||||
| `rulers` | Overrides the `editor.rulers` config key for the language. |
|
||||
| `path-completion` | Overrides the `editor.path-completion` config key for the language. |
|
||||
| `word-completion` | Overrides the [`editor.word-completion`](./editor.md#editorword-completion-section) configuration for the language. |
|
||||
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
|
||||
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.
|
||||
| `rainbow-brackets` | Overrides the `editor.rainbow-brackets` config key for the language |
|
||||
|
||||
### File-type detection and the `file-types` key
|
||||
|
||||
@@ -109,7 +111,7 @@ of the formatter command. In particular, the `%{buffer_name}` variable can be pa
|
||||
argument to the formatter:
|
||||
|
||||
```toml
|
||||
formatter = { command = "mylang-formatter" , args = ["--stdin", "--stdin-filename %{buffer_name}"] }
|
||||
formatter = { command = "mylang-formatter" , args = ["--stdin", "--stdin-filename", "%{buffer_name}"] }
|
||||
```
|
||||
|
||||
## Language Server configuration
|
||||
|
@@ -15,6 +15,8 @@ Helix' keymap and interaction model ([Using Helix](#usage.md)) is easier to adop
|
||||
| [Visual Studio Code](https://code.visualstudio.com/) | [Helix for VS Code](https://marketplace.visualstudio.com/items?itemName=jasew.vscode-helix-emulation) extension|
|
||||
| [Zed](https://zed.dev/) | native via keybindings ([Bug](https://github.com/zed-industries/zed/issues/4642)) |
|
||||
| [CodeMirror](https://codemirror.net/) | [codemirror-helix](https://gitlab.com/_rvidal/codemirror-helix) |
|
||||
| [Lite XL](https://lite-xl.com/) | [lite-modal-hx](https://codeberg.org/Mandarancio/lite-modal-hx) |
|
||||
| [Lapce](https://lap.dev/lapce/) | | Requested: https://github.com/lapce/lapce/issues/281 |
|
||||
|
||||
|
||||
## Shells
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
- [Linux](#linux)
|
||||
- [Ubuntu/Debian](#ubuntudebian)
|
||||
- [Ubuntu (PPA)](#ubuntu-ppa)
|
||||
- [Fedora/RHEL](#fedorarhel)
|
||||
- [Arch Linux extra](#arch-linux-extra)
|
||||
- [NixOS](#nixos)
|
||||
@@ -26,21 +25,11 @@ The following third party repositories are available:
|
||||
|
||||
### Ubuntu/Debian
|
||||
|
||||
Install the Debian package from the release page.
|
||||
Install the Debian package [from the release page](https://github.com/helix-editor/helix/releases/latest).
|
||||
|
||||
If you are running a system older than Ubuntu 22.04, Mint 21, or Debian 12, you can build the `.deb` file locally
|
||||
[from source](./building-from-source.md#building-the-debian-package).
|
||||
|
||||
### Ubuntu (PPA)
|
||||
|
||||
Add the `PPA` for Helix:
|
||||
|
||||
```sh
|
||||
sudo add-apt-repository ppa:maveonair/helix-editor
|
||||
sudo apt update
|
||||
sudo apt install helix
|
||||
```
|
||||
|
||||
### Fedora/RHEL
|
||||
|
||||
```sh
|
||||
|
@@ -89,24 +89,26 @@ Cmd-s = ":write" # Cmd or Win or Meta and 's' to write
|
||||
|
||||
Special keys are encoded as follows:
|
||||
|
||||
| Key name | Representation |
|
||||
| --- | --- |
|
||||
| Backspace | `"backspace"` |
|
||||
| Space | `"space"` |
|
||||
| Return/Enter | `"ret"` |
|
||||
| Left | `"left"` |
|
||||
| Right | `"right"` |
|
||||
| Up | `"up"` |
|
||||
| Down | `"down"` |
|
||||
| Home | `"home"` |
|
||||
| End | `"end"` |
|
||||
| Page Up | `"pageup"` |
|
||||
| Page Down | `"pagedown"` |
|
||||
| Tab | `"tab"` |
|
||||
| Delete | `"del"` |
|
||||
| Insert | `"ins"` |
|
||||
| Null | `"null"` |
|
||||
| Escape | `"esc"` |
|
||||
| Key name | Representation |
|
||||
| --- | --- |
|
||||
| Backspace | `"backspace"` |
|
||||
| Space | `"space"` |
|
||||
| Return/Enter | `"ret"` |
|
||||
| Left | `"left"` |
|
||||
| Right | `"right"` |
|
||||
| Up | `"up"` |
|
||||
| Down | `"down"` |
|
||||
| Home | `"home"` |
|
||||
| End | `"end"` |
|
||||
| Page Up | `"pageup"` |
|
||||
| Page Down | `"pagedown"` |
|
||||
| Tab | `"tab"` |
|
||||
| Delete | `"del"` |
|
||||
| Insert | `"ins"` |
|
||||
| Null | `"null"` |
|
||||
| Escape | `"esc"` |
|
||||
| Less Than (<) | `"lt"` |
|
||||
| Greater Than (>) | `"gt"` |
|
||||
|
||||
Keys can be disabled by binding them to the `no_op` command.
|
||||
|
||||
|
@@ -24,6 +24,7 @@ function or block of code.
|
||||
| `c` | Comment |
|
||||
| `T` | Test |
|
||||
| `g` | Change |
|
||||
| `x` | (X)HTML element |
|
||||
|
||||
> 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current
|
||||
document and a special tree-sitter query file to work properly. [Only
|
||||
|
@@ -130,6 +130,17 @@ inherits = "boo_berry"
|
||||
berry = "#2A2A4D"
|
||||
```
|
||||
|
||||
### Rainbow
|
||||
|
||||
The `rainbow` key is used for rainbow highlight for matching brackets.
|
||||
The key is a list of styles.
|
||||
|
||||
```toml
|
||||
rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }]
|
||||
```
|
||||
|
||||
Colors from the palette and modifiers may be used.
|
||||
|
||||
### Scopes
|
||||
|
||||
The following is a list of scopes available to use for styling:
|
||||
|
@@ -47,6 +47,9 @@
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<releases>
|
||||
<release version="25.07.1" date="2025-07-18">
|
||||
<url>https://github.com/helix-editor/helix/releases/tag/25.07.1</url>
|
||||
</release>
|
||||
<release version="25.07" date="2025-07-15">
|
||||
<url>https://helix-editor.com/news/release-25-07-highlights/</url>
|
||||
</release>
|
||||
|
@@ -13,8 +13,8 @@ _hx() {
|
||||
return 0
|
||||
;;
|
||||
--health)
|
||||
languages=$(hx --health | tail -n '+7' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g')
|
||||
mapfile -t COMPREPLY < <(compgen -W """$languages""" -- "$cur")
|
||||
languages=$(hx --health all-languages | tail -n '+2' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g')
|
||||
mapfile -t COMPREPLY < <(compgen -W """clipboard languages all-languages all $languages""" -- "$cur")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
@@ -4,6 +4,10 @@
|
||||
complete -c hx -s h -l help -d "Prints help information"
|
||||
complete -c hx -l tutor -d "Loads the tutorial"
|
||||
complete -c hx -l health -xa "(__hx_langs_ops)" -d "Checks for errors"
|
||||
complete -c hx -l health -xka all -d "Prints all diagnostic informations"
|
||||
complete -c hx -l health -xka all-languages -d "Lists all languages"
|
||||
complete -c hx -l health -xka languages -d "Lists user configured languages"
|
||||
complete -c hx -l health -xka clipboard -d "Prints system clipboard provider"
|
||||
complete -c hx -s g -l grammar -x -a "fetch build" -d "Fetch or build tree-sitter grammars"
|
||||
complete -c hx -s v -o vv -o vvv -d "Increases logging verbosity"
|
||||
complete -c hx -s V -l version -d "Prints version information"
|
||||
@@ -14,5 +18,5 @@ complete -c hx -l log -r -d "Specifies a file to use for logging"
|
||||
complete -c hx -s w -l working-dir -d "Specify initial working directory" -xa "(__fish_complete_directories)"
|
||||
|
||||
function __hx_langs_ops
|
||||
hx --health languages | tail -n '+2' | string replace -fr '^(\S+) .*' '$1'
|
||||
hx --health all-languages | tail -n '+2' | string replace -fr '^(\S+) .*' '$1'
|
||||
end
|
||||
|
@@ -5,8 +5,8 @@
|
||||
# The help message won't be overridden though, so it will still be present here
|
||||
|
||||
def health_categories [] {
|
||||
let languages = ^hx --health languages | detect columns | get Language | filter { $in != null }
|
||||
let completions = [ "all", "clipboard", "languages" ] | append $languages
|
||||
let languages = ^hx --health all-languages | detect columns | get Language | where { $in != null }
|
||||
let completions = [ "all", "clipboard", "languages", "all-languages" ] | append $languages
|
||||
return $completions
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,7 @@ _hx() {
|
||||
|
||||
case "$state" in
|
||||
health)
|
||||
local languages=($(hx --health | tail -n '+11' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g;s/[✘✓]//g'))
|
||||
local languages=($(hx --health all-languages | tail -n '+2' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g;s/[✘✓]//g'))
|
||||
_values 'language' $languages
|
||||
;;
|
||||
grammar)
|
||||
|
@@ -46,6 +46,8 @@ in
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
|
||||
propagatedBuildInputs = [ runtimeDir ];
|
||||
|
||||
nativeBuildInputs = [
|
||||
installShellFiles
|
||||
git
|
||||
|
@@ -1,10 +1,12 @@
|
||||
|
||||
| Crate | Description |
|
||||
| ----------- | ----------- |
|
||||
| helix-stdx | Extensions to the standard library (similar to [`rust-analyzer`'s](https://github.com/rust-lang/rust-analyzer/blob/ea413f67a8f730b4211c09e103f8207c62e7dbc3/crates/stdx/Cargo.toml#L5)) |
|
||||
| helix-core | Core editing primitives, functional. |
|
||||
| helix-lsp | Language server client |
|
||||
| helix-lsp-types | Language Server Protocol type definitions |
|
||||
| helix-dap | Debug Adapter Protocol (DAP) client |
|
||||
| helix-event | Primitives for defining and handling events within the editor |
|
||||
| helix-loader | Functions for building, fetching, and loading external resources |
|
||||
| helix-view | UI abstractions for use in backends, imperative shell. |
|
||||
| helix-term | Terminal UI |
|
||||
@@ -110,3 +112,17 @@ The `main` function sets up a new `Application` that runs the event loop.
|
||||
## TUI / Term
|
||||
|
||||
TODO: document Component and rendering related stuff
|
||||
|
||||
## Event
|
||||
|
||||
The `helix-event` crate defines primitives for defining and acting on events
|
||||
within the editor. "Events" cover things like opening, changing and closing of
|
||||
documents, starting and stopping of language servers and more.
|
||||
|
||||
`helix-event` has tools for defining events and registering _hooks_ which run
|
||||
any time an event is emitted. `helix-event` also provides `AsyncHook` - a tool
|
||||
for running cancellable tasks which run after events with _debouncing_.
|
||||
|
||||
See the `AsyncHook` type for more information. Events can be created within the
|
||||
`events!` macro. Synchronous hooks can be created with `register_hook!`. And
|
||||
editor-wide events can be sent to hooks with `helix_event::dispatch`.
|
||||
|
@@ -87,8 +87,6 @@
|
||||
$CC -c src/parser.c -o parser.o $FLAGS
|
||||
$CXX -shared -o $NAME.so *.o
|
||||
|
||||
ls -al
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
|
@@ -16,6 +16,7 @@ pub struct CompletionItem {
|
||||
pub enum CompletionProvider {
|
||||
Lsp(LanguageServerId),
|
||||
Path,
|
||||
Word,
|
||||
}
|
||||
|
||||
impl From<LanguageServerId> for CompletionProvider {
|
||||
|
@@ -228,6 +228,7 @@ impl FromStr for Ini {
|
||||
let glob = GlobBuilder::new(&glob_str)
|
||||
.literal_separator(true)
|
||||
.backslash_escape(true)
|
||||
.empty_alternates(true)
|
||||
.build()?;
|
||||
ini.sections.push(Section {
|
||||
glob: glob.compile_matcher(),
|
||||
|
@@ -153,6 +153,12 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
|
||||
// Give more weight to tabs, because their presence is a very
|
||||
// strong indicator.
|
||||
histogram[0] *= 2;
|
||||
// Gives less weight to single indent, as single spaces are
|
||||
// often used in certain languages' comment systems and rarely
|
||||
// used as the actual document indentation.
|
||||
if histogram[1] > 1 {
|
||||
histogram[1] /= 2;
|
||||
}
|
||||
|
||||
histogram
|
||||
};
|
||||
|
@@ -13,6 +13,7 @@ use std::{
|
||||
use anyhow::{Context, Result};
|
||||
use arc_swap::{ArcSwap, Guard};
|
||||
use config::{Configuration, FileType, LanguageConfiguration, LanguageServerConfiguration};
|
||||
use foldhash::HashSet;
|
||||
use helix_loader::grammar::get_language;
|
||||
use helix_stdx::rope::RopeSliceExt as _;
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -20,7 +21,10 @@ use ropey::RopeSlice;
|
||||
use tree_house::{
|
||||
highlighter,
|
||||
query_iter::QueryIter,
|
||||
tree_sitter::{Grammar, InactiveQueryCursor, InputEdit, Node, Query, RopeInput, Tree},
|
||||
tree_sitter::{
|
||||
query::{InvalidPredicateError, UserPredicate},
|
||||
Capture, Grammar, InactiveQueryCursor, InputEdit, Node, Pattern, Query, RopeInput, Tree,
|
||||
},
|
||||
Error, InjectionLanguageMarker, LanguageConfig as SyntaxConfig, Layer,
|
||||
};
|
||||
|
||||
@@ -28,6 +32,7 @@ use crate::{indent::IndentQuery, tree_sitter, ChangeSet, Language};
|
||||
|
||||
pub use tree_house::{
|
||||
highlighter::{Highlight, HighlightEvent},
|
||||
query_iter::QueryIterEvent,
|
||||
Error as HighlighterError, LanguageLoader, TreeCursor, TREE_SITTER_MATCH_LIMIT,
|
||||
};
|
||||
|
||||
@@ -37,6 +42,8 @@ pub struct LanguageData {
|
||||
syntax: OnceCell<Option<SyntaxConfig>>,
|
||||
indent_query: OnceCell<Option<IndentQuery>>,
|
||||
textobject_query: OnceCell<Option<TextObjectQuery>>,
|
||||
tag_query: OnceCell<Option<TagQuery>>,
|
||||
rainbow_query: OnceCell<Option<RainbowQuery>>,
|
||||
}
|
||||
|
||||
impl LanguageData {
|
||||
@@ -46,6 +53,8 @@ impl LanguageData {
|
||||
syntax: OnceCell::new(),
|
||||
indent_query: OnceCell::new(),
|
||||
textobject_query: OnceCell::new(),
|
||||
tag_query: OnceCell::new(),
|
||||
rainbow_query: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +163,74 @@ impl LanguageData {
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Compiles the tags.scm query for a language.
|
||||
/// This function should only be used by this module or the xtask crate.
|
||||
pub fn compile_tag_query(
|
||||
grammar: Grammar,
|
||||
config: &LanguageConfiguration,
|
||||
) -> Result<Option<TagQuery>> {
|
||||
let name = &config.language_id;
|
||||
let text = read_query(name, "tags.scm");
|
||||
if text.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let query = Query::new(grammar, &text, |_pattern, predicate| match predicate {
|
||||
// TODO: these predicates are allowed in tags.scm queries but not yet used.
|
||||
UserPredicate::IsPropertySet { key: "local", .. } => Ok(()),
|
||||
UserPredicate::Other(pred) => match pred.name() {
|
||||
"strip!" | "select-adjacent!" => Ok(()),
|
||||
_ => Err(InvalidPredicateError::unknown(predicate)),
|
||||
},
|
||||
_ => Err(InvalidPredicateError::unknown(predicate)),
|
||||
})
|
||||
.with_context(|| format!("Failed to compile tags.scm query for '{name}'"))?;
|
||||
Ok(Some(TagQuery { query }))
|
||||
}
|
||||
|
||||
fn tag_query(&self, loader: &Loader) -> Option<&TagQuery> {
|
||||
self.tag_query
|
||||
.get_or_init(|| {
|
||||
let grammar = self.syntax_config(loader)?.grammar;
|
||||
Self::compile_tag_query(grammar, &self.config)
|
||||
.map_err(|err| {
|
||||
log::error!("{err}");
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Compiles the rainbows.scm query for a language.
|
||||
/// This function should only be used by this module or the xtask crate.
|
||||
pub fn compile_rainbow_query(
|
||||
grammar: Grammar,
|
||||
config: &LanguageConfiguration,
|
||||
) -> Result<Option<RainbowQuery>> {
|
||||
let name = &config.language_id;
|
||||
let text = read_query(name, "rainbows.scm");
|
||||
if text.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let rainbow_query = RainbowQuery::new(grammar, &text)
|
||||
.with_context(|| format!("Failed to compile rainbows.scm query for '{name}'"))?;
|
||||
Ok(Some(rainbow_query))
|
||||
}
|
||||
|
||||
fn rainbow_query(&self, loader: &Loader) -> Option<&RainbowQuery> {
|
||||
self.rainbow_query
|
||||
.get_or_init(|| {
|
||||
let grammar = self.syntax_config(loader)?.grammar;
|
||||
Self::compile_rainbow_query(grammar, &self.config)
|
||||
.map_err(|err| {
|
||||
log::error!("{err}");
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn reconfigure(&self, scopes: &[String]) {
|
||||
if let Some(Some(config)) = self.syntax.get() {
|
||||
reconfigure_highlights(config, scopes);
|
||||
@@ -339,6 +416,14 @@ impl Loader {
|
||||
self.language(lang).textobject_query(self)
|
||||
}
|
||||
|
||||
pub fn tag_query(&self, lang: Language) -> Option<&TagQuery> {
|
||||
self.language(lang).tag_query(self)
|
||||
}
|
||||
|
||||
fn rainbow_query(&self, lang: Language) -> Option<&RainbowQuery> {
|
||||
self.language(lang).rainbow_query(self)
|
||||
}
|
||||
|
||||
pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
|
||||
&self.language_server_configs
|
||||
}
|
||||
@@ -511,6 +596,92 @@ impl Syntax {
|
||||
{
|
||||
QueryIter::new(&self.inner, source, loader, range)
|
||||
}
|
||||
|
||||
pub fn tags<'a>(
|
||||
&'a self,
|
||||
source: RopeSlice<'a>,
|
||||
loader: &'a Loader,
|
||||
range: impl RangeBounds<u32>,
|
||||
) -> QueryIter<'a, 'a, impl FnMut(Language) -> Option<&'a Query> + 'a, ()> {
|
||||
self.query_iter(
|
||||
source,
|
||||
|lang| loader.tag_query(lang).map(|q| &q.query),
|
||||
range,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn rainbow_highlights(
|
||||
&self,
|
||||
source: RopeSlice,
|
||||
rainbow_length: usize,
|
||||
loader: &Loader,
|
||||
range: impl RangeBounds<u32>,
|
||||
) -> OverlayHighlights {
|
||||
struct RainbowScope<'tree> {
|
||||
end: u32,
|
||||
node: Option<Node<'tree>>,
|
||||
highlight: Highlight,
|
||||
}
|
||||
|
||||
let mut scope_stack = Vec::<RainbowScope>::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut query_iter = self.query_iter::<_, (), _>(
|
||||
source,
|
||||
|lang| loader.rainbow_query(lang).map(|q| &q.query),
|
||||
range,
|
||||
);
|
||||
|
||||
while let Some(event) = query_iter.next() {
|
||||
let QueryIterEvent::Match(mat) = event else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let rainbow_query = loader
|
||||
.rainbow_query(query_iter.current_language())
|
||||
.expect("language must have a rainbow query to emit matches");
|
||||
|
||||
let byte_range = mat.node.byte_range();
|
||||
// Pop any scopes that end before this capture begins.
|
||||
while scope_stack
|
||||
.last()
|
||||
.is_some_and(|scope| byte_range.start >= scope.end)
|
||||
{
|
||||
scope_stack.pop();
|
||||
}
|
||||
|
||||
let capture = Some(mat.capture);
|
||||
if capture == rainbow_query.scope_capture {
|
||||
scope_stack.push(RainbowScope {
|
||||
end: byte_range.end,
|
||||
node: if rainbow_query
|
||||
.include_children_patterns
|
||||
.contains(&mat.pattern)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(mat.node.clone())
|
||||
},
|
||||
highlight: Highlight::new((scope_stack.len() % rainbow_length) as u32),
|
||||
});
|
||||
} else if capture == rainbow_query.bracket_capture {
|
||||
if let Some(scope) = scope_stack.last() {
|
||||
if !scope
|
||||
.node
|
||||
.as_ref()
|
||||
.is_some_and(|node| mat.node.parent().as_ref() != Some(node))
|
||||
{
|
||||
let start = source
|
||||
.byte_to_char(source.floor_char_boundary(byte_range.start as usize));
|
||||
let end =
|
||||
source.byte_to_char(source.ceil_char_boundary(byte_range.end as usize));
|
||||
highlights.push((scope.highlight, start..end));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OverlayHighlights::Heterogenous { highlights }
|
||||
}
|
||||
}
|
||||
|
||||
pub type Highlighter<'a> = highlighter::Highlighter<'a, 'a, Loader>;
|
||||
@@ -881,6 +1052,11 @@ impl TextObjectQuery {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TagQuery {
|
||||
pub query: Query,
|
||||
}
|
||||
|
||||
pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result {
|
||||
if node.child_count() == 0 {
|
||||
if node_is_visible(&node) {
|
||||
@@ -953,6 +1129,57 @@ fn pretty_print_tree_impl<W: fmt::Write>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finds the child of `node` which contains the given byte range.
|
||||
|
||||
pub fn child_for_byte_range<'a>(node: &Node<'a>, range: ops::Range<u32>) -> Option<Node<'a>> {
|
||||
for child in node.children() {
|
||||
let child_range = child.byte_range();
|
||||
|
||||
if range.start >= child_range.start && range.end <= child_range.end {
|
||||
return Some(child);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RainbowQuery {
|
||||
query: Query,
|
||||
include_children_patterns: HashSet<Pattern>,
|
||||
scope_capture: Option<Capture>,
|
||||
bracket_capture: Option<Capture>,
|
||||
}
|
||||
|
||||
impl RainbowQuery {
|
||||
fn new(grammar: Grammar, source: &str) -> Result<Self, tree_sitter::query::ParseError> {
|
||||
let mut include_children_patterns = HashSet::default();
|
||||
|
||||
let query = Query::new(grammar, source, |pattern, predicate| match predicate {
|
||||
UserPredicate::SetProperty {
|
||||
key: "rainbow.include-children",
|
||||
val,
|
||||
} => {
|
||||
if val.is_some() {
|
||||
return Err(
|
||||
"property 'rainbow.include-children' does not take an argument".into(),
|
||||
);
|
||||
}
|
||||
include_children_patterns.insert(pattern);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(InvalidPredicateError::unknown(predicate)),
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
include_children_patterns,
|
||||
scope_capture: query.get_capture("rainbow.scope"),
|
||||
bracket_capture: query.get_capture("rainbow.bracket"),
|
||||
query,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use once_cell::sync::Lazy;
|
||||
|
@@ -7,6 +7,7 @@ use serde::{ser::SerializeSeq as _, Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::{self, Display},
|
||||
num::NonZeroU8,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
@@ -60,6 +61,8 @@ pub struct LanguageConfiguration {
|
||||
|
||||
/// If set, overrides `editor.path-completion`.
|
||||
pub path_completion: Option<bool>,
|
||||
/// If set, overrides `editor.word-completion`.
|
||||
pub word_completion: Option<WordCompletion>,
|
||||
|
||||
#[serde(default)]
|
||||
pub diagnostic_severity: Severity,
|
||||
@@ -98,6 +101,8 @@ pub struct LanguageConfiguration {
|
||||
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
|
||||
#[serde(default)]
|
||||
pub persistent_diagnostic_sources: Vec<String>,
|
||||
/// Overrides the `editor.rainbow-brackets` config key for the language.
|
||||
pub rainbow_brackets: Option<bool>,
|
||||
}
|
||||
|
||||
impl LanguageConfiguration {
|
||||
@@ -572,6 +577,13 @@ pub struct SoftWrap {
|
||||
pub wrap_at_text_width: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct WordCompletion {
|
||||
pub enable: Option<bool>,
|
||||
pub trigger_length: Option<NonZeroU8>,
|
||||
}
|
||||
|
||||
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<rope::Regex>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
|
@@ -19,6 +19,16 @@ pub enum Operation {
|
||||
Insert(Tendril),
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
/// The number of characters affected by the operation.
|
||||
pub fn len_chars(&self) -> usize {
|
||||
match self {
|
||||
Self::Retain(n) | Self::Delete(n) => *n,
|
||||
Self::Insert(s) => s.chars().count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Assoc {
|
||||
Before,
|
||||
|
@@ -13,7 +13,7 @@ homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
foldhash.workspace = true
|
||||
hashbrown = "0.15"
|
||||
hashbrown = "0.16"
|
||||
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
|
||||
|
@@ -213,6 +213,27 @@ fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
|
||||
Ok(grammars)
|
||||
}
|
||||
|
||||
pub fn get_grammar_names() -> Result<Option<HashSet<String>>> {
|
||||
let config: Configuration = crate::config::user_lang_config()
|
||||
.context("Could not parse languages.toml")?
|
||||
.try_into()?;
|
||||
|
||||
let grammars = match config.grammar_selection {
|
||||
Some(GrammarSelection::Only { only: selections }) => Some(selections),
|
||||
Some(GrammarSelection::Except { except: rejections }) => Some(
|
||||
config
|
||||
.grammar
|
||||
.into_iter()
|
||||
.map(|grammar| grammar.grammar_id)
|
||||
.filter(|id| !rejections.contains(id))
|
||||
.collect(),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(grammars)
|
||||
}
|
||||
|
||||
fn run_parallel<F, Res>(grammars: Vec<GrammarConfiguration>, job: F) -> Vec<(String, Result<Res>)>
|
||||
where
|
||||
F: Fn(GrammarConfiguration) -> Result<Res> + Send + 'static + Clone,
|
||||
|
@@ -244,7 +244,12 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
|
||||
/// Otherwise (workspace, false) is returned
|
||||
pub fn find_workspace() -> (PathBuf, bool) {
|
||||
let current_dir = current_working_dir();
|
||||
for ancestor in current_dir.ancestors() {
|
||||
find_workspace_in(current_dir)
|
||||
}
|
||||
|
||||
pub fn find_workspace_in(dir: impl AsRef<Path>) -> (PathBuf, bool) {
|
||||
let dir = dir.as_ref();
|
||||
for ancestor in dir.ancestors() {
|
||||
if ancestor.join(".git").exists()
|
||||
|| ancestor.join(".svn").exists()
|
||||
|| ancestor.join(".jj").exists()
|
||||
@@ -254,7 +259,7 @@ pub fn find_workspace() -> (PathBuf, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
(current_dir, true)
|
||||
(dir.to_owned(), true)
|
||||
}
|
||||
|
||||
fn default_config_file() -> PathBuf {
|
||||
|
@@ -23,7 +23,7 @@ license = "MIT"
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
serde_json = "1.0.143"
|
||||
url = {version = "2.5.4", features = ["serde"]}
|
||||
|
||||
[features]
|
||||
|
@@ -25,7 +25,7 @@ globset = "0.4.16"
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.46", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio = { version = "1.47", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream.workspace = true
|
||||
parking_lot.workspace = true
|
||||
arc-swap = "1"
|
||||
|
@@ -19,7 +19,7 @@ which = "8.0"
|
||||
regex-cursor = "0.1.5"
|
||||
bitflags.workspace = true
|
||||
once_cell = "1.21"
|
||||
regex-automata = "0.4.9"
|
||||
regex-automata = "0.4.10"
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
@@ -71,6 +71,16 @@ mod imp {
|
||||
perms.set_mode(new_perms);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::fs::{File, FileTimes};
|
||||
use std::os::macos::fs::FileTimesExt;
|
||||
|
||||
let to_file = File::options().write(true).open(to)?;
|
||||
let times = FileTimes::new().set_created(from_meta.created()?);
|
||||
to_file.set_times(times)?;
|
||||
}
|
||||
|
||||
std::fs::set_permissions(to, perms)?;
|
||||
|
||||
Ok(())
|
||||
@@ -109,7 +119,13 @@ mod imp {
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt, io::AsRawHandle};
|
||||
use std::os::windows::{
|
||||
ffi::OsStrExt,
|
||||
fs::{FileTimesExt, OpenOptionsExt},
|
||||
io::AsRawHandle,
|
||||
};
|
||||
|
||||
use std::fs::{File, FileTimes};
|
||||
|
||||
struct SecurityDescriptor {
|
||||
sd: PSECURITY_DESCRIPTOR,
|
||||
@@ -413,6 +429,10 @@ mod imp {
|
||||
let meta = std::fs::metadata(from)?;
|
||||
let perms = meta.permissions();
|
||||
|
||||
let to_file = File::options().write(true).open(to)?;
|
||||
let times = FileTimes::new().set_created(meta.created()?);
|
||||
to_file.set_times(times)?;
|
||||
|
||||
std::fs::set_permissions(to, perms)?;
|
||||
|
||||
Ok(())
|
||||
|
@@ -54,14 +54,14 @@ anyhow = "1"
|
||||
once_cell = "1.21"
|
||||
|
||||
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.28", features = ["event-stream"] }
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["termina"] }
|
||||
termina = { workspace = true, 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.7.1" }
|
||||
termini = "1"
|
||||
indexmap = "2.10"
|
||||
indexmap = "2.11"
|
||||
|
||||
# Logging
|
||||
fern = "0.7"
|
||||
@@ -91,12 +91,11 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
grep-regex = "0.1.13"
|
||||
grep-searcher = "0.1.14"
|
||||
|
||||
dashmap = "6.0"
|
||||
|
||||
[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.174"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
|
||||
libc = "0.2.175"
|
||||
|
||||
[build-dependencies]
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
@@ -30,28 +30,27 @@ use crate::{
|
||||
};
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
#[cfg(not(feature = "integration"))]
|
||||
use std::io::stdout;
|
||||
use std::{io::stdin, path::Path, sync::Arc};
|
||||
use std::{
|
||||
io::{stdin, IsTerminal},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
use anyhow::Context;
|
||||
use anyhow::Error;
|
||||
use anyhow::{Context, Error};
|
||||
|
||||
use crossterm::{event::Event as CrosstermEvent, tty::IsTty};
|
||||
#[cfg(not(windows))]
|
||||
use {signal_hook::consts::signal, signal_hook_tokio::Signals};
|
||||
#[cfg(windows)]
|
||||
type Signals = futures_util::stream::Empty<()>;
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::backend::TerminaBackend;
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
use tui::backend::TestBackend;
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
type TerminalBackend = CrosstermBackend<std::io::Stdout>;
|
||||
type TerminalBackend = TerminaBackend;
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
type TerminalBackend = TestBackend;
|
||||
@@ -104,7 +103,8 @@ impl Application {
|
||||
let theme_loader = theme::Loader::new(&theme_parent_dirs);
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
let backend = CrosstermBackend::new(stdout(), &config.editor);
|
||||
let backend = TerminaBackend::new((&config.editor).into())
|
||||
.context("failed to create terminal backend")?;
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
let backend = TestBackend::new(120, 150);
|
||||
@@ -123,7 +123,11 @@ impl Application {
|
||||
})),
|
||||
handlers,
|
||||
);
|
||||
Self::load_configured_theme(&mut editor, &config.load());
|
||||
Self::load_configured_theme(
|
||||
&mut editor,
|
||||
&config.load(),
|
||||
terminal.backend().supports_true_color(),
|
||||
);
|
||||
|
||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.keys
|
||||
@@ -214,7 +218,7 @@ impl Application {
|
||||
} else {
|
||||
editor.new_file(Action::VerticalSplit);
|
||||
}
|
||||
} else if stdin().is_tty() || cfg!(feature = "integration") {
|
||||
} else if stdin().is_terminal() || cfg!(feature = "integration") {
|
||||
editor.new_file(Action::VerticalSplit);
|
||||
} else {
|
||||
editor
|
||||
@@ -282,7 +286,7 @@ impl Application {
|
||||
|
||||
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
|
||||
where
|
||||
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
|
||||
S: Stream<Item = std::io::Result<termina::Event>> + Unpin,
|
||||
{
|
||||
self.render().await;
|
||||
|
||||
@@ -295,7 +299,7 @@ impl Application {
|
||||
|
||||
pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
|
||||
where
|
||||
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
|
||||
S: Stream<Item = std::io::Result<termina::Event>> + Unpin,
|
||||
{
|
||||
loop {
|
||||
if self.editor.should_close() {
|
||||
@@ -367,7 +371,7 @@ impl Application {
|
||||
ConfigEvent::Update(editor_config) => {
|
||||
let mut app_config = (*self.config.load().clone()).clone();
|
||||
app_config.editor = *editor_config;
|
||||
if let Err(err) = self.terminal.reconfigure(app_config.editor.clone().into()) {
|
||||
if let Err(err) = self.terminal.reconfigure((&app_config.editor).into()) {
|
||||
self.editor.set_error(err.to_string());
|
||||
};
|
||||
self.config.store(Arc::new(app_config));
|
||||
@@ -396,7 +400,11 @@ impl Application {
|
||||
// the sake of locals highlighting.
|
||||
let lang_loader = helix_core::config::user_lang_loader()?;
|
||||
self.editor.syn_loader.store(Arc::new(lang_loader));
|
||||
Self::load_configured_theme(&mut self.editor, &default_config);
|
||||
Self::load_configured_theme(
|
||||
&mut self.editor,
|
||||
&default_config,
|
||||
self.terminal.backend().supports_true_color(),
|
||||
);
|
||||
|
||||
// Re-parse any open documents with the new language config.
|
||||
let lang_loader = self.editor.syn_loader.load();
|
||||
@@ -412,8 +420,7 @@ impl Application {
|
||||
document.replace_diagnostics(diagnostics, &[], None);
|
||||
}
|
||||
|
||||
self.terminal
|
||||
.reconfigure(default_config.editor.clone().into())?;
|
||||
self.terminal.reconfigure((&default_config.editor).into())?;
|
||||
// Store new config
|
||||
self.config.store(Arc::new(default_config));
|
||||
Ok(())
|
||||
@@ -430,8 +437,8 @@ impl Application {
|
||||
}
|
||||
|
||||
/// Load the theme set in configuration
|
||||
fn load_configured_theme(editor: &mut Editor, config: &Config) {
|
||||
let true_color = config.editor.true_color || crate::true_color();
|
||||
fn load_configured_theme(editor: &mut Editor, config: &Config, terminal_true_color: bool) {
|
||||
let true_color = terminal_true_color || config.editor.true_color || crate::true_color();
|
||||
let theme = config
|
||||
.theme
|
||||
.as_ref()
|
||||
@@ -503,7 +510,7 @@ impl Application {
|
||||
// https://github.com/neovim/neovim/issues/12322
|
||||
// https://github.com/neovim/neovim/pull/13084
|
||||
for retries in 1..=10 {
|
||||
match self.claim_term().await {
|
||||
match self.terminal.claim() {
|
||||
Ok(()) => break,
|
||||
Err(err) if retries == 10 => panic!("Failed to claim terminal: {}", err),
|
||||
Err(_) => continue,
|
||||
@@ -573,24 +580,41 @@ impl Application {
|
||||
doc.set_last_saved_revision(doc_save_event.revision, doc_save_event.save_time);
|
||||
|
||||
let lines = doc_save_event.text.len_lines();
|
||||
let mut sz = doc_save_event.text.len_bytes() as f32;
|
||||
let size = doc_save_event.text.len_bytes();
|
||||
|
||||
const SUFFIX: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
|
||||
let mut i = 0;
|
||||
while i < SUFFIX.len() - 1 && sz >= 1024.0 {
|
||||
sz /= 1024.0;
|
||||
i += 1;
|
||||
enum Size {
|
||||
Bytes(u16),
|
||||
HumanReadable(f32, &'static str),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Size {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Bytes(bytes) => write!(f, "{bytes}B"),
|
||||
Self::HumanReadable(size, suffix) => write!(f, "{size:.1}{suffix}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let size = if size < 1024 {
|
||||
Size::Bytes(size as u16)
|
||||
} else {
|
||||
const SUFFIX: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
|
||||
let mut size = size as f32;
|
||||
let mut i = 0;
|
||||
while i < SUFFIX.len() - 1 && size >= 1024.0 {
|
||||
size /= 1024.0;
|
||||
i += 1;
|
||||
}
|
||||
Size::HumanReadable(size, SUFFIX[i])
|
||||
};
|
||||
|
||||
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 {:.1}{}",
|
||||
"'{}' written, {lines}L {size}",
|
||||
get_relative_path(&doc_save_event.path).to_string_lossy(),
|
||||
lines,
|
||||
sz,
|
||||
SUFFIX[i],
|
||||
));
|
||||
}
|
||||
|
||||
@@ -635,7 +659,7 @@ impl Application {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
|
||||
pub async fn handle_terminal_events(&mut self, event: std::io::Result<termina::Event>) {
|
||||
let mut cx = crate::compositor::Context {
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
@@ -643,9 +667,9 @@ impl Application {
|
||||
};
|
||||
// Handle key events
|
||||
let should_redraw = match event.unwrap() {
|
||||
CrosstermEvent::Resize(width, height) => {
|
||||
termina::Event::WindowResized(termina::WindowSize { rows, cols, .. }) => {
|
||||
self.terminal
|
||||
.resize(Rect::new(0, 0, width, height))
|
||||
.resize(Rect::new(0, 0, cols, rows))
|
||||
.expect("Unable to resize terminal");
|
||||
|
||||
let area = self.terminal.size().expect("couldn't get terminal size");
|
||||
@@ -653,11 +677,11 @@ impl Application {
|
||||
self.compositor.resize(area);
|
||||
|
||||
self.compositor
|
||||
.handle_event(&Event::Resize(width, height), &mut cx)
|
||||
.handle_event(&Event::Resize(cols, rows), &mut cx)
|
||||
}
|
||||
// Ignore keyboard release events.
|
||||
CrosstermEvent::Key(crossterm::event::KeyEvent {
|
||||
kind: crossterm::event::KeyEventKind::Release,
|
||||
termina::Event::Key(termina::event::KeyEvent {
|
||||
kind: termina::event::KeyEventKind::Release,
|
||||
..
|
||||
}) => false,
|
||||
event => self.compositor.handle_event(&event.into(), &mut cx),
|
||||
@@ -1099,36 +1123,48 @@ impl Application {
|
||||
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)
|
||||
}
|
||||
|
||||
fn restore_term(&mut self) -> std::io::Result<()> {
|
||||
let terminal_config = self.config.load().editor.clone().into();
|
||||
use helix_view::graphics::CursorKind;
|
||||
self.terminal
|
||||
.backend_mut()
|
||||
.show_cursor(CursorKind::Block)
|
||||
.ok();
|
||||
self.terminal.restore(terminal_config)
|
||||
self.terminal.restore()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin {
|
||||
use termina::Terminal as _;
|
||||
let reader = self.terminal.backend().terminal().event_reader();
|
||||
termina::EventStream::new(reader, |event| !event.is_escape())
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin {
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/// A dummy stream that never polls as ready.
|
||||
pub struct DummyEventStream;
|
||||
|
||||
impl Stream for DummyEventStream {
|
||||
type Item = std::io::Result<termina::Event>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
DummyEventStream
|
||||
}
|
||||
|
||||
pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
|
||||
where
|
||||
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
|
||||
S: Stream<Item = std::io::Result<termina::Event>> + Unpin,
|
||||
{
|
||||
self.claim_term().await?;
|
||||
|
||||
// Exit the alternate screen and disable raw mode before panicking
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
// We can't handle errors properly inside this closure. And it's
|
||||
// probably not a good idea to `unwrap()` inside a panic handler.
|
||||
// So we just ignore the `Result`.
|
||||
let _ = TerminalBackend::force_restore();
|
||||
hook(info);
|
||||
}));
|
||||
self.terminal.claim()?;
|
||||
|
||||
self.event_loop(input_stream).await;
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
pub(crate) mod dap;
|
||||
pub(crate) mod lsp;
|
||||
pub(crate) mod syntax;
|
||||
pub(crate) mod typed;
|
||||
|
||||
pub use dap::*;
|
||||
@@ -11,6 +12,7 @@ use helix_stdx::{
|
||||
};
|
||||
use helix_vcs::{FileChange, Hunk};
|
||||
pub use lsp::*;
|
||||
pub use syntax::*;
|
||||
use tui::{
|
||||
text::{Span, Spans},
|
||||
widgets::Cell,
|
||||
@@ -20,7 +22,8 @@ pub use typed::*;
|
||||
use helix_core::{
|
||||
char_idx_at_visual_offset,
|
||||
chars::char_is_word,
|
||||
command_line, comment,
|
||||
command_line::{self, Args},
|
||||
comment,
|
||||
doc_formatter::TextFormat,
|
||||
encoding, find_workspace,
|
||||
graphemes::{self, next_grapheme_boundary},
|
||||
@@ -44,6 +47,7 @@ use helix_core::{
|
||||
use helix_view::{
|
||||
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
|
||||
editor::Action,
|
||||
expansion,
|
||||
info::Info,
|
||||
input::KeyEvent,
|
||||
keyboard::KeyCode,
|
||||
@@ -405,9 +409,13 @@ impl MappableCommand {
|
||||
buffer_picker, "Open buffer picker",
|
||||
jumplist_picker, "Open jumplist picker",
|
||||
symbol_picker, "Open symbol picker",
|
||||
syntax_symbol_picker, "Open symbol picker from syntax information",
|
||||
lsp_or_syntax_symbol_picker, "Open symbol picker from LSP or syntax information",
|
||||
changed_file_picker, "Open changed file picker",
|
||||
select_references_to_symbol_under_cursor, "Select symbol references",
|
||||
workspace_symbol_picker, "Open workspace symbol picker",
|
||||
syntax_workspace_symbol_picker, "Open workspace symbol picker from syntax information",
|
||||
lsp_or_syntax_workspace_symbol_picker, "Open workspace symbol picker from LSP or syntax information",
|
||||
diagnostics_picker, "Open diagnostic picker",
|
||||
workspace_diagnostics_picker, "Open workspace diagnostic picker",
|
||||
last_picker, "Open last picker",
|
||||
@@ -466,6 +474,8 @@ impl MappableCommand {
|
||||
smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
|
||||
insert_tab, "Insert tab char",
|
||||
insert_newline, "Insert newline char",
|
||||
insert_char_interactive, "Insert an interactively-chosen char",
|
||||
append_char_interactive, "Append an interactively-chosen char",
|
||||
delete_char_backward, "Delete previous char",
|
||||
delete_char_forward, "Delete next char",
|
||||
delete_word_backward, "Delete previous word",
|
||||
@@ -565,6 +575,8 @@ impl MappableCommand {
|
||||
goto_prev_comment, "Goto previous comment",
|
||||
goto_next_test, "Goto next test",
|
||||
goto_prev_test, "Goto previous test",
|
||||
goto_next_xml_element, "Goto next (X)HTML element",
|
||||
goto_prev_xml_element, "Goto previous (X)HTML element",
|
||||
goto_next_entry, "Goto next pairing",
|
||||
goto_prev_entry, "Goto previous pairing",
|
||||
goto_next_paragraph, "Goto next paragraph",
|
||||
@@ -3189,9 +3201,11 @@ fn buffer_picker(cx: &mut Context) {
|
||||
.into()
|
||||
}),
|
||||
];
|
||||
let initial_cursor = if items.len() <= 1 { 0 } else { 1 };
|
||||
let picker = Picker::new(columns, 2, items, (), |cx, meta, action| {
|
||||
cx.editor.switch(meta.id, action);
|
||||
})
|
||||
.with_initial_cursor(initial_cursor)
|
||||
.with_preview(|editor, meta| {
|
||||
let doc = &editor.documents.get(&meta.id)?;
|
||||
let lines = doc.selections().values().next().map(|selection| {
|
||||
@@ -4096,7 +4110,7 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
|
||||
}
|
||||
|
||||
pub mod insert {
|
||||
use crate::events::PostInsertChar;
|
||||
use crate::{events::PostInsertChar, key};
|
||||
|
||||
use super::*;
|
||||
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
||||
@@ -4175,11 +4189,15 @@ pub mod insert {
|
||||
}
|
||||
|
||||
pub fn insert_tab(cx: &mut Context) {
|
||||
insert_tab_impl(cx, 1)
|
||||
}
|
||||
|
||||
fn insert_tab_impl(cx: &mut Context, count: usize) {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
// TODO: round out to nearest indentation level (for example a line with 3 spaces should
|
||||
// indent by one to reach 4 spaces).
|
||||
|
||||
let indent = Tendril::from(doc.indent_style.as_str());
|
||||
let indent = Tendril::from(doc.indent_style.as_str().repeat(count));
|
||||
let transaction = Transaction::insert(
|
||||
doc.text(),
|
||||
&doc.selection(view.id).clone().cursors(doc.text().slice(..)),
|
||||
@@ -4188,6 +4206,49 @@ pub mod insert {
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
|
||||
pub fn append_char_interactive(cx: &mut Context) {
|
||||
// Save the current mode, so we can restore it later.
|
||||
let mode = cx.editor.mode;
|
||||
append_mode(cx);
|
||||
insert_selection_interactive(cx, mode);
|
||||
}
|
||||
|
||||
pub fn insert_char_interactive(cx: &mut Context) {
|
||||
let mode = cx.editor.mode;
|
||||
insert_mode(cx);
|
||||
insert_selection_interactive(cx, mode);
|
||||
}
|
||||
|
||||
fn insert_selection_interactive(cx: &mut Context, old_mode: Mode) {
|
||||
let count = cx.count();
|
||||
|
||||
// need to wait for next key
|
||||
cx.on_next_key(move |cx, event| {
|
||||
match event {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char(ch),
|
||||
..
|
||||
} => {
|
||||
for _ in 0..count {
|
||||
insert::insert_char(cx, ch)
|
||||
}
|
||||
}
|
||||
key!(Enter) => {
|
||||
if count != 1 {
|
||||
cx.editor
|
||||
.set_error("inserting multiple newlines not yet supported");
|
||||
return;
|
||||
}
|
||||
insert_newline(cx)
|
||||
}
|
||||
key!(Tab) => insert_tab_impl(cx, count),
|
||||
_ => (),
|
||||
};
|
||||
// Restore the old mode.
|
||||
cx.editor.mode = old_mode;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_newline(cx: &mut Context) {
|
||||
let config = cx.editor.config();
|
||||
let (view, doc) = current_ref!(cx.editor);
|
||||
@@ -5311,6 +5372,7 @@ fn rotate_selections_last(cx: &mut Context) {
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ReorderStrategy {
|
||||
RotateForward,
|
||||
RotateBackward,
|
||||
@@ -5323,34 +5385,50 @@ fn reorder_selection_contents(cx: &mut Context, strategy: ReorderStrategy) {
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
let selection = doc.selection(view.id);
|
||||
let mut fragments: Vec<_> = selection
|
||||
|
||||
let mut ranges: Vec<_> = selection
|
||||
.slices(text)
|
||||
.map(|fragment| fragment.chunks().collect())
|
||||
.collect();
|
||||
|
||||
let group = count
|
||||
.map(|count| count.get())
|
||||
.unwrap_or(fragments.len()) // default to rotating everything as one group
|
||||
.min(fragments.len());
|
||||
let rotate_by = count.map_or(1, |count| count.get().min(ranges.len()));
|
||||
|
||||
for chunk in fragments.chunks_mut(group) {
|
||||
// TODO: also modify main index
|
||||
match strategy {
|
||||
ReorderStrategy::RotateForward => chunk.rotate_right(1),
|
||||
ReorderStrategy::RotateBackward => chunk.rotate_left(1),
|
||||
ReorderStrategy::Reverse => chunk.reverse(),
|
||||
};
|
||||
}
|
||||
let primary_index = match strategy {
|
||||
ReorderStrategy::RotateForward => {
|
||||
ranges.rotate_right(rotate_by);
|
||||
// Like `usize::wrapping_add`, but provide a custom range from `0` to `ranges.len()`
|
||||
(selection.primary_index() + ranges.len() + rotate_by) % ranges.len()
|
||||
}
|
||||
ReorderStrategy::RotateBackward => {
|
||||
ranges.rotate_left(rotate_by);
|
||||
// Like `usize::wrapping_sub`, but provide a custom range from `0` to `ranges.len()`
|
||||
(selection.primary_index() + ranges.len() - rotate_by) % ranges.len()
|
||||
}
|
||||
ReorderStrategy::Reverse => {
|
||||
if rotate_by % 2 == 0 {
|
||||
// nothing changed, if we reverse something an even
|
||||
// amount of times, the output will be the same
|
||||
return;
|
||||
}
|
||||
ranges.reverse();
|
||||
// -1 to turn 1-based len into 0-based index
|
||||
(ranges.len() - 1) - selection.primary_index()
|
||||
}
|
||||
};
|
||||
|
||||
let transaction = Transaction::change(
|
||||
doc.text(),
|
||||
selection
|
||||
.ranges()
|
||||
.iter()
|
||||
.zip(fragments)
|
||||
.zip(ranges)
|
||||
.map(|(range, fragment)| (range.from(), range.to(), Some(fragment))),
|
||||
);
|
||||
|
||||
doc.set_selection(
|
||||
view.id,
|
||||
Selection::new(selection.ranges().into(), primary_index),
|
||||
);
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
|
||||
@@ -5882,6 +5960,14 @@ fn goto_prev_test(cx: &mut Context) {
|
||||
goto_ts_object_impl(cx, "test", Direction::Backward)
|
||||
}
|
||||
|
||||
fn goto_next_xml_element(cx: &mut Context) {
|
||||
goto_ts_object_impl(cx, "xml-element", Direction::Forward)
|
||||
}
|
||||
|
||||
fn goto_prev_xml_element(cx: &mut Context) {
|
||||
goto_ts_object_impl(cx, "xml-element", Direction::Backward)
|
||||
}
|
||||
|
||||
fn goto_next_entry(cx: &mut Context) {
|
||||
goto_ts_object_impl(cx, "entry", Direction::Forward)
|
||||
}
|
||||
@@ -5949,6 +6035,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
|
||||
'c' => textobject_treesitter("comment", range),
|
||||
'T' => textobject_treesitter("test", range),
|
||||
'e' => textobject_treesitter("entry", range),
|
||||
'x' => textobject_treesitter("xml-element", range),
|
||||
'p' => textobject::textobject_paragraph(text, range, objtype, count),
|
||||
'm' => textobject::textobject_pair_surround_closest(
|
||||
doc.syntax(),
|
||||
@@ -5993,6 +6080,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
|
||||
("e", "Data structure entry (tree-sitter)"),
|
||||
("m", "Closest surrounding pair (tree-sitter)"),
|
||||
("g", "Change"),
|
||||
("x", "(X)HTML element (tree-sitter)"),
|
||||
(" ", "... or any character acting as a pair"),
|
||||
];
|
||||
|
||||
@@ -6170,64 +6258,52 @@ enum ShellBehavior {
|
||||
}
|
||||
|
||||
fn shell_pipe(cx: &mut Context) {
|
||||
shell_prompt(cx, "pipe:".into(), ShellBehavior::Replace);
|
||||
shell_prompt_for_behavior(cx, "pipe:".into(), ShellBehavior::Replace);
|
||||
}
|
||||
|
||||
fn shell_pipe_to(cx: &mut Context) {
|
||||
shell_prompt(cx, "pipe-to:".into(), ShellBehavior::Ignore);
|
||||
shell_prompt_for_behavior(cx, "pipe-to:".into(), ShellBehavior::Ignore);
|
||||
}
|
||||
|
||||
fn shell_insert_output(cx: &mut Context) {
|
||||
shell_prompt(cx, "insert-output:".into(), ShellBehavior::Insert);
|
||||
shell_prompt_for_behavior(cx, "insert-output:".into(), ShellBehavior::Insert);
|
||||
}
|
||||
|
||||
fn shell_append_output(cx: &mut Context) {
|
||||
shell_prompt(cx, "append-output:".into(), ShellBehavior::Append);
|
||||
shell_prompt_for_behavior(cx, "append-output:".into(), ShellBehavior::Append);
|
||||
}
|
||||
|
||||
fn shell_keep_pipe(cx: &mut Context) {
|
||||
ui::prompt(
|
||||
cx,
|
||||
"keep-pipe:".into(),
|
||||
Some('|'),
|
||||
ui::completers::none,
|
||||
move |cx, input: &str, event: PromptEvent| {
|
||||
let shell = &cx.editor.config().shell;
|
||||
if event != PromptEvent::Validate {
|
||||
return;
|
||||
}
|
||||
if input.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let selection = doc.selection(view.id);
|
||||
shell_prompt(cx, "keep-pipe:".into(), |cx, args| {
|
||||
let shell = &cx.editor.config().shell;
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let selection = doc.selection(view.id);
|
||||
|
||||
let mut ranges = SmallVec::with_capacity(selection.len());
|
||||
let old_index = selection.primary_index();
|
||||
let mut index: Option<usize> = None;
|
||||
let text = doc.text().slice(..);
|
||||
let mut ranges = SmallVec::with_capacity(selection.len());
|
||||
let old_index = selection.primary_index();
|
||||
let mut index: Option<usize> = None;
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
for (i, range) in selection.ranges().iter().enumerate() {
|
||||
let fragment = range.slice(text);
|
||||
if let Err(err) = shell_impl(shell, input, Some(fragment.into())) {
|
||||
log::debug!("Shell command failed: {}", err);
|
||||
} else {
|
||||
ranges.push(*range);
|
||||
if i >= old_index && index.is_none() {
|
||||
index = Some(ranges.len() - 1);
|
||||
}
|
||||
for (i, range) in selection.ranges().iter().enumerate() {
|
||||
let fragment = range.slice(text);
|
||||
if let Err(err) = shell_impl(shell, args.join(" ").as_str(), Some(fragment.into())) {
|
||||
log::debug!("Shell command failed: {}", err);
|
||||
} else {
|
||||
ranges.push(*range);
|
||||
if i >= old_index && index.is_none() {
|
||||
index = Some(ranges.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ranges.is_empty() {
|
||||
cx.editor.set_error("No selections remaining");
|
||||
return;
|
||||
}
|
||||
if ranges.is_empty() {
|
||||
cx.editor.set_error("No selections remaining");
|
||||
return;
|
||||
}
|
||||
|
||||
let index = index.unwrap_or_else(|| ranges.len() - 1);
|
||||
doc.set_selection(view.id, Selection::new(ranges, index));
|
||||
},
|
||||
);
|
||||
let index = index.unwrap_or_else(|| ranges.len() - 1);
|
||||
doc.set_selection(view.id, Selection::new(ranges, index));
|
||||
});
|
||||
}
|
||||
|
||||
fn shell_impl(shell: &[String], cmd: &str, input: Option<Rope>) -> anyhow::Result<Tendril> {
|
||||
@@ -6382,25 +6458,35 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
|
||||
view.ensure_cursor_in_view(doc, config.scrolloff);
|
||||
}
|
||||
|
||||
fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
|
||||
fn shell_prompt<F>(cx: &mut Context, prompt: Cow<'static, str>, mut callback_fn: F)
|
||||
where
|
||||
F: FnMut(&mut compositor::Context, Args) + 'static,
|
||||
{
|
||||
ui::prompt(
|
||||
cx,
|
||||
prompt,
|
||||
Some('|'),
|
||||
ui::completers::shell,
|
||||
move |cx, input: &str, event: PromptEvent| {
|
||||
if event != PromptEvent::Validate {
|
||||
|editor, input| complete_command_args(editor, SHELL_SIGNATURE, &SHELL_COMPLETER, input, 0),
|
||||
move |cx, input, event| {
|
||||
if event != PromptEvent::Validate || input.is_empty() {
|
||||
return;
|
||||
}
|
||||
if input.is_empty() {
|
||||
return;
|
||||
match Args::parse(input, SHELL_SIGNATURE, true, |token| {
|
||||
expansion::expand(cx.editor, token).map_err(|err| err.into())
|
||||
}) {
|
||||
Ok(args) => callback_fn(cx, args),
|
||||
Err(err) => cx.editor.set_error(err.to_string()),
|
||||
}
|
||||
|
||||
shell(cx, input, &behavior);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn shell_prompt_for_behavior(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
|
||||
shell_prompt(cx, prompt, move |cx, args| {
|
||||
shell(cx, args.join(" ").as_str(), &behavior)
|
||||
})
|
||||
}
|
||||
|
||||
fn suspend(_cx: &mut Context) {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
@@ -6823,3 +6909,34 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) {
|
||||
}
|
||||
jump_to_label(cx, words, behaviour)
|
||||
}
|
||||
|
||||
fn lsp_or_syntax_symbol_picker(cx: &mut Context) {
|
||||
let doc = doc!(cx.editor);
|
||||
|
||||
if doc
|
||||
.language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
lsp::symbol_picker(cx);
|
||||
} else if doc.syntax().is_some() {
|
||||
syntax_symbol_picker(cx);
|
||||
} else {
|
||||
cx.editor
|
||||
.set_error("No language server supporting document symbols or syntax info available");
|
||||
}
|
||||
}
|
||||
|
||||
fn lsp_or_syntax_workspace_symbol_picker(cx: &mut Context) {
|
||||
let doc = doc!(cx.editor);
|
||||
|
||||
if doc
|
||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
lsp::workspace_symbol_picker(cx);
|
||||
} else {
|
||||
syntax_workspace_symbol_picker(cx);
|
||||
}
|
||||
}
|
||||
|
@@ -231,6 +231,13 @@ fn diag_picker(
|
||||
}
|
||||
}
|
||||
|
||||
flat_diag.sort_by(|a, b| {
|
||||
a.diag
|
||||
.severity
|
||||
.unwrap_or(lsp::DiagnosticSeverity::HINT)
|
||||
.cmp(&b.diag.severity.unwrap_or(lsp::DiagnosticSeverity::HINT))
|
||||
});
|
||||
|
||||
let styles = DiagnosticStyles {
|
||||
hint: cx.editor.theme.get("hint"),
|
||||
info: cx.editor.theme.get("info"),
|
||||
@@ -928,7 +935,13 @@ where
|
||||
}
|
||||
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
if locations.is_empty() {
|
||||
editor.set_error("No definition found.");
|
||||
editor.set_error(match feature {
|
||||
LanguageServerFeature::GotoDeclaration => "No declaration found.",
|
||||
LanguageServerFeature::GotoDefinition => "No definition found.",
|
||||
LanguageServerFeature::GotoTypeDefinition => "No type definition found.",
|
||||
LanguageServerFeature::GotoImplementation => "No implementation found.",
|
||||
_ => "No location found.",
|
||||
});
|
||||
} else {
|
||||
goto_impl(editor, compositor, locations);
|
||||
}
|
||||
|
446
helix-term/src/commands/syntax.rs
Normal file
446
helix-term/src/commands/syntax.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
iter,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures_util::FutureExt;
|
||||
use grep_regex::RegexMatcherBuilder;
|
||||
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
|
||||
use helix_core::{
|
||||
syntax::{Loader, QueryIterEvent},
|
||||
Rope, RopeSlice, Selection, Syntax, Uri,
|
||||
};
|
||||
use helix_stdx::{
|
||||
path,
|
||||
rope::{self, RopeSliceExt},
|
||||
};
|
||||
use helix_view::{
|
||||
align_view,
|
||||
document::{from_reader, SCRATCH_BUFFER_NAME},
|
||||
Align, Document, DocumentId, Editor,
|
||||
};
|
||||
use ignore::{DirEntry, WalkBuilder, WalkState};
|
||||
|
||||
use crate::{
|
||||
filter_picker_entry,
|
||||
ui::{
|
||||
overlay::overlaid,
|
||||
picker::{Injector, PathOrId},
|
||||
Picker, PickerColumn,
|
||||
},
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum TagKind {
|
||||
Class,
|
||||
Constant,
|
||||
Function,
|
||||
Interface,
|
||||
Macro,
|
||||
Module,
|
||||
Struct,
|
||||
Type,
|
||||
}
|
||||
|
||||
impl TagKind {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Class => "class",
|
||||
Self::Constant => "constant",
|
||||
Self::Function => "function",
|
||||
Self::Interface => "interface",
|
||||
Self::Macro => "macro",
|
||||
Self::Module => "module",
|
||||
Self::Struct => "struct",
|
||||
Self::Type => "type",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"class" => Some(TagKind::Class),
|
||||
"constant" => Some(TagKind::Constant),
|
||||
"function" => Some(TagKind::Function),
|
||||
"interface" => Some(TagKind::Interface),
|
||||
"macro" => Some(TagKind::Macro),
|
||||
"module" => Some(TagKind::Module),
|
||||
"struct" => Some(TagKind::Struct),
|
||||
"type" => Some(TagKind::Type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Uri is cheap to clone and DocumentId is Copy
|
||||
#[derive(Debug, Clone)]
|
||||
enum UriOrDocumentId {
|
||||
Uri(Uri),
|
||||
Id(DocumentId),
|
||||
}
|
||||
|
||||
impl UriOrDocumentId {
|
||||
fn path_or_id(&self) -> Option<PathOrId<'_>> {
|
||||
match self {
|
||||
Self::Id(id) => Some(PathOrId::Id(*id)),
|
||||
Self::Uri(uri) => uri.as_path().map(PathOrId::Path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Tag {
|
||||
kind: TagKind,
|
||||
name: String,
|
||||
start: usize,
|
||||
end: usize,
|
||||
start_line: usize,
|
||||
end_line: usize,
|
||||
doc: UriOrDocumentId,
|
||||
}
|
||||
|
||||
fn tags_iter<'a>(
|
||||
syntax: &'a Syntax,
|
||||
loader: &'a Loader,
|
||||
text: RopeSlice<'a>,
|
||||
doc: UriOrDocumentId,
|
||||
pattern: Option<&'a rope::Regex>,
|
||||
) -> impl Iterator<Item = Tag> + 'a {
|
||||
let mut tags_iter = syntax.tags(text, loader, ..);
|
||||
|
||||
iter::from_fn(move || loop {
|
||||
let QueryIterEvent::Match(mat) = tags_iter.next()? else {
|
||||
continue;
|
||||
};
|
||||
let query = &loader
|
||||
.tag_query(tags_iter.current_language())
|
||||
.expect("must have a tags query to emit matches")
|
||||
.query;
|
||||
let Some(kind) = query
|
||||
.capture_name(mat.capture)
|
||||
.strip_prefix("definition.")
|
||||
.and_then(TagKind::from_name)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let range = mat.node.byte_range();
|
||||
if pattern.is_some_and(|pattern| {
|
||||
!pattern.is_match(text.regex_input_at_bytes(range.start as usize..range.end as usize))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let start = text.byte_to_char(range.start as usize);
|
||||
let end = text.byte_to_char(range.end as usize);
|
||||
return Some(Tag {
|
||||
kind,
|
||||
name: text.slice(start..end).to_string(),
|
||||
start,
|
||||
end,
|
||||
start_line: text.char_to_line(start),
|
||||
end_line: text.char_to_line(end),
|
||||
doc: doc.clone(),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn syntax_symbol_picker(cx: &mut Context) {
|
||||
let doc = doc!(cx.editor);
|
||||
let Some(syntax) = doc.syntax() else {
|
||||
cx.editor
|
||||
.set_error("Syntax tree is not available on this buffer");
|
||||
return;
|
||||
};
|
||||
let doc_id = doc.id();
|
||||
let text = doc.text().slice(..);
|
||||
let loader = cx.editor.syn_loader.load();
|
||||
let tags = tags_iter(syntax, &loader, text, UriOrDocumentId::Id(doc.id()), None);
|
||||
|
||||
let columns = vec![
|
||||
PickerColumn::new("kind", |tag: &Tag, _| tag.kind.as_str().into()),
|
||||
PickerColumn::new("name", |tag: &Tag, _| tag.name.as_str().into()),
|
||||
];
|
||||
|
||||
let picker = Picker::new(
|
||||
columns,
|
||||
1, // name
|
||||
tags,
|
||||
(),
|
||||
move |cx, tag, action| {
|
||||
cx.editor.switch(doc_id, action);
|
||||
let view = view_mut!(cx.editor);
|
||||
let doc = doc_mut!(cx.editor, &doc_id);
|
||||
doc.set_selection(view.id, Selection::single(tag.start, tag.end));
|
||||
if action.align_view(view, doc.id()) {
|
||||
align_view(doc, view, Align::Center)
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_preview(|_editor, tag| {
|
||||
Some((tag.doc.path_or_id()?, Some((tag.start_line, tag.end_line))))
|
||||
})
|
||||
.truncate_start(false);
|
||||
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
pub fn syntax_workspace_symbol_picker(cx: &mut Context) {
|
||||
#[derive(Debug)]
|
||||
struct SearchState {
|
||||
searcher_builder: SearcherBuilder,
|
||||
walk_builder: WalkBuilder,
|
||||
regex_matcher_builder: RegexMatcherBuilder,
|
||||
rope_regex_builder: rope::RegexBuilder,
|
||||
search_root: PathBuf,
|
||||
/// A cache of files that have been parsed in prior searches.
|
||||
syntax_cache: DashMap<PathBuf, Option<(Rope, Syntax)>>,
|
||||
}
|
||||
|
||||
let mut searcher_builder = SearcherBuilder::new();
|
||||
searcher_builder.binary_detection(BinaryDetection::quit(b'\x00'));
|
||||
|
||||
// Search from the workspace that the currently focused document is within. This behaves like global
|
||||
// search most of the time but helps when you have two projects open in splits.
|
||||
let search_root = if let Some(path) = doc!(cx.editor).path() {
|
||||
helix_loader::find_workspace_in(path).0
|
||||
} else {
|
||||
helix_loader::find_workspace().0
|
||||
};
|
||||
|
||||
let absolute_root = search_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| search_root.clone());
|
||||
|
||||
let config = cx.editor.config();
|
||||
let dedup_symlinks = config.file_picker.deduplicate_links;
|
||||
|
||||
let mut walk_builder = WalkBuilder::new(&search_root);
|
||||
walk_builder
|
||||
.hidden(config.file_picker.hidden)
|
||||
.parents(config.file_picker.parents)
|
||||
.ignore(config.file_picker.ignore)
|
||||
.follow_links(config.file_picker.follow_symlinks)
|
||||
.git_ignore(config.file_picker.git_ignore)
|
||||
.git_global(config.file_picker.git_global)
|
||||
.git_exclude(config.file_picker.git_exclude)
|
||||
.max_depth(config.file_picker.max_depth)
|
||||
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks))
|
||||
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"))
|
||||
.add_custom_ignore_filename(".helix/ignore");
|
||||
|
||||
let mut regex_matcher_builder = RegexMatcherBuilder::new();
|
||||
regex_matcher_builder.case_smart(config.search.smart_case);
|
||||
let mut rope_regex_builder = rope::RegexBuilder::new();
|
||||
rope_regex_builder.syntax(rope::Config::new().case_insensitive(config.search.smart_case));
|
||||
let state = SearchState {
|
||||
searcher_builder,
|
||||
walk_builder,
|
||||
regex_matcher_builder,
|
||||
rope_regex_builder,
|
||||
search_root,
|
||||
syntax_cache: DashMap::default(),
|
||||
};
|
||||
let reg = cx.register.unwrap_or('/');
|
||||
cx.editor.registers.last_search_register = reg;
|
||||
let columns = vec![
|
||||
PickerColumn::new("kind", |tag: &Tag, _| tag.kind.as_str().into()),
|
||||
PickerColumn::new("name", |tag: &Tag, _| tag.name.as_str().into()).without_filtering(),
|
||||
PickerColumn::new("path", |tag: &Tag, state: &SearchState| {
|
||||
match &tag.doc {
|
||||
UriOrDocumentId::Uri(uri) => {
|
||||
if let Some(path) = uri.as_path() {
|
||||
let path = if let Ok(stripped) = path.strip_prefix(&state.search_root) {
|
||||
stripped
|
||||
} else {
|
||||
path
|
||||
};
|
||||
path.to_string_lossy().into()
|
||||
} else {
|
||||
uri.to_string().into()
|
||||
}
|
||||
}
|
||||
// This picker only uses `Id` for scratch buffers for better display.
|
||||
UriOrDocumentId::Id(_) => SCRATCH_BUFFER_NAME.into(),
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
let get_tags = |query: &str,
|
||||
editor: &mut Editor,
|
||||
state: Arc<SearchState>,
|
||||
injector: &Injector<_, _>| {
|
||||
if query.len() < 3 {
|
||||
return async { Ok(()) }.boxed();
|
||||
}
|
||||
// Attempt to find the tag in any open documents.
|
||||
let pattern = match state.rope_regex_builder.build(query) {
|
||||
Ok(pattern) => pattern,
|
||||
Err(err) => return async { Err(anyhow::anyhow!(err)) }.boxed(),
|
||||
};
|
||||
let loader = editor.syn_loader.load();
|
||||
for doc in editor.documents() {
|
||||
let Some(syntax) = doc.syntax() else { continue };
|
||||
let text = doc.text().slice(..);
|
||||
let uri_or_id = doc
|
||||
.uri()
|
||||
.map(UriOrDocumentId::Uri)
|
||||
.unwrap_or_else(|| UriOrDocumentId::Id(doc.id()));
|
||||
for tag in tags_iter(syntax, &loader, text.slice(..), uri_or_id, Some(&pattern)) {
|
||||
if injector.push(tag).is_err() {
|
||||
return async { Ok(()) }.boxed();
|
||||
}
|
||||
}
|
||||
}
|
||||
if !state.search_root.exists() {
|
||||
return async { Err(anyhow::anyhow!("Current working directory does not exist")) }
|
||||
.boxed();
|
||||
}
|
||||
let matcher = match state.regex_matcher_builder.build(query) {
|
||||
Ok(matcher) => {
|
||||
// Clear any "Failed to compile regex" errors out of the statusline.
|
||||
editor.clear_status();
|
||||
matcher
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!(
|
||||
"Failed to compile search pattern in workspace symbol search: {}",
|
||||
err
|
||||
);
|
||||
return async { Err(anyhow::anyhow!("Failed to compile regex")) }.boxed();
|
||||
}
|
||||
};
|
||||
let pattern = Arc::new(pattern);
|
||||
let injector = injector.clone();
|
||||
let loader = editor.syn_loader.load();
|
||||
let documents: HashSet<_> = editor
|
||||
.documents()
|
||||
.filter_map(Document::path)
|
||||
.cloned()
|
||||
.collect();
|
||||
async move {
|
||||
let searcher = state.searcher_builder.build();
|
||||
state.walk_builder.build_parallel().run(|| {
|
||||
let mut searcher = searcher.clone();
|
||||
let matcher = matcher.clone();
|
||||
let injector = injector.clone();
|
||||
let loader = loader.clone();
|
||||
let documents = &documents;
|
||||
let pattern = pattern.clone();
|
||||
let syntax_cache = &state.syntax_cache;
|
||||
Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return WalkState::Continue,
|
||||
};
|
||||
match entry.file_type() {
|
||||
Some(entry) if entry.is_file() => {}
|
||||
// skip everything else
|
||||
_ => return WalkState::Continue,
|
||||
};
|
||||
let path = entry.path();
|
||||
// If this document is open, skip it because we've already processed it above.
|
||||
if documents.contains(path) {
|
||||
return WalkState::Continue;
|
||||
};
|
||||
let mut quit = false;
|
||||
let sink = sinks::UTF8(|_line, _content| {
|
||||
if !syntax_cache.contains_key(path) {
|
||||
// Read the file into a Rope and attempt to recognize the language
|
||||
// and parse it with tree-sitter. Save the Rope and Syntax for future
|
||||
// queries.
|
||||
syntax_cache.insert(path.to_path_buf(), syntax_for_path(path, &loader));
|
||||
};
|
||||
let entry = syntax_cache.get(path).unwrap();
|
||||
let Some((text, syntax)) = entry.value() else {
|
||||
// If the file couldn't be parsed, move on.
|
||||
return Ok(false);
|
||||
};
|
||||
let uri = Uri::from(path::normalize(path));
|
||||
for tag in tags_iter(
|
||||
syntax,
|
||||
&loader,
|
||||
text.slice(..),
|
||||
UriOrDocumentId::Uri(uri),
|
||||
Some(&pattern),
|
||||
) {
|
||||
if injector.push(tag).is_err() {
|
||||
quit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Quit after seeing the first regex match. We only care to find files
|
||||
// that contain the pattern and then we run the tags query within
|
||||
// those. The location and contents of a match are irrelevant - it's
|
||||
// only important _if_ a file matches.
|
||||
Ok(false)
|
||||
});
|
||||
if let Err(err) = searcher.search_path(&matcher, path, sink) {
|
||||
log::info!("Workspace syntax search error: {}, {}", path.display(), err);
|
||||
}
|
||||
if quit {
|
||||
WalkState::Quit
|
||||
} else {
|
||||
WalkState::Continue
|
||||
}
|
||||
})
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
};
|
||||
let picker = Picker::new(
|
||||
columns,
|
||||
1, // name
|
||||
[],
|
||||
state,
|
||||
move |cx, tag, action| {
|
||||
let doc_id = match &tag.doc {
|
||||
UriOrDocumentId::Id(id) => *id,
|
||||
UriOrDocumentId::Uri(uri) => match cx.editor.open(uri.as_path().expect(""), action) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
cx.editor
|
||||
.set_error(format!("Failed to open file '{uri:?}': {e}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
let doc = doc_mut!(cx.editor, &doc_id);
|
||||
let view = view_mut!(cx.editor);
|
||||
let len_chars = doc.text().len_chars();
|
||||
if tag.start >= len_chars || tag.end > len_chars {
|
||||
cx.editor.set_error("The location you jumped to does not exist anymore because the file has changed.");
|
||||
return;
|
||||
}
|
||||
doc.set_selection(view.id, Selection::single(tag.start, tag.end));
|
||||
if action.align_view(view, doc.id()) {
|
||||
align_view(doc, view, Align::Center)
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_dynamic_query(get_tags, Some(275))
|
||||
.with_preview(move |_editor, tag| {
|
||||
Some((
|
||||
tag.doc.path_or_id()?,
|
||||
Some((tag.start_line, tag.end_line)),
|
||||
))
|
||||
})
|
||||
.truncate_start(false);
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
/// Create a Rope and language config for a given existing path without creating a full Document.
|
||||
fn syntax_for_path(path: &Path, loader: &Loader) -> Option<(Rope, Syntax)> {
|
||||
let mut file = std::fs::File::open(path).ok()?;
|
||||
let (rope, _encoding, _has_bom) = from_reader(&mut file, None).ok()?;
|
||||
let text = rope.slice(..);
|
||||
let language = loader
|
||||
.language_for_filename(path)
|
||||
.or_else(|| loader.language_for_shebang(text))?;
|
||||
Syntax::new(text, language, loader)
|
||||
.ok()
|
||||
.map(|syntax| (rope, syntax))
|
||||
}
|
@@ -29,15 +29,6 @@ pub struct TypableCommand {
|
||||
pub signature: Signature,
|
||||
}
|
||||
|
||||
impl TypableCommand {
|
||||
fn completer_for_argument_number(&self, n: usize) -> &Completer {
|
||||
match self.completer.positional_args.get(n) {
|
||||
Some(completer) => completer,
|
||||
_ => &self.completer.var_args,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CommandCompleter {
|
||||
// Arguments with specific completion methods based on their position.
|
||||
@@ -68,6 +59,13 @@ impl CommandCompleter {
|
||||
var_args: completer,
|
||||
}
|
||||
}
|
||||
|
||||
fn for_argument_number(&self, n: usize) -> &Completer {
|
||||
match self.positional_args.get(n) {
|
||||
Some(completer) => completer,
|
||||
_ => &self.var_args,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
|
||||
@@ -104,6 +102,10 @@ fn open(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow:
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
open_impl(cx, args, Action::Replace)
|
||||
}
|
||||
|
||||
fn open_impl(cx: &mut compositor::Context, args: Args, action: Action) -> anyhow::Result<()> {
|
||||
for arg in args {
|
||||
let (path, pos) = crate::args::parse_file(&arg);
|
||||
let path = helix_stdx::path::expand_tilde(path);
|
||||
@@ -113,7 +115,8 @@ fn open(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow:
|
||||
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(editor, path.into_owned());
|
||||
let picker =
|
||||
ui::file_picker(editor, path.into_owned()).with_default_action(action);
|
||||
compositor.push(Box::new(overlaid(picker)));
|
||||
},
|
||||
));
|
||||
@@ -122,7 +125,7 @@ fn open(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow:
|
||||
cx.jobs.callback(callback);
|
||||
} else {
|
||||
// Otherwise, just open the file
|
||||
let _ = cx.editor.open(&path, Action::Replace)?;
|
||||
let _ = cx.editor.open(&path, action)?;
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
|
||||
doc.set_selection(view.id, pos);
|
||||
@@ -1469,7 +1472,14 @@ fn update(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyho
|
||||
|
||||
let (_view, doc) = current!(cx.editor);
|
||||
if doc.is_modified() {
|
||||
write(cx, args, event)
|
||||
write_impl(
|
||||
cx,
|
||||
None,
|
||||
WriteOptions {
|
||||
force: false,
|
||||
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
@@ -1808,10 +1818,7 @@ fn vsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyho
|
||||
if args.is_empty() {
|
||||
split(cx.editor, Action::VerticalSplit);
|
||||
} else {
|
||||
for arg in args {
|
||||
cx.editor
|
||||
.open(&PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
|
||||
}
|
||||
open_impl(cx, args, Action::VerticalSplit)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1825,10 +1832,7 @@ fn hsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyho
|
||||
if args.is_empty() {
|
||||
split(cx.editor, Action::HorizontalSplit);
|
||||
} else {
|
||||
for arg in args {
|
||||
cx.editor
|
||||
.open(&PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
|
||||
}
|
||||
open_impl(cx, args, Action::HorizontalSplit)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2652,13 +2656,13 @@ const BUFFER_CLOSE_OTHERS_SIGNATURE: Signature = Signature {
|
||||
// but Signature does not yet allow for var args.
|
||||
|
||||
/// This command handles all of its input as-is with no quoting or flags.
|
||||
const SHELL_SIGNATURE: Signature = Signature {
|
||||
pub const SHELL_SIGNATURE: Signature = Signature {
|
||||
positionals: (1, Some(2)),
|
||||
raw_after: Some(1),
|
||||
..Signature::DEFAULT
|
||||
};
|
||||
|
||||
const SHELL_COMPLETER: CommandCompleter = CommandCompleter::positional(&[
|
||||
pub const SHELL_COMPLETER: CommandCompleter = CommandCompleter::positional(&[
|
||||
// Command name
|
||||
completers::program,
|
||||
// Shell argument(s)
|
||||
@@ -3237,6 +3241,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||
completer: CommandCompleter::none(),
|
||||
signature: Signature {
|
||||
positionals: (0, Some(0)),
|
||||
flags: &[WRITE_NO_FORMAT_FLAG],
|
||||
..Signature::DEFAULT
|
||||
},
|
||||
},
|
||||
@@ -3824,14 +3829,15 @@ fn complete_command_line(editor: &Editor, input: &str) -> Vec<ui::prompt::Comple
|
||||
.get(command)
|
||||
.map_or_else(Vec::new, |cmd| {
|
||||
let args_offset = command.len() + 1;
|
||||
complete_command_args(editor, cmd, rest, args_offset)
|
||||
complete_command_args(editor, cmd.signature, &cmd.completer, rest, args_offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_command_args(
|
||||
pub fn complete_command_args(
|
||||
editor: &Editor,
|
||||
command: &TypableCommand,
|
||||
signature: Signature,
|
||||
completer: &CommandCompleter,
|
||||
input: &str,
|
||||
offset: usize,
|
||||
) -> Vec<ui::prompt::Completion> {
|
||||
@@ -3843,7 +3849,7 @@ fn complete_command_args(
|
||||
let cursor = input.len();
|
||||
let prefix = &input[..cursor];
|
||||
let mut tokenizer = Tokenizer::new(prefix, false);
|
||||
let mut args = Args::new(command.signature, false);
|
||||
let mut args = Args::new(signature, false);
|
||||
let mut final_token = None;
|
||||
let mut is_last_token = true;
|
||||
|
||||
@@ -3887,7 +3893,7 @@ fn complete_command_args(
|
||||
.len()
|
||||
.checked_sub(1)
|
||||
.expect("completion state to be positional");
|
||||
let completer = command.completer_for_argument_number(n);
|
||||
let completer = completer.for_argument_number(n);
|
||||
|
||||
completer(editor, &token.content)
|
||||
.into_iter()
|
||||
@@ -3896,7 +3902,7 @@ fn complete_command_args(
|
||||
}
|
||||
CompletionState::Flag(_) => fuzzy_match(
|
||||
token.content.trim_start_matches('-'),
|
||||
command.signature.flags.iter().map(|flag| flag.name),
|
||||
signature.flags.iter().map(|flag| flag.name),
|
||||
false,
|
||||
)
|
||||
.into_iter()
|
||||
@@ -3921,7 +3927,7 @@ fn complete_command_args(
|
||||
.len()
|
||||
.checked_sub(1)
|
||||
.expect("completion state to be positional");
|
||||
command.completer_for_argument_number(n)
|
||||
completer.for_argument_number(n)
|
||||
});
|
||||
complete_expand(editor, &token, arg_completer, offset + token.content_start)
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ use crate::events;
|
||||
use crate::handlers::auto_save::AutoSaveHandler;
|
||||
use crate::handlers::signature_help::SignatureHelpHandler;
|
||||
|
||||
pub use helix_view::handlers::Handlers;
|
||||
pub use helix_view::handlers::{word_index, Handlers};
|
||||
|
||||
use self::document_colors::DocumentColorsHandler;
|
||||
|
||||
@@ -26,12 +26,14 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
let signature_hints = SignatureHelpHandler::new().spawn();
|
||||
let auto_save = AutoSaveHandler::new().spawn();
|
||||
let document_colors = DocumentColorsHandler::default().spawn();
|
||||
let word_index = word_index::Handler::spawn();
|
||||
|
||||
let handlers = Handlers {
|
||||
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
|
||||
signature_hints,
|
||||
auto_save,
|
||||
document_colors,
|
||||
word_index,
|
||||
};
|
||||
|
||||
helix_view::handlers::register_hooks(&handlers);
|
||||
|
@@ -30,6 +30,7 @@ mod item;
|
||||
mod path;
|
||||
mod request;
|
||||
mod resolve;
|
||||
mod word;
|
||||
|
||||
async fn handle_response(
|
||||
requests: &mut JoinSet<CompletionResponse>,
|
||||
@@ -82,7 +83,7 @@ async fn replace_completions(
|
||||
fn show_completion(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
items: Vec<CompletionItem>,
|
||||
mut items: Vec<CompletionItem>,
|
||||
context: HashMap<CompletionProvider, ResponseContext>,
|
||||
trigger: Trigger,
|
||||
) {
|
||||
@@ -101,6 +102,7 @@ fn show_completion(
|
||||
if ui.completion.is_some() {
|
||||
return;
|
||||
}
|
||||
word::retain_valid_completions(trigger, doc, view.id, &mut items);
|
||||
editor.handlers.completions.active_completions = context;
|
||||
|
||||
let completion_area = ui.set_completion(editor, items, trigger.pos, size);
|
||||
|
@@ -28,6 +28,8 @@ use crate::job::{dispatch, dispatch_blocking};
|
||||
use crate::ui;
|
||||
use crate::ui::editor::InsertEvent;
|
||||
|
||||
use super::word;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub(super) enum TriggerKind {
|
||||
Auto,
|
||||
@@ -242,10 +244,15 @@ fn request_completions(
|
||||
doc.selection(view.id).clone(),
|
||||
doc,
|
||||
handle.clone(),
|
||||
savepoint,
|
||||
savepoint.clone(),
|
||||
) {
|
||||
requests.spawn_blocking(path_completion_request);
|
||||
}
|
||||
if let Some(word_completion_request) =
|
||||
word::completion(editor, trigger, handle.clone(), savepoint)
|
||||
{
|
||||
requests.spawn_blocking(word_completion_request);
|
||||
}
|
||||
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
||||
|
134
helix-term/src/handlers/completion/word.rs
Normal file
134
helix-term/src/handlers/completion/word.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use helix_core::{
|
||||
self as core, chars::char_is_word, completion::CompletionProvider, movement, Transaction,
|
||||
};
|
||||
use helix_event::TaskHandle;
|
||||
use helix_stdx::rope::RopeSliceExt as _;
|
||||
use helix_view::{
|
||||
document::SavePoint, handlers::completion::ResponseContext, Document, Editor, ViewId,
|
||||
};
|
||||
|
||||
use super::{request::TriggerKind, CompletionItem, CompletionItems, CompletionResponse, Trigger};
|
||||
|
||||
const COMPLETION_KIND: &str = "word";
|
||||
|
||||
pub(super) fn completion(
|
||||
editor: &Editor,
|
||||
trigger: Trigger,
|
||||
handle: TaskHandle,
|
||||
savepoint: Arc<SavePoint>,
|
||||
) -> Option<impl FnOnce() -> CompletionResponse> {
|
||||
if !doc!(editor).word_completion_enabled() {
|
||||
return None;
|
||||
}
|
||||
let config = editor.config().word_completion;
|
||||
let doc_config = doc!(editor)
|
||||
.language_config()
|
||||
.and_then(|config| config.word_completion);
|
||||
let trigger_length = doc_config
|
||||
.and_then(|c| c.trigger_length)
|
||||
.unwrap_or(config.trigger_length)
|
||||
.get() as usize;
|
||||
|
||||
let (view, doc) = current_ref!(editor);
|
||||
let rope = doc.text().clone();
|
||||
let word_index = editor.handlers.word_index().clone();
|
||||
let text = doc.text().slice(..);
|
||||
let selection = doc.selection(view.id).clone();
|
||||
let pos = selection.primary().cursor(text);
|
||||
|
||||
let cursor = movement::move_prev_word_start(text, core::Range::point(pos), 1);
|
||||
if cursor.head == pos {
|
||||
return None;
|
||||
}
|
||||
if trigger.kind != TriggerKind::Manual
|
||||
&& text
|
||||
.slice(cursor.head..)
|
||||
.graphemes()
|
||||
.take(trigger_length)
|
||||
.take_while(|g| g.chars().all(char_is_word))
|
||||
.count()
|
||||
!= trigger_length
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let typed_word_range = cursor.head..pos;
|
||||
let typed_word = text.slice(typed_word_range.clone());
|
||||
let edit_diff = if typed_word
|
||||
.char(typed_word.len_chars().saturating_sub(1))
|
||||
.is_whitespace()
|
||||
{
|
||||
0
|
||||
} else {
|
||||
typed_word.len_chars()
|
||||
};
|
||||
|
||||
if handle.is_canceled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let future = move || {
|
||||
let text = rope.slice(..);
|
||||
let typed_word: Cow<_> = text.slice(typed_word_range).into();
|
||||
let items = word_index
|
||||
.matches(&typed_word)
|
||||
.into_iter()
|
||||
.filter(|word| word.as_str() != typed_word.as_ref())
|
||||
.map(|word| {
|
||||
let transaction = Transaction::change_by_selection(&rope, &selection, |range| {
|
||||
let cursor = range.cursor(text);
|
||||
(cursor - edit_diff, cursor, Some((&word).into()))
|
||||
});
|
||||
CompletionItem::Other(core::CompletionItem {
|
||||
transaction,
|
||||
label: word.into(),
|
||||
kind: Cow::Borrowed(COMPLETION_KIND),
|
||||
documentation: None,
|
||||
provider: CompletionProvider::Word,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
CompletionResponse {
|
||||
items: CompletionItems::Other(items),
|
||||
provider: CompletionProvider::Word,
|
||||
context: ResponseContext {
|
||||
is_incomplete: false,
|
||||
priority: 0,
|
||||
savepoint,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Some(future)
|
||||
}
|
||||
|
||||
pub(super) fn retain_valid_completions(
|
||||
trigger: Trigger,
|
||||
doc: &Document,
|
||||
view_id: ViewId,
|
||||
items: &mut Vec<CompletionItem>,
|
||||
) {
|
||||
if trigger.kind == TriggerKind::Manual {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view_id).primary().cursor(text);
|
||||
if text
|
||||
.get_char(cursor.saturating_sub(1))
|
||||
.is_some_and(|ch| ch.is_whitespace())
|
||||
{
|
||||
items.retain(|item| {
|
||||
!matches!(
|
||||
item,
|
||||
CompletionItem::Other(core::CompletionItem {
|
||||
provider: CompletionProvider::Word,
|
||||
..
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,22 +1,33 @@
|
||||
use crate::config::{Config, ConfigLoadError};
|
||||
use crossterm::{
|
||||
style::{Color, StyledContent, Stylize},
|
||||
tty::IsTty,
|
||||
};
|
||||
use helix_core::config::{default_lang_config, user_lang_config};
|
||||
use helix_loader::grammar::load_runtime_file;
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::{IsTerminal, Write},
|
||||
};
|
||||
use termina::{
|
||||
style::{ColorSpec, StyleExt as _, Stylized},
|
||||
Terminal as _,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum TsFeature {
|
||||
Highlight,
|
||||
TextObject,
|
||||
AutoIndent,
|
||||
Tags,
|
||||
RainbowBracket,
|
||||
}
|
||||
|
||||
impl TsFeature {
|
||||
pub fn all() -> &'static [Self] {
|
||||
&[Self::Highlight, Self::TextObject, Self::AutoIndent]
|
||||
&[
|
||||
Self::Highlight,
|
||||
Self::TextObject,
|
||||
Self::AutoIndent,
|
||||
Self::Tags,
|
||||
Self::RainbowBracket,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn runtime_filename(&self) -> &'static str {
|
||||
@@ -24,6 +35,8 @@ impl TsFeature {
|
||||
Self::Highlight => "highlights.scm",
|
||||
Self::TextObject => "textobjects.scm",
|
||||
Self::AutoIndent => "indents.scm",
|
||||
Self::Tags => "tags.scm",
|
||||
Self::RainbowBracket => "rainbows.scm",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +45,8 @@ impl TsFeature {
|
||||
Self::Highlight => "Syntax Highlighting",
|
||||
Self::TextObject => "Treesitter Textobjects",
|
||||
Self::AutoIndent => "Auto Indent",
|
||||
Self::Tags => "Code Navigation Tags",
|
||||
Self::RainbowBracket => "Rainbow Brackets",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +55,8 @@ impl TsFeature {
|
||||
Self::Highlight => "Highlight",
|
||||
Self::TextObject => "Textobject",
|
||||
Self::AutoIndent => "Indent",
|
||||
Self::Tags => "Tags",
|
||||
Self::RainbowBracket => "Rainbow",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +151,15 @@ pub fn clipboard() -> std::io::Result<()> {
|
||||
}
|
||||
|
||||
pub fn languages_all() -> std::io::Result<()> {
|
||||
languages(None)
|
||||
}
|
||||
|
||||
pub fn languages_selection() -> std::io::Result<()> {
|
||||
let selection = helix_loader::grammar::get_grammar_names().unwrap_or_default();
|
||||
languages(selection)
|
||||
}
|
||||
|
||||
fn languages(selection: Option<HashSet<String>>) -> std::io::Result<()> {
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
@@ -160,21 +186,24 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
headings.push(feat.short_title())
|
||||
}
|
||||
|
||||
let terminal_cols = crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80);
|
||||
let terminal_cols = termina::PlatformTerminal::new()
|
||||
.and_then(|terminal| terminal.get_dimensions())
|
||||
.map(|size| size.cols)
|
||||
.unwrap_or(80);
|
||||
let column_width = terminal_cols as usize / headings.len();
|
||||
let is_terminal = std::io::stdout().is_tty();
|
||||
let is_terminal = std::io::stdout().is_terminal();
|
||||
|
||||
let fit = |s: &str| -> StyledContent<String> {
|
||||
let fit = |s: &str| -> Stylized<'static> {
|
||||
format!(
|
||||
"{:column_width$}",
|
||||
s.get(..column_width - 2)
|
||||
.map(|s| format!("{}…", s))
|
||||
.unwrap_or_else(|| s.to_string())
|
||||
)
|
||||
.stylize()
|
||||
.stylized()
|
||||
};
|
||||
let color = |s: StyledContent<String>, c: Color| if is_terminal { s.with(c) } else { s };
|
||||
let bold = |s: StyledContent<String>| if is_terminal { s.bold() } else { s };
|
||||
let color = |s: Stylized<'static>, c: ColorSpec| if is_terminal { s.foreground(c) } else { s };
|
||||
let bold = |s: Stylized<'static>| if is_terminal { s.bold() } else { s };
|
||||
|
||||
for heading in headings {
|
||||
write!(stdout, "{}", bold(fit(heading)))?;
|
||||
@@ -187,15 +216,22 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
|
||||
let check_binary_with_name = |cmd: Option<(&str, &str)>| match cmd {
|
||||
Some((name, cmd)) => match helix_stdx::env::which(cmd) {
|
||||
Ok(_) => color(fit(&format!("✓ {}", name)), Color::Green),
|
||||
Err(_) => color(fit(&format!("✘ {}", name)), Color::Red),
|
||||
Ok(_) => color(fit(&format!("✓ {}", name)), ColorSpec::BRIGHT_GREEN),
|
||||
Err(_) => color(fit(&format!("✘ {}", name)), ColorSpec::BRIGHT_RED),
|
||||
},
|
||||
None => color(fit("None"), Color::Yellow),
|
||||
None => color(fit("None"), ColorSpec::BRIGHT_YELLOW),
|
||||
};
|
||||
|
||||
let check_binary = |cmd: Option<&str>| check_binary_with_name(cmd.map(|cmd| (cmd, cmd)));
|
||||
|
||||
for lang in &syn_loader_conf.language {
|
||||
if selection
|
||||
.as_ref()
|
||||
.is_some_and(|s| !s.contains(&lang.language_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
write!(stdout, "{}", fit(&lang.language_id))?;
|
||||
|
||||
let mut cmds = lang.language_servers.iter().filter_map(|ls| {
|
||||
@@ -217,8 +253,8 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
|
||||
for ts_feat in TsFeature::all() {
|
||||
match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() {
|
||||
true => write!(stdout, "{}", color(fit("✓"), Color::Green))?,
|
||||
false => write!(stdout, "{}", color(fit("✘"), Color::Red))?,
|
||||
true => write!(stdout, "{}", color(fit("✓"), ColorSpec::BRIGHT_GREEN))?,
|
||||
false => write!(stdout, "{}", color(fit("✘"), ColorSpec::BRIGHT_RED))?,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +266,14 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if selection.is_some() {
|
||||
writeln!(
|
||||
stdout,
|
||||
"\nThis list is filtered according to the 'use-grammars' option in languages.toml file.\n\
|
||||
To see the full list, use the '--health all' or '--health all-languages' option."
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -391,9 +435,16 @@ fn probe_treesitter_feature(lang: &str, feature: TsFeature) -> std::io::Result<(
|
||||
|
||||
pub fn print_health(health_arg: Option<String>) -> std::io::Result<()> {
|
||||
match health_arg.as_deref() {
|
||||
Some("languages") => languages_all()?,
|
||||
Some("languages") => languages_selection()?,
|
||||
Some("all-languages") => languages_all()?,
|
||||
Some("clipboard") => clipboard()?,
|
||||
None | Some("all") => {
|
||||
None => {
|
||||
general()?;
|
||||
clipboard()?;
|
||||
writeln!(std::io::stdout().lock())?;
|
||||
languages_selection()?;
|
||||
}
|
||||
Some("all") => {
|
||||
general()?;
|
||||
clipboard()?;
|
||||
writeln!(std::io::stdout().lock())?;
|
||||
|
@@ -120,6 +120,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"e" => goto_prev_entry,
|
||||
"T" => goto_prev_test,
|
||||
"p" => goto_prev_paragraph,
|
||||
"x" => goto_prev_xml_element,
|
||||
"space" => add_newline_above,
|
||||
},
|
||||
"]" => { "Right bracket"
|
||||
@@ -134,6 +135,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"e" => goto_next_entry,
|
||||
"T" => goto_next_test,
|
||||
"p" => goto_next_paragraph,
|
||||
"x" => goto_next_xml_element,
|
||||
"space" => add_newline_below,
|
||||
},
|
||||
|
||||
@@ -227,8 +229,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"E" => file_explorer_in_current_buffer_directory,
|
||||
"b" => buffer_picker,
|
||||
"j" => jumplist_picker,
|
||||
"s" => symbol_picker,
|
||||
"S" => workspace_symbol_picker,
|
||||
"s" => lsp_or_syntax_symbol_picker,
|
||||
"S" => lsp_or_syntax_workspace_symbol_picker,
|
||||
"d" => diagnostics_picker,
|
||||
"D" => workspace_diagnostics_picker,
|
||||
"g" => changed_file_picker,
|
||||
|
@@ -1,5 +1,4 @@
|
||||
use anyhow::{Context, Error, Result};
|
||||
use crossterm::event::EventStream;
|
||||
use helix_loader::VERSION_AND_GIT_HASH;
|
||||
use helix_term::application::Application;
|
||||
use helix_term::args::Args;
|
||||
@@ -63,8 +62,10 @@ FLAGS:
|
||||
-h, --help Prints help information
|
||||
--tutor Loads the tutorial
|
||||
--health [CATEGORY] Checks for potential errors in editor setup
|
||||
CATEGORY can be a language or one of 'clipboard', 'languages'
|
||||
or 'all'. 'all' is the default if not specified.
|
||||
CATEGORY can be a language or one of 'clipboard', 'languages',
|
||||
'all-languages' or 'all'. 'languages' is filtered according to
|
||||
user config, 'all-languages' and 'all' are not. If not specified,
|
||||
the default is the same as 'all', but with languages filtering.
|
||||
-g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml
|
||||
-c, --config <file> Specifies a file to use for configuration
|
||||
-v Increases logging verbosity each use for up to 3 times
|
||||
@@ -149,8 +150,9 @@ FLAGS:
|
||||
|
||||
// TODO: use the thread local executor to spawn the application task separately from the work pool
|
||||
let mut app = Application::new(args, config, lang_loader).context("unable to start Helix")?;
|
||||
let mut events = app.event_stream();
|
||||
|
||||
let exit_code = app.run(&mut EventStream::new()).await?;
|
||||
let exit_code = app.run(&mut events).await?;
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
@@ -127,6 +127,18 @@ impl EditorView {
|
||||
&text_annotations,
|
||||
));
|
||||
|
||||
if doc
|
||||
.language_config()
|
||||
.and_then(|config| config.rainbow_brackets)
|
||||
.unwrap_or(config.rainbow_brackets)
|
||||
{
|
||||
if let Some(overlay) =
|
||||
Self::doc_rainbow_highlights(doc, view_offset.anchor, inner.height, theme, &loader)
|
||||
{
|
||||
overlays.push(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
Self::doc_diagnostics_highlights_into(doc, theme, &mut overlays);
|
||||
|
||||
if is_focused {
|
||||
@@ -304,6 +316,27 @@ impl EditorView {
|
||||
text_annotations.collect_overlay_highlights(range)
|
||||
}
|
||||
|
||||
pub fn doc_rainbow_highlights(
|
||||
doc: &Document,
|
||||
anchor: usize,
|
||||
height: u16,
|
||||
theme: &Theme,
|
||||
loader: &syntax::Loader,
|
||||
) -> Option<OverlayHighlights> {
|
||||
let syntax = doc.syntax()?;
|
||||
let text = doc.text().slice(..);
|
||||
let row = text.char_to_line(anchor.min(text.len_chars()));
|
||||
let visible_range = Self::viewport_byte_range(text, row, height);
|
||||
let start = syntax::child_for_byte_range(
|
||||
&syntax.tree().root_node(),
|
||||
visible_range.start as u32..visible_range.end as u32,
|
||||
)
|
||||
.map_or(visible_range.start as u32, |node| node.start_byte());
|
||||
let range = start..visible_range.end as u32;
|
||||
|
||||
Some(syntax.rainbow_highlights(text, theme.rainbow_length(), loader, range))
|
||||
}
|
||||
|
||||
/// Get highlight spans for document diagnostics
|
||||
pub fn doc_diagnostics_highlights_into(
|
||||
doc: &Document,
|
||||
@@ -505,7 +538,7 @@ impl EditorView {
|
||||
};
|
||||
spans.push((selection_scope, range.anchor..selection_end));
|
||||
// add block cursors
|
||||
// skip primary cursor if terminal is unfocused - crossterm cursor is used in that case
|
||||
// skip primary cursor if terminal is unfocused - terminal cursor is used in that case
|
||||
if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
|
||||
spans.push((cursor_scope, cursor_start..range.head));
|
||||
}
|
||||
@@ -513,7 +546,7 @@ impl EditorView {
|
||||
// Reverse case.
|
||||
let cursor_end = next_grapheme_boundary(text, range.head);
|
||||
// add block cursors
|
||||
// skip primary cursor if terminal is unfocused - crossterm cursor is used in that case
|
||||
// skip primary cursor if terminal is unfocused - terminal cursor is used in that case
|
||||
if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
|
||||
spans.push((cursor_scope, range.head..cursor_end));
|
||||
}
|
||||
@@ -1126,6 +1159,8 @@ impl EditorView {
|
||||
let editor = &mut cxt.editor;
|
||||
|
||||
if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
|
||||
editor.focus(view_id);
|
||||
|
||||
let prev_view_id = view!(editor).id;
|
||||
let doc = doc_mut!(editor, &view!(editor, view_id).doc);
|
||||
|
||||
@@ -1149,7 +1184,6 @@ impl EditorView {
|
||||
self.clear_completion(editor);
|
||||
}
|
||||
|
||||
editor.focus(view_id);
|
||||
editor.ensure_cursor_in_view(view_id);
|
||||
|
||||
return EventResult::Consumed(None);
|
||||
@@ -1597,7 +1631,7 @@ impl Component for EditorView {
|
||||
if self.terminal_focused {
|
||||
(pos, CursorKind::Hidden)
|
||||
} else {
|
||||
// use crossterm cursor when terminal loses focus
|
||||
// use terminal cursor when terminal loses focus
|
||||
(pos, CursorKind::Underline)
|
||||
}
|
||||
}
|
||||
|
@@ -356,7 +356,7 @@ fn directory_content(path: &Path) -> Result<Vec<(PathBuf, bool)>, std::io::Error
|
||||
.map(|entry| {
|
||||
(
|
||||
entry.path(),
|
||||
entry.file_type().is_ok_and(|file_type| file_type.is_dir()),
|
||||
std::fs::metadata(entry.path()).is_ok_and(|metadata| metadata.is_dir()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
@@ -258,6 +258,7 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
|
||||
widths: Vec<Constraint>,
|
||||
|
||||
callback_fn: PickerCallback<T>,
|
||||
default_action: Action,
|
||||
|
||||
pub truncate_start: bool,
|
||||
/// Caches paths to documents
|
||||
@@ -382,6 +383,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||
truncate_start: true,
|
||||
show_preview: true,
|
||||
callback_fn: Box::new(callback_fn),
|
||||
default_action: Action::Replace,
|
||||
completion_height: 0,
|
||||
widths,
|
||||
preview_cache: HashMap::new(),
|
||||
@@ -424,6 +426,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_initial_cursor(mut self, cursor: u32) -> Self {
|
||||
self.cursor = cursor;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dynamic_query(
|
||||
mut self,
|
||||
callback: DynQueryCallback<T, D>,
|
||||
@@ -440,6 +447,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_default_action(mut self, action: Action) -> Self {
|
||||
self.default_action = action;
|
||||
self
|
||||
}
|
||||
|
||||
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
|
||||
pub fn move_by(&mut self, amount: u32, direction: Direction) {
|
||||
let len = self.matcher.snapshot().matched_item_count();
|
||||
@@ -1071,7 +1083,7 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
|
||||
key!(Esc) | ctrl!('c') => return close_fn(self),
|
||||
alt!(Enter) => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(ctx, option, Action::Replace);
|
||||
(self.callback_fn)(ctx, option, self.default_action);
|
||||
}
|
||||
}
|
||||
key!(Enter) => {
|
||||
@@ -1095,7 +1107,7 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
|
||||
self.handle_prompt_change(true);
|
||||
} else {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(ctx, option, Action::Replace);
|
||||
(self.callback_fn)(ctx, option, self.default_action);
|
||||
}
|
||||
if let Some(history_register) = self.prompt.history_register() {
|
||||
if let Err(err) = ctx
|
||||
|
@@ -779,8 +779,7 @@ impl Component for Prompt {
|
||||
col += self.line[self.cursor..]
|
||||
.graphemes(true)
|
||||
.next()
|
||||
.unwrap()
|
||||
.width();
|
||||
.map_or(0, |g| g.width());
|
||||
}
|
||||
|
||||
let line = area.height as usize - 1;
|
||||
|
@@ -158,6 +158,7 @@ where
|
||||
helix_view::editor::StatusLineElement::Spacer => render_spacer,
|
||||
helix_view::editor::StatusLineElement::VersionControl => render_version_control,
|
||||
helix_view::editor::StatusLineElement::Register => render_register,
|
||||
helix_view::editor::StatusLineElement::CurrentWorkingDirectory => render_cwd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,3 +574,16 @@ where
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn render_cwd<'a, F>(context: &mut RenderContext<'a>, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy,
|
||||
{
|
||||
let cwd = helix_stdx::env::current_working_dir();
|
||||
let cwd = cwd
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
write(context, cwd.into())
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ use super::*;
|
||||
|
||||
mod insert;
|
||||
mod movement;
|
||||
mod reverse_selection_contents;
|
||||
mod rotate_selection_contents;
|
||||
mod write;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
49
helix-term/tests/test/commands/reverse_selection_contents.rs
Normal file
49
helix-term/tests/test/commands/reverse_selection_contents.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use super::*;
|
||||
|
||||
const A: &str = indoc! {"
|
||||
#(a|)#
|
||||
#(b|)#
|
||||
#(c|)#
|
||||
#[d|]#
|
||||
#(e|)#"
|
||||
};
|
||||
const A_REV: &str = indoc! {"
|
||||
#(e|)#
|
||||
#[d|]#
|
||||
#(c|)#
|
||||
#(b|)#
|
||||
#(a|)#"
|
||||
};
|
||||
const B: &str = indoc! {"
|
||||
#(a|)#
|
||||
#(b|)#
|
||||
#[c|]#
|
||||
#(d|)#
|
||||
#(e|)#"
|
||||
};
|
||||
const B_REV: &str = indoc! {"
|
||||
#(e|)#
|
||||
#(d|)#
|
||||
#[c|]#
|
||||
#(b|)#
|
||||
#(a|)#"
|
||||
};
|
||||
|
||||
const CMD: &str = "<space>?reverse_selection_contents<ret>";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn reverse_selection_contents() -> anyhow::Result<()> {
|
||||
test((A, CMD, A_REV)).await?;
|
||||
test((B, CMD, B_REV)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn reverse_selection_contents_with_count() -> anyhow::Result<()> {
|
||||
test((B, format!("2{CMD}"), B)).await?;
|
||||
test((B, format!("3{CMD}"), B_REV)).await?;
|
||||
test((B, format!("4{CMD}"), B)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
71
helix-term/tests/test/commands/rotate_selection_contents.rs
Normal file
71
helix-term/tests/test/commands/rotate_selection_contents.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use super::*;
|
||||
|
||||
// Progression: A -> B -> C -> D
|
||||
// as we press `A-)`
|
||||
const A: &str = indoc! {"
|
||||
#(a|)#
|
||||
#(b|)#
|
||||
#(c|)#
|
||||
#[d|]#
|
||||
#(e|)#"
|
||||
};
|
||||
|
||||
const B: &str = indoc! {"
|
||||
#(e|)#
|
||||
#(a|)#
|
||||
#(b|)#
|
||||
#(c|)#
|
||||
#[d|]#"
|
||||
};
|
||||
|
||||
const C: &str = indoc! {"
|
||||
#[d|]#
|
||||
#(e|)#
|
||||
#(a|)#
|
||||
#(b|)#
|
||||
#(c|)#"
|
||||
};
|
||||
|
||||
const D: &str = indoc! {"
|
||||
#(c|)#
|
||||
#[d|]#
|
||||
#(e|)#
|
||||
#(a|)#
|
||||
#(b|)#"
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rotate_selection_contents_forward_repeated() -> anyhow::Result<()> {
|
||||
test((A, "<A-)>", B)).await?;
|
||||
test((B, "<A-)>", C)).await?;
|
||||
test((C, "<A-)>", D)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rotate_selection_contents_forward_with_count() -> anyhow::Result<()> {
|
||||
test((A, "2<A-)>", C)).await?;
|
||||
test((A, "3<A-)>", D)).await?;
|
||||
test((B, "2<A-)>", D)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rotate_selection_contents_backward_repeated() -> anyhow::Result<()> {
|
||||
test((D, "<A-(>", C)).await?;
|
||||
test((C, "<A-(>", B)).await?;
|
||||
test((B, "<A-(>", A)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rotate_selection_contents_backward_with_count() -> anyhow::Result<()> {
|
||||
test((D, "2<A-(>", B)).await?;
|
||||
test((D, "3<A-(>", A)).await?;
|
||||
test((C, "2<A-(>", A)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -6,11 +6,11 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::bail;
|
||||
use crossterm::event::{Event, KeyEvent};
|
||||
use helix_core::{diagnostic::Severity, test, Selection, Transaction};
|
||||
use helix_term::{application::Application, args::Args, config::Config, keymap::merge_keys};
|
||||
use helix_view::{current_ref, doc, editor::LspConfig, input::parse_macro, Editor};
|
||||
use tempfile::NamedTempFile;
|
||||
use termina::event::{Event, KeyEvent};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
/// Specify how to set up the input text with line feeds
|
||||
|
@@ -12,7 +12,7 @@ repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
default = ["termina"]
|
||||
|
||||
[dependencies]
|
||||
helix-view = { path = "../helix-view", features = ["term"] }
|
||||
@@ -21,7 +21,7 @@ helix-core = { path = "../helix-core" }
|
||||
bitflags.workspace = true
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation.workspace = true
|
||||
crossterm = { version = "0.28", optional = true }
|
||||
termina = { workspace = true, optional = true }
|
||||
termini = "1.0"
|
||||
once_cell = "1.21"
|
||||
log = "~0.4"
|
||||
|
@@ -1,465 +0,0 @@
|
||||
use crate::{backend::Backend, buffer::Cell, terminal::Config};
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, SetCursorStyle, Show},
|
||||
event::{
|
||||
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags,
|
||||
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Colors, Print, SetAttribute, SetBackgroundColor,
|
||||
SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear, ClearType},
|
||||
Command,
|
||||
};
|
||||
use helix_view::{
|
||||
editor::Config as EditorConfig,
|
||||
graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
use termini::TermInfo;
|
||||
|
||||
fn term_program() -> Option<String> {
|
||||
// Some terminals don't set $TERM_PROGRAM
|
||||
match std::env::var("TERM_PROGRAM") {
|
||||
Err(_) => std::env::var("TERM").ok(),
|
||||
Ok(term_program) => Some(term_program),
|
||||
}
|
||||
}
|
||||
fn vte_version() -> Option<usize> {
|
||||
std::env::var("VTE_VERSION").ok()?.parse().ok()
|
||||
}
|
||||
fn reset_cursor_approach(terminfo: TermInfo) -> String {
|
||||
let mut reset_str = "\x1B[0 q".to_string();
|
||||
|
||||
if let Some(termini::Value::Utf8String(se_str)) = terminfo.extended_cap("Se") {
|
||||
reset_str.push_str(se_str);
|
||||
};
|
||||
|
||||
reset_str.push_str(
|
||||
terminfo
|
||||
.utf8_string_cap(termini::StringCapability::CursorNormal)
|
||||
.unwrap_or(""),
|
||||
);
|
||||
|
||||
reset_str
|
||||
}
|
||||
|
||||
/// Describes terminal capabilities like extended underline, truecolor, etc.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Capabilities {
|
||||
/// Support for undercurled, underdashed, etc.
|
||||
has_extended_underlines: bool,
|
||||
/// Support for resetting the cursor style back to normal.
|
||||
reset_cursor_command: String,
|
||||
}
|
||||
|
||||
impl Default for Capabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
has_extended_underlines: false,
|
||||
reset_cursor_command: "\x1B[0 q".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
/// Detect capabilities from the terminfo database located based
|
||||
/// on the $TERM environment variable. If detection fails, returns
|
||||
/// a default value where no capability is supported, or just undercurl
|
||||
/// if config.undercurl is set.
|
||||
pub fn from_env_or_default(config: &EditorConfig) -> Self {
|
||||
match termini::TermInfo::from_env() {
|
||||
Err(_) => Capabilities {
|
||||
has_extended_underlines: config.undercurl,
|
||||
..Capabilities::default()
|
||||
},
|
||||
Ok(t) => Capabilities {
|
||||
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
|
||||
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
|
||||
// WezTerm supports underlines but a lot of distros don't properly install its terminfo
|
||||
has_extended_underlines: config.undercurl
|
||||
|| t.extended_cap("Smulx").is_some()
|
||||
|| t.extended_cap("Su").is_some()
|
||||
|| vte_version() >= Some(5102)
|
||||
|| matches!(term_program().as_deref(), Some("WezTerm")),
|
||||
reset_cursor_command: reset_cursor_approach(t),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Terminal backend supporting a wide variety of terminals
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
capabilities: Capabilities,
|
||||
supports_keyboard_enhancement_protocol: OnceCell<bool>,
|
||||
mouse_capture_enabled: bool,
|
||||
supports_bracketed_paste: bool,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend<W> {
|
||||
// helix is not usable without colors, but crossterm will disable
|
||||
// them by default if NO_COLOR is set in the environment. Override
|
||||
// this behaviour.
|
||||
crossterm::style::force_color_output(true);
|
||||
CrosstermBackend {
|
||||
buffer,
|
||||
capabilities: Capabilities::from_env_or_default(config),
|
||||
supports_keyboard_enhancement_protocol: OnceCell::new(),
|
||||
mouse_capture_enabled: false,
|
||||
supports_bracketed_paste: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supports_keyboard_enhancement_protocol(&self) -> bool {
|
||||
*self.supports_keyboard_enhancement_protocol
|
||||
.get_or_init(|| {
|
||||
use std::time::Instant;
|
||||
|
||||
let now = Instant::now();
|
||||
let supported = matches!(terminal::supports_keyboard_enhancement(), Ok(true));
|
||||
log::debug!(
|
||||
"The keyboard enhancement protocol is {}supported in this terminal (checked in {:?})",
|
||||
if supported { "" } else { "not " },
|
||||
Instant::now().duration_since(now)
|
||||
);
|
||||
supported
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.buffer.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn claim(&mut self, config: Config) -> io::Result<()> {
|
||||
terminal::enable_raw_mode()?;
|
||||
execute!(
|
||||
self.buffer,
|
||||
terminal::EnterAlternateScreen,
|
||||
EnableFocusChange
|
||||
)?;
|
||||
match execute!(self.buffer, EnableBracketedPaste,) {
|
||||
Err(err) if err.kind() == io::ErrorKind::Unsupported => {
|
||||
log::warn!("Bracketed paste is not supported on this terminal.");
|
||||
self.supports_bracketed_paste = false;
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
Ok(_) => (),
|
||||
};
|
||||
execute!(self.buffer, terminal::Clear(terminal::ClearType::All))?;
|
||||
if config.enable_mouse_capture {
|
||||
execute!(self.buffer, EnableMouseCapture)?;
|
||||
self.mouse_capture_enabled = true;
|
||||
}
|
||||
if self.supports_keyboard_enhancement_protocol() {
|
||||
execute!(
|
||||
self.buffer,
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
)
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reconfigure(&mut self, config: Config) -> io::Result<()> {
|
||||
if self.mouse_capture_enabled != config.enable_mouse_capture {
|
||||
if config.enable_mouse_capture {
|
||||
execute!(self.buffer, EnableMouseCapture)?;
|
||||
} else {
|
||||
execute!(self.buffer, DisableMouseCapture)?;
|
||||
}
|
||||
self.mouse_capture_enabled = config.enable_mouse_capture;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore(&mut self, config: Config) -> io::Result<()> {
|
||||
// reset cursor shape
|
||||
self.buffer
|
||||
.write_all(self.capabilities.reset_cursor_command.as_bytes())?;
|
||||
if config.enable_mouse_capture {
|
||||
execute!(self.buffer, DisableMouseCapture)?;
|
||||
}
|
||||
if self.supports_keyboard_enhancement_protocol() {
|
||||
execute!(self.buffer, PopKeyboardEnhancementFlags)?;
|
||||
}
|
||||
if self.supports_bracketed_paste {
|
||||
execute!(self.buffer, DisableBracketedPaste,)?;
|
||||
}
|
||||
execute!(
|
||||
self.buffer,
|
||||
DisableFocusChange,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
terminal::disable_raw_mode()
|
||||
}
|
||||
|
||||
fn force_restore() -> io::Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
// reset cursor shape
|
||||
write!(stdout, "\x1B[0 q")?;
|
||||
// Ignore errors on disabling, this might trigger on windows if we call
|
||||
// disable without calling enable previously
|
||||
let _ = execute!(stdout, DisableMouseCapture);
|
||||
let _ = execute!(stdout, PopKeyboardEnhancementFlags);
|
||||
let _ = execute!(stdout, DisableBracketedPaste);
|
||||
execute!(stdout, DisableFocusChange, terminal::LeaveAlternateScreen)?;
|
||||
terminal::disable_raw_mode()
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut underline_style = UnderlineStyle::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
queue!(self.buffer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.buffer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.buffer,
|
||||
SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
bg = cell.bg;
|
||||
}
|
||||
|
||||
let mut new_underline_style = cell.underline_style;
|
||||
if self.capabilities.has_extended_underlines {
|
||||
if cell.underline_color != underline_color {
|
||||
let color = CColor::from(cell.underline_color);
|
||||
queue!(self.buffer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
} else {
|
||||
match new_underline_style {
|
||||
UnderlineStyle::Reset | UnderlineStyle::Line => (),
|
||||
_ => new_underline_style = UnderlineStyle::Line,
|
||||
}
|
||||
}
|
||||
|
||||
if new_underline_style != underline_style {
|
||||
let attr = CAttribute::from(new_underline_style);
|
||||
queue!(self.buffer, SetAttribute(attr))?;
|
||||
underline_style = new_underline_style;
|
||||
}
|
||||
|
||||
queue!(self.buffer, Print(&cell.symbol))?;
|
||||
}
|
||||
|
||||
queue!(
|
||||
self.buffer,
|
||||
SetUnderlineColor(CColor::Reset),
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
)
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.buffer, Hide)
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
|
||||
let shape = match kind {
|
||||
CursorKind::Block => SetCursorStyle::SteadyBlock,
|
||||
CursorKind::Bar => SetCursorStyle::SteadyBar,
|
||||
CursorKind::Underline => SetCursorStyle::SteadyUnderScore,
|
||||
CursorKind::Hidden => unreachable!(),
|
||||
};
|
||||
execute!(self.buffer, Show, shape)
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
execute!(self.buffer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
execute!(self.buffer, Clear(ClearType::All))
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) =
|
||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
queue!(w, SetAttribute(CAttribute::NoReverse))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CAttribute::NoItalic))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CAttribute::NoBlink))?;
|
||||
}
|
||||
if removed.contains(Modifier::HIDDEN) {
|
||||
queue!(w, SetAttribute(CAttribute::NoHidden))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
queue!(w, SetAttribute(CAttribute::Reverse))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CAttribute::Bold))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CAttribute::Italic))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
|
||||
}
|
||||
if added.contains(Modifier::HIDDEN) {
|
||||
queue!(w, SetAttribute(CAttribute::Hidden))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Crossterm uses semicolon as a separator for colors
|
||||
/// this is actually not spec compliant (although commonly supported)
|
||||
/// However the correct approach is to use colons as a separator.
|
||||
/// This usually doesn't make a difference for emulators that do support colored underlines.
|
||||
/// However terminals that do not support colored underlines will ignore underlines colors with colons
|
||||
/// while escape sequences with semicolons are always processed which leads to weird visual artifacts.
|
||||
/// See [this nvim issue](https://github.com/neovim/neovim/issues/9270) for details
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SetUnderlineColor(pub CColor);
|
||||
|
||||
impl Command for SetUnderlineColor {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
let color = self.0;
|
||||
|
||||
if color == CColor::Reset {
|
||||
write!(f, "\x1b[59m")?;
|
||||
return Ok(());
|
||||
}
|
||||
f.write_str("\x1b[58:")?;
|
||||
|
||||
let res = match color {
|
||||
CColor::Black => f.write_str("5:0"),
|
||||
CColor::DarkGrey => f.write_str("5:8"),
|
||||
CColor::Red => f.write_str("5:9"),
|
||||
CColor::DarkRed => f.write_str("5:1"),
|
||||
CColor::Green => f.write_str("5:10"),
|
||||
CColor::DarkGreen => f.write_str("5:2"),
|
||||
CColor::Yellow => f.write_str("5:11"),
|
||||
CColor::DarkYellow => f.write_str("5:3"),
|
||||
CColor::Blue => f.write_str("5:12"),
|
||||
CColor::DarkBlue => f.write_str("5:4"),
|
||||
CColor::Magenta => f.write_str("5:13"),
|
||||
CColor::DarkMagenta => f.write_str("5:5"),
|
||||
CColor::Cyan => f.write_str("5:14"),
|
||||
CColor::DarkCyan => f.write_str("5:6"),
|
||||
CColor::White => f.write_str("5:15"),
|
||||
CColor::Grey => f.write_str("5:7"),
|
||||
CColor::Rgb { r, g, b } => write!(f, "2::{}:{}:{}", r, g, b),
|
||||
CColor::AnsiValue(val) => write!(f, "5:{}", val),
|
||||
_ => Ok(()),
|
||||
};
|
||||
res?;
|
||||
write!(f, "m")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"SetUnderlineColor not supported by winapi.",
|
||||
))
|
||||
}
|
||||
}
|
@@ -6,10 +6,10 @@ use crate::{buffer::Cell, terminal::Config};
|
||||
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
#[cfg(feature = "termina")]
|
||||
mod termina;
|
||||
#[cfg(feature = "termina")]
|
||||
pub use self::termina::TerminaBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
@@ -17,13 +17,11 @@ pub use self::test::TestBackend;
|
||||
/// Representation of a terminal backend.
|
||||
pub trait Backend {
|
||||
/// Claims the terminal for TUI use.
|
||||
fn claim(&mut self, config: Config) -> Result<(), io::Error>;
|
||||
fn claim(&mut self) -> Result<(), io::Error>;
|
||||
/// Update terminal configuration.
|
||||
fn reconfigure(&mut self, config: Config) -> Result<(), io::Error>;
|
||||
/// Restores the terminal to a normal state, undoes `claim`
|
||||
fn restore(&mut self, config: Config) -> Result<(), io::Error>;
|
||||
/// Forcibly resets the terminal, ignoring errors and configuration
|
||||
fn force_restore() -> Result<(), io::Error>;
|
||||
fn restore(&mut self) -> Result<(), io::Error>;
|
||||
/// Draws styled text to the terminal
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
@@ -32,8 +30,6 @@ pub trait Backend {
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
/// Sets the cursor to the given shape
|
||||
fn show_cursor(&mut self, kind: CursorKind) -> Result<(), io::Error>;
|
||||
/// Gets the current position of the cursor
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
/// Sets the cursor to the given position
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
/// Clears the terminal
|
||||
@@ -42,4 +38,5 @@ pub trait Backend {
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
/// Flushes the terminal buffer
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
fn supports_true_color(&self) -> bool;
|
||||
}
|
||||
|
626
helix-tui/src/backend/termina.rs
Normal file
626
helix-tui/src/backend/termina.rs
Normal file
@@ -0,0 +1,626 @@
|
||||
use std::io::{self, Write as _};
|
||||
|
||||
use helix_view::{
|
||||
graphics::{CursorKind, Rect, UnderlineStyle},
|
||||
theme::{Color, Modifier},
|
||||
};
|
||||
use termina::{
|
||||
escape::{
|
||||
csi::{self, Csi, SgrAttributes, SgrModifiers},
|
||||
dcs::{self, Dcs},
|
||||
},
|
||||
style::{CursorStyle, RgbColor},
|
||||
Event, OneBased, PlatformTerminal, Terminal as _, WindowSize,
|
||||
};
|
||||
use termini::TermInfo;
|
||||
|
||||
use crate::{buffer::Cell, terminal::Config};
|
||||
|
||||
use super::Backend;
|
||||
|
||||
// These macros are helpers to set/unset modes like bracketed paste or enter/exit the alternate
|
||||
// screen.
|
||||
macro_rules! decset {
|
||||
($mode:ident) => {
|
||||
Csi::Mode(csi::Mode::SetDecPrivateMode(csi::DecPrivateMode::Code(
|
||||
csi::DecPrivateModeCode::$mode,
|
||||
)))
|
||||
};
|
||||
}
|
||||
macro_rules! decreset {
|
||||
($mode:ident) => {
|
||||
Csi::Mode(csi::Mode::ResetDecPrivateMode(csi::DecPrivateMode::Code(
|
||||
csi::DecPrivateModeCode::$mode,
|
||||
)))
|
||||
};
|
||||
}
|
||||
|
||||
fn term_program() -> Option<String> {
|
||||
// Some terminals don't set $TERM_PROGRAM
|
||||
match std::env::var("TERM_PROGRAM") {
|
||||
Err(_) => std::env::var("TERM").ok(),
|
||||
Ok(term_program) => Some(term_program),
|
||||
}
|
||||
}
|
||||
fn vte_version() -> Option<usize> {
|
||||
std::env::var("VTE_VERSION").ok()?.parse().ok()
|
||||
}
|
||||
fn reset_cursor_approach(terminfo: TermInfo) -> String {
|
||||
let mut reset_str = Csi::Cursor(csi::Cursor::CursorStyle(CursorStyle::Default)).to_string();
|
||||
|
||||
if let Some(termini::Value::Utf8String(se_str)) = terminfo.extended_cap("Se") {
|
||||
reset_str.push_str(se_str);
|
||||
};
|
||||
|
||||
reset_str.push_str(
|
||||
terminfo
|
||||
.utf8_string_cap(termini::StringCapability::CursorNormal)
|
||||
.unwrap_or(""),
|
||||
);
|
||||
|
||||
reset_str
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct Capabilities {
|
||||
kitty_keyboard: KittyKeyboardSupport,
|
||||
synchronized_output: bool,
|
||||
true_color: bool,
|
||||
extended_underlines: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum KittyKeyboardSupport {
|
||||
/// The terminal doesn't support the protocol.
|
||||
#[default]
|
||||
None,
|
||||
/// The terminal supports the protocol but we haven't checked yet whether it has full or
|
||||
/// partial support for the flags we require.
|
||||
Some,
|
||||
/// The terminal only supports some of the flags we require.
|
||||
Partial,
|
||||
/// The terminal supports all flags require.
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TerminaBackend {
|
||||
terminal: PlatformTerminal,
|
||||
config: Config,
|
||||
capabilities: Capabilities,
|
||||
reset_cursor_command: String,
|
||||
is_synchronized_output_set: bool,
|
||||
}
|
||||
|
||||
impl TerminaBackend {
|
||||
pub fn new(config: Config) -> io::Result<Self> {
|
||||
let mut terminal = PlatformTerminal::new()?;
|
||||
let (capabilities, reset_cursor_command) =
|
||||
Self::detect_capabilities(&mut terminal, &config)?;
|
||||
|
||||
// In the case of a panic, reset the terminal eagerly. If we didn't do this and instead
|
||||
// relied on `Drop`, the backtrace would be lost because it is printed before we would
|
||||
// clear and exit the alternate screen.
|
||||
let hook_reset_cursor_command = reset_cursor_command.clone();
|
||||
terminal.set_panic_hook(move |term| {
|
||||
let _ = write!(
|
||||
term,
|
||||
"{}{}{}{}{}{}{}{}{}{}{}",
|
||||
Csi::Keyboard(csi::Keyboard::PopFlags(1)),
|
||||
decreset!(MouseTracking),
|
||||
decreset!(ButtonEventMouse),
|
||||
decreset!(AnyEventMouse),
|
||||
decreset!(RXVTMouse),
|
||||
decreset!(SGRMouse),
|
||||
&hook_reset_cursor_command,
|
||||
decreset!(BracketedPaste),
|
||||
decreset!(FocusTracking),
|
||||
Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)),
|
||||
decreset!(ClearAndEnableAlternateScreen),
|
||||
);
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
terminal,
|
||||
config,
|
||||
capabilities,
|
||||
reset_cursor_command,
|
||||
is_synchronized_output_set: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn terminal(&self) -> &PlatformTerminal {
|
||||
&self.terminal
|
||||
}
|
||||
|
||||
fn detect_capabilities(
|
||||
terminal: &mut PlatformTerminal,
|
||||
config: &Config,
|
||||
) -> io::Result<(Capabilities, String)> {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// Colibri "midnight"
|
||||
const TEST_COLOR: RgbColor = RgbColor::new(59, 34, 76);
|
||||
|
||||
terminal.enter_raw_mode()?;
|
||||
|
||||
let mut capabilities = Capabilities::default();
|
||||
let start = Instant::now();
|
||||
|
||||
// Many terminal extensions can be detected by querying the terminal for the state of the
|
||||
// extension and then sending a request for the primary device attributes (which is
|
||||
// consistently supported by all terminals). If we receive the status of the feature (for
|
||||
// example the current Kitty keyboard flags) then we know that the feature is supported.
|
||||
// If we only receive the device attributes then we know it is not.
|
||||
write!(
|
||||
terminal,
|
||||
"{}{}{}{}{}{}{}",
|
||||
// Kitty keyboard
|
||||
Csi::Keyboard(csi::Keyboard::QueryFlags),
|
||||
// Synchronized output
|
||||
Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code(
|
||||
csi::DecPrivateModeCode::SynchronizedOutput
|
||||
))),
|
||||
// True color and while we're at it, extended underlines:
|
||||
// <https://github.com/termstandard/colors?tab=readme-ov-file#querying-the-terminal>
|
||||
Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())),
|
||||
Csi::Sgr(csi::Sgr::UnderlineColor(TEST_COLOR.into())),
|
||||
Dcs::Request(dcs::DcsRequest::GraphicRendition),
|
||||
Csi::Sgr(csi::Sgr::Reset),
|
||||
// Finally request the primary device attributes
|
||||
Csi::Device(csi::Device::RequestPrimaryDeviceAttributes),
|
||||
)?;
|
||||
terminal.flush()?;
|
||||
|
||||
let device_attributes = |event: &Event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::Csi(Csi::Device(csi::Device::DeviceAttributes(_)))
|
||||
)
|
||||
};
|
||||
// TODO: tune this poll constant? Does it need to be longer when on an SSH connection?
|
||||
let poll_duration = Duration::from_millis(100);
|
||||
if terminal.poll(device_attributes, Some(poll_duration))? {
|
||||
while terminal.poll(Event::is_escape, Some(Duration::ZERO))? {
|
||||
match terminal.read(Event::is_escape)? {
|
||||
Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(_))) => {
|
||||
capabilities.kitty_keyboard = KittyKeyboardSupport::Some;
|
||||
}
|
||||
Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode {
|
||||
mode: csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SynchronizedOutput),
|
||||
setting: csi::DecModeSetting::Set | csi::DecModeSetting::Reset,
|
||||
})) => {
|
||||
capabilities.synchronized_output = true;
|
||||
}
|
||||
Event::Dcs(dcs::Dcs::Response {
|
||||
value: dcs::DcsResponse::GraphicRendition(sgrs),
|
||||
..
|
||||
}) => {
|
||||
capabilities.true_color =
|
||||
sgrs.contains(&csi::Sgr::Background(TEST_COLOR.into()));
|
||||
capabilities.extended_underlines =
|
||||
sgrs.contains(&csi::Sgr::UnderlineColor(TEST_COLOR.into()));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let end = Instant::now();
|
||||
log::debug!(
|
||||
"Detected terminal capabilities in {:?}: {capabilities:?}",
|
||||
end.duration_since(start)
|
||||
);
|
||||
} else {
|
||||
log::debug!("Failed to detect terminal capabilities within {poll_duration:?}. Using default capabilities only");
|
||||
}
|
||||
|
||||
capabilities.extended_underlines |= config.force_enable_extended_underlines;
|
||||
|
||||
let reset_cursor_approach = if let Ok(t) = termini::TermInfo::from_env() {
|
||||
capabilities.extended_underlines |= t.extended_cap("Smulx").is_some()
|
||||
|| t.extended_cap("Su").is_some()
|
||||
|| vte_version() >= Some(5102)
|
||||
// HACK: once WezTerm can support DECRQSS/DECRPSS for SGR we can remove this line.
|
||||
// <https://github.com/wezterm/wezterm/pull/6856>
|
||||
|| matches!(term_program().as_deref(), Some("WezTerm"));
|
||||
|
||||
reset_cursor_approach(t)
|
||||
} else {
|
||||
Csi::Cursor(csi::Cursor::CursorStyle(CursorStyle::Default)).to_string()
|
||||
};
|
||||
|
||||
terminal.enter_cooked_mode()?;
|
||||
|
||||
Ok((capabilities, reset_cursor_approach))
|
||||
}
|
||||
|
||||
fn enable_mouse_capture(&mut self) -> io::Result<()> {
|
||||
if self.config.enable_mouse_capture {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}{}{}{}{}",
|
||||
decset!(MouseTracking),
|
||||
decset!(ButtonEventMouse),
|
||||
decset!(AnyEventMouse),
|
||||
decset!(RXVTMouse),
|
||||
decset!(SGRMouse),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disable_mouse_capture(&mut self) -> io::Result<()> {
|
||||
if self.config.enable_mouse_capture {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}{}{}{}{}",
|
||||
decreset!(MouseTracking),
|
||||
decreset!(ButtonEventMouse),
|
||||
decreset!(AnyEventMouse),
|
||||
decreset!(RXVTMouse),
|
||||
decreset!(SGRMouse),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enable_extensions(&mut self) -> io::Result<()> {
|
||||
const KEYBOARD_FLAGS: csi::KittyKeyboardFlags =
|
||||
csi::KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
.union(csi::KittyKeyboardFlags::REPORT_ALTERNATE_KEYS);
|
||||
|
||||
match self.capabilities.kitty_keyboard {
|
||||
KittyKeyboardSupport::None | KittyKeyboardSupport::Partial => (),
|
||||
KittyKeyboardSupport::Full => {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Keyboard(csi::Keyboard::PushFlags(KEYBOARD_FLAGS))
|
||||
)?;
|
||||
}
|
||||
KittyKeyboardSupport::Some => {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}{}",
|
||||
// Enable the flags we need.
|
||||
Csi::Keyboard(csi::Keyboard::PushFlags(KEYBOARD_FLAGS)),
|
||||
// Then request the current flags. We need to check if the terminal enabled
|
||||
// all of the flags we require.
|
||||
Csi::Keyboard(csi::Keyboard::QueryFlags),
|
||||
)?;
|
||||
self.terminal.flush()?;
|
||||
|
||||
let event = self.terminal.read(|event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(_)))
|
||||
)
|
||||
})?;
|
||||
let Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(flags))) = event else {
|
||||
unreachable!();
|
||||
};
|
||||
if flags != KEYBOARD_FLAGS {
|
||||
log::info!("Turning off enhanced keyboard support because the terminal enabled different flags. Requested {KEYBOARD_FLAGS:?} but got {flags:?}");
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Keyboard(csi::Keyboard::PopFlags(1))
|
||||
)?;
|
||||
self.terminal.flush()?;
|
||||
self.capabilities.kitty_keyboard = KittyKeyboardSupport::Partial;
|
||||
} else {
|
||||
log::debug!(
|
||||
"The terminal fully supports the requested keyboard enhancement flags"
|
||||
);
|
||||
self.capabilities.kitty_keyboard = KittyKeyboardSupport::Full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disable_extensions(&mut self) -> io::Result<()> {
|
||||
if self.capabilities.kitty_keyboard == KittyKeyboardSupport::Full {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Keyboard(csi::Keyboard::PopFlags(1))
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// See <https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036>.
|
||||
// Synchronized output sequences tell the terminal when we are "starting to render" and
|
||||
// stopping, enabling to make better choices about when it draws a frame. This avoids all
|
||||
// kinds of ugly visual artifacts like tearing and flashing (i.e. the background color
|
||||
// after clearing the terminal).
|
||||
|
||||
fn start_synchronized_render(&mut self) -> io::Result<()> {
|
||||
if self.capabilities.synchronized_output && !self.is_synchronized_output_set {
|
||||
write!(self.terminal, "{}", decset!(SynchronizedOutput))?;
|
||||
self.is_synchronized_output_set = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end_sychronized_render(&mut self) -> io::Result<()> {
|
||||
if self.is_synchronized_output_set {
|
||||
write!(self.terminal, "{}", decreset!(SynchronizedOutput))?;
|
||||
self.is_synchronized_output_set = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TerminaBackend {
|
||||
fn claim(&mut self) -> io::Result<()> {
|
||||
self.terminal.enter_raw_mode()?;
|
||||
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}{}{}{}",
|
||||
// Enter an alternate screen.
|
||||
decset!(ClearAndEnableAlternateScreen),
|
||||
decset!(BracketedPaste),
|
||||
decset!(FocusTracking),
|
||||
// Clear the buffer. `ClearAndEnableAlternateScreen` **should** do this but some
|
||||
// things like mosh are buggy. See <https://github.com/helix-editor/helix/pull/1944>.
|
||||
Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)),
|
||||
)?;
|
||||
self.enable_mouse_capture()?;
|
||||
self.enable_extensions()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reconfigure(&mut self, mut config: Config) -> io::Result<()> {
|
||||
std::mem::swap(&mut self.config, &mut config);
|
||||
if self.config.enable_mouse_capture != config.enable_mouse_capture {
|
||||
if self.config.enable_mouse_capture {
|
||||
self.enable_mouse_capture()?;
|
||||
} else {
|
||||
self.disable_mouse_capture()?;
|
||||
}
|
||||
}
|
||||
self.capabilities.extended_underlines |= self.config.force_enable_extended_underlines;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore(&mut self) -> io::Result<()> {
|
||||
self.disable_extensions()?;
|
||||
self.disable_mouse_capture()?;
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}{}{}{}",
|
||||
&self.reset_cursor_command,
|
||||
decreset!(BracketedPaste),
|
||||
decreset!(FocusTracking),
|
||||
decreset!(ClearAndEnableAlternateScreen),
|
||||
)?;
|
||||
self.terminal.flush()?;
|
||||
self.terminal.enter_cooked_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
self.start_synchronized_render()?;
|
||||
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut underline_style = UnderlineStyle::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Cursor(csi::Cursor::Position {
|
||||
col: OneBased::from_zero_based(x),
|
||||
line: OneBased::from_zero_based(y),
|
||||
})
|
||||
)?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
|
||||
let mut attributes = SgrAttributes::default();
|
||||
if cell.fg != fg {
|
||||
attributes.foreground = Some(cell.fg.into());
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
attributes.background = Some(cell.bg.into());
|
||||
bg = cell.bg;
|
||||
}
|
||||
if cell.modifier != modifier {
|
||||
attributes.modifiers = diff_modifiers(modifier, cell.modifier);
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
|
||||
// Set underline style and color separately from SgrAttributes. Some terminals seem
|
||||
// to not like underline colors and styles being intermixed with other SGRs.
|
||||
let mut new_underline_style = cell.underline_style;
|
||||
if self.capabilities.extended_underlines {
|
||||
if cell.underline_color != underline_color {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Sgr(csi::Sgr::UnderlineColor(cell.underline_color.into()))
|
||||
)?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
} else {
|
||||
match new_underline_style {
|
||||
UnderlineStyle::Reset | UnderlineStyle::Line => (),
|
||||
_ => new_underline_style = UnderlineStyle::Line,
|
||||
}
|
||||
}
|
||||
if new_underline_style != underline_style {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Sgr(csi::Sgr::Underline(new_underline_style.into()))
|
||||
)?;
|
||||
underline_style = new_underline_style;
|
||||
}
|
||||
|
||||
// `attributes` will be empty if nothing changed between two cells. Empty
|
||||
// `SgrAttributes` behave the same as a `Sgr::Reset` rather than a 'no-op' though so
|
||||
// we should avoid writing them if they're empty.
|
||||
if !attributes.is_empty() {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Sgr(csi::Sgr::Attributes(attributes))
|
||||
)?;
|
||||
}
|
||||
|
||||
write!(self.terminal, "{}", &cell.symbol)?;
|
||||
}
|
||||
|
||||
write!(self.terminal, "{}", Csi::Sgr(csi::Sgr::Reset))?;
|
||||
|
||||
self.end_sychronized_render()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.terminal, "{}", decreset!(ShowCursor))?;
|
||||
self.flush()
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
|
||||
let style = match kind {
|
||||
CursorKind::Block => CursorStyle::SteadyBlock,
|
||||
CursorKind::Bar => CursorStyle::SteadyBar,
|
||||
CursorKind::Underline => CursorStyle::SteadyUnderline,
|
||||
CursorKind::Hidden => unreachable!(),
|
||||
};
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}{}",
|
||||
decset!(ShowCursor),
|
||||
Csi::Cursor(csi::Cursor::CursorStyle(style)),
|
||||
)?;
|
||||
self.flush()
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
let col = OneBased::from_zero_based(x);
|
||||
let line = OneBased::from_zero_based(y);
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Cursor(csi::Cursor::Position { line, col })
|
||||
)?;
|
||||
self.flush()
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.start_synchronized_render()?;
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay))
|
||||
)?;
|
||||
self.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let WindowSize { rows, cols, .. } = self.terminal.get_dimensions()?;
|
||||
Ok(Rect::new(0, 0, cols, rows))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.terminal.flush()
|
||||
}
|
||||
|
||||
fn supports_true_color(&self) -> bool {
|
||||
self.capabilities.true_color
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminaBackend {
|
||||
fn drop(&mut self) {
|
||||
// Avoid resetting the terminal while panicking because we set a panic hook above in
|
||||
// `Self::new`.
|
||||
if !std::thread::panicking() {
|
||||
let _ = self.disable_extensions();
|
||||
let _ = self.disable_mouse_capture();
|
||||
let _ = write!(
|
||||
self.terminal,
|
||||
"{}{}{}{}",
|
||||
&self.reset_cursor_command,
|
||||
decreset!(BracketedPaste),
|
||||
decreset!(FocusTracking),
|
||||
decreset!(ClearAndEnableAlternateScreen),
|
||||
);
|
||||
// NOTE: Drop for Platform terminal resets the mode and flushes the buffer when not
|
||||
// panicking.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_modifiers(from: Modifier, to: Modifier) -> SgrModifiers {
|
||||
let mut modifiers = SgrModifiers::default();
|
||||
|
||||
let removed = from - to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
modifiers |= SgrModifiers::NO_REVERSE;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) && !to.contains(Modifier::DIM) {
|
||||
modifiers |= SgrModifiers::INTENSITY_NORMAL;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
modifiers |= SgrModifiers::INTENSITY_NORMAL;
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
modifiers |= SgrModifiers::NO_ITALIC;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
modifiers |= SgrModifiers::NO_STRIKE_THROUGH;
|
||||
}
|
||||
if removed.contains(Modifier::HIDDEN) {
|
||||
modifiers |= SgrModifiers::NO_INVISIBLE;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
modifiers |= SgrModifiers::BLINK_NONE;
|
||||
}
|
||||
|
||||
let added = to - from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
modifiers |= SgrModifiers::REVERSE;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
modifiers |= SgrModifiers::INTENSITY_BOLD;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
modifiers |= SgrModifiers::INTENSITY_DIM;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
modifiers |= SgrModifiers::ITALIC;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
modifiers |= SgrModifiers::STRIKE_THROUGH;
|
||||
}
|
||||
if added.contains(Modifier::HIDDEN) {
|
||||
modifiers |= SgrModifiers::INVISIBLE;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
modifiers |= SgrModifiers::BLINK_SLOW;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
modifiers |= SgrModifiers::BLINK_RAPID;
|
||||
}
|
||||
|
||||
modifiers
|
||||
}
|
@@ -107,7 +107,7 @@ impl TestBackend {
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn claim(&mut self, _config: Config) -> Result<(), io::Error> {
|
||||
fn claim(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -115,11 +115,7 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore(&mut self, _config: Config) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn force_restore() -> Result<(), io::Error> {
|
||||
fn restore(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -143,10 +139,6 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
@@ -164,4 +156,8 @@ impl Backend for TestBackend {
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn supports_true_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@@ -1,133 +1,3 @@
|
||||
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
//! ## Adding `tui` as a dependency
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! tui = "0.15"
|
||||
//! crossterm = "0.19"
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//!
|
||||
//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
|
||||
//! abstraction over available backends that provides basic functionalities such as clearing the
|
||||
//! screen, hiding the cursor, etc.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use helix_tui::Terminal;
|
||||
//! use helix_tui::backend::CrosstermBackend;
|
||||
//! use helix_view::editor::Config;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let config = Config::default();
|
||||
//! let backend = CrosstermBackend::new(stdout, &config);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! You may also refer to the examples to find out how to create a `Terminal` for each available
|
||||
//! backend.
|
||||
//!
|
||||
//! ## Building a User Interface (UI)
|
||||
//!
|
||||
//! Every component of your interface will be implementing the `Widget` trait. The library comes
|
||||
//! with a predefined set of widgets that should meet most of your use cases. You are also free to
|
||||
//! implement your own.
|
||||
//!
|
||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||
//! to customize them. The widget is then rendered using the `Frame::render_widget` which take
|
||||
//! your widget instance an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use crossterm::terminal;
|
||||
//! use helix_tui::Terminal;
|
||||
//! use helix_tui::backend::CrosstermBackend;
|
||||
//! use helix_tui::widgets::{Widget, Block, Borders};
|
||||
//! use helix_tui::layout::{Layout, Constraint, Direction};
|
||||
//! use helix_view::editor::Config;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! terminal::enable_raw_mode().unwrap();
|
||||
//! let stdout = io::stdout();
|
||||
//! let config = Config::default();
|
||||
//! let backend = CrosstermBackend::new(stdout, &config);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! // terminal.draw(|f| {
|
||||
//! // let size = f.size();
|
||||
//! // let block = Block::default()
|
||||
//! // .title("Block")
|
||||
//! // .borders(Borders::ALL);
|
||||
//! // f.render_widget(block, size);
|
||||
//! // })?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Layout
|
||||
//!
|
||||
//! The library comes with a basic yet useful layout management object called `Layout`. As you may
|
||||
//! see below and in the examples, the library makes heavy use of the builder pattern to provide
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use crossterm::terminal;
|
||||
//! use helix_tui::Terminal;
|
||||
//! use helix_tui::backend::CrosstermBackend;
|
||||
//! use helix_tui::widgets::{Widget, Block, Borders};
|
||||
//! use helix_tui::layout::{Layout, Constraint, Direction};
|
||||
//! use helix_view::editor::Config;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! terminal::enable_raw_mode().unwrap();
|
||||
//! let stdout = io::stdout();
|
||||
//! let config = Config::default();
|
||||
//! let backend = CrosstermBackend::new(stdout, &config);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! // terminal.draw(|f| {
|
||||
//! // let chunks = Layout::default()
|
||||
//! // .direction(Direction::Vertical)
|
||||
//! // .margin(1)
|
||||
//! // .constraints(
|
||||
//! // [
|
||||
//! // Constraint::Percentage(10),
|
||||
//! // Constraint::Percentage(80),
|
||||
//! // Constraint::Percentage(10)
|
||||
//! // ].as_ref()
|
||||
//! // )
|
||||
//! // .split(f.size());
|
||||
//! // let block = Block::default()
|
||||
//! // .title("Block")
|
||||
//! // .borders(Borders::ALL);
|
||||
//! // f.render_widget(block, chunks[0]);
|
||||
//! // let block = Block::default()
|
||||
//! // .title("Block 2")
|
||||
//! // .borders(Borders::ALL);
|
||||
//! // f.render_widget(block, chunks[1]);
|
||||
//! // })?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This let you describe responsive terminal UI by nesting layouts. You should note that by
|
||||
//! default the computed layout tries to fill the available space completely. So if for any reason
|
||||
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
|
||||
//! corresponding area.
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
|
@@ -24,12 +24,14 @@ pub struct Viewport {
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub enable_mouse_capture: bool,
|
||||
pub force_enable_extended_underlines: bool,
|
||||
}
|
||||
|
||||
impl From<EditorConfig> for Config {
|
||||
fn from(config: EditorConfig) -> Self {
|
||||
impl From<&EditorConfig> for Config {
|
||||
fn from(config: &EditorConfig) -> Self {
|
||||
Self {
|
||||
enable_mouse_capture: config.mouse,
|
||||
force_enable_extended_underlines: config.undercurl,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,16 +104,16 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
pub fn claim(&mut self, config: Config) -> io::Result<()> {
|
||||
self.backend.claim(config)
|
||||
pub fn claim(&mut self) -> io::Result<()> {
|
||||
self.backend.claim()
|
||||
}
|
||||
|
||||
pub fn reconfigure(&mut self, config: Config) -> io::Result<()> {
|
||||
self.backend.reconfigure(config)
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, config: Config) -> io::Result<()> {
|
||||
self.backend.restore(config)
|
||||
pub fn restore(&mut self) -> io::Result<()> {
|
||||
self.backend.restore()
|
||||
}
|
||||
|
||||
// /// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
@@ -218,10 +220,6 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
|
||||
parking_lot.workspace = true
|
||||
arc-swap = { version = "1.7.1" }
|
||||
|
||||
gix = { version = "0.72.1", features = ["attributes", "status"], default-features = false, optional = true }
|
||||
gix = { version = "0.73.0", features = ["attributes", "status"], default-features = false, optional = true }
|
||||
imara-diff = "0.2.0"
|
||||
anyhow = "1"
|
||||
|
||||
|
@@ -12,7 +12,7 @@ homepage.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
term = ["crossterm"]
|
||||
term = ["termina"]
|
||||
unicode-lines = []
|
||||
|
||||
[dependencies]
|
||||
@@ -26,7 +26,7 @@ helix-vcs = { path = "../helix-vcs" }
|
||||
|
||||
bitflags.workspace = true
|
||||
anyhow = "1"
|
||||
crossterm = { version = "0.28", optional = true }
|
||||
termina = { workspace = true, optional = true }
|
||||
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -52,6 +52,8 @@ log = "~0.4"
|
||||
parking_lot.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
kstring = "2.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
clipboard-win = { version = "5.4", features = ["std"] }
|
||||
|
||||
|
@@ -109,7 +109,7 @@ impl InlineDiagnosticsConfig {
|
||||
impl Default for InlineDiagnosticsConfig {
|
||||
fn default() -> Self {
|
||||
InlineDiagnosticsConfig {
|
||||
cursor_line: DiagnosticFilter::Disable,
|
||||
cursor_line: DiagnosticFilter::Enable(Severity::Warning),
|
||||
other_lines: DiagnosticFilter::Disable,
|
||||
min_diagnostic_width: 40,
|
||||
prefix_len: 1,
|
||||
|
@@ -1,163 +0,0 @@
|
||||
// A minimal base64 implementation to keep from pulling in a crate for just that. It's based on
|
||||
// https://github.com/marshallpierce/rust-base64 but without all the customization options.
|
||||
// The biggest portion comes from
|
||||
// https://github.com/marshallpierce/rust-base64/blob/a675443d327e175f735a37f574de803d6a332591/src/engine/naive.rs#L42
|
||||
// Thanks, rust-base64!
|
||||
|
||||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2015 Alice Maz
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
use std::ops::{BitAnd, BitOr, Shl, Shr};
|
||||
|
||||
const PAD_BYTE: u8 = b'=';
|
||||
const ENCODE_TABLE: &[u8] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes();
|
||||
const LOW_SIX_BITS: u32 = 0x3F;
|
||||
|
||||
pub fn encode(input: &[u8]) -> String {
|
||||
let rem = input.len() % 3;
|
||||
let complete_chunks = input.len() / 3;
|
||||
let remainder_chunk = usize::from(rem != 0);
|
||||
let encoded_size = (complete_chunks + remainder_chunk) * 4;
|
||||
|
||||
let mut output = vec![0; encoded_size];
|
||||
|
||||
// complete chunks first
|
||||
let complete_chunk_len = input.len() - rem;
|
||||
|
||||
let mut input_index = 0_usize;
|
||||
let mut output_index = 0_usize;
|
||||
while input_index < complete_chunk_len {
|
||||
let chunk = &input[input_index..input_index + 3];
|
||||
|
||||
// populate low 24 bits from 3 bytes
|
||||
let chunk_int: u32 =
|
||||
(chunk[0] as u32).shl(16) | (chunk[1] as u32).shl(8) | (chunk[2] as u32);
|
||||
// encode 4x 6-bit output bytes
|
||||
output[output_index] = ENCODE_TABLE[chunk_int.shr(18) as usize];
|
||||
output[output_index + 1] = ENCODE_TABLE[chunk_int.shr(12_u8).bitand(LOW_SIX_BITS) as usize];
|
||||
output[output_index + 2] = ENCODE_TABLE[chunk_int.shr(6_u8).bitand(LOW_SIX_BITS) as usize];
|
||||
output[output_index + 3] = ENCODE_TABLE[chunk_int.bitand(LOW_SIX_BITS) as usize];
|
||||
|
||||
input_index += 3;
|
||||
output_index += 4;
|
||||
}
|
||||
|
||||
// then leftovers
|
||||
if rem == 2 {
|
||||
let chunk = &input[input_index..input_index + 2];
|
||||
|
||||
// high six bits of chunk[0]
|
||||
output[output_index] = ENCODE_TABLE[chunk[0].shr(2) as usize];
|
||||
// bottom 2 bits of [0], high 4 bits of [1]
|
||||
output[output_index + 1] = ENCODE_TABLE
|
||||
[(chunk[0].shl(4_u8).bitor(chunk[1].shr(4_u8)) as u32).bitand(LOW_SIX_BITS) as usize];
|
||||
// bottom 4 bits of [1], with the 2 bottom bits as zero
|
||||
output[output_index + 2] =
|
||||
ENCODE_TABLE[(chunk[1].shl(2_u8) as u32).bitand(LOW_SIX_BITS) as usize];
|
||||
output[output_index + 3] = PAD_BYTE;
|
||||
} else if rem == 1 {
|
||||
let byte = input[input_index];
|
||||
output[output_index] = ENCODE_TABLE[byte.shr(2) as usize];
|
||||
output[output_index + 1] =
|
||||
ENCODE_TABLE[(byte.shl(4_u8) as u32).bitand(LOW_SIX_BITS) as usize];
|
||||
output[output_index + 2] = PAD_BYTE;
|
||||
output[output_index + 3] = PAD_BYTE;
|
||||
}
|
||||
String::from_utf8(output).expect("Invalid UTF8")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn compare_encode(expected: &str, target: &[u8]) {
|
||||
assert_eq!(expected, super::encode(target));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rfc4648_0() {
|
||||
compare_encode("", b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rfc4648_1() {
|
||||
compare_encode("Zg==", b"f");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rfc4648_2() {
|
||||
compare_encode("Zm8=", b"fo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rfc4648_3() {
|
||||
compare_encode("Zm9v", b"foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rfc4648_4() {
|
||||
compare_encode("Zm9vYg==", b"foob");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rfc4648_5() {
|
||||
compare_encode("Zm9vYmE=", b"fooba");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rfc4648_6() {
|
||||
compare_encode("Zm9vYmFy", b"foobar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_all_ascii() {
|
||||
let mut ascii = Vec::<u8>::with_capacity(128);
|
||||
|
||||
for i in 0..128 {
|
||||
ascii.push(i);
|
||||
}
|
||||
|
||||
compare_encode(
|
||||
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
|
||||
D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8\
|
||||
=",
|
||||
&ascii,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_all_bytes() {
|
||||
let mut bytes = Vec::<u8>::with_capacity(256);
|
||||
|
||||
for i in 0..255 {
|
||||
bytes.push(i);
|
||||
}
|
||||
bytes.push(255); //bug with "overflowing" ranges?
|
||||
|
||||
compare_encode(
|
||||
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
|
||||
D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn\
|
||||
+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6\
|
||||
/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==",
|
||||
&bytes,
|
||||
);
|
||||
}
|
||||
}
|
@@ -292,10 +292,17 @@ mod external {
|
||||
},
|
||||
#[cfg(feature = "term")]
|
||||
Self::Termcode => {
|
||||
crossterm::queue!(
|
||||
std::io::stdout(),
|
||||
osc52::SetClipboardCommand::new(content, clipboard_type)
|
||||
)?;
|
||||
use std::io::Write;
|
||||
use termina::escape::osc::{self, Osc};
|
||||
let selection = match clipboard_type {
|
||||
ClipboardType::Clipboard => osc::Selection::CLIPBOARD,
|
||||
ClipboardType::Selection => osc::Selection::PRIMARY,
|
||||
};
|
||||
// NOTE: it would be ideal to have the terminal execute this but it _should_
|
||||
// work to send this over stdout instead.
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
write!(stdout, "{}", Osc::SetSelection(selection, content))?;
|
||||
stdout.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
Self::Custom(command_provider) => match clipboard_type {
|
||||
@@ -400,43 +407,6 @@ mod external {
|
||||
paste => "termux-clipboard-set";
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
mod osc52 {
|
||||
use {super::ClipboardType, crate::base64};
|
||||
|
||||
pub struct SetClipboardCommand {
|
||||
encoded_content: String,
|
||||
clipboard_type: ClipboardType,
|
||||
}
|
||||
|
||||
impl SetClipboardCommand {
|
||||
pub fn new(content: &str, clipboard_type: ClipboardType) -> Self {
|
||||
Self {
|
||||
encoded_content: base64::encode(content.as_bytes()),
|
||||
clipboard_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crossterm::Command for SetClipboardCommand {
|
||||
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
let kind = match &self.clipboard_type {
|
||||
ClipboardType::Clipboard => "c",
|
||||
ClipboardType::Selection => "p",
|
||||
};
|
||||
// Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/
|
||||
write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::result::Result<(), std::io::Error> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"OSC clipboard codes not supported by winapi.",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_command(
|
||||
cmd: &Command,
|
||||
input: Option<&str>,
|
||||
|
@@ -1810,6 +1810,12 @@ impl Document {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn word_completion_enabled(&self) -> bool {
|
||||
self.language_config()
|
||||
.and_then(|lang_config| lang_config.word_completion.and_then(|c| c.enable))
|
||||
.unwrap_or_else(|| self.config.load().word_completion.enable)
|
||||
}
|
||||
|
||||
pub fn path_completion_enabled(&self) -> bool {
|
||||
self.language_config()
|
||||
.and_then(|lang_config| lang_config.path_completion)
|
||||
|
@@ -278,6 +278,9 @@ pub struct Config {
|
||||
/// either absolute or relative to the current opened document or current working directory (if the buffer is not yet saved).
|
||||
/// Defaults to true.
|
||||
pub path_completion: bool,
|
||||
/// Configures completion of words from open buffers.
|
||||
/// Defaults to enabled with a trigger length of 7.
|
||||
pub word_completion: WordCompletion,
|
||||
/// Automatic formatting on save. Defaults to true.
|
||||
pub auto_format: bool,
|
||||
/// Default register used for yank/paste. Defaults to '"'
|
||||
@@ -376,6 +379,8 @@ pub struct Config {
|
||||
/// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to
|
||||
/// `true`.
|
||||
pub editor_config: bool,
|
||||
/// Whether to render rainbow colors for matching brackets. Defaults to `false`.
|
||||
pub rainbow_brackets: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
|
||||
@@ -625,6 +630,9 @@ pub enum StatusLineElement {
|
||||
|
||||
/// Indicator for selected register
|
||||
Register,
|
||||
|
||||
/// The base of current working directory
|
||||
CurrentWorkingDirectory,
|
||||
}
|
||||
|
||||
// Cursor shape is read and used on every rendered frame and so needs
|
||||
@@ -974,6 +982,22 @@ pub enum PopupBorderConfig {
|
||||
Menu,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct WordCompletion {
|
||||
pub enable: bool,
|
||||
pub trigger_length: NonZeroU8,
|
||||
}
|
||||
|
||||
impl Default for WordCompletion {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable: true,
|
||||
trigger_length: NonZeroU8::new(7).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -993,6 +1017,7 @@ impl Default for Config {
|
||||
auto_pairs: AutoPairConfig::default(),
|
||||
auto_completion: true,
|
||||
path_completion: true,
|
||||
word_completion: WordCompletion::default(),
|
||||
auto_format: true,
|
||||
default_yank_register: '"',
|
||||
auto_save: AutoSave::default(),
|
||||
@@ -1032,9 +1057,10 @@ impl Default for Config {
|
||||
indent_heuristic: IndentationHeuristic::default(),
|
||||
jump_label_alphabet: ('a'..='z').collect(),
|
||||
inline_diagnostics: InlineDiagnosticsConfig::default(),
|
||||
end_of_line_diagnostics: DiagnosticFilter::Disable,
|
||||
end_of_line_diagnostics: DiagnosticFilter::Enable(Severity::Hint),
|
||||
clipboard_provider: ClipboardProvider::default(),
|
||||
editor_config: true,
|
||||
rainbow_brackets: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1961,28 +1987,29 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn focus(&mut self, view_id: ViewId) {
|
||||
let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
|
||||
|
||||
// if leaving the view: mode should reset and the cursor should be
|
||||
// within view
|
||||
if prev_id != view_id {
|
||||
self.enter_normal_mode();
|
||||
self.ensure_cursor_in_view(view_id);
|
||||
|
||||
// Update jumplist selections with new document changes.
|
||||
for (view, _focused) in self.tree.views_mut() {
|
||||
let doc = doc_mut!(self, &view.doc);
|
||||
view.sync_changes(doc);
|
||||
}
|
||||
let view = view!(self, view_id);
|
||||
let doc = doc_mut!(self, &view.doc);
|
||||
doc.mark_as_focused();
|
||||
let focus_lost = self.tree.get(prev_id).doc;
|
||||
dispatch(DocumentFocusLost {
|
||||
editor: self,
|
||||
doc: focus_lost,
|
||||
});
|
||||
if self.tree.focus == view_id {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset mode to normal and ensure any pending changes are committed in the old document.
|
||||
self.enter_normal_mode();
|
||||
let (view, doc) = current!(self);
|
||||
doc.append_changes_to_history(view);
|
||||
self.ensure_cursor_in_view(view_id);
|
||||
// Update jumplist selections with new document changes.
|
||||
for (view, _focused) in self.tree.views_mut() {
|
||||
let doc = doc_mut!(self, &view.doc);
|
||||
view.sync_changes(doc);
|
||||
}
|
||||
|
||||
let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
|
||||
doc_mut!(self).mark_as_focused();
|
||||
|
||||
let focus_lost = self.tree.get(prev_id).doc;
|
||||
dispatch(DocumentFocusLost {
|
||||
editor: self,
|
||||
doc: focus_lost,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn focus_next(&mut self) {
|
||||
|
@@ -33,6 +33,10 @@ pub enum Variable {
|
||||
BufferName,
|
||||
/// A string containing the line-ending of the currently focused document.
|
||||
LineEnding,
|
||||
/// Curreng working directory
|
||||
CurrentWorkingDirectory,
|
||||
/// Nearest ancestor directory of the current working directory that contains `.git`, `.svn`, `jj` or `.helix`
|
||||
WorkspaceDirectory,
|
||||
// The name of current buffers language as set in `languages.toml`
|
||||
Language,
|
||||
// Primary selection
|
||||
@@ -49,6 +53,8 @@ impl Variable {
|
||||
Self::CursorColumn,
|
||||
Self::BufferName,
|
||||
Self::LineEnding,
|
||||
Self::CurrentWorkingDirectory,
|
||||
Self::WorkspaceDirectory,
|
||||
Self::Language,
|
||||
Self::Selection,
|
||||
Self::SelectionLineStart,
|
||||
@@ -61,6 +67,8 @@ impl Variable {
|
||||
Self::CursorColumn => "cursor_column",
|
||||
Self::BufferName => "buffer_name",
|
||||
Self::LineEnding => "line_ending",
|
||||
Self::CurrentWorkingDirectory => "current_working_directory",
|
||||
Self::WorkspaceDirectory => "workspace_directory",
|
||||
Self::Language => "language",
|
||||
Self::Selection => "selection",
|
||||
Self::SelectionLineStart => "selection_line_start",
|
||||
@@ -74,6 +82,8 @@ impl Variable {
|
||||
"cursor_column" => Some(Self::CursorColumn),
|
||||
"buffer_name" => Some(Self::BufferName),
|
||||
"line_ending" => Some(Self::LineEnding),
|
||||
"workspace_directory" => Some(Self::WorkspaceDirectory),
|
||||
"current_working_directory" => Some(Self::CurrentWorkingDirectory),
|
||||
"language" => Some(Self::Language),
|
||||
"selection" => Some(Self::Selection),
|
||||
"selection_line_start" => Some(Self::SelectionLineStart),
|
||||
@@ -235,6 +245,17 @@ fn expand_variable(editor: &Editor, variable: Variable) -> Result<Cow<'static, s
|
||||
}
|
||||
}
|
||||
Variable::LineEnding => Ok(Cow::Borrowed(doc.line_ending.as_str())),
|
||||
Variable::CurrentWorkingDirectory => Ok(std::borrow::Cow::Owned(
|
||||
helix_stdx::env::current_working_dir()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)),
|
||||
Variable::WorkspaceDirectory => Ok(std::borrow::Cow::Owned(
|
||||
helix_loader::find_workspace()
|
||||
.0
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)),
|
||||
Variable::Language => Ok(match doc.language_name() {
|
||||
Some(lang) => Cow::Owned(lang.to_owned()),
|
||||
None => Cow::Borrowed("text"),
|
||||
|
@@ -289,30 +289,28 @@ impl Color {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<Color> for crossterm::style::Color {
|
||||
impl From<Color> for termina::style::ColorSpec {
|
||||
fn from(color: Color) -> Self {
|
||||
use crossterm::style::Color as CColor;
|
||||
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::LightGray => CColor::Grey,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
Color::Reset => Self::Reset,
|
||||
Color::Black => Self::BLACK,
|
||||
Color::Red => Self::RED,
|
||||
Color::Green => Self::GREEN,
|
||||
Color::Yellow => Self::YELLOW,
|
||||
Color::Blue => Self::BLUE,
|
||||
Color::Magenta => Self::MAGENTA,
|
||||
Color::Cyan => Self::CYAN,
|
||||
Color::Gray => Self::BRIGHT_BLACK,
|
||||
Color::White => Self::WHITE,
|
||||
Color::LightRed => Self::BRIGHT_RED,
|
||||
Color::LightGreen => Self::BRIGHT_GREEN,
|
||||
Color::LightBlue => Self::BRIGHT_BLUE,
|
||||
Color::LightYellow => Self::BRIGHT_YELLOW,
|
||||
Color::LightMagenta => Self::BRIGHT_MAGENTA,
|
||||
Color::LightCyan => Self::BRIGHT_CYAN,
|
||||
Color::LightGray => Self::BRIGHT_WHITE,
|
||||
Color::Indexed(i) => Self::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => termina::style::RgbColor::new(r, g, b).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,15 +341,15 @@ impl FromStr for UnderlineStyle {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<UnderlineStyle> for crossterm::style::Attribute {
|
||||
impl From<UnderlineStyle> for termina::style::Underline {
|
||||
fn from(style: UnderlineStyle) -> Self {
|
||||
match style {
|
||||
UnderlineStyle::Line => crossterm::style::Attribute::Underlined,
|
||||
UnderlineStyle::Curl => crossterm::style::Attribute::Undercurled,
|
||||
UnderlineStyle::Dotted => crossterm::style::Attribute::Underdotted,
|
||||
UnderlineStyle::Dashed => crossterm::style::Attribute::Underdashed,
|
||||
UnderlineStyle::DoubleLine => crossterm::style::Attribute::DoubleUnderlined,
|
||||
UnderlineStyle::Reset => crossterm::style::Attribute::NoUnderline,
|
||||
UnderlineStyle::Reset => Self::None,
|
||||
UnderlineStyle::Line => Self::Single,
|
||||
UnderlineStyle::Curl => Self::Curly,
|
||||
UnderlineStyle::Dotted => Self::Dotted,
|
||||
UnderlineStyle::Dashed => Self::Dashed,
|
||||
UnderlineStyle::DoubleLine => Self::Double,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ pub mod completion;
|
||||
pub mod dap;
|
||||
pub mod diagnostics;
|
||||
pub mod lsp;
|
||||
pub mod word_index;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AutoSaveEvent {
|
||||
@@ -22,6 +23,7 @@ pub struct Handlers {
|
||||
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
|
||||
pub auto_save: Sender<AutoSaveEvent>,
|
||||
pub document_colors: Sender<lsp::DocumentColorsEvent>,
|
||||
pub word_index: word_index::Handler,
|
||||
}
|
||||
|
||||
impl Handlers {
|
||||
@@ -46,8 +48,13 @@ impl Handlers {
|
||||
};
|
||||
send_blocking(&self.signature_hints, event)
|
||||
}
|
||||
|
||||
pub fn word_index(&self) -> &word_index::WordIndex {
|
||||
&self.word_index.index
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_hooks(handlers: &Handlers) {
|
||||
lsp::register_hooks(handlers);
|
||||
word_index::register_hooks(handlers);
|
||||
}
|
||||
|
509
helix-view/src/handlers/word_index.rs
Normal file
509
helix-view/src/handlers/word_index.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
//! Indexing of words from open buffers.
|
||||
//!
|
||||
//! This provides an eventually consistent set of words used in any open buffers. This set is
|
||||
//! later used for lexical completion.
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap, iter, mem, sync::Arc, time::Duration};
|
||||
|
||||
use helix_core::{
|
||||
chars::char_is_word, fuzzy::fuzzy_match, movement, ChangeSet, Range, Rope, RopeSlice,
|
||||
};
|
||||
use helix_event::{register_hook, AsyncHook};
|
||||
use helix_stdx::rope::RopeSliceExt as _;
|
||||
use parking_lot::RwLock;
|
||||
use tokio::{sync::mpsc, time::Instant};
|
||||
|
||||
use crate::{
|
||||
events::{ConfigDidChange, DocumentDidChange, DocumentDidClose, DocumentDidOpen},
|
||||
DocumentId,
|
||||
};
|
||||
|
||||
use super::Handlers;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Change {
|
||||
old_text: Rope,
|
||||
text: Rope,
|
||||
changes: ChangeSet,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Event {
|
||||
Insert(Rope),
|
||||
Update(DocumentId, Change),
|
||||
Delete(DocumentId, Rope),
|
||||
/// Clear the entire word index.
|
||||
/// This is used to clear memory when the feature is turned off.
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Handler {
|
||||
pub(super) index: WordIndex,
|
||||
/// A sender into an async hook which debounces updates to the index.
|
||||
hook: mpsc::Sender<Event>,
|
||||
/// A sender to a tokio task which coordinates the indexing of documents.
|
||||
///
|
||||
/// See [WordIndex::run]. A supervisor-like task is in charge of spawning tasks to update the
|
||||
/// index. This ensures that consecutive edits to a document trigger the correct order of
|
||||
/// insertions and deletions into the word set.
|
||||
coordinator: mpsc::UnboundedSender<Event>,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub fn spawn() -> Self {
|
||||
let index = WordIndex::default();
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
tokio::spawn(index.clone().run(rx));
|
||||
Self {
|
||||
hook: Hook {
|
||||
changes: HashMap::default(),
|
||||
coordinator: tx.clone(),
|
||||
}
|
||||
.spawn(),
|
||||
index,
|
||||
coordinator: tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Hook {
|
||||
changes: HashMap<DocumentId, Change>,
|
||||
coordinator: mpsc::UnboundedSender<Event>,
|
||||
}
|
||||
|
||||
const DEBOUNCE: Duration = Duration::from_secs(1);
|
||||
|
||||
impl AsyncHook for Hook {
|
||||
type Event = Event;
|
||||
|
||||
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant> {
|
||||
match event {
|
||||
Event::Insert(_) => unreachable!("inserts are sent to the worker directly"),
|
||||
Event::Update(doc, change) => {
|
||||
if let Some(pending_change) = self.changes.get_mut(&doc) {
|
||||
// If there is already a change waiting for this document, merge the two
|
||||
// changes together by composing the changesets and saving the new `text`.
|
||||
pending_change.changes =
|
||||
mem::take(&mut pending_change.changes).compose(change.changes);
|
||||
pending_change.text = change.text;
|
||||
Some(Instant::now() + DEBOUNCE)
|
||||
} else if !is_changeset_significant(&change.changes) {
|
||||
// If the changeset is fairly large, debounce before updating the index.
|
||||
self.changes.insert(doc, change);
|
||||
Some(Instant::now() + DEBOUNCE)
|
||||
} else {
|
||||
// Otherwise if the change is small, queue the update to the index immediately.
|
||||
self.coordinator.send(Event::Update(doc, change)).unwrap();
|
||||
timeout
|
||||
}
|
||||
}
|
||||
Event::Delete(doc, text) => {
|
||||
// If there are pending changes that haven't been indexed since the last debounce,
|
||||
// forget them and delete the old text.
|
||||
if let Some(change) = self.changes.remove(&doc) {
|
||||
self.coordinator
|
||||
.send(Event::Delete(doc, change.old_text))
|
||||
.unwrap();
|
||||
} else {
|
||||
self.coordinator.send(Event::Delete(doc, text)).unwrap();
|
||||
}
|
||||
timeout
|
||||
}
|
||||
Event::Clear => unreachable!("clear is sent to the worker directly"),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
for (doc, change) in self.changes.drain() {
|
||||
self.coordinator.send(Event::Update(doc, change)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum number of grapheme clusters required to include a word in the index
|
||||
const MIN_WORD_GRAPHEMES: usize = 3;
|
||||
/// Maximum word length allowed (in chars)
|
||||
const MAX_WORD_LEN: usize = 50;
|
||||
|
||||
type Word = kstring::KString;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct WordIndexInner {
|
||||
/// Reference counted storage for words.
|
||||
///
|
||||
/// Words are very likely to be reused many times. Instead of storing duplicates we keep a
|
||||
/// reference count of times a word is used. When the reference count drops to zero the word
|
||||
/// is removed from the index.
|
||||
words: HashMap<Word, u32>,
|
||||
}
|
||||
|
||||
impl WordIndexInner {
|
||||
fn words(&self) -> impl Iterator<Item = &Word> {
|
||||
self.words.keys()
|
||||
}
|
||||
|
||||
fn insert(&mut self, word: RopeSlice) {
|
||||
let word: Cow<str> = word.into();
|
||||
if let Some(rc) = self.words.get_mut(word.as_ref()) {
|
||||
*rc = rc.saturating_add(1);
|
||||
} else {
|
||||
let word = match word {
|
||||
Cow::Owned(s) => Word::from_string(s),
|
||||
Cow::Borrowed(s) => Word::from_ref(s),
|
||||
};
|
||||
self.words.insert(word, 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, word: RopeSlice) {
|
||||
let word: Cow<str> = word.into();
|
||||
match self.words.get_mut(word.as_ref()) {
|
||||
Some(1) => {
|
||||
self.words.remove(word.as_ref());
|
||||
}
|
||||
Some(n) => *n -= 1,
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
std::mem::take(&mut self.words);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct WordIndex {
|
||||
inner: Arc<RwLock<WordIndexInner>>,
|
||||
}
|
||||
|
||||
impl WordIndex {
|
||||
pub fn matches(&self, pattern: &str) -> Vec<String> {
|
||||
let inner = self.inner.read();
|
||||
let mut matches = fuzzy_match(pattern, inner.words(), false);
|
||||
matches.sort_unstable_by_key(|(_, score)| *score);
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|(word, _)| word.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_document(&self, text: &Rope) {
|
||||
let mut inner = self.inner.write();
|
||||
for word in words(text.slice(..)) {
|
||||
inner.insert(word);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_document(&self, old_text: &Rope, text: &Rope, changes: &ChangeSet) {
|
||||
let mut inner = self.inner.write();
|
||||
for (old_window, new_window) in changed_windows(old_text.slice(..), text.slice(..), changes)
|
||||
{
|
||||
for word in words(new_window) {
|
||||
inner.insert(word);
|
||||
}
|
||||
for word in words(old_window) {
|
||||
inner.remove(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_document(&self, text: &Rope) {
|
||||
let mut inner = self.inner.write();
|
||||
for word in words(text.slice(..)) {
|
||||
inner.remove(word);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&self) {
|
||||
let mut inner = self.inner.write();
|
||||
inner.clear();
|
||||
}
|
||||
|
||||
/// Coordinate the indexing of documents.
|
||||
///
|
||||
/// This task wraps a MPSC queue and spawns blocking tasks which update the index. Updates
|
||||
/// are applied one-by-one to ensure that changes to the index are **serialized**:
|
||||
/// updates to each document must be applied in-order.
|
||||
async fn run(self, mut events: mpsc::UnboundedReceiver<Event>) {
|
||||
while let Some(event) = events.recv().await {
|
||||
let this = self.clone();
|
||||
tokio::task::spawn_blocking(move || match event {
|
||||
Event::Insert(text) => {
|
||||
this.add_document(&text);
|
||||
}
|
||||
Event::Update(
|
||||
_doc,
|
||||
Change {
|
||||
old_text,
|
||||
text,
|
||||
changes,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
this.update_document(&old_text, &text, &changes);
|
||||
}
|
||||
Event::Delete(_doc, text) => {
|
||||
this.remove_document(&text);
|
||||
}
|
||||
Event::Clear => {
|
||||
this.clear();
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn words(text: RopeSlice) -> impl Iterator<Item = RopeSlice> {
|
||||
let mut cursor = Range::point(0);
|
||||
if text
|
||||
.get_char(cursor.anchor)
|
||||
.is_some_and(|ch| !ch.is_whitespace())
|
||||
{
|
||||
let cursor_word_end = movement::move_next_word_end(text, cursor, 1);
|
||||
if cursor_word_end.anchor == 0 {
|
||||
cursor = cursor_word_end;
|
||||
}
|
||||
}
|
||||
|
||||
iter::from_fn(move || {
|
||||
while cursor.head <= text.len_chars() {
|
||||
let mut word = None;
|
||||
if text
|
||||
.slice(..cursor.head)
|
||||
.graphemes_rev()
|
||||
.take(MIN_WORD_GRAPHEMES)
|
||||
.take_while(|g| g.chars().all(char_is_word))
|
||||
.count()
|
||||
== MIN_WORD_GRAPHEMES
|
||||
{
|
||||
cursor.anchor += text
|
||||
.chars_at(cursor.anchor)
|
||||
.take_while(|&c| !char_is_word(c))
|
||||
.count();
|
||||
let slice = cursor.slice(text);
|
||||
if slice.len_chars() <= MAX_WORD_LEN {
|
||||
word = Some(slice);
|
||||
}
|
||||
}
|
||||
let head = cursor.head;
|
||||
cursor = movement::move_next_word_end(text, cursor, 1);
|
||||
if cursor.head == head {
|
||||
cursor.head = usize::MAX;
|
||||
}
|
||||
if word.is_some() {
|
||||
return word;
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Finds areas of the old and new texts around each operation in `changes`.
|
||||
///
|
||||
/// The window is larger than the changed area and can encompass multiple insert/delete operations
|
||||
/// if they are grouped closely together.
|
||||
///
|
||||
/// The ranges of the old and new text should usually be of different sizes. For example a
|
||||
/// deletion of "foo" surrounded by large retain sections would give a longer window into the
|
||||
/// `old_text` and shorter window of `new_text`. Vice-versa for an insertion. A full replacement
|
||||
/// of a word though would give two slices of the same size.
|
||||
fn changed_windows<'a>(
|
||||
old_text: RopeSlice<'a>,
|
||||
new_text: RopeSlice<'a>,
|
||||
changes: &'a ChangeSet,
|
||||
) -> impl Iterator<Item = (RopeSlice<'a>, RopeSlice<'a>)> {
|
||||
use helix_core::Operation::*;
|
||||
|
||||
let mut operations = changes.changes().iter().peekable();
|
||||
let mut old_pos = 0;
|
||||
let mut new_pos = 0;
|
||||
iter::from_fn(move || loop {
|
||||
let operation = operations.next()?;
|
||||
let old_start = old_pos;
|
||||
let new_start = new_pos;
|
||||
let len = operation.len_chars();
|
||||
match operation {
|
||||
Retain(_) => {
|
||||
old_pos += len;
|
||||
new_pos += len;
|
||||
continue;
|
||||
}
|
||||
Insert(_) => new_pos += len,
|
||||
Delete(_) => old_pos += len,
|
||||
}
|
||||
|
||||
// Scan ahead until a `Retain` is found which would end a window.
|
||||
while let Some(o) = operations.next_if(|op| !matches!(op, Retain(n) if *n > MAX_WORD_LEN)) {
|
||||
let len = o.len_chars();
|
||||
match o {
|
||||
Retain(_) => {
|
||||
old_pos += len;
|
||||
new_pos += len;
|
||||
}
|
||||
Delete(_) => old_pos += len,
|
||||
Insert(_) => new_pos += len,
|
||||
}
|
||||
}
|
||||
|
||||
let old_window = old_start.saturating_sub(MAX_WORD_LEN)
|
||||
..(old_pos + MAX_WORD_LEN).min(old_text.len_chars());
|
||||
let new_window = new_start.saturating_sub(MAX_WORD_LEN)
|
||||
..(new_pos + MAX_WORD_LEN).min(new_text.len_chars());
|
||||
|
||||
return Some((old_text.slice(old_window), new_text.slice(new_window)));
|
||||
})
|
||||
}
|
||||
|
||||
/// Estimates whether a changeset is significant or small.
|
||||
fn is_changeset_significant(changes: &ChangeSet) -> bool {
|
||||
use helix_core::Operation::*;
|
||||
|
||||
let mut diff = 0;
|
||||
for operation in changes.changes() {
|
||||
match operation {
|
||||
Retain(_) => continue,
|
||||
Delete(_) | Insert(_) => diff += operation.len_chars(),
|
||||
}
|
||||
}
|
||||
|
||||
// This is arbitrary and could be tuned further:
|
||||
diff > 1_000
|
||||
}
|
||||
|
||||
pub(crate) fn register_hooks(handlers: &Handlers) {
|
||||
let coordinator = handlers.word_index.coordinator.clone();
|
||||
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
|
||||
let doc = doc!(event.editor, &event.doc);
|
||||
if doc.word_completion_enabled() {
|
||||
coordinator.send(Event::Insert(doc.text().clone())).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.word_index.hook.clone();
|
||||
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||
if !event.ghost_transaction && event.doc.word_completion_enabled() {
|
||||
helix_event::send_blocking(
|
||||
&tx,
|
||||
Event::Update(
|
||||
event.doc.id(),
|
||||
Change {
|
||||
old_text: event.old_text.clone(),
|
||||
text: event.doc.text().clone(),
|
||||
changes: event.changes.clone(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.word_index.hook.clone();
|
||||
register_hook!(move |event: &mut DocumentDidClose<'_>| {
|
||||
if event.doc.word_completion_enabled() {
|
||||
helix_event::send_blocking(
|
||||
&tx,
|
||||
Event::Delete(event.doc.id(), event.doc.text().clone()),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let coordinator = handlers.word_index.coordinator.clone();
|
||||
register_hook!(move |event: &mut ConfigDidChange<'_>| {
|
||||
// The feature has been turned off. Clear the index and reclaim any used memory.
|
||||
if event.old.word_completion.enable && !event.new.word_completion.enable {
|
||||
coordinator.send(Event::Clear).unwrap();
|
||||
}
|
||||
|
||||
// The feature has been turned on. Index open documents.
|
||||
if !event.old.word_completion.enable && event.new.word_completion.enable {
|
||||
for doc in event.editor.documents() {
|
||||
if doc.word_completion_enabled() {
|
||||
coordinator.send(Event::Insert(doc.text().clone())).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
use helix_core::diff::compare_ropes;
|
||||
|
||||
impl WordIndex {
|
||||
fn words(&self) -> HashSet<String> {
|
||||
let inner = self.inner.read();
|
||||
inner.words().map(|w| w.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_words<I: ToString, T: IntoIterator<Item = I>>(text: &str, expected: T) {
|
||||
let text = Rope::from_str(text);
|
||||
let index = WordIndex::default();
|
||||
index.add_document(&text);
|
||||
let actual = index.words();
|
||||
let expected: HashSet<_> = expected.into_iter().map(|i| i.to_string()).collect();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_words("one two three", ["one", "two", "three"]);
|
||||
assert_words("a foo c", ["foo"]);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_diff<S, R, I>(before: &str, after: &str, expect_removed: R, expect_inserted: I)
|
||||
where
|
||||
S: ToString,
|
||||
R: IntoIterator<Item = S>,
|
||||
I: IntoIterator<Item = S>,
|
||||
{
|
||||
let before = Rope::from_str(before);
|
||||
let after = Rope::from_str(after);
|
||||
let diff = compare_ropes(&before, &after);
|
||||
let expect_removed: HashSet<_> =
|
||||
expect_removed.into_iter().map(|i| i.to_string()).collect();
|
||||
let expect_inserted: HashSet<_> =
|
||||
expect_inserted.into_iter().map(|i| i.to_string()).collect();
|
||||
|
||||
let index = WordIndex::default();
|
||||
index.add_document(&before);
|
||||
let words_before = index.words();
|
||||
index.update_document(&before, &after, diff.changes());
|
||||
let words_after = index.words();
|
||||
|
||||
let actual_removed = words_before.difference(&words_after).cloned().collect();
|
||||
let actual_inserted = words_after.difference(&words_before).cloned().collect();
|
||||
|
||||
eprintln!("\"{before}\" {words_before:?} => \"{after}\" {words_after:?}");
|
||||
assert_eq!(
|
||||
expect_removed, actual_removed,
|
||||
"expected {expect_removed:?} to be removed, instead {actual_removed:?} was"
|
||||
);
|
||||
assert_eq!(
|
||||
expect_inserted, actual_inserted,
|
||||
"expected {expect_inserted:?} to be inserted, instead {actual_inserted:?} was"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
assert_diff("one two three", "one five three", ["two"], ["five"]);
|
||||
assert_diff("one two three", "one to three", ["two"], []);
|
||||
assert_diff("one two three", "one three", ["two"], []);
|
||||
assert_diff("one two three", "one t{o three", ["two"], []);
|
||||
assert_diff("one foo three", "one fooo three", ["foo"], ["fooo"]);
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
//! Input event handling, currently backed by crossterm.
|
||||
//! Input event handling, currently backed by termina.
|
||||
use anyhow::{anyhow, Error};
|
||||
use helix_core::unicode::{segmentation::UnicodeSegmentation, width::UnicodeWidthStr};
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
@@ -65,7 +65,7 @@ pub enum MouseButton {
|
||||
pub struct KeyEvent {
|
||||
pub code: KeyCode,
|
||||
pub modifiers: KeyModifiers,
|
||||
// TODO: crossterm now supports kind & state if terminal supports kitty's extended protocol
|
||||
// TODO: termina now supports kind & state if terminal supports kitty's extended protocol
|
||||
}
|
||||
|
||||
impl KeyEvent {
|
||||
@@ -459,28 +459,31 @@ impl<'de> Deserialize<'de> for KeyEvent {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::Event> for Event {
|
||||
fn from(event: crossterm::event::Event) -> Self {
|
||||
impl From<termina::event::Event> for Event {
|
||||
fn from(event: termina::event::Event) -> Self {
|
||||
match event {
|
||||
crossterm::event::Event::Key(key) => Self::Key(key.into()),
|
||||
crossterm::event::Event::Mouse(mouse) => Self::Mouse(mouse.into()),
|
||||
crossterm::event::Event::Resize(w, h) => Self::Resize(w, h),
|
||||
crossterm::event::Event::FocusGained => Self::FocusGained,
|
||||
crossterm::event::Event::FocusLost => Self::FocusLost,
|
||||
crossterm::event::Event::Paste(s) => Self::Paste(s),
|
||||
termina::event::Event::Key(key) => Self::Key(key.into()),
|
||||
termina::event::Event::Mouse(mouse) => Self::Mouse(mouse.into()),
|
||||
termina::event::Event::WindowResized(termina::WindowSize { rows, cols, .. }) => {
|
||||
Self::Resize(cols, rows)
|
||||
}
|
||||
termina::event::Event::FocusIn => Self::FocusGained,
|
||||
termina::event::Event::FocusOut => Self::FocusLost,
|
||||
termina::event::Event::Paste(s) => Self::Paste(s),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::MouseEvent> for MouseEvent {
|
||||
impl From<termina::event::MouseEvent> for MouseEvent {
|
||||
fn from(
|
||||
crossterm::event::MouseEvent {
|
||||
termina::event::MouseEvent {
|
||||
kind,
|
||||
column,
|
||||
row,
|
||||
modifiers,
|
||||
}: crossterm::event::MouseEvent,
|
||||
}: termina::event::MouseEvent,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind: kind.into(),
|
||||
@@ -492,40 +495,40 @@ impl From<crossterm::event::MouseEvent> for MouseEvent {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::MouseEventKind> for MouseEventKind {
|
||||
fn from(kind: crossterm::event::MouseEventKind) -> Self {
|
||||
impl From<termina::event::MouseEventKind> for MouseEventKind {
|
||||
fn from(kind: termina::event::MouseEventKind) -> Self {
|
||||
match kind {
|
||||
crossterm::event::MouseEventKind::Down(button) => Self::Down(button.into()),
|
||||
crossterm::event::MouseEventKind::Up(button) => Self::Up(button.into()),
|
||||
crossterm::event::MouseEventKind::Drag(button) => Self::Drag(button.into()),
|
||||
crossterm::event::MouseEventKind::Moved => Self::Moved,
|
||||
crossterm::event::MouseEventKind::ScrollDown => Self::ScrollDown,
|
||||
crossterm::event::MouseEventKind::ScrollUp => Self::ScrollUp,
|
||||
crossterm::event::MouseEventKind::ScrollLeft => Self::ScrollLeft,
|
||||
crossterm::event::MouseEventKind::ScrollRight => Self::ScrollRight,
|
||||
termina::event::MouseEventKind::Down(button) => Self::Down(button.into()),
|
||||
termina::event::MouseEventKind::Up(button) => Self::Up(button.into()),
|
||||
termina::event::MouseEventKind::Drag(button) => Self::Drag(button.into()),
|
||||
termina::event::MouseEventKind::Moved => Self::Moved,
|
||||
termina::event::MouseEventKind::ScrollDown => Self::ScrollDown,
|
||||
termina::event::MouseEventKind::ScrollUp => Self::ScrollUp,
|
||||
termina::event::MouseEventKind::ScrollLeft => Self::ScrollLeft,
|
||||
termina::event::MouseEventKind::ScrollRight => Self::ScrollRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::MouseButton> for MouseButton {
|
||||
fn from(button: crossterm::event::MouseButton) -> Self {
|
||||
impl From<termina::event::MouseButton> for MouseButton {
|
||||
fn from(button: termina::event::MouseButton) -> Self {
|
||||
match button {
|
||||
crossterm::event::MouseButton::Left => MouseButton::Left,
|
||||
crossterm::event::MouseButton::Right => MouseButton::Right,
|
||||
crossterm::event::MouseButton::Middle => MouseButton::Middle,
|
||||
termina::event::MouseButton::Left => MouseButton::Left,
|
||||
termina::event::MouseButton::Right => MouseButton::Right,
|
||||
termina::event::MouseButton::Middle => MouseButton::Middle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::KeyEvent> for KeyEvent {
|
||||
impl From<termina::event::KeyEvent> for KeyEvent {
|
||||
fn from(
|
||||
crossterm::event::KeyEvent {
|
||||
termina::event::KeyEvent {
|
||||
code, modifiers, ..
|
||||
}: crossterm::event::KeyEvent,
|
||||
}: termina::event::KeyEvent,
|
||||
) -> Self {
|
||||
if code == crossterm::event::KeyCode::BackTab {
|
||||
if code == termina::event::KeyCode::BackTab {
|
||||
// special case for BackTab -> Shift-Tab
|
||||
let mut modifiers: KeyModifiers = modifiers.into();
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
@@ -543,24 +546,24 @@ impl From<crossterm::event::KeyEvent> for KeyEvent {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<KeyEvent> for crossterm::event::KeyEvent {
|
||||
impl From<KeyEvent> for termina::event::KeyEvent {
|
||||
fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
|
||||
if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
|
||||
// special case for Shift-Tab -> BackTab
|
||||
let mut modifiers = modifiers;
|
||||
modifiers.remove(KeyModifiers::SHIFT);
|
||||
crossterm::event::KeyEvent {
|
||||
code: crossterm::event::KeyCode::BackTab,
|
||||
termina::event::KeyEvent {
|
||||
code: termina::event::KeyCode::BackTab,
|
||||
modifiers: modifiers.into(),
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE,
|
||||
kind: termina::event::KeyEventKind::Press,
|
||||
state: termina::event::KeyEventState::NONE,
|
||||
}
|
||||
} else {
|
||||
crossterm::event::KeyEvent {
|
||||
termina::event::KeyEvent {
|
||||
code: code.into(),
|
||||
modifiers: modifiers.into(),
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE,
|
||||
kind: termina::event::KeyEventKind::Press,
|
||||
state: termina::event::KeyEventState::NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,9 +13,9 @@ bitflags! {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<KeyModifiers> for crossterm::event::KeyModifiers {
|
||||
impl From<KeyModifiers> for termina::event::Modifiers {
|
||||
fn from(key_modifiers: KeyModifiers) -> Self {
|
||||
use crossterm::event::KeyModifiers as CKeyModifiers;
|
||||
use termina::event::Modifiers as CKeyModifiers;
|
||||
|
||||
let mut result = CKeyModifiers::NONE;
|
||||
|
||||
@@ -37,9 +37,9 @@ impl From<KeyModifiers> for crossterm::event::KeyModifiers {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::KeyModifiers> for KeyModifiers {
|
||||
fn from(val: crossterm::event::KeyModifiers) -> Self {
|
||||
use crossterm::event::KeyModifiers as CKeyModifiers;
|
||||
impl From<termina::event::Modifiers> for KeyModifiers {
|
||||
fn from(val: termina::event::Modifiers) -> Self {
|
||||
use termina::event::Modifiers as CKeyModifiers;
|
||||
|
||||
let mut result = KeyModifiers::NONE;
|
||||
|
||||
@@ -92,9 +92,9 @@ pub enum MediaKeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<MediaKeyCode> for crossterm::event::MediaKeyCode {
|
||||
impl From<MediaKeyCode> for termina::event::MediaKeyCode {
|
||||
fn from(media_key_code: MediaKeyCode) -> Self {
|
||||
use crossterm::event::MediaKeyCode as CMediaKeyCode;
|
||||
use termina::event::MediaKeyCode as CMediaKeyCode;
|
||||
|
||||
match media_key_code {
|
||||
MediaKeyCode::Play => CMediaKeyCode::Play,
|
||||
@@ -115,9 +115,9 @@ impl From<MediaKeyCode> for crossterm::event::MediaKeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::MediaKeyCode> for MediaKeyCode {
|
||||
fn from(val: crossterm::event::MediaKeyCode) -> Self {
|
||||
use crossterm::event::MediaKeyCode as CMediaKeyCode;
|
||||
impl From<termina::event::MediaKeyCode> for MediaKeyCode {
|
||||
fn from(val: termina::event::MediaKeyCode) -> Self {
|
||||
use termina::event::MediaKeyCode as CMediaKeyCode;
|
||||
|
||||
match val {
|
||||
CMediaKeyCode::Play => MediaKeyCode::Play,
|
||||
@@ -171,9 +171,9 @@ pub enum ModifierKeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode {
|
||||
impl From<ModifierKeyCode> for termina::event::ModifierKeyCode {
|
||||
fn from(modifier_key_code: ModifierKeyCode) -> Self {
|
||||
use crossterm::event::ModifierKeyCode as CModifierKeyCode;
|
||||
use termina::event::ModifierKeyCode as CModifierKeyCode;
|
||||
|
||||
match modifier_key_code {
|
||||
ModifierKeyCode::LeftShift => CModifierKeyCode::LeftShift,
|
||||
@@ -195,9 +195,9 @@ impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::ModifierKeyCode> for ModifierKeyCode {
|
||||
fn from(val: crossterm::event::ModifierKeyCode) -> Self {
|
||||
use crossterm::event::ModifierKeyCode as CModifierKeyCode;
|
||||
impl From<termina::event::ModifierKeyCode> for ModifierKeyCode {
|
||||
fn from(val: termina::event::ModifierKeyCode) -> Self {
|
||||
use termina::event::ModifierKeyCode as CModifierKeyCode;
|
||||
|
||||
match val {
|
||||
CModifierKeyCode::LeftShift => ModifierKeyCode::LeftShift,
|
||||
@@ -280,9 +280,9 @@ pub enum KeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<KeyCode> for crossterm::event::KeyCode {
|
||||
impl From<KeyCode> for termina::event::KeyCode {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
use crossterm::event::KeyCode as CKeyCode;
|
||||
use termina::event::KeyCode as CKeyCode;
|
||||
|
||||
match key_code {
|
||||
KeyCode::Backspace => CKeyCode::Backspace,
|
||||
@@ -298,10 +298,10 @@ impl From<KeyCode> for crossterm::event::KeyCode {
|
||||
KeyCode::Tab => CKeyCode::Tab,
|
||||
KeyCode::Delete => CKeyCode::Delete,
|
||||
KeyCode::Insert => CKeyCode::Insert,
|
||||
KeyCode::F(f_number) => CKeyCode::F(f_number),
|
||||
KeyCode::F(f_number) => CKeyCode::Function(f_number),
|
||||
KeyCode::Char(character) => CKeyCode::Char(character),
|
||||
KeyCode::Null => CKeyCode::Null,
|
||||
KeyCode::Esc => CKeyCode::Esc,
|
||||
KeyCode::Esc => CKeyCode::Escape,
|
||||
KeyCode::CapsLock => CKeyCode::CapsLock,
|
||||
KeyCode::ScrollLock => CKeyCode::ScrollLock,
|
||||
KeyCode::NumLock => CKeyCode::NumLock,
|
||||
@@ -316,9 +316,9 @@ impl From<KeyCode> for crossterm::event::KeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::KeyCode> for KeyCode {
|
||||
fn from(val: crossterm::event::KeyCode) -> Self {
|
||||
use crossterm::event::KeyCode as CKeyCode;
|
||||
impl From<termina::event::KeyCode> for KeyCode {
|
||||
fn from(val: termina::event::KeyCode) -> Self {
|
||||
use termina::event::KeyCode as CKeyCode;
|
||||
|
||||
match val {
|
||||
CKeyCode::Backspace => KeyCode::Backspace,
|
||||
@@ -335,10 +335,10 @@ impl From<crossterm::event::KeyCode> for KeyCode {
|
||||
CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
|
||||
CKeyCode::Delete => KeyCode::Delete,
|
||||
CKeyCode::Insert => KeyCode::Insert,
|
||||
CKeyCode::F(f_number) => KeyCode::F(f_number),
|
||||
CKeyCode::Function(f_number) => KeyCode::F(f_number),
|
||||
CKeyCode::Char(character) => KeyCode::Char(character),
|
||||
CKeyCode::Null => KeyCode::Null,
|
||||
CKeyCode::Esc => KeyCode::Esc,
|
||||
CKeyCode::Escape => KeyCode::Esc,
|
||||
CKeyCode::CapsLock => KeyCode::CapsLock,
|
||||
CKeyCode::ScrollLock => KeyCode::ScrollLock,
|
||||
CKeyCode::NumLock => KeyCode::NumLock,
|
||||
|
@@ -2,7 +2,6 @@
|
||||
pub mod macros;
|
||||
|
||||
pub mod annotations;
|
||||
pub mod base64;
|
||||
pub mod clipboard;
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
|
@@ -227,6 +227,7 @@ pub struct Theme {
|
||||
// tree-sitter highlight styles are stored in a Vec to optimize lookups
|
||||
scopes: Vec<String>,
|
||||
highlights: Vec<Style>,
|
||||
rainbow_length: usize,
|
||||
}
|
||||
|
||||
impl From<Value> for Theme {
|
||||
@@ -253,12 +254,20 @@ impl<'de> Deserialize<'de> for Theme {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn build_theme_values(
|
||||
mut values: Map<String, Value>,
|
||||
) -> (HashMap<String, Style>, Vec<String>, Vec<Style>, Vec<String>) {
|
||||
) -> (
|
||||
HashMap<String, Style>,
|
||||
Vec<String>,
|
||||
Vec<Style>,
|
||||
usize,
|
||||
Vec<String>,
|
||||
) {
|
||||
let mut styles = HashMap::new();
|
||||
let mut scopes = Vec::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut rainbow_length = 0;
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
@@ -277,6 +286,27 @@ fn build_theme_values(
|
||||
styles.reserve(values.len());
|
||||
scopes.reserve(values.len());
|
||||
highlights.reserve(values.len());
|
||||
|
||||
for (i, style) in values
|
||||
.remove("rainbow")
|
||||
.and_then(|value| match palette.parse_style_array(value) {
|
||||
Ok(styles) => Some(styles),
|
||||
Err(err) => {
|
||||
warnings.push(err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(default_rainbow)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let name = format!("rainbow.{i}");
|
||||
styles.insert(name.clone(), style);
|
||||
scopes.push(name);
|
||||
highlights.push(style);
|
||||
rainbow_length += 1;
|
||||
}
|
||||
|
||||
for (name, style_value) in values {
|
||||
let mut style = Style::default();
|
||||
if let Err(err) = palette.parse_style(&mut style, style_value) {
|
||||
@@ -289,9 +319,19 @@ fn build_theme_values(
|
||||
highlights.push(style);
|
||||
}
|
||||
|
||||
(styles, scopes, highlights, warnings)
|
||||
(styles, scopes, highlights, rainbow_length, warnings)
|
||||
}
|
||||
|
||||
fn default_rainbow() -> Vec<Style> {
|
||||
vec![
|
||||
Style::default().fg(Color::Red),
|
||||
Style::default().fg(Color::Yellow),
|
||||
Style::default().fg(Color::Green),
|
||||
Style::default().fg(Color::Blue),
|
||||
Style::default().fg(Color::Cyan),
|
||||
Style::default().fg(Color::Magenta),
|
||||
]
|
||||
}
|
||||
impl Theme {
|
||||
/// To allow `Highlight` to represent arbitrary RGB colors without turning it into an enum,
|
||||
/// we interpret the last 256^3 numbers as RGB.
|
||||
@@ -382,6 +422,10 @@ impl Theme {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rainbow_length(&self) -> usize {
|
||||
self.rainbow_length
|
||||
}
|
||||
|
||||
fn from_toml(value: Value) -> (Self, Vec<String>) {
|
||||
if let Value::Table(table) = value {
|
||||
Theme::from_keys(table)
|
||||
@@ -392,12 +436,14 @@ impl Theme {
|
||||
}
|
||||
|
||||
fn from_keys(toml_keys: Map<String, Value>) -> (Self, Vec<String>) {
|
||||
let (styles, scopes, highlights, load_errors) = build_theme_values(toml_keys);
|
||||
let (styles, scopes, highlights, rainbow_length, load_errors) =
|
||||
build_theme_values(toml_keys);
|
||||
|
||||
let theme = Self {
|
||||
styles,
|
||||
scopes,
|
||||
highlights,
|
||||
rainbow_length,
|
||||
..Default::default()
|
||||
};
|
||||
(theme, load_errors)
|
||||
@@ -541,6 +587,21 @@ impl ThemePalette {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_style_array(&self, value: Value) -> Result<Vec<Style>, String> {
|
||||
let mut styles = Vec::new();
|
||||
|
||||
for v in value
|
||||
.as_array()
|
||||
.ok_or_else(|| format!("Could not parse value as an array: '{value}'"))?
|
||||
{
|
||||
let mut style = Style::default();
|
||||
self.parse_style(&mut style, v.clone())?;
|
||||
styles.push(style);
|
||||
}
|
||||
|
||||
Ok(styles)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Value> for ThemePalette {
|
||||
|
244
languages.toml
244
languages.toml
@@ -65,8 +65,9 @@ julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--hist
|
||||
just-lsp = { command = "just-lsp" }
|
||||
koka = { command = "koka", args = ["--language-server", "--lsstdio"] }
|
||||
koto-ls = { command = "koto-ls" }
|
||||
kotlin-lsp = { command = "kotlin-lsp", args = ["--stdio"] }
|
||||
kotlin-language-server = { command = "kotlin-language-server" }
|
||||
lean = { command = "lean", args = [ "--server", "--memory=1024" ] }
|
||||
lean = { command = "lean", args = ["--server"] }
|
||||
ltex-ls = { command = "ltex-ls" }
|
||||
ltex-ls-plus = { command = "ltex-ls-plus" }
|
||||
markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
|
||||
@@ -445,6 +446,18 @@ formatter = { command = "fish_indent" }
|
||||
name = "fish"
|
||||
source = { git = "https://github.com/ram02z/tree-sitter-fish", rev = "a78aef9abc395c600c38a037ac779afc7e3cc9e0" }
|
||||
|
||||
[[language]]
|
||||
name = "flatbuffers"
|
||||
scope = "source.flatbuffers"
|
||||
injection-regex = "(flatbuffers?|fbs)"
|
||||
file-types = ["fbs"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "flatbuffers"
|
||||
source = { git = "https://github.com/yuanchenxi95/tree-sitter-flatbuffers", rev = "95e6f9ef101ea97e870bf6eebc0bd1fdfbaf5490" }
|
||||
|
||||
[[language]]
|
||||
name = "mint"
|
||||
scope = "source.mint"
|
||||
@@ -674,7 +687,7 @@ scope = "source.csharp"
|
||||
injection-regex = "c-?sharp"
|
||||
file-types = ["cs", "csx", "cake"]
|
||||
roots = ["sln", "csproj"]
|
||||
comment-token = "//"
|
||||
comment-tokens = ["//", "///"]
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
indent = { tab-width = 4, unit = "\t" }
|
||||
language-servers = [ "omnisharp" ]
|
||||
@@ -778,7 +791,7 @@ args = { mode = "core", program = "{0}", coreFilePath = "{1}" }
|
||||
|
||||
[[grammar]]
|
||||
name = "go"
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "64457ea6b73ef5422ed1687178d4545c3e91334a" }
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "12fe553fdaaa7449f764bc876fd777704d4fb752" }
|
||||
|
||||
[[language]]
|
||||
name = "gomod"
|
||||
@@ -822,6 +835,16 @@ indent = { tab-width = 4, unit = "\t" }
|
||||
name = "gowork"
|
||||
source = { git = "https://github.com/omertuc/tree-sitter-go-work", rev = "6dd9dd79fb51e9f2abc829d5e97b15015b6a8ae2" }
|
||||
|
||||
[[language]]
|
||||
name = "go-format-string"
|
||||
scope = "source.go-format-string"
|
||||
file-types = []
|
||||
injection-regex = "go-format-string"
|
||||
|
||||
[[grammar]]
|
||||
name = "go-format-string"
|
||||
source = { git = "https://codeberg.org/kpbaks/tree-sitter-go-format-string", rev = "06587ea641155db638f46a32c959d68796cd36bb" }
|
||||
|
||||
[[language]]
|
||||
name = "javascript"
|
||||
scope = "source.js"
|
||||
@@ -927,7 +950,7 @@ indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "css"
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "6e327db434fec0ee90f006697782e43ec855adf5" }
|
||||
|
||||
[[language]]
|
||||
name = "scss"
|
||||
@@ -963,6 +986,7 @@ scope = "source.htmldjango"
|
||||
injection-regex = "htmldjango"
|
||||
language-servers = ["djlsp", "vscode-html-language-server", "superhtml"]
|
||||
file-types = []
|
||||
roots = ["manage.py"]
|
||||
|
||||
[language.auto-pairs]
|
||||
'"' = '"'
|
||||
@@ -1021,6 +1045,7 @@ shebangs = []
|
||||
comment-token = "#"
|
||||
language-servers = [ "nil", "nixd" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
formatter = { command = "nixfmt" }
|
||||
|
||||
[[grammar]]
|
||||
name = "nix"
|
||||
@@ -1066,6 +1091,7 @@ file-types = [
|
||||
{ glob = "Scanfile" },
|
||||
{ glob = "Snapfile" },
|
||||
{ glob = "Gymfile" },
|
||||
{ glob = ".irbrc" },
|
||||
]
|
||||
shebangs = ["ruby"]
|
||||
comment-token = "#"
|
||||
@@ -1173,7 +1199,7 @@ roots = ["composer.json", "index.php"]
|
||||
|
||||
[[grammar]]
|
||||
name = "blade"
|
||||
source = { git = "https://github.com/EmranMR/tree-sitter-blade", rev = "4c66efe1e05c639c555ee70092021b8223d2f440" }
|
||||
source = { git = "https://github.com/EmranMR/tree-sitter-blade", rev = "59ce5b68e288002e3aee6cf5a379bbef21adbe6c" }
|
||||
|
||||
[[language]]
|
||||
name = "twig"
|
||||
@@ -1404,6 +1430,16 @@ language-servers = [ "lua-language-server" ]
|
||||
name = "lua"
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-lua", rev = "88e446476a1e97a8724dff7a23e2d709855077f2" }
|
||||
|
||||
[[language]]
|
||||
name = "luap"
|
||||
scope = "source.luap"
|
||||
file-types = []
|
||||
injection-regex = "luap"
|
||||
|
||||
[[grammar]]
|
||||
name = "luap"
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-luap", rev = "c134aaec6acf4fa95fe4aa0dc9aba3eacdbbe55a" }
|
||||
|
||||
[[grammar]]
|
||||
name = "teal"
|
||||
source = { git = "https://github.com/euclidianAce/tree-sitter-teal", rev = "3db655924b2ff1c54fdf6371b5425ea6b5dccefe" }
|
||||
@@ -1457,12 +1493,15 @@ file-types = [
|
||||
{ glob = ".clang-format" },
|
||||
{ glob = ".clang-tidy" },
|
||||
{ glob = ".gem/credentials" },
|
||||
{ glob = ".kube/config" },
|
||||
"sublime-syntax"
|
||||
]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = [ "yaml-language-server", "ansible-language-server" ]
|
||||
injection-regex = "yml|yaml"
|
||||
formatter = { command = "yamlfmt", args = ['-'] }
|
||||
auto-format = true
|
||||
|
||||
[[grammar]]
|
||||
name = "yaml"
|
||||
@@ -1483,6 +1522,7 @@ scope = "source.haskell"
|
||||
injection-regex = "hs|haskell"
|
||||
file-types = ["hs", "hs-boot", "hsc"]
|
||||
roots = ["Setup.hs", "stack.yaml", "cabal.project"]
|
||||
shebangs = ["runhaskell", "stack"]
|
||||
comment-token = "--"
|
||||
block-comment-tokens = { start = "{-", end = "-}" }
|
||||
language-servers = [ "haskell-language-server" ]
|
||||
@@ -1557,7 +1597,7 @@ args = { console = "internalConsole", attachCommands = [ "platform select remote
|
||||
|
||||
[[grammar]]
|
||||
name = "zig"
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-zig", rev = "eb7d58c2dc4fbeea4745019dee8df013034ae66b" }
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-zig", rev = "b71affffdb4222ff2d2dea6e164f76603b0be6bc" }
|
||||
|
||||
[[language]]
|
||||
name = "prolog"
|
||||
@@ -1783,6 +1823,18 @@ roots = [".marksman.toml"]
|
||||
language-servers = [ "marksman", "markdown-oxide" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
block-comment-tokens = { start = "<!--", end = "-->" }
|
||||
word-completion.trigger-length = 4
|
||||
|
||||
[language.auto-pairs]
|
||||
'(' = ')'
|
||||
'{' = '}'
|
||||
'[' = ']'
|
||||
'"' = '"'
|
||||
"'" = "'"
|
||||
'`' = '`'
|
||||
'‘' = '’'
|
||||
'«' = '»'
|
||||
'“' = '”'
|
||||
|
||||
[[grammar]]
|
||||
name = "markdown"
|
||||
@@ -1869,7 +1921,7 @@ file-types = [
|
||||
{ glob = "containerfile.*" },
|
||||
]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
language-servers = [ "docker-langserver" ]
|
||||
|
||||
[[grammar]]
|
||||
@@ -1971,7 +2023,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev =
|
||||
[[language]]
|
||||
name = "git-ignore"
|
||||
scope = "source.gitignore"
|
||||
file-types = [{ glob = ".gitignore_global" }, { glob = "git/ignore" }, { glob = ".ignore" }, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }, { glob = ".*ignore" }, { glob = ".git-blame-ignore-revs" }]
|
||||
file-types = [{ glob = ".gitignore_global" }, { glob = "git/ignore" }, { glob = ".git/info/exclude" }, { glob = ".ignore" }, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }, { glob = ".*ignore" }, { glob = ".git-blame-ignore-revs" }]
|
||||
injection-regex = "git-ignore"
|
||||
comment-token = "#"
|
||||
grammar = "gitignore"
|
||||
@@ -2046,7 +2098,10 @@ roots = ["rebar.config"]
|
||||
shebangs = ["escript"]
|
||||
comment-token = "%%"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
language-servers = [ "erlang-ls", "elp" ]
|
||||
language-servers = [
|
||||
{ name = "erlang-ls", except-features = ["document-symbols", "workspace-symbols"] },
|
||||
{ name = "elp", except-features = ["document-symbols", "workspace-symbols"] }
|
||||
]
|
||||
|
||||
[[grammar]]
|
||||
name = "erlang"
|
||||
@@ -2132,7 +2187,7 @@ auto-format = true
|
||||
|
||||
[[grammar]]
|
||||
name = "gleam"
|
||||
source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "ee93c639dc82148d716919df336ad612fd33538e" }
|
||||
source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "dae1551a9911b24f41d876c23f2ab05ece0a9d4c" }
|
||||
|
||||
[[language]]
|
||||
name = "quarto"
|
||||
@@ -2484,7 +2539,7 @@ source = {git = "https://github.com/vlang/v-analyzer", subpath = "tree_sitter_v"
|
||||
[[language]]
|
||||
name = "verilog"
|
||||
scope = "source.verilog"
|
||||
file-types = ["v", "vh", "sv", "svh"]
|
||||
file-types = ["v", "vh"]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
language-servers = [ "svlangserver" ]
|
||||
@@ -2495,6 +2550,19 @@ injection-regex = "verilog"
|
||||
name = "verilog"
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-verilog", rev = "4457145e795b363f072463e697dfe2f6973c9a52" }
|
||||
|
||||
[[language]]
|
||||
name = "systemverilog"
|
||||
scope = "source.systemverilog"
|
||||
file-types = ["sv", "svh"]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = ["verible-verilog-ls"]
|
||||
|
||||
[[grammar]]
|
||||
name = "systemverilog"
|
||||
source = { git = "https://github.com/gmlarumbe/tree-sitter-systemverilog", rev = "3bd2c5d2f60ed7b07c2177b34e2976ad9a87c659" }
|
||||
|
||||
[[language]]
|
||||
name = "edoc"
|
||||
scope = "source.edoc"
|
||||
@@ -2563,7 +2631,7 @@ source = { git = "https://github.com/sogaiu/tree-sitter-clojure", rev = "e57c569
|
||||
name = "starlark"
|
||||
scope = "source.starlark"
|
||||
injection-regex = "(starlark|bzl|bazel|buck)"
|
||||
file-types = ["bzl", "bazel", "star", { glob = "BUILD" }, { glob = "BUCK" }, { glob = "BUILD.*" }, { glob = "Tiltfile" }, { glob = "WORKSPACE" }, { glob = "WORKSPACE.bzlmod" }]
|
||||
file-types = ["bzl", "bazel", "star", { glob = "BUILD" }, { glob = "BUCK" }, { glob = "BUILD.*" }, { glob = "Tiltfile" }, { glob = "WORKSPACE" }, { glob = "WORKSPACE.bzlmod" }, { glob = "PACKAGE" }]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
language-servers = [ "starpls" ]
|
||||
@@ -2931,7 +2999,8 @@ file-types = [
|
||||
"sublime-snippet",
|
||||
"xsl",
|
||||
"mpd",
|
||||
"smil"
|
||||
"smil",
|
||||
"gpx",
|
||||
]
|
||||
block-comment-tokens = { start = "<!--", end = "-->" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
@@ -3039,9 +3108,11 @@ file-types = [
|
||||
{ glob = "hgrc" },
|
||||
{ glob = "npmrc" },
|
||||
{ glob = "rclone.conf" },
|
||||
{ glob = ".aws/config" },
|
||||
"properties",
|
||||
"cfg",
|
||||
"directory"
|
||||
"directory",
|
||||
{ glob = ".wslconfig" },
|
||||
]
|
||||
injection-regex = "ini"
|
||||
comment-token = "#"
|
||||
@@ -3064,7 +3135,7 @@ formatter = { command = "inko", args = ["fmt", "-"] }
|
||||
|
||||
[[grammar]]
|
||||
name = "inko"
|
||||
source = { git = "https://github.com/inko-lang/tree-sitter-inko", rev = "7860637ce1b43f5f79cfb7cc3311bf3234e9479f" }
|
||||
source = { git = "https://github.com/inko-lang/tree-sitter-inko", rev = "f58a87ac4dc6a7955c64c9e4408fbd693e804686" }
|
||||
|
||||
[[language]]
|
||||
name = "bicep"
|
||||
@@ -3116,7 +3187,7 @@ indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "matlab"
|
||||
source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "b0a0198b182574cd3ca0447264c83331901b9338" }
|
||||
source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "585b52b9b16d8e626299a76360ef6ab4f9731aed" }
|
||||
|
||||
[[language]]
|
||||
name = "ponylang"
|
||||
@@ -3283,7 +3354,7 @@ file-types = ["rst"]
|
||||
|
||||
[[grammar]]
|
||||
name = "rst"
|
||||
source = { git = "https://github.com/stsewd/tree-sitter-rst", rev = "25e6328872ac3a764ba8b926aea12719741103f1" }
|
||||
source = { git = "https://github.com/stsewd/tree-sitter-rst", rev = "ab09cab886a947c62a8c6fa94d3ad375f3f6a73d" }
|
||||
|
||||
[[language]]
|
||||
name = "capnp"
|
||||
@@ -3311,6 +3382,17 @@ language-servers = [ "cs" ]
|
||||
name = "smithy"
|
||||
source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "8327eb84d55639ffbe08c9dc82da7fff72a1ad07" }
|
||||
|
||||
[[language]]
|
||||
name = "hdl"
|
||||
scope = "source.hdl"
|
||||
file-types = ["hdl"]
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
injection-regex = "hdl"
|
||||
|
||||
[[grammar]]
|
||||
name = "hdl"
|
||||
source = { git = "https://github.com/quantonganh/tree-sitter-hdl", rev = "7d0418fd71470b0430e6f914cc76c1a9d968491d" }
|
||||
|
||||
[[language]]
|
||||
name = "vhdl"
|
||||
scope = "source.vhdl"
|
||||
@@ -3418,7 +3500,7 @@ language-servers = ["just-lsp"]
|
||||
|
||||
[[grammar]]
|
||||
name = "just"
|
||||
source = { git = "https://github.com/poliorcetics/tree-sitter-just", rev = "8d03cfdd7ab89ff76d935827de1b93450fa0ec0a" }
|
||||
source = { git = "https://github.com/poliorcetics/tree-sitter-just", rev = "b75dace757e5d122d25c1a1a7772cb87b560f829" }
|
||||
|
||||
[[language]]
|
||||
name = "gn"
|
||||
@@ -3556,7 +3638,7 @@ name = "jjdescription"
|
||||
scope = "jj.description"
|
||||
file-types = [{ glob = "*.jjdescription" }]
|
||||
comment-token = "JJ:"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
rulers = [51, 73]
|
||||
text-width = 72
|
||||
|
||||
@@ -3694,7 +3776,7 @@ language-servers = [ "templ" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "templ"
|
||||
source = { git = "https://github.com/vrischmann/tree-sitter-templ", rev = "db662414ccd6f7c78b1e834e7abe11c224b04759" }
|
||||
source = { git = "https://github.com/vrischmann/tree-sitter-templ", rev = "47594c5cbef941e6a3ccf4ddb934a68cf4c68075" }
|
||||
|
||||
[[language]]
|
||||
name = "dbml"
|
||||
@@ -3767,7 +3849,7 @@ language-servers = ["koka"]
|
||||
|
||||
[[grammar]]
|
||||
name = "koka"
|
||||
source = { git = "https://github.com/mtoohey31/tree-sitter-koka", rev = "96d070c3700692858035f3524cc0ad944cef2594" }
|
||||
source = { git = "https://github.com/koka-community/tree-sitter-koka", rev = "fd3b482274d6988349ba810ea5740e29153b1baf" }
|
||||
|
||||
[[language]]
|
||||
name = "tact"
|
||||
@@ -4238,10 +4320,11 @@ comment-token = "#"
|
||||
block-comment-tokens = ["#-", "-#"]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = ["koto-ls"]
|
||||
formatter = {command = "koto", args = ["--format"]}
|
||||
|
||||
[[grammar]]
|
||||
name = "koto"
|
||||
source = { git = "https://github.com/koto-lang/tree-sitter-koto", rev = "b420f7922d0d74905fd0d771e5b83be9ee8a8a9a" }
|
||||
source = { git = "https://github.com/koto-lang/tree-sitter-koto", rev = "2ffc77c14f0ac1674384ff629bfc207b9c57ed89" }
|
||||
|
||||
[[language]]
|
||||
name = "gpr"
|
||||
@@ -4402,6 +4485,24 @@ language-servers = ["sourcepawn-studio"]
|
||||
name = "sourcepawn"
|
||||
source = { git = "https://github.com/nilshelmig/tree-sitter-sourcepawn", rev = "f2af8d0dc14c6790130cceb2a20027eb41a8297c" }
|
||||
|
||||
|
||||
[[grammar]]
|
||||
name = "vim"
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-vim", rev = "f3cd62d8bd043ef20507e84bb6b4b53731ccf3a7" }
|
||||
|
||||
[[language]]
|
||||
name = "vim"
|
||||
scope = "source.vim"
|
||||
injection-regex = "vim"
|
||||
comment-token = '"'
|
||||
indent = { tab-width = 4, unit = "\t" }
|
||||
file-types = [
|
||||
"vim",
|
||||
{ glob = ".vimrc" },
|
||||
{ glob = ".nvimrc" },
|
||||
{ glob = ".exrc" }
|
||||
]
|
||||
|
||||
[[language]]
|
||||
name = "tlaplus"
|
||||
scope = "scope.tlaplus"
|
||||
@@ -4549,3 +4650,102 @@ comment-tokens = ["#"]
|
||||
[[grammar]]
|
||||
name = "properties"
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-properties", rev = "579b62f5ad8d96c2bb331f07d1408c92767531d9" }
|
||||
|
||||
[[language]]
|
||||
name = "robots.txt"
|
||||
scope = "source.robots.txt"
|
||||
file-types = [{ glob = "robots.txt" }]
|
||||
injection-regex = "robots[\\.-]txt"
|
||||
grammar = "robots"
|
||||
comment-token = "#"
|
||||
|
||||
[[grammar]]
|
||||
name = "robots"
|
||||
source = { git = "https://github.com/opa-oz/tree-sitter-robots-txt", rev = "8e3a4205b76236bb6dbebdbee5afc262ce38bb62" }
|
||||
|
||||
[[language]]
|
||||
name = "pip-requirements"
|
||||
scope = "source.pip-requirements"
|
||||
injection-regex = "(pip-)?requirements(\\.txt)?"
|
||||
grammar = "requirements"
|
||||
file-types = [{ glob = "requirements.txt" }, { glob = "constraints.txt" }]
|
||||
|
||||
[[grammar]]
|
||||
name = "requirements"
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-requirements", rev = "caeb2ba854dea55931f76034978de1fd79362939" }
|
||||
|
||||
[[language]]
|
||||
name = "kconfig"
|
||||
file-types = ["kconfig", { glob = "kconfig.*" }]
|
||||
scope = "source.kconfig"
|
||||
|
||||
[[grammar]]
|
||||
name = "kconfig"
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-kconfig" , rev = "9ac99fe4c0c27a35dc6f757cef534c646e944881" }
|
||||
|
||||
[[language]]
|
||||
name = "doxyfile"
|
||||
scope = "source.doxyfile"
|
||||
injection-regex = "[Dd]oxyfile"
|
||||
file-types = [{ glob = "Doxyfile" }]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "doxyfile"
|
||||
source = { git = "https://github.com/tingerrr/tree-sitter-doxyfile/", rev = "18e44c6da639632a4e42264c7193df34be915f34" }
|
||||
|
||||
[[language]]
|
||||
name = "cross-config"
|
||||
scope = "source.cross-config"
|
||||
injection-regex = "cross(-config)"
|
||||
grammar = "toml"
|
||||
comment-token = "#"
|
||||
file-types = [{glob = "Cross.toml"}]
|
||||
language-servers = [ "taplo", "tombi" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
# https://git-cliff.org/docs/configuration/
|
||||
[[language]]
|
||||
name = "git-cliff-config"
|
||||
scope = "source.git-cliff-config"
|
||||
injection-regex = "git-cliff(-config)"
|
||||
grammar = "toml"
|
||||
comment-token = "#"
|
||||
file-types = [{ glob = "cliff.toml" }]
|
||||
language-servers = ["taplo", "tombi"]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "cython"
|
||||
scope = "source.cython"
|
||||
file-types = ["pxd", "pxi", "pyx"]
|
||||
comment-token = "#"
|
||||
roots = ["pyproject.toml", "setup.py", "poetry.lock"]
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "cython"
|
||||
source = { git = "https://github.com/b0o/tree-sitter-cython", rev = "62f44f5e7e41dde03c5f0a05f035e293bcf2bcf8" }
|
||||
|
||||
[[language]]
|
||||
name = "shellcheckrc"
|
||||
scope = "source.shellcheckrc"
|
||||
injection-regex = "shellcheck(rc)?"
|
||||
file-types = [{glob = "shellcheckrc"}, {glob = ".shellcheckrc"}]
|
||||
comment-token = "#"
|
||||
|
||||
[[grammar]]
|
||||
name = "shellcheckrc"
|
||||
source = {git = "https://codeberg.org/kpbaks/tree-sitter-shellcheckrc", rev = "ad3da4e8f7fd72dcc5e93a6b89822c59a7cd10ff"}
|
||||
|
||||
[[grammar]]
|
||||
name = "strictdoc"
|
||||
source = { git = "https://github.com/manueldiagostino/tree-sitter-strictdoc", rev = "070edcf23f7c85af355437706048f73833e0ea10" }
|
||||
|
||||
[[language]]
|
||||
name = "strictdoc"
|
||||
scope = "source.strictdoc"
|
||||
injection-regex = "strictdoc"
|
||||
file-types = ["sdoc", "sgra"]
|
||||
comment-token = ".."
|
||||
|
7
runtime/queries/_jsx/textobjects.scm
Normal file
7
runtime/queries/_jsx/textobjects.scm
Normal file
@@ -0,0 +1,7 @@
|
||||
; See runtime/queries/ecma/README.md for more info.
|
||||
|
||||
(jsx_self_closing_element) @xml-element.around @xml-element.inside
|
||||
|
||||
(jsx_element (jsx_opening_element) (_)* @xml-element.inside (jsx_closing_element))
|
||||
|
||||
(jsx_element) @xml-element.around
|
@@ -79,6 +79,7 @@
|
||||
(property_signature "?" @punctuation.special)
|
||||
|
||||
(conditional_type ["?" ":"] @operator)
|
||||
(ternary_expression ["?" ":"] @operator)
|
||||
|
||||
; Keywords
|
||||
; --------
|
||||
|
@@ -9,3 +9,30 @@
|
||||
|
||||
((regex) @injection.content
|
||||
(#set! injection.language "regex"))
|
||||
|
||||
(command
|
||||
name: (command_name (word) @_command (#any-of? @_command "jq" "jaq"))
|
||||
argument: [
|
||||
(raw_string) @injection.content
|
||||
(string (string_content) @injection.content)
|
||||
]
|
||||
(#set! injection.language "jq"))
|
||||
|
||||
(command
|
||||
name: (command_name (word) @_command (#eq? @_command "alias"))
|
||||
argument: (concatenation
|
||||
(word)
|
||||
[
|
||||
(raw_string) @injection.content
|
||||
(string (string_content) @injection.content)
|
||||
])
|
||||
(#set! injection.language "bash"))
|
||||
|
||||
(command
|
||||
name: (command_name (word) @_command (#any-of? @_command "eval" "trap"))
|
||||
.
|
||||
argument: [
|
||||
(raw_string) @injection.content
|
||||
(string (string_content) @injection.content)
|
||||
]
|
||||
(#set! injection.language "bash"))
|
||||
|
20
runtime/queries/bash/rainbows.scm
Normal file
20
runtime/queries/bash/rainbows.scm
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
(function_definition)
|
||||
(compound_statement)
|
||||
(subshell)
|
||||
(test_command)
|
||||
(subscript)
|
||||
(parenthesized_expression)
|
||||
(array)
|
||||
(expansion)
|
||||
(command_substitution)
|
||||
] @rainbow.scope
|
||||
|
||||
[
|
||||
"(" ")"
|
||||
"((" "))"
|
||||
"${" "$("
|
||||
"{" "}"
|
||||
"[" "]"
|
||||
"[[" "]]"
|
||||
] @rainbow.bracket
|
1
runtime/queries/bash/tags.scm
Normal file
1
runtime/queries/bash/tags.scm
Normal file
@@ -0,0 +1 @@
|
||||
(function_definition name: (word) @definition.function)
|
@@ -1,8 +1,9 @@
|
||||
; inherits: html
|
||||
|
||||
((directive_start) @start
|
||||
(directive_end) @end.after
|
||||
(#set! role block))
|
||||
|
||||
|
||||
((bracket_start) @start
|
||||
(bracket_end) @end
|
||||
(#set! role block))
|
||||
(#set! role block))
|
||||
|
@@ -1,8 +1,16 @@
|
||||
(directive) @tag
|
||||
(directive_start) @tag
|
||||
(directive_end) @tag
|
||||
; inherits: html
|
||||
|
||||
(directive) @keyword.directive
|
||||
(directive_start) @keyword.directive
|
||||
(directive_end) @keyword.directive
|
||||
(comment) @comment
|
||||
|
||||
; merged with blade punctuation
|
||||
[
|
||||
(bracket_start)
|
||||
(bracket_end)
|
||||
"{{"
|
||||
"}}"
|
||||
"{!!"
|
||||
"!!}"
|
||||
"("
|
||||
")"
|
||||
] @punctuation.bracket
|
||||
|
@@ -1,14 +1,108 @@
|
||||
((text) @injection.content
|
||||
(#set! injection.combined)
|
||||
(#set! injection.language "php"))
|
||||
; inherits: html
|
||||
|
||||
; tree-sitter-comment injection
|
||||
; if available
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
||||
|
||||
((php_only) @injection.content
|
||||
(#set! injection.language "php-only"))
|
||||
|
||||
((parameter) @injection.content
|
||||
(#set! injection.include-children)
|
||||
(#set! injection.language "php-only"))
|
||||
((parameter) @injection.content
|
||||
(#set! injection.include-children) ; You may need this, depending on your editor e.g Helix
|
||||
(#set! injection.language "php-only"))
|
||||
|
||||
; ; Livewire attributes
|
||||
(attribute
|
||||
(attribute_name) @_attr
|
||||
(#any-of? @_attr
|
||||
"wire:click"
|
||||
"wire:submit"
|
||||
"wire:model"
|
||||
"wire:loading"
|
||||
"wire:navigate"
|
||||
"wire:current"
|
||||
"wire:cloak"
|
||||
"wire:dirty"
|
||||
"wire:confirm"
|
||||
"wire:transition"
|
||||
"wire:init"
|
||||
"wire:poll"
|
||||
"wire:offline"
|
||||
"wire:ignore"
|
||||
"wire:replace"
|
||||
"wire:show"
|
||||
"wire:stream"
|
||||
"wire:text"
|
||||
)
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @injection.content)
|
||||
(#set! injection.language "javascript"))
|
||||
|
||||
; ; See #33
|
||||
; ; AlpineJS attributes
|
||||
(attribute
|
||||
(attribute_name) @_attr
|
||||
(#match? @_attr "^x-[a-z]+")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @injection.content)
|
||||
(#set! injection.language "javascript"))
|
||||
|
||||
; ; Apline Events
|
||||
(attribute
|
||||
(attribute_name) @_attr
|
||||
(#match? @_attr "^@[a-z]+")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @injection.content)
|
||||
(#set! injection.language "javascript"))
|
||||
|
||||
; ; normal HTML element alpine attributes
|
||||
(element
|
||||
(_
|
||||
(tag_name) @_tag
|
||||
(#match? @_tag "[^x][^-]")
|
||||
(attribute
|
||||
(attribute_name) @_attr
|
||||
(#match? @_attr "^:[a-z]+")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @injection.content)
|
||||
(#set! injection.combined)
|
||||
(#set! injection.language "javascript"))))
|
||||
|
||||
; ; ; Blade escaped JS attributes
|
||||
; ; <x-foo ::bar="baz" />
|
||||
(element
|
||||
(_
|
||||
(tag_name) @_tag
|
||||
(#match? @_tag "^x-[a-z]+")
|
||||
(attribute
|
||||
(attribute_name) @_attr
|
||||
(#match? @_attr "^::[a-z]+")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @injection.content)
|
||||
(#set! injection.language "javascript"))))
|
||||
|
||||
|
||||
; ; ; Blade escaped JS attributes
|
||||
; ; <htmlTag :class="baz" />
|
||||
(element
|
||||
(_
|
||||
(attribute_name) @_attr
|
||||
(#match? @_attr "^:[a-z]+")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @injection.content)
|
||||
(#set! injection.language "javascript")))
|
||||
|
||||
|
||||
; Blade PHP attributes
|
||||
(element
|
||||
(_
|
||||
(tag_name) @_tag
|
||||
(#match? @_tag "^x-[a-z]+")
|
||||
(attribute
|
||||
(attribute_name) @_attr
|
||||
(#match? @_attr "^:[a-z]+")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @injection.content)
|
||||
(#set! injection.language "php-only"))))
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user