Compare commits

...

163 Commits

Author SHA1 Message Date
evie
6fffaf6a7d add highlighting for nearley (#14482) 2025-10-04 15:22:28 -04:00
Lingxuan Ye
81b4a2c9d8 style: unify verbs in help message to base form (#14501) 2025-10-04 15:22:13 -04:00
alschena
e50e8a3638 feat(languages): add support for the eiffel programming language (#14470)
Co-authored-by: Alessandro Schena <alessandro.schena@constructor.org>
2025-10-04 15:12:09 -04:00
may
0a2972980f queries(scheme): properly highlight define-values (#14504) 2025-10-04 15:01:45 -04:00
Maksymilian Wrzesień
a8ac3c5010 feat: enable comment highlights in jsonnet (#14527) 2025-10-04 14:59:28 -04:00
Remo Senekowitsch
5c3e4b67d9 Add injections for mise config (#14525) 2025-10-04 14:55:40 -04:00
Will Faught
cb3c450125 Add .itermcolors to xml file types (#14531) 2025-10-04 14:55:00 -04:00
Will Faught
f327307faa Add .terminal to xml file types (#14532) 2025-10-04 14:49:43 -04:00
Remo Senekowitsch
abe82e7474 Add injections for inline scripts in jj config (#14530) 2025-10-04 14:48:54 -04:00
dependabot[bot]
bfcbef10c5 build(deps): bump the rust-dependencies group with 8 updates (#14518)
Bumps the rust-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [thiserror](https://github.com/dtolnay/thiserror) | `2.0.16` | `2.0.17` |
| [tempfile](https://github.com/Stebalien/tempfile) | `3.22.0` | `3.23.0` |
| [regex](https://github.com/rust-lang/regex) | `1.11.2` | `1.11.3` |
| [serde](https://github.com/serde-rs/serde) | `1.0.226` | `1.0.228` |
| [libc](https://github.com/rust-lang/libc) | `0.2.175` | `0.2.176` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.38` | `1.2.39` |
| [regex-automata](https://github.com/rust-lang/regex) | `0.4.10` | `0.4.11` |
| [windows-sys](https://github.com/microsoft/windows-rs) | `0.61.0` | `0.61.1` |


Updates `thiserror` from 2.0.16 to 2.0.17
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.16...2.0.17)

Updates `tempfile` from 3.22.0 to 3.23.0
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.22.0...v3.23.0)

Updates `regex` from 1.11.2 to 1.11.3
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.2...1.11.3)

Updates `serde` from 1.0.226 to 1.0.228
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.226...v1.0.228)

Updates `libc` from 0.2.175 to 0.2.176
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.176/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.175...0.2.176)

Updates `cc` from 1.2.38 to 1.2.39
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.38...cc-v1.2.39)

Updates `regex-automata` from 0.4.10 to 0.4.11
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/commits)

Updates `windows-sys` from 0.61.0 to 0.61.1
- [Release notes](https://github.com/microsoft/windows-rs/releases)
- [Commits](https://github.com/microsoft/windows-rs/commits)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tempfile
  dependency-version: 3.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: regex
  dependency-version: 1.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde
  dependency-version: 1.0.228
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.176
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.39
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: regex-automata
  dependency-version: 0.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: windows-sys
  dependency-version: 0.61.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 23:13:15 -04:00
Jesús González
109c812233 Add rainbow brackets support to Darcula themes (#14506) 2025-09-25 15:47:01 -05:00
Norman Paniagua
50c1a676f1 Update doom-one theme (#14384) 2025-09-25 22:16:30 +02:00
Jakob Jordan
ed08fbd410 Update zenburn theme (#14459) 2025-09-24 08:55:53 -05:00
Michael Davis
ce351f4b11 Avoid cloning language server names in Document::save
`language_servers` is a `HashMap<String, Arc<Client>>` so the clients
are cheap to clone but not the language server names. The names are
unused in the save future so we can avoid the unnecessary clones by
looking only at the `HashMap::values`.
2025-09-23 10:22:50 -04:00
Sofus
a5d0a0e1c2 Support textDocument/diagnostic specification (Pull diagnostics) (#11315)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-09-23 09:10:58 -05:00
Alexander Shirokov
ba506a51cb Use the default terminal size when runtime detection fails
This commit adds a fallback terminal size to use when runtime detection
fails. Since calling `size` from `Terminal` will never fail now, it also
changes the function’s signature.

If the client code wants to know whether the call succeeded or failed,
it’s still possible to call `size` via `Backend`.
2025-09-23 09:33:25 -04:00
dependabot[bot]
0ebe450991 build(deps): bump the rust-dependencies group with 6 updates (#14483)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-23 08:29:37 -05:00
RoloEdits
37fe42d05d fix(lints): clippy 1.89-1.90 (#14223) 2025-09-22 08:44:17 -05:00
Sylvain Terrien
55167c21df feat: flatten single-child dirs in file explorer (#14173) 2025-09-22 08:32:27 -05:00
Karem Abdul-Samad
8acfc55280 feat: add file explorer options (#13888) 2025-09-21 14:56:44 -04:00
Daniel Fichtinger
1426e1feb5 update source for mail grammar (#14479)
Co-authored-by: Daniel Fichtinger <daniel@ficd.sh>
2025-09-21 14:42:03 -04:00
Axlefublr
1bf9879778 update nushell parser and queries (#14377) 2025-09-21 11:16:53 -05:00
Michael Davis
23a647aee8 Support mode 2031 dark/light mode detection (#14356) 2025-09-21 10:41:04 -05:00
Michael Davis
c2b582aa45 tui: Use Crossterm on Windows (#14454) 2025-09-21 09:35:08 -05:00
Ian Hobson
0ae37dc52b Add rainbows for Koto (#14469) 2025-09-18 09:12:32 -05:00
Michael Davis
dbb472d4c4 tui: Patch cell content style instead of overwriting
This is a follow-up to #13776 to patch cell contents rather than
overwrite the contents. `ui.text.focus` might only set a modifier like
bold or italic. This should not become the only style in the cell: it
should be layered on top so that the cell has the directory and text
colors from the contents.
2025-09-16 10:51:35 -04:00
Sri Senthil Balaji J
46e3617f6b fix(queries/svelte): inherit html for injections and set svelte_raw_text to typescript (#14460) 2025-09-16 09:35:34 -04:00
dependabot[bot]
c0921202a0 build(deps): bump the rust-dependencies group with 6 updates (#14458)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 20:35:59 -04:00
Piotr Ginał
ffe513d6aa statusline: dynamic padding for unfocused mode (#14440) 2025-09-15 17:50:05 -04:00
Sri Senthil Balaji J
d015eff4aa fix(helix-view/clipboard): use serde generated name for Win32Yank (#14455) 2025-09-15 10:18:49 -05:00
CalebLarsen
a4d780483b fix: Make Table Cells set the style of their text (#13776) 2025-09-15 09:01:21 -05:00
Ash Joseph
ea2541f424 Added Ashokai theme family (#14446) 2025-09-15 08:45:35 -05:00
Sri Senthil Balaji J
69713975c6 Switch svelte treesitter and update queries (#14343) 2025-09-15 08:44:20 -05:00
Xavier R. Guérin
2dddace12e Add support for the SlightLisp language (#14236) 2025-09-15 08:35:50 -05:00
CalebLarsen
66737c6a4f Close prompts when switching windows (#13620) 2025-09-15 08:32:43 -05:00
Matt
fbf6407ab3 Extend DebugArgumentValue to support toml tables, ref #14282 (#14283) 2025-09-15 05:56:59 -03:00
Kristoffer Plagborg Bak Sørensen
1f020b1d72 feat(languages): add latexmkrc to perl file-types (#14450) 2025-09-14 19:11:17 -04:00
Antti Eskelinen
9acfa51209 Add .tmux.conf glob to bash file-types (#14449) 2025-09-14 09:11:06 -04:00
Lukáš Lalinský
2d7436fc20 Add support for matching function call arguments in Zig (#14436) 2025-09-13 20:13:21 -04:00
Igor Támara
1388166570 feat: add support for grammar wikitext the mediawiki dialect (#14432) 2025-09-13 20:02:01 -04:00
Steffen
342f2a982d Update fortran tree sitter files (#14448) 2025-09-13 19:28:20 -04:00
Steffen
5bfc7bfc07 feat: add uppercase file extensions for Fortran (#14447) 2025-09-13 19:25:32 -04:00
Kristoffer Plagborg Bak Sørensen
5609e3b3c2 feat: add .kube/kuberc to list of yaml file-types (#14444) 2025-09-13 19:13:51 -04:00
Kristoffer Plagborg Bak Sørensen
c614467be8 feat(languages): add podman *.conf files to toml file-types (#14443) 2025-09-13 19:13:10 -04:00
Michael Davis
c531b7a4fa Add debug logging of reset-cursor escape sequence
This can be used to debug the escape sequence used to reset the cursor
when quitting Helix. The cursor should reset to whatever is configured
in the terminal but this usually needs information from terminfo.
2025-09-12 10:42:44 -04:00
Valentin Cocaud
92b0a2f414 feat(helix-tui): add configuration to manually enable/disable KKP (#14398) 2025-09-12 09:32:22 -05:00
Michael Davis
378b27cad9 Bump termina to v0.1.1
This fixes the build for Illumos as termina now skips compiling
macOS-specific polling functions when not building for macOS.
2025-09-12 10:12:55 -04:00
fl0
209558645a Add new theme: Nvim-dark (#14403) 2025-09-10 09:09:30 -05:00
dependabot[bot]
987b04bd26 build(deps): bump the rust-dependencies group with 6 updates (#14410)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-10 09:08:49 -05:00
Grégoire Locqueville
322bb1c189 Fix Lean LSP module imports (#14381)
Co-authored-by: Grégoire Locqueville <gregoire.locqueville@cnrs.fr>
2025-09-10 09:07:59 -05:00
quantonganh
34e0f7e82f Add a simple language server for HDL (nand2tetris) (#14415) 2025-09-10 08:45:50 -05:00
Kristoffer Plagborg Bak Sørensen
97293c9f36 feat(languages): add gitlab-ci language specialized from yaml (#14396) 2025-09-10 08:41:04 -05:00
Kristoffer Plagborg Bak Sørensen
71038266e8 feat(highlights): add support for syntax features in hurl version 6.0.0 (#12725) 2025-09-10 08:39:34 -05:00
Charles Hall
05a99c2cca fix white/bright white base16 color mixup (#14409) 2025-09-10 08:38:45 -05:00
Axlefublr
14030d0b63 kdl queries: basic tag.scm and niri injections (#14401) 2025-09-10 08:28:31 -05:00
Abderrahmane TAHRI JOUTI
ff376b0d1a move cyan_light to jetbrains_cyan_light (#14412) 2025-09-10 08:27:25 -05:00
Kristoffer Plagborg Bak Sørensen
d25726f573 feat: add fods and fodt extensions to xml filetype (#14416) 2025-09-10 08:26:52 -05:00
may
8f2af68b30 scheme: add block comment token, textobjects.scm (#14408)
* lang(scheme): add block comment token

* queries(scheme): add comment text objects

* chore: run cargo xtask docgen
2025-09-10 08:26:27 -05:00
Kristoffer Plagborg Bak Sørensen
fe8e21a07f feat(bash): add init scripts for xinit and startx to bash file-types (#14397) 2025-09-10 08:22:10 -05:00
may
d0218f7e78 queries(scheme): mark the variables of do as @variable 2025-09-05 10:36:13 -04:00
may
e2333f60ae queries(scheme): convert a #match? to a #any-of? 2025-09-05 10:36:13 -04:00
may
70187c430c queries(scheme): more consistently indent with two spaces
... and also remove a trailing space
2025-09-05 10:36:13 -04:00
kpbaks
8058fefef0 feat: add docker-language-server
Official language server for Dockerfiles, Compose files, and Bake files.
https://github.com/docker/docker-language-server/
2025-09-05 10:35:26 -04:00
kpbaks
0928e5ea1c feat: add docker-bake language
Docker bake files are primarily written in `hcl`, but you can also write
it in `json` and `yaml`. The official material and documentation for the
feature uses `hcl`, which is why this commit makes the choice of
associating the `docker-bake` language with `docker-bake.hcl` files.
The primary motivation of this specialization is to inject the
`dockerfile` language into the `dockerfile-inline` attribute.
2025-09-05 10:35:26 -04:00
Ariel Chenet
b391185716 Add Expert LSP support for Elixir and Heex languages (#14395)
Co-authored-by: Ariel Chenet <arielchenet@pm.me>
2025-09-05 09:31:37 -05:00
may
f59dc9e48f mark arguments of case-lambda as variable, mark case-lambda as keyword (#14386) 2025-09-04 09:08:20 -05:00
Kristoffer Plagborg Bak Sørensen
d63c2d2fea feat: add runhaskell and stack as haskell shebangs (#14385) 2025-09-04 09:06:27 -05:00
Kristoffer Plagborg Bak Sørensen
0a4207be32 feat: detect .wslconfig as ini filetype (#14383) 2025-09-04 09:06:01 -05:00
Kristoffer Plagborg Bak Sørensen
3adc021c06 feat(ruby): detect .irbrc file as ruby (#14382) 2025-09-04 09:05:34 -05:00
Axlefublr
d1750a7502 fish injection query for nu, on nu -c (#14376) 2025-09-03 10:17:07 -05:00
dependabot[bot]
c5f0a4bc22 build(deps): bump the rust-dependencies group with 2 updates (#14373)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 08:16:33 -05:00
Kristoffer Plagborg Bak Sørensen
4967229e85 feat(ruby): inject bash into builtin ways of running system commands (#14372) 2025-09-02 17:51:42 -05:00
Nora Breitmoser-Widdecke
68f11f9324 language support for strictdoc (#14314) 2025-09-02 08:56:15 -05:00
Kristoffer Plagborg Bak Sørensen
af74a61ad4 feat(languages): detect more vim file-types (#14365) 2025-09-02 08:39:21 -05:00
Andrey R.
cfb5158cd1 fix(theme): correct goto_word and comments background in flatwhite.toml (#14348) 2025-09-01 10:23:26 -04:00
Dang
e3fafb6bad use constant.builtin.boolean scope for ecma boolean (#14357) 2025-08-31 17:24:50 -04:00
Michael Davis
6e9939a2d1 Improve "written" message for small (<1024B) files
Using a float when the file size is in bytes is confusing. Instead we
should show "123B" for <1024B and use floats only for KiB and above.
2025-08-31 12:44:55 -04:00
Michael Davis
b08aba8e8e Remove Backend::get_cursor
It is unused and cannot be used on some terminal hosts like `conhost`
that do not respond to VT queries. This doesn't have any affect on
behavior - I'm removing it so that we don't rely on it in the future.
2025-08-31 12:25:01 -04:00
Michael Davis
83abbe56df Replace Crossterm with Termina
This change switches out the terminal manipulation library to one I've
been working on: Termina. It's somewhat similar to Crossterm API-wise
but is a bit lower-level. It's also influenced a lot by TermWiz - the
terminal manipulation library in WezTerm which we've considered
switching to a few times.

Termina is more verbose than Crossterm as it has a lower level interface
that exposes escape sequences and pushes handling to the application.
API-wise the important piece is that the equivalents of Crossterm's
`poll_internal` / `read_internal` are exposed. This is used for reading
the cursor position in both Crossterm and Termina, for example, but also
now can be used to detect features like the Kitty keyboard protocol and
synchronized output sequences simultaneously.
2025-08-31 12:25:01 -04:00
Michael Davis
9cc912a63e tui: Refactor Config type handling in backends
The `Config` can be passed when creating the backend (for example
`CrosstermBackend::new`) and is already updated in the
`Backend::reconfigure` callback. Recreating the tui `Config` during
`claim` and `restore` is unnecessary and causes a clone of the editor's
Config which is a fairly large type. This change drops the `Config`
parameter from those callbacks and updates the callers. Instead it is
passed to `CrosstermBackend` which then owns it.

I've also moved the override from the `editor.undercurl` key onto the
tui `Config` type - I believe it was just an oversight that this was not
done originally. And I've updated the `From<EditorConfig> for Config`
to take a reference to the editor's `Config` to avoid the unnecessary
clone during `CrosstermBackend::new` and `Backend::reconfigure`.
2025-08-31 10:50:02 -04:00
Nik Revenco
fe1393cec8 queries(rust): Highlight type infer in more places (#14351) 2025-08-31 09:01:45 -04:00
Nik Revenco
392e444ff9 queries(rust): properly highlight Dioxus' rsx! macro (#14354) 2025-08-31 09:01:05 -04:00
Nik Revenco
0ea5d87985 queries(rust): Highlight tacit functions when we are 100% sure they are (#14350) 2025-08-31 08:58:46 -04:00
Kristoffer Plagborg Bak Sørensen
6b73c3c550 feat: add shellcheckrc language (#14202) 2025-08-31 08:53:42 -04:00
Michael Davis
b309d72688 Fix bugs in Editor::focus (#14262) 2025-08-31 08:52:40 -04:00
Kalpaj Chaudhari
d546a799e5 Add tag queries for java, kotlin, protobuf and bash (#14349) 2025-08-31 08:47:58 -04:00
Arthur
7c37e8acea support cython (#14128) 2025-08-31 08:35:33 -04:00
Michael Davis
d4c91daa5e queries: Inject regex into regular Rust string literals
Previously regex was injected only into raw string literals.

    Regex::new(r"[a-z]") // was highlighted
    Regex::new("[a-z]") // is now also highlighted
2025-08-31 08:23:08 -04:00
Robin Kraft
dc7c2acc08 add a link to release packe on debian installation docs (#14346) 2025-08-31 07:43:57 -04:00
Kristoffer Plagborg Bak Sørensen
99cea8c284 feat(bash): inject bash into builtins expecting bash code as input (#14268) 2025-08-30 13:46:08 -04:00
Tijs-B
077c901be9 fix(htmldjango): add roots to htmldjango language (#14305) 2025-08-30 13:42:23 -04:00
Bryce Berger
a5bf7c0d5e queries: update highlights/injections for jjconfig (#14308) 2025-08-30 13:38:26 -04:00
Michael Davis
8ab20720da Update tree-sitter-gleam
The latest changes in the grammar add support for `echo ... as`
expressions.
2025-08-30 13:30:33 -04:00
dependabot[bot]
feeaec097a build(deps): bump the rust-dependencies group with 8 updates (#14317)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-30 13:27:26 -04:00
Georgios Tsoulis
4f5eaa4186 docs: fix possible error when building the optimized version from source (#14330) 2025-08-30 13:25:53 -04:00
Jonas Köhnen
7a5b618fe5 Fix shell pipe command lines not using expansions (#14191) 2025-08-30 13:16:22 -04:00
RoloEdits
77ff51caa4 feat(grammar): update zig tree-sitter (#14336) 2025-08-29 10:16:45 -04:00
Erik
7e4e556f84 feat(languages): Add buck2 PACKAGE files as starlark file type (#14324) 2025-08-27 09:46:17 -05:00
zoey
96c60198ec update: nyxvamp-theme (#14319) 2025-08-26 17:20:07 -04:00
Ross Smyth
3dadd82c89 nix: Remove debug ls -al (#14320) 2025-08-26 17:19:41 -04:00
Jaakko Paju
5a8fb732f2 add AWS config and kube config file support (#14316) 2025-08-26 10:44:01 -05:00
Kristoffer Plagborg Bak Sørensen
8671882ee2 feat(languages): specialize toml file-type for git-cliff config file (#14301) 2025-08-25 08:50:38 -05:00
Sylvain Terrien
1d3e65fdbc theme: add color-modes support to molokai (#14250) 2025-08-25 08:49:46 -05:00
Uładzisłaŭ Hińko
f81b59fc15 Add NvChad's solarized dark theme (#14280) 2025-08-25 08:49:09 -05:00
Vítor Galvão
cc8e890906 languages.toml: Add GPX extension to XML (#14300)
Co-authored-by: Poliorcetics <poliorcetics@users.noreply.github.com>
2025-08-25 08:38:04 -05:00
Michael Davis
aa14cd38fc queries: Fix precedence of Rust for-loop keyword highlight
The `for` literal node is marked as a `keyword` since it can also show
up outside of for loops, like in `for<'a> fn(&'a T)`. The for loop
highlight which tags `keyword.control.repeat` needs to move lower in the
file than the `keyword` one to take precedence.
2025-08-25 09:31:12 -04:00
Michael Davis
22a3b10dd8 themes: Fix other modus vivendi licenses
This is the same as 95c378a764 but for the other variants.
2025-08-22 09:35:34 -04:00
ishanray
535e6ee77b Make buffer picker default to last file (#14176) 2025-08-22 08:30:31 -05:00
Aidan Gauland
4b40b45527 Add Flatbuffers language and grammar (#14275) 2025-08-22 08:05:54 -05:00
Michael Davis
95c378a764 themes: Fix modus vivendi license
The GFDL-1.3-only text must've been copied from the modus vivendi website
<https://protesilaos.com/emacs/modus-themes#h:b14c3fcb-13dd-4144-9d92-2c58b3ed16d3>.
The actual license is a GPL-3.0-or-later:
<https://github.com/protesilaos/modus-themes/blob/main/COPYING>.
2025-08-22 08:59:54 -04:00
Kristoffer Plagborg Bak Sørensen
74bb02ffe7 feat(languages): specilize toml file-type for cross-rs config file (#14274) 2025-08-19 18:19:44 -04:00
Matej Almáši
b81ee02db4 fix adwaita-light ui.virtual color (#14238) 2025-08-19 18:09:34 -04:00
dependabot[bot]
9ec07cf1f6 build(deps): bump the rust-dependencies group with 3 updates (#14270)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 10:57:21 -05:00
evie
9f34f8b5ff don't remove highlights for selections in modus_* themes (#14232) 2025-08-18 08:25:03 -05:00
Kristoffer Plagborg Bak Sørensen
3fb1443162 feat(python): add regex injection to functions in the std re module (#14254) 2025-08-18 08:24:38 -05:00
Kristoffer Plagborg Bak Sørensen
207c0e3899 feat: inject jq into jq "..." string literals in common shells (#14253) 2025-08-18 08:23:53 -05:00
Erasin Wang
f9f5fe6b12 Add rainbows for Lua (#14256) 2025-08-18 08:22:50 -05:00
Erasin Wang
e5e7fe43ce Add queries for Wgsl (#14257) 2025-08-18 08:22:15 -05:00
Erasin Wang
da4ede9535 Add rainbows for ron (#14258) 2025-08-18 08:21:48 -05:00
Ricardo Fernández Serrata
6f26a257d5 Recognize .git/info/exclude as .gitignore (#14264) 2025-08-18 08:20:44 -05:00
Michael Davis
b1eb9c09f4 queries: Fix highlight of ':' in TypeScript ternary expressions
The ':' node is set to punctuation.delimiter in typescript and this
comes after the ternary_expression pattern in the ecma highlights. So we
need to copy that pattern after the punctuation.delimiter pattern.
2025-08-18 09:16:41 -04:00
Gabor Pihaj
b6ccbddc77 fix: add runtimeDir as propagated build input (#14247) 2025-08-16 10:51:17 -04:00
Gareth Widlansky
a4a2b50a50 Add support for KConfig (#14205) 2025-08-15 09:52:58 -05:00
Francesco Urbani
e5d1f6c517 feat: add a tree-sitter grammar for systemverilog (#14174) 2025-08-15 09:06:10 -05:00
Erik
050e1d9b47 feat: add a tree-sitter grammar for Doxyfile (#14235) 2025-08-15 09:01:42 -05:00
Nick Pupko
2b9cc20d23 Improve error messages for goto LSP commands (#14171) 2025-08-15 08:33:57 -05:00
Artyom Vasich
9e3b510dc7 fix(go): enable markdown injections in function doc comments (#14230) 2025-08-15 08:30:42 -05:00
Gareth Widlansky
6b93c77033 add rainbows.scm for tsq (#14216) 2025-08-15 08:30:23 -05:00
Erasin Wang
fdaec3f972 Add rainbows.scm for PHP (#14228) 2025-08-15 08:27:15 -05:00
rusty-snake
8a898c88bc book: Add Helix mode in Lite XL and Lapce (#14210) 2025-08-15 08:26:46 -05:00
Fabian Mettler
e0544d01f1 Remove the Ubuntu PPA installation method from the docs (#14237) 2025-08-15 08:25:00 -05:00
dependabot[bot]
001efa801e build(deps): bump actions/download-artifact from 4 to 5 (#14219)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-08-12 14:12:22 -05:00
dependabot[bot]
00dbca93c4 build(deps): bump the rust-dependencies group with 5 updates (#14222)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 07:53:31 -05:00
dependabot[bot]
6726c1f41c build(deps): bump actions/checkout from 4 to 5 (#14220)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 07:52:43 -05:00
evie
7747d3b93e adjust cyan in modus_vivendi rainbow (and yellow in _tritanopia) (#14218) 2025-08-12 07:51:46 -05:00
Peter Rice
4d0466d30c Add rainbows.scm queries for Janet (#14203) 2025-08-10 16:52:32 -04:00
Mike Boutin
ed2807ae07 Improve onedarker theme by adding highlight scopes (#14194) 2025-08-10 09:41:04 -04:00
Kristoffer Plagborg Bak Sørensen
327f3852f4 feat: add pip-requirements language (#14161) 2025-08-09 20:17:42 -04:00
Friedrich Stoltzfus
155fde5178 feat: add tree-sitter rainbow query for Swift (#14165) 2025-08-09 20:16:05 -04:00
Nik Revenco
f5a399c7f9 fix: 2 bugs in the Alt-) and Alt-( commands (#13985) 2025-08-09 20:13:37 -04:00
Trevor Gross
cb7188d5cc jj: Set the indent to four spaces for descriptions (#14199) 2025-08-09 20:07:03 -04:00
Poliorcetics
a44695e4e8 just: bump grammar support to fix alias name bug and add tag queries (#14169) 2025-08-09 19:57:16 -04:00
Jonas Köhnen
ef3a49d03c Support features of :open in :vsplit & :hsplit (#13461) 2025-08-09 19:50:49 -04:00
Kristoffer Plagborg Bak Sørensen
56fa9bf7c1 feat: inject css into standard browser API methods that expect css selector as argument (#14181) 2025-08-09 19:42:27 -04:00
Álan Crístoffer
b5a9c34e14 languages(matlab): bump grammar version (#14195) 2025-08-09 19:39:39 -04:00
infastin
e8e36a6a8e feat: update go grammar and queries (#14167) 2025-08-09 18:45:23 -04:00
infastin
18572973e6 feat: update css grammar and highlights.scm (#14147)
* feat: update css grammar and highlights.scm

* chore: replace any-of with match

* fix: replace match with eq

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-08-08 09:34:09 +09:00
Isaac Corbrey
4a25f63169 languages: Add doc comment token for C# (#14164) 2025-08-08 09:33:27 +09:00
dependabot[bot]
0345400c41 build(deps): bump the rust-dependencies group with 4 updates (#14163)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 08:26:46 -05:00
quantonganh
43990ed0c8 feat: add a tree-sitter grammar for hdl (#14140) 2025-08-03 17:38:22 -04:00
Kristoffer Plagborg Bak Sørensen
178c55708a feat: add robots.txt language (#14089) 2025-08-03 17:19:09 -04:00
Kristoffer Plagborg Bak Sørensen
6c0d598183 Update go-format-string grammar and queries (#14106) 2025-08-03 17:18:09 -04:00
evie
5b5f6daab3 fix parentheses in popup description for html tag matching (#14157) 2025-08-03 17:17:44 -04:00
zoey
601c904e50 chore: visual fixes to nyxvamp radiance base variant theme (#14130) 2025-08-03 17:11:51 -04:00
chtenb
93cf3b1baf Update koka grammar and queries to support koka v3 (#14135) 2025-08-03 17:04:49 -04:00
Kristoffer Plagborg Bak Sørensen
fdfc6df122 Add --no-format flag to :update command (#14136) 2025-08-03 16:56:33 -04:00
Rich
d2595930fa docs: add encodings for < and > literals in remapping (#14144)
Currently docs do not include the remappings for literal < or >, which can be important characters for macros relating to markup languages.
2025-08-02 11:27:27 -04:00
evie
758f80a4fc give modus-vivendi rainbows (#14131) 2025-07-31 09:01:53 -05:00
Trevor Gross
e58b08d22a docker: Update the default indentation to match other editors (#14108) 2025-07-30 08:50:20 -05:00
Alex Pearwin
62f3cd3f5a docs: Correct expansion argument formatting (#14123) 2025-07-30 08:47:50 -05:00
Mo Bitar
39cccc23e5 book: Update inline diagnostics defaults (#14124) 2025-07-30 08:45:14 -05:00
dependabot[bot]
f0be627dcb build(deps): bump the rust-dependencies group with 7 updates (#14109)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 09:40:56 -05:00
wcampbell
1bdd8ae784 Update rst to v0.2.0 (#14111) 2025-07-29 09:40:00 -05:00
242 changed files with 11583 additions and 2267 deletions

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,8 @@ in
allowBuiltinFetchGit = true;
};
propagatedBuildInputs = [ runtimeDir ];
nativeBuildInputs = [
installShellFiles
git

View File

@@ -87,8 +87,6 @@
$CC -c src/parser.c -o parser.o $FLAGS
$CXX -shared -o $NAME.so *.o
ls -al
runHook postBuild
'';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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))]
{

View File

@@ -164,6 +164,13 @@ pub fn dap_start_impl(
arr.iter().map(|v| v.replace(&pattern, &param)).collect(),
),
DebugArgumentValue::Boolean(_) => value,
DebugArgumentValue::Table(map) => DebugArgumentValue::Table(
map.into_iter()
.map(|(mk, mv)| {
(mk.replace(&pattern, &param), mv.replace(&pattern, &param))
})
.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());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(_) => {}
};
}

View 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(())
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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(())
}

View 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(())
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,133 +1,3 @@
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
//! terminal users interfaces and dashboards.
//!
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
//!
//! # 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,7 +102,7 @@ struct EventAccumulator {
render_lock: Option<RenderLock>,
}
impl<'a> EventAccumulator {
impl EventAccumulator {
fn new() -> EventAccumulator {
EventAccumulator {
diff_base: None,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
pub mod macros;
pub mod annotations;
pub mod base64;
pub mod clipboard;
pub mod document;
pub mod editor;

View File

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

View File

@@ -441,7 +441,7 @@ impl Tree {
}
}
pub fn traverse(&self) -> Traverse {
pub fn traverse(&self) -> Traverse<'_> {
Traverse::new(self)
}

View File

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

View File

@@ -79,6 +79,7 @@
(property_signature "?" @punctuation.special)
(conditional_type ["?" ":"] @operator)
(ternary_expression ["?" ":"] @operator)
; Keywords
; --------

View File

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

View File

@@ -0,0 +1 @@
(function_definition name: (word) @definition.function)

View File

@@ -0,0 +1 @@
; inherits: toml

View 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"))

View File

@@ -0,0 +1 @@
; inherits: toml

View File

@@ -0,0 +1 @@
; inherits: toml

View File

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

View 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

View 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)

View 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

View 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"))

View 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