Compare commits

...

93 Commits

Author SHA1 Message Date
Michael Davis c4415119fd
Add a hidden column for the global search line contents
We could expand on this in the future to have different preview modes
that you can toggle between with C-t. Currently that binding just hides
the preview but it could switch between different preview modes and in
one mode hide the path and just show the line contents.
2 months ago
Michael Davis b1d90d1931
global_search: Suggest latest '/' register value 2 months ago
Michael Davis d5e4cf8009
Refactor global_search as a dynamic Picker 2 months ago
Michael Davis cfa8dbb5a4
Consolidate DynamicPicker into Picker
DynamicPicker is a thin wrapper over Picker that holds some additional
state, similar to the old FilePicker type. Like with FilePicker, we want
to fold the two types together, having Picker optionally hold that
extra state.

The DynamicPicker is a little more complicated than FilePicker was
though - it holds a query callback and current query string in state and
provides some debounce for queries using the IdleTimeout event.
We can move all of that state and debounce logic into an AsyncHook
implementation, introduced here as `DynamicQueryHandler`. The hook
receives updates to the primary query and debounces those events so
that once a query has been idle for a short time (275ms) we re-run
the query.

A standard Picker created through `new` for example can be promoted into
a Dynamic picker by chaining the new `with_dynamic_query` function, very
similar to FilePicker's replacement `with_preview`.

The workspace symbol picker has been migrated to the new way of writing
dynamic pickers as an example. The child commit will promote global
search into a dynamic Picker as well.
2 months ago
Michael Davis 72c37af9b9
Bump nucleo to v0.4.1
We will use this in the child commit to improve the picker's running
indicator. Nucleo 0.4.0 includes an `active_injectors` member that we
can use to detect if anything can push to the picker. When that count
drops to zero we can remove the running indicator.

Nucleo 0.4.1 contains a fix for crashes with interactive global search
on a large directory.
2 months ago
Michael Davis 1ebb515cf3
Implement Error for InjectorShutdown 2 months ago
Michael Davis 3a164ebf60
Replace picker shutdown bool with version number
This works nicely for dynamic pickers: we stop any running jobs like
global search that are pushing to the injector by incrementing the
version number when we start a new request. The boolean only allowed
us to shut the picker down once, but with a usize a picker can have
multiple "sessions" / "life-cycles" where it receives new options
from an injector.
2 months ago
Michael Davis b814b3e347
Add column configurations for existing pickers
This removes the menu::Item implementations for picker item types and
adds `Vec<Column<T, D>>` configurations.
2 months ago
Michael Davis 8a07f357e5
Add a special query syntax for Pickers to select columns
Now that the picker is defined as a table, we need a way to provide
input for each field in the picker. We introduce a small query syntax
that supports multiple columns without being too verbose. Fields are
specified as `%field:pattern`. The default column for a picker doesn't
need the `%field:` prefix. The field name may be selected by a prefix
of the field, for example `%p:foo.rs` rather than `%path:foo.rs`.

Co-authored-by: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
2 months ago
Michael Davis 4908438de0
Refactor Picker in terms of columns
`menu::Item` is replaced with column configurations for each picker
which control how a column is displayed and whether it is passed to
nucleo for filtering. (This is used for dynamic pickers so that we can
filter those items with the dynamic picker callback rather than nucleo.)

The picker has a new lucene-like syntax that can be used to filter the
picker only on certain criteria. If a filter is not specified, the text
in the prompt applies to the picker's configured "primary" column.

Adding column configurations for each picker is left for the child
commit.
2 months ago
Michael Davis cac0930674
Use an AsyncHook for picker preview highlighting
The picker previously used the IdleTimeout event as a trigger for
syntax-highlighting the currently selected document in the preview pane.
This is a bit ad-hoc now that the event system has landed and we can
refactor towards an AsyncHook (like those used for LSP completion and
signature-help). This should resolve some odd scenarios where the
preview did not highlight because of a race between the idle timeout
and items appearing in the picker.
2 months ago
Armando Pérez Marqués 47995bfb0c
Add jump label color ("rosewater") to catppuccin themes (#9983) 2 months ago
Tobias Brunner 7bce9530d3
Add jump label color to rose-pine themes (#9981) 2 months ago
Florent Nuttens da2dec174a feat: add jump label color to onedark theme 2 months ago
Florent Nuttens 628dcd5c56 feat: add jump label color to dark plus theme 2 months ago
Florent Nuttens 2178adfe93 feat: add jump label color to catppuccin themes 2 months ago
ves 54fab657be
Add jump label color to horizon-dark theme (#9984) 2 months ago
Yomain 8f65bfe089
feat: add jump label color to dracula themes (#9973) 2 months ago
Pascal Kuthe b46064b8c4 Add an Amp-like jump command
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2 months ago
Pascal Kuthe 3001f22b31 add reverse rope grapheme iterator 2 months ago
Pascal Kuthe 66b9ff1d2a dismiss pending keys properly for mouse/paste 2 months ago
Pascal Kuthe 69e07ab61e use slices instead of Rc for virtual text 2 months ago
Michael Davis 68b21578ac Reimplement tree motions in terms of syntax::TreeCursor
This uses the new TreeCursor type from the parent commit to reimplement
the tree-sitter motions (`A-p/o/i/n`). Other tree-sitter related
features like textobjects are not touched with this change and will
need a different, unrelated approach to solve.
2 months ago
Michael Davis b1222f0664 Add a TreeCursor type that travels over injection layers
This uses the layer parentage information from the parent commit to
traverse the layers. It's a similar API to `tree_sitter:TreeCursor`
but internally it does not use a `tree_sitter::TreeCursor` currently
because that interface is behaving very unexpectedly. Using the
`next_sibling`/`prev_sibling`/`parent` API on `tree_sitter::Node`
reflects the previous code's behavior so this should result in no
surprising changes.
2 months ago
Michael Davis 6dd46bfe1c syntax: Track parent LanguageLayer IDs
This commit adds a `parent` field to the `LanguageLayer`. This
information is conveniently already available when we parse injections.
This will be used in the child commit to create a type that can
traverse injection layers using this information.
2 months ago
Jaakko Paju d5c2973cd1
Fix repeat last motion for goto next/prev diagnostic (#9966) 2 months ago
Carter Watson be307a4204
fix: undefined bufferline colors (#9960) 2 months ago
David Else c9e34c556b
Add rclone.conf as a glob to make it behave as an ini file (#9959) 2 months ago
Michael Davis f5d95de227 C++: Improve parameter highlighting
This adds parameter highlighting for reference parameters and defaulted
parameters. For example:

```cpp
auto strip_prefix_only(std::string& s,
                       Hidden_Homonym skip_hidden_homonym = {}) const
    -> Affixing_Result<Prefix>;
```

Previously both parameters were only highlighted as variables.
2 months ago
Michael Davis c099dde2a7 Rust: Highlight extern crate aliases
For example `extern crate alloc as myalloc;`
2 months ago
Michael Davis 9ceeea5a83 Update tree-sitter-gleam and highlights
This contains a few syntax fixes. The highlights have been updated as
well for reserved identifiers and escape sequences
2 months ago
Michael Davis fdcd461e65 Update tree-sitter-erlang and highlights
A few changes:

* 0-arity type specs like the following previously would not have the
  expected 'variable.parameter' highlighting for the return type:

    -spec foo() -> Value when Value :: term().

* Highlight module, type and function docs as documentation comments
  and inject markdown into them.

* Replace `#match?` predicates with `#any-of?` where possible.

* Remove custom auto-pairs. Now that Erlang uses markdown for
  documentation, the asciidoc-style backtick-singlequote pair is no
  longer useful.
2 months ago
Michael Davis 459eb9a4c1 Recognize 'mmd' as mermaid 2 months ago
Michael Davis 961025433d Update tree-sitter-git-commit
This commit has partial support for escapes within strings.
2 months ago
JR 51739f1290
Create a tutor entry for using splits (#9417)
* WIP

* WIP

* WIP

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* WIP

* WIP

* WIP

* Fix typos

* Fix typos

* Minor updates

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Remove mention of arrows in split tutorial

* Do not mention starting in normal mode

* Fix right drift of titles

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Reflow paragraphs

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

---------

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2 months ago
Jordan Reger 5ba36fe9b3
Update rasmus.toml (#9939)
* Update rasmus.toml

Remove "ui.virtual" setting as it selects seemingly random characters to highlight.

* Add ui.virtual.ruler
2 months ago
Michael Davis 7f5fd63835
Evenly space statusline areas when there isn't space to align middle (#9950)
The refactor in bcf7b263 introduced a possible subtraction with overflow
when the statusline is layed out so that the left or right sides are
larger than the padding it would take to align the center area to the
middle.

When the left or right areas are too large, we can evenly space the
elements rather than trying to align the center area to the middle.
This prevents possible underflows and makes sense visually - it's
still easy to tell the areas apart at a glance.
2 months ago
David Else 52a0734120
Add new theme keys for LSP diagnostic tags for dark_plus (#9949) 2 months ago
Hendrik Norkowski b8e79c0ef5
fix(languages): specify correct comment-token for PKGBUILD files (#9943) 2 months ago
Szabin bcf7b26393
Refactor statusline elements to build `Spans` (#9122)
* Refactor statusline elements to return Spans

* Split render fn to build Spans and blit to Surface
2 months ago
Arthur Deierlein 427dd2f383
Add support for ember.js templates (#9902)
* feat: add support for ember .hbs (glimmer) templates

* adjust highlights to helix

* highlight this correctly in block statements

* correctly highlight attributes

* correctly highlight hash_pair

* add newline to highlights.scm

* refactor: use #any-of and #eq instead of #match

* chore: add newline to languages.toml
2 months ago
Kirawi d9de809a57
add register completion (#9936) 2 months ago
Tobias Hunger 1d1806c85a
Ignore more version control systems (#9935)
Ignore `.pijul` and `.jj` as well as `.git`. This makes hx so much more
usable with VCSes other than git!
2 months ago
Arthur Deierlein 4b4947639a
feat: add suport for helm charts (#9900) 2 months ago
Cyrill Schenkel 5b8bfc5476
Prevent `shell_keep_pipe` from stopping on nonzero exit status code (#9817)
The `shell_impl` and `shell_impl_async` functions no longer return
`success` because it was always `true`. If the command didn't succeed
both functions would return an `Err`.

This was also the reason, why `shell_keep_pipe` didn't work. It relied
upon the value of `success` and aborted in case of an `Err`.
It now removes any selection for which `shell_impl` returns `Err`.

If the command always fails, the selections are preserved and an error
message is displayed in the status bar.
2 months ago
Damian Zaręba 485c5cf0b8
Initial Ada language support (after stale) (#9908)
* Adding initial support for ada language, based off #7790 PR from tomekw

* More translation to helix-specific tree-sitter scm labels, add ada gpr switch to ada LSP

* Generate ada in lang-support.md using cargo xtask docgen

* Update tree-sitter definitions according to comments

* Remove .gpr glob from languages.toml

* Fix unit in languages.toml for ada, update locals.scm to helix needs
2 months ago
Arthur Deierlein 0b6dea6dc2
Enhance support for PKGBUILDS (#9909)
* enhance support for PKGBUILDS

* run cargo xtask docgen
2 months ago
Luis Useche 6a22d7d1ca
Do not stop reloading docs on error (#9870)
In the `reload-all` command, we should not stop reloading the documents
if one error is found. Instead, we should report the error and continue
trying to reload the current open documents. This is useful in cases
where a backing file does not exist temporarily (e.g. when editing a git
patch in the outstanding chain that doesn't have a file just yet).

This change also remove the error messages in the cases where the
backing is `None`, like in new docs or `tutor`.
2 months ago
Khang Nguyen Duy 4d2282cbcc
Respect undercurl config even with no terminfo (#9897)
I have just found out that my recent Windows Terminal version
supported rendering undercurl (see
https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-20-release
). However, looking at the source, terminfo is required for helix to
emit the undercurl control code, which isn't available on Windows AFAIK.

This commit make helix respects the `editor.undercurl` option when
there is no terminfo.

Tested on Windows Terminal Preview 1.20

Signed-off-by: Khang Nguyen <khang.nguyenduycse@hcmut.edu.vn>
2 months ago
dependabot[bot] 3d4889ce9a
build(deps): bump the rust-dependencies group with 3 updates (#9929)
Bumps the rust-dependencies group with 3 updates: [bitflags](https://github.com/bitflags/bitflags), [toml](https://github.com/toml-rs/toml) and [lsp-types](https://github.com/gluon-lang/lsp-types).


Updates `bitflags` from 2.4.2 to 2.5.0
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.4.2...2.5.0)

Updates `toml` from 0.8.10 to 0.8.12
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.10...toml-v0.8.12)

Updates `lsp-types` from 0.95.0 to 0.95.1
- [Changelog](https://github.com/gluon-lang/lsp-types/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gluon-lang/lsp-types/compare/v0.95.0...v0.95.1)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: lsp-types
  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>
2 months ago
mo8it 0f5430ab9e Optimize get_truncated_path 2 months ago
mo8it e91ec8e880 Optimize getting a relative path 2 months ago
mo8it 6ed93b6e49 Optimize fold_home_dir 2 months ago
mo8it 6607938bf8 Call as_ref only once 2 months ago
Blaž Hrastnik 13533cb99c ci: Group minor rust dependency dependabot updates
https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups
2 months ago
dependabot[bot] df8352ec05
build(deps): bump tokio-stream from 0.1.14 to 0.1.15 (#9926)
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.14 to 0.1.15.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.14...tokio-stream-0.1.15)

---
updated-dependencies:
- dependency-name: tokio-stream
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
dependabot[bot] ef797acf0d
build(deps): bump anyhow from 1.0.80 to 1.0.81 (#9925)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.80 to 1.0.81.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.80...1.0.81)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
dependabot[bot] 7ef583bea7
build(deps): bump gix from 0.58.0 to 0.61.0 (#9924)
Bumps [gix](https://github.com/Byron/gitoxide) from 0.58.0 to 0.61.0.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Byron/gitoxide/compare/gix-v0.58.0...gix-v0.61.0)

---
updated-dependencies:
- dependency-name: gix
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
dependabot[bot] 326c078356
build(deps): bump thiserror from 1.0.57 to 1.0.58 (#9923)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.57 to 1.0.58.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.57...1.0.58)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
dependabot[bot] 8dc50bded9
build(deps): bump clipboard-win from 5.2.0 to 5.3.0 (#9922)
Bumps [clipboard-win](https://github.com/DoumanAsh/clipboard-win) from 5.2.0 to 5.3.0.
- [Commits](https://github.com/DoumanAsh/clipboard-win/commits)

---
updated-dependencies:
- dependency-name: clipboard-win
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
Jaakko Paju 58022586a0
Add yaml LSP for docker compose (#9916)
* Add yaml LSP for docker compose

* Change docs
2 months ago
Matthew Toohey 2e4653ea31
add koka language support (#8727)
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
2 months ago
Phil 94d210c9bf
Add initial support for SuperCollider (#9329) 2 months ago
Michael Davis 4ffe993533 Fix malformed predicates in highlighting queries 2 months ago
Michael Davis 38af99f05f Bump tree-sitter to 0.22.2 2 months ago
Joey Hain 476e6baf8f
Add textobject queries for vala (#8541) 2 months ago
Leonardo Eugênio d99b6177c2
Add blade support (#9513)
* Add php-only language config and queries

php-only is required enabling php injections like in blade templates

* Add blade templates support
2 months ago
Carsten Führmann eead105f94
Fix selected text background in toykonight (#9789)
Before the fix, the color that the original (Neovim) tokyonight uses for
Neovim's visual mode was used in Helix for highlighting the selected
item in the picker. But for the highlighting of selected _text_ in
Helix (corresponding to Neovim's visual mode), a much darker background
was used. This made it very hard to pick out selected text, in
particular in suboptimal lighting conditions.

I swapped the two colors, so that text selection now looks like in
Neovim, while selected items in the picker are highlighted a bit less
strongly. (But still easy to see because the whole line is highlighted.)
2 months ago
Erasin Wang 64389f97fe
Updated grammar for hurl 4 (#9775) 2 months ago
Benedikt Ritter f7913c1a3b
Extend groovy support (#9677)
* Extend groovy support

Use more complete parser introduced in nvm-treesitter in
d4dac523d2

* Update runtime/queries/groovy/locals.scm

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

* Drop indent.scm for groovy

It was copied from the tree-sitter repository but is not
compatiblw with the way indent queries are implemented
in Helix.

* Adapt groovy highlights to helix syntax

* Update documentation

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2 months ago
Alexis Mousset 8457652da1
Update to modus-themes 4.4.0 (#9912) 2 months ago
Jonas De Vuyst 0301d01e78
Fix URL in doc chapter about Textobject queries (#5773)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2 months ago
Dan Cardamore 3890376a23
add 'file-absolute-path' to statusline (#4535)
* feat: add 'file-abs-path' to statusline (#4434)

* cleanup implementation

* rename to be non-abbreviated names

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2 months ago
George "Riye" Hollister e36774c2c8
Add Support for JSONC (#9906)
* Added `jsonc` language with support for comments

The `vscode-json-language-server` accepts `jsonc` as a language id.
Allowing the use of comments within JSON files.

* fix: Update `injdection-rejex` to be unique

* fix: use includes to remove redundant queries

* ci: Generate language-support docs
2 months ago
Arthur Deierlein 9ec0271873
Add support for hyprland config (#9899)
* feat: add hyprland config language

* adjust indents to helix

* adjust highlights to helix
2 months ago
Arthur Deierlein 61f7d9ce2f
fix typo "braket" in jsx highlights (#9910) 2 months ago
Emi 761df60077
Keybind for Extend/shrink selection up and down (#9080)
* implement another selection modifying command

* Selection feels more ergonomic in case of swapping the direction. This also fixes a problem when starting at an empty line.

* rename select_line_up/down to select_line_above/below

* apply clippy suggestion of using cmp instead of if-chain

* revert `Extent` implementing `Clone/Copy`

* move select_line functions below extend_line implementations

* implement help add function, which saturates at the number of text lines

---------

Co-authored-by: Emi <emanuel.boehm@gmail.com>
2 months ago
Nick 6fea7876a4
Fix comment key bind behaviour in OCaml (#9894) 2 months ago
Michael Davis 9282f1b8e5
Handle starting and continuing the count separately (#9887) 2 months ago
Mike Trinkala b961acf746
Update regex-cursor (#9891) 2 months ago
Kirawi 0c51ab16d0
Add a yank diagnostic command (#9640)
* yank diagnostic command

* improve success message

* move to a typed command

* docgen
2 months ago
Michael Davis 6c4d986c1b Use non-deprecated chrono Duration functions 2 months ago
dependabot[bot] b44b627b14 build(deps): bump chrono from 0.4.34 to 0.4.35
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.34 to 0.4.35.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.34...v0.4.35)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2 months ago
dependabot[bot] e01a558294
build(deps): bump log from 0.4.20 to 0.4.21 (#9856)
Bumps [log](https://github.com/rust-lang/log) from 0.4.20 to 0.4.21.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.20...0.4.21)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
fnuttens 3915b04bd9
fix(themes-catppuccin): make inlay hints more legible (#9859) 2 months ago
dependabot[bot] 2d15acdf60
build(deps): bump libloading from 0.8.2 to 0.8.3 (#9857)
Bumps [libloading](https://github.com/nagisa/rust_libloading) from 0.8.2 to 0.8.3.
- [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.2...0.8.3)

---
updated-dependencies:
- dependency-name: libloading
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
dependabot[bot] ab61874efb
build(deps): bump cc from 1.0.88 to 1.0.90 (#9855)
Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.0.88 to 1.0.90.
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.88...1.0.90)

---
updated-dependencies:
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
dependabot[bot] 2e2a1d6f61
build(deps): bump open from 5.0.1 to 5.1.2 (#9854)
Bumps [open](https://github.com/Byron/open-rs) from 5.0.1 to 5.1.2.
- [Release notes](https://github.com/Byron/open-rs/releases)
- [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md)
- [Commits](https://github.com/Byron/open-rs/compare/v5.0.1...v5.1.2)

---
updated-dependencies:
- dependency-name: open
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
dependabot[bot] 2d589e74f0
build(deps): bump cachix/install-nix-action from 25 to 26 (#9851)
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 25 to 26.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Commits](https://github.com/cachix/install-nix-action/compare/v25...v26)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months ago
Kalpaj Chaudhari c145999bff
treesitter: Add textobjects for native funcs and constructors (#9806)
This allows native functions and constructors to be accessible as part
of goto_{next,prev}_func.

Change-Id: Ia1234004e8b38e1c5871331a38fcf4f267da935e
2 months ago
Aidan Gauland 3bd493299f
Use Nu language for NUON files (#9839) 2 months ago
Markus F.X.J. Oberhumer 0dc67ff885
helix-term: allow to backspace out-of the command prompt (#9828) 2 months ago
Matthew Toohey e3c6c82828
add linker script language (#9835) 2 months ago

@ -7,6 +7,14 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
tree-sitter:
patterns:
- "tree-sitter*"
rust-dependencies:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"

@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v4
- name: Install nix
uses: cachix/install-nix-action@v25
uses: cachix/install-nix-action@v26
- name: Authenticate with Cachix
uses: cachix/cachix-action@v14

291
Cargo.lock generated

@ -62,9 +62,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.80"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]]
name = "arc-swap"
@ -101,9 +101,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.2"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bstr"
@ -116,15 +116,6 @@ dependencies = [
"serde",
]
[[package]]
name = "btoi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad"
dependencies = [
"num-traits",
]
[[package]]
name = "bumpalo"
version = "3.12.0"
@ -145,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.88"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc"
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
[[package]]
name = "cfg-if"
@ -168,9 +159,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.34"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
dependencies = [
"android-tzdata",
"iana-time-zone",
@ -180,9 +171,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
version = "5.2.0"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f9a0700e0127ba15d1d52dd742097f821cd9c65939303a44d970465040a297"
checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee"
dependencies = [
"error-code",
]
@ -218,12 +209,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cov-mark"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3"
[[package]]
name = "crc32fast"
version = "1.3.2"
@ -282,7 +267,7 @@ version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"crossterm_winapi",
"filedescriptor",
"futures-core",
@ -415,9 +400,6 @@ name = "faster-hex"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
dependencies = [
"serde",
]
[[package]]
name = "fastrand"
@ -467,6 +449,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -531,9 +519,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "gix"
version = "0.58.0"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31887c304d9a935f3e5494fb5d6a0106c34e965168ec0db9b457424eedd0c741"
checksum = "e4e0e59a44bf00de058ee98d6ecf3c9ed8f8842c1da642258ae4120d41ded8f7"
dependencies = [
"gix-actor",
"gix-attributes",
@ -579,13 +567,13 @@ dependencies = [
[[package]]
name = "gix-actor"
version = "0.30.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a7bb9fad6125c81372987c06469601d37e1a2d421511adb69971b9083517a8a"
checksum = "45c3a3bde455ad2ee8ba8a195745241ce0b770a8a26faae59fcf409d01b28c46"
dependencies = [
"bstr",
"btoi",
"gix-date",
"gix-utils",
"itoa",
"thiserror",
"winnow",
@ -593,9 +581,9 @@ dependencies = [
[[package]]
name = "gix-attributes"
version = "0.22.0"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214ee3792e504ee1ce206b36dcafa4f328ca313d1e2ac0b41433d68ef4e14260"
checksum = "eefb48f42eac136a4a0023f49a54ec31be1c7a9589ed762c45dcb9b953f7ecc8"
dependencies = [
"bstr",
"gix-glob",
@ -610,27 +598,27 @@ dependencies = [
[[package]]
name = "gix-bitmap"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b6cd0f246180034ddafac9b00a112f19178135b21eb031b3f79355891f7325"
checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae"
dependencies = [
"thiserror",
]
[[package]]
name = "gix-chunk"
version = "0.4.7"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "003ec6deacf68076a0c157271a127e0bb2c031c1a41f7168cbe5d248d9b85c78"
checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52"
dependencies = [
"thiserror",
]
[[package]]
name = "gix-command"
version = "0.3.3"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce1ffc7db3fb50b7dae6ecd937a3527cb725f444614df2ad8988d81806f13f09"
checksum = "f90009020dc4b3de47beed28e1334706e0a330ddd17f5cfeb097df3b15a54b77"
dependencies = [
"bstr",
"gix-path",
@ -640,9 +628,9 @@ dependencies = [
[[package]]
name = "gix-commitgraph"
version = "0.24.0"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82dbd7fb959862e3df2583331f0ad032ac93533e8a52f1b0694bc517f5d292bc"
checksum = "f7b102311085da4af18823413b5176d7c500fb2272eaf391cfa8635d8bcb12c4"
dependencies = [
"bstr",
"gix-chunk",
@ -654,9 +642,9 @@ dependencies = [
[[package]]
name = "gix-config"
version = "0.34.0"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e62bf2073b6ce3921ffa6d8326f645f30eec5fc4a8e8a4bc0fcb721a2f3f69dc"
checksum = "62129c75e4b6229fe15fb9838cdc00c655e87105b651e4edd7c183fc5288b5d1"
dependencies = [
"bstr",
"gix-config-value",
@ -675,11 +663,11 @@ dependencies = [
[[package]]
name = "gix-config-value"
version = "0.14.4"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e7bfb37a46ed0b8468db37a6d8a0a61d56bdbe4603ae492cb322e5f3958"
checksum = "fbd06203b1a9b33a78c88252a625031b094d9e1b647260070c25b09910c0a804"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"bstr",
"gix-path",
"libc",
@ -688,9 +676,9 @@ dependencies = [
[[package]]
name = "gix-date"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb7f3dfb72bebe3449b5e642be64e3c6ccbe9821c8b8f19f487cf5bfbbf4067e"
checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c"
dependencies = [
"bstr",
"itoa",
@ -700,9 +688,9 @@ dependencies = [
[[package]]
name = "gix-diff"
version = "0.40.0"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdcb5e49c4b9729dd1c361040ae5c3cd7c497b2260b18c954f62db3a63e98cf"
checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4"
dependencies = [
"bstr",
"gix-hash",
@ -712,9 +700,9 @@ dependencies = [
[[package]]
name = "gix-discover"
version = "0.29.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4669218f3ec0cbbf8f16857b32200890f8ca585f36f5817242e4115fe4551af"
checksum = "64bab49087ed3710caf77e473dc0efc54ca33d8ccc6441359725f121211482b1"
dependencies = [
"bstr",
"dunce",
@ -728,9 +716,9 @@ dependencies = [
[[package]]
name = "gix-features"
version = "0.38.0"
version = "0.38.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "184f7f7d4e45db0e2a362aeaf12c06c5e84817d0ef91d08e8e90170dad9f0b07"
checksum = "db4254037d20a247a0367aa79333750146a369719f0c6617fec4f5752cc62b37"
dependencies = [
"crc32fast",
"flate2",
@ -747,9 +735,9 @@ dependencies = [
[[package]]
name = "gix-filter"
version = "0.9.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9240862840fb740d209422937195e129e4ed3da49af212383260134bea8f6c1a"
checksum = "bd71bf3e64d8fb5d5635d4166ca5a36fe56b292ffff06eab1d93ea47fd5beb89"
dependencies = [
"bstr",
"encoding_rs",
@ -768,9 +756,9 @@ dependencies = [
[[package]]
name = "gix-fs"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4436e883d5769f9fb18677b8712b49228357815f9e4104174a6fc2d8461a437b"
checksum = "634b8a743b0aae03c1a74ee0ea24e8c5136895efac64ce52b3ea106e1c6f0613"
dependencies = [
"gix-features",
"gix-utils",
@ -778,11 +766,11 @@ dependencies = [
[[package]]
name = "gix-glob"
version = "0.16.0"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4965a1d06d0ab84a29d4a67697a97352ab14ae1da821084e5afb1fd6d8191ca0"
checksum = "682bdc43cb3c00dbedfcc366de2a849b582efd8d886215dbad2ea662ec156bb5"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"bstr",
"gix-features",
"gix-path",
@ -790,9 +778,9 @@ dependencies = [
[[package]]
name = "gix-hash"
version = "0.14.1"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0ed89cdc1dce26685c80271c4287077901de3c3dd90234d5fa47c22b2268653"
checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e"
dependencies = [
"faster-hex",
"thiserror",
@ -800,9 +788,9 @@ dependencies = [
[[package]]
name = "gix-hashtable"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe47d8c0887f82355e2e9e16b6cecaa4d5e5346a7a474ca78ff94de1db35a5b"
checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242"
dependencies = [
"gix-hash",
"hashbrown 0.14.3",
@ -811,9 +799,9 @@ dependencies = [
[[package]]
name = "gix-ignore"
version = "0.11.0"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7069aaca4a05784c4cb44e392f0eaf627c6e57e05d3100c0e2386a37a682f0"
checksum = "640dbeb4f5829f9fc14d31f654a34a0350e43a24e32d551ad130d99bf01f63f1"
dependencies = [
"bstr",
"gix-glob",
@ -824,14 +812,14 @@ dependencies = [
[[package]]
name = "gix-index"
version = "0.29.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7152181ba8f0a3addc5075dd612cea31fc3e252b29c8be8c45f4892bf87426"
checksum = "549621f13d9ccf325a7de45506a3266af0d08f915181c5687abb5e8669bfd2e6"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"bstr",
"btoi",
"filetime",
"fnv",
"gix-bitmap",
"gix-features",
"gix-fs",
@ -839,6 +827,8 @@ dependencies = [
"gix-lock",
"gix-object",
"gix-traverse",
"gix-utils",
"hashbrown 0.14.3",
"itoa",
"libc",
"memmap2",
@ -860,9 +850,9 @@ dependencies = [
[[package]]
name = "gix-macros"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75e7ab728059f595f6ddc1ad8771b8d6a231971ae493d9d5948ecad366ee8bb"
checksum = "1dff438f14e67e7713ab9332f5fd18c8f20eb7eb249494f6c2bf170522224032"
dependencies = [
"proc-macro2",
"quote",
@ -871,16 +861,16 @@ dependencies = [
[[package]]
name = "gix-object"
version = "0.41.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693ce9d30741506cb082ef2d8b797415b48e032cce0ab23eff894c19a7e4777b"
checksum = "3d4f8efae72030df1c4a81d02dbe2348e748d9b9a11e108ed6efbd846326e051"
dependencies = [
"bstr",
"btoi",
"gix-actor",
"gix-date",
"gix-features",
"gix-hash",
"gix-utils",
"gix-validate",
"itoa",
"smallvec",
@ -890,9 +880,9 @@ dependencies = [
[[package]]
name = "gix-odb"
version = "0.57.0"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ba2fa9e81f2461b78b4d81a807867667326c84cdab48e0aed7b73a593aa1be4"
checksum = "81b55378c719693380f66d9dd21ce46721eed2981d8789fc698ec1ada6fa176e"
dependencies = [
"arc-swap",
"gix-date",
@ -910,9 +900,9 @@ dependencies = [
[[package]]
name = "gix-pack"
version = "0.47.0"
version = "0.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da5f3e78c96b76c4e6fe5e8e06b76221e4a0ee9a255aa935ed1fdf68988dfd8"
checksum = "6391aeaa030ad64aba346a9f5c69bb1c4e5c6fb4411705b03b40b49d8614ec30"
dependencies = [
"clru",
"gix-chunk",
@ -942,9 +932,9 @@ dependencies = [
[[package]]
name = "gix-path"
version = "0.10.4"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a6282621aed1becc3f83d64099a564b3b9063f22783d9a87ea502a3e9f2e40"
checksum = "23623cf0f475691a6d943f898c4d0b89f5c1a2a64d0f92bce0e0322ee6528783"
dependencies = [
"bstr",
"gix-trace",
@ -955,11 +945,11 @@ dependencies = [
[[package]]
name = "gix-pathspec"
version = "0.6.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cbd49750edb26b0a691e5246fc635fa554d344da825cd20fa9ee0da9c1b761f"
checksum = "1a96ed0e71ce9084a471fddfa74e842576a7cbf02fe8bd50388017ac461aed97"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"bstr",
"gix-attributes",
"gix-config-value",
@ -970,20 +960,20 @@ dependencies = [
[[package]]
name = "gix-quote"
version = "0.4.10"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7dc10303d73a960d10fb82f81188b036ac3e6b11b5795b20b1a60b51d1321f"
checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff"
dependencies = [
"bstr",
"btoi",
"gix-utils",
"thiserror",
]
[[package]]
name = "gix-ref"
version = "0.41.0"
version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5818958994ad7879fa566f5441ebcc48f0926aa027b28948e6fbf6578894dc31"
checksum = "fd4aba68b925101cb45d6df328979af0681364579db889098a0de75b36c77b65"
dependencies = [
"gix-actor",
"gix-date",
@ -1003,9 +993,9 @@ dependencies = [
[[package]]
name = "gix-refspec"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613aa4d93034c5791d13bdc635e530f4ddab1412ddfb4a8215f76213177b61c7"
checksum = "dde848865834a54fe4d9b4573f15d0e9a68eaf3d061b42d3ed52b4b8acf880b2"
dependencies = [
"bstr",
"gix-hash",
@ -1017,9 +1007,9 @@ dependencies = [
[[package]]
name = "gix-revision"
version = "0.26.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288f6549d7666db74dc3f169a9a333694fc28ecd2f5aa7b2c979c89eb556751a"
checksum = "9e34196e1969bd5d36e2fbc4467d893999132219d503e23474a8ad2b221cb1e8"
dependencies = [
"bstr",
"gix-date",
@ -1033,9 +1023,9 @@ dependencies = [
[[package]]
name = "gix-revwalk"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b9b4d91dfc5c14fee61a28c65113ded720403b65a0f46169c0460f731a5d03c"
checksum = "e0a7d393ae814eeaae41a333c0ff684b243121cc61ccdc5bbe9897094588047d"
dependencies = [
"gix-commitgraph",
"gix-date",
@ -1048,11 +1038,11 @@ dependencies = [
[[package]]
name = "gix-sec"
version = "0.10.4"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8d9bf462feaf05f2121cba7399dbc6c34d88a9cad58fc1e95027791d6a3c6d2"
checksum = "fddc27984a643b20dd03e97790555804f98cf07404e0e552c0ad8133266a79a1"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"gix-path",
"libc",
"windows-sys 0.52.0",
@ -1060,9 +1050,9 @@ dependencies = [
[[package]]
name = "gix-submodule"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73182f6c1f5ed1ed94ba16581ac62593d5e29cd1c028b2af618f836283b8f8d4"
checksum = "4fb7ea05666362472fecd44c1fc35fe48a5b9b841b431cc4f85b95e6f20c23ec"
dependencies = [
"bstr",
"gix-config",
@ -1088,15 +1078,15 @@ dependencies = [
[[package]]
name = "gix-trace"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b202d766a7fefc596e2cc6a89cda8ad8ad733aed82da635ac120691112a9b1"
checksum = "9b838b2db8f62c9447d483a4c28d251b67fee32741a82cb4d35e9eb4e9fdc5ab"
[[package]]
name = "gix-traverse"
version = "0.37.0"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfc30c5b5e4e838683b59e1b0574ce6bc1c35916df9709aaab32bb7751daf08b"
checksum = "95aef84bc777025403a09788b1e4815c06a19332e9e5d87a955e1ed7da9bf0cf"
dependencies = [
"gix-commitgraph",
"gix-date",
@ -1110,9 +1100,9 @@ dependencies = [
[[package]]
name = "gix-url"
version = "0.27.0"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26f1981ecc700f4fd73ae62b9ca2da7c8816c8fd267f0185e3f8c21e967984ac"
checksum = "8f0b24f3ecc79a5a53539de9c2e99425d0ef23feacdcf3faac983aa9a2f26849"
dependencies = [
"bstr",
"gix-features",
@ -1124,9 +1114,9 @@ dependencies = [
[[package]]
name = "gix-utils"
version = "0.1.9"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e839f3d0798b296411263da6bee780a176ef8008a5dfc31287f7eda9266ab8"
checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92"
dependencies = [
"fastrand",
"unicode-normalization",
@ -1134,9 +1124,9 @@ dependencies = [
[[package]]
name = "gix-validate"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac7cc36f496bd5d96cdca0f9289bb684480725d40db60f48194aa7723b883854"
checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545"
dependencies = [
"bstr",
"thiserror",
@ -1144,9 +1134,9 @@ dependencies = [
[[package]]
name = "gix-worktree"
version = "0.30.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca36bb3dc54038c66507dc75c4d8edbee2d6d5cc45227b4eb508ad13dd60a006"
checksum = "fe78e03af9eec168eb187e05463a981c57f0a915f64b1788685a776bd2ef969c"
dependencies = [
"bstr",
"gix-attributes",
@ -1232,7 +1222,7 @@ version = "23.10.0"
dependencies = [
"ahash",
"arc-swap",
"bitflags 2.4.2",
"bitflags 2.5.0",
"chrono",
"dunce",
"encoding_rs",
@ -1387,6 +1377,7 @@ dependencies = [
"smallvec",
"tempfile",
"termini",
"thiserror",
"tokio",
"tokio-stream",
"toml",
@ -1397,7 +1388,7 @@ dependencies = [
name = "helix-tui"
version = "23.10.0"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"cassowary",
"crossterm",
"helix-core",
@ -1431,7 +1422,7 @@ version = "23.10.0"
dependencies = [
"anyhow",
"arc-swap",
"bitflags 2.4.2",
"bitflags 2.5.0",
"chardetng",
"clipboard-win",
"crossterm",
@ -1603,9 +1594,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libloading"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets 0.52.0",
@ -1638,15 +1629,15 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.20"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "lsp-types"
version = "0.95.0"
version = "0.95.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "158c1911354ef73e8fe42da6b10c0484cb65c7f1007f28022e847706c1ab6984"
checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365"
dependencies = [
"bitflags 1.3.2",
"serde",
@ -1702,9 +1693,9 @@ dependencies = [
[[package]]
name = "nucleo"
version = "0.2.1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5331f4bcce475cf28cb29c95366c3091af4b0aa7703f1a6bc858f29718fdf3"
checksum = "6350a138d8860658523a7593cbf6813999d17a099371d14f70c5c905b37593e9"
dependencies = [
"nucleo-matcher",
"parking_lot",
@ -1713,11 +1704,10 @@ dependencies = [
[[package]]
name = "nucleo-matcher"
version = "0.2.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
dependencies = [
"cov-mark",
"memchr",
"unicode-segmentation",
]
@ -1767,9 +1757,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "open"
version = "5.0.1"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32"
dependencies = [
"is-wsl",
"libc",
@ -1844,7 +1834,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"memchr",
"unicase",
]
@ -1950,9 +1940,9 @@ dependencies = [
[[package]]
name = "regex-cursor"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a43718aa0040434d45728c43f56bd53bda75a91c46954cdf0f2ff4dbc8aabbe7"
checksum = "ae4327b5fde3ae6fda0152128d3d59b95a5aad7be91c405869300091720f7169"
dependencies = [
"log",
"memchr",
@ -1989,7 +1979,7 @@ version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys",
@ -2256,18 +2246,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.57"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.57"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
@ -2359,9 +2349,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
@ -2370,9 +2360,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.10"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
dependencies = [
"serde",
"serde_spanned",
@ -2391,9 +2381,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.4"
version = "0.22.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951"
checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd"
dependencies = [
"indexmap",
"serde",
@ -2404,8 +2394,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.10"
source = "git+https://github.com/helix-editor/tree-sitter?rev=660481dbf71413eba5a928b0b0ab8da50c1109e0#660481dbf71413eba5a928b0b0ab8da50c1109e0"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb9c9f15eae91dcd00ee0d86a281d16e6263786991b662b34fa9632c21a046b"
dependencies = [
"cc",
"regex",
@ -2812,9 +2803,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.28"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
dependencies = [
"memchr",
]

@ -37,8 +37,8 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.20", git = "https://github.com/helix-editor/tree-sitter", rev = "660481dbf71413eba5a928b0b0ab8da50c1109e0" }
nucleo = "0.2.0"
tree-sitter = { version = "0.22" }
nucleo = "0.4.1"
[workspace.package]
version = "23.10.0"

@ -68,6 +68,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | "abcdefghijklmnopqrstuvwxyz"
### `[editor.statusline]` Section
@ -108,6 +109,7 @@ The following statusline elements can be configured:
| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
| `spinner` | A progress spinner indicating LSP activity |
| `file-name` | The path/name of the opened file |
| `file-absolute-path` | The absolute path/name of the opened file |
| `file-base-name` | The basename of the opened file |
| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |

@ -1,5 +1,6 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| ada | ✓ | ✓ | | `ada_language_server`, `ada_language_server` |
| agda | ✓ | | | |
| astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` |
@ -8,6 +9,7 @@
| beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` |
| blade | ✓ | | | |
| blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
@ -30,7 +32,7 @@
| devicetree | ✓ | | | |
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
| diff | ✓ | | | |
| docker-compose | ✓ | | ✓ | `docker-compose-langserver` |
| docker-compose | ✓ | | ✓ | `docker-compose-langserver`, `yaml-language-server` |
| dockerfile | ✓ | | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | |
@ -58,6 +60,7 @@
| git-ignore | ✓ | | | |
| git-rebase | ✓ | | | |
| gleam | ✓ | ✓ | | `gleam` |
| glimmer | ✓ | | | `ember-language-server` |
| glsl | ✓ | ✓ | ✓ | |
| gn | ✓ | | | |
| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
@ -72,11 +75,13 @@
| haskell-persistent | ✓ | | | |
| hcl | ✓ | ✓ | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` |
| helm | ✓ | | | `helm_ls` |
| hocon | ✓ | | ✓ | |
| hoon | ✓ | | | |
| hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` |
| hurl | ✓ | | ✓ | |
| hyprlang | ✓ | | ✓ | |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
@ -87,13 +92,16 @@
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| json5 | ✓ | | | |
| jsonc | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | ✓ | ✓ | `julia` |
| just | ✓ | ✓ | ✓ | |
| kdl | ✓ | ✓ | ✓ | |
| koka | ✓ | | ✓ | |
| kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` |
| ld | ✓ | | ✓ | |
| lean | ✓ | | | `lean` |
| ledger | ✓ | | | |
| llvm | ✓ | ✓ | ✓ | |
@ -128,6 +136,8 @@
| pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` |
| php-only | ✓ | | | |
| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` |
| pkl | ✓ | | ✓ | |
| po | ✓ | ✓ | | |
| pod | ✓ | | | |
@ -165,6 +175,7 @@
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
| strace | ✓ | | | |
| supercollider | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` |
@ -185,7 +196,7 @@
| unison | ✓ | | ✓ | |
| uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` |
| vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | |

@ -86,3 +86,4 @@
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
| `:redraw` | Clear and re-render the whole UI |
| `:move` | Move the current buffer and its corresponding file to a different path |
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |

@ -44,4 +44,4 @@ doesn't make sense in a navigation context.
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+path%3A%2A%2A/textobjects.scm&type=Code&ref=advsearch&l=&l=

@ -49,7 +49,7 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `G` | Go to line number `<n>` | `goto_line` |
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
| `Alt-.` | Repeat last motion (`f`, `t`, `m`, `[` or `]`) | `repeat_last_motion` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
@ -224,6 +224,7 @@ Jumps to various locations.
| `.` | Go to last modification in current file | `goto_last_modification` |
| `j` | Move down textual (instead of visual) line | `move_line_down` |
| `k` | Move up textual (instead of visual) line | `move_line_up` |
| `w` | Show labels at each word and select the word that belongs to the entered labels | `goto_word` |
#### Match mode

@ -301,6 +301,7 @@ These scopes are used for theming the editor interface:
| `ui.bufferline.background` | Style for bufferline background |
| `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.picker.header` | Column names in pickers with multiple columns |
| `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Default text style, command prompts, popup text, etc. |
@ -314,6 +315,7 @@ These scopes are used for theming the editor interface:
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
| `ui.virtual.jump-label` | Style for virtual jump labels |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |

@ -31,7 +31,7 @@ tree-sitter.workspace = true
once_cell = "1.19"
arc-swap = "1"
regex = "1"
bitflags = "2.4"
bitflags = "2.5"
ahash = "0.8.11"
hashbrown = { version = "0.14.3", features = ["raw"] }
dunce = "1.0"

@ -116,7 +116,7 @@ impl Default for TextFormat {
#[derive(Debug)]
pub struct DocumentFormatter<'t> {
text_fmt: &'t TextFormat,
annotations: &'t TextAnnotations,
annotations: &'t TextAnnotations<'t>,
/// The visual position at the end of the last yielded word boundary
visual_pos: Position,

@ -1,5 +1,3 @@
use std::rc::Rc;
use crate::doc_formatter::{DocumentFormatter, TextFormat};
use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
@ -105,7 +103,7 @@ fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay
DocumentFormatter::new_at_prev_checkpoint(
text.into(),
&TextFormat::new_test(softwrap),
TextAnnotations::default().add_overlay(overlays.into(), None),
TextAnnotations::default().add_overlay(overlays, None),
char_pos,
)
.0
@ -142,7 +140,7 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -
DocumentFormatter::new_at_prev_checkpoint(
text.into(),
&TextFormat::new_test(softwrap),
TextAnnotations::default().add_inline_annotations(annotations.into(), None),
TextAnnotations::default().add_inline_annotations(annotations, None),
0,
)
.0
@ -164,15 +162,24 @@ fn annotation() {
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
);
}
#[test]
fn annotation_and_overlay() {
let annotations = [InlineAnnotation {
char_idx: 0,
text: "fooo".into(),
}];
let overlay = [Overlay {
char_idx: 0,
grapheme: "\t".into(),
}];
assert_eq!(
DocumentFormatter::new_at_prev_checkpoint(
"bbar".into(),
&TextFormat::new_test(false),
TextAnnotations::default()
.add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None)
.add_overlay(Rc::new([Overlay::new(0, "\t")]), None),
.add_inline_annotations(annotations.as_slice(), None)
.add_overlay(overlay.as_slice(), None),
0,
)
.0

@ -1,6 +1,6 @@
use std::ops::DerefMut;
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
use nucleo::Config;
use parking_lot::Mutex;
@ -38,6 +38,12 @@ pub fn fuzzy_match<T: AsRef<str>>(
if path {
matcher.config.set_match_paths();
}
let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false);
let pattern = Atom::new(
pattern,
CaseMatching::Smart,
Normalization::Smart,
AtomKind::Fuzzy,
false,
);
pattern.match_list(items, &mut matcher)
}

@ -425,6 +425,85 @@ impl<'a> Iterator for RopeGraphemes<'a> {
}
}
/// An iterator over the graphemes of a `RopeSlice` in reverse.
#[derive(Clone)]
pub struct RevRopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl<'a> fmt::Debug for RevRopeGraphemes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RevRopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl<'a> RevRopeGraphemes<'a> {
#[must_use]
pub fn new(slice: RopeSlice) -> RevRopeGraphemes {
let (mut chunks, mut cur_chunk_start, _, _) = slice.chunks_at_byte(slice.len_bytes());
chunks.reverse();
let first_chunk = chunks.next().unwrap_or("");
cur_chunk_start -= first_chunk.len();
RevRopeGraphemes {
text: slice,
chunks,
cur_chunk: first_chunk,
cur_chunk_start,
cursor: GraphemeCursor::new(slice.len_bytes(), slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RevRopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
self.cur_chunk = self.chunks.next().unwrap_or("");
self.cur_chunk_start -= self.cur_chunk.len();
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a >= self.cur_chunk_start + self.cur_chunk.len() {
Some(self.text.byte_slice(b..a))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[b2..a2]).into())
}
}
}
/// A highly compressed Cow<'a, str> that holds
/// atmost u31::MAX bytes and is readonly
pub struct GraphemeStr<'a> {

@ -27,7 +27,7 @@ pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
let date_time = NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?;
Some(
date_time
.checked_add_signed(Duration::minutes(amount))?
.checked_add_signed(Duration::try_minutes(amount)?)?
.format(format.fmt)
.to_string(),
)
@ -35,14 +35,15 @@ pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
(true, false) => {
let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
Some(
date.checked_add_signed(Duration::days(amount))?
date.checked_add_signed(Duration::try_days(amount)?)?
.format(format.fmt)
.to_string(),
)
}
(false, true) => {
let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
let (adjusted_time, _) = time.overflowing_add_signed(Duration::minutes(amount));
let (adjusted_time, _) =
time.overflowing_add_signed(Duration::try_minutes(amount)?);
Some(adjusted_time.format(format.fmt).to_string())
}
(false, false) => None,

@ -1,42 +1,52 @@
use crate::{Range, RopeSlice, Selection, Syntax};
use tree_sitter::Node;
use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(syntax, text, selection, |mut node, from, to| {
while node.start_byte() == from && node.end_byte() == to {
node = node.parent()?;
let cursor = &mut syntax.walk();
selection.transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
let byte_range = from..to;
cursor.reset_to_byte_range(from, to);
while cursor.node().byte_range() == byte_range {
if !cursor.goto_parent() {
break;
}
}
Some(node)
let node = cursor.node();
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
Range::new(to, from).with_direction(range.direction())
})
}
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
descendant.child(0).or(Some(descendant))
select_node_impl(syntax, text, selection, |cursor| {
cursor.goto_first_child();
})
}
pub fn select_sibling<F>(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
sibling_fn: &F,
) -> Selection
where
F: Fn(Node) -> Option<Node>,
{
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
find_sibling_recursive(descendant, sibling_fn)
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(syntax, text, selection, |cursor| {
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
break;
}
}
})
}
fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
where
F: Fn(Node) -> Option<Node>,
{
sibling_fn(node).or_else(|| {
node.parent()
.and_then(|node| find_sibling_recursive(node, sibling_fn))
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(syntax, text, selection, |cursor| {
while !cursor.goto_prev_sibling() {
if !cursor.goto_parent() {
break;
}
}
})
}
@ -44,33 +54,25 @@ fn select_node_impl<F>(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
select_fn: F,
motion: F,
) -> Selection
where
F: Fn(Node, usize, usize) -> Option<Node>,
F: Fn(&mut TreeCursor),
{
let tree = syntax.tree();
let cursor = &mut syntax.walk();
selection.transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
let node = match tree
.root_node()
.descendant_for_byte_range(from, to)
.and_then(|node| select_fn(node, from, to))
{
Some(node) => node,
None => return range,
};
cursor.reset_to_byte_range(from, to);
motion(cursor);
let node = cursor.node();
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
if range.head < range.anchor {
Range::new(to, from)
} else {
Range::new(from, to)
}
Range::new(from, to).with_direction(range.direction())
})
}

@ -705,6 +705,15 @@ impl IntoIterator for Selection {
}
}
impl From<Range> for Selection {
fn from(range: Range) -> Self {
Self {
ranges: smallvec![range],
primary_index: 0,
}
}
}
// TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_or_remove_matches(

@ -1,3 +1,5 @@
mod tree_cursor;
use crate::{
auto_pairs::AutoPairs,
chars::char_is_line_ending,
@ -21,7 +23,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::{self, Display},
hash::{Hash, Hasher},
mem::{replace, transmute},
mem::replace,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
@ -32,6 +34,8 @@ use serde::{ser::SerializeSeq, Deserialize, Serialize};
use helix_loader::grammar::{get_language, load_runtime_file};
pub use tree_cursor::TreeCursor;
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
where
D: serde::Deserializer<'de>,
@ -805,7 +809,7 @@ impl LanguageConfiguration {
if query_text.is_empty() {
return None;
}
let lang = self.highlight_config.get()?.as_ref()?.language;
let lang = &self.highlight_config.get()?.as_ref()?.language;
Query::new(lang, &query_text)
.map_err(|e| {
log::error!(
@ -1090,6 +1094,7 @@ impl Syntax {
start_point: Point::new(0, 0),
end_point: Point::new(usize::MAX, usize::MAX),
}],
parent: None,
};
// track scope_descriptor: a Vec of scopes for item in tree
@ -1360,6 +1365,7 @@ impl Syntax {
depth,
ranges,
flags: LayerUpdateFlags::empty(),
parent: Some(layer_id),
};
// Find an identical existing layer
@ -1493,6 +1499,12 @@ impl Syntax {
.descendant_for_byte_range(start, end)
}
pub fn walk(&self) -> TreeCursor<'_> {
// data structure to find the smallest range that contains a point
// when some of the ranges in the structure can overlap.
TreeCursor::new(&self.layers, self.root)
}
// Commenting
// comment_strings_for_pos
// is_commented
@ -1525,6 +1537,7 @@ pub struct LanguageLayer {
pub ranges: Vec<Range>,
pub depth: u32,
flags: LayerUpdateFlags,
parent: Option<LayerId>,
}
/// This PartialEq implementation only checks if that
@ -1544,13 +1557,7 @@ impl PartialEq for LanguageLayer {
impl Hash for LanguageLayer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.depth.hash(state);
// The transmute is necessary here because tree_sitter::Language does not derive Hash at the moment.
// However it does use #[repr] transparent so the transmute here is safe
// as `Language` (which `Grammar` is an alias for) is just a newtype wrapper around a (thin) pointer.
// This is also compatible with the PartialEq implementation of language
// as that is just a pointer comparison.
let language: *const () = unsafe { transmute(self.config.language) };
language.hash(state);
self.config.language.hash(state);
self.ranges.hash(state);
}
}
@ -1567,7 +1574,7 @@ impl LanguageLayer {
.map_err(|_| Error::InvalidRanges)?;
parser
.set_language(self.config.language)
.set_language(&self.config.language)
.map_err(|_| Error::InvalidLanguage)?;
// unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
@ -1726,7 +1733,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize};
use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, TextProvider, Tree, TreeCursor,
QueryMatch, Range, TextProvider, Tree,
};
const CANCELLATION_CHECK_INTERVAL: usize = 100;
@ -1867,7 +1874,7 @@ impl HighlightConfiguration {
// Construct a single query by concatenating the three query strings, but record the
// range of pattern indices that belong to each individual string.
let query = Query::new(language, &query_source)?;
let query = Query::new(&language, &query_source)?;
let mut highlights_pattern_index = 0;
for i in 0..(query.pattern_count()) {
let pattern_offset = query.start_byte_for_pattern(i);
@ -1876,7 +1883,7 @@ impl HighlightConfiguration {
}
}
let injections_query = Query::new(language, injection_query)?;
let injections_query = Query::new(&language, injection_query)?;
let combined_injections_patterns = (0..injections_query.pattern_count())
.filter(|&i| {
injections_query
@ -2660,7 +2667,7 @@ pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result
fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W,
cursor: &mut TreeCursor,
cursor: &mut tree_sitter::TreeCursor,
depth: usize,
) -> fmt::Result {
let node = cursor.node();
@ -2730,7 +2737,7 @@ mod test {
.unwrap();
let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap();
let query = Query::new(&language, query_str).unwrap();
let textobject = TextObjectQuery { query };
let mut cursor = QueryCursor::new();
@ -2970,7 +2977,7 @@ mod test {
// rule but `name` and `body` belong to an unnamed helper `_method_rest`.
// This can cause a bug with a pretty-printing implementation that
// uses `Node::field_name_for_child` to determine field names but is
// fixed when using `TreeCursor::field_name`.
// fixed when using `tree_sitter::TreeCursor::field_name`.
let source = "def self.method_name
true
end";

@ -0,0 +1,160 @@
use std::{cmp::Reverse, ops::Range};
use super::{LanguageLayer, LayerId};
use slotmap::HopSlotMap;
use tree_sitter::Node;
/// The byte range of an injection layer.
///
/// Injection ranges may overlap, but all overlapping parts are subsets of their parent ranges.
/// This allows us to sort the ranges ahead of time in order to efficiently find a range that
/// contains a point with maximum depth.
#[derive(Debug)]
struct InjectionRange {
start: usize,
end: usize,
layer_id: LayerId,
depth: u32,
}
pub struct TreeCursor<'a> {
layers: &'a HopSlotMap<LayerId, LanguageLayer>,
root: LayerId,
current: LayerId,
injection_ranges: Vec<InjectionRange>,
// TODO: Ideally this would be a `tree_sitter::TreeCursor<'a>` but
// that returns very surprising results in testing.
cursor: Node<'a>,
}
impl<'a> TreeCursor<'a> {
pub(super) fn new(layers: &'a HopSlotMap<LayerId, LanguageLayer>, root: LayerId) -> Self {
let mut injection_ranges = Vec::new();
for (layer_id, layer) in layers.iter() {
// Skip the root layer
if layer.parent.is_none() {
continue;
}
for byte_range in layer.ranges.iter() {
let range = InjectionRange {
start: byte_range.start_byte,
end: byte_range.end_byte,
layer_id,
depth: layer.depth,
};
injection_ranges.push(range);
}
}
injection_ranges.sort_unstable_by_key(|range| (range.end, Reverse(range.depth)));
let cursor = layers[root].tree().root_node();
Self {
layers,
root,
current: root,
injection_ranges,
cursor,
}
}
pub fn node(&self) -> Node<'a> {
self.cursor
}
pub fn goto_parent(&mut self) -> bool {
if let Some(parent) = self.node().parent() {
self.cursor = parent;
return true;
}
// If we are already on the root layer, we cannot ascend.
if self.current == self.root {
return false;
}
// Ascend to the parent layer.
let range = self.node().byte_range();
let parent_id = self.layers[self.current]
.parent
.expect("non-root layers have a parent");
self.current = parent_id;
let root = self.layers[self.current].tree().root_node();
self.cursor = root
.descendant_for_byte_range(range.start, range.end)
.unwrap_or(root);
true
}
/// Finds the injection layer that has exactly the same range as the given `range`.
fn layer_id_of_byte_range(&self, search_range: Range<usize>) -> Option<LayerId> {
let start_idx = self
.injection_ranges
.partition_point(|range| range.end < search_range.end);
self.injection_ranges[start_idx..]
.iter()
.take_while(|range| range.end == search_range.end)
.find_map(|range| (range.start == search_range.start).then_some(range.layer_id))
}
pub fn goto_first_child(&mut self) -> bool {
// Check if the current node's range is an exact injection layer range.
if let Some(layer_id) = self
.layer_id_of_byte_range(self.node().byte_range())
.filter(|&layer_id| layer_id != self.current)
{
// Switch to the child layer.
self.current = layer_id;
self.cursor = self.layers[self.current].tree().root_node();
true
} else if let Some(child) = self.cursor.child(0) {
// Otherwise descend in the current tree.
self.cursor = child;
true
} else {
false
}
}
pub fn goto_next_sibling(&mut self) -> bool {
if let Some(sibling) = self.cursor.next_sibling() {
self.cursor = sibling;
true
} else {
false
}
}
pub fn goto_prev_sibling(&mut self) -> bool {
if let Some(sibling) = self.cursor.prev_sibling() {
self.cursor = sibling;
true
} else {
false
}
}
/// Finds the injection layer that contains the given start-end range.
fn layer_id_containing_byte_range(&self, start: usize, end: usize) -> LayerId {
let start_idx = self
.injection_ranges
.partition_point(|range| range.end < end);
self.injection_ranges[start_idx..]
.iter()
.take_while(|range| range.start < end)
.find_map(|range| (range.start <= start).then_some(range.layer_id))
.unwrap_or(self.root)
}
pub fn reset_to_byte_range(&mut self, start: usize, end: usize) {
self.current = self.layer_id_containing_byte_range(start, end);
let root = self.layers[self.current].tree().root_node();
self.cursor = root.descendant_for_byte_range(start, end).unwrap_or(root);
}
}

@ -1,6 +1,5 @@
use std::cell::Cell;
use std::ops::Range;
use std::rc::Rc;
use crate::syntax::Highlight;
use crate::Tendril;
@ -92,23 +91,23 @@ pub struct LineAnnotation {
}
#[derive(Debug)]
struct Layer<A, M> {
annotations: Rc<[A]>,
struct Layer<'a, A, M> {
annotations: &'a [A],
current_index: Cell<usize>,
metadata: M,
}
impl<A, M: Clone> Clone for Layer<A, M> {
impl<A, M: Clone> Clone for Layer<'_, A, M> {
fn clone(&self) -> Self {
Layer {
annotations: self.annotations.clone(),
annotations: self.annotations,
current_index: self.current_index.clone(),
metadata: self.metadata.clone(),
}
}
}
impl<A, M> Layer<A, M> {
impl<A, M> Layer<'_, A, M> {
pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
let new_index = self
.annotations
@ -128,8 +127,8 @@ impl<A, M> Layer<A, M> {
}
}
impl<A, M> From<(Rc<[A]>, M)> for Layer<A, M> {
fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer<A, M> {
impl<'a, A, M> From<(&'a [A], M)> for Layer<'a, A, M> {
fn from((annotations, metadata): (&'a [A], M)) -> Layer<A, M> {
Layer {
annotations,
current_index: Cell::new(0),
@ -147,13 +146,13 @@ fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> u
/// Annotations that change that is displayed when the document is render.
/// Also commonly called virtual text.
#[derive(Default, Debug, Clone)]
pub struct TextAnnotations {
inline_annotations: Vec<Layer<InlineAnnotation, Option<Highlight>>>,
overlays: Vec<Layer<Overlay, Option<Highlight>>>,
line_annotations: Vec<Layer<LineAnnotation, ()>>,
pub struct TextAnnotations<'a> {
inline_annotations: Vec<Layer<'a, InlineAnnotation, Option<Highlight>>>,
overlays: Vec<Layer<'a, Overlay, Option<Highlight>>>,
line_annotations: Vec<Layer<'a, LineAnnotation, ()>>,
}
impl TextAnnotations {
impl<'a> TextAnnotations<'a> {
/// Prepare the TextAnnotations for iteration starting at char_idx
pub fn reset_pos(&self, char_idx: usize) {
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
@ -194,7 +193,7 @@ impl TextAnnotations {
/// the annotations that belong to the layers added first will be shown first.
pub fn add_inline_annotations(
&mut self,
layer: Rc<[InlineAnnotation]>,
layer: &'a [InlineAnnotation],
highlight: Option<Highlight>,
) -> &mut Self {
self.inline_annotations.push((layer, highlight).into());
@ -211,7 +210,7 @@ impl TextAnnotations {
///
/// If multiple layers contain overlay at the same position
/// the overlay from the layer added last will be show.
pub fn add_overlay(&mut self, layer: Rc<[Overlay]>, highlight: Option<Highlight>) -> &mut Self {
pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option<Highlight>) -> &mut Self {
self.overlays.push((layer, highlight).into());
self
}
@ -220,7 +219,7 @@ impl TextAnnotations {
///
/// The line annotations **must be sorted** by their `char_idx`.
/// Multiple line annotations with the same `char_idx` **are not allowed**.
pub fn add_line_annotation(&mut self, layer: Rc<[LineAnnotation]>) -> &mut Self {
pub fn add_line_annotation(&mut self, layer: &'a [LineAnnotation]) -> &mut Self {
self.line_annotations.push((layer, ()).into());
self
}

@ -28,6 +28,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.36", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.14"
tokio-stream = "0.1.15"
parking_lot = "0.12.1"
arc-swap = "1"

@ -16,7 +16,7 @@ dunce = "1.0"
etcetera = "0.8"
ropey = { version = "1.6.1", default-features = false }
which = "6.0"
regex-cursor = "0.1.3"
regex-cursor = "0.1.4"
[dev-dependencies]
tempfile = "3.10"

@ -42,8 +42,9 @@ pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
pub fn which<T: AsRef<OsStr>>(
binary_name: T,
) -> Result<std::path::PathBuf, ExecutableNotFoundError> {
which::which(binary_name.as_ref()).map_err(|err| ExecutableNotFoundError {
command: binary_name.as_ref().to_string_lossy().into_owned(),
let binary_name = binary_name.as_ref();
which::which(binary_name).map_err(|err| ExecutableNotFoundError {
command: binary_name.to_string_lossy().into_owned(),
inner: err,
})
}

@ -2,6 +2,7 @@ pub use etcetera::home_dir;
use std::{
borrow::Cow,
ffi::OsString,
path::{Component, Path, PathBuf},
};
@ -9,14 +10,21 @@ use crate::env::current_working_dir;
/// Replaces users home directory from `path` with tilde `~` if the directory
/// is available, otherwise returns the path unchanged.
pub fn fold_home_dir(path: &Path) -> PathBuf {
pub fn fold_home_dir<'a, P>(path: P) -> Cow<'a, Path>
where
P: Into<Cow<'a, Path>>,
{
let path = path.into();
if let Ok(home) = home_dir() {
if let Ok(stripped) = path.strip_prefix(&home) {
return PathBuf::from("~").join(stripped);
let mut path = OsString::with_capacity(2 + stripped.as_os_str().len());
path.push("~/");
path.push(stripped);
return Cow::Owned(PathBuf::from(path));
}
}
path.to_path_buf()
path
}
/// Expands tilde `~` into users home directory if available, otherwise returns the path
@ -125,18 +133,21 @@ pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
normalize(path)
}
pub fn get_relative_path(path: impl AsRef<Path>) -> PathBuf {
let path = PathBuf::from(path.as_ref());
let path = if path.is_absolute() {
pub fn get_relative_path<'a, P>(path: P) -> Cow<'a, Path>
where
P: Into<Cow<'a, Path>>,
{
let path = path.into();
if path.is_absolute() {
let cwdir = normalize(current_working_dir());
normalize(&path)
.strip_prefix(cwdir)
.map(PathBuf::from)
.unwrap_or(path)
} else {
path
};
fold_home_dir(&path)
if let Ok(stripped) = normalize(&path).strip_prefix(cwdir) {
return Cow::Owned(PathBuf::from(stripped));
}
return fold_home_dir(path);
}
path
}
/// Returns a truncated filepath where the basepart of the path is reduced to the first
@ -170,21 +181,20 @@ pub fn get_relative_path(path: impl AsRef<Path>) -> PathBuf {
///
pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
let cwd = current_working_dir();
let path = path
.as_ref()
.strip_prefix(cwd)
.unwrap_or_else(|_| path.as_ref());
let path = path.as_ref();
let path = path.strip_prefix(cwd).unwrap_or(path);
let file = path.file_name().unwrap_or_default();
let base = path.parent().unwrap_or_else(|| Path::new(""));
let mut ret = PathBuf::new();
let mut ret = PathBuf::with_capacity(file.len());
// A char can't be directly pushed to a PathBuf
let mut first_char_buffer = String::new();
for d in base {
ret.push(
d.to_string_lossy()
.chars()
.next()
.unwrap_or_default()
.to_string(),
);
let Some(first_char) = d.to_string_lossy().chars().next() else {
break;
};
first_char_buffer.push(first_char);
ret.push(&first_char_buffer);
first_char_buffer.clear();
}
ret.push(file);
ret

@ -56,9 +56,10 @@ ignore = "0.4"
pulldown-cmark = { version = "0.10", default-features = false }
# file type detection
content_inspector = "0.2.4"
thiserror = "1.0"
# opening URLs
open = "5.0.1"
open = "5.1.2"
url = "2.5.0"
# config

File diff suppressed because it is too large Load Diff

@ -12,7 +12,7 @@ use helix_view::{editor::Breakpoint, graphics::Margin};
use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tui::{text::Spans, widgets::Row};
use tui::text::Spans;
use std::collections::HashMap;
use std::future::Future;
@ -22,38 +22,6 @@ use anyhow::{anyhow, bail};
use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
impl ui::menu::Item for StackFrame {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
self.name.as_str().into() // TODO: include thread_states in the label
}
}
impl ui::menu::Item for DebugTemplate {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
self.name.as_str().into()
}
}
impl ui::menu::Item for Thread {
type Data = ThreadStates;
fn format(&self, thread_states: &Self::Data) -> Row {
format!(
"{} ({})",
self.name,
thread_states
.get(&self.id)
.map(|state| state.as_str())
.unwrap_or("unknown")
)
.into()
}
}
fn thread_picker(
cx: &mut Context,
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
@ -73,9 +41,23 @@ fn thread_picker(
let debugger = debugger!(editor);
let thread_states = debugger.thread_states.clone();
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
callback_fn(cx.editor, thread)
})
let columns = vec![
ui::PickerColumn::new("name", |item: &Thread, _| item.name.as_str().into()),
ui::PickerColumn::new("state", |item: &Thread, thread_states: &ThreadStates| {
thread_states
.get(&item.id)
.map(|state| state.as_str())
.unwrap_or("unknown")
.into()
}),
];
let picker = Picker::new(
columns,
0,
threads,
thread_states,
move |cx, thread, _action| callback_fn(cx.editor, thread),
)
.with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frame = frames.first()?;
@ -268,7 +250,14 @@ pub fn dap_launch(cx: &mut Context) {
let templates = config.templates.clone();
let columns = vec![ui::PickerColumn::new(
"template",
|item: &DebugTemplate, _| item.name.as_str().into(),
)];
cx.push_layer(Box::new(overlaid(Picker::new(
columns,
0,
templates,
(),
|cx, template, _action| {
@ -735,7 +724,10 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let frames = debugger.stack_frames[&thread_id].clone();
let picker = Picker::new(frames, (), move |cx, frame, _action| {
let columns = vec![ui::PickerColumn::new("frame", |item: &StackFrame, _| {
item.name.as_str().into() // TODO: include thread_states in the label
})];
let picker = Picker::new(columns, 0, frames, (), move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find
let pos = debugger.stack_frames[&thread_id]

@ -9,10 +9,7 @@ use helix_lsp::{
Client, OffsetEncoding,
};
use tokio_stream::StreamExt;
use tui::{
text::{Span, Spans},
widgets::Row,
};
use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor};
@ -30,7 +27,7 @@ use helix_view::{
use crate::{
compositor::{self, Compositor},
job::Callback,
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
};
use std::{
@ -63,69 +60,11 @@ macro_rules! language_server_with_feature {
}};
}
impl ui::menu::Item for lsp::Location {
/// Current working directory.
type Data = PathBuf;
fn format(&self, cwdir: &Self::Data) -> Row {
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
// common case where it has 5 digits or less, which should be enough for a cast majority
// of usages).
let mut res = String::with_capacity(self.uri.as_str().len());
if self.uri.scheme() == "file" {
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
let mut write_path_to_res = || -> Option<()> {
let path = self.uri.to_file_path().ok()?;
res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy());
Some(())
};
write_path_to_res();
} else {
// Never allocates since we declared the string with this capacity already.
res.push_str(self.uri.as_str());
}
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter)
write!(&mut res, ":{}", self.range.start.line + 1)
.expect("Will only failed if allocating fail");
res.into()
}
}
struct SymbolInformationItem {
symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding,
}
impl ui::menu::Item for SymbolInformationItem {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
fn format(&self, current_doc_path: &Self::Data) -> Row {
if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
self.symbol.name.as_str().into()
} else {
match self.symbol.location.uri.to_file_path() {
Ok(path) => {
let get_relative_path = path::get_relative_path(path.as_path());
format!(
"{} ({})",
&self.symbol.name,
get_relative_path.to_string_lossy()
)
.into()
}
Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
}
}
}
}
struct DiagnosticStyles {
hint: Style,
info: Style,
@ -139,48 +78,6 @@ struct PickerDiagnostic {
offset_encoding: OffsetEncoding,
}
impl ui::menu::Item for PickerDiagnostic {
type Data = (DiagnosticStyles, DiagnosticsFormat);
fn format(&self, (styles, format): &Self::Data) -> Row {
let mut style = self
.diag
.severity
.map(|s| match s {
DiagnosticSeverity::HINT => styles.hint,
DiagnosticSeverity::INFORMATION => styles.info,
DiagnosticSeverity::WARNING => styles.warning,
DiagnosticSeverity::ERROR => styles.error,
_ => Style::default(),
})
.unwrap_or_default();
// remove background as it is distracting in the picker list
style.bg = None;
let code = match self.diag.code.as_ref() {
Some(NumberOrString::Number(n)) => format!(" ({n})"),
Some(NumberOrString::String(s)) => format!(" ({s})"),
None => String::new(),
};
let path = match format {
DiagnosticsFormat::HideSourcePath => String::new(),
DiagnosticsFormat::ShowSourcePath => {
let path = path::get_truncated_path(&self.path);
format!("{}: ", path.to_string_lossy())
}
};
Spans::from(vec![
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
.into()
}
}
fn location_to_file_location(location: &lsp::Location) -> FileLocation {
let path = location.uri.to_file_path().unwrap();
let line = Some((
@ -242,18 +139,91 @@ fn jump_to_position(
}
}
type SymbolPicker = Picker<SymbolInformationItem>;
fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
match kind {
lsp::SymbolKind::FILE => "file",
lsp::SymbolKind::MODULE => "module",
lsp::SymbolKind::NAMESPACE => "namespace",
lsp::SymbolKind::PACKAGE => "package",
lsp::SymbolKind::CLASS => "class",
lsp::SymbolKind::METHOD => "method",
lsp::SymbolKind::PROPERTY => "property",
lsp::SymbolKind::FIELD => "field",
lsp::SymbolKind::CONSTRUCTOR => "construct",
lsp::SymbolKind::ENUM => "enum",
lsp::SymbolKind::INTERFACE => "interface",
lsp::SymbolKind::FUNCTION => "function",
lsp::SymbolKind::VARIABLE => "variable",
lsp::SymbolKind::CONSTANT => "constant",
lsp::SymbolKind::STRING => "string",
lsp::SymbolKind::NUMBER => "number",
lsp::SymbolKind::BOOLEAN => "boolean",
lsp::SymbolKind::ARRAY => "array",
lsp::SymbolKind::OBJECT => "object",
lsp::SymbolKind::KEY => "key",
lsp::SymbolKind::NULL => "null",
lsp::SymbolKind::ENUM_MEMBER => "enummem",
lsp::SymbolKind::STRUCT => "struct",
lsp::SymbolKind::EVENT => "event",
lsp::SymbolKind::OPERATOR => "operator",
lsp::SymbolKind::TYPE_PARAMETER => "typeparam",
_ => {
log::warn!("Unknown symbol kind: {:?}", kind);
""
}
}
}
type SymbolPicker = Picker<SymbolInformationItem, ()>;
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
Picker::new(symbols, current_path, move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
})
fn sym_picker(symbols: Vec<SymbolInformationItem>, workspace: bool) -> SymbolPicker {
let symbol_kind_column = ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
display_symbol_kind(item.symbol.kind).into()
});
let columns = if workspace {
vec![
symbol_kind_column,
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
item.symbol.name.as_str().into()
})
.without_filtering(),
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
match item.symbol.location.uri.to_file_path() {
Ok(path) => path::get_relative_path(path.as_path())
.to_string_lossy()
.to_string()
.into(),
Err(_) => item.symbol.location.uri.to_string().into(),
}
}),
]
} else {
vec![
symbol_kind_column,
// Some symbols in the document symbol picker may have a URI that isn't
// the current file. It should be rare though, so we concatenate that
// URI in with the symbol name in this picker.
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
item.symbol.name.as_str().into()
}),
]
};
Picker::new(
columns,
1, // name column
symbols,
(),
move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
},
)
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
.truncate_start(false)
}
@ -264,11 +234,13 @@ enum DiagnosticsFormat {
HideSourcePath,
}
type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
fn diag_picker(
cx: &Context,
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, usize)>>,
format: DiagnosticsFormat,
) -> Picker<PickerDiagnostic> {
) -> DiagnosticsPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs
@ -294,9 +266,50 @@ fn diag_picker(
error: cx.editor.theme.get("error"),
};
let mut columns = vec![
ui::PickerColumn::new(
"severity",
|item: &PickerDiagnostic, styles: &DiagnosticStyles| {
match item.diag.severity {
Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint),
Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info),
Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning),
Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error),
_ => Span::raw(""),
}
.into()
},
),
ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| {
match item.diag.code.as_ref() {
Some(NumberOrString::Number(n)) => n.to_string().into(),
Some(NumberOrString::String(s)) => s.as_str().into(),
None => "".into(),
}
}),
ui::PickerColumn::new("message", |item: &PickerDiagnostic, _| {
item.diag.message.as_str().into()
}),
];
let mut primary_column = 2; // message
if format == DiagnosticsFormat::ShowSourcePath {
columns.insert(
// between message code and message
2,
ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
let path = path::get_truncated_path(&item.path);
path.to_string_lossy().to_string().into()
}),
);
primary_column += 1;
}
Picker::new(
columns,
primary_column,
flat_diag,
(styles, format),
styles,
move |cx,
PickerDiagnostic {
path,
@ -378,7 +391,6 @@ pub fn symbol_picker(cx: &mut Context) {
}
})
.collect();
let current_url = doc.url();
if futures.is_empty() {
cx.editor
@ -393,7 +405,7 @@ pub fn symbol_picker(cx: &mut Context) {
symbols.append(&mut lsp_items);
}
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = sym_picker(symbols, current_url);
let picker = sym_picker(symbols, false);
compositor.push(Box::new(overlaid(picker)))
};
@ -402,6 +414,8 @@ pub fn symbol_picker(cx: &mut Context) {
}
pub fn workspace_symbol_picker(cx: &mut Context) {
use crate::ui::picker::Injector;
let doc = doc!(cx.editor);
if doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
@ -413,19 +427,21 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
return;
}
let get_symbols = move |pattern: String, editor: &mut Editor| {
let get_symbols = |pattern: &str, editor: &mut Editor, _data, injector: &Injector<_, _>| {
let doc = doc!(editor);
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
let request = language_server.workspace_symbols(pattern.clone()).unwrap();
let request = language_server
.workspace_symbols(pattern.to_string())
.unwrap();
let offset_encoding = language_server.offset_encoding();
async move {
let json = request.await?;
let response =
let response: Vec<_> =
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
.unwrap_or_default()
.into_iter()
@ -444,30 +460,56 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
editor.set_error("No configured language server supports workspace symbols");
}
let injector = injector.clone();
async move {
let mut symbols = Vec::new();
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
while let Some(mut lsp_items) = futures.try_next().await? {
symbols.append(&mut lsp_items);
while let Some(lsp_items) = futures.try_next().await? {
for item in lsp_items {
injector.push(item)?;
}
}
anyhow::Ok(symbols)
Ok(())
}
.boxed()
};
let columns = vec![
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
display_symbol_kind(item.symbol.kind).into()
}),
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
item.symbol.name.as_str().into()
})
.without_filtering(),
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
match item.symbol.location.uri.to_file_path() {
Ok(path) => path::get_relative_path(path.as_path())
.to_string_lossy()
.to_string()
.into(),
Err(_) => item.symbol.location.uri.to_string().into(),
}
}),
];
let picker = Picker::new(
columns,
1, // name column
vec![],
(),
move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
},
)
.with_preview(|_editor, item| Some(location_to_file_location(&item.symbol.location)))
.with_dynamic_query(get_symbols, None)
.truncate_start(false);
let current_url = doc.url();
let initial_symbols = get_symbols("".to_owned(), cx.editor);
cx.jobs.callback(async move {
let symbols = initial_symbols.await?;
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = sym_picker(symbols, current_url);
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlaid(dyn_picker)))
};
Ok(Callback::EditorCompositor(Box::new(call)))
});
cx.push_layer(Box::new(overlaid(picker)));
}
pub fn diagnostics_picker(cx: &mut Context) {
@ -750,13 +792,6 @@ pub fn code_action(cx: &mut Context) {
});
}
impl ui::menu::Item for lsp::Command {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
self.title.as_str().into()
}
}
pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
@ -822,7 +857,38 @@ fn goto_impl(
}
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
let columns = vec![ui::PickerColumn::new(
"location",
|item: &lsp::Location, cwdir: &std::path::PathBuf| {
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
// common case where it has 5 digits or less, which should be enough for a cast majority
// of usages).
let mut res = String::with_capacity(item.uri.as_str().len());
if item.uri.scheme() == "file" {
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
if let Ok(path) = item.uri.to_file_path() {
res.push_str(
&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(),
);
}
} else {
// Never allocates since we declared the string with this capacity already.
res.push_str(item.uri.as_str());
}
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter)
write!(&mut res, ":{}", item.range.start.line + 1)
.expect("Will only failed if allocating fail");
res.into()
},
)];
let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
})
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
@ -1315,11 +1381,11 @@ fn compute_inlay_hints_for_view(
view_id,
DocumentInlayHints {
id: new_doc_inlay_hints_id,
type_inlay_hints: type_inlay_hints.into(),
parameter_inlay_hints: parameter_inlay_hints.into(),
other_inlay_hints: other_inlay_hints.into(),
padding_before_inlay_hints: padding_before_inlay_hints.into(),
padding_after_inlay_hints: padding_after_inlay_hints.into(),
type_inlay_hints,
parameter_inlay_hints,
other_inlay_hints,
padding_before_inlay_hints,
padding_after_inlay_hints,
},
);
doc.inlay_hints_oudated = false;

@ -1318,7 +1318,11 @@ fn reload_all(
// Ensure that the view is synced with the document's history.
view.sync_changes(doc);
doc.reload(view, &cx.editor.diff_providers)?;
if let Err(error) = doc.reload(view, &cx.editor.diff_providers) {
cx.editor.set_error(format!("{}", error));
continue;
}
if let Some(path) = doc.path() {
cx.editor
.language_servers
@ -1391,9 +1395,14 @@ fn lsp_workspace_command(
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, language_server_id, command.clone());
});
let columns = vec![ui::PickerColumn::new(
"title",
|command: &helix_lsp::lsp::Command, _| command.title.as_str().into(),
)];
let picker =
ui::Picker::new(columns, 0, commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, language_server_id, command.clone());
});
compositor.push(Box::new(overlaid(picker)))
},
));
@ -2257,7 +2266,7 @@ fn run_shell_command(
let args = args.join(" ");
let callback = async move {
let (output, success) = shell_impl_async(&shell, &args, None).await?;
let output = shell_impl_async(&shell, &args, None).await?;
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
if !output.is_empty() {
@ -2270,11 +2279,7 @@ fn run_shell_command(
));
compositor.replace_or_push("shell", popup);
}
if success {
editor.set_status("Command succeeded");
} else {
editor.set_error("Command failed");
}
editor.set_status("Command succeeded");
},
));
Ok(call)
@ -2414,6 +2419,46 @@ fn move_buffer(
Ok(())
}
fn yank_diagnostic(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let reg = match args.first() {
Some(s) => {
ensure!(s.chars().count() == 1, format!("Invalid register {s}"));
s.chars().next().unwrap()
}
None => '+',
};
let (view, doc) = current_ref!(cx.editor);
let primary = doc.selection(view.id).primary();
// Look only for diagnostics that intersect with the primary selection
let diag: Vec<_> = doc
.diagnostics()
.iter()
.filter(|d| primary.overlaps(&helix_core::Range::new(d.range.start, d.range.end)))
.map(|d| d.message.clone())
.collect();
let n = diag.len();
if n == 0 {
bail!("No diagnostics under primary selection");
}
cx.editor.registers.write(reg, diag)?;
cx.editor.set_status(format!(
"Yanked {n} diagnostic{} to register {reg}",
if n == 1 { "" } else { "s" }
));
Ok(())
}
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@ -3005,7 +3050,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
aliases: &[],
doc: "Clear given register. If no argument is provided, clear all registers.",
fun: clear_register,
signature: CommandSignature::none(),
signature: CommandSignature::all(completers::register),
},
TypableCommand {
name: "redraw",
@ -3021,6 +3066,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: move_buffer,
signature: CommandSignature::positional(&[completers::filename]),
},
TypableCommand {
name: "yank-diagnostic",
aliases: &[],
doc: "Yank diagnostic(s) under primary cursor to register, or clipboard by default",
fun: yank_diagnostic,
signature: CommandSignature::all(completers::register),
},
];
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =

@ -58,6 +58,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"k" => move_line_up,
"j" => move_line_down,
"." => goto_last_modification,
"w" => goto_word,
},
":" => command_mode,
@ -360,6 +361,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"g" => { "Goto"
"k" => extend_line_up,
"j" => extend_line_down,
"w" => extend_to_word,
},
}));
let insert = keymap!({ "Insert mode"

@ -48,10 +48,13 @@ fn true_color() -> bool {
/// Function used for filtering dir entries in the various file pickers.
fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> bool {
// We always want to ignore the .git directory, otherwise if
// We always want to ignore popular VCS directories, otherwise if
// `ignore` is turned off, we end up with a lot of noise
// in our picker.
if entry.file_name() == ".git" {
if matches!(
entry.file_name().to_str(),
Some(".git" | ".pijul" | ".jj" | ".hg")
) {
return false;
}

@ -916,13 +916,15 @@ impl EditorView {
fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
match (event, cxt.editor.count) {
// count handling
(key!(i @ '0'), Some(_)) | (key!(i @ '1'..='9'), _)
if !self.keymaps.contains_key(mode, event) =>
{
// If the count is already started and the input is a number, always continue the count.
(key!(i @ '0'..='9'), Some(count)) => {
let i = i.to_digit(10).unwrap() as usize;
cxt.editor.count = NonZeroUsize::new(count.get() * 10 + i);
}
// A non-zero digit will start the count if that number isn't used by a keymap.
(key!(i @ '1'..='9'), None) if !self.keymaps.contains_key(mode, event) => {
let i = i.to_digit(10).unwrap() as usize;
cxt.editor.count =
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
cxt.editor.count = NonZeroUsize::new(i);
}
// special handling for repeat operator
(key!('.'), _) if self.keymaps.pending().is_empty() => {
@ -1046,13 +1048,33 @@ impl EditorView {
}
impl EditorView {
/// must be called whenever the editor processed input that
/// is not a `KeyEvent`. In these cases any pending keys/on next
/// key callbacks must be canceled.
fn handle_non_key_input(&mut self, cxt: &mut commands::Context) {
cxt.editor.status_msg = None;
cxt.editor.reset_idle_timer();
// HACKS: create a fake key event that will never trigger any actual map
// and therefore simply acts as "dismiss"
let null_key_event = KeyEvent {
code: KeyCode::Null,
modifiers: KeyModifiers::empty(),
};
// dismiss any pending keys
if let Some(on_next_key) = self.on_next_key.take() {
on_next_key(cxt, null_key_event);
}
self.handle_keymap_event(cxt.editor.mode, cxt, null_key_event);
self.pseudo_pending.clear();
}
fn handle_mouse_event(
&mut self,
event: &MouseEvent,
cxt: &mut commands::Context,
) -> EventResult {
if event.kind != MouseEventKind::Moved {
cxt.editor.reset_idle_timer();
self.handle_non_key_input(cxt)
}
let config = cxt.editor.config();
@ -1277,6 +1299,7 @@ impl Component for EditorView {
match event {
Event::Paste(contents) => {
self.handle_non_key_input(&mut cx);
cx.count = cx.editor.count;
commands::paste_bracketed_value(&mut cx, contents.clone());
cx.editor.count = None;

@ -1,11 +1,11 @@
use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
use std::{borrow::Cow, cmp::Reverse};
use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
};
use helix_core::fuzzy::MATCHER;
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
use nucleo::{Config, Utf32Str};
use tui::{
buffer::Buffer as Surface,
@ -38,18 +38,6 @@ pub trait Item: Sync + Send + 'static {
}
}
impl Item for PathBuf {
/// Root prefix to strip.
type Data = PathBuf;
fn format(&self, root_path: &Self::Data) -> Row {
self.strip_prefix(root_path)
.unwrap_or(self)
.to_string_lossy()
.into()
}
}
pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
pub struct Menu<T: Item> {
@ -99,7 +87,13 @@ impl<T: Item> Menu<T> {
pub fn score(&mut self, pattern: &str, incremental: bool) {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
let pattern = Atom::new(
pattern,
CaseMatching::Ignore,
Normalization::Smart,
AtomKind::Fuzzy,
false,
);
let mut buf = Vec::new();
if incremental {
self.matches.retain_mut(|(index, score)| {

@ -21,7 +21,7 @@ pub use editor::EditorView;
use helix_stdx::rope;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, Picker};
pub use picker::{Column as PickerColumn, FileLocation, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
@ -172,7 +172,9 @@ pub fn raw_regex_prompt(
cx.push_layer(Box::new(prompt));
}
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
type FilePicker = Picker<PathBuf, PathBuf>;
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant;
@ -219,16 +221,31 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
});
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
} else {
format!("unable to open \"{}\"", path.display())
};
cx.editor.set_error(err);
}
})
let columns = vec![PickerColumn::new(
"path",
|item: &PathBuf, root: &PathBuf| {
item.strip_prefix(root)
.unwrap_or(item)
.to_string_lossy()
.into()
},
)];
let picker = Picker::new(
columns,
0,
Vec::new(),
root,
move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
} else {
format!("unable to open \"{}\"", path.display())
};
cx.editor.set_error(err);
}
},
)
.with_preview(|_editor, path| Some((path.clone().into(), None)));
let injector = picker.injector();
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
@ -509,4 +526,18 @@ pub mod completers {
files
}
}
pub fn register(editor: &Editor, input: &str) -> Vec<Completion> {
let iter = editor
.registers
.iter_preview()
// Exclude special registers that shouldn't be written to
.filter(|(ch, _)| !matches!(ch, '%' | '#' | '.'))
.map(|(ch, _)| ch.to_string());
fuzzy_match(input, iter, false)
.into_iter()
.map(|(name, _)| ((0..), name.into()))
.collect()
}
}

@ -1,33 +1,38 @@
mod handlers;
mod query;
use crate::{
alt,
compositor::{self, Component, Compositor, Context, Event, EventResult},
ctrl,
job::Callback,
key, shift,
ctrl, key, shift,
ui::{
self,
document::{render_document, LineDecoration, LinePos, TextRenderer},
EditorView,
},
};
use futures_util::{future::BoxFuture, FutureExt};
use nucleo::pattern::CaseMatching;
use futures_util::future::BoxFuture;
use helix_event::AsyncHook;
use nucleo::pattern::{CaseMatching, Normalization};
use nucleo::{Config, Nucleo, Utf32String};
use thiserror::Error;
use tokio::sync::mpsc::Sender;
use tui::{
buffer::Buffer as Surface,
layout::Constraint,
text::{Span, Spans},
widgets::{Block, BorderType, Borders, Cell, Table},
widgets::{Block, BorderType, Borders, Cell, Row, Table},
};
use tui::widgets::Widget;
use std::{
borrow::Cow,
collections::HashMap,
io::Read,
path::PathBuf,
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
atomic::{self, AtomicUsize},
Arc,
},
};
@ -36,7 +41,6 @@ use crate::ui::{Prompt, PromptEvent};
use helix_core::{
char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
Syntax,
};
use helix_view::{
editor::Action,
@ -46,8 +50,9 @@ use helix_view::{
Document, DocumentId, Editor,
};
use self::handlers::{DynamicQueryHandler, PreviewHighlightHandler};
pub const ID: &str = "picker";
use super::{menu::Item, overlay::Overlay};
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes
@ -56,14 +61,14 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
#[derive(PartialEq, Eq, Hash)]
pub enum PathOrId {
Id(DocumentId),
Path(PathBuf),
Path(Arc<Path>),
}
impl PathOrId {
fn get_canonicalized(self) -> Self {
use PathOrId::*;
match self {
Path(path) => Path(helix_stdx::path::canonicalize(path)),
Path(path) => Path(helix_stdx::path::canonicalize(&path).into()),
Id(id) => Id(id),
}
}
@ -71,7 +76,7 @@ impl PathOrId {
impl From<PathBuf> for PathOrId {
fn from(v: PathBuf) -> Self {
Self::Path(v)
Self::Path(v.as_path().into())
}
}
@ -123,62 +128,119 @@ impl Preview<'_, '_> {
}
}
fn item_to_nucleo<T: Item>(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> {
let row = item.format(editor_data);
let mut cells = row.cells.iter();
let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum());
let cell = cells.next()?;
if let Some(cell) = cell.content.lines.first() {
for span in &cell.0 {
text.push_str(&span.content);
fn inject_nucleo_item<T, D>(
injector: &nucleo::Injector<T>,
columns: &[Column<T, D>],
item: T,
editor_data: &D,
) {
let column_texts: Vec<Utf32String> = columns
.iter()
.filter(|column| column.filter)
.map(|column| column.format_text(&item, editor_data).into())
.collect();
injector.push(item, |dst| {
for (i, text) in column_texts.into_iter().enumerate() {
dst[i] = text;
}
}
for cell in cells {
text.push(' ');
if let Some(cell) = cell.content.lines.first() {
for span in &cell.0 {
text.push_str(&span.content);
}
}
}
Some((item, text.into()))
});
}
pub struct Injector<T: Item> {
pub struct Injector<T, D> {
dst: nucleo::Injector<T>,
editor_data: Arc<T::Data>,
shutown: Arc<AtomicBool>,
columns: Arc<[Column<T, D>]>,
editor_data: Arc<D>,
version: usize,
picker_version: Arc<AtomicUsize>,
}
impl<T: Item> Clone for Injector<T> {
impl<I, D> Clone for Injector<I, D> {
fn clone(&self) -> Self {
Injector {
dst: self.dst.clone(),
columns: self.columns.clone(),
editor_data: self.editor_data.clone(),
shutown: self.shutown.clone(),
version: self.version,
picker_version: self.picker_version.clone(),
}
}
}
#[derive(Error, Debug)]
#[error("picker has been shut down")]
pub struct InjectorShutdown;
impl<T: Item> Injector<T> {
impl<T, D> Injector<T, D> {
pub fn push(&self, item: T) -> Result<(), InjectorShutdown> {
if self.shutown.load(atomic::Ordering::Relaxed) {
if self.version != self.picker_version.load(atomic::Ordering::Relaxed) {
return Err(InjectorShutdown);
}
if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
self.dst.push(item, |dst| dst[0] = matcher_text);
}
inject_nucleo_item(&self.dst, &self.columns, item, &self.editor_data);
Ok(())
}
}
pub struct Picker<T: Item> {
editor_data: Arc<T::Data>,
shutdown: Arc<AtomicBool>,
type ColumnFormatFn<T, D> = for<'a> fn(&'a T, &'a D) -> Cell<'a>;
pub struct Column<T, D> {
name: String,
format: ColumnFormatFn<T, D>,
/// Whether the column should be passed to nucleo for matching and filtering.
/// `DynamicPicker` uses this so that the dynamic column (for example regex in
/// global search) is not used for filtering twice.
filter: bool,
hidden: bool,
}
impl<T, D> Column<T, D> {
pub fn new(name: impl Into<String>, format: ColumnFormatFn<T, D>) -> Self {
Self {
name: name.into(),
format,
filter: true,
hidden: false,
}
}
/// A column which does not display any contents
pub fn hidden(name: impl Into<String>) -> Self {
let format = |_: &T, _: &D| unreachable!();
Self {
name: name.into(),
format,
filter: false,
hidden: true,
}
}
pub fn without_filtering(mut self) -> Self {
self.filter = false;
self
}
fn format<'a>(&self, item: &'a T, data: &'a D) -> Cell<'a> {
(self.format)(item, data)
}
fn format_text<'a>(&self, item: &'a T, data: &'a D) -> Cow<'a, str> {
let text: String = self.format(item, data).content.into();
text.into()
}
}
/// Returns a new list of options to replace the contents of the picker
/// when called with the current picker query,
type DynQueryCallback<T, D> =
fn(&str, &mut Editor, Arc<D>, &Injector<T, D>) -> BoxFuture<'static, anyhow::Result<()>>;
pub struct Picker<T: 'static + Send + Sync, D: 'static> {
column_names: Arc<[Arc<str>]>,
columns: Arc<[Column<T, D>]>,
primary_column: usize,
editor_data: Arc<D>,
version: Arc<AtomicUsize>,
matcher: Nucleo<T>,
/// Current height of the completions box
@ -186,7 +248,7 @@ pub struct Picker<T: Item> {
cursor: u32,
prompt: Prompt,
previous_pattern: String,
query: query::PickerQuery,
/// Whether to show the preview panel (default true)
show_preview: bool,
@ -197,67 +259,90 @@ pub struct Picker<T: Item> {
pub truncate_start: bool,
/// Caches paths to documents
preview_cache: HashMap<PathBuf, CachedPreview>,
preview_cache: HashMap<Arc<Path>, CachedPreview>,
read_buffer: Vec<u8>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Option<FileCallback<T>>,
/// An event handler for syntax highlighting the currently previewed file.
preview_highlight_handler: Sender<Arc<Path>>,
dynamic_query_handler: Option<Sender<Arc<str>>>,
}
impl<T: Item + 'static> Picker<T> {
pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
pub fn stream(columns: Vec<Column<T, D>>, editor_data: D) -> (Nucleo<T>, Injector<T, D>) {
let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
assert!(matcher_columns > 0);
let matcher = Nucleo::new(
Config::DEFAULT,
Arc::new(helix_event::request_redraw),
None,
1,
matcher_columns,
);
let streamer = Injector {
dst: matcher.injector(),
columns: columns.into(),
editor_data: Arc::new(editor_data),
shutown: Arc::new(AtomicBool::new(false)),
version: 0,
picker_version: Arc::new(AtomicUsize::new(0)),
};
(matcher, streamer)
}
pub fn new(
columns: Vec<Column<T, D>>,
primary_column: usize,
options: Vec<T>,
editor_data: T::Data,
editor_data: D,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
assert!(matcher_columns > 0);
let matcher = Nucleo::new(
Config::DEFAULT,
Arc::new(helix_event::request_redraw),
None,
1,
matcher_columns,
);
let injector = matcher.injector();
for item in options {
if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) {
injector.push(item, |dst| dst[0] = matcher_text);
}
inject_nucleo_item(&injector, &columns, item, &editor_data);
}
Self::with(
matcher,
columns.into(),
primary_column,
Arc::new(editor_data),
Arc::new(AtomicBool::new(false)),
Arc::new(AtomicUsize::new(0)),
callback_fn,
)
}
pub fn with_stream(
matcher: Nucleo<T>,
injector: Injector<T>,
primary_column: usize,
injector: Injector<T, D>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
Self::with(
matcher,
injector.columns,
primary_column,
injector.editor_data,
injector.picker_version,
callback_fn,
)
}
fn with(
matcher: Nucleo<T>,
editor_data: Arc<T::Data>,
shutdown: Arc<AtomicBool>,
columns: Arc<[Column<T, D>]>,
default_column: usize,
editor_data: Arc<D>,
version: Arc<AtomicUsize>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
assert!(!columns.is_empty());
let prompt = Prompt::new(
"".into(),
None,
@ -265,29 +350,45 @@ impl<T: Item + 'static> Picker<T> {
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
);
let column_names: Arc<[_]> = columns
.iter()
.map(|column| column.name.as_str().into())
.collect();
let widths = columns
.iter()
.map(|column| Constraint::Length(column.name.chars().count() as u16))
.collect();
Self {
column_names,
columns,
primary_column: default_column,
matcher,
editor_data,
shutdown,
version,
cursor: 0,
prompt,
previous_pattern: String::new(),
query: query::PickerQuery::default(),
truncate_start: true,
show_preview: true,
callback_fn: Box::new(callback_fn),
completion_height: 0,
widths: Vec::new(),
widths,
preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: None,
preview_highlight_handler: PreviewHighlightHandler::<T, D>::default().spawn(),
dynamic_query_handler: None,
}
}
pub fn injector(&self) -> Injector<T> {
pub fn injector(&self) -> Injector<T, D> {
Injector {
dst: self.matcher.injector(),
columns: self.columns.clone(),
editor_data: self.editor_data.clone(),
shutown: self.shutdown.clone(),
version: self.version.load(atomic::Ordering::Relaxed),
picker_version: self.version.clone(),
}
}
@ -307,14 +408,20 @@ impl<T: Item + 'static> Picker<T> {
self
}
pub fn set_options(&mut self, new_options: Vec<T>) {
self.matcher.restart(false);
let injector = self.matcher.injector();
for item in new_options {
if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
injector.push(item, |dst| dst[0] = matcher_text);
}
}
pub fn with_history_register(mut self, history_register: Option<char>) -> Self {
self.prompt.with_history_register(history_register);
self
}
pub fn with_dynamic_query(
mut self,
callback: DynQueryCallback<T, D>,
debounce_ms: Option<u64>,
) -> Self {
let handler = DynamicQueryHandler::new(callback, debounce_ms).spawn();
helix_event::send_blocking(&handler, self.primary_query());
self.dynamic_query_handler = Some(handler);
self
}
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
@ -367,25 +474,66 @@ impl<T: Item + 'static> Picker<T> {
.map(|item| item.data)
}
fn primary_query(&self) -> Arc<str> {
self.query
.get(&self.column_names[self.primary_column])
.cloned()
.unwrap_or_else(|| "".into())
}
fn header_height(&self) -> u16 {
if self.columns.len() > 1 {
1
} else {
0
}
}
pub fn toggle_preview(&mut self) {
self.show_preview = !self.show_preview;
}
fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
let pattern = self.prompt.line();
// TODO: better track how the pattern has changed
if pattern != &self.previous_pattern {
self.handle_prompt_change();
}
EventResult::Consumed(None)
}
fn handle_prompt_change(&mut self) {
// TODO: better track how the pattern has changed
let line = self.prompt.line();
let new_query = query::parse(&self.column_names, self.primary_column, line);
if new_query != self.query {
for (i, column) in self
.columns
.iter()
.filter(|column| column.filter)
.enumerate()
{
let pattern: &str = new_query
.get(column.name.as_str())
.map(|f| &**f)
.unwrap_or("");
let append = self
.query
.get(column.name.as_str())
.map(|old_pattern| pattern.starts_with(&**old_pattern))
.unwrap_or(false);
self.matcher.pattern.reparse(
0,
i,
pattern,
CaseMatching::Smart,
pattern.starts_with(&self.previous_pattern),
Normalization::Smart,
append,
);
self.previous_pattern = pattern.clone();
}
self.query = new_query;
if let Some(handler) = &self.dynamic_query_handler {
helix_event::send_blocking(handler, self.primary_query());
}
}
EventResult::Consumed(None)
}
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
@ -403,16 +551,26 @@ impl<T: Item + 'static> Picker<T> {
) -> Preview<'picker, 'editor> {
match path_or_id {
PathOrId::Path(path) => {
let path = &path;
if let Some(doc) = editor.document_by_path(path) {
if let Some(doc) = editor.document_by_path(&path) {
return Preview::EditorDocument(doc);
}
if self.preview_cache.contains_key(path) {
return Preview::Cached(&self.preview_cache[path]);
if self.preview_cache.contains_key(&path) {
let preview = &self.preview_cache[&path];
match preview {
// If the document isn't highlighted yet, attempt to highlight it.
CachedPreview::Document(doc) if doc.language_config().is_none() => {
helix_event::send_blocking(
&self.preview_highlight_handler,
path.clone(),
);
}
_ => (),
}
return Preview::Cached(preview);
}
let data = std::fs::File::open(path).and_then(|file| {
let data = std::fs::File::open(&path).and_then(|file| {
let metadata = file.metadata()?;
// Read up to 1kb to detect the content type
let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
@ -427,14 +585,21 @@ impl<T: Item + 'static> Picker<T> {
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
CachedPreview::LargeFile
}
_ => Document::open(path, None, None, editor.config.clone())
.map(|doc| CachedPreview::Document(Box::new(doc)))
_ => Document::open(&path, None, None, editor.config.clone())
.map(|doc| {
// Asynchronously highlight the new document
helix_event::send_blocking(
&self.preview_highlight_handler,
path.clone(),
);
CachedPreview::Document(Box::new(doc))
})
.unwrap_or(CachedPreview::NotFound),
},
)
.unwrap_or(CachedPreview::NotFound);
self.preview_cache.insert(path.to_owned(), preview);
Preview::Cached(&self.preview_cache[path])
self.preview_cache.insert(path.clone(), preview);
Preview::Cached(&self.preview_cache[&path])
}
PathOrId::Id(id) => {
let doc = editor.documents.get(&id).unwrap();
@ -443,84 +608,6 @@ impl<T: Item + 'static> Picker<T> {
}
}
fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
let Some((current_file, _)) = self.current_file(cx.editor) else {
return EventResult::Consumed(None);
};
// Try to find a document in the cache
let doc = match &current_file {
PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
Some(CachedPreview::Document(ref mut doc)) => doc,
_ => return EventResult::Consumed(None),
},
};
let mut callback: Option<compositor::Callback> = None;
// Then attempt to highlight it if it has no language set
if doc.language_config().is_none() {
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader.load())
{
doc.language = Some(language_config.clone());
let text = doc.text().clone();
let loader = cx.editor.syn_loader.clone();
let job = tokio::task::spawn_blocking(move || {
let syntax = language_config
.highlight_config(&loader.load().scopes())
.and_then(|highlight_config| {
Syntax::new(text.slice(..), highlight_config, loader)
});
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
let Some(syntax) = syntax else {
log::info!("highlighting picker item failed");
return;
};
let picker = match compositor.find::<Overlay<Self>>() {
Some(Overlay { content, .. }) => Some(content),
None => compositor
.find::<Overlay<DynamicPicker<T>>>()
.map(|overlay| &mut overlay.content.file_picker),
};
let Some(picker) = picker else {
log::info!("picker closed before syntax highlighting finished");
return;
};
// Try to find a document in the cache
let doc = match current_file {
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
Some(CachedPreview::Document(ref mut doc)) => {
let diagnostics = Editor::doc_diagnostics(
&editor.language_servers,
&editor.diagnostics,
doc,
);
doc.replace_diagnostics(diagnostics, &[], None);
doc
}
_ => return,
},
};
doc.syntax = Some(syntax);
};
Callback::EditorCompositor(Box::new(callback))
});
let tmp: compositor::Callback = Box::new(move |_, ctx| {
ctx.jobs
.callback(job.map(|res| res.map_err(anyhow::Error::from)))
});
callback = Some(Box::new(tmp))
}
}
// QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
// but it could be interesting in the future
EventResult::Consumed(callback)
}
fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let status = self.matcher.tick(10);
let snapshot = self.matcher.snapshot();
@ -555,7 +642,11 @@ impl<T: Item + 'static> Picker<T> {
let count = format!(
"{}{}/{}",
if status.running { "(running) " } else { "" },
if status.running || self.matcher.active_injectors() > 0 {
"(running) "
} else {
""
},
snapshot.matched_item_count(),
snapshot.item_count(),
);
@ -579,7 +670,7 @@ impl<T: Item + 'static> Picker<T> {
// -- Render the contents:
// subtract area of prompt from top
let inner = inner.clip_top(2);
let rows = inner.height as u32;
let rows = inner.height.saturating_sub(self.header_height()) as u32;
let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
let cursor = self.cursor.saturating_sub(offset);
let end = offset
@ -593,83 +684,102 @@ impl<T: Item + 'static> Picker<T> {
}
let options = snapshot.matched_items(offset..end).map(|item| {
snapshot.pattern().column_pattern(0).indices(
item.matcher_columns[0].slice(..),
&mut matcher,
&mut indices,
);
indices.sort_unstable();
indices.dedup();
let mut row = item.data.format(&self.editor_data);
let mut grapheme_idx = 0u32;
let mut indices = indices.drain(..);
let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
if self.widths.len() < row.cells.len() {
self.widths.resize(row.cells.len(), Constraint::Length(0));
}
let mut widths = self.widths.iter_mut();
for cell in &mut row.cells {
let mut matcher_index = 0;
Row::new(self.columns.iter().map(|column| {
if column.hidden {
return Cell::default();
}
let Some(Constraint::Length(max_width)) = widths.next() else {
unreachable!();
};
// merge index highlights on top of existing hightlights
let mut span_list = Vec::new();
let mut current_span = String::new();
let mut current_style = Style::default();
let mut width = 0;
let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
for span in spans {
// this looks like a bug on first glance, we are iterating
// graphemes but treating them as char indices. The reason that
// this is correct is that nucleo will only ever consider the first char
// of a grapheme (and discard the rest of the grapheme) so the indices
// returned by nucleo are essentially grapheme indecies
for grapheme in span.content.graphemes(true) {
let style = if grapheme_idx == next_highlight_idx {
next_highlight_idx = indices.next().unwrap_or(u32::MAX);
span.style.patch(highlight_style)
} else {
span.style
};
if style != current_style {
if !current_span.is_empty() {
span_list.push(Span::styled(current_span, current_style))
let mut cell = column.format(item.data, &self.editor_data);
let width = if column.filter {
snapshot.pattern().column_pattern(matcher_index).indices(
item.matcher_columns[matcher_index].slice(..),
&mut matcher,
&mut indices,
);
indices.sort_unstable();
indices.dedup();
let mut indices = indices.drain(..);
let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
let mut span_list = Vec::new();
let mut current_span = String::new();
let mut current_style = Style::default();
let mut grapheme_idx = 0u32;
let mut width = 0;
let spans: &[Span] =
cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
for span in spans {
// this looks like a bug on first glance, we are iterating
// graphemes but treating them as char indices. The reason that
// this is correct is that nucleo will only ever consider the first char
// of a grapheme (and discard the rest of the grapheme) so the indices
// returned by nucleo are essentially grapheme indecies
for grapheme in span.content.graphemes(true) {
let style = if grapheme_idx == next_highlight_idx {
next_highlight_idx = indices.next().unwrap_or(u32::MAX);
span.style.patch(highlight_style)
} else {
span.style
};
if style != current_style {
if !current_span.is_empty() {
span_list.push(Span::styled(current_span, current_style))
}
current_span = String::new();
current_style = style;
}
current_span = String::new();
current_style = style;
current_span.push_str(grapheme);
grapheme_idx += 1;
}
current_span.push_str(grapheme);
grapheme_idx += 1;
width += span.width();
}
width += span.width();
}
span_list.push(Span::styled(current_span, current_style));
span_list.push(Span::styled(current_span, current_style));
cell = Cell::from(Spans::from(span_list));
matcher_index += 1;
width
} else {
cell.content
.lines
.first()
.map(|line| line.width())
.unwrap_or_default()
};
if width as u16 > *max_width {
*max_width = width as u16;
}
*cell = Cell::from(Spans::from(span_list));
// spacer
if grapheme_idx == next_highlight_idx {
next_highlight_idx = indices.next().unwrap_or(u32::MAX);
}
grapheme_idx += 1;
}
row
cell
}))
});
let table = Table::new(options)
let mut table = Table::new(options)
.style(text_style)
.highlight_style(selected)
.highlight_symbol(" > ")
.column_spacing(1)
.widths(&self.widths);
// -- Header
if self.columns.len() > 1 {
let header_style = cx.editor.theme.get("ui.picker.header");
table = table.header(Row::new(self.columns.iter().map(|column| {
if column.hidden {
Cell::default()
} else {
Cell::from(Span::styled(column.name.as_str(), header_style))
}
})));
}
use tui::widgets::TableState;
table.render_table(
@ -800,7 +910,7 @@ impl<T: Item + 'static> Picker<T> {
}
}
impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I, D> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+
// |prompt | |preview |
@ -828,9 +938,6 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
}
fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
if let Event::IdleTimeout = event {
return self.handle_idle_timeout(ctx);
}
// TODO: keybinds for scrolling preview
let key_event = match event {
@ -854,7 +961,7 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
// be restarting the stream somehow once the picker gets
// reopened instead (like for an FS crawl) that would also remove the
// need for the special case above but that is pretty tricky
picker.shutdown.store(true, atomic::Ordering::Relaxed);
picker.version.fetch_add(1, atomic::Ordering::Relaxed);
Box::new(|compositor: &mut Compositor, _ctx| {
// remove the layer
compositor.last_picker = compositor.pop();
@ -863,9 +970,6 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
EventResult::Consumed(Some(callback))
};
// So that idle timeout retriggers
ctx.editor.reset_idle_timer();
match key_event {
shift!(Tab) | key!(Up) | ctrl!('p') => {
self.move_by(1, Direction::Backward);
@ -892,10 +996,21 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
}
}
key!(Enter) => {
if let Some(option) = self.selection() {
(self.callback_fn)(ctx, option, Action::Replace);
// If the prompt has a history completion and is empty, use enter to accept
// that completion
if let Some(completion) = self
.prompt
.first_history_completion(ctx.editor)
.filter(|_| self.prompt.line().is_empty())
{
self.prompt.set_line(completion.to_string(), ctx.editor);
self.handle_prompt_change();
} else {
if let Some(option) = self.selection() {
(self.callback_fn)(ctx, option, Action::Replace);
}
return close_fn(self);
}
return close_fn(self);
}
ctrl!('s') => {
if let Some(option) = self.selection() {
@ -932,7 +1047,7 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
}
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
self.completion_height = height.saturating_sub(4);
self.completion_height = height.saturating_sub(4 + self.header_height());
Some((width, height))
}
@ -940,81 +1055,11 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
Some(ID)
}
}
impl<T: Item> Drop for Picker<T> {
impl<T: 'static + Send + Sync, D> Drop for Picker<T, D> {
fn drop(&mut self) {
// ensure we cancel any ongoing background threads streaming into the picker
self.shutdown.store(true, atomic::Ordering::Relaxed)
self.version.fetch_add(1, atomic::Ordering::Relaxed);
}
}
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
/// Returns a new list of options to replace the contents of the picker
/// when called with the current picker query,
pub type DynQueryCallback<T> =
Box<dyn Fn(String, &mut Editor) -> BoxFuture<'static, anyhow::Result<Vec<T>>>>;
/// A picker that updates its contents via a callback whenever the
/// query string changes. Useful for live grep, workspace symbols, etc.
pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
file_picker: Picker<T>,
query_callback: DynQueryCallback<T>,
query: String,
}
impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
Self {
file_picker,
query_callback,
query: String::new(),
}
}
}
impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.file_picker.render(area, surface, cx);
}
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
let event_result = self.file_picker.handle_event(event, cx);
let current_query = self.file_picker.prompt.line();
if !matches!(event, Event::IdleTimeout) || self.query == *current_query {
return event_result;
}
self.query.clone_from(current_query);
let new_options = (self.query_callback)(current_query.to_owned(), cx.editor);
cx.jobs.callback(async move {
let new_options = new_options.await?;
let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
// Wrapping of pickers in overlay is done outside the picker code,
// so this is fragile and will break if wrapped in some other widget.
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(ID) {
Some(overlay) => &mut overlay.content.file_picker,
None => return,
};
picker.set_options(new_options);
editor.reset_idle_timer();
}));
anyhow::Ok(callback)
});
EventResult::Consumed(None)
}
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
self.file_picker.cursor(area, ctx)
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
self.file_picker.required_size(viewport)
}
fn id(&self) -> Option<&'static str> {
Some(ID)
}
}

@ -0,0 +1,170 @@
use std::{
path::Path,
sync::{atomic, Arc},
time::Duration,
};
use helix_event::AsyncHook;
use tokio::time::Instant;
use crate::{job, ui::overlay::Overlay};
use super::{CachedPreview, DynQueryCallback, Picker};
pub(super) struct PreviewHighlightHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
trigger: Option<Arc<Path>>,
phantom_data: std::marker::PhantomData<(T, D)>,
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Default for PreviewHighlightHandler<T, D> {
fn default() -> Self {
Self {
trigger: None,
phantom_data: Default::default(),
}
}
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
for PreviewHighlightHandler<T, D>
{
type Event = Arc<Path>;
fn handle_event(
&mut self,
path: Self::Event,
timeout: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
if self
.trigger
.as_ref()
.is_some_and(|trigger| trigger == &path)
{
// If the path hasn't changed, don't reset the debounce
timeout
} else {
self.trigger = Some(path);
Some(Instant::now() + Duration::from_millis(150))
}
}
fn finish_debounce(&mut self) {
let Some(path) = self.trigger.take() else { return };
job::dispatch_blocking(move |editor, compositor| {
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Picker<T, D>>>() else {
return;
};
let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path) else {
return
};
if doc.language_config().is_some() {
return;
}
let Some(language_config) = doc.detect_language_config(&editor.syn_loader.load()) else { return };
doc.language = Some(language_config.clone());
let text = doc.text().clone();
let loader = editor.syn_loader.clone();
tokio::task::spawn_blocking(move || {
let Some(syntax) =
language_config
.highlight_config(&loader.load().scopes())
.and_then(|highlight_config| {
helix_core::Syntax::new(
text.slice(..),
highlight_config,
loader,
)
}) else {
log::info!("highlighting picker item failed");
return;
};
job::dispatch_blocking(move |editor, compositor| {
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Picker<T, D>>>() else {
log::info!("picker closed before syntax highlighting finished");
return;
};
let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path) else {
return
};
let diagnostics = helix_view::Editor::doc_diagnostics(
&editor.language_servers,
&editor.diagnostics,
doc,
);
doc.replace_diagnostics(diagnostics, &[], None);
doc.syntax = Some(syntax);
});
});
});
}
}
pub(super) struct DynamicQueryHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
callback: Arc<DynQueryCallback<T, D>>,
// Duration used as a debounce.
// Defaults to 100ms if not provided via `Picker::with_dynamic_query`. Callers may want to set
// this higher if the dynamic query is expensive - for example global search.
debounce: Duration,
last_query: Arc<str>,
query: Option<Arc<str>>,
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> DynamicQueryHandler<T, D> {
pub(super) fn new(callback: DynQueryCallback<T, D>, duration_ms: Option<u64>) -> Self {
Self {
callback: Arc::new(callback),
debounce: Duration::from_millis(duration_ms.unwrap_or(100)),
last_query: "".into(),
query: None,
}
}
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook for DynamicQueryHandler<T, D> {
type Event = Arc<str>;
fn handle_event(&mut self, query: Self::Event, _timeout: Option<Instant>) -> Option<Instant> {
if query == self.last_query {
// If the search query reverts to the last one we requested, no need to
// make a new request.
self.query = None;
None
} else {
self.query = Some(query);
Some(Instant::now() + self.debounce)
}
}
fn finish_debounce(&mut self) {
let Some(query) = self.query.take() else { return };
self.last_query = query.clone();
let callback = self.callback.clone();
job::dispatch_blocking(move |editor, compositor| {
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Picker<T, D>>>() else {
return;
};
// Increment the version number to cancel any ongoing requests.
picker.version.fetch_add(1, atomic::Ordering::Relaxed);
picker.matcher.restart(false);
let injector = picker.injector();
let get_options = (callback)(&query, editor, picker.editor_data.clone(), &injector);
tokio::spawn(async move {
if let Err(err) = get_options.await {
log::info!("Dynamic request failed: {err}");
}
// The picker's shows its running indicator when there are any active
// injectors. When we're done injecting new options, drop the injector
// and request a redraw to remove the running indicator.
drop(injector);
helix_event::request_redraw();
});
})
}
}

@ -0,0 +1,210 @@
use std::{collections::HashMap, sync::Arc};
pub(super) type PickerQuery = HashMap<Arc<str>, Arc<str>>;
pub(super) fn parse(column_names: &[Arc<str>], primary_column: usize, input: &str) -> PickerQuery {
let mut fields: HashMap<Arc<str>, String> = HashMap::new();
let primary_field = &column_names[primary_column];
let mut escaped = false;
let mut quoted = false;
let mut in_field = false;
let mut field = None;
let mut text = String::new();
macro_rules! finish_field {
() => {
let key = field.take().unwrap_or(primary_field);
if let Some(pattern) = fields.get_mut(key) {
pattern.push(' ');
pattern.push_str(&text);
text.clear();
} else {
fields.insert(key.clone(), std::mem::take(&mut text));
}
};
}
for ch in input.chars() {
match ch {
// Backslash escaping
'\\' => escaped = !escaped,
_ if escaped => {
// Allow escaping '%' and '"'
if !matches!(ch, '%' | '"') {
text.push('\\');
}
text.push(ch);
escaped = false;
}
// Double quoting
'"' => quoted = !quoted,
'%' | ':' | ' ' if quoted => text.push(ch),
// Space either completes the current word if no field is specified
// or field if one is specified.
'%' | ' ' if !text.is_empty() => {
finish_field!();
in_field = ch == '%';
}
'%' => in_field = true,
':' if in_field => {
// Go over all columns and their indices, find all that starts with field key,
// select a column that fits key the most.
field = column_names
.iter()
.filter(|col| col.starts_with(&text))
// select "fittest" column
.min_by_key(|col| col.len());
text.clear();
in_field = false;
}
_ => text.push(ch),
}
}
if !in_field && !text.is_empty() {
finish_field!();
}
fields
.into_iter()
.map(|(field, query)| (field, query.as_str().into()))
.collect()
}
#[cfg(test)]
mod test {
use helix_core::hashmap;
use super::*;
#[test]
fn parse_query_test() {
let columns = &[
"primary".into(),
"field1".into(),
"field2".into(),
"another".into(),
"anode".into(),
];
let primary_column = 0;
// Basic field splitting
assert_eq!(
parse(columns, primary_column, "hello world"),
hashmap!(
"primary".into() => "hello world".into(),
)
);
assert_eq!(
parse(columns, primary_column, "hello %field1:world %field2:!"),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "world".into(),
"field2".into() => "!".into(),
)
);
assert_eq!(
parse(columns, primary_column, "%field1:abc %field2:def xyz"),
hashmap!(
"primary".into() => "xyz".into(),
"field1".into() => "abc".into(),
"field2".into() => "def".into(),
)
);
// Trailing space is trimmed
assert_eq!(
parse(columns, primary_column, "hello "),
hashmap!(
"primary".into() => "hello".into(),
)
);
// Trailing fields are trimmed.
assert_eq!(
parse(columns, primary_column, "hello %foo"),
hashmap!(
"primary".into() => "hello".into(),
)
);
// Quoting
assert_eq!(
parse(columns, primary_column, r#"hello %field1:"a b c""#),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "a b c".into(),
)
);
// Escaping
assert_eq!(
parse(columns, primary_column, r#"hello\ world"#),
hashmap!(
"primary".into() => r#"hello\ world"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"hello \%field1:world"#),
hashmap!(
"primary".into() => "hello %field1:world".into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"hello %field1:"a\"b""#),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => r#"a"b"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"%field1:hello\ world"#),
hashmap!(
"field1".into() => r#"hello\ world"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"%field1:"hello\ world""#),
hashmap!(
"field1".into() => r#"hello\ world"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"\bfoo\b"#),
hashmap!(
"primary".into() => r#"\bfoo\b"#.into(),
)
);
// Prefix
assert_eq!(
parse(columns, primary_column, "hello %anot:abc"),
hashmap!(
"primary".into() => "hello".into(),
"another".into() => "abc".into(),
)
);
assert_eq!(
parse(columns, primary_column, "hello %ano:abc"),
hashmap!(
"primary".into() => "hello".into(),
"anode".into() => "abc".into()
)
);
assert_eq!(
parse(columns, primary_column, "hello %field1:xyz %fie:abc"),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "xyz abc".into()
)
);
assert_eq!(
parse(columns, primary_column, "hello %fie:abc"),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "abc".into()
)
);
}
}

@ -92,11 +92,15 @@ impl Prompt {
}
pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
self.set_line(line, editor);
self
}
pub fn set_line(&mut self, line: String, editor: &Editor) {
let cursor = line.len();
self.line = line;
self.cursor = cursor;
self.recalculate_completion(editor);
self
}
pub fn with_language(
@ -112,6 +116,19 @@ impl Prompt {
&self.line
}
pub fn with_history_register(&mut self, history_register: Option<char>) -> &mut Self {
self.history_register = history_register;
self
}
pub(crate) fn first_history_completion<'a>(
&'a self,
editor: &'a Editor,
) -> Option<Cow<'a, str>> {
self.history_register
.and_then(|reg| editor.registers.first(reg, editor))
}
pub fn recalculate_completion(&mut self, editor: &Editor) {
self.exit_selection();
self.completion = (self.completion_fn)(editor, &self.line);
@ -476,10 +493,7 @@ impl Prompt {
let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
if self.line.is_empty() {
// Show the most recently entered value as a suggestion.
if let Some(suggestion) = self
.history_register
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
{
if let Some(suggestion) = self.first_history_completion(cx.editor) {
surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
}
} else if let Some((language, loader)) = self.language.as_ref() {
@ -544,6 +558,10 @@ impl Component for Prompt {
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
ctrl!('h') | key!(Backspace) | shift!(Backspace) => {
if self.line.is_empty() {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn;
}
self.delete_char_backwards(cx.editor);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
@ -574,8 +592,7 @@ impl Component for Prompt {
self.recalculate_completion(cx.editor);
} else {
let last_item = self
.history_register
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
.first_history_completion(cx.editor)
.map(|entry| entry.to_string())
.unwrap_or_else(|| String::from(""));

@ -4,7 +4,6 @@ use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect,
theme::Style,
Document, Editor, View,
};
@ -20,7 +19,6 @@ pub struct RenderContext<'a> {
pub view: &'a View,
pub focused: bool,
pub spinners: &'a ProgressSpinners,
pub parts: RenderBuffer<'a>,
}
impl<'a> RenderContext<'a> {
@ -37,18 +35,10 @@ impl<'a> RenderContext<'a> {
view,
focused,
spinners,
parts: RenderBuffer::default(),
}
}
}
#[derive(Default)]
pub struct RenderBuffer<'a> {
pub left: Spans<'a>,
pub center: Spans<'a>,
pub right: Spans<'a>,
}
pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) {
let base_style = if context.focused {
context.editor.theme.get("ui.statusline")
@ -58,90 +48,93 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface
surface.set_style(viewport.with_height(1), base_style);
let write_left = |context: &mut RenderContext, text, style| {
append(&mut context.parts.left, text, &base_style, style)
};
let write_center = |context: &mut RenderContext, text, style| {
append(&mut context.parts.center, text, &base_style, style)
};
let write_right = |context: &mut RenderContext, text, style| {
append(&mut context.parts.right, text, &base_style, style)
};
// Left side of the status line.
let config = context.editor.config();
let element_ids = &config.statusline.left;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_left));
let statusline = render_statusline(context, viewport.width as usize);
surface.set_spans(
viewport.x,
viewport.y,
&context.parts.left,
context.parts.left.width() as u16,
&statusline,
statusline.width() as u16,
);
}
// Right side of the status line.
pub fn render_statusline<'a>(context: &mut RenderContext, width: usize) -> Spans<'a> {
let config = context.editor.config();
let element_ids = &config.statusline.right;
element_ids
let element_ids = &config.statusline.left;
let mut left = element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_right));
surface.set_spans(
viewport.x
+ viewport
.width
.saturating_sub(context.parts.right.width() as u16),
viewport.y,
&context.parts.right,
context.parts.right.width() as u16,
);
// Center of the status line.
.flat_map(|render| render(context).0)
.collect::<Vec<Span>>();
let element_ids = &config.statusline.center;
element_ids
let mut center = element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_center));
.flat_map(|render| render(context).0)
.collect::<Vec<Span>>();
// Width of the empty space between the left and center area and between the center and right area.
let spacing = 1u16;
let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16;
let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing);
let center_width = center_max_width.min(context.parts.center.width() as u16);
surface.set_spans(
viewport.x + viewport.width / 2 - center_width / 2,
viewport.y,
&context.parts.center,
center_width,
);
}
let element_ids = &config.statusline.right;
let mut right = element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.flat_map(|render| render(context).0)
.collect::<Vec<Span>>();
let left_area_width: usize = left.iter().map(|s| s.width()).sum();
let center_area_width: usize = center.iter().map(|s| s.width()).sum();
let right_area_width: usize = right.iter().map(|s| s.width()).sum();
let min_spacing_between_areas = 1usize;
let sides_space_required = left_area_width + right_area_width + min_spacing_between_areas;
let total_space_required = sides_space_required + center_area_width + min_spacing_between_areas;
let mut statusline: Vec<Span> = vec![];
if center_area_width > 0 && total_space_required <= width {
// SAFETY: this subtraction cannot underflow because `left_area_width + center_area_width + right_area_width`
// is smaller than `total_space_required`, which is smaller than `width` in this branch.
let total_spacers = width - (left_area_width + center_area_width + right_area_width);
// This is how much padding space it would take on either side to align the center area to the middle.
let center_margin = (width - center_area_width) / 2;
let left_spacers = if left_area_width < center_margin && right_area_width < center_margin {
// Align the center area to the middle if there is enough space on both sides.
center_margin - left_area_width
} else {
// Otherwise split the available space evenly and use it as margin.
// The center element won't be aligned to the middle but it will be evenly
// spaced between the left and right areas.
total_spacers / 2
};
let right_spacers = total_spacers - left_spacers;
statusline.append(&mut left);
statusline.push(" ".repeat(left_spacers).into());
statusline.append(&mut center);
statusline.push(" ".repeat(right_spacers).into());
statusline.append(&mut right);
} else if right_area_width > 0 && sides_space_required <= width {
let side_areas_width = left_area_width + right_area_width;
statusline.append(&mut left);
statusline.push(" ".repeat(width - side_areas_width).into());
statusline.append(&mut right);
} else if left_area_width <= width {
statusline.append(&mut left);
}
fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option<Style>) {
buffer.0.push(Span::styled(
text,
style.map_or(*base_style, |s| (*base_style).patch(s)),
));
statusline.into()
}
fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn get_render_function<'a>(
element_id: StatusLineElementID,
) -> impl Fn(&RenderContext) -> Spans<'a> {
match element_id {
helix_view::editor::StatusLineElement::Mode => render_mode,
helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
helix_view::editor::StatusLineElement::FileBaseName => render_file_base_name,
helix_view::editor::StatusLineElement::FileName => render_file_name,
helix_view::editor::StatusLineElement::FileAbsolutePath => render_file_absolute_path,
helix_view::editor::StatusLineElement::FileModificationIndicator => {
render_file_modification_indicator
}
@ -165,48 +158,40 @@ where
}
}
fn render_mode<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_mode<'a>(context: &RenderContext) -> Spans<'a> {
let visible = context.focused;
let config = context.editor.config();
let modenames = &config.statusline.mode;
write(
context,
format!(
" {} ",
if visible {
match context.editor.mode() {
Mode::Insert => &modenames.insert,
Mode::Select => &modenames.select,
Mode::Normal => &modenames.normal,
}
} else {
// If not focused, explicitly leave an empty space instead of returning None.
" "
}
),
if visible && config.color_modes {
let modename = if visible {
match context.editor.mode() {
Mode::Insert => modenames.insert.clone(),
Mode::Select => modenames.select.clone(),
Mode::Normal => modenames.normal.clone(),
}
} else {
// If not focused, explicitly leave an empty space.
" ".into()
};
let modename = format!(" {} ", modename);
if config.color_modes {
Span::styled(
modename,
match context.editor.mode() {
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
}
} else {
None
},
);
Mode::Insert => context.editor.theme.get("ui.statusline.insert"),
Mode::Select => context.editor.theme.get("ui.statusline.select"),
Mode::Normal => context.editor.theme.get("ui.statusline.normal"),
},
)
.into()
} else {
Span::raw(modename).into()
}
}
// TODO think about handling multiple language servers
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_lsp_spinner<'a>(context: &RenderContext) -> Spans<'a> {
let language_server = context.doc.language_servers().next();
write(
context,
Span::raw(
language_server
.and_then(|srv| {
context
@ -217,14 +202,11 @@ where
// Even if there's no spinner; reserve its space to avoid elements frequently shifting.
.unwrap_or(" ")
.to_string(),
None,
);
)
.into()
}
fn render_diagnostics<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
let (warnings, errors) = context
.doc
.diagnostics()
@ -239,29 +221,28 @@ where
counts
});
let mut output = Spans::default();
if warnings > 0 {
write(
context,
output.0.push(Span::styled(
"●".to_string(),
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
context.editor.theme.get("warning"),
));
output.0.push(Span::raw(format!(" {} ", warnings)));
}
if errors > 0 {
write(
context,
output.0.push(Span::styled(
"●".to_string(),
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
context.editor.theme.get("error"),
));
output.0.push(Span::raw(format!(" {} ", errors)));
}
output
}
fn render_workspace_diagnostics<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_workspace_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
let (warnings, errors) =
context
.editor
@ -277,51 +258,49 @@ where
counts
});
let mut output = Spans::default();
if warnings > 0 || errors > 0 {
write(context, " W ".into(), None);
output.0.push(Span::raw(" W "));
}
if warnings > 0 {
write(
context,
output.0.push(Span::styled(
"●".to_string(),
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
context.editor.theme.get("warning"),
));
output.0.push(Span::raw(format!(" {} ", warnings)));
}
if errors > 0 {
write(
context,
output.0.push(Span::styled(
"●".to_string(),
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
context.editor.theme.get("error"),
));
output.0.push(Span::raw(format!(" {} ", errors)));
}
output
}
fn render_selections<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_selections<'a>(context: &RenderContext) -> Spans<'a> {
let count = context.doc.selection(context.view.id).len();
write(
context,
format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }),
None,
);
Span::raw(format!(
" {} sel{} ",
count,
if count == 1 { "" } else { "s" }
))
.into()
}
fn render_primary_selection_length<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_primary_selection_length<'a>(context: &RenderContext) -> Spans<'a> {
let tot_sel = context.doc.selection(context.view.id).primary().len();
write(
context,
format!(" {} char{} ", tot_sel, if tot_sel == 1 { "" } else { "s" }),
None,
);
Span::raw(format!(
" {} char{} ",
tot_sel,
if tot_sel == 1 { "" } else { "s" }
))
.into()
}
fn get_position(context: &RenderContext) -> Position {
@ -335,55 +314,33 @@ fn get_position(context: &RenderContext) -> Position {
)
}
fn render_position<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_position<'a>(context: &RenderContext) -> Spans<'a> {
let position = get_position(context);
write(
context,
format!(" {}:{} ", position.row + 1, position.col + 1),
None,
);
Span::raw(format!(" {}:{} ", position.row + 1, position.col + 1)).into()
}
fn render_total_line_numbers<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_total_line_numbers<'a>(context: &RenderContext) -> Spans<'a> {
let total_line_numbers = context.doc.text().len_lines();
write(context, format!(" {} ", total_line_numbers), None);
Span::raw(format!(" {} ", total_line_numbers)).into()
}
fn render_position_percentage<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_position_percentage<'a>(context: &RenderContext) -> Spans<'a> {
let position = get_position(context);
let maxrows = context.doc.text().len_lines();
write(
context,
format!("{}%", (position.row + 1) * 100 / maxrows),
None,
);
Span::raw(format!("{}%", (position.row + 1) * 100 / maxrows)).into()
}
fn render_file_encoding<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_file_encoding<'a>(context: &RenderContext) -> Spans<'a> {
let enc = context.doc.encoding();
if enc != encoding::UTF_8 {
write(context, format!(" {} ", enc.name()), None);
Span::raw(format!(" {} ", enc.name())).into()
} else {
Spans::default()
}
}
fn render_file_line_ending<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_file_line_ending<'a>(context: &RenderContext) -> Spans<'a> {
use helix_core::LineEnding::*;
let line_ending = match context.doc.line_ending {
Crlf => "CRLF",
@ -402,22 +359,16 @@ where
PS => "PS", // U+2029 -- ParagraphSeparator
};
write(context, format!(" {} ", line_ending), None);
Span::raw(format!(" {} ", line_ending)).into()
}
fn render_file_type<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_file_type<'a>(context: &RenderContext) -> Spans<'a> {
let file_type = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
write(context, format!(" {} ", file_type), None);
Span::raw(format!(" {} ", file_type)).into()
}
fn render_file_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_file_name<'a>(context: &RenderContext) -> Spans<'a> {
let title = {
let rel_path = context.doc.relative_path();
let path = rel_path
@ -427,13 +378,23 @@ where
format!(" {} ", path)
};
write(context, title, None);
Span::raw(title).into()
}
fn render_file_absolute_path<'a>(context: &RenderContext) -> Spans<'a> {
let title = {
let path = context.doc.path();
let path = path
.as_ref()
.map(|p| p.to_string_lossy())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
format!(" {} ", path)
};
Span::raw(title).into()
}
fn render_file_modification_indicator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_file_modification_indicator<'a>(context: &RenderContext) -> Spans<'a> {
let title = (if context.doc.is_modified() {
"[+]"
} else {
@ -441,76 +402,60 @@ where
})
.to_string();
write(context, title, None);
Span::raw(title).into()
}
fn render_read_only_indicator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_read_only_indicator<'a>(context: &RenderContext) -> Spans<'a> {
let title = if context.doc.readonly {
" [readonly] "
} else {
""
}
.to_string();
write(context, title, None);
Span::raw(title).into()
}
fn render_file_base_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_file_base_name<'a>(context: &RenderContext) -> Spans<'a> {
let title = {
let rel_path = context.doc.relative_path();
let path = rel_path
.as_ref()
.and_then(|p| p.as_path().file_name().map(|s| s.to_string_lossy()))
.and_then(|p| p.file_name().map(|s| s.to_string_lossy()))
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
format!(" {} ", path)
};
write(context, title, None);
Span::raw(title).into()
}
fn render_separator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_separator<'a>(context: &RenderContext) -> Spans<'a> {
let sep = &context.editor.config().statusline.separator;
write(
context,
Span::styled(
sep.to_string(),
Some(context.editor.theme.get("ui.statusline.separator")),
);
context.editor.theme.get("ui.statusline.separator"),
)
.into()
}
fn render_spacer<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
write(context, String::from(" "), None);
fn render_spacer<'a>(_context: &RenderContext) -> Spans<'a> {
Span::raw(" ").into()
}
fn render_version_control<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_version_control<'a>(context: &RenderContext) -> Spans<'a> {
let head = context
.doc
.version_control_head()
.unwrap_or_default()
.to_string();
write(context, head, None);
Span::raw(head).into()
}
fn render_register<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
fn render_register<'a>(context: &RenderContext) -> Spans<'a> {
if let Some(reg) = context.editor.selected_register {
write(context, format!(" reg={} ", reg), None)
Span::raw(format!(" reg={} ", reg)).into()
} else {
Spans::default()
}
}

@ -635,3 +635,60 @@ async fn test_surround_delete() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn tree_sitter_motions_work_across_injections() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-o>",
"<script>let #[|x = 1]#;</script>",
),
)
.await?;
// When the full injected layer is selected, expand_selection jumps to
// a more shallow layer.
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>#[|let x = 1;]#</script>",
"<A-o>",
"#[|<script>let x = 1;</script>]#",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x = 1]#;</script>",
"<A-i>",
"<script>let #[|x]# = 1;</script>",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-n>",
"<script>let x #[|=]# 1;</script>",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-p>",
"<script>#[|let]# x = 1;</script>",
),
)
.await?;
Ok(())
}

@ -18,7 +18,7 @@ default = ["crossterm"]
helix-view = { path = "../helix-view", features = ["term"] }
helix-core = { path = "../helix-core" }
bitflags = "2.4"
bitflags = "2.5"
cassowary = "0.3"
unicode-segmentation = "1.11"
crossterm = { version = "0.27", optional = true }

@ -52,10 +52,14 @@ impl Default for Capabilities {
impl Capabilities {
/// Detect capabilities from the terminfo database located based
/// on the $TERM environment variable. If detection fails, returns
/// a default value where no capability is supported.
/// a default value where no capability is supported, or just undercurl
/// if config.undercurl is set.
pub fn from_env_or_default(config: &EditorConfig) -> Self {
match termini::TermInfo::from_env() {
Err(_) => Capabilities::default(),
Err(_) => Capabilities {
has_extended_underlines: config.undercurl,
..Capabilities::default()
},
Ok(t) => Capabilities {
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines

@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
parking_lot = "0.12"
arc-swap = { version = "1.7.0" }
gix = { version = "0.58.0", features = ["attributes"], default-features = false, optional = true }
gix = { version = "0.61.0", features = ["attributes"], default-features = false, optional = true }
imara-diff = "0.1.5"
anyhow = "1"

@ -23,7 +23,7 @@ helix-lsp = { path = "../helix-lsp" }
helix-dap = { path = "../helix-dap" }
helix-vcs = { path = "../helix-vcs" }
bitflags = "2.4"
bitflags = "2.5"
anyhow = "1"
crossterm = { version = "0.27", optional = true }
@ -50,7 +50,7 @@ parking_lot = "0.12.1"
[target.'cfg(windows)'.dependencies]
clipboard-win = { version = "5.2", features = ["std"] }
clipboard-win = { version = "5.3", features = ["std"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"

@ -8,7 +8,7 @@ use helix_core::chars::char_is_word;
use helix_core::doc_formatter::TextFormat;
use helix_core::encoding::Encoding;
use helix_core::syntax::{Highlight, LanguageServerFeature};
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
use helix_core::text_annotations::{InlineAnnotation, Overlay};
use helix_lsp::util::lsp_pos_to_pos;
use helix_vcs::{DiffHandle, DiffProviderRegistry};
@ -21,7 +21,6 @@ use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::SystemTime;
@ -126,6 +125,7 @@ pub struct Document {
///
/// To know if they're up-to-date, check the `id` field in `DocumentInlayHints`.
pub(crate) inlay_hints: HashMap<ViewId, DocumentInlayHints>,
pub(crate) jump_labels: HashMap<ViewId, Vec<Overlay>>,
/// Set to `true` when the document is updated, reset to `false` on the next inlay hints
/// update from the LSP
pub inlay_hints_oudated: bool,
@ -200,22 +200,22 @@ pub struct DocumentInlayHints {
pub id: DocumentInlayHintsId,
/// Inlay hints of `TYPE` kind, if any.
pub type_inlay_hints: Rc<[InlineAnnotation]>,
pub type_inlay_hints: Vec<InlineAnnotation>,
/// Inlay hints of `PARAMETER` kind, if any.
pub parameter_inlay_hints: Rc<[InlineAnnotation]>,
pub parameter_inlay_hints: Vec<InlineAnnotation>,
/// Inlay hints that are neither `TYPE` nor `PARAMETER`.
///
/// LSPs are not required to associate a kind to their inlay hints, for example Rust-Analyzer
/// currently never does (February 2023) and the LSP spec may add new kinds in the future that
/// we want to display even if we don't have some special highlighting for them.
pub other_inlay_hints: Rc<[InlineAnnotation]>,
pub other_inlay_hints: Vec<InlineAnnotation>,
/// Inlay hint padding. When creating the final `TextAnnotations`, the `before` padding must be
/// added first, then the regular inlay hints, then the `after` padding.
pub padding_before_inlay_hints: Rc<[InlineAnnotation]>,
pub padding_after_inlay_hints: Rc<[InlineAnnotation]>,
pub padding_before_inlay_hints: Vec<InlineAnnotation>,
pub padding_after_inlay_hints: Vec<InlineAnnotation>,
}
impl DocumentInlayHints {
@ -223,11 +223,11 @@ impl DocumentInlayHints {
pub fn empty_with_id(id: DocumentInlayHintsId) -> Self {
Self {
id,
type_inlay_hints: Rc::new([]),
parameter_inlay_hints: Rc::new([]),
other_inlay_hints: Rc::new([]),
padding_before_inlay_hints: Rc::new([]),
padding_after_inlay_hints: Rc::new([]),
type_inlay_hints: Vec::new(),
parameter_inlay_hints: Vec::new(),
other_inlay_hints: Vec::new(),
padding_before_inlay_hints: Vec::new(),
padding_after_inlay_hints: Vec::new(),
}
}
}
@ -666,6 +666,7 @@ impl Document {
version_control_head: None,
focused_at: std::time::Instant::now(),
readonly: false,
jump_labels: HashMap::new(),
}
}
@ -993,11 +994,13 @@ impl Document {
provider_registry: &DiffProviderRegistry,
) -> Result<(), Error> {
let encoding = self.encoding;
let path = self
.path()
.filter(|path| path.exists())
.ok_or_else(|| anyhow!("can't find file to reload from {:?}", self.display_name()))?
.to_owned();
let path = match self.path() {
None => return Ok(()),
Some(path) => match path.exists() {
true => path.to_owned(),
false => bail!("can't find file to reload from {:?}", self.display_name()),
},
};
// Once we have a valid path we check if its readonly status has changed
self.detect_readonly();
@ -1137,6 +1140,7 @@ impl Document {
pub fn remove_view(&mut self, view_id: ViewId) {
self.selections.remove(&view_id);
self.inlay_hints.remove(&view_id);
self.jump_labels.remove(&view_id);
}
/// Apply a [`Transaction`] to the [`Document`] to change its text.
@ -1264,13 +1268,12 @@ impl Document {
});
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
if let Some(data) = Rc::get_mut(annotations) {
changes.update_positions(
data.iter_mut()
.map(|annotation| (&mut annotation.char_idx, Assoc::After)),
);
}
let apply_inlay_hint_changes = |annotations: &mut Vec<InlineAnnotation>| {
changes.update_positions(
annotations
.iter_mut()
.map(|annotation| (&mut annotation.char_idx, Assoc::After)),
);
};
self.inlay_hints_oudated = true;
@ -1685,7 +1688,7 @@ impl Document {
&self.selections
}
pub fn relative_path(&self) -> Option<PathBuf> {
pub fn relative_path(&self) -> Option<Cow<Path>> {
self.path
.as_deref()
.map(helix_stdx::path::get_relative_path)
@ -1938,17 +1941,19 @@ impl Document {
}
}
/// Get the text annotations that apply to the whole document, those that do not apply to any
/// specific view.
pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations {
TextAnnotations::default()
}
/// Set the inlay hints for this document and `view_id`.
pub fn set_inlay_hints(&mut self, view_id: ViewId, inlay_hints: DocumentInlayHints) {
self.inlay_hints.insert(view_id, inlay_hints);
}
pub fn set_jump_labels(&mut self, view_id: ViewId, labels: Vec<Overlay>) {
self.jump_labels.insert(view_id, labels);
}
pub fn remove_jump_labels(&mut self, view_id: ViewId) {
self.jump_labels.remove(&view_id);
}
/// Get the inlay hints for this document and `view_id`.
pub fn inlay_hints(&self, view_id: ViewId) -> Option<&DocumentInlayHints> {
self.inlay_hints.get(&view_id)

@ -22,7 +22,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
borrow::Cow,
cell::Cell,
collections::{BTreeMap, HashMap},
collections::{BTreeMap, HashMap, HashSet},
fs,
io::{self, stdin},
num::NonZeroUsize,
@ -212,6 +212,23 @@ impl Default for FilePickerConfig {
}
}
fn deserialize_alphabet<'de, D>(deserializer: D) -> Result<Vec<char>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let str = String::deserialize(deserializer)?;
let chars: Vec<_> = str.chars().collect();
let unique_chars: HashSet<_> = chars.iter().copied().collect();
if unique_chars.len() != chars.len() {
return Err(<D::Error as Error>::custom(
"jump-label-alphabet must contain unique characters",
));
}
Ok(chars)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
@ -305,6 +322,9 @@ pub struct Config {
/// Which indent heuristic to use when a new line is inserted
#[serde(default)]
pub indent_heuristic: IndentationHeuristic,
/// labels characters used in jumpmode
#[serde(skip_serializing, deserialize_with = "deserialize_alphabet")]
pub jump_label_alphabet: Vec<char>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -487,6 +507,9 @@ pub enum StatusLineElement {
/// The relative file path
FileName,
/// The file absolute path
FileAbsolutePath,
// The file modification indicator
FileModificationIndicator,
@ -867,6 +890,7 @@ impl Default for Config {
smart_tab: Some(SmartTabConfig::default()),
popup_border: PopupBorderConfig::None,
indent_heuristic: IndentationHeuristic::default(),
jump_label_alphabet: ('a'..='z').collect(),
}
}
}

@ -19,7 +19,6 @@ use helix_core::{
use std::{
collections::{HashMap, VecDeque},
fmt,
rc::Rc,
};
const JUMP_LIST_CAPACITY: usize = 30;
@ -409,10 +408,19 @@ impl View {
}
/// Get the text annotations to display in the current view for the given document and theme.
pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations {
// TODO custom annotations for custom views like side by side diffs
let mut text_annotations = doc.text_annotations(theme);
pub fn text_annotations<'a>(
&self,
doc: &'a Document,
theme: Option<&Theme>,
) -> TextAnnotations<'a> {
let mut text_annotations = TextAnnotations::default();
if let Some(labels) = doc.jump_labels.get(&self.id) {
let style = theme
.and_then(|t| t.find_scope_index("ui.virtual.jump-label"))
.map(Highlight);
text_annotations.add_overlay(labels, style);
}
let DocumentInlayHints {
id: _,
@ -436,20 +444,15 @@ impl View {
.and_then(|t| t.find_scope_index("ui.virtual.inlay-hint"))
.map(Highlight);
let mut add_annotations = |annotations: &Rc<[_]>, style| {
if !annotations.is_empty() {
text_annotations.add_inline_annotations(Rc::clone(annotations), style);
}
};
// Overlapping annotations are ignored apart from the first so the order here is not random:
// types -> parameters -> others should hopefully be the "correct" order for most use cases,
// with the padding coming before and after as expected.
add_annotations(padding_before_inlay_hints, None);
add_annotations(type_inlay_hints, type_style);
add_annotations(parameter_inlay_hints, parameter_style);
add_annotations(other_inlay_hints, other_style);
add_annotations(padding_after_inlay_hints, None);
text_annotations
.add_inline_annotations(padding_before_inlay_hints, None)
.add_inline_annotations(type_inlay_hints, type_style)
.add_inline_annotations(parameter_inlay_hints, parameter_style)
.add_inline_annotations(other_inlay_hints, other_style)
.add_inline_annotations(padding_after_inlay_hints, None);
text_annotations
}

@ -6,6 +6,8 @@ use-grammars = { except = [ "hare", "wren", "gemini" ] }
[language-server]
als = { command = "als" }
ada-language-server = { command = "ada_language_server" }
ada-gpr-language-server = {command = "ada_language_server", args = ["--language-gpr"]}
awk-language-server = { command = "awk-language-server" }
bash-language-server = { command = "bash-language-server", args = ["start"] }
bass = { command = "bass", args = ["--lsp"] }
@ -96,6 +98,9 @@ yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
zls = { command = "zls" }
blueprint-compiler = { command = "blueprint-compiler", args = ["lsp"] }
typst-lsp = { command = "typst-lsp" }
pkgbuild-language-server = { command = "pkgbuild-language-server" }
helm_ls = { command = "helm_ls", args = ["serve"] }
ember-language-server = { command = "ember-language-server", args = ["--stdio"] }
[language-server.ansible-language-server]
command = "ansible-language-server"
@ -367,7 +372,6 @@ scope = "source.json"
injection-regex = "json"
file-types = [
"json",
"jsonc",
"arb",
"ipynb",
"geojson",
@ -396,6 +400,15 @@ indent = { tab-width = 2, unit = " " }
name = "json"
source = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "73076754005a460947cafe8e03a8cf5fa4fa2938" }
[[language]]
name = "jsonc"
scope = "source.json"
injection-regex = "jsonc"
file-types = ["jsonc"]
grammar = "json"
language-servers = [ "vscode-json-language-server" ]
auto-format = true
indent = { tab-width = 2, unit = " " }
[[language]]
name = "json5"
@ -898,7 +911,6 @@ file-types = [
{ glob = ".zshrc" },
{ glob = ".zimrc" },
{ glob = "APKBUILD" },
{ glob = "PKGBUILD" },
{ glob = ".bash_aliases" },
{ glob = ".Renviron" },
{ glob = ".xprofile" },
@ -932,6 +944,29 @@ indent = { tab-width = 4, unit = " " }
name = "php"
source = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "f860e598194f4a71747f91789bf536b393ad4a56" }
[[language]]
name = "php-only"
scope = "source.php-only"
injection-regex = "php-only"
file-types = []
indent = { tab-width = 4, unit = " " }
roots = ["composer.json", "index.php"]
[[grammar]]
name = "php-only"
source = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "cf1f4a0f1c01c705c1d6cf992b104028d5df0b53", subpath = "php_only" }
[[language]]
name = "blade"
scope = "source.blade.php"
file-types = [{ glob = "*.blade.php" }, "blade"]
injection-regex = "blade"
roots = ["composer.json", "index.php"]
[[grammar]]
name = "blade"
source = { git = "https://github.com/EmranMR/tree-sitter-blade", rev = "4c66efe1e05c639c555ee70092021b8223d2f440" }
[[language]]
name = "twig"
scope = "source.twig"
@ -1083,7 +1118,6 @@ injection-regex = "ocaml"
file-types = ["ml"]
shebangs = ["ocaml", "ocamlrun", "ocamlscript"]
block-comment-tokens = { start = "(*", end = "*)" }
comment-token = "(**)"
language-servers = [ "ocamllsp" ]
indent = { tab-width = 2, unit = " " }
@ -1520,7 +1554,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"]
language-servers = [ "docker-compose-langserver" ]
language-servers = [ "docker-compose-langserver", "yaml-language-server" ]
file-types = [{ glob = "docker-compose.yaml" }, { glob = "docker-compose.yml" }]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
@ -1583,7 +1617,7 @@ indent = { tab-width = 4, unit = "\t" }
[[grammar]]
name = "git-config"
source = { git = "https://github.com/the-mikedavis/tree-sitter-git-config", rev = "0e4f0baf90b57e5aeb62dcdbf03062c6315d43ea" }
source = { git = "https://github.com/the-mikedavis/tree-sitter-git-config", rev = "9c2a1b7894e6d9eedfe99805b829b4ecd871375e" }
[[language]]
name = "git-attributes"
@ -1677,17 +1711,9 @@ comment-token = "%%"
indent = { tab-width = 4, unit = " " }
language-servers = [ "erlang-ls" ]
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
"'" = "'"
'`' = "'"
[[grammar]]
name = "erlang"
source = { git = "https://github.com/the-mikedavis/tree-sitter-erlang", rev = "731e50555a51f0d8635992b0e60dc98cc47a58d7" }
source = { git = "https://github.com/the-mikedavis/tree-sitter-erlang", rev = "9d4b36a76d5519e3dbf1ec4f4b61bb1a293f584c" }
[[language]]
name = "kotlin"
@ -1769,7 +1795,7 @@ auto-format = true
[[grammar]]
name = "gleam"
source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "b2afa4fd6bb41a7bf912b034c653c90af7ae5122" }
source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "bcf9c45b56cbe46e9dac5eee0aee75df270000ac" }
[[language]]
name = "ron"
@ -1934,7 +1960,7 @@ source = { git = "https://github.com/PrestonKnopp/tree-sitter-godot-resource", r
name = "nu"
scope = "source.nu"
injection-regex = "nu"
file-types = ["nu"]
file-types = ["nu", "nuon"]
shebangs = ["nu"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
@ -2337,6 +2363,21 @@ language-servers = [ "jsonnet-language-server" ]
name = "jsonnet"
source = { git = "https://github.com/sourcegraph/tree-sitter-jsonnet", rev = "0475a5017ad7dc84845d1d33187f2321abcb261d" }
[[language]]
name = "ada"
scope = "source.ada"
injection-regex = "ada"
file-types = ["adb", "ads", "gpr"]
roots = ["alire.toml"]
comment-token = "--"
indent = { tab-width = 3, unit = " " }
language-servers = ["ada-language-server", "ada-gpr-language-server"]
[[grammar]]
name = "ada"
source = { git = "https://github.com/briot/tree-sitter-ada", rev = "ba0894efa03beb70780156b91e28c716b7a4764d" }
[[language]]
name = "astro"
scope = "source.astro"
@ -2575,6 +2616,7 @@ file-types = [
"kube",
"network",
{ glob = ".editorconfig" },
{ glob = "rclone.conf" },
"properties",
"cfg",
"directory"
@ -2619,7 +2661,7 @@ source = { git = "https://github.com/yuja/tree-sitter-qmljs", rev = "0b2b25bcaa7
name = "mermaid"
scope = "source.mermaid"
injection-regex = "mermaid"
file-types = ["mermaid"]
file-types = ["mermaid", "mmd"]
comment-token = "%%"
indent = { tab-width = 4, unit = " " }
@ -2894,11 +2936,12 @@ scope = "source.hurl"
injection-regex = "hurl"
file-types = ["hurl"]
comment-token = "#"
formatter = { command = "hurlfmt" }
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "hurl"
source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "264c42064b61ee21abe88d0061f29a0523352e22" }
source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "cd1a0ada92cc73dd0f4d7eedc162be4ded758591" }
[[language]]
name = "markdoc"
@ -3199,6 +3242,18 @@ indent = { tab-width = 2, unit = " " }
name = "hocon"
source = { git = "https://github.com/antosha417/tree-sitter-hocon", rev = "c390f10519ae69fdb03b3e5764f5592fb6924bcc" }
[[language]]
name = "koka"
scope = "source.koka"
injection-regex = "koka"
file-types = ["kk"]
comment-token = "//"
indent = { tab-width = 8, unit = " " }
[[grammar]]
name = "koka"
source = { git = "https://github.com/mtoohey31/tree-sitter-koka", rev = "2527e152d4b6a79fd50aebd8d0b4b4336c94a034" }
[[language]]
name = "tact"
scope = "source.tact"
@ -3240,7 +3295,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "groovy"
source = { git = "https://github.com/Decodetalkers/tree-sitter-groovy", rev = "7e023227f46fee428b16a0288eeb0f65ee2523ec" }
source = { git = "https://github.com/murtaza64/tree-sitter-groovy", rev = "235009aad0f580211fc12014bb0846c3910130c1" }
[[language]]
name = "fidl"
@ -3273,3 +3328,83 @@ indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "powershell"
source = { git = "https://github.com/airbus-cert/tree-sitter-powershell", rev = "c9316be0faca5d5b9fd3b57350de650755f42dc0" }
[[language]]
name = "ld"
scope = "source.ld"
injection-regex = "ld"
file-types = ["ld"]
block-comment-tokens = { start = "/*", end = "*/" }
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "ld"
source = { git = "https://github.com/mtoohey31/tree-sitter-ld", rev = "81978cde3844bfc199851e39c80a20ec6444d35e" }
[[language]]
name = "hyprlang"
scope = "source.hyprlang"
roots = ["hyprland.conf"]
file-types = [ { glob = "hyprland.conf"} ]
comment-token = "#"
grammar = "hyprlang"
[[grammar]]
name = "hyprlang"
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-hyprlang", rev = "27af9b74acf89fa6bed4fb8cb8631994fcb2e6f3"}
[[language]]
name = "supercollider"
scope = "source.supercollider"
injection-regex = "supercollider"
file-types = ["scd", "sc", "quark"]
comment-token = "//"
indent = { tab-width = 4, unit = "\t" }
[[grammar]]
name = "supercollider"
source = { git = "https://github.com/madskjeldgaard/tree-sitter-supercollider", rev = "3b35bd0fded4423c8fb30e9585c7bacbcd0e8095" }
[[language]]
name = "pkgbuild"
scope = "source.bash"
file-types = [{ glob = "PKGBUILD" }]
comment-token = "#"
grammar = "bash"
language-servers = [
"pkgbuild-language-server",
{ except-features = [
"diagnostics",
], name = "bash-language-server" },
]
[[language]]
name = "helm"
grammar = "gotmpl"
scope = "source.helm"
roots = ["Chart.yaml"]
comment-token = "#"
language-servers = ["helm_ls"]
file-types = [ { glob = "templates/*.yaml" }, { glob = "templates/_helpers.tpl"}, { glob = "templates/NOTES.txt" } ]
[[language]]
name = "glimmer"
scope = "source.glimmer"
injection-regex = "hbs"
file-types = [{ glob = "{app,addon}/{components,templates}/*.hbs" }]
block-comment-tokens = { start = "{{!", end = "}}" }
roots = ["package.json", "ember-cli-build.js"]
grammar = "glimmer"
language-servers = ["ember-language-server"]
formatter = { command = "prettier", args = ['--parser', 'glimmer'] }
[language.auto-pairs]
'"' = '"'
'{' = '}'
'(' = ')'
'<' = '>'
"'" = "'"
[[grammar]]
name = "glimmer"
source = { git = "https://github.com/ember-tooling/tree-sitter-glimmer", rev = "5dc6d1040e8ff8978ff3680e818d85447bbc10aa" }

@ -40,4 +40,4 @@
(jsx_closing_element ["</" ">"] @punctuation.bracket)
; <Component />
(jsx_self_closing_element ["<" "/>"] @punctuation.braket)
(jsx_self_closing_element ["<" "/>"] @punctuation.bracket)

@ -0,0 +1,15 @@
; Support for folding in Ada
;; za toggles folding a package, subprogram, if statement or loop
[
(package_declaration)
(generic_package_declaration)
(package_body)
(subprogram_declaration)
(subprogram_body)
(block_statement)
(if_statement)
(loop_statement)
(gnatprep_declarative_if_statement)
(gnatprep_if_statement)
] @fold

@ -0,0 +1,176 @@
;; highlight queries.
;; See the syntax at https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries
;; See also https://github.com/nvim-treesitter/nvim-treesitter/blob/master/CONTRIBUTING.md#parser-configurations
;; for a list of recommended @ tags, though not all of them have matching
;; highlights in neovim.
[
"abort"
"abs"
"abstract"
"accept"
"access"
"all"
"array"
"at"
"begin"
"declare"
"delay"
"delta"
"digits"
"do"
"end"
"entry"
"exit"
"generic"
"interface"
"is"
"limited"
"of"
"others"
"out"
"pragma"
"private"
"range"
"synchronized"
"tagged"
"task"
"terminate"
"until"
"when"
] @keyword
[
"null"
] @constant.builtin
[
"aliased"
"constant"
"renames"
] @keyword.storage
[
"mod"
"new"
"protected"
"record"
"subtype"
"type"
] @type.builtin
[
"with"
"use"
] @keyword.control.import
[
"body"
"function"
"overriding"
"procedure"
"package"
"separate"
] @keyword.function
[
"and"
"in"
"not"
"or"
"xor"
] @operator
[
"while"
"loop"
"for"
"parallel"
"reverse"
"some"
] @kewyord.control.repeat
[
"return"
] @keyword.control.return
[
"case"
"if"
"else"
"then"
"elsif"
"select"
] @keyword.control.conditional
[
"exception"
"raise"
] @keyword.control.exception
(comment) @comment
(string_literal) @string
(character_literal) @string
(numeric_literal) @constant.numeric
;; Highlight the name of subprograms
(procedure_specification name: (_) @function.builtin)
(function_specification name: (_) @function.builtin)
(package_declaration name: (_) @function.builtin)
(package_body name: (_) @function.builtin)
(generic_instantiation name: (_) @function.builtin)
(entry_declaration . (identifier) @function.builtin)
;; Some keywords should take different categories depending on the context
(use_clause "use" @keyword.control.import "type" @keyword.control.import)
(with_clause "private" @keyword.control.import)
(with_clause "limited" @keyword.control.import)
(use_clause (_) @namespace)
(with_clause (_) @namespace)
(loop_statement "end" @keyword.control.repeat)
(if_statement "end" @keyword.control.conditional)
(loop_parameter_specification "in" @keyword.control.repeat)
(loop_parameter_specification "in" @keyword.control.repeat)
(iterator_specification ["in" "of"] @keyword.control.repeat)
(range_attribute_designator "range" @keyword.control.repeat)
(raise_statement "with" @keyword.control.exception)
(gnatprep_declarative_if_statement) @keyword.directive
(gnatprep_if_statement) @keyword.directive
(gnatprep_identifier) @keyword.directive
(subprogram_declaration "is" @keyword.function "abstract" @keyword.function)
(aspect_specification "with" @keyword.function)
(full_type_declaration "is" @type.builtin)
(subtype_declaration "is" @type.builtin)
(record_definition "end" @type.builtin)
(full_type_declaration (_ "access" @type.builtin))
(array_type_definition "array" @type.builtin "of" @type.builtin)
(access_to_object_definition "access" @type.builtin)
(access_to_object_definition "access" @type.builtin
[
(general_access_modifier "constant" @type.builtin)
(general_access_modifier "all" @type.builtin)
]
)
(range_constraint "range" @type.builtin)
(signed_integer_type_definition "range" @type.builtin)
(index_subtype_definition "range" @type.builtin)
(record_type_definition "abstract" @type.builtin)
(record_type_definition "tagged" @type.builtin)
(record_type_definition "limited" @type.builtin)
(record_type_definition (record_definition "null" @type.builtin))
(private_type_declaration "is" @type.builtin "private" @type.builtin)
(private_type_declaration "tagged" @type.builtin)
(private_type_declaration "limited" @type.builtin)
(task_type_declaration "task" @type.builtin "is" @type.builtin)
;; Gray the body of expression functions
(expression_function_declaration
(function_specification)
"is"
(_) @attribute
)
(subprogram_declaration (aspect_specification) @attribute)
;; Highlight full subprogram specifications
; (subprogram_body
; [
; (procedure_specification)
; (function_specification)
; ] @function.builtin.spec
; )

@ -0,0 +1,32 @@
;; Better highlighting by referencing to the definition, for variable references.
;; See https://tree-sitter.github.io/tree-sitter/syntax-highlighting#local-variables
(compilation) @local.scope
(package_declaration) @local.scope
(package_body) @local.scope
(subprogram_declaration) @local.scope
(subprogram_body) @local.scope
(block_statement) @local.scope
(with_clause (_) @local.definition)
(procedure_specification name: (_) @local.definition)
(function_specification name: (_) @local.definition)
(package_declaration name: (_) @local.definition)
(package_body name: (_) @local.definition)
(generic_instantiation . name: (_) @local.definition)
(component_declaration . (identifier) @local.definition)
(exception_declaration . (identifier) @local.definition)
(formal_object_declaration . (identifier) @local.definition)
(object_declaration . (identifier) @local.definition)
(parameter_specification . (identifier) @local.definition)
(full_type_declaration . (identifier) @local.definition)
(private_type_declaration . (identifier) @local.definition)
(private_extension_declaration . (identifier) @local.definition)
(incomplete_type_declaration . (identifier) @local.definition)
(protected_type_declaration . (identifier) @local.definition)
(formal_complete_type_declaration . (identifier) @local.definition)
(formal_incomplete_type_declaration . (identifier) @local.definition)
(task_type_declaration . (identifier) @local.definition)
(subtype_declaration . (identifier) @local.definition)
(identifier) @local.reference

@ -0,0 +1,21 @@
;; Support for high-level text objects selections.
;; For instance:
;; maf (v)isually select (a) (f)unction or subprogram
;; mif (v)isually select (i)nside a (f)unction or subprogram
;; mai (v)isually select (a) (i)f statement (or loop)
;; mii (v)isually select (i)nside an (i)f statement (or loop)
;;
;; For navigations using textobjects, check link below:
;; https://docs.helix-editor.com/master/usage.html#navigating-using-tree-sitter-textobjects
;;
;; For Textobject queries explaination, check out link below:
;; https://docs.helix-editor.com/master/guides/textobject.html
(subprogram_body) @function.around
(subprogram_body (non_empty_declarative_part) @function.inside)
(subprogram_body (handled_sequence_of_statements) @function.inside)
(function_specification) @function.around
(procedure_specification) @function.around
(package_declaration) @function.around
(generic_package_declaration) @function.around
(package_body) @function.around

@ -0,0 +1,8 @@
((directive_start) @start
(directive_end) @end.after
(#set! role block))
((bracket_start) @start
(bracket_end) @end
(#set! role block))

@ -0,0 +1,4 @@
(directive) @tag
(directive_start) @tag
(directive_end) @tag
(comment) @comment

@ -0,0 +1,9 @@
((text) @injection.content
(#set! injection.combined)
(#set! injection.language php))
((php_only) @injection.content
(#set! injection.language php-only))
((parameter) @injection.content
(#set! injection.language php-only))

@ -49,6 +49,13 @@
(this) @variable.builtin
(nullptr) @constant.builtin
; Parameters
(parameter_declaration
declarator: (reference_declarator (identifier) @variable.parameter))
(optional_parameter_declaration
declarator: (identifier) @variable.parameter)
; Keywords
(template_argument_list (["<" ">"] @punctuation.bracket))

@ -3,7 +3,7 @@
(attribute
name: (atom) @keyword
(arguments (atom) @namespace)
(#match? @keyword "(module|behaviou?r)"))
(#any-of? @keyword "module" "behaviour" "behavior"))
(attribute
name: (atom) @keyword
@ -50,12 +50,20 @@
name: (atom) @keyword
(arguments
(_) @keyword.directive)
(#match? @keyword "ifn?def"))
(#any-of? @keyword "ifndef" "ifdef"))
(attribute
name: (atom) @keyword
module: (atom) @namespace
(#match? @keyword "(spec|callback)"))
(#any-of? @keyword "spec" "callback"))
(attribute
name: (atom) @keyword
(arguments [
(string)
(sigil)
] @comment.block.documentation)
(#any-of? @keyword "doc" "moduledoc"))
; Functions
(function_clause name: (atom) @function)
@ -84,7 +92,7 @@
((attribute
name: (atom) @keyword
(stab_clause
pattern: (arguments (variable) @variable.parameter)
pattern: (arguments (variable)? @variable.parameter)
body: (variable)? @variable.parameter))
(#match? @keyword "(spec|callback)"))
; functions

@ -5,3 +5,13 @@
((comment (comment_content) @injection.content)
(#set! injection.language "comment"))
; EEP-59 doc attributes use markdown by default.
(attribute
name: (atom) @_attribute
(arguments [
(string (quoted_content) @injection.content)
(sigil (quoted_content) @injection.content)
])
(#set! injection.language "markdown")
(#any-of? @_attribute "doc" "moduledoc"))

@ -1,7 +1,7 @@
; Specs and Callbacks
(attribute
(stab_clause
pattern: (arguments (variable) @local.definition)
pattern: (arguments (variable)? @local.definition)
; If a spec uses a variable as the return type (and later a `when` clause to type it):
body: (variable)? @local.definition)) @local.scope

@ -19,9 +19,10 @@
[
"["
"]"
"\""
] @punctuation.bracket
"=" @punctuation.delimiter
["=" "\\"] @punctuation.delimiter
(escape_sequence) @constant.character.escape
(comment) @comment

@ -61,10 +61,17 @@
; Literals
(string) @string
((escape_sequence) @warning
(#eq? @warning "\\e")) ; deprecated escape sequence
(escape_sequence) @constant.character.escape
(bit_string_segment_option) @function.builtin
(integer) @constant.numeric.integer
(float) @constant.numeric.float
; Reserved identifiers
((identifier) @error
(#any-of? @error "auto" "delegate" "derive" "else" "implement" "macro" "test" "echo"))
; Variables
(identifier) @variable
(discard) @comment.unused

@ -0,0 +1,94 @@
; === Tag Names ===
; Tags that start with a lower case letter are HTML tags
; We'll also use this highlighting for named blocks (which start with `:`)
((tag_name) @tag
(#match? @tag "^(:)?[a-z]"))
; Tags that start with a capital letter are Glimmer components
((tag_name) @constructor
(#match? @constructor "^[A-Z]"))
(attribute_name) @attribute
(string_literal) @string
(number_literal) @constant.numeric.integer
(boolean_literal) @constant.builtin.boolean
(concat_statement) @string
; === Block Statements ===
; Highlight the brackets
(block_statement_start) @punctuation.delimiter
(block_statement_end) @punctuation.delimiter
; Highlight `if`/`each`/`let`
(block_statement_start path: (identifier) @keyword.control.conditional)
(block_statement_end path: (identifier) @keyword.control.conditional)
((mustache_statement (identifier) @keyword.control.conditional)
(#eq? @keyword.control.conditional "else"))
; == Mustache Statements ===
; Hightlight the whole statement, to color brackets and separators
(mustache_statement) @punctuation.delimiter
; An identifier in a mustache expression is a variable
((mustache_statement [
(path_expression (identifier) @variable)
(identifier) @variable
])
(#not-any-of? @variable "yield" "outlet" "this" "else"))
; As are arguments in a block statement
((block_statement_start argument: [
(path_expression (identifier) @variable)
(identifier) @variable
])
(#not-eq? @variable "this"))
; As is an identifier in a block param
(block_params (identifier) @variable)
; As are helper arguments
((helper_invocation argument: [
(path_expression (identifier) @variable)
(identifier) @variable
])
(#not-eq? @variable "this"))
; `this` should be highlighted as a built-in variable
((identifier) @variable.builtin
(#eq? @variable.builtin "this"))
; If the identifier is just "yield" or "outlet", it's a keyword
((mustache_statement (identifier) @keyword.control.return)
(#any-of? @keyword.control.return "yield" "outlet"))
; Helpers are functions
((helper_invocation helper: [
(path_expression (identifier) @function)
(identifier) @function
])
(#not-any-of? @function "if" "yield"))
((helper_invocation helper: (identifier) @keyword.control.conditional)
(#any-of? @keyword.control.conditional "if" "yield"))
(hash_pair key: (identifier) @variable)
(hash_pair value: (identifier) @variable)
(hash_pair [
(path_expression (identifier) @variable)
(identifier) @variable
])
(comment_statement) @comment
(attribute_node "=" @operator)
(block_params "as" @keyword.control)
(block_params "|" @operator)
[
"<"
">"
"</"
"/>"
] @punctuation.delimiter

@ -2,7 +2,7 @@
(call_expression
function: (identifier) @function.builtin
(match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$"))
(#match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$"))
(call_expression
function: (identifier) @function)
@ -19,7 +19,7 @@
name: (identifier) @type.parameter))
((type_identifier) @type.builtin
(match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$"))
(#match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$"))
(type_identifier) @type

@ -0,0 +1,6 @@
[
(argument_list)
(closure)
(list)
(map)
] @fold

@ -1,96 +1,268 @@
(unit
(identifier) @variable)
[
"!instanceof"
"assert"
"class"
"extends"
"instanceof"
"package"
] @keyword
(string
(identifier) @variable)
[
"!in"
"as"
"in"
] @keyword.operator
[
"case"
"default"
"else"
"if"
"switch"
] @keyword.control.conditional
(escape_sequence) @constant.character.escape
[
"catch"
"finally"
"try"
] @keyword.control.exception
(block
(unit
(identifier) @namespace))
"def" @keyword.function
(func
(identifier) @function)
"import" @keyword.control.import
(number) @constant.numeric
[
"for"
"while"
(break)
(continue)
] @keyword.control.repeat
((identifier) @constant.builtin.boolean
(#any-of? @constant.builtin.boolean "true" "false"))
"return" @keyword.control.return
((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
((identifier) @constant.builtin
(#eq? @constant.builtin "null"))
((identifier) @type
(#any-of? @type
"String"
"Map"
"Object"
"Boolean"
"Integer"
"List"))
((identifier) @function.builtin
(#any-of? @function.builtin
"void"
"id"
"version"
"apply"
"implementation"
"testImplementation"
"androidTestImplementation"
"debugImplementation"))
((identifier) @keyword.storage.modifier
(#eq? @keyword.storage.modifier "static"))
((identifier) @keyword.storage.type
(#any-of? @keyword.storage.type "class" "def" "interface"))
((identifier) @keyword
(#any-of? @keyword
"assert"
"new"
"extends"
"implements"
"instanceof"))
((identifier) @keyword.control.import
(#any-of? @keyword.control.import "import" "package"))
((identifier) @keyword.storage.modifier
(#any-of? @keyword.storage.modifier
"abstract"
"protected"
"private"
"public"))
((identifier) @keyword.control.exception
(#any-of? @keyword.control.exception
"throw"
"finally"
"try"
"catch"))
[
"true"
"false"
] @constant.builtin.boolean
(null) @constant.builtin
"this" @variable.builtin
[
"int"
"char"
"short"
"long"
"boolean"
"float"
"double"
"void"
] @type.builtin
[
"final"
"private"
"protected"
"public"
"static"
"synchronized"
] @keyword.storage.modifier
(comment) @comment
(shebang) @keyword.directive
(string) @string
(string
(escape_sequence) @constant.character.escape)
(string
(interpolation
"$" @punctuation.special))
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
[
(line_comment)
(block_comment)
] @comment
":"
","
"."
] @punctuation.delimiter
(number_literal) @constant.numeric
((block_comment) @comment.block.documentation
(#match? @comment.block.documentation "^/[*][*][^*](?s:.)*[*]/$"))
(identifier) @variable
((line_comment) @comment.block.documentation
(#match? @comment.block.documentation "^///[^/]*.*$"))
((identifier) @constant
(#match? @constant "^[A-Z][A-Z_]+"))
[
(operators)
(leading_key)
"%"
"*"
"/"
"+"
"-"
"<<"
">>"
">>>"
".."
"..<"
"<..<"
"<.."
"<"
"<="
">"
">="
"=="
"!="
"<=>"
"==="
"!=="
"=~"
"==~"
"&"
"^"
"|"
"&&"
"||"
"?:"
"+"
"*"
".&"
".@"
"?."
"*."
"*"
"*:"
"++"
"--"
"!"
] @operator
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
(string
"/" @string)
(ternary_op
([
"?"
":"
]) @keyword.operator)
(map
(map_item
key: (identifier) @variable.parameter))
(parameter
type: (identifier) @type
name: (identifier) @variable.parameter)
(generic_param
name: (identifier) @variable.parameter)
(declaration
type: (identifier) @type)
(function_definition
type: (identifier) @type)
(function_declaration
type: (identifier) @type)
(class_definition
name: (identifier) @type)
(class_definition
superclass: (identifier) @type)
(generic_param
superclass: (identifier) @type)
(type_with_generics
(identifier) @type)
(type_with_generics
(generics
(identifier) @type))
(generics
[
"<"
">"
] @punctuation.bracket)
(generic_parameters
[
"<"
">"
] @punctuation.bracket)
; TODO: Class literals with PascalCase
(declaration
"=" @operator)
(assignment
"=" @operator)
(function_call
function: (identifier) @function)
(function_call
function:
(dotted_identifier
(identifier) @function .))
(function_call
(argument_list
(map_item
key: (identifier) @variable.parameter)))
(juxt_function_call
function: (identifier) @function)
(juxt_function_call
function:
(dotted_identifier
(identifier) @function .))
(juxt_function_call
(argument_list
(map_item
key: (identifier) @variable.parameter)))
(function_definition
function: (identifier) @function)
(function_declaration
function: (identifier) @function)
(annotation) @function.macro
(annotation
(identifier) @function.macro)
"@interface" @function.macro
(groovy_doc) @comment.block.documentation
(groovy_doc
[
(groovy_doc_param)
(groovy_doc_throws)
(groovy_doc_tag)
] @string.special)
(groovy_doc
(groovy_doc_param
(identifier) @variable.parameter))
(groovy_doc
(groovy_doc_throws
(identifier) @type))

@ -1,2 +1,5 @@
([(line_comment) (block_comment)] @injection.content
(#set! injection.language "comment"))
((comment) @injection.content
(#set! injection.language "comment"))
((groovy_doc) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,6 @@
(function_definition) @local.scope
(parameter
name: (identifier) @local.definition)
(identifier) @local.reference

@ -51,6 +51,20 @@
"sha256"
"md5"
"bytes"
"daysAfterNow"
"daysBeforeNow"
"htmlEscape"
"htmlUnescape"
"decode"
"format"
"nth"
"replace"
"split"
"toDate"
"toInt"
"urlEncode"
"urlDecode"
"count"
] @function.builtin
(filter) @attribute
@ -59,8 +73,11 @@
[
"null"
"cacert"
"compressed"
"location"
"insecure"
"path-as-is"
"proxy"
"max-redirs"
"retry"
"retry-interval"
@ -124,4 +141,4 @@
"base64,"
"file,"
"hex,"
] @string.special
] @string.special

@ -0,0 +1,58 @@
(comment) @comment
[
"source"
"exec"
"exec-once"
] @function.builtin
(keyword
(name) @keyword)
(assignment
(name) @variable.other.member)
(section
(name) @namespace)
(section
device: (device_name) @type)
(variable) @variable
"$" @punctuation.special
(boolean) @constant.builtin.boolean
(string) @string
(mod) @constant
[
"rgb"
"rgba"
] @function.builtin
[
(number)
(legacy_hex)
(angle)
(hex)
] @constant.numeric
"deg" @type
"," @punctuation.delimiter
[
"("
")"
"{"
"}"
] @punctuation.bracket
[
"="
"-"
"+"
] @operator

@ -0,0 +1,6 @@
(section) @indent
(section
"}" @outdent)
"}" @extend

@ -0,0 +1,3 @@
(exec
(string) @injection.content
(#set! injection.language "bash"))

@ -1,4 +1,7 @@
(method_declaration
body: (_)? @function.inside) @function.around
(constructor_declaration
body: (_) @function.inside) @function.around
(interface_declaration

@ -0,0 +1,2 @@
; inherits: json
(comment) @comment

@ -231,7 +231,7 @@
] @keyword
; TODO: fix this
((identifier) @keyword (match? @keyword "global|local"))
((identifier) @keyword (#match? @keyword "global|local"))
; ---------
; Operators
@ -277,12 +277,12 @@
; SCREAMING_SNAKE_CASE
(
(identifier) @constant
(match? @constant "^[A-Z][A-Z0-9_]*$"))
(#match? @constant "^[A-Z][A-Z0-9_]*$"))
; remaining identifiers that start with capital letters should be types (PascalCase)
(
(identifier) @type
(match? @type "^[A-Z]"))
(#match? @type "^[A-Z]"))
; Field expressions are either module content or struct fields.
; Module types and constants should already be captured, so this

@ -0,0 +1,272 @@
; Function calls
(appexpr
function: (appexpr
(atom
(qidentifier
[
(qvarid) @function
(qidop) @function
(identifier
[(varid) (idop)] @function)
])))
["(" (block) (fnexpr)])
(ntlappexpr
function: (ntlappexpr
(atom
(qidentifier
[
(qvarid) @function
(qidop) @function
(identifier
[(varid) (idop)] @function)
])))
["(" (block) (fnexpr)])
(appexpr
field: (atom
(qidentifier
[
(qvarid) @function
(qidop) @function
(identifier
[(varid) (idop)] @function)
])))
(appexpr
(appexpr
field: (atom
(qidentifier
[
(qvarid) @variable
(qidop) @variable
(identifier
[(varid) (idop)] @variable)
])))
"[")
(ntlappexpr
field: (atom
(qidentifier
[
(qvarid) @function
(qidop) @function
(identifier
[(varid) (idop)] @function)
])))
(ntlappexpr
(ntlappexpr
field: (atom
(qidentifier
[
(qvarid) @variable
(qidop) @variable
(identifier
[(varid) (idop)] @variable)
])))
"[")
[
"initially"
"finally"
] @function.special
; Function definitions
(puredecl
(funid
(identifier
[(varid) (idop)] @function)))
(fundecl
(funid
(identifier
[(varid) (idop)] @function)))
(operation
(identifier
[(varid) (idop)] @function))
; Identifiers
(puredecl
(binder
(identifier
[(varid) (idop)] @constant)))
; TODO: Highlight vars differently once helix has an appropriate highlight query
; for that purpose.
(pparameter
(pattern
(identifier
(varid) @variable.parameter)))
(paramid
(identifier
(varid) @variable.parameter))
(typedecl
"effect"
(varid) @type)
(typeid
(varid) @type)
(tbinder
(varid) @type)
(typecon
(varid) @type)
(qvarid
(qid) @namespace)
(modulepath (varid) @namespace)
(qconid) @namespace
(qidop) @namespace
(varid) @variable
(conid) @constructor
; Operators
[
"!"
"~"
"="
":="
(idop)
(op)
(qidop)
] @operator
; Keywords
[
"as"
"behind"
(externtarget)
"forall"
"handle"
"handler"
"in"
"infix"
"infixl"
"infixr"
"inject"
"mask"
"other"
"pub"
"public"
"some"
] @keyword
[
"con"
"control"
"ctl"
"fn"
"fun"
"rawctl"
"rcontrol"
] @keyword.function
"with" @keyword.control
[
"elif"
"else"
"if"
"match"
"then"
] @keyword.control.conditional
[
"import"
"include"
"module"
] @keyword.control.import
[
"alias"
"effect"
"struct"
"type"
"val"
"var"
] @keyword.storage.type
[
"abstract"
"co"
"extend"
"extern"
"fbip"
"final"
"fip"
"inline"
"linear"
"named"
"noinline"
"open"
"override"
"raw"
"rec"
"ref"
"reference"
"scoped"
"tail"
"value"
] @keyword.storage.modifier
"return" @keyword.control.return
; Delimiters
(matchrule "|" @punctuation.delimiter)
[
","
"->"
"."
":"
"::"
"<-"
";"
] @punctuation.delimiter
[
"<"
">"
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
; Literals
[
(string)
(char)
] @string
(escape) @constant.character.escape
(float) @constant.numeric.float
(int) @constant.numeric.integer
; Comment
[
(linecomment)
(blockcomment)
] @comment

@ -0,0 +1,39 @@
[
(appexpr ["[" "("]) ; Applications.
(ntlappexpr ["[" "("])
(atom ["[" "("]) ; Lists and tuples.
(program (moduledecl "{")) ; Braced module declarations.
(funbody)
(block)
(handlerexpr)
(opclausex)
] @indent
[
(typedecl
[(typeid) (opdecls)]) ; Avoid matching single-operation effects.
(externdecl)
(matchexpr)
(matchrule)
; For ifexprs, branches (once they exist) will contain blocks if they're
; indented so we just need to make sure the initial indent happens when we're
; creating them.
"then"
"else"
] @indent @extend
(matchrule "->" @indent @extend)
; Handling for error recovery.
(ERROR "fun") @indent @extend
(ERROR "match") @indent @extend
(ERROR "->" @indent.always @extend)
; Don't outdent on function parameter declarations.
(atom ")" @outdent @extend.prevent-once)
[
"]"
"}"
] @outdent @extend.prevent-once

@ -0,0 +1,2 @@
([(linecomment) (blockcomment)] @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,30 @@
(modulebody) @local.scope
(block) @local.scope
(pattern
(identifier
(varid) @local.definition))
(decl
(apattern
(pattern
(identifier
(varid) @local.definition))))
(puredecl
(funid
(identifier
(varid) @local.definition)))
(puredecl
(binder
(identifier
(varid) @local.definition)))
(decl
(binder
(identifier
(varid) @local.definition)))
(identifier (varid) @local.reference)

@ -0,0 +1,173 @@
; Identifiers
(section
.
(NAME) @namespace)
(NAME) @variable
; Operators
[
"="
"+="
"-="
"*="
"/="
"<<="
">>="
"&="
"|="
"^="
"*"
"/"
"%"
"+"
"-"
"<<"
">>"
"=="
"!="
"<="
">="
"<"
">"
"&"
"^"
"|"
"&&"
"||"
"?"
] @operator
; Keywords
[
"ABSOLUTE"
"ADDR"
"ALIGNOF"
"ASSERT"
"BYTE"
"CONSTANT"
"DATA_SEGMENT_ALIGN"
"DATA_SEGMENT_END"
"DATA_SEGMENT_RELRO_END"
"DEFINED"
"LOADADDR"
"LOG2CEIL"
"LONG"
"MAX"
"MIN"
"NEXT"
"QUAD"
"SHORT"
"SIZEOF"
"SQUAD"
"FILL"
"SEGMENT_START"
] @function.builtin
[
"CONSTRUCTORS"
"CREATE_OBJECT_SYMBOLS"
"LINKER_VERSION"
"SIZEOF_HEADERS"
] @constant.builtin
[
"AFTER"
"ALIGN"
"ALIGN_WITH_INPUT"
"ASCIZ"
"AS_NEEDED"
"AT"
"BEFORE"
"BIND"
"BLOCK"
"COPY"
"DSECT"
"ENTRY"
"EXCLUDE_FILE"
"EXTERN"
"extern"
"FLOAT"
"FORCE_COMMON_ALLOCATION"
"FORCE_GROUP_ALLOCATION"
"global"
"GROUP"
"HIDDEN"
"HLL"
"INCLUDE"
"INFO"
"INHIBIT_COMMON_ALLOCATION"
"INPUT"
"INPUT_SECTION_FLAGS"
"KEEP"
"l"
"LD_FEATURE"
"len"
"LENGTH"
"local"
"MAP"
"MEMORY"
"NOCROSSREFS"
"NOCROSSREFS_TO"
"NOFLOAT"
"NOLOAD"
"o"
"ONLY_IF_RO"
"ONLY_IF_RW"
"org"
"ORIGIN"
"OUTPUT"
"OUTPUT_ARCH"
"OUTPUT_FORMAT"
"OVERLAY"
"PHDRS"
"PROVIDE"
"PROVIDE_HIDDEN"
"READONLY"
"REGION_ALIAS"
"REVERSE"
"SEARCH_DIR"
"SECTIONS"
"SORT"
"SORT_BY_ALIGNMENT"
"SORT_BY_INIT_PRIORITY"
"SORT_BY_NAME"
"SORT_NONE"
"SPECIAL"
"STARTUP"
"SUBALIGN"
"SYSLIB"
"TARGET"
"TYPE"
"VERSION"
] @keyword
; Delimiters
[
","
";"
"&"
":"
">"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
; Literals
(INT) @constant.numeric.integer
; Comment
(comment) @comment

@ -0,0 +1,12 @@
[
(sections)
(memory)
(section)
(phdrs)
(overlay_section)
(version)
(vers_node)
(vers_defns)
] @indent
"}" @outdent @extend.prevent-once

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,123 @@
(php_tag) @tag
"?>" @tag
; Types
(primitive_type) @type.builtin
(cast_type) @type.builtin
(named_type (name) @type) @type
(named_type (qualified_name) @type) @type
; Functions
(array_creation_expression "array" @function.builtin)
(list_literal "list" @function.builtin)
(method_declaration
name: (name) @function.method)
(function_call_expression
function: [(qualified_name (name)) (name)] @function)
(scoped_call_expression
name: (name) @function)
(member_call_expression
name: (name) @function.method)
(function_definition
name: (name) @function)
; Member
(property_element
(variable_name) @variable.other.member)
(member_access_expression
name: (variable_name (name)) @variable.other.member)
(member_access_expression
name: (name) @variable.other.member)
; Variables
(relative_scope) @variable.builtin
((name) @constant
(#match? @constant "^_?[A-Z][A-Z\\d_]+$"))
((name) @constant.builtin
(#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
((name) @constructor
(#match? @constructor "^[A-Z]"))
((name) @variable.builtin
(#eq? @variable.builtin "this"))
(variable_name) @variable
; Basic tokens
[
(string)
(string_value)
(encapsed_string)
(heredoc)
(heredoc_body)
(nowdoc_body)
] @string
(boolean) @constant.builtin.boolean
(null) @constant.builtin
(integer) @constant.builtin.integer
(float) @constant.builtin.float
(comment) @comment
"$" @operator
; Keywords
"abstract" @keyword
"as" @keyword
"break" @keyword
"case" @keyword
"catch" @keyword
"class" @keyword
"const" @keyword
"continue" @keyword
"declare" @keyword
"default" @keyword
"do" @keyword
"echo" @keyword
"else" @keyword
"elseif" @keyword
"enddeclare" @keyword
"endforeach" @keyword
"endif" @keyword
"endswitch" @keyword
"endwhile" @keyword
"extends" @keyword
"final" @keyword
"finally" @keyword
"for" @keyword
"foreach" @keyword
"function" @keyword
"global" @keyword
"if" @keyword
"implements" @keyword
"include_once" @keyword
"include" @keyword
"insteadof" @keyword
"interface" @keyword
"namespace" @keyword
"new" @keyword
"private" @keyword
"protected" @keyword
"public" @keyword
"require_once" @keyword
"require" @keyword
"return" @keyword
"static" @keyword
"switch" @keyword
"throw" @keyword
"trait" @keyword
"try" @keyword
"use" @keyword
"while" @keyword

@ -0,0 +1,10 @@
((comment) @injection.content
(#set! injection.language "comment"))
(heredoc
(heredoc_body) @injection.content
(heredoc_end) @injection.language)
(nowdoc
(nowdoc_body) @injection.content
(heredoc_end) @injection.language)

@ -0,0 +1,40 @@
(namespace_definition
name: (namespace_name) @name) @module
(interface_declaration
name: (name) @name) @definition.interface
(trait_declaration
name: (name) @name) @definition.interface
(class_declaration
name: (name) @name) @definition.class
(class_interface_clause [(name) (qualified_name)] @name) @impl
(property_declaration
(property_element (variable_name (name) @name))) @definition.field
(function_definition
name: (name) @name) @definition.function
(method_declaration
name: (name) @name) @definition.function
(object_creation_expression
[
(qualified_name (name) @name)
(variable_name (name) @name)
]) @reference.class
(function_call_expression
function: [
(qualified_name (name) @name)
(variable_name (name)) @name
]) @reference.call
(scoped_call_expression
name: (name) @name) @reference.call
(member_call_expression
name: (name) @name) @reference.call

@ -172,7 +172,7 @@
(clazz (identifier) @type)
(typeAlias (identifier) @type)
((identifier) @type
(match? @type "^[A-Z]"))
(#match? @type "^[A-Z]"))
(typeArgumentList
"<" @punctuation.bracket

@ -18,7 +18,7 @@
(
((methodCallExpr (identifier) @methodName (argumentList (slStringLiteral) @injection.content))
(#set! injection.language "regex"))
(eq? @methodName "Regex"))
(#eq? @methodName "Regex"))
((lineComment) @injection.content
(#set! injection.language "comment"))

@ -376,7 +376,8 @@
(use_wildcard
(identifier) @namespace)
(extern_crate_declaration
name: (identifier) @namespace)
name: (identifier) @namespace
alias: (identifier)? @namespace)
(mod_item
name: (identifier) @namespace)
(scoped_use_list

@ -44,9 +44,9 @@
"@while" @keyword.control.repeat
((property_name) @variable
(match? @variable "^--"))
(#match? @variable "^--"))
((plain_value) @variable
(match? @variable "^--"))
(#match? @variable "^--"))
(tag_name) @tag
(universal_selector) @tag

@ -0,0 +1,7 @@
[
(function_call)
(code_block)
(function_block)
(control_structure)
] @fold

@ -0,0 +1,76 @@
(line_comment) @comment.line
(block_comment) @comment.block
(argument name: (identifier) @variable.parameter)
(local_var name: (identifier) @variable)
(environment_var name:(identifier) @variable.builtin)
(builtin_var) @constant.builtin
(function_definition name: (variable) @function)
(named_argument name: (identifier) @variable.other.member)
(method_call name: (method_name) @function.method)
(class) @keyword.storage.type
(number) @constant.numeric
(float) @constant.numeric.float
(string) @string
(symbol) @string.special.symbol
[
"&&"
"||"
"&"
"|"
"^"
"=="
"!="
"<"
"<="
">"
">="
"<<"
">>"
"+"
"-"
"*"
"/"
"%"
"="
"|@|"
"@@"
"@|@"
] @operator
[
"arg"
"classvar"
"const"
"var"
] @keyword
[
"("
")"
"["
"]"
"{"
"}"
"|"
] @punctuation.bracket
[
";"
"."
","
] @punctuation.delimiter
(control_structure) @keyword.control.conditional
(escape_sequence) @string.special
(duplicated_statement) @keyword.control.repeat

@ -0,0 +1,27 @@
(method_declaration
(block) @function.inside) @function.around
(creation_method_declaration
(block) @function.inside) @function.around
(method_declaration
((parameter) @parameter.inside . ","? @parameter.around) @parameter.around)
[
(class_declaration)
(struct_declaration)
(interface_declaration)
] @class.around
(type_arguments
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(creation_method_declaration
((parameter) @parameter.inside . ","? @parameter.around) @parameter.around)
(method_call_expression
((argument) @parameter.inside . ","? @parameter.around) @parameter.around)
(comment) @comment.inside
(comment)+ @comment.around

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save