mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 00:13:28 +02:00
Compare commits
163 Commits
dependabot
...
6fffaf6a7d
Author | SHA1 | Date | |
---|---|---|---|
|
6fffaf6a7d | ||
|
81b4a2c9d8 | ||
|
e50e8a3638 | ||
|
0a2972980f | ||
|
a8ac3c5010 | ||
|
5c3e4b67d9 | ||
|
cb3c450125 | ||
|
f327307faa | ||
|
abe82e7474 | ||
|
bfcbef10c5 | ||
|
109c812233 | ||
|
50c1a676f1 | ||
|
ed08fbd410 | ||
|
ce351f4b11 | ||
|
a5d0a0e1c2 | ||
|
ba506a51cb | ||
|
0ebe450991 | ||
|
37fe42d05d | ||
|
55167c21df | ||
|
8acfc55280 | ||
|
1426e1feb5 | ||
|
1bf9879778 | ||
|
23a647aee8 | ||
|
c2b582aa45 | ||
|
0ae37dc52b | ||
|
dbb472d4c4 | ||
|
46e3617f6b | ||
|
c0921202a0 | ||
|
ffe513d6aa | ||
|
d015eff4aa | ||
|
a4d780483b | ||
|
ea2541f424 | ||
|
69713975c6 | ||
|
2dddace12e | ||
|
66737c6a4f | ||
|
fbf6407ab3 | ||
|
1f020b1d72 | ||
|
9acfa51209 | ||
|
2d7436fc20 | ||
|
1388166570 | ||
|
342f2a982d | ||
|
5bfc7bfc07 | ||
|
5609e3b3c2 | ||
|
c614467be8 | ||
|
c531b7a4fa | ||
|
92b0a2f414 | ||
|
378b27cad9 | ||
|
209558645a | ||
|
987b04bd26 | ||
|
322bb1c189 | ||
|
34e0f7e82f | ||
|
97293c9f36 | ||
|
71038266e8 | ||
|
05a99c2cca | ||
|
14030d0b63 | ||
|
ff376b0d1a | ||
|
d25726f573 | ||
|
8f2af68b30 | ||
|
fe8e21a07f | ||
|
d0218f7e78 | ||
|
e2333f60ae | ||
|
70187c430c | ||
|
8058fefef0 | ||
|
0928e5ea1c | ||
|
b391185716 | ||
|
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 |
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
|
||||
|
12
.github/workflows/release.yml
vendored
12
.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
|
||||
@@ -104,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
|
||||
@@ -235,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
|
||||
|
555
Cargo.lock
generated
555
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -41,16 +41,17 @@ 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.23.0"
|
||||
bitflags = "2.9"
|
||||
unicode-segmentation = "1.2"
|
||||
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
|
||||
foldhash = "0.1"
|
||||
foldhash = "0.2"
|
||||
parking_lot = "0.12"
|
||||
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"
|
||||
|
||||
[workspace.package]
|
||||
version = "25.7.1"
|
||||
|
@@ -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
|
||||
```
|
||||
|
@@ -60,10 +60,11 @@
|
||||
| `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"
|
||||
| `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. |
|
||||
| `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`, `win32-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` |
|
||||
| `kitty-keyboard-protocol` | Whether to enable Kitty Keyboard Protocol. Can be `enabled`, `disabled` or `auto` | `auto` |
|
||||
|
||||
### `[editor.clipboard-provider]` Section
|
||||
|
||||
@@ -223,6 +224,25 @@ Example:
|
||||
!.gitattributes
|
||||
```
|
||||
|
||||
### `[editor.file-explorer]` Section
|
||||
|
||||
In addition to the options for the file picker and global search, a similar set of options is presented to configure the file explorer separately. However, unlike the file picker, the defaults are set to avoid ignoring most files.
|
||||
|
||||
Note that the ignore files consulted by the file explorer when `ignore` is set to true are the same ones used by the file picker, including the aforementioned Helix-specific ignore files.
|
||||
|
||||
|
||||
| Key | Description | Default |
|
||||
|--|--|---------|
|
||||
|`hidden` | Enables ignoring hidden files | `false`
|
||||
|`follow-symlinks` | Follow symlinks instead of ignoring them | `false`
|
||||
|`parents` | Enables reading ignore files from parent directories | `false`
|
||||
|`ignore` | Enables reading `.ignore` files | `false`
|
||||
|`git-ignore` | Enables reading `.gitignore` files | `false`
|
||||
|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `false`
|
||||
|`git-exclude` | Enables reading `.git/info/exclude` files | `false`
|
||||
|`flatten-dirs` | Enables flattening single child directories | `true`
|
||||
|
||||
|
||||
### `[editor.auto-pairs]` Section
|
||||
|
||||
Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
|
||||
@@ -453,7 +473,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` |
|
||||
|
@@ -7,7 +7,7 @@
|
||||
| amber | ✓ | | | | | `amber-lsp` |
|
||||
| astro | ✓ | | | | | `astro-ls` |
|
||||
| awk | ✓ | ✓ | | | | `awk-language-server` |
|
||||
| bash | ✓ | ✓ | ✓ | | ✓ | `bash-language-server` |
|
||||
| bash | ✓ | ✓ | ✓ | ✓ | ✓ | `bash-language-server` |
|
||||
| bass | ✓ | | | | | `bass` |
|
||||
| beancount | ✓ | | | | | `beancount-language-server` |
|
||||
| bibtex | ✓ | | | | | `texlab` |
|
||||
@@ -31,11 +31,13 @@
|
||||
| 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 | ✓ | | | | | |
|
||||
@@ -44,18 +46,21 @@
|
||||
| dhall | ✓ | ✓ | | | | `dhall-lsp-server` |
|
||||
| diff | ✓ | | | | | |
|
||||
| djot | ✓ | | | | | |
|
||||
| docker-compose | ✓ | ✓ | ✓ | | | `docker-compose-langserver`, `yaml-language-server` |
|
||||
| dockerfile | ✓ | ✓ | | | | `docker-langserver` |
|
||||
| docker-bake | ✓ | ✓ | ✓ | ✓ | ✓ | `docker-language-server` |
|
||||
| docker-compose | ✓ | ✓ | ✓ | | | `docker-compose-langserver`, `yaml-language-server`, `docker-language-server` |
|
||||
| dockerfile | ✓ | ✓ | | | | `docker-langserver`, `docker-language-server` |
|
||||
| dot | ✓ | | | | | `dot-language-server` |
|
||||
| doxyfile | ✓ | ✓ | ✓ | ✓ | | |
|
||||
| dtd | ✓ | | | | | |
|
||||
| dune | ✓ | | | | | |
|
||||
| dunstrc | ✓ | | | | | |
|
||||
| earthfile | ✓ | ✓ | ✓ | | | `earthlyls` |
|
||||
| edoc | ✓ | | | | | |
|
||||
| eex | ✓ | | | | | |
|
||||
| eiffel | ✓ | ✓ | ✓ | | | `eiffel-language-server` |
|
||||
| ejs | ✓ | | | | | |
|
||||
| elisp | ✓ | | | ✓ | | |
|
||||
| elixir | ✓ | ✓ | ✓ | ✓ | ✓ | `elixir-ls` |
|
||||
| elixir | ✓ | ✓ | ✓ | ✓ | ✓ | `elixir-ls`, `expert` |
|
||||
| elm | ✓ | ✓ | | ✓ | | `elm-language-server` |
|
||||
| elvish | ✓ | | | | | `elvish` |
|
||||
| env | ✓ | ✓ | | | | |
|
||||
@@ -66,6 +71,7 @@
|
||||
| fga | ✓ | ✓ | ✓ | | | |
|
||||
| fidl | ✓ | | | | | |
|
||||
| fish | ✓ | ✓ | ✓ | | | `fish-lsp` |
|
||||
| flatbuffers | ✓ | | | | | |
|
||||
| forth | ✓ | | | | | `forth-lsp` |
|
||||
| fortran | ✓ | | ✓ | | | `fortls` |
|
||||
| fsharp | ✓ | | | | | `fsautocomplete` |
|
||||
@@ -75,18 +81,20 @@
|
||||
| gherkin | ✓ | | | | | |
|
||||
| ghostty | ✓ | | | | | |
|
||||
| git-attributes | ✓ | | | | | |
|
||||
| git-cliff-config | ✓ | ✓ | | | ✓ | `taplo`, `tombi` |
|
||||
| git-commit | ✓ | ✓ | | | | |
|
||||
| git-config | ✓ | ✓ | | | | |
|
||||
| git-ignore | ✓ | | | | | |
|
||||
| git-notes | ✓ | | | | | |
|
||||
| git-rebase | ✓ | | | | | |
|
||||
| gitlab-ci | ✓ | ✓ | ✓ | ✓ | ✓ | `yaml-language-server`, `gitlab-ci-ls` |
|
||||
| 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 | ✓ | | | | | |
|
||||
| go-format-string | ✓ | | | | ✓ | |
|
||||
| godot-resource | ✓ | ✓ | | | | |
|
||||
| gomod | ✓ | | | | | `gopls` |
|
||||
| gotmpl | ✓ | | | | | `gopls` |
|
||||
@@ -100,7 +108,8 @@
|
||||
| haskell | ✓ | ✓ | | | | `haskell-language-server-wrapper` |
|
||||
| haskell-persistent | ✓ | | | | | |
|
||||
| hcl | ✓ | ✓ | ✓ | | | `terraform-ls` |
|
||||
| heex | ✓ | ✓ | | | | `elixir-ls` |
|
||||
| hdl | ✓ | | | | | `hdls` |
|
||||
| heex | ✓ | ✓ | | | | `elixir-ls`, `expert` |
|
||||
| helm | ✓ | | | | | `helm_ls` |
|
||||
| hocon | ✓ | ✓ | ✓ | | | |
|
||||
| hoon | ✓ | | | | | |
|
||||
@@ -114,8 +123,8 @@
|
||||
| ini | ✓ | | | | | |
|
||||
| ink | ✓ | | | | | |
|
||||
| inko | ✓ | ✓ | ✓ | ✓ | | |
|
||||
| janet | ✓ | | ✓ | | | |
|
||||
| java | ✓ | ✓ | ✓ | | ✓ | `jdtls` |
|
||||
| janet | ✓ | | ✓ | | ✓ | |
|
||||
| java | ✓ | ✓ | ✓ | ✓ | ✓ | `jdtls` |
|
||||
| javascript | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| jinja | ✓ | | | | | |
|
||||
| jjconfig | ✓ | ✓ | ✓ | | | `taplo`, `tombi` |
|
||||
@@ -131,22 +140,23 @@
|
||||
| jsonnet | ✓ | | | | | `jsonnet-language-server` |
|
||||
| jsx | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| julia | ✓ | ✓ | ✓ | | | `julia` |
|
||||
| just | ✓ | ✓ | ✓ | | | `just-lsp` |
|
||||
| kdl | ✓ | ✓ | ✓ | | | |
|
||||
| just | ✓ | ✓ | ✓ | ✓ | | `just-lsp` |
|
||||
| kconfig | ✓ | | ✓ | | | |
|
||||
| kdl | ✓ | ✓ | ✓ | ✓ | | |
|
||||
| koka | ✓ | | ✓ | | | `koka` |
|
||||
| kotlin | ✓ | ✓ | ✓ | | | `kotlin-language-server` |
|
||||
| koto | ✓ | ✓ | ✓ | | | `koto-ls` |
|
||||
| kotlin | ✓ | ✓ | ✓ | ✓ | | `kotlin-language-server` |
|
||||
| koto | ✓ | ✓ | ✓ | | ✓ | `koto-ls` |
|
||||
| latex | ✓ | ✓ | | | | `texlab` |
|
||||
| ld | ✓ | | ✓ | | | |
|
||||
| ldif | ✓ | | | | | |
|
||||
| lean | ✓ | | | | | `lean` |
|
||||
| lean | ✓ | | | | | `lake` |
|
||||
| ledger | ✓ | | | | | |
|
||||
| llvm | ✓ | ✓ | ✓ | | | |
|
||||
| llvm-mir | ✓ | ✓ | ✓ | | | |
|
||||
| llvm-mir-yaml | ✓ | | ✓ | | | |
|
||||
| log | ✓ | | | | | |
|
||||
| lpf | ✓ | | | | | |
|
||||
| lua | ✓ | ✓ | ✓ | | | `lua-language-server` |
|
||||
| lua | ✓ | ✓ | ✓ | | ✓ | `lua-language-server` |
|
||||
| luap | ✓ | | | | | |
|
||||
| luau | ✓ | ✓ | ✓ | | | `luau-lsp` |
|
||||
| mail | ✓ | ✓ | | | | |
|
||||
@@ -159,16 +169,18 @@
|
||||
| mermaid | ✓ | | | | | |
|
||||
| meson | ✓ | | ✓ | | | `mesonlsp` |
|
||||
| mint | | | | | | `mint` |
|
||||
| miseconfig | ✓ | ✓ | ✓ | | | `taplo`, `tombi` |
|
||||
| mojo | ✓ | ✓ | ✓ | | | `pixi` |
|
||||
| move | ✓ | | | | | |
|
||||
| msbuild | ✓ | | ✓ | | | |
|
||||
| nasm | ✓ | ✓ | | | | `asm-lsp` |
|
||||
| nearley | ✓ | | | | ✓ | |
|
||||
| nestedtext | ✓ | ✓ | ✓ | | | |
|
||||
| nginx | ✓ | | | | | |
|
||||
| nickel | ✓ | | ✓ | | | `nls` |
|
||||
| nim | ✓ | ✓ | ✓ | | | `nimlangserver` |
|
||||
| nix | ✓ | ✓ | ✓ | | ✓ | `nil`, `nixd` |
|
||||
| nu | ✓ | | | | | `nu` |
|
||||
| nu | ✓ | ✓ | ✓ | | | `nu` |
|
||||
| nunjucks | ✓ | | | | | |
|
||||
| ocaml | ✓ | | ✓ | | | `ocamllsp` |
|
||||
| ocaml-interface | ✓ | | | | | `ocamllsp` |
|
||||
@@ -182,8 +194,9 @@
|
||||
| pem | ✓ | | | | | |
|
||||
| perl | ✓ | ✓ | ✓ | | | `perlnavigator` |
|
||||
| pest | ✓ | ✓ | ✓ | | | `pest-language-server` |
|
||||
| php | ✓ | ✓ | ✓ | ✓ | | `intelephense` |
|
||||
| php | ✓ | ✓ | ✓ | ✓ | ✓ | `intelephense` |
|
||||
| php-only | ✓ | | | ✓ | | |
|
||||
| pip-requirements | ✓ | | | | | |
|
||||
| pkgbuild | ✓ | ✓ | ✓ | | | `termux-language-server`, `bash-language-server` |
|
||||
| pkl | ✓ | | ✓ | | | `pkl-lsp` |
|
||||
| po | ✓ | ✓ | | | | |
|
||||
@@ -193,7 +206,7 @@
|
||||
| prisma | ✓ | ✓ | | | | `prisma-language-server` |
|
||||
| prolog | ✓ | | ✓ | | | `swipl` |
|
||||
| properties | ✓ | ✓ | | | | |
|
||||
| protobuf | ✓ | ✓ | ✓ | | | `buf`, `pb`, `protols` |
|
||||
| protobuf | ✓ | ✓ | ✓ | ✓ | | `buf`, `pb`, `protols` |
|
||||
| prql | ✓ | | | | | |
|
||||
| pug | ✓ | | | | | |
|
||||
| purescript | ✓ | ✓ | | | | `purescript-language-server` |
|
||||
@@ -208,7 +221,8 @@
|
||||
| rescript | ✓ | ✓ | | | | `rescript-language-server` |
|
||||
| rmarkdown | ✓ | | ✓ | | | `R` |
|
||||
| robot | ✓ | | | | | `robotframework_ls` |
|
||||
| ron | ✓ | | ✓ | | | |
|
||||
| robots.txt | ✓ | ✓ | | ✓ | | |
|
||||
| ron | ✓ | | ✓ | ✓ | ✓ | |
|
||||
| rst | ✓ | | | | | |
|
||||
| ruby | ✓ | ✓ | ✓ | ✓ | ✓ | `ruby-lsp`, `solargraph` |
|
||||
| rust | ✓ | ✓ | ✓ | ✓ | ✓ | `rust-analyzer` |
|
||||
@@ -216,10 +230,12 @@
|
||||
| rust-format-args-macro | ✓ | ✓ | ✓ | | ✓ | |
|
||||
| sage | ✓ | ✓ | | | | |
|
||||
| scala | ✓ | ✓ | ✓ | | | `metals` |
|
||||
| scheme | ✓ | | ✓ | | ✓ | |
|
||||
| scheme | ✓ | ✓ | ✓ | | ✓ | |
|
||||
| scss | ✓ | | | | ✓ | `vscode-css-language-server` |
|
||||
| shellcheckrc | ✓ | ✓ | | | | |
|
||||
| slang | ✓ | ✓ | ✓ | | | `slangd` |
|
||||
| slint | ✓ | ✓ | ✓ | | | `slint-lsp` |
|
||||
| slisp | ✓ | | | ✓ | | |
|
||||
| smali | ✓ | | ✓ | | | |
|
||||
| smithy | ✓ | | | | | `cs` |
|
||||
| sml | ✓ | | | | | |
|
||||
@@ -232,11 +248,13 @@
|
||||
| sshclientconfig | ✓ | | | | | |
|
||||
| starlark | ✓ | ✓ | ✓ | | ✓ | `starpls` |
|
||||
| strace | ✓ | | | | | |
|
||||
| strictdoc | ✓ | | | ✓ | | |
|
||||
| supercollider | ✓ | | | | | |
|
||||
| svelte | ✓ | | ✓ | | | `svelteserver` |
|
||||
| sway | ✓ | ✓ | ✓ | | | `forc` |
|
||||
| swift | ✓ | ✓ | | | | `sourcekit-lsp` |
|
||||
| swift | ✓ | ✓ | | | ✓ | `sourcekit-lsp` |
|
||||
| systemd | ✓ | | | | | `systemd-lsp` |
|
||||
| systemverilog | ✓ | | | | | |
|
||||
| t32 | ✓ | | | | | |
|
||||
| tablegen | ✓ | ✓ | ✓ | | | |
|
||||
| tact | ✓ | ✓ | ✓ | | | |
|
||||
@@ -251,7 +269,7 @@
|
||||
| tlaplus | ✓ | | | | | |
|
||||
| todotxt | ✓ | | | | | |
|
||||
| toml | ✓ | ✓ | | | ✓ | `taplo`, `tombi` |
|
||||
| tsq | ✓ | | | | | `ts_query_ls` |
|
||||
| tsq | ✓ | | | | ✓ | `ts_query_ls` |
|
||||
| tsx | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| twig | ✓ | | | | | |
|
||||
| typescript | ✓ | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
@@ -273,7 +291,8 @@
|
||||
| webc | ✓ | | | | | |
|
||||
| werk | ✓ | | | | | |
|
||||
| wesl | ✓ | ✓ | | | | |
|
||||
| wgsl | ✓ | | | | | `wgsl-analyzer` |
|
||||
| wgsl | ✓ | ✓ | ✓ | ✓ | ✓ | `wgsl-analyzer` |
|
||||
| wikitext | ✓ | | | | | `wikitext-lsp` |
|
||||
| wit | ✓ | | ✓ | | | |
|
||||
| wren | ✓ | ✓ | ✓ | | | |
|
||||
| xit | ✓ | | | | | |
|
||||
|
@@ -111,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.
|
||||
|
||||
|
@@ -2,6 +2,17 @@
|
||||
|
||||
To use a theme add `theme = "<name>"` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme <name>`.
|
||||
|
||||
Separate themes can be configured for light and dark modes. On terminals supporting [mode 2031 dark/light detection](https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md), the theme mode is detected from the terminal.
|
||||
|
||||
```toml
|
||||
[theme]
|
||||
dark = "catppuccin_frappe"
|
||||
light = "catppuccin_latte"
|
||||
## Optional. Used if the terminal doesn't declare a preference.
|
||||
## Defaults to the theme set for `dark` if not specified.
|
||||
# fallback = "catppuccin_frappe"
|
||||
```
|
||||
|
||||
## Creating a theme
|
||||
|
||||
Create a file with the name of your theme as the file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes` or `%AppData%\helix\themes` on Windows). The directory might have to be created beforehand.
|
||||
|
@@ -46,6 +46,8 @@ in
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
|
||||
propagatedBuildInputs = [ runtimeDir ];
|
||||
|
||||
nativeBuildInputs = [
|
||||
installShellFiles
|
||||
git
|
||||
|
@@ -87,8 +87,6 @@
|
||||
$CC -c src/parser.c -o parser.o $FLAGS
|
||||
$CXX -shared -o $NAME.so *.o
|
||||
|
||||
ls -al
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
|
@@ -30,7 +30,7 @@ unicode-segmentation.workspace = true
|
||||
# For now lets lock the version to avoid rendering glitches
|
||||
# when installing without `--locked`
|
||||
unicode-width = "=0.1.12"
|
||||
unicode-general-category = "1.0"
|
||||
unicode-general-category = "1.1"
|
||||
slotmap.workspace = true
|
||||
tree-house.workspace = true
|
||||
once_cell = "1.21"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, collections::HashMap, iter};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use tree_house::TREE_SITTER_MATCH_LIMIT;
|
||||
@@ -214,7 +214,10 @@ fn whitespace_with_same_width(text: RopeSlice) -> String {
|
||||
if grapheme == "\t" {
|
||||
s.push('\t');
|
||||
} else {
|
||||
s.extend(std::iter::repeat(' ').take(grapheme_width(&Cow::from(grapheme))));
|
||||
s.extend(std::iter::repeat_n(
|
||||
' ',
|
||||
grapheme_width(&Cow::from(grapheme)),
|
||||
));
|
||||
}
|
||||
}
|
||||
s
|
||||
@@ -243,10 +246,10 @@ pub fn normalize_indentation(
|
||||
original_len += 1;
|
||||
}
|
||||
if indent_style == IndentStyle::Tabs {
|
||||
dst.extend(iter::repeat('\t').take(len / tab_width));
|
||||
dst.extend(std::iter::repeat_n('\t', len / tab_width));
|
||||
len %= tab_width;
|
||||
}
|
||||
dst.extend(iter::repeat(' ').take(len));
|
||||
dst.extend(std::iter::repeat_n(' ', len));
|
||||
original_len
|
||||
}
|
||||
|
||||
|
@@ -361,7 +361,7 @@ impl Transform {
|
||||
}
|
||||
}
|
||||
FormatItem::Conditional(i, ref if_, ref else_) => {
|
||||
if cap.get_group(i).map_or(true, |mat| mat.is_empty()) {
|
||||
if cap.get_group(i).is_none_or(|mat| mat.is_empty()) {
|
||||
buf.push_str(else_)
|
||||
} else {
|
||||
buf.push_str(if_)
|
||||
|
@@ -562,15 +562,15 @@ impl Syntax {
|
||||
self.inner.tree_for_byte_range(start, end)
|
||||
}
|
||||
|
||||
pub fn named_descendant_for_byte_range(&self, start: u32, end: u32) -> Option<Node> {
|
||||
pub fn named_descendant_for_byte_range(&self, start: u32, end: u32) -> Option<Node<'_>> {
|
||||
self.inner.named_descendant_for_byte_range(start, end)
|
||||
}
|
||||
|
||||
pub fn descendant_for_byte_range(&self, start: u32, end: u32) -> Option<Node> {
|
||||
pub fn descendant_for_byte_range(&self, start: u32, end: u32) -> Option<Node<'_>> {
|
||||
self.inner.descendant_for_byte_range(start, end)
|
||||
}
|
||||
|
||||
pub fn walk(&self) -> TreeCursor {
|
||||
pub fn walk(&self) -> TreeCursor<'_> {
|
||||
self.inner.walk()
|
||||
}
|
||||
|
||||
@@ -1073,7 +1073,7 @@ fn node_is_visible(node: &Node) -> bool {
|
||||
node.is_missing() || (node.is_named() && node.grammar().node_kind_is_visible(node.kind_id()))
|
||||
}
|
||||
|
||||
fn format_anonymous_node_kind(kind: &str) -> Cow<str> {
|
||||
fn format_anonymous_node_kind(kind: &str) -> Cow<'_, str> {
|
||||
if kind.contains('"') {
|
||||
Cow::Owned(kind.replace('"', "\\\""))
|
||||
} else {
|
||||
@@ -1130,7 +1130,6 @@ fn pretty_print_tree_impl<W: fmt::Write>(
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
@@ -270,6 +270,7 @@ pub enum LanguageServerFeature {
|
||||
WorkspaceSymbols,
|
||||
// Symbols, use bitflags, see above?
|
||||
Diagnostics,
|
||||
PullDiagnostics,
|
||||
RenameSymbol,
|
||||
InlayHints,
|
||||
DocumentColors,
|
||||
@@ -294,6 +295,7 @@ impl Display for LanguageServerFeature {
|
||||
DocumentSymbols => "document-symbols",
|
||||
WorkspaceSymbols => "workspace-symbols",
|
||||
Diagnostics => "diagnostics",
|
||||
PullDiagnostics => "pull-diagnostics",
|
||||
RenameSymbol => "rename-symbol",
|
||||
InlayHints => "inlay-hints",
|
||||
DocumentColors => "document-colors",
|
||||
@@ -446,6 +448,7 @@ pub enum DebugArgumentValue {
|
||||
String(String),
|
||||
Array(Vec<String>),
|
||||
Boolean(bool),
|
||||
Table(HashMap<String, String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
|
@@ -520,7 +520,7 @@ impl ChangeSet {
|
||||
pos
|
||||
}
|
||||
|
||||
pub fn changes_iter(&self) -> ChangeIterator {
|
||||
pub fn changes_iter(&self) -> ChangeIterator<'_> {
|
||||
ChangeIterator::new(self)
|
||||
}
|
||||
}
|
||||
@@ -753,7 +753,7 @@ impl Transaction {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn changes_iter(&self) -> ChangeIterator {
|
||||
pub fn changes_iter(&self) -> ChangeIterator<'_> {
|
||||
self.changes.changes_iter()
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -22,8 +22,8 @@ license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
url = {version = "2.5.4", features = ["serde"]}
|
||||
|
||||
[features]
|
||||
|
@@ -129,6 +129,7 @@ pub struct CodeActionParams {
|
||||
/// response for CodeActionRequest
|
||||
pub type CodeActionResponse = Vec<CodeActionOrCommand>;
|
||||
|
||||
#[allow(clippy::large_enum_variant)] // TODO: In a separate PR attempt the `Box<CodeAction>` pattern.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CodeActionOrCommand {
|
||||
|
@@ -40,14 +40,6 @@ pub struct WorkDoneProgressCancelParams {
|
||||
pub token: ProgressToken,
|
||||
}
|
||||
|
||||
/// Options to signal work done progress support in server capabilities.
|
||||
#[derive(Debug, Eq, PartialEq, Default, Deserialize, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkDoneProgressOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub work_done_progress: Option<bool>,
|
||||
}
|
||||
|
||||
/// An optional token that a server can use to report work done progress
|
||||
#[derive(Debug, Eq, PartialEq, Default, Deserialize, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@@ -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"
|
||||
|
@@ -372,6 +372,7 @@ impl Client {
|
||||
Some(OneOf::Left(true) | OneOf::Right(_))
|
||||
),
|
||||
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
|
||||
LanguageServerFeature::PullDiagnostics => capabilities.diagnostic_provider.is_some(),
|
||||
LanguageServerFeature::RenameSymbol => matches!(
|
||||
capabilities.rename_provider,
|
||||
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
|
||||
@@ -602,6 +603,9 @@ impl Client {
|
||||
did_rename: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
diagnostic: Some(lsp::DiagnosticWorkspaceClientCapabilities {
|
||||
refresh_support: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document: Some(lsp::TextDocumentClientCapabilities {
|
||||
@@ -679,6 +683,10 @@ impl Client {
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
diagnostic: Some(lsp::DiagnosticClientCapabilities {
|
||||
dynamic_registration: Some(false),
|
||||
related_document_support: Some(true),
|
||||
}),
|
||||
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
|
||||
version_support: Some(true),
|
||||
tag_support: Some(lsp::TagSupport {
|
||||
@@ -1229,6 +1237,32 @@ impl Client {
|
||||
Some(self.call::<lsp::request::RangeFormatting>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_diagnostic(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
previous_result_id: Option<String>,
|
||||
) -> Option<impl Future<Output = Result<lsp::DocumentDiagnosticReportResult>>> {
|
||||
let capabilities = self.capabilities();
|
||||
|
||||
// Return early if the server does not support pull diagnostic.
|
||||
let identifier = match capabilities.diagnostic_provider.as_ref()? {
|
||||
lsp::DiagnosticServerCapabilities::Options(cap) => cap.identifier.clone(),
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(cap) => {
|
||||
cap.diagnostic_options.identifier.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let params = lsp::DocumentDiagnosticParams {
|
||||
text_document,
|
||||
identifier,
|
||||
previous_result_id,
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
|
||||
partial_result_params: lsp::PartialResultParams::default(),
|
||||
};
|
||||
|
||||
Some(self.call::<lsp::request::DocumentDiagnosticRequest>(params))
|
||||
}
|
||||
|
||||
pub fn text_document_document_highlight(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
|
@@ -463,6 +463,7 @@ pub enum MethodCall {
|
||||
RegisterCapability(lsp::RegistrationParams),
|
||||
UnregisterCapability(lsp::UnregistrationParams),
|
||||
ShowDocument(lsp::ShowDocumentParams),
|
||||
WorkspaceDiagnosticRefresh,
|
||||
}
|
||||
|
||||
impl MethodCall {
|
||||
@@ -494,6 +495,7 @@ impl MethodCall {
|
||||
let params: lsp::ShowDocumentParams = params.parse()?;
|
||||
Self::ShowDocument(params)
|
||||
}
|
||||
lsp::request::WorkspaceDiagnosticRefresh::METHOD => Self::WorkspaceDiagnosticRefresh,
|
||||
_ => {
|
||||
return Err(Error::Unhandled);
|
||||
}
|
||||
|
@@ -19,14 +19,14 @@ 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]
|
||||
windows-sys = { version = "0.60", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
rustix = { version = "1.0", features = ["fs"] }
|
||||
rustix = { version = "1.1", features = ["fs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
@@ -85,7 +85,7 @@ fn find_brace_end(src: &[u8]) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>) -> Cow<OsStr> {
|
||||
fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>) -> Cow<'_, OsStr> {
|
||||
use regex_automata::meta::Regex;
|
||||
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
@@ -157,7 +157,7 @@ fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>)
|
||||
/// * `${<var>:-<default>}`, `${<var>-<default>}`
|
||||
/// * `${<var>:=<default>}`, `${<var>=default}`
|
||||
///
|
||||
pub fn expand<S: AsRef<OsStr> + ?Sized>(src: &S) -> Cow<OsStr> {
|
||||
pub fn expand<S: AsRef<OsStr> + ?Sized>(src: &S) -> Cow<'_, OsStr> {
|
||||
expand_impl(src.as_ref(), |var| std::env::var_os(var))
|
||||
}
|
||||
|
||||
|
@@ -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", "crossterm"] }
|
||||
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"
|
||||
@@ -93,12 +93,12 @@ grep-searcher = "0.1.14"
|
||||
|
||||
dashmap = "6.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
crossterm = { version = "0.28", features = ["event-stream"] }
|
||||
|
||||
[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.176"
|
||||
|
||||
[build-dependencies]
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
@@ -30,32 +30,41 @@ 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;
|
||||
#[cfg_attr(windows, allow(unused_imports))]
|
||||
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"))]
|
||||
#[cfg(all(not(windows), not(feature = "integration")))]
|
||||
use tui::backend::TerminaBackend;
|
||||
|
||||
#[cfg(all(windows, not(feature = "integration")))]
|
||||
use tui::backend::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
use tui::backend::TestBackend;
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
#[cfg(all(not(windows), not(feature = "integration")))]
|
||||
type TerminalBackend = TerminaBackend;
|
||||
#[cfg(all(windows, not(feature = "integration")))]
|
||||
type TerminalBackend = CrosstermBackend<std::io::Stdout>;
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
type TerminalBackend = TestBackend;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
type TerminalEvent = termina::Event;
|
||||
#[cfg(windows)]
|
||||
type TerminalEvent = crossterm::event::Event;
|
||||
|
||||
type Terminal = tui::terminal::Terminal<TerminalBackend>;
|
||||
|
||||
pub struct Application {
|
||||
@@ -68,6 +77,8 @@ pub struct Application {
|
||||
signals: Signals,
|
||||
jobs: Jobs,
|
||||
lsp_progress: LspProgressMap,
|
||||
|
||||
theme_mode: Option<theme::Mode>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
@@ -103,14 +114,18 @@ impl Application {
|
||||
theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
|
||||
let theme_loader = theme::Loader::new(&theme_parent_dirs);
|
||||
|
||||
#[cfg(not(feature = "integration"))]
|
||||
let backend = CrosstermBackend::new(stdout(), &config.editor);
|
||||
#[cfg(all(not(windows), not(feature = "integration")))]
|
||||
let backend = TerminaBackend::new((&config.editor).into())
|
||||
.context("failed to create terminal backend")?;
|
||||
#[cfg(all(windows, not(feature = "integration")))]
|
||||
let backend = CrosstermBackend::new(std::io::stdout(), (&config.editor).into());
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
let backend = TestBackend::new(120, 150);
|
||||
|
||||
let theme_mode = backend.get_theme_mode();
|
||||
let terminal = Terminal::new(backend)?;
|
||||
let area = terminal.size().expect("couldn't get terminal size");
|
||||
let area = terminal.size();
|
||||
let mut compositor = Compositor::new(area);
|
||||
let config = Arc::new(ArcSwap::from_pointee(config));
|
||||
let handlers = handlers::setup(config.clone());
|
||||
@@ -123,7 +138,12 @@ impl Application {
|
||||
})),
|
||||
handlers,
|
||||
);
|
||||
Self::load_configured_theme(&mut editor, &config.load());
|
||||
Self::load_configured_theme(
|
||||
&mut editor,
|
||||
&config.load(),
|
||||
terminal.backend().supports_true_color(),
|
||||
theme_mode,
|
||||
);
|
||||
|
||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.keys
|
||||
@@ -214,7 +234,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
|
||||
@@ -242,6 +262,7 @@ impl Application {
|
||||
signals,
|
||||
jobs: Jobs::new(),
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
theme_mode,
|
||||
};
|
||||
|
||||
Ok(app)
|
||||
@@ -282,7 +303,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<TerminalEvent>> + Unpin,
|
||||
{
|
||||
self.render().await;
|
||||
|
||||
@@ -295,7 +316,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<TerminalEvent>> + Unpin,
|
||||
{
|
||||
loop {
|
||||
if self.editor.should_close() {
|
||||
@@ -367,7 +388,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 +417,12 @@ 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(),
|
||||
self.theme_mode,
|
||||
);
|
||||
|
||||
// Re-parse any open documents with the new language config.
|
||||
let lang_loader = self.editor.syn_loader.load();
|
||||
@@ -412,8 +438,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,12 +455,18 @@ 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,
|
||||
mode: Option<theme::Mode>,
|
||||
) {
|
||||
let true_color = terminal_true_color || config.editor.true_color || crate::true_color();
|
||||
let theme = config
|
||||
.theme
|
||||
.as_ref()
|
||||
.and_then(|theme| {
|
||||
.and_then(|theme_config| {
|
||||
let theme = theme_config.choose(mode);
|
||||
editor
|
||||
.theme_loader
|
||||
.load(theme)
|
||||
@@ -503,7 +534,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,
|
||||
@@ -511,7 +542,7 @@ impl Application {
|
||||
}
|
||||
|
||||
// redraw the terminal
|
||||
let area = self.terminal.size().expect("couldn't get terminal size");
|
||||
let area = self.terminal.size();
|
||||
self.compositor.resize(area);
|
||||
self.terminal.clear().expect("couldn't clear terminal");
|
||||
|
||||
@@ -573,24 +604,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 +683,10 @@ 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<TerminalEvent>) {
|
||||
#[cfg(not(windows))]
|
||||
use termina::escape::csi;
|
||||
|
||||
let mut cx = crate::compositor::Context {
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
@@ -643,20 +694,51 @@ impl Application {
|
||||
};
|
||||
// Handle key events
|
||||
let should_redraw = match event.unwrap() {
|
||||
CrosstermEvent::Resize(width, height) => {
|
||||
#[cfg(not(windows))]
|
||||
termina::Event::WindowResized(termina::WindowSize { rows, cols, .. }) => {
|
||||
self.terminal
|
||||
.resize(Rect::new(0, 0, cols, rows))
|
||||
.expect("Unable to resize terminal");
|
||||
|
||||
let area = self.terminal.size();
|
||||
|
||||
self.compositor.resize(area);
|
||||
|
||||
self.compositor
|
||||
.handle_event(&Event::Resize(cols, rows), &mut cx)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
// Ignore keyboard release events.
|
||||
termina::Event::Key(termina::event::KeyEvent {
|
||||
kind: termina::event::KeyEventKind::Release,
|
||||
..
|
||||
}) => false,
|
||||
#[cfg(not(windows))]
|
||||
termina::Event::Csi(csi::Csi::Mode(csi::Mode::ReportTheme(mode))) => {
|
||||
Self::load_configured_theme(
|
||||
&mut self.editor,
|
||||
&self.config.load(),
|
||||
self.terminal.backend().supports_true_color(),
|
||||
Some(mode.into()),
|
||||
);
|
||||
true
|
||||
}
|
||||
#[cfg(windows)]
|
||||
TerminalEvent::Resize(width, height) => {
|
||||
self.terminal
|
||||
.resize(Rect::new(0, 0, width, height))
|
||||
.expect("Unable to resize terminal");
|
||||
|
||||
let area = self.terminal.size().expect("couldn't get terminal size");
|
||||
let area = self.terminal.size();
|
||||
|
||||
self.compositor.resize(area);
|
||||
|
||||
self.compositor
|
||||
.handle_event(&Event::Resize(width, height), &mut cx)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
// Ignore keyboard release events.
|
||||
CrosstermEvent::Key(crossterm::event::KeyEvent {
|
||||
crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
kind: crossterm::event::KeyEventKind::Release,
|
||||
..
|
||||
}) => false,
|
||||
@@ -1021,6 +1103,26 @@ impl Application {
|
||||
let result = self.handle_show_document(params, offset_encoding);
|
||||
Ok(json!(result))
|
||||
}
|
||||
Ok(MethodCall::WorkspaceDiagnosticRefresh) => {
|
||||
let language_server = language_server!().id();
|
||||
|
||||
let documents: Vec<_> = self
|
||||
.editor
|
||||
.documents
|
||||
.values()
|
||||
.filter(|x| x.supports_language_server(language_server))
|
||||
.map(|x| x.id())
|
||||
.collect();
|
||||
|
||||
for document in documents {
|
||||
handlers::diagnostics::request_document_diagnostics(
|
||||
&mut self.editor,
|
||||
document,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(serde_json::Value::Null)
|
||||
}
|
||||
};
|
||||
|
||||
let language_server = language_server!();
|
||||
@@ -1099,36 +1201,60 @@ 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(all(not(feature = "integration"), not(windows)))]
|
||||
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<TerminalEvent>> + Unpin {
|
||||
use termina::{escape::csi, Terminal as _};
|
||||
let reader = self.terminal.backend().terminal().event_reader();
|
||||
termina::EventStream::new(reader, |event| {
|
||||
// Accept either non-escape sequences or theme mode updates.
|
||||
!event.is_escape()
|
||||
|| matches!(
|
||||
event,
|
||||
termina::Event::Csi(csi::Csi::Mode(csi::Mode::ReportTheme(_)))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "integration"), windows))]
|
||||
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<TerminalEvent>> + Unpin {
|
||||
crossterm::event::EventStream::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<TerminalEvent>> + 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<TerminalEvent>;
|
||||
|
||||
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<TerminalEvent>> + 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;
|
||||
|
||||
|
@@ -22,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},
|
||||
@@ -46,6 +47,7 @@ use helix_core::{
|
||||
use helix_view::{
|
||||
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
|
||||
editor::Action,
|
||||
expansion,
|
||||
info::Info,
|
||||
input::KeyEvent,
|
||||
keyboard::KeyCode,
|
||||
@@ -3199,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| {
|
||||
@@ -5368,6 +5372,7 @@ fn rotate_selections_last(cx: &mut Context) {
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ReorderStrategy {
|
||||
RotateForward,
|
||||
RotateBackward,
|
||||
@@ -5380,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);
|
||||
}
|
||||
|
||||
@@ -6059,7 +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)"),
|
||||
("x", "(X)HTML element (tree-sitter)"),
|
||||
(" ", "... or any character acting as a pair"),
|
||||
];
|
||||
|
||||
@@ -6237,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> {
|
||||
@@ -6449,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))]
|
||||
{
|
||||
|
@@ -164,6 +164,13 @@ pub fn dap_start_impl(
|
||||
arr.iter().map(|v| v.replace(&pattern, ¶m)).collect(),
|
||||
),
|
||||
DebugArgumentValue::Boolean(_) => value,
|
||||
DebugArgumentValue::Table(map) => DebugArgumentValue::Table(
|
||||
map.into_iter()
|
||||
.map(|(mk, mv)| {
|
||||
(mk.replace(&pattern, ¶m), mv.replace(&pattern, ¶m))
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -182,6 +189,9 @@ pub fn dap_start_impl(
|
||||
DebugArgumentValue::Boolean(bool) => {
|
||||
args.insert(k, to_value(bool).unwrap());
|
||||
}
|
||||
DebugArgumentValue::Table(map) => {
|
||||
args.insert(k, to_value(map).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -100,7 +100,7 @@ struct PickerDiagnostic {
|
||||
diag: lsp::Diagnostic,
|
||||
}
|
||||
|
||||
fn location_to_file_location(location: &Location) -> Option<FileLocation> {
|
||||
fn location_to_file_location(location: &Location) -> Option<FileLocation<'_>> {
|
||||
let path = location.uri.as_path()?;
|
||||
let line = Some((
|
||||
location.range.start.line as usize,
|
||||
@@ -589,7 +589,7 @@ struct CodeActionOrCommandItem {
|
||||
|
||||
impl ui::menu::Item for CodeActionOrCommandItem {
|
||||
type Data = ();
|
||||
fn format(&self, _data: &Self::Data) -> Row {
|
||||
fn format(&self, _data: &Self::Data) -> Row<'_> {
|
||||
match &self.lsp_item {
|
||||
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
|
||||
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
|
||||
@@ -935,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);
|
||||
}
|
||||
@@ -1140,7 +1146,7 @@ pub fn rename_symbol(cx: &mut Context) {
|
||||
|
||||
let Some(language_server) = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
|
||||
.find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
|
||||
.find(|ls| language_server_id.is_none_or(|id| id == ls.id()))
|
||||
else {
|
||||
cx.editor
|
||||
.set_error("No configured language server supports symbol renaming");
|
||||
|
@@ -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
|
||||
},
|
||||
},
|
||||
@@ -3735,7 +3740,7 @@ pub(super) fn command_mode(cx: &mut Context) {
|
||||
cx.push_layer(Box::new(prompt));
|
||||
}
|
||||
|
||||
fn command_line_doc(input: &str) -> Option<Cow<str>> {
|
||||
fn command_line_doc(input: &str) -> Option<Cow<'_, str>> {
|
||||
let (command, _, _) = command_line::split(input);
|
||||
let command = TYPABLE_COMMAND_MAP.get(command)?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
@@ -136,6 +136,11 @@ impl Compositor {
|
||||
Some(self.layers.remove(idx))
|
||||
}
|
||||
|
||||
pub fn remove_type<T: 'static>(&mut self) {
|
||||
let type_name = std::any::type_name::<T>();
|
||||
self.layers
|
||||
.retain(|component| component.type_name() != type_name);
|
||||
}
|
||||
pub fn handle_event(&mut self, event: &Event, cx: &mut Context) -> bool {
|
||||
// If it is a key event, a macro is being recorded, and a macro isn't being replayed,
|
||||
// push the key event to the recording.
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use crate::keymap;
|
||||
use crate::keymap::{merge_keys, KeyTrie};
|
||||
use helix_loader::merge_toml_values;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::{document::Mode, theme};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
@@ -11,7 +11,7 @@ use toml::de::Error as TomlError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
pub theme: Option<String>,
|
||||
pub theme: Option<theme::Config>,
|
||||
pub keys: HashMap<Mode, KeyTrie>,
|
||||
pub editor: helix_view::editor::Config,
|
||||
}
|
||||
@@ -19,7 +19,7 @@ pub struct Config {
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigRaw {
|
||||
pub theme: Option<String>,
|
||||
pub theme: Option<theme::Config>,
|
||||
pub keys: Option<HashMap<Mode, KeyTrie>>,
|
||||
pub editor: Option<toml::Value>,
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use diagnostics::PullAllDocumentsDiagnosticHandler;
|
||||
use helix_event::AsyncHook;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::events;
|
||||
use crate::handlers::auto_save::AutoSaveHandler;
|
||||
use crate::handlers::diagnostics::PullDiagnosticsHandler;
|
||||
use crate::handlers::signature_help::SignatureHelpHandler;
|
||||
|
||||
pub use helix_view::handlers::{word_index, Handlers};
|
||||
@@ -14,8 +16,9 @@ use self::document_colors::DocumentColorsHandler;
|
||||
|
||||
mod auto_save;
|
||||
pub mod completion;
|
||||
mod diagnostics;
|
||||
pub mod diagnostics;
|
||||
mod document_colors;
|
||||
mod prompt;
|
||||
mod signature_help;
|
||||
mod snippet;
|
||||
|
||||
@@ -27,6 +30,8 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
let auto_save = AutoSaveHandler::new().spawn();
|
||||
let document_colors = DocumentColorsHandler::default().spawn();
|
||||
let word_index = word_index::Handler::spawn();
|
||||
let pull_diagnostics = PullDiagnosticsHandler::default().spawn();
|
||||
let pull_all_documents_diagnostics = PullAllDocumentsDiagnosticHandler::default().spawn();
|
||||
|
||||
let handlers = Handlers {
|
||||
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
|
||||
@@ -34,6 +39,8 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
auto_save,
|
||||
document_colors,
|
||||
word_index,
|
||||
pull_diagnostics,
|
||||
pull_all_documents_diagnostics,
|
||||
};
|
||||
|
||||
helix_view::handlers::register_hooks(&handlers);
|
||||
@@ -43,5 +50,6 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
diagnostics::register_hooks(&handlers);
|
||||
snippet::register_hooks(&handlers);
|
||||
document_colors::register_hooks(&handlers);
|
||||
prompt::register_hooks(&handlers);
|
||||
handlers
|
||||
}
|
||||
|
@@ -67,6 +67,7 @@ impl LspCompletionItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)] // TODO: In a separate PR attempt the `Box<LspCompletionItem>` pattern.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum CompletionItem {
|
||||
Lsp(LspCompletionItem),
|
||||
|
@@ -87,7 +87,7 @@ impl helix_event::AsyncHook for CompletionHandler {
|
||||
if self
|
||||
.trigger
|
||||
.or(self.in_flight)
|
||||
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
|
||||
.is_none_or(|trigger| trigger.doc != doc || trigger.view != view)
|
||||
{
|
||||
self.trigger = Some(Trigger {
|
||||
pos: trigger_pos,
|
||||
|
@@ -1,12 +1,28 @@
|
||||
use helix_event::{register_hook, send_blocking};
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use std::collections::HashSet;
|
||||
use std::mem;
|
||||
use std::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use helix_core::diagnostic::DiagnosticProvider;
|
||||
use helix_core::syntax::config::LanguageServerFeature;
|
||||
use helix_core::Uri;
|
||||
use helix_event::{cancelable_future, register_hook, send_blocking};
|
||||
use helix_lsp::{lsp, LanguageServerId};
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::DiagnosticsDidChange;
|
||||
use helix_view::events::{
|
||||
DiagnosticsDidChange, DocumentDidChange, DocumentDidOpen, LanguageServerInitialized,
|
||||
};
|
||||
use helix_view::handlers::diagnostics::DiagnosticEvent;
|
||||
use helix_view::handlers::lsp::{PullAllDocumentsDiagnosticsEvent, PullDiagnosticsEvent};
|
||||
use helix_view::handlers::Handlers;
|
||||
use helix_view::{DocumentId, Editor};
|
||||
|
||||
use crate::events::OnModeSwitch;
|
||||
use crate::job;
|
||||
|
||||
pub(super) fn register_hooks(_handlers: &Handlers) {
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
|
||||
if event.editor.mode != Mode::Insert {
|
||||
for (view, _) in event.editor.tree.views_mut() {
|
||||
@@ -21,4 +37,265 @@ pub(super) fn register_hooks(_handlers: &Handlers) {
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.pull_diagnostics.clone();
|
||||
let tx_all_documents = handlers.pull_all_documents_diagnostics.clone();
|
||||
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||
if event
|
||||
.doc
|
||||
.has_language_server_with_feature(LanguageServerFeature::PullDiagnostics)
|
||||
&& !event.ghost_transaction
|
||||
{
|
||||
// Cancel the ongoing request, if present.
|
||||
event.doc.pull_diagnostic_controller.cancel();
|
||||
let document_id = event.doc.id();
|
||||
send_blocking(&tx, PullDiagnosticsEvent { document_id });
|
||||
|
||||
let inter_file_dependencies_language_servers = event
|
||||
.doc
|
||||
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
|
||||
.filter(|language_server| {
|
||||
language_server
|
||||
.capabilities()
|
||||
.diagnostic_provider
|
||||
.as_ref()
|
||||
.is_some_and(|diagnostic_provider| match diagnostic_provider {
|
||||
lsp::DiagnosticServerCapabilities::Options(options) => {
|
||||
options.inter_file_dependencies
|
||||
}
|
||||
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
|
||||
options.diagnostic_options.inter_file_dependencies
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|language_server| language_server.id())
|
||||
.collect();
|
||||
|
||||
send_blocking(
|
||||
&tx_all_documents,
|
||||
PullAllDocumentsDiagnosticsEvent {
|
||||
language_servers: inter_file_dependencies_language_servers,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
|
||||
request_document_diagnostics(event.editor, event.doc);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
|
||||
let doc_ids: Vec<_> = event.editor.documents.keys().copied().collect();
|
||||
|
||||
for doc_id in doc_ids {
|
||||
request_document_diagnostics(event.editor, doc_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct PullDiagnosticsHandler {
|
||||
document_ids: HashSet<DocumentId>,
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for PullDiagnosticsHandler {
|
||||
type Event = PullDiagnosticsEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
_timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
self.document_ids.insert(event.document_id);
|
||||
Some(Instant::now() + Duration::from_millis(250))
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let document_ids = mem::take(&mut self.document_ids);
|
||||
job::dispatch_blocking(move |editor, _| {
|
||||
for document_id in document_ids {
|
||||
request_document_diagnostics(editor, document_id);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct PullAllDocumentsDiagnosticHandler {
|
||||
language_servers: HashSet<LanguageServerId>,
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for PullAllDocumentsDiagnosticHandler {
|
||||
type Event = PullAllDocumentsDiagnosticsEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
_timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
self.language_servers.extend(&event.language_servers);
|
||||
Some(Instant::now() + Duration::from_secs(1))
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let language_servers = mem::take(&mut self.language_servers);
|
||||
job::dispatch_blocking(move |editor, _| {
|
||||
let documents: Vec<_> = editor.documents.keys().copied().collect();
|
||||
|
||||
for document in documents {
|
||||
request_document_diagnostics_for_language_severs(
|
||||
editor,
|
||||
document,
|
||||
language_servers.clone(),
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn request_document_diagnostics_for_language_severs(
|
||||
editor: &mut Editor,
|
||||
doc_id: DocumentId,
|
||||
language_servers: HashSet<LanguageServerId>,
|
||||
) {
|
||||
let Some(doc) = editor.document_mut(doc_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let cancel = doc.pull_diagnostic_controller.restart();
|
||||
|
||||
let mut futures: FuturesUnordered<_> = language_servers
|
||||
.iter()
|
||||
.filter_map(|x| doc.language_servers().find(|y| &y.id() == x))
|
||||
.filter_map(|language_server| {
|
||||
let future = language_server
|
||||
.text_document_diagnostic(doc.identifier(), doc.previous_diagnostic_id.clone())?;
|
||||
|
||||
let identifier = language_server
|
||||
.capabilities()
|
||||
.diagnostic_provider
|
||||
.as_ref()
|
||||
.and_then(|diagnostic_provider| match diagnostic_provider {
|
||||
lsp::DiagnosticServerCapabilities::Options(options) => {
|
||||
options.identifier.clone()
|
||||
}
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
|
||||
options.diagnostic_options.identifier.clone()
|
||||
}
|
||||
});
|
||||
|
||||
let language_server_id = language_server.id();
|
||||
let provider = DiagnosticProvider::Lsp {
|
||||
server_id: language_server_id,
|
||||
identifier,
|
||||
};
|
||||
let uri = doc.uri()?;
|
||||
|
||||
Some(async move {
|
||||
let result = future.await;
|
||||
|
||||
(result, provider, uri)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if futures.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut retry_language_servers = HashSet::new();
|
||||
loop {
|
||||
match cancelable_future(futures.next(), &cancel).await {
|
||||
Some(Some((Ok(result), provider, uri))) => {
|
||||
job::dispatch(move |editor, _| {
|
||||
handle_pull_diagnostics_response(editor, result, provider, uri, doc_id);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Some(Some((Err(err), DiagnosticProvider::Lsp { server_id, .. }, _))) => {
|
||||
let parsed_cancellation_data = if let helix_lsp::Error::Rpc(error) = err {
|
||||
error.data.and_then(|data| {
|
||||
serde_json::from_value::<lsp::DiagnosticServerCancellationData>(data)
|
||||
.ok()
|
||||
})
|
||||
} else {
|
||||
log::error!("Pull diagnostic request failed: {err}");
|
||||
continue;
|
||||
};
|
||||
if parsed_cancellation_data.is_some_and(|data| data.retrigger_request) {
|
||||
retry_language_servers.insert(server_id);
|
||||
}
|
||||
}
|
||||
Some(None) => break,
|
||||
// The request was cancelled.
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
|
||||
if !retry_language_servers.is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
job::dispatch(move |editor, _| {
|
||||
request_document_diagnostics_for_language_severs(
|
||||
editor,
|
||||
doc_id,
|
||||
retry_language_servers,
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn request_document_diagnostics(editor: &mut Editor, doc_id: DocumentId) {
|
||||
let Some(doc) = editor.document(doc_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let language_servers = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
|
||||
.map(|language_servers| language_servers.id())
|
||||
.collect();
|
||||
|
||||
request_document_diagnostics_for_language_severs(editor, doc_id, language_servers);
|
||||
}
|
||||
|
||||
fn handle_pull_diagnostics_response(
|
||||
editor: &mut Editor,
|
||||
result: lsp::DocumentDiagnosticReportResult,
|
||||
provider: DiagnosticProvider,
|
||||
uri: Uri,
|
||||
document_id: DocumentId,
|
||||
) {
|
||||
match result {
|
||||
lsp::DocumentDiagnosticReportResult::Report(report) => {
|
||||
let result_id = match report {
|
||||
lsp::DocumentDiagnosticReport::Full(report) => {
|
||||
editor.handle_lsp_diagnostics(
|
||||
&provider,
|
||||
uri,
|
||||
None,
|
||||
report.full_document_diagnostic_report.items,
|
||||
);
|
||||
|
||||
report.full_document_diagnostic_report.result_id
|
||||
}
|
||||
lsp::DocumentDiagnosticReport::Unchanged(report) => {
|
||||
Some(report.unchanged_document_diagnostic_report.result_id)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(doc) = editor.document_mut(document_id) {
|
||||
doc.previous_diagnostic_id = result_id;
|
||||
};
|
||||
}
|
||||
lsp::DocumentDiagnosticReportResult::Partial(_) => {}
|
||||
};
|
||||
}
|
||||
|
17
helix-term/src/handlers/prompt.rs
Normal file
17
helix-term/src/handlers/prompt.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use helix_event::register_hook;
|
||||
use helix_view::events::DocumentFocusLost;
|
||||
use helix_view::handlers::Handlers;
|
||||
|
||||
use crate::job::{self};
|
||||
use crate::ui;
|
||||
|
||||
pub(super) fn register_hooks(_handlers: &Handlers) {
|
||||
register_hook!(move |_event: &mut DocumentFocusLost<'_>| {
|
||||
job::dispatch_blocking(move |_, compositor| {
|
||||
if compositor.find::<ui::Prompt>().is_some() {
|
||||
compositor.remove_type::<ui::Prompt>();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
});
|
||||
}
|
@@ -1,11 +1,14 @@
|
||||
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::{collections::HashSet, io::Write};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::{IsTerminal, Write},
|
||||
};
|
||||
use termina::{
|
||||
style::{ColorSpec, StyleExt as _, Stylized},
|
||||
Terminal as _,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum TsFeature {
|
||||
@@ -183,21 +186,24 @@ fn languages(selection: Option<HashSet<String>>) -> 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)))?;
|
||||
@@ -210,10 +216,10 @@ fn languages(selection: Option<HashSet<String>>) -> 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)));
|
||||
@@ -247,8 +253,8 @@ fn languages(selection: Option<HashSet<String>>) -> 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))?,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
@@ -57,24 +56,24 @@ USAGE:
|
||||
hx [FLAGS] [files]...
|
||||
|
||||
ARGS:
|
||||
<files>... Sets the input file to use, position can also be specified via file[:row[:col]]
|
||||
<files>... Set the input file to use, position can also be specified via file[:row[:col]]
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
--tutor Loads the tutorial
|
||||
--health [CATEGORY] Checks for potential errors in editor setup
|
||||
-h, --help Print help information
|
||||
--tutor Load the tutorial
|
||||
--health [CATEGORY] Check for potential errors in editor setup
|
||||
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
|
||||
--log <file> Specifies a file to use for logging
|
||||
-g, --grammar {{fetch|build}} Fetch or builds tree-sitter grammars listed in languages.toml
|
||||
-c, --config <file> Specify a file to use for configuration
|
||||
-v Increase logging verbosity each use for up to 3 times
|
||||
--log <file> Specify a file to use for logging
|
||||
(default file: {})
|
||||
-V, --version Prints version information
|
||||
--vsplit Splits all given files vertically into different windows
|
||||
--hsplit Splits all given files horizontally into different windows
|
||||
-V, --version Print version information
|
||||
--vsplit Split all given files vertically into different windows
|
||||
--hsplit Split all given files horizontally into different windows
|
||||
-w, --working-dir <path> Specify an initial working directory
|
||||
+N Open the first given file at line number N
|
||||
",
|
||||
@@ -151,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)
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
use crate::handlers::completion::LspCompletionItem;
|
||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
use crate::{
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
handlers::completion::{
|
||||
trigger_auto_completion, CompletionItem, CompletionResponse, LspCompletionItem,
|
||||
ResolveHandler,
|
||||
trigger_auto_completion, CompletionItem, CompletionResponse, ResolveHandler,
|
||||
},
|
||||
};
|
||||
use helix_core::snippets::{ActiveSnippet, RenderedSnippet, Snippet};
|
||||
@@ -28,7 +28,7 @@ use std::cmp::Reverse;
|
||||
impl menu::Item for CompletionItem {
|
||||
type Data = Style;
|
||||
|
||||
fn format(&self, dir_style: &Self::Data) -> menu::Row {
|
||||
fn format(&self, dir_style: &Self::Data) -> menu::Row<'_> {
|
||||
let deprecated = match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => {
|
||||
item.deprecated.unwrap_or_default()
|
||||
|
@@ -214,7 +214,7 @@ impl<'a> TextRenderer<'a> {
|
||||
let tab_width = doc.tab_width();
|
||||
let tab = if ws_render.tab() == WhitespaceRenderValue::All {
|
||||
std::iter::once(ws_chars.tab)
|
||||
.chain(std::iter::repeat(ws_chars.tabpad).take(tab_width - 1))
|
||||
.chain(std::iter::repeat_n(ws_chars.tabpad, tab_width - 1))
|
||||
.collect()
|
||||
} else {
|
||||
" ".repeat(tab_width)
|
||||
|
@@ -538,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));
|
||||
}
|
||||
@@ -546,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));
|
||||
}
|
||||
@@ -1159,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);
|
||||
|
||||
@@ -1182,7 +1184,6 @@ impl EditorView {
|
||||
self.clear_completion(editor);
|
||||
}
|
||||
|
||||
editor.focus(view_id);
|
||||
editor.ensure_cursor_in_view(view_id);
|
||||
|
||||
return EventResult::Consumed(None);
|
||||
@@ -1630,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)
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ pub trait Item: Sync + Send + 'static {
|
||||
/// Additional editor state that is used for label calculation.
|
||||
type Data: Sync + Send + 'static;
|
||||
|
||||
fn format(&self, data: &Self::Data) -> Row;
|
||||
fn format(&self, data: &Self::Data) -> Row<'_>;
|
||||
}
|
||||
|
||||
pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
|
||||
|
@@ -185,6 +185,22 @@ pub fn raw_regex_prompt(
|
||||
cx.push_layer(Box::new(prompt));
|
||||
}
|
||||
|
||||
/// We want to exclude files that the editor can't handle yet
|
||||
fn get_excluded_types() -> ignore::types::Types {
|
||||
use ignore::types::TypesBuilder;
|
||||
let mut type_builder = TypesBuilder::new();
|
||||
type_builder
|
||||
.add(
|
||||
"compressed",
|
||||
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
|
||||
)
|
||||
.expect("Invalid type definition");
|
||||
type_builder.negate("all");
|
||||
type_builder
|
||||
.build()
|
||||
.expect("failed to build excluded_types")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FilePickerData {
|
||||
root: PathBuf,
|
||||
@@ -193,7 +209,7 @@ pub struct FilePickerData {
|
||||
type FilePicker = Picker<PathBuf, FilePickerData>;
|
||||
|
||||
pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
|
||||
use ignore::{types::TypesBuilder, WalkBuilder};
|
||||
use ignore::WalkBuilder;
|
||||
use std::time::Instant;
|
||||
|
||||
let config = editor.config();
|
||||
@@ -208,7 +224,8 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
|
||||
let absolute_root = root.canonicalize().unwrap_or_else(|_| root.clone());
|
||||
|
||||
let mut walk_builder = WalkBuilder::new(&root);
|
||||
walk_builder
|
||||
|
||||
let mut files = walk_builder
|
||||
.hidden(config.file_picker.hidden)
|
||||
.parents(config.file_picker.parents)
|
||||
.ignore(config.file_picker.ignore)
|
||||
@@ -218,31 +235,18 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
|
||||
.git_exclude(config.file_picker.git_exclude)
|
||||
.sort_by_file_name(|name1, name2| name1.cmp(name2))
|
||||
.max_depth(config.file_picker.max_depth)
|
||||
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
|
||||
|
||||
walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
|
||||
walk_builder.add_custom_ignore_filename(".helix/ignore");
|
||||
|
||||
// We want to exclude files that the editor can't handle yet
|
||||
let mut type_builder = TypesBuilder::new();
|
||||
type_builder
|
||||
.add(
|
||||
"compressed",
|
||||
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
|
||||
)
|
||||
.expect("Invalid type definition");
|
||||
type_builder.negate("all");
|
||||
let excluded_types = type_builder
|
||||
.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")
|
||||
.types(get_excluded_types())
|
||||
.build()
|
||||
.expect("failed to build excluded_types");
|
||||
walk_builder.types(excluded_types);
|
||||
let mut files = walk_builder.build().filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
if !entry.file_type()?.is_file() {
|
||||
return None;
|
||||
}
|
||||
Some(entry.into_path())
|
||||
});
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
if !entry.file_type()?.is_file() {
|
||||
return None;
|
||||
}
|
||||
Some(entry.into_path())
|
||||
});
|
||||
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
|
||||
|
||||
let columns = [PickerColumn::new(
|
||||
@@ -304,7 +308,7 @@ type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>;
|
||||
|
||||
pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std::io::Error> {
|
||||
let directory_style = editor.theme.get("ui.text.directory");
|
||||
let directory_content = directory_content(&root)?;
|
||||
let directory_content = directory_content(&root, editor)?;
|
||||
|
||||
let columns = [PickerColumn::new(
|
||||
"path",
|
||||
@@ -350,24 +354,64 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std
|
||||
Ok(picker)
|
||||
}
|
||||
|
||||
fn directory_content(path: &Path) -> Result<Vec<(PathBuf, bool)>, std::io::Error> {
|
||||
let mut content: Vec<_> = std::fs::read_dir(path)?
|
||||
.flatten()
|
||||
.map(|entry| {
|
||||
(
|
||||
entry.path(),
|
||||
std::fs::metadata(entry.path()).is_ok_and(|metadata| metadata.is_dir()),
|
||||
)
|
||||
fn directory_content(root: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)>, std::io::Error> {
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
let config = editor.config();
|
||||
|
||||
let mut walk_builder = WalkBuilder::new(root);
|
||||
|
||||
let mut content: Vec<(PathBuf, bool)> = walk_builder
|
||||
.hidden(config.file_explorer.hidden)
|
||||
.parents(config.file_explorer.parents)
|
||||
.ignore(config.file_explorer.ignore)
|
||||
.follow_links(config.file_explorer.follow_symlinks)
|
||||
.git_ignore(config.file_explorer.git_ignore)
|
||||
.git_global(config.file_explorer.git_global)
|
||||
.git_exclude(config.file_explorer.git_exclude)
|
||||
.max_depth(Some(1))
|
||||
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"))
|
||||
.add_custom_ignore_filename(".helix/ignore")
|
||||
.types(get_excluded_types())
|
||||
.build()
|
||||
.filter_map(|entry| {
|
||||
entry
|
||||
.map(|entry| {
|
||||
let is_dir = entry
|
||||
.file_type()
|
||||
.is_some_and(|file_type| file_type.is_dir());
|
||||
let mut path = entry.path().to_path_buf();
|
||||
if is_dir && path != root && config.file_explorer.flatten_dirs {
|
||||
while let Some(single_child_directory) = get_child_if_single_dir(&path) {
|
||||
path = single_child_directory;
|
||||
}
|
||||
}
|
||||
(path, is_dir)
|
||||
})
|
||||
.ok()
|
||||
.filter(|entry| entry.0 != root)
|
||||
})
|
||||
.collect();
|
||||
|
||||
content.sort_by(|(path1, is_dir1), (path2, is_dir2)| (!is_dir1, path1).cmp(&(!is_dir2, path2)));
|
||||
if path.parent().is_some() {
|
||||
content.insert(0, (path.join(".."), true));
|
||||
|
||||
if root.parent().is_some() {
|
||||
content.insert(0, (root.join(".."), true));
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn get_child_if_single_dir(path: &Path) -> Option<PathBuf> {
|
||||
let mut entries = path.read_dir().ok()?;
|
||||
let entry = entries.next()?.ok()?;
|
||||
if entries.next().is_none() && entry.file_type().is_ok_and(|file_type| file_type.is_dir()) {
|
||||
Some(entry.path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub mod completers {
|
||||
use super::Utf8PathBuf;
|
||||
use crate::ui::prompt::Completion;
|
||||
@@ -740,3 +784,27 @@ pub mod completers {
|
||||
completions
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{create_dir, File};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_child_if_single_dir() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
||||
assert_eq!(get_child_if_single_dir(root.path()), None);
|
||||
|
||||
let dir = root.path().join("dir1");
|
||||
create_dir(&dir).unwrap();
|
||||
|
||||
assert_eq!(get_child_if_single_dir(root.path()), Some(dir));
|
||||
|
||||
let file = root.path().join("file");
|
||||
File::create(file).unwrap();
|
||||
|
||||
assert_eq!(get_child_if_single_dir(root.path()), None);
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -308,7 +309,10 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||
F: Fn(&mut Context, &T, Action) + 'static,
|
||||
{
|
||||
let columns: Arc<[_]> = columns.into_iter().collect();
|
||||
let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
|
||||
let matcher_columns = columns
|
||||
.iter()
|
||||
.filter(|col: &&Column<T, D>| col.filter)
|
||||
.count() as u32;
|
||||
assert!(matcher_columns > 0);
|
||||
let matcher = Nucleo::new(
|
||||
Config::DEFAULT,
|
||||
@@ -382,6 +386,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 +429,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 +450,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();
|
||||
@@ -595,11 +610,15 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||
let preview = std::fs::metadata(&path)
|
||||
.and_then(|metadata| {
|
||||
if metadata.is_dir() {
|
||||
let files = super::directory_content(&path)?;
|
||||
let files = super::directory_content(&path, editor)?;
|
||||
let file_names: Vec<_> = files
|
||||
.iter()
|
||||
.filter_map(|(path, is_dir)| {
|
||||
let name = path.file_name()?.to_string_lossy();
|
||||
.filter_map(|(file_path, is_dir)| {
|
||||
let name = file_path
|
||||
.strip_prefix(&path)
|
||||
.map(|p| Some(p.as_os_str()))
|
||||
.unwrap_or_else(|_| file_path.file_name())?
|
||||
.to_string_lossy();
|
||||
if *is_dir {
|
||||
Some((format!("{}/", name), true))
|
||||
} else {
|
||||
@@ -881,7 +900,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||
if let Some((preview, range)) = self.get_preview(cx.editor) {
|
||||
let doc = match preview.document() {
|
||||
Some(doc)
|
||||
if range.map_or(true, |(start, end)| {
|
||||
if range.is_none_or(|(start, end)| {
|
||||
start <= end && end <= doc.text().len_lines()
|
||||
}) =>
|
||||
{
|
||||
@@ -1071,7 +1090,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 +1114,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
|
||||
|
@@ -1,7 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use helix_core::indent::IndentStyle;
|
||||
use helix_core::{coords_at_pos, encoding, Position};
|
||||
use helix_core::{coords_at_pos, encoding, unicode::width::UnicodeWidthStr, Position};
|
||||
use helix_lsp::lsp::DiagnosticSeverity;
|
||||
use helix_view::document::DEFAULT_LANGUAGE_NAME;
|
||||
use helix_view::{
|
||||
@@ -169,18 +167,16 @@ where
|
||||
let visible = context.focused;
|
||||
let config = context.editor.config();
|
||||
let modenames = &config.statusline.mode;
|
||||
let mode_str = match context.editor.mode() {
|
||||
Mode::Insert => &modenames.insert,
|
||||
Mode::Select => &modenames.select,
|
||||
Mode::Normal => &modenames.normal,
|
||||
};
|
||||
let content = if visible {
|
||||
Cow::Owned(format!(
|
||||
" {} ",
|
||||
match context.editor.mode() {
|
||||
Mode::Insert => &modenames.insert,
|
||||
Mode::Select => &modenames.select,
|
||||
Mode::Normal => &modenames.normal,
|
||||
}
|
||||
))
|
||||
format!(" {mode_str} ")
|
||||
} else {
|
||||
// If not focused, explicitly leave an empty space instead of returning None.
|
||||
Cow::Borrowed(" ")
|
||||
" ".repeat(mode_str.width() + 2)
|
||||
};
|
||||
let style = if visible && config.color_modes {
|
||||
match context.editor.mode() {
|
||||
|
@@ -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,13 +6,17 @@ 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 tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use crossterm::event::{Event, KeyEvent};
|
||||
#[cfg(not(windows))]
|
||||
use termina::event::{Event, KeyEvent};
|
||||
|
||||
/// Specify how to set up the input text with line feeds
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LineFeedHandling {
|
||||
|
@@ -12,7 +12,7 @@ repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
default = ["termina", "crossterm"]
|
||||
|
||||
[dependencies]
|
||||
helix-view = { path = "../helix-view", features = ["term"] }
|
||||
@@ -21,7 +21,10 @@ 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"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
crossterm = { version = "0.28", optional = true }
|
||||
|
@@ -14,10 +14,7 @@ use crossterm::{
|
||||
terminal::{self, Clear, ClearType},
|
||||
Command,
|
||||
};
|
||||
use helix_view::{
|
||||
editor::Config as EditorConfig,
|
||||
graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle},
|
||||
};
|
||||
use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -74,17 +71,17 @@ impl Capabilities {
|
||||
/// 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 {
|
||||
pub fn from_env_or_default(config: &Config) -> Self {
|
||||
match termini::TermInfo::from_env() {
|
||||
Err(_) => Capabilities {
|
||||
has_extended_underlines: config.undercurl,
|
||||
has_extended_underlines: config.force_enable_extended_underlines,
|
||||
..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
|
||||
has_extended_underlines: config.force_enable_extended_underlines
|
||||
|| t.extended_cap("Smulx").is_some()
|
||||
|| t.extended_cap("Su").is_some()
|
||||
|| vte_version() >= Some(5102)
|
||||
@@ -98,6 +95,7 @@ impl Capabilities {
|
||||
/// Terminal backend supporting a wide variety of terminals
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
config: Config,
|
||||
capabilities: Capabilities,
|
||||
supports_keyboard_enhancement_protocol: OnceCell<bool>,
|
||||
mouse_capture_enabled: bool,
|
||||
@@ -108,14 +106,15 @@ impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend<W> {
|
||||
pub fn new(buffer: W, config: Config) -> 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),
|
||||
capabilities: Capabilities::from_env_or_default(&config),
|
||||
config,
|
||||
supports_keyboard_enhancement_protocol: OnceCell::new(),
|
||||
mouse_capture_enabled: false,
|
||||
supports_bracketed_paste: true,
|
||||
@@ -157,7 +156,7 @@ impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn claim(&mut self, config: Config) -> io::Result<()> {
|
||||
fn claim(&mut self) -> io::Result<()> {
|
||||
terminal::enable_raw_mode()?;
|
||||
execute!(
|
||||
self.buffer,
|
||||
@@ -173,7 +172,7 @@ where
|
||||
Ok(_) => (),
|
||||
};
|
||||
execute!(self.buffer, terminal::Clear(terminal::ClearType::All))?;
|
||||
if config.enable_mouse_capture {
|
||||
if self.config.enable_mouse_capture {
|
||||
execute!(self.buffer, EnableMouseCapture)?;
|
||||
self.mouse_capture_enabled = true;
|
||||
}
|
||||
@@ -198,15 +197,16 @@ where
|
||||
}
|
||||
self.mouse_capture_enabled = config.enable_mouse_capture;
|
||||
}
|
||||
self.config = config;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore(&mut self, config: Config) -> io::Result<()> {
|
||||
fn restore(&mut self) -> io::Result<()> {
|
||||
// reset cursor shape
|
||||
self.buffer
|
||||
.write_all(self.capabilities.reset_cursor_command.as_bytes())?;
|
||||
if config.enable_mouse_capture {
|
||||
if self.config.enable_mouse_capture {
|
||||
execute!(self.buffer, DisableMouseCapture)?;
|
||||
}
|
||||
if self.supports_keyboard_enhancement_protocol() {
|
||||
@@ -223,20 +223,6 @@ where
|
||||
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)>,
|
||||
@@ -316,11 +302,6 @@ where
|
||||
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))
|
||||
}
|
||||
@@ -339,6 +320,14 @@ where
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn supports_true_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn get_theme_mode(&self) -> Option<helix_view::theme::Mode> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@@ -6,9 +6,14 @@ use crate::{buffer::Cell, terminal::Config};
|
||||
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(all(feature = "termina", not(windows)))]
|
||||
mod termina;
|
||||
#[cfg(all(feature = "termina", not(windows)))]
|
||||
pub use self::termina::TerminaBackend;
|
||||
|
||||
#[cfg(all(feature = "termina", windows))]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(all(feature = "termina", windows))]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
mod test;
|
||||
@@ -17,13 +22,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 +35,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 +43,6 @@ 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;
|
||||
fn get_theme_mode(&self) -> Option<helix_view::theme::Mode>;
|
||||
}
|
||||
|
649
helix-tui/src/backend/termina.rs
Normal file
649
helix-tui/src/backend/termina.rs
Normal file
@@ -0,0 +1,649 @@
|
||||
use std::io::{self, Write as _};
|
||||
|
||||
use helix_view::{
|
||||
editor::KittyKeyboardProtocolConfig,
|
||||
graphics::{CursorKind, Rect, UnderlineStyle},
|
||||
theme::{self, Color, Modifier},
|
||||
};
|
||||
use termina::{
|
||||
escape::{
|
||||
csi::{self, Csi, SgrAttributes, SgrModifiers},
|
||||
dcs::{self, Dcs},
|
||||
},
|
||||
style::{CursorStyle, RgbColor},
|
||||
Event, OneBased, PlatformTerminal, Terminal as _, WindowSize,
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct Capabilities {
|
||||
kitty_keyboard: KittyKeyboardSupport,
|
||||
synchronized_output: bool,
|
||||
true_color: bool,
|
||||
extended_underlines: bool,
|
||||
theme_mode: Option<theme::Mode>,
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
capabilities.kitty_keyboard = match config.kitty_keyboard_protocol {
|
||||
KittyKeyboardProtocolConfig::Disabled => KittyKeyboardSupport::None,
|
||||
KittyKeyboardProtocolConfig::Enabled => KittyKeyboardSupport::Full,
|
||||
KittyKeyboardProtocolConfig::Auto => {
|
||||
write!(terminal, "{}", Csi::Keyboard(csi::Keyboard::QueryFlags))?;
|
||||
KittyKeyboardSupport::None
|
||||
}
|
||||
};
|
||||
|
||||
// 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,
|
||||
"{}{}{}{}{}{}{}",
|
||||
// Synchronized output
|
||||
Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code(
|
||||
csi::DecPrivateModeCode::SynchronizedOutput
|
||||
))),
|
||||
// Mode 2031 theme updates. Query the current theme.
|
||||
Csi::Mode(csi::Mode::QueryTheme),
|
||||
// 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::Csi(Csi::Mode(csi::Mode::ReportTheme(mode))) => {
|
||||
capabilities.theme_mode = Some(mode.into());
|
||||
}
|
||||
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 mut reset_cursor_command =
|
||||
Csi::Cursor(csi::Cursor::CursorStyle(CursorStyle::Default)).to_string();
|
||||
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"));
|
||||
|
||||
if let Some(termini::Value::Utf8String(se_str)) = t.extended_cap("Se") {
|
||||
reset_cursor_command.push_str(se_str);
|
||||
};
|
||||
reset_cursor_command.push_str(
|
||||
t.utf8_string_cap(termini::StringCapability::CursorNormal)
|
||||
.unwrap_or(""),
|
||||
);
|
||||
log::debug!(
|
||||
"Cursor reset escape sequence detected from terminfo: {reset_cursor_command:?}"
|
||||
);
|
||||
} else {
|
||||
log::debug!("terminfo could not be read, using default cursor reset escape sequence: {reset_cursor_command:?}");
|
||||
}
|
||||
|
||||
terminal.enter_cooked_mode()?;
|
||||
|
||||
Ok((capabilities, reset_cursor_command))
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.capabilities.theme_mode.is_some() {
|
||||
// Enable mode 2031 theme mode notifications:
|
||||
write!(self.terminal, "{}", decset!(Theme))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disable_extensions(&mut self) -> io::Result<()> {
|
||||
if self.capabilities.kitty_keyboard == KittyKeyboardSupport::Full {
|
||||
write!(
|
||||
self.terminal,
|
||||
"{}",
|
||||
Csi::Keyboard(csi::Keyboard::PopFlags(1))
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.capabilities.theme_mode.is_some() {
|
||||
// Mode 2031 theme notifications.
|
||||
write!(self.terminal, "{}", decreset!(Theme))?;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn get_theme_mode(&self) -> Option<theme::Mode> {
|
||||
self.capabilities.theme_mode
|
||||
}
|
||||
}
|
||||
|
||||
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,12 @@ impl Backend for TestBackend {
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn supports_true_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn get_theme_mode(&self) -> Option<helix_view::theme::Mode> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
//! Frontend for [Backend]
|
||||
|
||||
use crate::{backend::Backend, buffer::Buffer};
|
||||
use helix_view::editor::Config as EditorConfig;
|
||||
use helix_view::editor::{Config as EditorConfig, KittyKeyboardProtocolConfig};
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use std::io;
|
||||
|
||||
@@ -24,12 +24,16 @@ pub struct Viewport {
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub enable_mouse_capture: bool,
|
||||
pub force_enable_extended_underlines: bool,
|
||||
pub kitty_keyboard_protocol: KittyKeyboardProtocolConfig,
|
||||
}
|
||||
|
||||
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,
|
||||
kitty_keyboard_protocol: config.kitty_keyboard_protocol,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +73,14 @@ where
|
||||
viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Default terminal size: 80 columns, 24 lines
|
||||
pub const DEFAULT_TERMINAL_SIZE: Rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 80,
|
||||
height: 24,
|
||||
};
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -76,7 +88,7 @@ where
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
let size = backend.size().unwrap_or(DEFAULT_TERMINAL_SIZE);
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
@@ -102,16 +114,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.
|
||||
@@ -155,7 +167,7 @@ where
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<Rect> {
|
||||
let size = self.size()?;
|
||||
let size = self.size();
|
||||
if size != self.viewport.area {
|
||||
self.resize(size)?;
|
||||
};
|
||||
@@ -218,10 +230,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)
|
||||
}
|
||||
@@ -235,7 +243,7 @@ where
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
pub fn size(&self) -> Rect {
|
||||
self.backend.size().unwrap_or(DEFAULT_TERMINAL_SIZE)
|
||||
}
|
||||
}
|
||||
|
@@ -374,6 +374,30 @@ impl<'a> Text<'a> {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Patch text with a new style. Only updates fields that are in the new style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::Text;
|
||||
/// # use helix_view::graphics::{Color, Style};
|
||||
/// let style1 = Style::default().fg(Color::Yellow);
|
||||
/// let style2 = Style::default().fg(Color::Yellow).bg(Color::Black);
|
||||
/// let mut half_styled_text = Text::styled(String::from("The first line\nThe second line"), style1);
|
||||
/// let full_styled_text = Text::styled(String::from("The first line\nThe second line"), style2);
|
||||
/// assert_ne!(half_styled_text, full_styled_text);
|
||||
///
|
||||
/// half_styled_text.patch_style(Style::default().bg(Color::Black));
|
||||
/// assert_eq!(half_styled_text, full_styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
for span in &mut line.0 {
|
||||
span.style = span.style.patch(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a new style to existing text.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -386,13 +410,13 @@ impl<'a> Text<'a> {
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// raw_text.set_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
pub fn set_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
for span in &mut line.0 {
|
||||
span.style = span.style.patch(style);
|
||||
span.style = style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -37,9 +37,15 @@ pub struct Cell<'a> {
|
||||
impl Cell<'_> {
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self.set_style(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn set_style(&mut self, style: Style) {
|
||||
self.style = style;
|
||||
self.content.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
@@ -453,6 +459,9 @@ impl Table<'_> {
|
||||
};
|
||||
if is_selected {
|
||||
buf.set_style(table_row_area, self.highlight_style);
|
||||
for cell in &mut table_row.cells {
|
||||
cell.set_style(self.highlight_style);
|
||||
}
|
||||
}
|
||||
let mut col = table_row_start_col;
|
||||
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
|
||||
|
128
helix-tui/tests/text.rs
Normal file
128
helix-tui/tests/text.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use helix_tui::text::{Span, Spans, StyledGrapheme, Text};
|
||||
use helix_view::graphics::{Color, Modifier, Style};
|
||||
|
||||
// Text
|
||||
#[test]
|
||||
fn text_width() {
|
||||
let text = Text::from("The first line\nThe second line");
|
||||
assert_eq!(15, text.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_height() {
|
||||
let text = Text::from("The first line\nThe second line");
|
||||
assert_eq!(2, text.height());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_style() {
|
||||
let style1 = Style::default().fg(Color::Yellow);
|
||||
let style2 = Style::default().fg(Color::Yellow).bg(Color::Black);
|
||||
let mut half_styled_text =
|
||||
Text::styled(String::from("The first line\nThe second line"), style1);
|
||||
let full_styled_text = Text::styled(String::from("The first line\nThe second line"), style2);
|
||||
assert_ne!(half_styled_text, full_styled_text);
|
||||
|
||||
half_styled_text.patch_style(Style::default().bg(Color::Black));
|
||||
assert_eq!(half_styled_text, full_styled_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
assert_ne!(raw_text, styled_text);
|
||||
|
||||
raw_text.set_style(style);
|
||||
assert_eq!(raw_text, styled_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_extend() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut text = Text::from("The first line\nThe second line");
|
||||
assert_eq!(2, text.height());
|
||||
|
||||
// Adding two more unstyled lines
|
||||
text.extend(Text::raw("These are two\nmore lines!"));
|
||||
assert_eq!(4, text.height());
|
||||
|
||||
// Adding a final two styled lines
|
||||
text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
assert_eq!(6, text.height());
|
||||
}
|
||||
|
||||
// Span
|
||||
|
||||
#[test]
|
||||
fn styled_graphemes() {
|
||||
let style = Style::default().fg(Color::Yellow);
|
||||
let span = Span::styled("Text", style);
|
||||
let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
let styled_graphemes = span.styled_graphemes(style);
|
||||
assert_eq!(
|
||||
vec![
|
||||
StyledGrapheme {
|
||||
symbol: "T",
|
||||
style: Style {
|
||||
fg: Some(Color::Yellow),
|
||||
bg: Some(Color::Black),
|
||||
underline_color: None,
|
||||
underline_style: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
},
|
||||
},
|
||||
StyledGrapheme {
|
||||
symbol: "e",
|
||||
style: Style {
|
||||
fg: Some(Color::Yellow),
|
||||
bg: Some(Color::Black),
|
||||
underline_color: None,
|
||||
underline_style: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
},
|
||||
},
|
||||
StyledGrapheme {
|
||||
symbol: "x",
|
||||
style: Style {
|
||||
fg: Some(Color::Yellow),
|
||||
bg: Some(Color::Black),
|
||||
underline_color: None,
|
||||
underline_style: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
},
|
||||
},
|
||||
StyledGrapheme {
|
||||
symbol: "t",
|
||||
style: Style {
|
||||
fg: Some(Color::Yellow),
|
||||
bg: Some(Color::Black),
|
||||
underline_color: None,
|
||||
underline_style: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
},
|
||||
},
|
||||
],
|
||||
styled_graphemes.collect::<Vec<StyledGrapheme>>()
|
||||
);
|
||||
}
|
||||
|
||||
// Spans
|
||||
|
||||
#[test]
|
||||
fn spans_width() {
|
||||
let spans = Spans::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
]);
|
||||
assert_eq!(7, spans.width());
|
||||
}
|
@@ -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"
|
||||
|
||||
|
@@ -72,7 +72,7 @@ impl DiffHandle {
|
||||
}
|
||||
|
||||
/// Load the actual diff
|
||||
pub fn load(&self) -> Diff {
|
||||
pub fn load(&self) -> Diff<'_> {
|
||||
Diff {
|
||||
diff: self.diff.read(),
|
||||
inverted: self.inverted,
|
||||
|
@@ -128,7 +128,7 @@ impl InternedRopeLines {
|
||||
/// Returns the `InternedInput` for performing the diff.
|
||||
/// If `diff_base` or `doc` is so large that performing a diff could slow the editor
|
||||
/// this function returns `None`.
|
||||
pub fn interned_lines(&self) -> Option<&InternedInput<RopeSlice>> {
|
||||
pub fn interned_lines(&self) -> Option<&InternedInput<RopeSlice<'_>>> {
|
||||
if self.is_too_large() {
|
||||
None
|
||||
} else {
|
||||
|
@@ -102,7 +102,7 @@ struct EventAccumulator {
|
||||
render_lock: Option<RenderLock>,
|
||||
}
|
||||
|
||||
impl<'a> EventAccumulator {
|
||||
impl EventAccumulator {
|
||||
fn new() -> EventAccumulator {
|
||||
EventAccumulator {
|
||||
diff_base: None,
|
||||
|
@@ -12,7 +12,7 @@ homepage.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
term = ["crossterm"]
|
||||
term = ["termina", "crossterm"]
|
||||
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
|
||||
|
||||
@@ -56,10 +56,11 @@ kstring = "2.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
clipboard-win = { version = "5.4", features = ["std"] }
|
||||
crossterm = { version = "0.28", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
rustix = { version = "1.0", features = ["fs"] }
|
||||
rustix = { version = "1.1", features = ["fs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
helix-tui = { path = "../helix-tui" }
|
||||
|
@@ -186,7 +186,7 @@ impl<'a> InlineDiagnosticAccumulator<'a> {
|
||||
.doc
|
||||
.diagnostics
|
||||
.get(self.idx)
|
||||
.map_or(true, |diag| diag.range.start != grapheme.char_idx)
|
||||
.is_none_or(|diag| diag.range.start != grapheme.char_idx)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@@ -193,7 +193,7 @@ mod external {
|
||||
Self::Wayland => builtin_name("wayland", &WL_CLIPBOARD),
|
||||
Self::XClip => builtin_name("x-clip", &XCLIP),
|
||||
Self::XSel => builtin_name("x-sel", &XSEL),
|
||||
Self::Win32Yank => builtin_name("win-32-yank", &WIN32),
|
||||
Self::Win32Yank => builtin_name("win32-yank", &WIN32),
|
||||
Self::Tmux => builtin_name("tmux", &TMUX),
|
||||
Self::Termux => builtin_name("termux", &TERMUX),
|
||||
#[cfg(windows)]
|
||||
@@ -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>,
|
||||
|
@@ -204,11 +204,14 @@ pub struct Document {
|
||||
|
||||
pub readonly: bool,
|
||||
|
||||
pub previous_diagnostic_id: Option<String>,
|
||||
|
||||
/// Annotations for LSP document color swatches
|
||||
pub color_swatches: Option<DocumentColorSwatches>,
|
||||
// NOTE: ideally this would live on the handler for color swatches. This is blocked on a
|
||||
// large refactor that would make `&mut Editor` available on the `DocumentDidChange` event.
|
||||
pub color_swatch_controller: TaskController,
|
||||
pub pull_diagnostic_controller: TaskController,
|
||||
|
||||
// NOTE: this field should eventually go away - we should use the Editor's syn_loader instead
|
||||
// of storing a copy on every doc. Then we can remove the surrounding `Arc` and use the
|
||||
@@ -728,6 +731,8 @@ impl Document {
|
||||
color_swatches: None,
|
||||
color_swatch_controller: TaskController::new(),
|
||||
syn_loader,
|
||||
previous_diagnostic_id: None,
|
||||
pull_diagnostic_controller: TaskController::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,7 +980,7 @@ impl Document {
|
||||
};
|
||||
|
||||
let identifier = self.path().map(|_| self.identifier());
|
||||
let language_servers = self.language_servers.clone();
|
||||
let language_servers: Vec<_> = self.language_servers.values().cloned().collect();
|
||||
|
||||
// mark changes up to now as saved
|
||||
let current_rev = self.get_current_revision();
|
||||
@@ -1119,7 +1124,7 @@ impl Document {
|
||||
text: text.clone(),
|
||||
};
|
||||
|
||||
for (_, language_server) in language_servers {
|
||||
for language_server in language_servers {
|
||||
if !language_server.is_initialized() {
|
||||
continue;
|
||||
}
|
||||
@@ -1655,7 +1660,7 @@ impl Document {
|
||||
let savepoint_idx = self
|
||||
.savepoints
|
||||
.iter()
|
||||
.position(|savepoint_ref| savepoint_ref.as_ptr() == savepoint as *const _)
|
||||
.position(|savepoint_ref| std::ptr::eq(savepoint_ref.as_ptr(), savepoint))
|
||||
.expect("Savepoint must belong to this document");
|
||||
|
||||
let savepoint_ref = self.savepoints.remove(savepoint_idx);
|
||||
@@ -2284,6 +2289,10 @@ impl Document {
|
||||
pub fn reset_all_inlay_hints(&mut self) {
|
||||
self.inlay_hints = Default::default();
|
||||
}
|
||||
|
||||
pub fn has_language_server_with_feature(&self, feature: LanguageServerFeature) -> bool {
|
||||
self.language_servers_with_feature(feature).next().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
@@ -221,6 +221,49 @@ impl Default for FilePickerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct FileExplorerConfig {
|
||||
/// IgnoreOptions
|
||||
/// Enables ignoring hidden files.
|
||||
/// Whether to hide hidden files in file explorer and global search results. Defaults to false.
|
||||
pub hidden: bool,
|
||||
/// Enables following symlinks.
|
||||
/// Whether to follow symbolic links in file picker and file or directory completions. Defaults to false.
|
||||
pub follow_symlinks: bool,
|
||||
/// Enables reading ignore files from parent directories. Defaults to false.
|
||||
pub parents: bool,
|
||||
/// Enables reading `.ignore` files.
|
||||
/// Whether to hide files listed in .ignore in file picker and global search results. Defaults to false.
|
||||
pub ignore: bool,
|
||||
/// Enables reading `.gitignore` files.
|
||||
/// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to false.
|
||||
pub git_ignore: bool,
|
||||
/// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
|
||||
/// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to false.
|
||||
pub git_global: bool,
|
||||
/// Enables reading `.git/info/exclude` files.
|
||||
/// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to false.
|
||||
pub git_exclude: bool,
|
||||
/// Whether to flatten single-child directories in file explorer. Defaults to true.
|
||||
pub flatten_dirs: bool,
|
||||
}
|
||||
|
||||
impl Default for FileExplorerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hidden: false,
|
||||
follow_symlinks: false,
|
||||
parents: false,
|
||||
ignore: false,
|
||||
git_ignore: false,
|
||||
git_global: false,
|
||||
git_exclude: false,
|
||||
flatten_dirs: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_alphabet<S>(alphabet: &[char], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
@@ -318,6 +361,7 @@ pub struct Config {
|
||||
/// Whether to display infoboxes. Defaults to true.
|
||||
pub auto_info: bool,
|
||||
pub file_picker: FilePickerConfig,
|
||||
pub file_explorer: FileExplorerConfig,
|
||||
/// Configuration of the statusline elements
|
||||
pub statusline: StatusLineConfig,
|
||||
/// Shape for cursor in each mode
|
||||
@@ -381,6 +425,17 @@ pub struct Config {
|
||||
pub editor_config: bool,
|
||||
/// Whether to render rainbow colors for matching brackets. Defaults to `false`.
|
||||
pub rainbow_brackets: bool,
|
||||
/// Whether to enable Kitty Keyboard Protocol
|
||||
pub kitty_keyboard_protocol: KittyKeyboardProtocolConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone, Copy)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum KittyKeyboardProtocolConfig {
|
||||
#[default]
|
||||
Auto,
|
||||
Disabled,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
|
||||
@@ -1027,6 +1082,7 @@ impl Default for Config {
|
||||
completion_trigger_len: 2,
|
||||
auto_info: true,
|
||||
file_picker: FilePickerConfig::default(),
|
||||
file_explorer: FileExplorerConfig::default(),
|
||||
statusline: StatusLineConfig::default(),
|
||||
cursor_shape: CursorShapeConfig::default(),
|
||||
true_color: false,
|
||||
@@ -1061,6 +1117,7 @@ impl Default for Config {
|
||||
clipboard_provider: ClipboardProvider::default(),
|
||||
editor_config: true,
|
||||
rainbow_brackets: false,
|
||||
kitty_keyboard_protocol: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1587,7 +1644,7 @@ impl Editor {
|
||||
doc.language_servers.iter().filter(|(name, doc_ls)| {
|
||||
language_servers
|
||||
.get(*name)
|
||||
.map_or(true, |ls| ls.id() != doc_ls.id())
|
||||
.is_none_or(|ls| ls.id() != doc_ls.id())
|
||||
});
|
||||
|
||||
for (_, language_server) in doc_language_servers_not_in_registry {
|
||||
@@ -1597,7 +1654,7 @@ impl Editor {
|
||||
let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
|
||||
doc.language_servers
|
||||
.get(*name)
|
||||
.map_or(true, |doc_ls| ls.id() != doc_ls.id())
|
||||
.is_none_or(|doc_ls| ls.id() != doc_ls.id())
|
||||
});
|
||||
|
||||
for (_, language_server) in language_servers_not_in_doc {
|
||||
@@ -1987,28 +2044,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) {
|
||||
|
@@ -289,6 +289,33 @@ impl Color {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<Color> for termina::style::ColorSpec {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
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::BRIGHT_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::WHITE,
|
||||
Color::Indexed(i) => Self::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => termina::style::RgbColor::new(r, g, b).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<Color> for crossterm::style::Color {
|
||||
fn from(color: Color) -> Self {
|
||||
use crossterm::style::Color as CColor;
|
||||
@@ -316,7 +343,6 @@ impl From<Color> for crossterm::style::Color {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UnderlineStyle {
|
||||
Reset,
|
||||
@@ -343,6 +369,20 @@ impl FromStr for UnderlineStyle {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<UnderlineStyle> for termina::style::Underline {
|
||||
fn from(style: UnderlineStyle) -> Self {
|
||||
match style {
|
||||
UnderlineStyle::Reset => Self::None,
|
||||
UnderlineStyle::Line => Self::Single,
|
||||
UnderlineStyle::Curl => Self::Curly,
|
||||
UnderlineStyle::Dotted => Self::Dotted,
|
||||
UnderlineStyle::Dashed => Self::Dashed,
|
||||
UnderlineStyle::DoubleLine => Self::Double,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<UnderlineStyle> for crossterm::style::Attribute {
|
||||
fn from(style: UnderlineStyle) -> Self {
|
||||
match style {
|
||||
|
@@ -69,7 +69,7 @@ pub fn diagnostic<'doc>(
|
||||
.iter()
|
||||
.take_while(|d| {
|
||||
d.line == line
|
||||
&& d.provider.language_server_id().map_or(true, |id| {
|
||||
&& d.provider.language_server_id().is_none_or(|id| {
|
||||
doc.language_servers_with_feature(LanguageServerFeature::Diagnostics)
|
||||
.any(|ls| ls.id() == id)
|
||||
})
|
||||
|
@@ -24,6 +24,8 @@ pub struct Handlers {
|
||||
pub auto_save: Sender<AutoSaveEvent>,
|
||||
pub document_colors: Sender<lsp::DocumentColorsEvent>,
|
||||
pub word_index: word_index::Handler,
|
||||
pub pull_diagnostics: Sender<lsp::PullDiagnosticsEvent>,
|
||||
pub pull_all_documents_diagnostics: Sender<lsp::PullAllDocumentsDiagnosticsEvent>,
|
||||
}
|
||||
|
||||
impl Handlers {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::editor::Action;
|
||||
@@ -30,6 +31,14 @@ pub enum SignatureHelpEvent {
|
||||
RequestComplete { open: bool },
|
||||
}
|
||||
|
||||
pub struct PullDiagnosticsEvent {
|
||||
pub document_id: DocumentId,
|
||||
}
|
||||
|
||||
pub struct PullAllDocumentsDiagnosticsEvent {
|
||||
pub language_servers: HashSet<LanguageServerId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyEditError {
|
||||
pub kind: ApplyEditErrorKind,
|
||||
@@ -355,7 +364,7 @@ impl Editor {
|
||||
&& diagnostic
|
||||
.source
|
||||
.as_ref()
|
||||
.map_or(true, |source| !unchanged_diag_sources.contains(source))
|
||||
.is_none_or(|source| !unchanged_diag_sources.contains(source))
|
||||
};
|
||||
let diagnostics = Self::doc_diagnostics_with_filter(
|
||||
&self.language_servers,
|
||||
|
@@ -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,6 +459,117 @@ impl<'de> Deserialize<'de> for KeyEvent {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<termina::event::Event> for Event {
|
||||
fn from(event: termina::event::Event) -> Self {
|
||||
match event {
|
||||
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<termina::event::MouseEvent> for MouseEvent {
|
||||
fn from(
|
||||
termina::event::MouseEvent {
|
||||
kind,
|
||||
column,
|
||||
row,
|
||||
modifiers,
|
||||
}: termina::event::MouseEvent,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind: kind.into(),
|
||||
column,
|
||||
row,
|
||||
modifiers: modifiers.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<termina::event::MouseEventKind> for MouseEventKind {
|
||||
fn from(kind: termina::event::MouseEventKind) -> Self {
|
||||
match kind {
|
||||
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<termina::event::MouseButton> for MouseButton {
|
||||
fn from(button: termina::event::MouseButton) -> Self {
|
||||
match button {
|
||||
termina::event::MouseButton::Left => MouseButton::Left,
|
||||
termina::event::MouseButton::Right => MouseButton::Right,
|
||||
termina::event::MouseButton::Middle => MouseButton::Middle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<termina::event::KeyEvent> for KeyEvent {
|
||||
fn from(
|
||||
termina::event::KeyEvent {
|
||||
code, modifiers, ..
|
||||
}: termina::event::KeyEvent,
|
||||
) -> Self {
|
||||
if code == termina::event::KeyCode::BackTab {
|
||||
// special case for BackTab -> Shift-Tab
|
||||
let mut modifiers: KeyModifiers = modifiers.into();
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
Self {
|
||||
code: KeyCode::Tab,
|
||||
modifiers,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
code: code.into(),
|
||||
modifiers: modifiers.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
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);
|
||||
termina::event::KeyEvent {
|
||||
code: termina::event::KeyCode::BackTab,
|
||||
modifiers: modifiers.into(),
|
||||
kind: termina::event::KeyEventKind::Press,
|
||||
state: termina::event::KeyEventState::NONE,
|
||||
}
|
||||
} else {
|
||||
termina::event::KeyEvent {
|
||||
code: code.into(),
|
||||
modifiers: modifiers.into(),
|
||||
kind: termina::event::KeyEventKind::Press,
|
||||
state: termina::event::KeyEventState::NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::Event> for Event {
|
||||
fn from(event: crossterm::event::Event) -> Self {
|
||||
match event {
|
||||
@@ -472,7 +583,7 @@ impl From<crossterm::event::Event> for Event {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::MouseEvent> for MouseEvent {
|
||||
fn from(
|
||||
crossterm::event::MouseEvent {
|
||||
@@ -491,7 +602,7 @@ impl From<crossterm::event::MouseEvent> for MouseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::MouseEventKind> for MouseEventKind {
|
||||
fn from(kind: crossterm::event::MouseEventKind) -> Self {
|
||||
match kind {
|
||||
@@ -507,7 +618,7 @@ impl From<crossterm::event::MouseEventKind> for MouseEventKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::MouseButton> for MouseButton {
|
||||
fn from(button: crossterm::event::MouseButton) -> Self {
|
||||
match button {
|
||||
@@ -518,7 +629,7 @@ impl From<crossterm::event::MouseButton> for MouseButton {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::KeyEvent> for KeyEvent {
|
||||
fn from(
|
||||
crossterm::event::KeyEvent {
|
||||
@@ -542,7 +653,7 @@ impl From<crossterm::event::KeyEvent> for KeyEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<KeyEvent> for crossterm::event::KeyEvent {
|
||||
fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
|
||||
if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
|
||||
@@ -565,7 +676,6 @@ impl From<KeyEvent> for crossterm::event::KeyEvent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
|
||||
use anyhow::Context;
|
||||
let mut keys_res: anyhow::Result<_> = Ok(Vec::new());
|
||||
|
@@ -13,6 +13,54 @@ bitflags! {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<KeyModifiers> for termina::event::Modifiers {
|
||||
fn from(key_modifiers: KeyModifiers) -> Self {
|
||||
use termina::event::Modifiers as CKeyModifiers;
|
||||
|
||||
let mut result = CKeyModifiers::NONE;
|
||||
|
||||
if key_modifiers.contains(KeyModifiers::SHIFT) {
|
||||
result.insert(CKeyModifiers::SHIFT);
|
||||
}
|
||||
if key_modifiers.contains(KeyModifiers::CONTROL) {
|
||||
result.insert(CKeyModifiers::CONTROL);
|
||||
}
|
||||
if key_modifiers.contains(KeyModifiers::ALT) {
|
||||
result.insert(CKeyModifiers::ALT);
|
||||
}
|
||||
if key_modifiers.contains(KeyModifiers::SUPER) {
|
||||
result.insert(CKeyModifiers::SUPER);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
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;
|
||||
|
||||
if val.contains(CKeyModifiers::SHIFT) {
|
||||
result.insert(KeyModifiers::SHIFT);
|
||||
}
|
||||
if val.contains(CKeyModifiers::CONTROL) {
|
||||
result.insert(KeyModifiers::CONTROL);
|
||||
}
|
||||
if val.contains(CKeyModifiers::ALT) {
|
||||
result.insert(KeyModifiers::ALT);
|
||||
}
|
||||
if val.contains(CKeyModifiers::SUPER) {
|
||||
result.insert(KeyModifiers::SUPER);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<KeyModifiers> for crossterm::event::KeyModifiers {
|
||||
fn from(key_modifiers: KeyModifiers) -> Self {
|
||||
use crossterm::event::KeyModifiers as CKeyModifiers;
|
||||
@@ -36,7 +84,7 @@ impl From<KeyModifiers> for crossterm::event::KeyModifiers {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::KeyModifiers> for KeyModifiers {
|
||||
fn from(val: crossterm::event::KeyModifiers) -> Self {
|
||||
use crossterm::event::KeyModifiers as CKeyModifiers;
|
||||
@@ -59,7 +107,6 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a media key (as part of [`KeyCode::Media`]).
|
||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum MediaKeyCode {
|
||||
@@ -92,6 +139,52 @@ pub enum MediaKeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<MediaKeyCode> for termina::event::MediaKeyCode {
|
||||
fn from(media_key_code: MediaKeyCode) -> Self {
|
||||
use termina::event::MediaKeyCode as CMediaKeyCode;
|
||||
|
||||
match media_key_code {
|
||||
MediaKeyCode::Play => CMediaKeyCode::Play,
|
||||
MediaKeyCode::Pause => CMediaKeyCode::Pause,
|
||||
MediaKeyCode::PlayPause => CMediaKeyCode::PlayPause,
|
||||
MediaKeyCode::Reverse => CMediaKeyCode::Reverse,
|
||||
MediaKeyCode::Stop => CMediaKeyCode::Stop,
|
||||
MediaKeyCode::FastForward => CMediaKeyCode::FastForward,
|
||||
MediaKeyCode::Rewind => CMediaKeyCode::Rewind,
|
||||
MediaKeyCode::TrackNext => CMediaKeyCode::TrackNext,
|
||||
MediaKeyCode::TrackPrevious => CMediaKeyCode::TrackPrevious,
|
||||
MediaKeyCode::Record => CMediaKeyCode::Record,
|
||||
MediaKeyCode::LowerVolume => CMediaKeyCode::LowerVolume,
|
||||
MediaKeyCode::RaiseVolume => CMediaKeyCode::RaiseVolume,
|
||||
MediaKeyCode::MuteVolume => CMediaKeyCode::MuteVolume,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
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,
|
||||
CMediaKeyCode::Pause => MediaKeyCode::Pause,
|
||||
CMediaKeyCode::PlayPause => MediaKeyCode::PlayPause,
|
||||
CMediaKeyCode::Reverse => MediaKeyCode::Reverse,
|
||||
CMediaKeyCode::Stop => MediaKeyCode::Stop,
|
||||
CMediaKeyCode::FastForward => MediaKeyCode::FastForward,
|
||||
CMediaKeyCode::Rewind => MediaKeyCode::Rewind,
|
||||
CMediaKeyCode::TrackNext => MediaKeyCode::TrackNext,
|
||||
CMediaKeyCode::TrackPrevious => MediaKeyCode::TrackPrevious,
|
||||
CMediaKeyCode::Record => MediaKeyCode::Record,
|
||||
CMediaKeyCode::LowerVolume => MediaKeyCode::LowerVolume,
|
||||
CMediaKeyCode::RaiseVolume => MediaKeyCode::RaiseVolume,
|
||||
CMediaKeyCode::MuteVolume => MediaKeyCode::MuteVolume,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<MediaKeyCode> for crossterm::event::MediaKeyCode {
|
||||
fn from(media_key_code: MediaKeyCode) -> Self {
|
||||
use crossterm::event::MediaKeyCode as CMediaKeyCode;
|
||||
@@ -114,7 +207,7 @@ impl From<MediaKeyCode> for crossterm::event::MediaKeyCode {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::MediaKeyCode> for MediaKeyCode {
|
||||
fn from(val: crossterm::event::MediaKeyCode) -> Self {
|
||||
use crossterm::event::MediaKeyCode as CMediaKeyCode;
|
||||
@@ -136,7 +229,6 @@ impl From<crossterm::event::MediaKeyCode> for MediaKeyCode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a media key (as part of [`KeyCode::Modifier`]).
|
||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum ModifierKeyCode {
|
||||
@@ -171,6 +263,54 @@ pub enum ModifierKeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<ModifierKeyCode> for termina::event::ModifierKeyCode {
|
||||
fn from(modifier_key_code: ModifierKeyCode) -> Self {
|
||||
use termina::event::ModifierKeyCode as CModifierKeyCode;
|
||||
|
||||
match modifier_key_code {
|
||||
ModifierKeyCode::LeftShift => CModifierKeyCode::LeftShift,
|
||||
ModifierKeyCode::LeftControl => CModifierKeyCode::LeftControl,
|
||||
ModifierKeyCode::LeftAlt => CModifierKeyCode::LeftAlt,
|
||||
ModifierKeyCode::LeftSuper => CModifierKeyCode::LeftSuper,
|
||||
ModifierKeyCode::LeftHyper => CModifierKeyCode::LeftHyper,
|
||||
ModifierKeyCode::LeftMeta => CModifierKeyCode::LeftMeta,
|
||||
ModifierKeyCode::RightShift => CModifierKeyCode::RightShift,
|
||||
ModifierKeyCode::RightControl => CModifierKeyCode::RightControl,
|
||||
ModifierKeyCode::RightAlt => CModifierKeyCode::RightAlt,
|
||||
ModifierKeyCode::RightSuper => CModifierKeyCode::RightSuper,
|
||||
ModifierKeyCode::RightHyper => CModifierKeyCode::RightHyper,
|
||||
ModifierKeyCode::RightMeta => CModifierKeyCode::RightMeta,
|
||||
ModifierKeyCode::IsoLevel3Shift => CModifierKeyCode::IsoLevel3Shift,
|
||||
ModifierKeyCode::IsoLevel5Shift => CModifierKeyCode::IsoLevel5Shift,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
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,
|
||||
CModifierKeyCode::LeftControl => ModifierKeyCode::LeftControl,
|
||||
CModifierKeyCode::LeftAlt => ModifierKeyCode::LeftAlt,
|
||||
CModifierKeyCode::LeftSuper => ModifierKeyCode::LeftSuper,
|
||||
CModifierKeyCode::LeftHyper => ModifierKeyCode::LeftHyper,
|
||||
CModifierKeyCode::LeftMeta => ModifierKeyCode::LeftMeta,
|
||||
CModifierKeyCode::RightShift => ModifierKeyCode::RightShift,
|
||||
CModifierKeyCode::RightControl => ModifierKeyCode::RightControl,
|
||||
CModifierKeyCode::RightAlt => ModifierKeyCode::RightAlt,
|
||||
CModifierKeyCode::RightSuper => ModifierKeyCode::RightSuper,
|
||||
CModifierKeyCode::RightHyper => ModifierKeyCode::RightHyper,
|
||||
CModifierKeyCode::RightMeta => ModifierKeyCode::RightMeta,
|
||||
CModifierKeyCode::IsoLevel3Shift => ModifierKeyCode::IsoLevel3Shift,
|
||||
CModifierKeyCode::IsoLevel5Shift => ModifierKeyCode::IsoLevel5Shift,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode {
|
||||
fn from(modifier_key_code: ModifierKeyCode) -> Self {
|
||||
use crossterm::event::ModifierKeyCode as CModifierKeyCode;
|
||||
@@ -194,7 +334,7 @@ impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::ModifierKeyCode> for ModifierKeyCode {
|
||||
fn from(val: crossterm::event::ModifierKeyCode) -> Self {
|
||||
use crossterm::event::ModifierKeyCode as CModifierKeyCode;
|
||||
@@ -217,7 +357,6 @@ impl From<crossterm::event::ModifierKeyCode> for ModifierKeyCode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a key.
|
||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum KeyCode {
|
||||
@@ -280,6 +419,79 @@ pub enum KeyCode {
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<KeyCode> for termina::event::KeyCode {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
use termina::event::KeyCode as CKeyCode;
|
||||
|
||||
match key_code {
|
||||
KeyCode::Backspace => CKeyCode::Backspace,
|
||||
KeyCode::Enter => CKeyCode::Enter,
|
||||
KeyCode::Left => CKeyCode::Left,
|
||||
KeyCode::Right => CKeyCode::Right,
|
||||
KeyCode::Up => CKeyCode::Up,
|
||||
KeyCode::Down => CKeyCode::Down,
|
||||
KeyCode::Home => CKeyCode::Home,
|
||||
KeyCode::End => CKeyCode::End,
|
||||
KeyCode::PageUp => CKeyCode::PageUp,
|
||||
KeyCode::PageDown => CKeyCode::PageDown,
|
||||
KeyCode::Tab => CKeyCode::Tab,
|
||||
KeyCode::Delete => CKeyCode::Delete,
|
||||
KeyCode::Insert => CKeyCode::Insert,
|
||||
KeyCode::F(f_number) => CKeyCode::Function(f_number),
|
||||
KeyCode::Char(character) => CKeyCode::Char(character),
|
||||
KeyCode::Null => CKeyCode::Null,
|
||||
KeyCode::Esc => CKeyCode::Escape,
|
||||
KeyCode::CapsLock => CKeyCode::CapsLock,
|
||||
KeyCode::ScrollLock => CKeyCode::ScrollLock,
|
||||
KeyCode::NumLock => CKeyCode::NumLock,
|
||||
KeyCode::PrintScreen => CKeyCode::PrintScreen,
|
||||
KeyCode::Pause => CKeyCode::Pause,
|
||||
KeyCode::Menu => CKeyCode::Menu,
|
||||
KeyCode::KeypadBegin => CKeyCode::KeypadBegin,
|
||||
KeyCode::Media(media_key_code) => CKeyCode::Media(media_key_code.into()),
|
||||
KeyCode::Modifier(modifier_key_code) => CKeyCode::Modifier(modifier_key_code.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
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,
|
||||
CKeyCode::Enter => KeyCode::Enter,
|
||||
CKeyCode::Left => KeyCode::Left,
|
||||
CKeyCode::Right => KeyCode::Right,
|
||||
CKeyCode::Up => KeyCode::Up,
|
||||
CKeyCode::Down => KeyCode::Down,
|
||||
CKeyCode::Home => KeyCode::Home,
|
||||
CKeyCode::End => KeyCode::End,
|
||||
CKeyCode::PageUp => KeyCode::PageUp,
|
||||
CKeyCode::PageDown => KeyCode::PageDown,
|
||||
CKeyCode::Tab => KeyCode::Tab,
|
||||
CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
|
||||
CKeyCode::Delete => KeyCode::Delete,
|
||||
CKeyCode::Insert => KeyCode::Insert,
|
||||
CKeyCode::Function(f_number) => KeyCode::F(f_number),
|
||||
CKeyCode::Char(character) => KeyCode::Char(character),
|
||||
CKeyCode::Null => KeyCode::Null,
|
||||
CKeyCode::Escape => KeyCode::Esc,
|
||||
CKeyCode::CapsLock => KeyCode::CapsLock,
|
||||
CKeyCode::ScrollLock => KeyCode::ScrollLock,
|
||||
CKeyCode::NumLock => KeyCode::NumLock,
|
||||
CKeyCode::PrintScreen => KeyCode::PrintScreen,
|
||||
CKeyCode::Pause => KeyCode::Pause,
|
||||
CKeyCode::Menu => KeyCode::Menu,
|
||||
CKeyCode::KeypadBegin => KeyCode::KeypadBegin,
|
||||
CKeyCode::Media(media_key_code) => KeyCode::Media(media_key_code.into()),
|
||||
CKeyCode::Modifier(modifier_key_code) => KeyCode::Modifier(modifier_key_code.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<KeyCode> for crossterm::event::KeyCode {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
use crossterm::event::KeyCode as CKeyCode;
|
||||
@@ -315,7 +527,7 @@ impl From<KeyCode> for crossterm::event::KeyCode {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
#[cfg(all(feature = "term", windows))]
|
||||
impl From<crossterm::event::KeyCode> for KeyCode {
|
||||
fn from(val: crossterm::event::KeyCode) -> Self {
|
||||
use crossterm::event::KeyCode as CKeyCode;
|
||||
|
@@ -2,7 +2,6 @@
|
||||
pub mod macros;
|
||||
|
||||
pub mod annotations;
|
||||
pub mod base64;
|
||||
pub mod clipboard;
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
|
@@ -35,6 +35,75 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
|
||||
..Theme::from(BASE16_DEFAULT_THEME_DATA.clone())
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Mode {
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<termina::escape::csi::ThemeMode> for Mode {
|
||||
fn from(mode: termina::escape::csi::ThemeMode) -> Self {
|
||||
match mode {
|
||||
termina::escape::csi::ThemeMode::Dark => Self::Dark,
|
||||
termina::escape::csi::ThemeMode::Light => Self::Light,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
light: String,
|
||||
dark: String,
|
||||
/// A theme to choose when the terminal did not declare either light or dark mode.
|
||||
/// When not specified the dark theme is preferred.
|
||||
fallback: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn choose(&self, preference: Option<Mode>) -> &str {
|
||||
match preference {
|
||||
Some(Mode::Light) => &self.light,
|
||||
Some(Mode::Dark) => &self.dark,
|
||||
None => self.fallback.as_ref().unwrap_or(&self.dark),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields, rename_all = "kebab-case")]
|
||||
enum InnerConfig {
|
||||
Constant(String),
|
||||
Adaptive {
|
||||
dark: String,
|
||||
light: String,
|
||||
fallback: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
let inner = InnerConfig::deserialize(deserializer)?;
|
||||
|
||||
let (light, dark, fallback) = match inner {
|
||||
InnerConfig::Constant(theme) => (theme.clone(), theme.clone(), None),
|
||||
InnerConfig::Adaptive {
|
||||
light,
|
||||
dark,
|
||||
fallback,
|
||||
} => (light, dark, fallback),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
light,
|
||||
dark,
|
||||
fallback,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Loader {
|
||||
/// Theme directories to search from highest to lowest priority
|
||||
|
@@ -441,7 +441,7 @@ impl Tree {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn traverse(&self) -> Traverse {
|
||||
pub fn traverse(&self) -> Traverse<'_> {
|
||||
Traverse::new(self)
|
||||
}
|
||||
|
||||
|
346
languages.toml
346
languages.toml
@@ -38,22 +38,26 @@ docker-compose-langserver = { command = "docker-compose-langserver", args = ["--
|
||||
dot-language-server = { command = "dot-language-server", args = ["--stdio"] }
|
||||
dts-lsp = { command = "dts-lsp" }
|
||||
earthlyls = { command = "earthlyls" }
|
||||
eiffel-language-server = {command = "eiffel-language-server"}
|
||||
elixir-ls = { command = "elixir-ls", config = { elixirLS.dialyzerEnabled = false } }
|
||||
elm-language-server = { command = "elm-language-server" }
|
||||
elp = { command = "elp", args = ["server"] }
|
||||
elvish = { command = "elvish", args = ["-lsp"] }
|
||||
erlang-ls = { command = "erlang_ls" }
|
||||
expert = { command = "expert" }
|
||||
fennel-ls = { command = "fennel-ls" }
|
||||
fish-lsp = { command = "fish-lsp", args = ["start"], environment = { fish_lsp_show_client_popups = "false" } }
|
||||
forc = { command = "forc", args = ["lsp"] }
|
||||
forth-lsp = { command = "forth-lsp" }
|
||||
fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
|
||||
fsharp-ls = { command = "fsautocomplete", config = { AutomaticWorkspaceInit = true } }
|
||||
gitlab-ci-ls = { command = "gitlab-ci-ls" }
|
||||
gleam = { command = "gleam", args = ["lsp"] }
|
||||
glsl_analyzer = { command = "glsl_analyzer" }
|
||||
graphql-language-service = { command = "graphql-lsp", args = ["server", "-m", "stream"] }
|
||||
harper-ls = { command = "harper-ls", args = ["--stdio"] }
|
||||
haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
|
||||
hdls = { command = "hdls" }
|
||||
hyprls = { command = "hyprls" }
|
||||
idris2-lsp = { command = "idris2-lsp" }
|
||||
intelephense = { command = "intelephense", args = ["--stdio"] }
|
||||
@@ -67,7 +71,7 @@ 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"] }
|
||||
lean = { command = "lake", args = ["serve"] }
|
||||
ltex-ls = { command = "ltex-ls" }
|
||||
ltex-ls-plus = { command = "ltex-ls-plus" }
|
||||
markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
|
||||
@@ -139,6 +143,7 @@ vscode-html-language-server = { command = "vscode-html-language-server", args =
|
||||
vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true, json = { validate = { enable = true } } } }
|
||||
vuels = { command = "vue-language-server", args = ["--stdio"], config = { typescript = { tsdk = "node_modules/typescript/lib/" } } }
|
||||
wgsl-analyzer = { command = "wgsl-analyzer" }
|
||||
wikitext-lsp = { command = "wikitext-lsp", args = ["--stdio"]}
|
||||
yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
|
||||
yls = { command = "yls", args = ["-vv"] }
|
||||
zls = { command = "zls" }
|
||||
@@ -275,6 +280,10 @@ mode = "location"
|
||||
command = "clarinet"
|
||||
args = ["lsp"]
|
||||
|
||||
[language-server.docker-language-server]
|
||||
command = "docker-language-server"
|
||||
args = ["start", "--stdio"]
|
||||
|
||||
[[language]]
|
||||
name = "rust"
|
||||
scope = "source.rust"
|
||||
@@ -351,7 +360,20 @@ source = { git = "https://github.com/FuelLabs/tree-sitter-sway", rev = "e491a005
|
||||
name = "toml"
|
||||
scope = "source.toml"
|
||||
injection-regex = "toml"
|
||||
file-types = ["toml", { glob = "pdm.lock" }, { glob = "poetry.lock" }, { glob = "Cargo.lock" }, { glob = "uv.lock" }]
|
||||
file-types = [
|
||||
"toml",
|
||||
{ glob = "pdm.lock" },
|
||||
{ glob = "poetry.lock" },
|
||||
{ glob = "Cargo.lock" },
|
||||
{ glob = "uv.lock" },
|
||||
{ glob = "containers.conf" },
|
||||
{ glob = "containers.conf.d/*.conf" },
|
||||
{ glob = "containers.conf.modules/*.conf" },
|
||||
{ glob = "mounts.conf" },
|
||||
{ glob = "policy.conf" },
|
||||
{ glob = "registries.conf" },
|
||||
{ glob = "storage.conf" },
|
||||
]
|
||||
comment-token = "#"
|
||||
language-servers = [ "taplo", "tombi" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
@@ -401,6 +423,17 @@ auto-format = true
|
||||
name = "textproto"
|
||||
source = { git = "https://github.com/PorterAtGoogle/tree-sitter-textproto", rev = "568471b80fd8793d37ed01865d8c2208a9fefd1b"}
|
||||
|
||||
[[language]]
|
||||
name = "eiffel"
|
||||
scope = "source.eiffel"
|
||||
file-types = ["e"]
|
||||
comment-token = "--"
|
||||
language-servers = ["eiffel-language-server"]
|
||||
|
||||
[[grammar]]
|
||||
name = "eiffel"
|
||||
source = { git = "https://github.com/imustafin/tree-sitter-eiffel", rev = "d934fb44f1d22bb76be6b56a7b2425ab3b1daf8b" }
|
||||
|
||||
[[language]]
|
||||
name = "elixir"
|
||||
scope = "source.elixir"
|
||||
@@ -409,7 +442,7 @@ file-types = ["ex", "exs", { glob = "mix.lock" }]
|
||||
shebangs = ["elixir"]
|
||||
roots = ["mix.exs", "mix.lock"]
|
||||
comment-token = "#"
|
||||
language-servers = [ "elixir-ls" ]
|
||||
language-servers = [ "elixir-ls", "expert" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
@@ -446,6 +479,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"
|
||||
@@ -675,7 +720,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" ]
|
||||
@@ -779,7 +824,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"
|
||||
@@ -831,7 +876,7 @@ injection-regex = "go-format-string"
|
||||
|
||||
[[grammar]]
|
||||
name = "go-format-string"
|
||||
source = { git = "https://codeberg.org/kpbaks/tree-sitter-go-format-string", rev = "45b559b74be97535abfc77ade4405343a43e5ca4" }
|
||||
source = { git = "https://codeberg.org/kpbaks/tree-sitter-go-format-string", rev = "06587ea641155db638f46a32c959d68796cd36bb" }
|
||||
|
||||
[[language]]
|
||||
name = "javascript"
|
||||
@@ -938,7 +983,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"
|
||||
@@ -974,6 +1019,7 @@ scope = "source.htmldjango"
|
||||
injection-regex = "htmldjango"
|
||||
language-servers = ["djlsp", "vscode-html-language-server", "superhtml"]
|
||||
file-types = []
|
||||
roots = ["manage.py"]
|
||||
|
||||
[language.auto-pairs]
|
||||
'"' = '"'
|
||||
@@ -1078,6 +1124,7 @@ file-types = [
|
||||
{ glob = "Scanfile" },
|
||||
{ glob = "Snapfile" },
|
||||
{ glob = "Gymfile" },
|
||||
{ glob = ".irbrc" },
|
||||
]
|
||||
shebangs = ["ruby"]
|
||||
comment-token = "#"
|
||||
@@ -1116,6 +1163,7 @@ file-types = [
|
||||
"zshrc_Apple_Terminal",
|
||||
{ glob = "i3/config" },
|
||||
{ glob = "sway/config" },
|
||||
{ glob = ".tmux.conf" },
|
||||
{ glob = "tmux.conf" },
|
||||
{ glob = ".bash_history" },
|
||||
{ glob = ".bash_login" },
|
||||
@@ -1138,6 +1186,10 @@ file-types = [
|
||||
{ glob = ".yashrc" },
|
||||
{ glob = ".yash_profile" },
|
||||
{ glob = ".hushlogin" },
|
||||
{ glob = ".xinitrc" }, # ~/.xinitrc
|
||||
{ glob = "xinitrc" }, # /etc/X11/xinit/xinitrc
|
||||
{ glob = ".xserverrc" }, # ~/.xserverrc
|
||||
{ glob = "xserverrc" }, # /etc/X11/xinit/xserverrc
|
||||
]
|
||||
shebangs = ["sh", "bash", "dash", "zsh"]
|
||||
comment-token = "#"
|
||||
@@ -1452,7 +1504,7 @@ language-servers = [ "svelteserver" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "svelte"
|
||||
source = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "60ea1d673a1a3eeeb597e098d9ada9ed0c79ef4b" }
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-svelte", rev = "ae5199db47757f785e43a14b332118a5474de1a2" }
|
||||
|
||||
[[language]]
|
||||
name = "vue"
|
||||
@@ -1479,6 +1531,8 @@ file-types = [
|
||||
{ glob = ".clang-format" },
|
||||
{ glob = ".clang-tidy" },
|
||||
{ glob = ".gem/credentials" },
|
||||
{ glob = ".kube/config" },
|
||||
{ glob = ".kube/kuberc" },
|
||||
"sublime-syntax"
|
||||
]
|
||||
comment-token = "#"
|
||||
@@ -1507,6 +1561,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" ]
|
||||
@@ -1581,7 +1636,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 = "6479aa13f32f701c383083d8b28360ebd682fb7d" }
|
||||
|
||||
[[language]]
|
||||
name = "prolog"
|
||||
@@ -1659,7 +1714,22 @@ source = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "88408ff
|
||||
[[language]]
|
||||
name = "perl"
|
||||
scope = "source.perl"
|
||||
file-types = ["pl", "pm", "t", "psgi", "raku", "rakumod", "rakutest", "rakudoc", "nqp", "p6", "pl6", "pm6"]
|
||||
file-types =[
|
||||
"pl",
|
||||
"pm",
|
||||
"t",
|
||||
"psgi",
|
||||
"raku",
|
||||
"rakumod",
|
||||
"rakutest",
|
||||
"rakudoc",
|
||||
"nqp",
|
||||
"p6",
|
||||
"pl6",
|
||||
"pm6",
|
||||
{glob = "latexmkrc"},
|
||||
{glob = ".latexmkrc"},
|
||||
]
|
||||
shebangs = ["perl"]
|
||||
comment-token = "#"
|
||||
language-servers = [ "perlnavigator" ]
|
||||
@@ -1796,7 +1866,7 @@ injection-regex = "mail|eml|email"
|
||||
|
||||
[[grammar]]
|
||||
name = "mail"
|
||||
source = { git = "https://github.com/ficcdaf/tree-sitter-mail", rev = "8e60f38efbae1cc5f22833ae13c5500dd0f3b12f" }
|
||||
source = { git = "https://codeberg.org/ficd/tree-sitter-mail", rev = "8e60f38efbae1cc5f22833ae13c5500dd0f3b12f" }
|
||||
|
||||
[[language]]
|
||||
name = "markdown"
|
||||
@@ -1905,8 +1975,8 @@ file-types = [
|
||||
{ glob = "containerfile.*" },
|
||||
]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = [ "docker-langserver" ]
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
language-servers = [ "docker-langserver", "docker-language-server" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "dockerfile"
|
||||
@@ -1916,7 +1986,7 @@ source = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev =
|
||||
name = "docker-compose"
|
||||
scope = "source.yaml.docker-compose"
|
||||
roots = ["docker-compose.yaml", "docker-compose.yml", "compose.yaml", "compose.yml"]
|
||||
language-servers = [ "docker-compose-langserver", "yaml-language-server" ]
|
||||
language-servers = [ "docker-compose-langserver", "yaml-language-server", "docker-language-server" ]
|
||||
file-types = [{ glob = "docker-compose.yaml" }, { glob = "docker-compose.yml" }, { glob = "compose.yaml" }, { glob = "compose.yml" }]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
@@ -2007,7 +2077,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"
|
||||
@@ -2171,7 +2241,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"
|
||||
@@ -2292,7 +2362,7 @@ file-types = ["heex"]
|
||||
roots = ["mix.exs", "mix.lock"]
|
||||
block-comment-tokens = { start = "<!--", end = "-->" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = [ "elixir-ls" ]
|
||||
language-servers = [ "elixir-ls", "expert" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "heex"
|
||||
@@ -2354,7 +2424,7 @@ language-servers = [ "nu-lsp" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "nu"
|
||||
source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "358c4f509eb97f0148bbd25ad36acc729819b9c1" }
|
||||
source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "cc4624fbc6ec3563d98fbe8f215a8b8e10b16f32" }
|
||||
|
||||
[[language]]
|
||||
name = "vala"
|
||||
@@ -2492,6 +2562,7 @@ injection-regex = "scheme"
|
||||
file-types = ["ss", "scm", "sld"]
|
||||
shebangs = ["scheme", "guile", "chicken"]
|
||||
comment-token = ";"
|
||||
block-comment-tokens = { start = "#|", end = "|#" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[language.auto-pairs]
|
||||
@@ -2523,7 +2594,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" ]
|
||||
@@ -2534,6 +2605,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"
|
||||
@@ -2602,7 +2686,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" ]
|
||||
@@ -2637,7 +2721,7 @@ language-servers = [ "idris2-lsp" ]
|
||||
name = "fortran"
|
||||
scope = "source.fortran"
|
||||
injection-regex = "fortran"
|
||||
file-types = ["f", "for", "f90", "f95", "f03"]
|
||||
file-types = ["f", "for", "f90", "f95", "f03", "F", "F90", "F95", "F03"]
|
||||
roots = ["fpm.toml"]
|
||||
comment-token = "!"
|
||||
indent = { tab-width = 4, unit = " "}
|
||||
@@ -2645,7 +2729,7 @@ language-servers = [ "fortls" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "fortran"
|
||||
source = { git = "https://github.com/stadelmanma/tree-sitter-fortran", rev = "f0f2f100952a353e64e26b0fa710b4c296d7af13" }
|
||||
source = { git = "https://github.com/stadelmanma/tree-sitter-fortran", rev = "8334abca785db3a041292e3b3b818a82a55b238f" }
|
||||
|
||||
[[language]]
|
||||
name = "ungrammar"
|
||||
@@ -2970,7 +3054,12 @@ file-types = [
|
||||
"sublime-snippet",
|
||||
"xsl",
|
||||
"mpd",
|
||||
"smil"
|
||||
"smil",
|
||||
"gpx",
|
||||
"fodt",
|
||||
"fods",
|
||||
"itermcolors",
|
||||
"terminal",
|
||||
]
|
||||
block-comment-tokens = { start = "<!--", end = "-->" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
@@ -3078,9 +3167,11 @@ file-types = [
|
||||
{ glob = "hgrc" },
|
||||
{ glob = "npmrc" },
|
||||
{ glob = "rclone.conf" },
|
||||
{ glob = ".aws/config" },
|
||||
"properties",
|
||||
"cfg",
|
||||
"directory"
|
||||
"directory",
|
||||
{ glob = ".wslconfig" },
|
||||
]
|
||||
injection-regex = "ini"
|
||||
comment-token = "#"
|
||||
@@ -3155,7 +3246,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"
|
||||
@@ -3322,7 +3413,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"
|
||||
@@ -3350,6 +3441,18 @@ 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"
|
||||
language-servers = [ "hdls" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "hdl"
|
||||
source = { git = "https://github.com/quantonganh/tree-sitter-hdl", rev="293902330423b2cd36ab1ec4b6b967163a4ed57b" }
|
||||
|
||||
[[language]]
|
||||
name = "vhdl"
|
||||
scope = "source.vhdl"
|
||||
@@ -3419,7 +3522,7 @@ indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "hurl"
|
||||
source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "cd1a0ada92cc73dd0f4d7eedc162be4ded758591" }
|
||||
source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "1124058cd192e80d80914652a5850a5b1887cc10" }
|
||||
|
||||
[[language]]
|
||||
name = "markdoc"
|
||||
@@ -3457,7 +3560,7 @@ language-servers = ["just-lsp"]
|
||||
|
||||
[[grammar]]
|
||||
name = "just"
|
||||
source = { git = "https://github.com/poliorcetics/tree-sitter-just", rev = "0f84211c637813bcf1eb32c9e35847cdaea8760d" }
|
||||
source = { git = "https://github.com/poliorcetics/tree-sitter-just", rev = "b75dace757e5d122d25c1a1a7772cb87b560f829" }
|
||||
|
||||
[[language]]
|
||||
name = "gn"
|
||||
@@ -3595,7 +3698,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
|
||||
|
||||
@@ -3621,6 +3724,25 @@ file-types = ["jjtemplate"]
|
||||
name = "jjtemplate"
|
||||
source = { git = "https://github.com/bryceberger/tree-sitter-jjtemplate", rev = "4313eda8ac31c60e550e3ad5841b100a0a686715" }
|
||||
|
||||
[[language]]
|
||||
name = "miseconfig"
|
||||
scope = "source.miseconfig"
|
||||
injection-regex = "miseconfig"
|
||||
grammar = "toml"
|
||||
file-types = [
|
||||
{ glob = "mise.toml" },
|
||||
{ glob = ".mise.toml" },
|
||||
{ glob = "mise.*.toml" },
|
||||
{ glob = ".mise.*.toml" },
|
||||
{ glob = "mise/config.toml" },
|
||||
{ glob = ".mise/config.toml" },
|
||||
{ glob = ".config/mise.toml" },
|
||||
{ glob = ".config/mise/conf.d/*.toml" },
|
||||
]
|
||||
comment-token = "#"
|
||||
language-servers = [ "taplo", "tombi" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "jq"
|
||||
scope = "source.jq"
|
||||
@@ -3733,7 +3855,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"
|
||||
@@ -3806,7 +3928,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"
|
||||
@@ -4281,7 +4403,7 @@ formatter = {command = "koto", args = ["--format"]}
|
||||
|
||||
[[grammar]]
|
||||
name = "koto"
|
||||
source = { git = "https://github.com/koto-lang/tree-sitter-koto", rev = "2ffc77c14f0ac1674384ff629bfc207b9c57ed89" }
|
||||
source = { git = "https://github.com/koto-lang/tree-sitter-koto", rev = "633744bca404ae4edb961a3c2d7bc947a987afa4" }
|
||||
|
||||
[[language]]
|
||||
name = "gpr"
|
||||
@@ -4456,6 +4578,8 @@ indent = { tab-width = 4, unit = "\t" }
|
||||
file-types = [
|
||||
"vim",
|
||||
{ glob = ".vimrc" },
|
||||
{ glob = ".nvimrc" },
|
||||
{ glob = ".exrc" }
|
||||
]
|
||||
|
||||
[[language]]
|
||||
@@ -4605,3 +4729,161 @@ 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 = " " }
|
||||
|
||||
[[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 = ".."
|
||||
|
||||
[[language]]
|
||||
name = "docker-bake"
|
||||
scope = "source.docker-bake"
|
||||
injection-regex = "docker-bake"
|
||||
grammar = "hcl"
|
||||
file-types = [
|
||||
{ glob = "docker-bake.hcl" },
|
||||
{ glob = "docker-bake.override.hcl" },
|
||||
]
|
||||
comment-token = "#"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = ["docker-language-server"]
|
||||
|
||||
[[language]]
|
||||
name = "gitlab-ci"
|
||||
scope = "source.gitlab-ci"
|
||||
injection-regex = "^gitlab-ci$"
|
||||
file-types = [{ glob = ".gitlab-ci.yml" }]
|
||||
grammar = "yaml"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = ["yaml-language-server", "gitlab-ci-ls"]
|
||||
comment-token = "#"
|
||||
|
||||
[[grammar]]
|
||||
name = "wikitext"
|
||||
source = { git = "https://github.com/santhoshtr/tree-sitter-wikitext", rev = "444214b31695e9dd4d32fb06247397fb8778a9d2"}
|
||||
|
||||
[[language]]
|
||||
name = "wikitext"
|
||||
scope = "source.wikitext"
|
||||
file-types = ["wikimedia", "mediawiki", "wikitext"]
|
||||
language-servers = ["wikitext-lsp"]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
block-comment-tokens = { start = "<!--", end = "-->" }
|
||||
word-completion.trigger-length = 4
|
||||
|
||||
[[language]]
|
||||
name = "slisp"
|
||||
scope = "source.sl"
|
||||
injection-regex = "sl"
|
||||
file-types = ["sl"]
|
||||
comment-token = ";"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "slisp"
|
||||
source = { git = "https://github.com/xenogenics/tree-sitter-slisp", rev = "29f9c6707ce9dfc2fc915d175ec720b207f179f3" }
|
||||
|
||||
[[language]]
|
||||
name = "nearley"
|
||||
scope = "source.nearley"
|
||||
file-types = ["ne"]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "nearley"
|
||||
source = { git = "https://github.com/mi2ebi/tree-sitter-nearley", rev = "12d01113e194c8e83f6341aab8c2a5f21db9cac9" }
|
||||
|
@@ -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"))
|
||||
|
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
runtime/queries/cross-config/highlights.scm
Normal file
1
runtime/queries/cross-config/highlights.scm
Normal file
@@ -0,0 +1 @@
|
||||
; inherits: toml
|
9
runtime/queries/cross-config/injections.scm
Normal file
9
runtime/queries/cross-config/injections.scm
Normal file
@@ -0,0 +1,9 @@
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
||||
|
||||
; https://github.com/cross-rs/cross/blob/main/docs/config_file.md
|
||||
(pair
|
||||
(bare_key) @_key (#eq? @_key "pre-build")
|
||||
(array
|
||||
(string) @injection.content)
|
||||
(#set! injection.language "bash"))
|
1
runtime/queries/cross-config/rainbows.scm
Normal file
1
runtime/queries/cross-config/rainbows.scm
Normal file
@@ -0,0 +1 @@
|
||||
; inherits: toml
|
1
runtime/queries/cross-config/textobjects.scm
Normal file
1
runtime/queries/cross-config/textobjects.scm
Normal file
@@ -0,0 +1 @@
|
||||
; inherits: toml
|
@@ -1,67 +1,78 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(tag_name)
|
||||
(nesting_selector)
|
||||
(universal_selector)
|
||||
(tag_name)
|
||||
(nesting_selector)
|
||||
(universal_selector)
|
||||
] @tag
|
||||
|
||||
[
|
||||
"~"
|
||||
">"
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
"="
|
||||
"^="
|
||||
"|="
|
||||
"~="
|
||||
"$="
|
||||
"*="
|
||||
"~"
|
||||
">"
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
"="
|
||||
"^="
|
||||
"|="
|
||||
"~="
|
||||
"$="
|
||||
"*="
|
||||
] @operator
|
||||
|
||||
[
|
||||
"and"
|
||||
"not"
|
||||
"only"
|
||||
"or"
|
||||
"and"
|
||||
"not"
|
||||
"only"
|
||||
"or"
|
||||
] @keyword.operator
|
||||
|
||||
(attribute_selector (plain_value) @string)
|
||||
|
||||
(property_name) @variable.other.member
|
||||
(plain_value) @constant
|
||||
|
||||
((property_name) @variable
|
||||
(#match? @variable "^--"))
|
||||
(#match? @variable "^--"))
|
||||
((plain_value) @variable
|
||||
(#match? @variable "^--"))
|
||||
(#match? @variable "^--"))
|
||||
|
||||
(attribute_name) @attribute
|
||||
(class_name) @label
|
||||
(feature_name) @variable.other.member
|
||||
(function_name) @function
|
||||
(id_name) @label
|
||||
(namespace_name) @namespace
|
||||
|
||||
[
|
||||
"@charset"
|
||||
"@import"
|
||||
"@keyframes"
|
||||
"@media"
|
||||
"@namespace"
|
||||
"@supports"
|
||||
(at_keyword)
|
||||
(from)
|
||||
(important)
|
||||
(to)
|
||||
(keyword_query)
|
||||
(keyframes_name)
|
||||
(unit)
|
||||
] @keyword
|
||||
(attribute_name) @attribute
|
||||
(pseudo_element_selector (tag_name) @attribute)
|
||||
(pseudo_class_selector (class_name) @attribute)
|
||||
|
||||
[
|
||||
"#"
|
||||
"."
|
||||
"@charset"
|
||||
"@import"
|
||||
"@keyframes"
|
||||
"@media"
|
||||
"@namespace"
|
||||
"@supports"
|
||||
(at_keyword)
|
||||
(from)
|
||||
(important)
|
||||
(to)
|
||||
(keyword_query)
|
||||
(keyframes_name)
|
||||
(unit)
|
||||
] @keyword
|
||||
|
||||
; @apply something;
|
||||
(at_rule
|
||||
. (at_keyword) @keyword
|
||||
. (keyword_query) @constant
|
||||
(#eq? @keyword "@apply"))
|
||||
|
||||
[
|
||||
"#"
|
||||
"."
|
||||
] @punctuation
|
||||
|
||||
(string_value) @string
|
||||
@@ -72,17 +83,17 @@
|
||||
(float_value) @constant.numeric.float
|
||||
|
||||
[
|
||||
")"
|
||||
"("
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
")"
|
||||
"("
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
","
|
||||
";"
|
||||
":"
|
||||
"::"
|
||||
","
|
||||
";"
|
||||
":"
|
||||
"::"
|
||||
] @punctuation.delimiter
|
||||
|
28
runtime/queries/cython/folds.scm
Normal file
28
runtime/queries/cython/folds.scm
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
(function_definition)
|
||||
(class_definition)
|
||||
(while_statement)
|
||||
(for_statement)
|
||||
(if_statement)
|
||||
(with_statement)
|
||||
(try_statement)
|
||||
(match_statement)
|
||||
(import_from_statement)
|
||||
(parameters)
|
||||
(argument_list)
|
||||
(parenthesized_expression)
|
||||
(generator_expression)
|
||||
(list_comprehension)
|
||||
(set_comprehension)
|
||||
(dictionary_comprehension)
|
||||
(tuple)
|
||||
(list)
|
||||
(set)
|
||||
(dictionary)
|
||||
(string)
|
||||
] @fold
|
||||
|
||||
[
|
||||
(import_statement)
|
||||
(import_from_statement)
|
||||
]+ @fold
|
199
runtime/queries/cython/highlights.scm
Normal file
199
runtime/queries/cython/highlights.scm
Normal file
@@ -0,0 +1,199 @@
|
||||
; Punctuation
|
||||
|
||||
["," "." ":" ";" (ellipsis)] @punctuation.delimiter
|
||||
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
|
||||
(interpolation
|
||||
"{" @punctuation.special
|
||||
"}" @punctuation.special)
|
||||
|
||||
; Identifier naming conventions
|
||||
|
||||
(identifier) @variable
|
||||
|
||||
((identifier) @constructor
|
||||
(#match? @constructor "^[A-Z]"))
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z_]*$"))
|
||||
|
||||
; Function calls
|
||||
|
||||
(decorator) @function
|
||||
|
||||
(call
|
||||
function: (attribute attribute: (identifier) @function.method))
|
||||
(call
|
||||
function: (identifier) @function)
|
||||
|
||||
; Builtin functions
|
||||
|
||||
((call
|
||||
function: (identifier) @function.builtin)
|
||||
(#any-of?
|
||||
@function.builtin
|
||||
"abs" "all" "any" "ascii" "bin" "bool" "breakpoint" "bytearray" "bytes" "callable" "chr" "classmethod" "compile" "complex" "delattr" "dict" "dir" "divmod" "enumerate" "eval" "exec" "filter" "float" "format" "frozenset" "getattr" "globals" "hasattr" "hash" "help" "hex" "id" "input" "int" "isinstance" "issubclass" "iter" "len" "list" "locals" "map" "max" "memoryview" "min" "next" "object" "oct" "open" "ord" "pow" "print" "property" "range" "repr" "reversed" "round" "set" "setattr" "slice" "sorted" "staticmethod" "str" "sum" "super" "tuple" "type" "vars" "zip" "__import__"))
|
||||
|
||||
; Types
|
||||
|
||||
(maybe_typed_name
|
||||
type: ((_) @type))
|
||||
|
||||
(type
|
||||
(identifier) @type)
|
||||
|
||||
(c_type
|
||||
type: ((_) @type))
|
||||
(c_type
|
||||
((identifier) @type))
|
||||
(c_type
|
||||
((int_type) @type))
|
||||
|
||||
(maybe_typed_name
|
||||
name: ((identifier) @variable))
|
||||
|
||||
; Function definitions
|
||||
|
||||
(function_definition
|
||||
name: (identifier) @function)
|
||||
|
||||
(cdef_statement
|
||||
(cvar_def
|
||||
(maybe_typed_name
|
||||
name: ((identifier) @function))
|
||||
(c_function_definition)))
|
||||
|
||||
(cvar_decl
|
||||
(c_type
|
||||
([(identifier) (int_type)]))
|
||||
(c_name
|
||||
((identifier) @function))
|
||||
(c_function_definition))
|
||||
|
||||
(attribute attribute: (identifier) @variable.other.member)
|
||||
|
||||
; Literals
|
||||
|
||||
[
|
||||
(none)
|
||||
] @constant.builtin
|
||||
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
] @constant.builtin.boolean
|
||||
|
||||
(integer) @constant.numeric.integer
|
||||
(float) @constant.numeric.float
|
||||
|
||||
(comment) @comment
|
||||
(string) @string
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
(interpolation
|
||||
"{" @punctuation.special
|
||||
"}" @punctuation.special) @embedded
|
||||
|
||||
[
|
||||
"-"
|
||||
"-="
|
||||
"!="
|
||||
"*"
|
||||
"**"
|
||||
"**="
|
||||
"*="
|
||||
"/"
|
||||
"//"
|
||||
"//="
|
||||
"/="
|
||||
"&"
|
||||
"&="
|
||||
"%"
|
||||
"%="
|
||||
"^"
|
||||
"^="
|
||||
"+"
|
||||
"->"
|
||||
"+="
|
||||
"<"
|
||||
"<<"
|
||||
"<<="
|
||||
"<="
|
||||
"<>"
|
||||
"="
|
||||
":="
|
||||
"=="
|
||||
">"
|
||||
">="
|
||||
">>"
|
||||
">>="
|
||||
"|"
|
||||
"|="
|
||||
"~"
|
||||
"@="
|
||||
"and"
|
||||
"in"
|
||||
"is"
|
||||
"not"
|
||||
"or"
|
||||
"@"
|
||||
] @operator
|
||||
|
||||
[
|
||||
"as"
|
||||
"assert"
|
||||
"async"
|
||||
"await"
|
||||
"break"
|
||||
"class"
|
||||
"continue"
|
||||
"def"
|
||||
"del"
|
||||
"elif"
|
||||
"else"
|
||||
"except"
|
||||
"exec"
|
||||
"finally"
|
||||
"for"
|
||||
"from"
|
||||
"global"
|
||||
"if"
|
||||
"import"
|
||||
"lambda"
|
||||
"nonlocal"
|
||||
"pass"
|
||||
"print"
|
||||
"raise"
|
||||
"return"
|
||||
"try"
|
||||
"while"
|
||||
"with"
|
||||
"yield"
|
||||
"match"
|
||||
"case"
|
||||
|
||||
; cython-specific
|
||||
"cdef"
|
||||
"cpdef"
|
||||
"ctypedef"
|
||||
"cimport"
|
||||
"nogil"
|
||||
"gil"
|
||||
"extern"
|
||||
"inline"
|
||||
"public"
|
||||
"readonly"
|
||||
"struct"
|
||||
"union"
|
||||
"enum"
|
||||
"fused"
|
||||
"property"
|
||||
"namespace"
|
||||
"cppclass"
|
||||
"const"
|
||||
] @keyword.control
|
||||
|
||||
(dotted_name
|
||||
(identifier)* @namespace)
|
||||
|
||||
(aliased_import
|
||||
alias: (identifier) @namespace)
|
96
runtime/queries/cython/indents.scm
Normal file
96
runtime/queries/cython/indents.scm
Normal file
@@ -0,0 +1,96 @@
|
||||
[
|
||||
(list)
|
||||
(tuple)
|
||||
(dictionary)
|
||||
(set)
|
||||
(if_statement)
|
||||
(for_statement)
|
||||
(while_statement)
|
||||
(with_statement)
|
||||
(try_statement)
|
||||
(match_statement)
|
||||
(case_clause)
|
||||
(import_from_statement)
|
||||
|
||||
(parenthesized_expression)
|
||||
(generator_expression)
|
||||
(list_comprehension)
|
||||
(set_comprehension)
|
||||
(dictionary_comprehension)
|
||||
|
||||
(tuple_pattern)
|
||||
(list_pattern)
|
||||
(argument_list)
|
||||
(parameters)
|
||||
(binary_operator)
|
||||
|
||||
(function_definition)
|
||||
(cdef_statement)
|
||||
(class_definition)
|
||||
] @indent
|
||||
|
||||
; Workaround for the tree-sitter grammar creating large errors when a
|
||||
; try_statement is missing the except/finally clause
|
||||
|
||||
(ERROR
|
||||
"try"
|
||||
.
|
||||
":" @indent @extend)
|
||||
(ERROR
|
||||
.
|
||||
"def") @indent @extend
|
||||
(ERROR
|
||||
(block) @indent @extend
|
||||
(#set! "scope" "all"))
|
||||
|
||||
(ERROR
|
||||
"try"
|
||||
.
|
||||
":"
|
||||
(ERROR
|
||||
(block
|
||||
(expression_statement
|
||||
(identifier) @_except) @indent.branch))
|
||||
(#eq? @_except "except"))
|
||||
|
||||
[
|
||||
(if_statement)
|
||||
(for_statement)
|
||||
(while_statement)
|
||||
(with_statement)
|
||||
(try_statement)
|
||||
(match_statement)
|
||||
(case_clause)
|
||||
|
||||
(cdef_statement)
|
||||
(function_definition)
|
||||
(class_definition)
|
||||
] @extend
|
||||
|
||||
[
|
||||
(return_statement)
|
||||
(break_statement)
|
||||
(continue_statement)
|
||||
(raise_statement)
|
||||
(pass_statement)
|
||||
] @extend.prevent-once
|
||||
|
||||
[
|
||||
")"
|
||||
"]"
|
||||
"}"
|
||||
] @outdent
|
||||
(elif_clause
|
||||
"elif" @outdent)
|
||||
(else_clause
|
||||
"else" @outdent)
|
||||
|
||||
(parameters
|
||||
.
|
||||
(identifier) @anchor
|
||||
(#set! "scope" "tail")) @align
|
||||
|
||||
(argument_list
|
||||
.
|
||||
(_) @anchor
|
||||
(#set! "scope" "tail")) @align
|
18
runtime/queries/cython/injections.scm
Normal file
18
runtime/queries/cython/injections.scm
Normal file
@@ -0,0 +1,18 @@
|
||||
(call
|
||||
function: (attribute
|
||||
object: (identifier) @_re)
|
||||
arguments: (argument_list
|
||||
.
|
||||
(string
|
||||
(string_content) @injection.content))
|
||||
(#eq? @_re "re")
|
||||
(#set! injection.language "regex"))
|
||||
|
||||
((binary_operator
|
||||
left: (string
|
||||
(string_content) @injection.content)
|
||||
operator: "%")
|
||||
(#set! injection.language "printf"))
|
||||
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
115
runtime/queries/cython/locals.scm
Normal file
115
runtime/queries/cython/locals.scm
Normal file
@@ -0,0 +1,115 @@
|
||||
; Program structure
|
||||
(module) @local.scope
|
||||
|
||||
(class_definition
|
||||
body: (block
|
||||
(expression_statement
|
||||
(assignment
|
||||
left: (identifier) @local.definition)))) @local.scope
|
||||
|
||||
(class_definition
|
||||
body: (block
|
||||
(expression_statement
|
||||
(assignment
|
||||
left: (_
|
||||
(identifier) @local.definition))))) @local.scope
|
||||
|
||||
; Imports
|
||||
(aliased_import
|
||||
alias: (identifier) @local.definition.namespace)
|
||||
|
||||
(import_statement
|
||||
name: (dotted_name
|
||||
(identifier) @local.definition.namespace))
|
||||
|
||||
(import_from_statement
|
||||
name: (dotted_name
|
||||
(identifier) @local.definition.namespace))
|
||||
|
||||
; Function with parameters, defines parameters
|
||||
(parameters
|
||||
(identifier) @local.definition.variable.parameter)
|
||||
|
||||
(default_parameter
|
||||
(identifier) @local.definition.variable.parameter)
|
||||
|
||||
(typed_parameter
|
||||
(identifier) @local.definition.variable.parameter)
|
||||
|
||||
(typed_default_parameter
|
||||
(identifier) @local.definition.variable.parameter)
|
||||
|
||||
; *args parameter
|
||||
(parameters
|
||||
(list_splat_pattern
|
||||
(identifier) @local.definition))
|
||||
|
||||
; **kwargs parameter
|
||||
(parameters
|
||||
(dictionary_splat_pattern
|
||||
(identifier) @local.definition.variable.parameter))
|
||||
|
||||
(class_definition
|
||||
body: (block
|
||||
(function_definition
|
||||
name: (identifier) @local.definition.function)))
|
||||
|
||||
; Loops
|
||||
; not a scope!
|
||||
(for_in_loop
|
||||
left: (pattern_list
|
||||
(identifier) @local.definition.variable))
|
||||
|
||||
(for_in_loop
|
||||
left: (tuple_pattern
|
||||
(identifier) @local.definition.variable))
|
||||
|
||||
(for_in_loop
|
||||
left: (identifier) @local.definition.variable)
|
||||
|
||||
; not a scope!
|
||||
;(while_statement) @local.scope
|
||||
; for in list comprehension
|
||||
(for_in_clause
|
||||
left: (identifier) @local.definition.variable)
|
||||
|
||||
(for_in_clause
|
||||
left: (tuple_pattern
|
||||
(identifier) @local.definition.variable))
|
||||
|
||||
(for_in_clause
|
||||
left: (pattern_list
|
||||
(identifier) @local.definition.variable))
|
||||
|
||||
(dictionary_comprehension) @local.scope
|
||||
|
||||
(list_comprehension) @local.scope
|
||||
|
||||
(set_comprehension) @local.scope
|
||||
|
||||
; Assignments
|
||||
(assignment
|
||||
left: (identifier) @local.definition.variable)
|
||||
|
||||
(assignment
|
||||
left: (pattern_list
|
||||
(identifier) @local.definition.variable))
|
||||
|
||||
(assignment
|
||||
left: (tuple_pattern
|
||||
(identifier) @local.definition.variable))
|
||||
|
||||
(assignment
|
||||
left: (attribute
|
||||
(identifier)
|
||||
(identifier) @local.definition.variable))
|
||||
|
||||
; Walrus operator x := 1
|
||||
(named_expression
|
||||
(identifier) @local.definition.variable)
|
||||
|
||||
(as_pattern
|
||||
alias: (as_pattern_target) @local.definition.variable)
|
||||
|
||||
; REFERENCES
|
||||
(identifier) @local.reference
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user