diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5295437815027..286441b664aa5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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: "/" diff --git a/CHANGELOG.md b/CHANGELOG.md index b5edbf72b122b..de15f143c0a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,256 @@ +# 24.03 (2024-03-30) + +As always, a big thank you to all of the contributors! This release saw changes from 125 contributors. + +Breaking changes: + +- `suffix` file-types in the `file-types` key in language configuration have been removed ([#8006](https://github.com/helix-editor/helix/pull/8006)) +- The `file-types` key in language configuration no longer matches full filenames without a glob pattern ([#8006](https://github.com/helix-editor/helix/pull/8006)) + +Features: + +- Open URLs with the `goto_file` command ([#5820](https://github.com/helix-editor/helix/pull/5820)) +- Support drawing a border around popups and menus ([#4313](https://github.com/helix-editor/helix/pull/4313), [#9508](https://github.com/helix-editor/helix/pull/9508)) +- Track long lived diagnostic sources like Clippy or `rustc` ([#6447](https://github.com/helix-editor/helix/pull/6447), [#9280](https://github.com/helix-editor/helix/pull/9280)) + - This improves the handling of diagnostics from sources that only update the diagnostic positions on save. +- Add support for LSP `window/showDocument` requests ([#8865](https://github.com/helix-editor/helix/pull/8865)) +- Refactor ad-hoc hooks to use a new generic event system ([#8021](https://github.com/helix-editor/helix/pull/8021), [#9668](https://github.com/helix-editor/helix/pull/9668), [#9660](https://github.com/helix-editor/helix/pull/9660)) + - This improves the behavior of autocompletions. For example navigating in insert mode no longer automatically triggers completions. +- Allow using globs in the language configuration `file-types` key ([#8006](https://github.com/helix-editor/helix/pull/8006)) +- Allow specifying required roots for situational LSP activation ([#8696](https://github.com/helix-editor/helix/pull/8696)) +- Extend selections using mouse clicks in select mode ([#5436](https://github.com/helix-editor/helix/pull/5436)) +- Toggle block comments ([#4718](https://github.com/helix-editor/helix/pull/4718), [#9894](https://github.com/helix-editor/helix/pull/9894)) +- Support LSP diagnostic tags ([#9780](https://github.com/helix-editor/helix/pull/9780)) +- Add a `file-absolute-path` statusline element ([#4535](https://github.com/helix-editor/helix/pull/4535)) +- Cross injection layers in tree-sitter motions (`A-p`/`A-o`/`A-i`/`A-n`) ([#5176](https://github.com/helix-editor/helix/pull/5176)) +- Add a Amp-editor-like jumping command ([#8875](https://github.com/helix-editor/helix/pull/8875)) + +Commands: + +- `:move` - move buffers with LSP support ([#8584](https://github.com/helix-editor/helix/pull/8584)) + - Also see [#8949](https://github.com/helix-editor/helix/pull/8949) which made path changes conform to the LSP spec and fixed the behavior of this command. +- `page_cursor_up`, `page_cursor_down`, `page_cursor_half_up`, `page_cursor_half_down` - commands for scrolling the cursor and page together ([#8015](https://github.com/helix-editor/helix/pull/8015)) +- `:yank-diagnostic` - yank the diagnostic(s) under the primary cursor ([#9640](https://github.com/helix-editor/helix/pull/9640)) +- `select_line_above` / `select_line_below` - extend or shrink a selection based on the direction and anchor ([#9080](https://github.com/helix-editor/helix/pull/9080)) + +Usability improvements: + +- Make `roots` key of `[[language]]` entries in `languages.toml` configuration optional ([#8803](https://github.com/helix-editor/helix/pull/8803)) +- Exit select mode in commands that modify the buffer ([#8689](https://github.com/helix-editor/helix/pull/8689)) +- Use crossterm cursor when out of focus ([#6858](https://github.com/helix-editor/helix/pull/6858), [#8934](https://github.com/helix-editor/helix/pull/8934)) +- Join empty lines with only one space in `join_selections` ([#8989](https://github.com/helix-editor/helix/pull/8989)) +- Introduce a hybrid tree-sitter and contextual indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307)) +- Allow configuring the indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307)) +- Check for LSP rename support before showing rename prompt ([#9277](https://github.com/helix-editor/helix/pull/9277)) +- Normalize `S-` keymaps to uppercase ascii ([#9213](https://github.com/helix-editor/helix/pull/9213)) +- Add formatter status to `--health` output ([#7986](https://github.com/helix-editor/helix/pull/7986)) +- Change path normalization strategy to not resolve symlinks ([#9330](https://github.com/helix-editor/helix/pull/9330)) +- Select subtree within injections in `:tree-sitter-subtree` ([#9309](https://github.com/helix-editor/helix/pull/9309)) +- Use tilde expansion and normalization for `$HELIX_RUNTIME` paths ([1bc7aac](https://github.com/helix-editor/helix/commit/1bc7aac)) +- Improve failure message for LSP goto references ([#9382](https://github.com/helix-editor/helix/pull/9382)) +- Use injection syntax trees for bracket matching ([5e0b3cc](https://github.com/helix-editor/helix/commit/5e0b3cc)) +- Respect injections in `:tree-sitter-highlight-name` ([8b6565c](https://github.com/helix-editor/helix/commit/8b6565c)) +- Respect injections in `move_parent_node_end` ([035b8ea](https://github.com/helix-editor/helix/commit/035b8ea)) +- Use `gix` pipeline filter instead of manual CRLF implementation ([#9503](https://github.com/helix-editor/helix/pull/9503)) +- Follow Neovim's truecolor detection ([#9577](https://github.com/helix-editor/helix/pull/9577)) +- Reload language configuration with `:reload`, SIGHUP ([#9415](https://github.com/helix-editor/helix/pull/9415)) +- Allow numbers as bindings ([#8471](https://github.com/helix-editor/helix/pull/8471), [#9887](https://github.com/helix-editor/helix/pull/9887)) +- Respect undercurl config when terminfo is not available ([#9897](https://github.com/helix-editor/helix/pull/9897)) +- Ignore `.pijul`, `.hg`, `.jj` in addition to `.git` in file pickers configured to show hidden files ([#9935](https://github.com/helix-editor/helix/pull/9935)) +- Add completion for registers to `:clear-register` and `:yank-diagnostic` ([#9936](https://github.com/helix-editor/helix/pull/9936)) +- Repeat last motion for goto next/prev diagnostic ([#9966](https://github.com/helix-editor/helix/pull/9966)) +- Allow configuring a character to use when rendering narrow no-breaking space ([#9604](https://github.com/helix-editor/helix/pull/9604)) +- Switch to a streaming regex engine (regex-cursor crate) to significantly speed up regex-based commands and features ([#9422](https://github.com/helix-editor/helix/pull/9422), [#9756](https://github.com/helix-editor/helix/pull/9756), [#9891](https://github.com/helix-editor/helix/pull/9891)) + +Fixes: + +- Swap `*` and `+` registers ([#8703](https://github.com/helix-editor/helix/pull/8703), [#8708](https://github.com/helix-editor/helix/pull/8708)) +- Use terminfo to reset terminal cursor style ([#8591](https://github.com/helix-editor/helix/pull/8591)) +- Fix precedence of `@align` captures in indentat computation ([#8659](https://github.com/helix-editor/helix/pull/8659)) +- Only render the preview if a Picker has a preview function ([#8667](https://github.com/helix-editor/helix/pull/8667)) +- Fix the precedence of `ui.virtual.whitespace` ([#8750](https://github.com/helix-editor/helix/pull/8750), [#8879](https://github.com/helix-editor/helix/pull/8879)) +- Fix crash in `:indent-style` ([#9087](https://github.com/helix-editor/helix/pull/9087)) +- Fix `didSave` text inclusion when sync capability is a kind variant ([#9101](https://github.com/helix-editor/helix/pull/9101)) +- Update the history of newly focused views ([#9271](https://github.com/helix-editor/helix/pull/9271)) +- Initialize diagnostics when opening a document ([#8873](https://github.com/helix-editor/helix/pull/8873)) +- Sync views when applying edits to unfocused views ([#9173](https://github.com/helix-editor/helix/pull/9173)) + - This fixes crashes that could occur from LSP workspace edits or `:write-all`. +- Treat non-numeric `+arg`s passed in the CLI args as filenames ([#9333](https://github.com/helix-editor/helix/pull/9333)) +- Fix crash when using `mm` on an empty plaintext file ([2fb7e50](https://github.com/helix-editor/helix/commit/2fb7e50)) +- Ignore empty tree-sitter nodes in match bracket ([445f7a2](https://github.com/helix-editor/helix/commit/445f7a2)) +- Exit a language server if it sends a message with invalid JSON ([#9332](https://github.com/helix-editor/helix/pull/9332)) +- Handle failures to enable bracketed paste ([#9353](https://github.com/helix-editor/helix/pull/9353)) +- Gate all captures in a pattern behind `#is-not? local` predicates ([#9390](https://github.com/helix-editor/helix/pull/9390)) +- Make path changes LSP spec conformant ([#8949](https://github.com/helix-editor/helix/pull/8949)) +- Use range positions to determine `insert_newline` motion ([#9448](https://github.com/helix-editor/helix/pull/9448)) +- Fix division by zero when prompt completion area is too small ([#9524](https://github.com/helix-editor/helix/pull/9524)) +- Add changes to history in clipboard replacement typable commands ([#9625](https://github.com/helix-editor/helix/pull/9625)) +- Fix a crash in DAP with an unspecified `line` in breakpoints ([#9632](https://github.com/helix-editor/helix/pull/9632)) +- Fix space handling for filenames in bash completion ([#9702](https://github.com/helix-editor/helix/pull/9702), [#9708](https://github.com/helix-editor/helix/pull/9708)) +- Key diagnostics off of paths instead of LSP URIs ([#7367](https://github.com/helix-editor/helix/pull/7367)) +- Fix panic when using `join_selections_space` ([#9783](https://github.com/helix-editor/helix/pull/9783)) +- Fix panic when using `surround_replace`, `surround_delete` ([#9796](https://github.com/helix-editor/helix/pull/9796)) +- Fix panic in `surround_replace`, `surround_delete` with nested surrounds and multiple cursors ([#9815](https://github.com/helix-editor/helix/pull/9815)) +- Fix panic in `select_textobject_around` ([#9832](https://github.com/helix-editor/helix/pull/9832)) +- Don't stop reloading documents when reloading fails in `:reload-all` ([#9870](https://github.com/helix-editor/helix/pull/9870)) +- Prevent `shell_keep_pipe` from stopping on nonzero exit status codes ([#9817](https://github.com/helix-editor/helix/pull/9817)) + +Themes: + +- Add `gruber-dark` ([#8598](https://github.com/helix-editor/helix/pull/8598)) +- Update `everblush` ([#8705](https://github.com/helix-editor/helix/pull/8705)) +- Update `papercolor` ([#8718](https://github.com/helix-editor/helix/pull/8718), [#8827](https://github.com/helix-editor/helix/pull/8827)) +- Add `polmandres` ([#8759](https://github.com/helix-editor/helix/pull/8759)) +- Add `starlight` ([#8787](https://github.com/helix-editor/helix/pull/8787)) +- Update `naysayer` ([#8838](https://github.com/helix-editor/helix/pull/8838)) +- Add modus operandi themes ([#8728](https://github.com/helix-editor/helix/pull/8728), [#9912](https://github.com/helix-editor/helix/pull/9912)) +- Update `rose_pine` ([#8946](https://github.com/helix-editor/helix/pull/8946)) +- Update `darcula` ([#8738](https://github.com/helix-editor/helix/pull/8738), [#9002](https://github.com/helix-editor/helix/pull/9002), [#9449](https://github.com/helix-editor/helix/pull/9449), [#9588](https://github.com/helix-editor/helix/pull/9588)) +- Add modus vivendi themes ([#8894](https://github.com/helix-editor/helix/pull/8894), [#9912](https://github.com/helix-editor/helix/pull/9912)) +- Add `horizon-dark` ([#9008](https://github.com/helix-editor/helix/pull/9008), [#9493](https://github.com/helix-editor/helix/pull/9493)) +- Update `noctis` ([#9123](https://github.com/helix-editor/helix/pull/9123)) +- Update `nord` ([#9135](https://github.com/helix-editor/helix/pull/9135)) +- Update monokai pro themes ([#9148](https://github.com/helix-editor/helix/pull/9148)) +- Update tokyonight themes ([#9099](https://github.com/helix-editor/helix/pull/9099), [#9724](https://github.com/helix-editor/helix/pull/9724), [#9789](https://github.com/helix-editor/helix/pull/9789)) +- Add `ttox` ([#8524](https://github.com/helix-editor/helix/pull/8524)) +- Add `voxed` ([#9164](https://github.com/helix-editor/helix/pull/9164)) +- Update `sonokai` ([#9370](https://github.com/helix-editor/helix/pull/9370), [#9376](https://github.com/helix-editor/helix/pull/9376), [#5379](https://github.com/helix-editor/helix/pull/5379)) +- Update `onedark`, `onedarker` ([#9397](https://github.com/helix-editor/helix/pull/9397)) +- Update `cyan_light` ([#9375](https://github.com/helix-editor/helix/pull/9375), [#9688](https://github.com/helix-editor/helix/pull/9688)) +- Add `gruvbox_light_soft`, `gruvbox_light_hard` ([#9266](https://github.com/helix-editor/helix/pull/9266)) +- Update GitHub themes ([#9487](https://github.com/helix-editor/helix/pull/9487)) +- Add `term16_dark`, `term16_light` ([#9477](https://github.com/helix-editor/helix/pull/9477)) +- Update Zed themes ([#9544](https://github.com/helix-editor/helix/pull/9544), [#9549](https://github.com/helix-editor/helix/pull/9549)) +- Add `curzon` ([#9553](https://github.com/helix-editor/helix/pull/9553)) +- Add `monokai_soda` ([#9651](https://github.com/helix-editor/helix/pull/9651)) +- Update catppuccin themes ([#9859](https://github.com/helix-editor/helix/pull/9859)) +- Update `rasmus` ([#9939](https://github.com/helix-editor/helix/pull/9939)) +- Update `dark_plus` ([#9949](https://github.com/helix-editor/helix/pull/9949), [628dcd5](https://github.com/helix-editor/helix/commit/628dcd5)) +- Update gruvbox themes ([#9960](https://github.com/helix-editor/helix/pull/9960)) +- Add jump label theming to `dracula` ([#9973](https://github.com/helix-editor/helix/pull/9973)) +- Add jump label theming to `horizon-dark` ([#9984](https://github.com/helix-editor/helix/pull/9984)) +- Add jump label theming to catppuccin themes ([2178adf](https://github.com/helix-editor/helix/commit/2178adf), [#9983](https://github.com/helix-editor/helix/pull/9983)) +- Add jump label theming to `onedark` themes ([da2dec1](https://github.com/helix-editor/helix/commit/da2dec1)) +- Add jump label theming to rose-pine themes ([#9981](https://github.com/helix-editor/helix/pull/9981)) +- Add jump label theming to Nord themes ([#10008](https://github.com/helix-editor/helix/pull/10008)) +- Add jump label theming to Monokai themes ([#10009](https://github.com/helix-editor/helix/pull/10009)) +- Add jump label theming to gruvbox themes ([#10012](https://github.com/helix-editor/helix/pull/10012)) +- Add jump label theming to `kanagawa` ([#10030](https://github.com/helix-editor/helix/pull/10030)) +- Update material themes ([#10043](https://github.com/helix-editor/helix/pull/10043)) +- Add `jetbrains_dark` ([#9967](https://github.com/helix-editor/helix/pull/9967)) + +New languages: + +- Typst ([#7474](https://github.com/helix-editor/helix/pull/7474)) +- LPF ([#8536](https://github.com/helix-editor/helix/pull/8536)) +- GN ([#6969](https://github.com/helix-editor/helix/pull/6969)) +- DBML ([#8860](https://github.com/helix-editor/helix/pull/8860)) +- log ([#8916](https://github.com/helix-editor/helix/pull/8916)) +- Janet ([#9081](https://github.com/helix-editor/helix/pull/9081), [#9247](https://github.com/helix-editor/helix/pull/9247)) +- Agda ([#8285](https://github.com/helix-editor/helix/pull/8285)) +- Avro ([#9113](https://github.com/helix-editor/helix/pull/9113)) +- Smali ([#9089](https://github.com/helix-editor/helix/pull/9089)) +- HOCON ([#9203](https://github.com/helix-editor/helix/pull/9203)) +- Tact ([#9512](https://github.com/helix-editor/helix/pull/9512)) +- PKL ([#9515](https://github.com/helix-editor/helix/pull/9515)) +- CEL ([#9296](https://github.com/helix-editor/helix/pull/9296)) +- SpiceDB ([#9296](https://github.com/helix-editor/helix/pull/9296)) +- Hoon ([#9190](https://github.com/helix-editor/helix/pull/9190)) +- DockerCompose ([#9661](https://github.com/helix-editor/helix/pull/9661), [#9916](https://github.com/helix-editor/helix/pull/9916)) +- Groovy ([#9350](https://github.com/helix-editor/helix/pull/9350), [#9681](https://github.com/helix-editor/helix/pull/9681), [#9677](https://github.com/helix-editor/helix/pull/9677)) +- FIDL ([#9713](https://github.com/helix-editor/helix/pull/9713)) +- Powershell ([#9827](https://github.com/helix-editor/helix/pull/9827)) +- ld ([#9835](https://github.com/helix-editor/helix/pull/9835)) +- Hyperland config ([#9899](https://github.com/helix-editor/helix/pull/9899)) +- JSONC ([#9906](https://github.com/helix-editor/helix/pull/9906)) +- PHP Blade ([#9513](https://github.com/helix-editor/helix/pull/9513)) +- SuperCollider ([#9329](https://github.com/helix-editor/helix/pull/9329)) +- Koka ([#8727](https://github.com/helix-editor/helix/pull/8727)) +- PKGBUILD ([#9909](https://github.com/helix-editor/helix/pull/9909), [#9943](https://github.com/helix-editor/helix/pull/9943)) +- Ada ([#9908](https://github.com/helix-editor/helix/pull/9908)) +- Helm charts ([#9900](https://github.com/helix-editor/helix/pull/9900)) +- Ember.js templates ([#9902](https://github.com/helix-editor/helix/pull/9902)) +- Ohm ([#9991](https://github.com/helix-editor/helix/pull/9991)) + +Updated languages and queries: + +- Add HTML injection queries for Rust ([#8603](https://github.com/helix-editor/helix/pull/8603)) +- Switch to tree-sitter-ron for RON ([#8624](https://github.com/helix-editor/helix/pull/8624)) +- Update and improve comment highlighting ([#8564](https://github.com/helix-editor/helix/pull/8564), [#9253](https://github.com/helix-editor/helix/pull/9253), [#9800](https://github.com/helix-editor/helix/pull/9800), [#10014](https://github.com/helix-editor/helix/pull/10014)) +- Highlight type parameters in Rust ([#8660](https://github.com/helix-editor/helix/pull/8660)) +- Change KDL tree-sitter parsers ([#8652](https://github.com/helix-editor/helix/pull/8652)) +- Update tree-sitter-markdown ([#8721](https://github.com/helix-editor/helix/pull/8721), [#10039](https://github.com/helix-editor/helix/pull/10039)) +- Update tree-sitter-purescript ([#8712](https://github.com/helix-editor/helix/pull/8712)) +- Add type parameter highlighting to TypeScript, Go, Haskell, OCaml and Kotlin ([#8718](https://github.com/helix-editor/helix/pull/8718)) +- Add indentation queries for Scheme and lisps using tree-sitter-scheme ([#8720](https://github.com/helix-editor/helix/pull/8720)) +- Recognize `meson_options.txt` as Meson ([#8794](https://github.com/helix-editor/helix/pull/8794)) +- Add language server configuration for Nushell ([#8878](https://github.com/helix-editor/helix/pull/8878)) +- Recognize `musicxml` as XML ([#8935](https://github.com/helix-editor/helix/pull/8935)) +- Update tree-sitter-rescript ([#8962](https://github.com/helix-editor/helix/pull/8962)) +- Update tree-sitter-python ([#8976](https://github.com/helix-editor/helix/pull/8976)) +- Recognize `.envrc.local` and `.envrc.private` as env ([#8988](https://github.com/helix-editor/helix/pull/8988)) +- Update tree-sitter-gleam ([#9003](https://github.com/helix-editor/helix/pull/9003), [9ceeea5](https://github.com/helix-editor/helix/commit/9ceeea5)) +- Update tree-sitter-d ([#9021](https://github.com/helix-editor/helix/pull/9021)) +- Fix R-markdown language name for LSP detection ([#9012](https://github.com/helix-editor/helix/pull/9012)) +- Add haskell-language-server LSP configuration ([#9111](https://github.com/helix-editor/helix/pull/9111)) +- Recognize `glif` as XML ([#9130](https://github.com/helix-editor/helix/pull/9130)) +- Recognize `.prettierrc` as JSON ([#9214](https://github.com/helix-editor/helix/pull/9214)) +- Add auto-pairs configuration for scheme ([#9232](https://github.com/helix-editor/helix/pull/9232)) +- Add textobject queries for Scala ([#9191](https://github.com/helix-editor/helix/pull/9191)) +- Add textobject queries for Protobuf ([#9184](https://github.com/helix-editor/helix/pull/9184)) +- Update tree-sitter-wren ([#8544](https://github.com/helix-editor/helix/pull/8544)) +- Add `spago.yaml` as an LSP root for PureScript ([#9362](https://github.com/helix-editor/helix/pull/9362)) +- Improve highlight and indent queries for Bash, Make and CSS ([#9393](https://github.com/helix-editor/helix/pull/9393)) +- Update tree-sitter-scala ([#9348](https://github.com/helix-editor/helix/pull/9348), [#9340](https://github.com/helix-editor/helix/pull/9340), [#9475](https://github.com/helix-editor/helix/pull/9475)) +- Recognize `.bash_history` as Bash ([#9401](https://github.com/helix-editor/helix/pull/9401)) +- Recognize Helix ignore files as ignore ([#9447](https://github.com/helix-editor/helix/pull/9447)) +- Inject SQL into Scala SQL strings ([#9428](https://github.com/helix-editor/helix/pull/9428)) +- Update gdscript textobjects ([#9288](https://github.com/helix-editor/helix/pull/9288)) +- Update Go queries ([#9510](https://github.com/helix-editor/helix/pull/9510), [#9525](https://github.com/helix-editor/helix/pull/9525)) +- Update tree-sitter-nushell ([#9502](https://github.com/helix-editor/helix/pull/9502)) +- Update tree-sitter-unison, add indent queries ([#9505](https://github.com/helix-editor/helix/pull/9505)) +- Update tree-sitter-slint ([#9551](https://github.com/helix-editor/helix/pull/9551), [#9698](https://github.com/helix-editor/helix/pull/9698)) +- Update tree-sitter-swift ([#9586](https://github.com/helix-editor/helix/pull/9586)) +- Add `fish_indent` as formatter for fish ([78ed3ad](https://github.com/helix-editor/helix/commit/78ed3ad)) +- Recognize `zon` as Zig ([#9582](https://github.com/helix-editor/helix/pull/9582)) +- Add a formatter for Odin ([#9537](https://github.com/helix-editor/helix/pull/9537)) +- Update tree-sitter-erlang ([#9627](https://github.com/helix-editor/helix/pull/9627), [fdcd461](https://github.com/helix-editor/helix/commit/fdcd461)) +- Capture Rust fields as argument textobjects ([#9637](https://github.com/helix-editor/helix/pull/9637)) +- Improve Dart textobjects ([#9644](https://github.com/helix-editor/helix/pull/9644)) +- Recognize `tmux.conf` as a bash file-type ([#9653](https://github.com/helix-editor/helix/pull/9653)) +- Add textobjects queries for Nix ([#9659](https://github.com/helix-editor/helix/pull/9659)) +- Add textobjects queries for HCL ([#9658](https://github.com/helix-editor/helix/pull/9658)) +- Recognize osm and osc extensions as XML ([#9697](https://github.com/helix-editor/helix/pull/9697)) +- Update tree-sitter-sql ([#9634](https://github.com/helix-editor/helix/pull/9634)) +- Recognize pde Processing files as Java ([#9741](https://github.com/helix-editor/helix/pull/9741)) +- Update tree-sitter-lua ([#9727](https://github.com/helix-editor/helix/pull/9727)) +- Switch tree-sitter-nim parsers ([#9722](https://github.com/helix-editor/helix/pull/9722)) +- Recognize GTK builder ui files as XML ([#9754](https://github.com/helix-editor/helix/pull/9754)) +- Add configuration for markdown-oxide language server ([#9758](https://github.com/helix-editor/helix/pull/9758)) +- Add a shebang for elvish ([#9779](https://github.com/helix-editor/helix/pull/9779)) +- Fix precedence of Svelte TypeScript injection ([#9777](https://github.com/helix-editor/helix/pull/9777)) +- Recognize common Dockerfile file types ([#9772](https://github.com/helix-editor/helix/pull/9772)) +- Recognize NUON files as Nu ([#9839](https://github.com/helix-editor/helix/pull/9839)) +- Add textobjects for Java native functions and constructors ([#9806](https://github.com/helix-editor/helix/pull/9806)) +- Fix "braket" typeo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910)) +- Update tree-sitter-hurl ([#9775](https://github.com/helix-editor/helix/pull/9775)) +- Add textobjects queries for Vala ([#8541](https://github.com/helix-editor/helix/pull/8541)) +- Update tree-sitter-git-config ([9610254](https://github.com/helix-editor/helix/commit/9610254)) +- Recognize 'mmd' as Mermaid ([459eb9a](https://github.com/helix-editor/helix/commit/459eb9a)) +- Highlight Rust extern crate aliases ([c099dde](https://github.com/helix-editor/helix/commit/c099dde)) +- Improve parameter highlighting in C++ ([f5d95de](https://github.com/helix-editor/helix/commit/f5d95de)) +- Recognize 'rclone.conf' as INI ([#9959](https://github.com/helix-editor/helix/pull/9959)) +- Add injections for GraphQL and ERB in Ruby heredocs ([#10036](https://github.com/helix-editor/helix/pull/10036)) +- Add `main.odin` to Odin LSP roots ([#9968](https://github.com/helix-editor/helix/pull/9968)) + +Packaging: + +- Allow user overlays in Nix grammars build ([#8749](https://github.com/helix-editor/helix/pull/8749)) +- Set Cargo feature resolver to v2 ([#8917](https://github.com/helix-editor/helix/pull/8917)) +- Use workspace inheritance for common Cargo metadata ([#8925](https://github.com/helix-editor/helix/pull/8925)) +- Remove sourcehut-based tree-sitter grammars from default build ([#9316](https://github.com/helix-editor/helix/pull/9316), [#9326](https://github.com/helix-editor/helix/pull/9326)) +- Add icon to Windows executable ([#9104](https://github.com/helix-editor/helix/pull/9104)) + # 23.10 (2023-10-24) A big shout out to all the contributors! We had 118 contributors in this release. diff --git a/Cargo.lock b/Cargo.lock index a79e6555c6d8c..c402ba506f46c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,15 +62,15 @@ 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" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3d0060af21e8d11a926981cc00c6c1541aa91dd64b9f881985c3da1094425f" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "autocfg" @@ -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" @@ -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", ] @@ -282,7 +273,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 +406,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 +455,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 +525,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 +573,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 +587,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 +604,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 +634,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 +648,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 +669,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 +682,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 +694,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 +706,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 +722,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 +741,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 +762,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 +772,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 +784,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 +794,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 +805,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 +818,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 +833,8 @@ dependencies = [ "gix-lock", "gix-object", "gix-traverse", + "gix-utils", + "hashbrown 0.14.3", "itoa", "libc", "memmap2", @@ -860,9 +856,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 +867,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 +886,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 +906,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 +938,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 +951,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 +966,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 +999,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 +1013,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 +1029,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 +1044,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 +1056,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 +1084,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 +1106,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 +1120,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 +1130,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 +1140,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", @@ -1228,11 +1224,11 @@ dependencies = [ [[package]] name = "helix-core" -version = "23.10.0" +version = "24.3.0" dependencies = [ "ahash", "arc-swap", - "bitflags 2.4.2", + "bitflags 2.5.0", "chrono", "dunce", "encoding_rs", @@ -1265,7 +1261,7 @@ dependencies = [ [[package]] name = "helix-dap" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "fern", @@ -1280,7 +1276,7 @@ dependencies = [ [[package]] name = "helix-event" -version = "23.10.0" +version = "24.3.0" dependencies = [ "ahash", "anyhow", @@ -1294,7 +1290,7 @@ dependencies = [ [[package]] name = "helix-loader" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "cc", @@ -1313,7 +1309,7 @@ dependencies = [ [[package]] name = "helix-lsp" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", @@ -1336,13 +1332,13 @@ dependencies = [ [[package]] name = "helix-parsec" -version = "23.10.0" +version = "24.3.0" [[package]] name = "helix-stdx" -version = "23.10.0" +version = "24.3.0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "dunce", "etcetera", "regex-cursor", @@ -1355,7 +1351,7 @@ dependencies = [ [[package]] name = "helix-term" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", @@ -1398,9 +1394,9 @@ dependencies = [ [[package]] name = "helix-tui" -version = "23.10.0" +version = "24.3.0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cassowary", "crossterm", "helix-core", @@ -1414,7 +1410,7 @@ dependencies = [ [[package]] name = "helix-vcs" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", @@ -1430,11 +1426,11 @@ dependencies = [ [[package]] name = "helix-view" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", - "bitflags 2.4.2", + "bitflags 2.5.0", "chardetng", "clipboard-win", "crossterm", @@ -1552,9 +1548,9 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "is-docker" @@ -1648,9 +1644,9 @@ 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", @@ -1848,7 +1844,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", ] @@ -1931,9 +1927,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1989,11 +1985,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -2152,9 +2148,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smartstring" @@ -2260,18 +2256,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", @@ -2363,9 +2359,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", @@ -2374,9 +2370,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", @@ -2395,9 +2391,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", @@ -2408,8 +2404,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", @@ -2565,15 +2562,14 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "which" -version = "6.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" dependencies = [ "either", "home", - "once_cell", "rustix", - "windows-sys 0.52.0", + "winsafe", ] [[package]] @@ -2816,16 +2812,22 @@ 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", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "xtask" -version = "23.10.0" +version = "24.3.0" dependencies = [ "helix-core", "helix-loader", diff --git a/Cargo.toml b/Cargo.toml index 91f6e7cae498b..3bfc4b8e18a30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,11 +37,11 @@ package.helix-tui.opt-level = 2 package.helix-term.opt-level = 2 [workspace.dependencies] -tree-sitter = { version = "0.20", git = "https://github.com/helix-editor/tree-sitter", rev = "660481dbf71413eba5a928b0b0ab8da50c1109e0" } +tree-sitter = { version = "0.22" } nucleo = "0.2.0" [workspace.package] -version = "23.10.0" +version = "24.3.0" edition = "2021" authors = ["Blaž Hrastnik "] categories = ["editor"] diff --git a/book/src/configuration.md b/book/src/configuration.md index d8793645793c7..5e22cebf8f662 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -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 @@ -75,7 +76,7 @@ Allows configuring the statusline at the bottom of the editor. The configuration distinguishes between three areas of the status line: -`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]` +`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]` Statusline elements can be defined as follows: @@ -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 | @@ -177,7 +179,7 @@ All git related options are only enabled in a git repository. |`git-ignore` | Enables reading `.gitignore` files | `true` |`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true` |`git-exclude` | Enables reading `.git/info/exclude` files | `true` -|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`. +|`max-depth` | Set with an integer value for maximum depth to recurse | Unset by default Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files. @@ -223,7 +225,7 @@ Additionally, this setting can be used in a language config. Unless the editor setting is `false`, this will override the editor config in documents with this language. -Example `languages.toml` that adds <> and removes '' +Example `languages.toml` that adds `<>` and removes `''` ```toml [[language]] @@ -253,8 +255,8 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r | Key | Description | Default | |-----|-------------|---------| -| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline` | `"none"` | -| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below | +| `render` | Whether to render whitespace. May either be `all` or `none`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `none` | +| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below | Example @@ -265,11 +267,14 @@ render = "all" [editor.whitespace.render] space = "all" tab = "all" +nbsp = "none" +nnbsp = "none" newline = "none" [editor.whitespace.characters] space = "·" nbsp = "⍽" +nnbsp = "␣" tab = "→" newline = "⏎" tabpad = "·" # Tabs will look like "→···" (depending on tab width) diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 7792bf5941819..6bbea10aceb4c 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -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,8 @@ | beancount | ✓ | | | | | bibtex | ✓ | | | `texlab` | | bicep | ✓ | | | `bicep-langserver` | +| bitbake | ✓ | | | `bitbake-language-server` | +| blade | ✓ | | | | | blueprint | ✓ | | | `blueprint-compiler` | | c | ✓ | ✓ | ✓ | `clangd` | | c-sharp | ✓ | ✓ | | `OmniSharp` | @@ -30,7 +33,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 +61,7 @@ | git-ignore | ✓ | | | | | git-rebase | ✓ | | | | | gleam | ✓ | ✓ | | `gleam` | +| glimmer | ✓ | | | `ember-language-server` | | glsl | ✓ | ✓ | ✓ | | | gn | ✓ | | | | | go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` | @@ -65,18 +69,20 @@ | gomod | ✓ | | | `gopls` | | gotmpl | ✓ | | | `gopls` | | gowork | ✓ | | | `gopls` | -| graphql | ✓ | | | `graphql-lsp` | +| graphql | ✓ | ✓ | | `graphql-lsp` | | groovy | ✓ | | | | | hare | ✓ | | | | | haskell | ✓ | ✓ | | `haskell-language-server-wrapper` | | haskell-persistent | ✓ | | | | | hcl | ✓ | ✓ | ✓ | `terraform-ls` | | heex | ✓ | ✓ | | `elixir-ls` | +| helm | ✓ | | | `helm_ls` | | hocon | ✓ | | ✓ | | | hoon | ✓ | | | | | hosts | ✓ | | | | | html | ✓ | | | `vscode-html-language-server` | | hurl | ✓ | | ✓ | | +| hyprlang | ✓ | | ✓ | | | idris | | | | `idris2-lsp` | | iex | ✓ | | | | | ini | ✓ | | | | @@ -85,13 +91,15 @@ | javascript | ✓ | ✓ | ✓ | `typescript-language-server` | | jinja | ✓ | | | | | jsdoc | ✓ | | | | -| json | ✓ | | ✓ | `vscode-json-language-server` | +| 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 | ✓ | | ✓ | | @@ -121,6 +129,7 @@ | ocaml | ✓ | | ✓ | `ocamllsp` | | ocaml-interface | ✓ | | | `ocamllsp` | | odin | ✓ | | ✓ | `ols` | +| ohm | ✓ | ✓ | ✓ | | | opencl | ✓ | ✓ | ✓ | `clangd` | | openscad | ✓ | | | `openscad-lsp` | | org | ✓ | | | | @@ -129,6 +138,8 @@ | pem | ✓ | | | | | perl | ✓ | ✓ | ✓ | `perlnavigator` | | php | ✓ | ✓ | ✓ | `intelephense` | +| php-only | ✓ | | | | +| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` | | pkl | ✓ | | ✓ | | | po | ✓ | ✓ | | | | pod | ✓ | | | | @@ -166,6 +177,7 @@ | sshclientconfig | ✓ | | | | | starlark | ✓ | ✓ | | | | strace | ✓ | | | | +| supercollider | ✓ | | | | | svelte | ✓ | | ✓ | `svelteserver` | | sway | ✓ | ✓ | ✓ | `forc` | | swift | ✓ | | | `sourcekit-lsp` | @@ -176,7 +188,7 @@ | templ | ✓ | | | `templ` | | tfvars | ✓ | | ✓ | `terraform-ls` | | todotxt | ✓ | | | | -| toml | ✓ | | | `taplo` | +| toml | ✓ | ✓ | | `taplo` | | tsq | ✓ | | | | | tsx | ✓ | ✓ | ✓ | `typescript-language-server` | | twig | ✓ | | | | @@ -186,7 +198,7 @@ | unison | ✓ | | ✓ | | | uxntal | ✓ | | | | | v | ✓ | ✓ | ✓ | `v-analyzer` | -| vala | ✓ | | | `vala-language-server` | +| vala | ✓ | ✓ | | `vala-language-server` | | verilog | ✓ | ✓ | | `svlangserver` | | vhdl | ✓ | | | `vhdl_ls` | | vhs | ✓ | | | | diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md index 405f11c1b00a3..a31baef937e95 100644 --- a/book/src/guides/textobject.md +++ b/book/src/guides/textobject.md @@ -25,6 +25,8 @@ The following [captures][tree-sitter-captures] are recognized: | `parameter.inside` | | `comment.inside` | | `comment.around` | +| `entry.inside` | +| `entry.around` | [Example query files][textobject-examples] can be found in the helix GitHub repository. @@ -44,4 +46,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= diff --git a/book/src/keymap.md b/book/src/keymap.md index bb09b03197b8f..3a5ccca533114 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -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 `` | `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 diff --git a/book/src/themes.md b/book/src/themes.md index 04d6a69b39142..29a8c4ba82e8e 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -314,6 +314,7 @@ These scopes are used for theming the editor interface: | `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) | | `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) | | `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) | +| `ui.virtual.jump-label` | Style for virtual jump labels | | `ui.menu` | Code and command completion menus | | `ui.menu.selected` | Selected autocomplete item | | `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar | diff --git a/contrib/Helix.appdata.xml b/contrib/Helix.appdata.xml index c455b242e90ee..a46ef796be40f 100644 --- a/contrib/Helix.appdata.xml +++ b/contrib/Helix.appdata.xml @@ -6,27 +6,26 @@ Helix A post-modern text editor مُحَرِّرُ نُصُوصٍ سَابِقٌ لِعَهدِه + + Blaž Hrastnik +

Helix is a terminal-based text editor inspired by Kakoune / Neovim and written in Rust.

+

+ مُحَرِّرُ نُصُوصٍ يَعمَلُ فِي الطَّرَفِيَّة، مُستَلهَمٌ مِن Kakoune وَ Neovim وَمَكتُوبٌ بِلُغَةِ رَست البَرمَجِيَّة. +

  • Vim-like modal editing
  • +
  • تَحرِيرٌ وَضعِيٌّ شَبيهٌ بِـVim
  • Multiple selections
  • +
  • تَحدِيدَاتٌ لِلنَّصِ مُتَعَدِّدَة
  • Built-in language server support
  • +
  • دَعْمٌ مُدمَجٌ لِخَوادِمِ اللُّغَات
  • Smart, incremental syntax highlighting and code editing via tree-sitter
  • -
-
- -

- مُحَرِّرُ نُصُوصٍ يَعمَلُ فِي الطَّرَفِيَّة، مُستَلهَمٌ مِن Kakoune وَ Neovim وَمَكتُوبٌ بِلُغَةِ رَست البَرمَجِيَّة. -

-
    -
  • تَحرِيرٌ وَضعِيٌّ شَبيهٌ بِـVim
  • -
  • تَحدِيدَاتٌ لِلنَّصِ مُتَعَدِّدَة
  • -
  • دَعْمٌ مُدمَجٌ لِخَوادِمِ اللُّغَات
  • -
  • تَحرِيرُ التَّعلِيمَاتِ البَّرمَجِيَّةِ مَعَ تَمييزٍ لِلتَّركِيبِ النَّحُويِّ بِواسِطَةِ tree-sitter
  • +
  • تَحرِيرُ التَّعلِيمَاتِ البَّرمَجِيَّةِ مَعَ تَمييزٍ لِلتَّركِيبِ النَّحُويِّ بِواسِطَةِ tree-sitter
@@ -48,6 +47,9 @@ + + https://helix-editor.com/news/release-24-03-highlights/ + https://helix-editor.com/news/release-23-10-highlights/ @@ -71,9 +73,9 @@ - + keyboard - + Utility diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 5e2dbd977f759..7482262eb59e3 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -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" @@ -56,4 +56,4 @@ globset = "0.4.14" [dev-dependencies] quickcheck = { version = "1", default-features = false } -indoc = "2.0.4" +indoc = "2.0.5" diff --git a/helix-core/src/doc_formatter.rs b/helix-core/src/doc_formatter.rs index c7dc9081f5cfd..cbe2da3b697a5 100644 --- a/helix-core/src/doc_formatter.rs +++ b/helix-core/src/doc_formatter.rs @@ -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, diff --git a/helix-core/src/doc_formatter/test.rs b/helix-core/src/doc_formatter/test.rs index ac8918bb71ea0..d2b6ddc74a915 100644 --- a/helix-core/src/doc_formatter/test.rs +++ b/helix-core/src/doc_formatter/test.rs @@ -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 diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index d9e5e0224732a..7cb5cd0625d5c 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -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> { + 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> { diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index d2d4fe70ac028..0df105f1a5170 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -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( - syntax: &Syntax, - text: RopeSlice, - selection: Selection, - sibling_fn: &F, -) -> Selection -where - F: Fn(Node) -> Option, -{ - 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(node: Node, sibling_fn: F) -> Option -where - F: Fn(Node) -> Option, -{ - 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( syntax: &Syntax, text: RopeSlice, selection: Selection, - select_fn: F, + motion: F, ) -> Selection where - F: Fn(Node, usize, usize) -> Option, + 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()) }) } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index bd252deb9d2cf..579499de5e4ab 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -705,6 +705,15 @@ impl IntoIterator for Selection { } } +impl From 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( diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 3b224e1b2a7c1..78abc0b0a7407 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -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, 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, pub depth: u32, flags: LayerUpdateFlags, + parent: Option, } /// This PartialEq implementation only checks if that @@ -1544,13 +1557,7 @@ impl PartialEq for LanguageLayer { impl Hash for LanguageLayer { fn hash(&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(fmt: &mut W, node: Node) -> fmt::Result fn pretty_print_tree_impl( 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"; diff --git a/helix-core/src/syntax/tree_cursor.rs b/helix-core/src/syntax/tree_cursor.rs new file mode 100644 index 0000000000000..d9d140c9f747a --- /dev/null +++ b/helix-core/src/syntax/tree_cursor.rs @@ -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, + root: LayerId, + current: LayerId, + injection_ranges: Vec, + // 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, 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) -> Option { + 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); + } +} diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs index 11d19d4856425..1576914e3653c 100644 --- a/helix-core/src/text_annotations.rs +++ b/helix-core/src/text_annotations.rs @@ -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 { - annotations: Rc<[A]>, +struct Layer<'a, A, M> { + annotations: &'a [A], current_index: Cell, metadata: M, } -impl Clone for Layer { +impl 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 Layer { +impl 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 Layer { } } -impl From<(Rc<[A]>, M)> for Layer { - fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer { +impl<'a, A, M> From<(&'a [A], M)> for Layer<'a, A, M> { + fn from((annotations, metadata): (&'a [A], M)) -> Layer { Layer { annotations, current_index: Cell::new(0), @@ -147,13 +146,13 @@ fn reset_pos(layers: &[Layer], 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>>, - overlays: Vec>>, - line_annotations: Vec>, +pub struct TextAnnotations<'a> { + inline_annotations: Vec>>, + overlays: Vec>>, + line_annotations: Vec>, } -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, ) -> &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) -> &mut Self { + pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option) -> &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 } diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 4284b05209304..88af103889993 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -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" diff --git a/helix-stdx/src/env.rs b/helix-stdx/src/env.rs index 90a0aee8737d5..59aba0adc2ce5 100644 --- a/helix-stdx/src/env.rs +++ b/helix-stdx/src/env.rs @@ -42,8 +42,9 @@ pub fn binary_exists>(binary_name: T) -> bool { pub fn which>( binary_name: T, ) -> Result { - 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, }) } diff --git a/helix-stdx/src/path.rs b/helix-stdx/src/path.rs index 1dc4d0b2448b5..968596a703fc9 100644 --- a/helix-stdx/src/path.rs +++ b/helix-stdx/src/path.rs @@ -2,21 +2,30 @@ pub use etcetera::home_dir; use std::{ borrow::Cow, - path::{Component, Path, PathBuf}, + ffi::OsString, + path::{Component, Path, PathBuf, MAIN_SEPARATOR_STR}, }; use crate::env::current_working_dir; /// Replaces users home directory from `path` with tilde `~` if the directory /// is available, otherwise returns the path unchanged. -pub fn fold_home_dir(path: &Path) -> PathBuf { +pub fn fold_home_dir<'a, P>(path: P) -> Cow<'a, Path> +where + P: Into>, +{ + 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(MAIN_SEPARATOR_STR); + 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 +134,21 @@ pub fn canonicalize(path: impl AsRef) -> PathBuf { normalize(path) } -pub fn get_relative_path(path: impl AsRef) -> 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>, +{ + 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 +182,20 @@ pub fn get_relative_path(path: impl AsRef) -> PathBuf { /// pub fn get_truncated_path(path: impl AsRef) -> 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 diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index bc3117d205e97..b00e97488f2fa 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -41,7 +41,7 @@ crossterm = { version = "0.27", features = ["event-stream"] } signal-hook = "0.3" tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } -arc-swap = { version = "1.7.0" } +arc-swap = { version = "1.7.1" } termini = "1" # Logging @@ -83,5 +83,5 @@ helix-loader = { path = "../helix-loader" } [dev-dependencies] smallvec = "1.13" -indoc = "2.0.4" +indoc = "2.0.5" tempfile = "3.10.1" diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4ac2496ebd48d..d927d3f471b0b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,9 +10,12 @@ use tui::widgets::Row; pub use typed::*; use helix_core::{ - char_idx_at_visual_offset, comment, + char_idx_at_visual_offset, + chars::char_is_word, + comment, doc_formatter::TextFormat, - encoding, find_workspace, graphemes, + encoding, find_workspace, + graphemes::{self, next_grapheme_boundary, RevRopeGraphemes}, history::UndoKind, increment, indent, indent::IndentStyle, @@ -24,9 +27,8 @@ use helix_core::{ search::{self, CharMatcher}, selection, shellwords, surround, syntax::{BlockCommentToken, LanguageServerFeature}, - text_annotations::TextAnnotations, + text_annotations::{Overlay, TextAnnotations}, textobject, - tree_sitter::Node, unicode::width::UnicodeWidthChar, visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes, RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -57,6 +59,7 @@ use crate::{ use crate::job::{self, Jobs}; use std::{ + cmp::Ordering, collections::{HashMap, HashSet}, fmt, future::Future, @@ -300,6 +303,8 @@ impl MappableCommand { extend_line, "Select current line, if already selected, extend to another line based on the anchor", extend_line_below, "Select current line, if already selected, extend to next line", extend_line_above, "Select current line, if already selected, extend to previous line", + select_line_above, "Select current line, if already selected, extend or shrink line above based on the anchor", + select_line_below, "Select current line, if already selected, extend or shrink line below based on the anchor", extend_to_line_bounds, "Extend selection to line bounds", shrink_to_line_bounds, "Shrink selection to line bounds", delete_selection, "Delete selection", @@ -470,6 +475,8 @@ impl MappableCommand { goto_prev_comment, "Goto previous comment", goto_next_test, "Goto next test", goto_prev_test, "Goto previous test", + goto_next_entry, "Goto next pairing", + goto_prev_entry, "Goto previous pairing", goto_next_paragraph, "Goto next paragraph", goto_prev_paragraph, "Goto previous paragraph", dap_launch, "Launch debug target", @@ -500,6 +507,8 @@ impl MappableCommand { record_macro, "Record macro", replay_macro, "Replay macro", command_palette, "Open command palette", + goto_word, "Jump to a two-character label", + extend_to_word, "Extend to a two-character label", ); } @@ -615,6 +624,7 @@ fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movem &mut annotations, ) }); + drop(annotations); doc.set_selection(view.id, selection); } @@ -1635,7 +1645,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor let doc_text = doc.text().slice(..); let viewport = view.inner_area(doc); let text_fmt = doc.text_format(viewport.width, None); - let mut annotations = view.text_annotations(doc, None); + let mut annotations = view.text_annotations(&*doc, None); (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset( doc_text, view.offset.anchor, @@ -1713,6 +1723,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor let mut sel = doc.selection(view.id).clone(); let idx = sel.primary_index(); sel = sel.replace(idx, prim_sel); + drop(annotations); doc.set_selection(view.id, sel); } @@ -2435,7 +2446,6 @@ fn extend_line_below(cx: &mut Context) { fn extend_line_above(cx: &mut Context) { extend_line_impl(cx, Extend::Above); } - fn extend_line_impl(cx: &mut Context, extend: Extend) { let count = cx.count(); let (view, doc) = current!(cx.editor); @@ -2474,6 +2484,59 @@ fn extend_line_impl(cx: &mut Context, extend: Extend) { doc.set_selection(view.id, selection); } +fn select_line_below(cx: &mut Context) { + select_line_impl(cx, Extend::Below); +} +fn select_line_above(cx: &mut Context) { + select_line_impl(cx, Extend::Above); +} +fn select_line_impl(cx: &mut Context, extend: Extend) { + let mut count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text(); + let saturating_add = |a: usize, b: usize| (a + b).min(text.len_lines()); + let selection = doc.selection(view.id).clone().transform(|range| { + let (start_line, end_line) = range.line_range(text.slice(..)); + let start = text.line_to_char(start_line); + let end = text.line_to_char(saturating_add(end_line, 1)); + let direction = range.direction(); + + // Extending to line bounds is counted as one step + if range.from() != start || range.to() != end { + count = count.saturating_sub(1) + } + let (anchor_line, head_line) = match (&extend, direction) { + (Extend::Above, Direction::Forward) => (start_line, end_line.saturating_sub(count)), + (Extend::Above, Direction::Backward) => (end_line, start_line.saturating_sub(count)), + (Extend::Below, Direction::Forward) => (start_line, saturating_add(end_line, count)), + (Extend::Below, Direction::Backward) => (end_line, saturating_add(start_line, count)), + }; + let (anchor, head) = match anchor_line.cmp(&head_line) { + Ordering::Less => ( + text.line_to_char(anchor_line), + text.line_to_char(saturating_add(head_line, 1)), + ), + Ordering::Equal => match extend { + Extend::Above => ( + text.line_to_char(saturating_add(anchor_line, 1)), + text.line_to_char(head_line), + ), + Extend::Below => ( + text.line_to_char(head_line), + text.line_to_char(saturating_add(anchor_line, 1)), + ), + }, + + Ordering::Greater => ( + text.line_to_char(saturating_add(anchor_line, 1)), + text.line_to_char(head_line), + ), + }; + Range::new(anchor, head) + }); + + doc.set_selection(view.id, selection); +} fn extend_to_line_bounds(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -3384,48 +3447,55 @@ fn goto_last_diag(cx: &mut Context) { } fn goto_next_diag(cx: &mut Context) { - let (view, doc) = current!(cx.editor); + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); - let cursor_pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); + let cursor_pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); - let diag = doc - .diagnostics() - .iter() - .find(|diag| diag.range.start > cursor_pos) - .or_else(|| doc.diagnostics().first()); + let diag = doc + .diagnostics() + .iter() + .find(|diag| diag.range.start > cursor_pos) + .or_else(|| doc.diagnostics().first()); - let selection = match diag { - Some(diag) => Selection::single(diag.range.start, diag.range.end), - None => return, + let selection = match diag { + Some(diag) => Selection::single(diag.range.start, diag.range.end), + None => return, + }; + doc.set_selection(view.id, selection); }; - doc.set_selection(view.id, selection); + + cx.editor.apply_motion(motion); } fn goto_prev_diag(cx: &mut Context) { - let (view, doc) = current!(cx.editor); + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); - let cursor_pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); + let cursor_pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); - let diag = doc - .diagnostics() - .iter() - .rev() - .find(|diag| diag.range.start < cursor_pos) - .or_else(|| doc.diagnostics().last()); - - let selection = match diag { - // NOTE: the selection is reversed because we're jumping to the - // previous diagnostic. - Some(diag) => Selection::single(diag.range.end, diag.range.start), - None => return, + let diag = doc + .diagnostics() + .iter() + .rev() + .find(|diag| diag.range.start < cursor_pos) + .or_else(|| doc.diagnostics().last()); + + let selection = match diag { + // NOTE: the selection is reversed because we're jumping to the + // previous diagnostic. + Some(diag) => Selection::single(diag.range.end, diag.range.start), + None => return, + }; + doc.set_selection(view.id, selection); }; - doc.set_selection(view.id, selection); + cx.editor.apply_motion(motion) } fn goto_first_change(cx: &mut Context) { @@ -4713,18 +4783,17 @@ fn shrink_selection(cx: &mut Context) { cx.editor.apply_motion(motion); } -fn select_sibling_impl(cx: &mut Context, sibling_fn: &'static F) +fn select_sibling_impl(cx: &mut Context, sibling_fn: F) where - F: Fn(Node) -> Option, + F: Fn(&helix_core::Syntax, RopeSlice, Selection) -> Selection + 'static, { - let motion = |editor: &mut Editor| { + let motion = move |editor: &mut Editor| { let (view, doc) = current!(editor); if let Some(syntax) = doc.syntax() { let text = doc.text().slice(..); let current_selection = doc.selection(view.id); - let selection = - object::select_sibling(syntax, text, current_selection.clone(), sibling_fn); + let selection = sibling_fn(syntax, text, current_selection.clone()); doc.set_selection(view.id, selection); } }; @@ -4732,11 +4801,11 @@ where } fn select_next_sibling(cx: &mut Context) { - select_sibling_impl(cx, &|node| Node::next_sibling(&node)) + select_sibling_impl(cx, object::select_next_sibling) } fn select_prev_sibling(cx: &mut Context) { - select_sibling_impl(cx, &|node| Node::prev_sibling(&node)) + select_sibling_impl(cx, object::select_prev_sibling) } fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) { @@ -5100,6 +5169,14 @@ fn goto_prev_test(cx: &mut Context) { goto_ts_object_impl(cx, "test", Direction::Backward) } +fn goto_next_entry(cx: &mut Context) { + goto_ts_object_impl(cx, "entry", Direction::Forward) +} + +fn goto_prev_entry(cx: &mut Context) { + goto_ts_object_impl(cx, "entry", Direction::Backward) +} + fn select_textobject_around(cx: &mut Context) { select_textobject(cx, textobject::TextObject::Around); } @@ -5164,6 +5241,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { 'a' => textobject_treesitter("parameter", range), 'c' => textobject_treesitter("comment", range), 'T' => textobject_treesitter("test", range), + 'e' => textobject_treesitter("entry", range), 'p' => textobject::textobject_paragraph(text, range, objtype, count), 'm' => textobject::textobject_pair_surround_closest( text, range, objtype, count, @@ -5196,6 +5274,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { ("a", "Argument/parameter (tree-sitter)"), ("c", "Comment (tree-sitter)"), ("T", "Test (tree-sitter)"), + ("e", "Data structure entry (tree-sitter)"), ("m", "Closest surrounding pair"), (" ", "... or any character acting as a pair"), ]; @@ -5381,16 +5460,9 @@ fn shell_keep_pipe(cx: &mut Context) { for (i, range) in selection.ranges().iter().enumerate() { let fragment = range.slice(text); - let (_output, success) = match shell_impl(shell, input, Some(fragment.into())) { - Ok(result) => result, - Err(err) => { - cx.editor.set_error(err.to_string()); - return; - } - }; - - // if the process exits successfully, keep the selection - if success { + if let Err(err) = shell_impl(shell, input, Some(fragment.into())) { + log::debug!("Shell command failed: {}", err); + } else { ranges.push(*range); if i >= old_index && index.is_none() { index = Some(ranges.len() - 1); @@ -5409,7 +5481,7 @@ fn shell_keep_pipe(cx: &mut Context) { ); } -fn shell_impl(shell: &[String], cmd: &str, input: Option) -> anyhow::Result<(Tendril, bool)> { +fn shell_impl(shell: &[String], cmd: &str, input: Option) -> anyhow::Result { tokio::task::block_in_place(|| helix_lsp::block_on(shell_impl_async(shell, cmd, input))) } @@ -5417,7 +5489,7 @@ async fn shell_impl_async( shell: &[String], cmd: &str, input: Option, -) -> anyhow::Result<(Tendril, bool)> { +) -> anyhow::Result { use std::process::Stdio; use tokio::process::Command; ensure!(!shell.is_empty(), "No shell set"); @@ -5480,7 +5552,7 @@ async fn shell_impl_async( let str = std::str::from_utf8(&output.stdout) .map_err(|_| anyhow!("Process did not output valid UTF-8"))?; let tendril = Tendril::from(str); - Ok((tendril, output.status.success())) + Ok(tendril) } fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { @@ -5501,14 +5573,14 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let mut shell_output: Option = None; let mut offset = 0isize; for range in selection.ranges() { - let (output, success) = if let Some(output) = shell_output.as_ref() { - (output.clone(), true) + let output = if let Some(output) = shell_output.as_ref() { + output.clone() } else { let fragment = range.slice(text); match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { Ok(result) => { if !pipe { - shell_output = Some(result.0.clone()); + shell_output = Some(result.clone()); } result } @@ -5519,11 +5591,6 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { } }; - if !success { - cx.editor.set_error("Command failed"); - return; - } - let output_len = output.chars().count(); let (from, to, deleted_len) = match behavior { @@ -5764,3 +5831,182 @@ fn replay_macro(cx: &mut Context) { cx.editor.macro_replaying.pop(); })); } + +fn goto_word(cx: &mut Context) { + jump_to_word(cx, Movement::Move) +} + +fn extend_to_word(cx: &mut Context) { + jump_to_word(cx, Movement::Extend) +} + +fn jump_to_label(cx: &mut Context, labels: Vec, behaviour: Movement) { + let doc = doc!(cx.editor); + let alphabet = &cx.editor.config().jump_label_alphabet; + if labels.is_empty() { + return; + } + let alphabet_char = |i| { + let mut res = Tendril::new(); + res.push(alphabet[i]); + res + }; + + // Add label for each jump candidate to the View as virtual text. + let text = doc.text().slice(..); + let mut overlays: Vec<_> = labels + .iter() + .enumerate() + .flat_map(|(i, range)| { + [ + Overlay::new(range.from(), alphabet_char(i / alphabet.len())), + Overlay::new( + graphemes::next_grapheme_boundary(text, range.from()), + alphabet_char(i % alphabet.len()), + ), + ] + }) + .collect(); + overlays.sort_unstable_by_key(|overlay| overlay.char_idx); + let (view, doc) = current!(cx.editor); + doc.set_jump_labels(view.id, overlays); + + // Accept two characters matching a visible label. Jump to the candidate + // for that label if it exists. + let primary_selection = doc.selection(view.id).primary(); + let view = view.id; + let doc = doc.id(); + cx.on_next_key(move |cx, event| { + let alphabet = &cx.editor.config().jump_label_alphabet; + let Some(i ) = event.char().and_then(|ch| alphabet.iter().position(|&it| it == ch)) else { + doc_mut!(cx.editor, &doc).remove_jump_labels(view); + return; + }; + let outer = i * alphabet.len(); + // Bail if the given character cannot be a jump label. + if outer > labels.len() { + doc_mut!(cx.editor, &doc).remove_jump_labels(view); + return; + } + cx.on_next_key(move |cx, event| { + doc_mut!(cx.editor, &doc).remove_jump_labels(view); + let alphabet = &cx.editor.config().jump_label_alphabet; + let Some(inner ) = event.char().and_then(|ch| alphabet.iter().position(|&it| it == ch)) else { + return; + }; + if let Some(mut range) = labels.get(outer + inner).copied() { + range = if behaviour == Movement::Extend { + let anchor = if range.anchor < range.head { + let from = primary_selection.from(); + if range.anchor < from { + range.anchor + } else { + from + } + } else { + let to = primary_selection.to(); + if range.anchor > to { + range.anchor + } else { + to + } + }; + Range::new(anchor, range.head) + }else{ + range.with_direction(Direction::Forward) + }; + doc_mut!(cx.editor, &doc).set_selection(view, range.into()); + } + }); + }); +} + +fn jump_to_word(cx: &mut Context, behaviour: Movement) { + // Calculate the jump candidates: ranges for any visible words with two or + // more characters. + let alphabet = &cx.editor.config().jump_label_alphabet; + let jump_label_limit = alphabet.len() * alphabet.len(); + let mut words = Vec::with_capacity(jump_label_limit); + let (view, doc) = current_ref!(cx.editor); + let text = doc.text().slice(..); + + // This is not necessarily exact if there is virtual text like soft wrap. + // It's ok though because the extra jump labels will not be rendered. + let start = text.line_to_char(text.char_to_line(view.offset.anchor)); + let end = text.line_to_char(view.estimate_last_doc_line(doc) + 1); + + let primary_selection = doc.selection(view.id).primary(); + let cursor = primary_selection.cursor(text); + let mut cursor_fwd = Range::point(cursor); + let mut cursor_rev = Range::point(cursor); + if text.get_char(cursor).is_some_and(|c| !c.is_whitespace()) { + let cursor_word_end = movement::move_next_word_end(text, cursor_fwd, 1); + // single grapheme words need a specical case + if cursor_word_end.anchor == cursor { + cursor_fwd = cursor_word_end; + } + let cursor_word_start = movement::move_prev_word_start(text, cursor_rev, 1); + if cursor_word_start.anchor == next_grapheme_boundary(text, cursor) { + cursor_rev = cursor_word_start; + } + } + 'outer: loop { + let mut changed = false; + while cursor_fwd.head < end { + cursor_fwd = movement::move_next_word_end(text, cursor_fwd, 1); + // The cursor is on a word that is atleast two graphemes long and + // madeup of word characters. The latter condition is needed because + // move_next_word_end simply treats a sequence of characters from + // the same char class as a word so `=<` would also count as a word. + let add_label = RevRopeGraphemes::new(text.slice(..cursor_fwd.head)) + .take(2) + .take_while(|g| g.chars().all(char_is_word)) + .count() + == 2; + if !add_label { + continue; + } + changed = true; + // skip any leading whitespace + cursor_fwd.anchor += text + .chars_at(cursor_fwd.anchor) + .take_while(|&c| !char_is_word(c)) + .count(); + words.push(cursor_fwd); + if words.len() == jump_label_limit { + break 'outer; + } + break; + } + while cursor_rev.head > start { + cursor_rev = movement::move_prev_word_start(text, cursor_rev, 1); + // The cursor is on a word that is atleast two graphemes long and + // madeup of word characters. The latter condition is needed because + // move_prev_word_start simply treats a sequence of characters from + // the same char class as a word so `=<` would also count as a word. + let add_label = RopeGraphemes::new(text.slice(cursor_rev.head..)) + .take(2) + .take_while(|g| g.chars().all(char_is_word)) + .count() + == 2; + if !add_label { + continue; + } + changed = true; + cursor_rev.anchor -= text + .chars_at(cursor_rev.anchor) + .reversed() + .take_while(|&c| !char_is_word(c)) + .count(); + words.push(cursor_rev); + if words.len() == jump_label_limit { + break 'outer; + } + break; + } + if !changed { + break; + } + } + jump_to_label(cx, words, behaviour) +} diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index a3168dc2d5853..63d1608f928c7 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1315,11 +1315,11 @@ fn compute_inlay_hints_for_view( view_id, DocumentInlayHints { id: new_doc_inlay_hints_id, - type_inlay_hints: type_inlay_hints.into(), - parameter_inlay_hints: parameter_inlay_hints.into(), - other_inlay_hints: other_inlay_hints.into(), - padding_before_inlay_hints: padding_before_inlay_hints.into(), - padding_after_inlay_hints: padding_after_inlay_hints.into(), + type_inlay_hints, + parameter_inlay_hints, + other_inlay_hints, + padding_before_inlay_hints, + padding_after_inlay_hints, }, ); doc.inlay_hints_oudated = false; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 384db4ac358eb..0dfcdb6e9bee4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -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 @@ -2257,7 +2261,7 @@ fn run_shell_command( let args = args.join(" "); let callback = async move { - let (output, success) = shell_impl_async(&shell, &args, None).await?; + let output = shell_impl_async(&shell, &args, None).await?; let call: job::Callback = Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { if !output.is_empty() { @@ -2270,11 +2274,7 @@ fn run_shell_command( )); compositor.replace_or_push("shell", popup); } - if success { - editor.set_status("Command succeeded"); - } else { - editor.set_error("Command failed"); - } + editor.set_status("Command succeeded"); }, )); Ok(call) @@ -2423,6 +2423,14 @@ fn yank_diagnostic( 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(); @@ -2438,14 +2446,6 @@ fn yank_diagnostic( bail!("No diagnostics under primary selection"); } - let reg = match args.get(0) { - Some(s) => { - ensure!(s.chars().count() == 1, format!("Invalid register {s}")); - s.chars().next().unwrap() - } - None => '+', - }; - cx.editor.registers.write(reg, diag)?; cx.editor.set_status(format!( "Yanked {n} diagnostic{} to register {reg}", @@ -3045,7 +3045,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ aliases: &[], doc: "Clear given register. If no argument is provided, clear all registers.", fun: clear_register, - signature: CommandSignature::none(), + signature: CommandSignature::all(completers::register), }, TypableCommand { name: "redraw", @@ -3066,7 +3066,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ aliases: &[], doc: "Yank diagnostic(s) under primary cursor to register, or clipboard by default", fun: yank_diagnostic, - signature: CommandSignature::none(), + signature: CommandSignature::all(completers::register), }, ]; diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index ef5369f8505eb..1b7d9b8c0b923 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -9,7 +9,6 @@ use crate::handlers::completion::CompletionHandler; use crate::handlers::signature_help::SignatureHelpHandler; pub use completion::trigger_auto_completion; -pub use helix_view::handlers::lsp::SignatureHelpInvoked; pub use helix_view::handlers::Handlers; mod completion; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index bab662b04dd35..ffd076ad3c38c 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -58,6 +58,7 @@ pub fn default() -> HashMap { "k" => move_line_up, "j" => move_line_down, "." => goto_last_modification, + "w" => goto_word, }, ":" => command_mode, @@ -113,6 +114,7 @@ pub fn default() -> HashMap { "t" => goto_prev_class, "a" => goto_prev_parameter, "c" => goto_prev_comment, + "e" => goto_prev_entry, "T" => goto_prev_test, "p" => goto_prev_paragraph, "space" => add_newline_above, @@ -126,6 +128,7 @@ pub fn default() -> HashMap { "t" => goto_next_class, "a" => goto_next_parameter, "c" => goto_next_comment, + "e" => goto_next_entry, "T" => goto_next_test, "p" => goto_next_paragraph, "space" => add_newline_below, @@ -360,6 +363,7 @@ pub fn default() -> HashMap { "g" => { "Goto" "k" => extend_line_up, "j" => extend_line_down, + "w" => extend_to_word, }, })); let insert = keymap!({ "Insert mode" diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index cdde86ec5903c..7719a6965548d 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -20,8 +20,6 @@ mod handlers; use ignore::DirEntry; use url::Url; -pub use keymap::macros::*; - #[cfg(windows)] fn true_color() -> bool { true @@ -48,10 +46,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; } diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index dc61ca2e3b2bc..b571b83c2ddd5 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -341,6 +341,7 @@ pub struct TextRenderer<'a> { pub indent_guide_style: Style, pub newline: String, pub nbsp: String, + pub nnbsp: String, pub space: String, pub tab: String, pub virtual_tab: String, @@ -395,6 +396,11 @@ impl<'a> TextRenderer<'a> { } else { " ".to_owned() }; + let nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All { + ws_chars.nnbsp.into() + } else { + " ".to_owned() + }; let text_style = theme.get("ui.text"); @@ -405,6 +411,7 @@ impl<'a> TextRenderer<'a> { indent_guide_char: editor_config.indent_guides.character.into(), newline, nbsp, + nnbsp, space, tab, virtual_tab, @@ -448,6 +455,7 @@ impl<'a> TextRenderer<'a> { let width = grapheme.width(); let space = if is_virtual { " " } else { &self.space }; let nbsp = if is_virtual { " " } else { &self.nbsp }; + let nnbsp = if is_virtual { " " } else { &self.nnbsp }; let tab = if is_virtual { &self.virtual_tab } else { @@ -461,6 +469,7 @@ impl<'a> TextRenderer<'a> { // TODO special rendering for other whitespaces? Grapheme::Other { ref g } if g == " " => space, Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp, + Grapheme::Other { ref g } if g == "\u{202F}" => nnbsp, Grapheme::Other { ref g } => g, Grapheme::Newline => &self.newline, }; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f3bba5d1c7552..ad7aa5c5a89ac 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -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; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index a4b148af34426..b5969818cf563 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -509,4 +509,18 @@ pub mod completers { files } } + + pub fn register(editor: &Editor, input: &str) -> Vec { + 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() + } } diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index b38b8b6e3fbcd..4f379b4a1d5b9 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -11,6 +11,7 @@ use tui::{ use helix_core::Position; use helix_view::{ graphics::{Margin, Rect}, + input::{MouseEvent, MouseEventKind}, Editor, }; @@ -23,6 +24,7 @@ pub struct Popup { margin: Margin, size: (u16, u16), child_size: (u16, u16), + area: Rect, position_bias: Open, scroll: usize, auto_close: bool, @@ -40,6 +42,7 @@ impl Popup { size: (0, 0), position_bias: Open::Below, child_size: (0, 0), + area: Rect::new(0, 0, 0, 0), scroll: 0, auto_close: false, ignore_escape_key: false, @@ -146,6 +149,14 @@ impl Popup { } } + pub fn scroll_half_page_down(&mut self) { + self.scroll(self.size.1 as usize / 2, true) + } + + pub fn scroll_half_page_up(&mut self) { + self.scroll(self.size.1 as usize / 2, false) + } + /// Toggles the Popup's scrollbar. /// Consider disabling the scrollbar in case the child /// already has its own. @@ -171,12 +182,44 @@ impl Popup { // clip to viewport viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)) } + + fn handle_mouse_event( + &mut self, + &MouseEvent { + kind, + column: x, + row: y, + .. + }: &MouseEvent, + ) -> EventResult { + let mouse_is_within_popup = x >= self.area.left() + && x < self.area.right() + && y >= self.area.top() + && y < self.area.bottom(); + + if !mouse_is_within_popup { + return EventResult::Ignored(None); + } + + match kind { + MouseEventKind::ScrollDown if self.has_scrollbar => { + self.scroll_half_page_down(); + EventResult::Consumed(None) + } + MouseEventKind::ScrollUp if self.has_scrollbar => { + self.scroll_half_page_up(); + EventResult::Consumed(None) + } + _ => EventResult::Ignored(None), + } + } } impl Component for Popup { fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { let key = match event { Event::Key(event) => *event, + Event::Mouse(event) => return self.handle_mouse_event(event), Event::Resize(_, _) => { // TODO: calculate inner area, call component's handle_event with that area return EventResult::Ignored(None); @@ -200,11 +243,11 @@ impl Component for Popup { EventResult::Consumed(Some(close_fn)) } ctrl!('d') => { - self.scroll(self.size.1 as usize / 2, true); + self.scroll_half_page_down(); EventResult::Consumed(None) } ctrl!('u') => { - self.scroll(self.size.1 as usize / 2, false); + self.scroll_half_page_up(); EventResult::Consumed(None) } _ => { @@ -249,6 +292,7 @@ impl Component for Popup { fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { let area = self.area(viewport, cx.editor); + self.area = area; cx.scroll = Some(self.scroll); // clear area diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index d46c131381944..a6ee7f05dde6e 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -544,10 +544,6 @@ 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); } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 9871828ee3d05..8a87242f99e8d 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -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::>(); 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::>(); - // 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::>(); + + 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 = 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