diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index ae2e5835e..39dc08982 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 + uses: peaceiris/actions-mdbook@v2 with: mdbook-version: 'latest' # mdbook-version: '0.4.8' @@ -27,14 +27,14 @@ jobs: echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV - name: Deploy stable - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 if: startswith(github.ref, 'refs/tags/') with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./book/book - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./book/book diff --git a/CHANGELOG.md b/CHANGELOG.md index b5edbf72b..de15f143c 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 13c1ea333..017a248a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,15 +62,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[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" @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cfg-if" @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] @@ -338,6 +338,19 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dunce" version = "1.0.4" @@ -352,9 +365,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -525,9 +538,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gix" -version = "0.61.0" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e0e59a44bf00de058ee98d6ecf3c9ed8f8842c1da642258ae4120d41ded8f7" +checksum = "5631c64fb4cd48eee767bf98a3cbc5c9318ef3bb71074d4c099a2371510282b6" dependencies = [ "gix-actor", "gix-attributes", @@ -536,6 +549,7 @@ dependencies = [ "gix-config", "gix-date", "gix-diff", + "gix-dir", "gix-discover", "gix-features", "gix-filter", @@ -557,6 +571,7 @@ dependencies = [ "gix-revision", "gix-revwalk", "gix-sec", + "gix-status", "gix-submodule", "gix-tempfile", "gix-trace", @@ -648,9 +663,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.36.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62129c75e4b6229fe15fb9838cdc00c655e87105b651e4edd7c183fc5288b5d1" +checksum = "7580e05996e893347ad04e1eaceb92e1c0e6a3ffe517171af99bf6b6df0ca6e5" dependencies = [ "bstr", "gix-config-value", @@ -694,13 +709,41 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.42.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4" +checksum = "a5fbc24115b957346cd23fb0f47d830eb799c46c89cdcf2f5acc9bf2938c2d01" dependencies = [ "bstr", + "gix-command", + "gix-filter", + "gix-fs", "gix-hash", "gix-object", + "gix-path", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "imara-diff", + "thiserror", +] + +[[package]] +name = "gix-dir" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6943a1f213ad7a060a0548ece229be53f3c2151534b126446ce3533eaf5f14c" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", "thiserror", ] @@ -741,9 +784,9 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd71bf3e64d8fb5d5635d4166ca5a36fe56b292ffff06eab1d93ea47fd5beb89" +checksum = "5c0d1f01af62bfd2fb3dd291acc2b29d4ab3e96ad52a679174626508ce98ef12" dependencies = [ "bstr", "encoding_rs", @@ -762,9 +805,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634b8a743b0aae03c1a74ee0ea24e8c5136895efac64ce52b3ea106e1c6f0613" +checksum = "e2184c40e7910529677831c8b481acf788ffd92427ed21fad65b6aa637e631b8" dependencies = [ "gix-features", "gix-utils", @@ -818,9 +861,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549621f13d9ccf325a7de45506a3266af0d08f915181c5687abb5e8669bfd2e6" +checksum = "3383122cf18655ef4c097c0b935bba5eb56983947959aaf3b0ceb1949d4dd371" dependencies = [ "bitflags 2.5.0", "bstr", @@ -886,9 +929,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b55378c719693380f66d9dd21ce46721eed2981d8789fc698ec1ada6fa176e" +checksum = "e8bbb43d2fefdc4701ffdf9224844d05b136ae1b9a73c2f90710c8dd27a93503" dependencies = [ "arc-swap", "gix-date", @@ -906,9 +949,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.49.0" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6391aeaa030ad64aba346a9f5c69bb1c4e5c6fb4411705b03b40b49d8614ec30" +checksum = "b58bad27c7677fa6b587aab3a1aca0b6c97373bd371a0a4290677c838c9bcaf1" dependencies = [ "clru", "gix-chunk", @@ -926,9 +969,9 @@ dependencies = [ [[package]] name = "gix-packetline-blocking" -version = "0.17.3" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8ef6dd3ea50e26f3bf572e90c034d033c804d340cd1eb386392f184a9ba2f7" +checksum = "c31d42378a3d284732e4d589979930d0d253360eccf7ec7a80332e5ccb77e14a" dependencies = [ "bstr", "faster-hex", @@ -951,9 +994,9 @@ dependencies = [ [[package]] name = "gix-pathspec" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a96ed0e71ce9084a471fddfa74e842576a7cbf02fe8bd50388017ac461aed97" +checksum = "d479789f3abd10f68a709454ce04cd68b54092ee882c8622ae3aa1bb9bf8496c" dependencies = [ "bitflags 2.5.0", "bstr", @@ -1054,6 +1097,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "gix-status" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c413bfd2952e4ee92e48438dac3c696f3555e586a34d184a427f6bedd1e4f9" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "thiserror", +] + [[package]] name = "gix-submodule" version = "0.10.0" @@ -1075,6 +1140,7 @@ version = "13.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30" dependencies = [ + "dashmap", "gix-fs", "libc", "once_cell", @@ -1084,16 +1150,17 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b838b2db8f62c9447d483a4c28d251b67fee32741a82cb4d35e9eb4e9fdc5ab" +checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e" [[package]] name = "gix-traverse" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95aef84bc777025403a09788b1e4815c06a19332e9e5d87a955e1ed7da9bf0cf" +checksum = "f4029ec209b0cc480d209da3837a42c63801dd8548f09c1f4502c60accb62aeb" dependencies = [ + "bitflags 2.5.0", "gix-commitgraph", "gix-date", "gix-hash", @@ -1106,9 +1173,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0b24f3ecc79a5a53539de9c2e99425d0ef23feacdcf3faac983aa9a2f26849" +checksum = "0db829ebdca6180fbe32be7aed393591df6db4a72dbbc0b8369162390954d1cf" dependencies = [ "bstr", "gix-features", @@ -1120,10 +1187,11 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92" +checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" dependencies = [ + "bstr", "fastrand", "unicode-normalization", ] @@ -1140,9 +1208,9 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe78e03af9eec168eb187e05463a981c57f0a915f64b1788685a776bd2ef969c" +checksum = "359a87dfef695b5f91abb9a424c947edca82768f34acfc269659f66174a510b4" dependencies = [ "bstr", "gix-attributes", @@ -1224,7 +1292,7 @@ dependencies = [ [[package]] name = "helix-core" -version = "23.10.0" +version = "24.3.0" dependencies = [ "ahash", "arc-swap", @@ -1261,7 +1329,7 @@ dependencies = [ [[package]] name = "helix-dap" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "fern", @@ -1276,7 +1344,7 @@ dependencies = [ [[package]] name = "helix-event" -version = "23.10.0" +version = "24.3.0" dependencies = [ "ahash", "anyhow", @@ -1290,7 +1358,7 @@ dependencies = [ [[package]] name = "helix-loader" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "cc", @@ -1309,7 +1377,7 @@ dependencies = [ [[package]] name = "helix-lsp" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", @@ -1325,6 +1393,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "slotmap", "thiserror", "tokio", "tokio-stream", @@ -1332,23 +1401,26 @@ 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.5.0", "dunce", "etcetera", "regex-cursor", "ropey", + "rustix", "tempfile", "which", + "windows-sys 0.52.0", ] [[package]] name = "helix-term" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", @@ -1391,7 +1463,7 @@ dependencies = [ [[package]] name = "helix-tui" -version = "23.10.0" +version = "24.3.0" dependencies = [ "bitflags 2.5.0", "cassowary", @@ -1407,7 +1479,7 @@ dependencies = [ [[package]] name = "helix-vcs" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", @@ -1423,7 +1495,7 @@ dependencies = [ [[package]] name = "helix-view" -version = "23.10.0" +version = "24.3.0" dependencies = [ "anyhow", "arc-swap", @@ -1448,6 +1520,7 @@ dependencies = [ "serde", "serde_json", "slotmap", + "tempfile", "tokio", "tokio-stream", "toml", @@ -1544,9 +1617,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" @@ -1923,9 +1996,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", @@ -1981,9 +2054,9 @@ 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.5.0", "errno", @@ -2021,18 +2094,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -2041,9 +2114,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -2144,9 +2217,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" @@ -2325,9 +2398,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -2400,9 +2473,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.22.2" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb9c9f15eae91dcd00ee0d86a281d16e6263786991b662b34fa9632c21a046b" +checksum = "688200d842c76dd88f9a7719ecb0483f79f5a766fb1c100756d5d8a059abc71b" dependencies = [ "cc", "regex", @@ -2558,15 +2631,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,9 +2888,15 @@ 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 e56aedc8f..6206281b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,9 +39,10 @@ package.helix-term.opt-level = 2 [workspace.dependencies] tree-sitter = { version = "0.22" } nucleo = "0.2.0" +slotmap = "1.0.7" [workspace.package] -version = "23.10.0" +version = "24.3.0" edition = "2021" authors = ["Blaž Hrastnik "] categories = ["editor"] diff --git a/base16_theme.toml b/base16_theme.toml index 268a38df6..4a071a60b 100644 --- a/base16_theme.toml +++ b/base16_theme.toml @@ -28,6 +28,8 @@ "label" = "magenta" "namespace" = "magenta" "ui.help" = { fg = "white", bg = "black" } +"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "black" } "markup.heading" = "blue" "markup.list" = "red" diff --git a/book/src/configuration.md b/book/src/configuration.md index 8857af82a..5e22cebf8 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: @@ -178,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. @@ -224,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]] @@ -254,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 @@ -266,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 b74a8cdb7..8418138c8 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -1,6 +1,7 @@ | Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP | | --- | --- | --- | --- | --- | | ada | ✓ | ✓ | | `ada_language_server`, `ada_language_server` | +| adl | ✓ | ✓ | ✓ | | | agda | ✓ | | | | | astro | ✓ | | | | | awk | ✓ | ✓ | | `awk-language-server` | @@ -9,6 +10,7 @@ | beancount | ✓ | | | | | bibtex | ✓ | | | `texlab` | | bicep | ✓ | | | `bicep-langserver` | +| bitbake | ✓ | | | `bitbake-language-server` | | blade | ✓ | | | | | blueprint | ✓ | | | `blueprint-compiler` | | c | ✓ | ✓ | ✓ | `clangd` | @@ -36,6 +38,7 @@ | dockerfile | ✓ | | | `docker-langserver` | | dot | ✓ | | | `dot-language-server` | | dtd | ✓ | | | | +| earthfile | ✓ | ✓ | ✓ | `earthlyls` | | edoc | ✓ | | | | | eex | ✓ | | | | | ejs | ✓ | | | | @@ -68,7 +71,7 @@ | gomod | ✓ | | | `gopls` | | gotmpl | ✓ | | | `gopls` | | gowork | ✓ | | | `gopls` | -| graphql | ✓ | | | `graphql-lsp` | +| graphql | ✓ | ✓ | | `graphql-lsp` | | groovy | ✓ | | | | | hare | ✓ | | | | | haskell | ✓ | ✓ | | `haskell-language-server-wrapper` | @@ -80,7 +83,7 @@ | hoon | ✓ | | | | | hosts | ✓ | | | | | html | ✓ | | | `vscode-html-language-server` | -| hurl | ✓ | | ✓ | | +| hurl | ✓ | ✓ | ✓ | | | hyprlang | ✓ | | ✓ | | | idris | | | | `idris2-lsp` | | iex | ✓ | | | | @@ -90,7 +93,7 @@ | 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` | @@ -98,10 +101,11 @@ | julia | ✓ | ✓ | ✓ | `julia` | | just | ✓ | ✓ | ✓ | | | kdl | ✓ | ✓ | ✓ | | -| koka | ✓ | | ✓ | | +| koka | ✓ | | ✓ | `koka` | | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | | ld | ✓ | | ✓ | | +| ldif | ✓ | | | | | lean | ✓ | | | `lean` | | ledger | ✓ | | | | | llvm | ✓ | ✓ | ✓ | | @@ -118,6 +122,7 @@ | mermaid | ✓ | | | | | meson | ✓ | | ✓ | | | mint | | | | `mint` | +| move | ✓ | | | | | msbuild | ✓ | | ✓ | | | nasm | ✓ | ✓ | | | | nickel | ✓ | | ✓ | `nls` | @@ -128,6 +133,7 @@ | ocaml | ✓ | | ✓ | `ocamllsp` | | ocaml-interface | ✓ | | | `ocamllsp` | | odin | ✓ | | ✓ | `ols` | +| ohm | ✓ | ✓ | ✓ | | | opencl | ✓ | ✓ | ✓ | `clangd` | | openscad | ✓ | | | `openscad-lsp` | | org | ✓ | | | | @@ -135,6 +141,7 @@ | passwd | ✓ | | | | | pem | ✓ | | | | | perl | ✓ | ✓ | ✓ | `perlnavigator` | +| pest | ✓ | ✓ | ✓ | `pest-language-server` | | php | ✓ | ✓ | ✓ | `intelephense` | | php-only | ✓ | | | | | pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` | @@ -169,7 +176,7 @@ | smali | ✓ | | ✓ | | | smithy | ✓ | | | `cs` | | sml | ✓ | | | | -| solidity | ✓ | | | `solc` | +| solidity | ✓ | ✓ | | `solc` | | spicedb | ✓ | | | | | sql | ✓ | | | | | sshclientconfig | ✓ | | | | @@ -183,15 +190,16 @@ | tablegen | ✓ | ✓ | ✓ | | | tact | ✓ | ✓ | ✓ | | | task | ✓ | | | | +| tcl | ✓ | | ✓ | | | templ | ✓ | | | `templ` | | tfvars | ✓ | | ✓ | `terraform-ls` | | todotxt | ✓ | | | | -| toml | ✓ | | | `taplo` | +| toml | ✓ | ✓ | | `taplo` | | tsq | ✓ | | | | | tsx | ✓ | ✓ | ✓ | `typescript-language-server` | | twig | ✓ | | | | | typescript | ✓ | ✓ | ✓ | `typescript-language-server` | -| typst | ✓ | | | `typst-lsp` | +| typst | ✓ | | | `tinymist`, `typst-lsp` | | ungrammar | ✓ | | | | | unison | ✓ | | ✓ | | | uxntal | ✓ | | | | @@ -209,6 +217,7 @@ | wren | ✓ | ✓ | ✓ | | | xit | ✓ | | | | | xml | ✓ | | ✓ | | +| xtc | ✓ | | | | | yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` | | yuck | ✓ | | | | | zig | ✓ | ✓ | ✓ | `zls` | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index dbb8b5f38..cf1b550dc 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -87,3 +87,4 @@ | `:redraw` | Clear and re-render the whole UI | | `:move` | Move the current buffer and its corresponding file to a different path | | `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default | +| `:read`, `:r` | Load a file into buffer | diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md index b83c067e4..a31baef93 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. diff --git a/book/src/keymap.md b/book/src/keymap.md index bb09b0319..65a223efb 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -24,6 +24,8 @@ > 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type. +> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts. + ## Normal mode Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key. @@ -49,7 +51,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 +226,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 @@ -279,7 +282,8 @@ This layer is a kludge of mappings, mostly pickers. | `F` | Open file picker at current working directory | `file_picker_in_current_directory` | | `b` | Open buffer picker | `buffer_picker` | | `j` | Open jumplist picker | `jumplist_picker` | -| `g` | Debug (experimental) | N/A | +| `g` | Open changed file picker | `changed_file_picker` | +| `G` | Debug (experimental) | N/A | | `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | | `s` | Open document symbol picker (**LSP**) | `symbol_picker` | | `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | diff --git a/book/src/lang-support.md b/book/src/lang-support.md index 3f96673bc..aede33fa0 100644 --- a/book/src/lang-support.md +++ b/book/src/lang-support.md @@ -1,7 +1,7 @@ # Language Support The following languages and Language Servers are supported. To use -Language Server features, you must first [install][lsp-install-wiki] the +Language Server features, you must first [configure][lsp-config-wiki] the appropriate Language Server. You can check the language support in your installed helix version with `hx --health`. @@ -11,6 +11,6 @@ Languages][adding-languages] guide for more language configuration information. {{#include ./generated/lang-support.md}} -[lsp-install-wiki]: https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers +[lsp-config-wiki]: https://github.com/helix-editor/helix/wiki/Language-Server-Configurations [lang-config]: ./languages.md [adding-languages]: ./guides/adding_languages.md diff --git a/book/src/languages.md b/book/src/languages.md index dd93fec53..33ecbb92f 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -150,6 +150,8 @@ They have to be defined in the `[language-server]` table as described in the pre Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default. +The definition order of language servers affects the order in the results list of code action menu. + In case multiple language servers are specified in the `language-servers` attribute of a `language`, it's often useful to only enable/disable certain language-server features for these language servers. diff --git a/book/src/themes.md b/book/src/themes.md index 04d6a69b3..e3b95c0a7 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -36,13 +36,6 @@ For inspiration, you can find the default `theme.toml` user-submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes). -### Using the linter - -Use the supplied linting tool to check for errors and missing scopes: - -```sh -cargo xtask themelint onedark # replace onedark with -``` ## The details of theme creation @@ -186,6 +179,7 @@ We use a similar set of scopes as - `parameter` - Function parameters - `other` - `member` - Fields of composite data types (e.g. structs, unions) + - `private` - Private fields that use a unique syntax (currently just ECMAScript-based languages) - `label` @@ -213,6 +207,7 @@ We use a similar set of scopes as - `function` - `builtin` - `method` + - `private` - Private methods that use a unique syntax (currently just ECMAScript-based languages) - `macro` - `special` (preprocessor in C) @@ -251,6 +246,7 @@ We use a similar set of scopes as - `gutter` - gutter indicator - `delta` - modifications - `moved` - renamed or moved files/changes + - `conflict` - merge conflicts - `gutter` - gutter indicator #### Interface @@ -314,6 +310,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 c455b242e..a46ef796b 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 a99250692..16d6b457f 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -25,8 +25,7 @@ smartstring = "1.0.1" unicode-segmentation = "1.11" unicode-width = "0.1" unicode-general-category = "0.6" -# slab = "0.4.2" -slotmap = "1.0" +slotmap.workspace = true tree-sitter.workspace = true once_cell = "1.19" arc-swap = "1" @@ -56,4 +55,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/diagnostic.rs b/helix-core/src/diagnostic.rs index 52d77a9aa..d41119d38 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -1,4 +1,6 @@ //! LSP diagnostic utility types. +use std::fmt; + use serde::{Deserialize, Serialize}; /// Describes the severity level of a [`Diagnostic`]. @@ -47,8 +49,25 @@ pub struct Diagnostic { pub message: String, pub severity: Option, pub code: Option, - pub language_server_id: usize, + pub provider: DiagnosticProvider, pub tags: Vec, pub source: Option, pub data: Option, } + +// TODO turn this into an enum + feature flag when lsp becomes optional +pub type DiagnosticProvider = LanguageServerId; + +// while I would prefer having this in helix-lsp that necessitates a bunch of +// conversions I would rather not add. I think its fine since this just a very +// trivial newtype wrapper and we would need something similar once we define +// completions in core +slotmap::new_key_type! { + pub struct LanguageServerId; +} + +impl fmt::Display for LanguageServerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/helix-core/src/doc_formatter.rs b/helix-core/src/doc_formatter.rs index c7dc9081f..cbe2da3b6 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 ac8918bb7..d2b6ddc74 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 d9e5e0224..a02d6e4dd 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -278,23 +278,6 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize } } -/// Returns the passed byte index if it's already a grapheme boundary, -/// or the next grapheme boundary byte index if not. -#[must_use] -#[inline] -pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize { - if byte_idx == 0 { - byte_idx - } else { - // TODO: optimize so we're not constructing grapheme cursor twice - if is_grapheme_boundary_byte(slice, byte_idx) { - byte_idx - } else { - next_grapheme_boundary_byte(slice, byte_idx) - } - } -} - /// Returns whether the given char position is a grapheme boundary. #[must_use] pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool { @@ -425,6 +408,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/indent.rs b/helix-core/src/indent.rs index 2a0a3876c..ca72fc3a2 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -247,41 +247,18 @@ fn add_indent_level( } } -/// Computes for node and all ancestors whether they are the first node on their line. -/// The first entry in the return value represents the root node, the last one the node itself -fn get_first_in_line(mut node: Node, new_line_byte_pos: Option) -> Vec { - let mut first_in_line = Vec::new(); - loop { - if let Some(prev) = node.prev_sibling() { - // If we insert a new line, the first node at/after the cursor is considered to be the first in its line - let first = prev.end_position().row != node.start_position().row - || new_line_byte_pos.map_or(false, |byte_pos| { - node.start_byte() >= byte_pos && prev.start_byte() < byte_pos - }); - first_in_line.push(Some(first)); - } else { - // Nodes that have no previous siblings are first in their line if and only if their parent is - // (which we don't know yet) - first_in_line.push(None); - } - if let Some(parent) = node.parent() { - node = parent; - } else { - break; - } - } - - let mut result = Vec::with_capacity(first_in_line.len()); - let mut parent_is_first = true; // The root node is by definition the first node in its line - for first in first_in_line.into_iter().rev() { - if let Some(first) = first { - result.push(first); - parent_is_first = first; - } else { - result.push(parent_is_first); +/// Return true if only whitespace comes before the node on its line. +/// If given, new_line_byte_pos is treated the same way as any existing newline. +fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option) -> bool { + let mut line_start_byte_pos = text.line_to_byte(node.start_position().row); + if let Some(pos) = new_line_byte_pos { + if line_start_byte_pos < pos && pos <= node.start_byte() { + line_start_byte_pos = pos; } } - result + text.byte_slice(line_start_byte_pos..node.start_byte()) + .chars() + .all(|c| c.is_whitespace()) } /// The total indent for some line of code. @@ -852,7 +829,6 @@ pub fn treesitter_indent_for_pos<'a>( byte_pos, new_line_byte_pos, )?; - let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos)); let mut result = Indentation::default(); // We always keep track of all the indent changes on one line, in order to only indent once @@ -861,9 +837,7 @@ pub fn treesitter_indent_for_pos<'a>( let mut indent_for_line_below = Indentation::default(); loop { - // This can safely be unwrapped because `first_in_line` contains - // one entry for each ancestor of the node (which is what we iterate over) - let is_first = *first_in_line.last().unwrap(); + let is_first = is_first_in_line(node, text, new_line_byte_pos); // Apply all indent definitions for this node. // Since we only iterate over each node once, we can remove the @@ -906,7 +880,6 @@ pub fn treesitter_indent_for_pos<'a>( } node = parent; - first_in_line.pop(); } else { // Only add the indentation for the line below if that line // is not after the line that the indentation is calculated for. diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index b8bcc28ca..95d6a3dc4 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -9,16 +9,32 @@ use crate::Syntax; const MAX_PLAINTEXT_SCAN: usize = 10000; const MATCH_LIMIT: usize = 16; -// Limit matching pairs to only ( ) { } [ ] < > ' ' " " -const PAIRS: &[(char, char)] = &[ +pub const BRACKETS: [(char, char); 7] = [ ('(', ')'), ('{', '}'), ('[', ']'), ('<', '>'), - ('\'', '\''), - ('\"', '\"'), + ('«', '»'), + ('「', '」'), + ('(', ')'), ]; +// The difference between BRACKETS and PAIRS is that we can find matching +// BRACKETS in a plain text file, but we can't do the same for PAIRs. +// PAIRS also contains all BRACKETS. +pub const PAIRS: [(char, char); BRACKETS.len() + 3] = { + let mut pairs = [(' ', ' '); BRACKETS.len() + 3]; + let mut idx = 0; + while idx < BRACKETS.len() { + pairs[idx] = BRACKETS[idx]; + idx += 1; + } + pairs[idx] = ('"', '"'); + pairs[idx + 1] = ('\'', '\''); + pairs[idx + 2] = ('`', '`'); + pairs +}; + /// Returns the position of the matching bracket under cursor. /// /// If the cursor is on the opening bracket, the position of @@ -30,7 +46,7 @@ const PAIRS: &[(char, char)] = &[ /// If no matching bracket is found, `None` is returned. #[must_use] pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option { - if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) { + if pos >= doc.len_chars() || !is_valid_pair(doc.char(pos)) { return None; } find_pair(syntax, doc, pos, false) @@ -67,7 +83,7 @@ fn find_pair( let (start_byte, end_byte) = surrounding_bytes(doc, &node)?; let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte)); - if is_valid_pair(doc, start_char, end_char) { + if is_valid_pair_on_pos(doc, start_char, end_char) { if end_byte == pos { return Some(start_char); } @@ -140,14 +156,22 @@ fn find_pair( /// If no matching bracket is found, `None` is returned. #[must_use] pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option { - // Don't do anything when the cursor is not on top of a bracket. let bracket = doc.get_char(cursor_pos)?; + let matching_bracket = { + let pair = get_pair(bracket); + if pair.0 == bracket { + pair.1 + } else { + pair.0 + } + }; + // Don't do anything when the cursor is not on top of a bracket. if !is_valid_bracket(bracket) { return None; } // Determine the direction of the matching. - let is_fwd = is_forward_bracket(bracket); + let is_fwd = is_open_bracket(bracket); let chars_iter = if is_fwd { doc.chars_at(cursor_pos + 1) } else { @@ -159,19 +183,7 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() { if candidate == bracket { open_cnt += 1; - } else if is_valid_pair( - doc, - if is_fwd { - cursor_pos - } else { - cursor_pos - i - 1 - }, - if is_fwd { - cursor_pos + i + 1 - } else { - cursor_pos - }, - ) { + } else if candidate == matching_bracket { // Return when all pending brackets have been closed. if open_cnt == 1 { return Some(if is_fwd { @@ -187,15 +199,49 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt None } -fn is_valid_bracket(c: char) -> bool { - PAIRS.iter().any(|(l, r)| *l == c || *r == c) +/// Returns the open and closing chars pair. If not found in +/// [`BRACKETS`] returns (ch, ch). +/// +/// ``` +/// use helix_core::match_brackets::get_pair; +/// +/// assert_eq!(get_pair('['), ('[', ']')); +/// assert_eq!(get_pair('}'), ('{', '}')); +/// assert_eq!(get_pair('"'), ('"', '"')); +/// ``` +pub fn get_pair(ch: char) -> (char, char) { + PAIRS + .iter() + .find(|(open, close)| *open == ch || *close == ch) + .copied() + .unwrap_or((ch, ch)) +} + +pub fn is_open_bracket(ch: char) -> bool { + BRACKETS.iter().any(|(l, _)| *l == ch) +} + +pub fn is_close_bracket(ch: char) -> bool { + BRACKETS.iter().any(|(_, r)| *r == ch) +} + +pub fn is_valid_bracket(ch: char) -> bool { + BRACKETS.iter().any(|(l, r)| *l == ch || *r == ch) +} + +pub fn is_open_pair(ch: char) -> bool { + PAIRS.iter().any(|(l, _)| *l == ch) +} + +pub fn is_close_pair(ch: char) -> bool { + PAIRS.iter().any(|(_, r)| *r == ch) } -fn is_forward_bracket(c: char) -> bool { - PAIRS.iter().any(|(l, _)| *l == c) +pub fn is_valid_pair(ch: char) -> bool { + PAIRS.iter().any(|(l, r)| *l == ch || *r == ch) } -fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool { +fn is_valid_pair_on_pos(doc: RopeSlice, start_char: usize, end_char: usize) -> bool { PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) } diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index d2d4fe70a..17a393caf 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,76 +1,137 @@ -use crate::{Range, RopeSlice, Selection, Syntax}; -use tree_sitter::Node; +use crate::{movement::Direction, 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(); + }, + None, + ) } -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; + } + } + }, + Some(Direction::Forward), + ) +} + +pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + selection.transform_iter(|range| { + let mut cursor = syntax.walk(); + let (from, to) = range.into_byte_range(text); + cursor.reset_to_byte_range(from, to); + + if !cursor.goto_parent_with(|parent| parent.child_count() > 1) { + return vec![range].into_iter(); + } + + select_children(&mut cursor, text, range).into_iter() }) } -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_all_children(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + selection.transform_iter(|range| { + let mut cursor = syntax.walk(); + let (from, to) = range.into_byte_range(text); + cursor.reset_to_byte_range(from, to); + select_children(&mut cursor, text, range).into_iter() }) } +fn select_children<'n>( + cursor: &'n mut TreeCursor<'n>, + text: RopeSlice, + range: Range, +) -> Vec { + let children = cursor + .named_children() + .map(|child| Range::from_node(child, text, range.direction())) + .collect::>(); + + if !children.is_empty() { + children + } else { + vec![range] + } +} + +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; + } + } + }, + Some(Direction::Backward), + ) +} + fn select_node_impl( syntax: &Syntax, text: RopeSlice, selection: Selection, - select_fn: F, + motion: F, + direction: Option, ) -> 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(direction.unwrap_or_else(|| range.direction())) }) } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index bd252deb9..48eaf289c 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -14,6 +14,7 @@ use crate::{ use helix_stdx::rope::{self, RopeSliceExt}; use smallvec::{smallvec, SmallVec}; use std::borrow::Cow; +use tree_sitter::Node; /// A single selection range. /// @@ -73,6 +74,12 @@ impl Range { Self::new(head, head) } + pub fn from_node(node: Node, text: RopeSlice, direction: Direction) -> Self { + let from = text.byte_to_char(node.start_byte()); + let to = text.byte_to_char(node.end_byte()); + Range::new(from, to).with_direction(direction) + } + /// Start of the range. #[inline] #[must_use] @@ -115,7 +122,7 @@ impl Range { } /// `Direction::Backward` when head < anchor. - /// `Direction::Backward` otherwise. + /// `Direction::Forward` otherwise. #[inline] #[must_use] pub fn direction(&self) -> Direction { @@ -376,6 +383,12 @@ impl Range { let second = graphemes.next(); first.is_some() && second.is_none() } + + /// Converts this char range into an in order byte range, discarding + /// direction. + pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) { + (text.char_to_byte(self.from()), text.char_to_byte(self.to())) + } } impl From<(usize, usize)> for Range { @@ -705,6 +718,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( @@ -774,7 +796,9 @@ pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection { let mut start = sel_start; for line in sel.slice(text).lines() { - let Some(line_ending) = get_line_ending(&line) else { break }; + let Some(line_ending) = get_line_ending(&line) else { + break; + }; let line_end = start + line.len_chars(); // TODO: retain range direction result.push(Range::new(start, line_end - line_ending.len_chars())); diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index ed9764883..e45346c92 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -1,18 +1,16 @@ use std::fmt::Display; -use crate::{movement::Direction, search, Range, Selection}; +use crate::{ + graphemes::next_grapheme_boundary, + match_brackets::{ + find_matching_bracket, find_matching_bracket_fuzzy, get_pair, is_close_bracket, + is_open_bracket, + }, + movement::Direction, + search, Range, Selection, Syntax, +}; use ropey::RopeSlice; -pub const PAIRS: &[(char, char)] = &[ - ('(', ')'), - ('[', ']'), - ('{', '}'), - ('<', '>'), - ('«', '»'), - ('「', '」'), - ('(', ')'), -]; - #[derive(Debug, PartialEq, Eq)] pub enum Error { PairNotFound, @@ -34,32 +32,68 @@ impl Display for Error { type Result = std::result::Result; -/// Given any char in [PAIRS], return the open and closing chars. If not found in -/// [PAIRS] return (ch, ch). +/// Finds the position of surround pairs of any [`crate::match_brackets::PAIRS`] +/// using tree-sitter when possible. /// -/// ``` -/// use helix_core::surround::get_pair; +/// # Returns /// -/// assert_eq!(get_pair('['), ('[', ']')); -/// assert_eq!(get_pair('}'), ('{', '}')); -/// assert_eq!(get_pair('"'), ('"', '"')); -/// ``` -pub fn get_pair(ch: char) -> (char, char) { - PAIRS - .iter() - .find(|(open, close)| *open == ch || *close == ch) - .copied() - .unwrap_or((ch, ch)) +/// Tuple `(anchor, head)`, meaning it is not always ordered. +pub fn find_nth_closest_pairs_pos( + syntax: Option<&Syntax>, + text: RopeSlice, + range: Range, + skip: usize, +) -> Result<(usize, usize)> { + match syntax { + Some(syntax) => find_nth_closest_pairs_ts(syntax, text, range, skip), + None => find_nth_closest_pairs_plain(text, range, skip), + } } -pub fn find_nth_closest_pairs_pos( +fn find_nth_closest_pairs_ts( + syntax: &Syntax, text: RopeSlice, range: Range, mut skip: usize, ) -> Result<(usize, usize)> { - let is_open_pair = |ch| PAIRS.iter().any(|(open, _)| *open == ch); - let is_close_pair = |ch| PAIRS.iter().any(|(_, close)| *close == ch); + let mut opening = range.from(); + // We want to expand the selection if we are already on the found pair, + // otherwise we would need to subtract "-1" from "range.to()". + let mut closing = range.to(); + + while skip > 0 { + closing = find_matching_bracket_fuzzy(syntax, text, closing).ok_or(Error::PairNotFound)?; + opening = find_matching_bracket(syntax, text, closing).ok_or(Error::PairNotFound)?; + // If we're already on a closing bracket "find_matching_bracket_fuzzy" will return + // the position of the opening bracket. + if closing < opening { + (opening, closing) = (closing, opening); + } + + // In case found brackets are partially inside current selection. + if range.from() < opening || closing < range.to() - 1 { + closing = next_grapheme_boundary(text, closing); + } else { + skip -= 1; + if skip != 0 { + closing = next_grapheme_boundary(text, closing); + } + } + } + + // Keep the original direction. + if let Direction::Forward = range.direction() { + Ok((opening, closing)) + } else { + Ok((closing, opening)) + } +} +fn find_nth_closest_pairs_plain( + text: RopeSlice, + range: Range, + mut skip: usize, +) -> Result<(usize, usize)> { let mut stack = Vec::with_capacity(2); let pos = range.from(); let mut close_pos = pos.saturating_sub(1); @@ -67,7 +101,7 @@ pub fn find_nth_closest_pairs_pos( for ch in text.chars_at(pos) { close_pos += 1; - if is_open_pair(ch) { + if is_open_bracket(ch) { // Track open pairs encountered so that we can step over // the corresponding close pairs that will come up further // down the loop. We want to find a lone close pair whose @@ -76,7 +110,7 @@ pub fn find_nth_closest_pairs_pos( continue; } - if !is_close_pair(ch) { + if !is_close_bracket(ch) { // We don't care if this character isn't a brace pair item, // so short circuit here. continue; @@ -157,7 +191,11 @@ pub fn find_nth_pairs_pos( ) }; - Option::zip(open, close).ok_or(Error::PairNotFound) + // preserve original direction + match range.direction() { + Direction::Forward => Option::zip(open, close).ok_or(Error::PairNotFound), + Direction::Backward => Option::zip(close, open).ok_or(Error::PairNotFound), + } } fn find_nth_open_pair( @@ -249,6 +287,7 @@ fn find_nth_close_pair( /// are automatically detected around each cursor (note that this may result /// in them selecting different surround characters for each selection). pub fn get_surround_pos( + syntax: Option<&Syntax>, text: RopeSlice, selection: &Selection, ch: Option, @@ -257,9 +296,13 @@ pub fn get_surround_pos( let mut change_pos = Vec::new(); for &range in selection { - let (open_pos, close_pos) = match ch { - Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?, - None => find_nth_closest_pairs_pos(text, range, skip)?, + let (open_pos, close_pos) = { + let range_raw = match ch { + Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?, + None => find_nth_closest_pairs_pos(syntax, text, range, skip)?, + }; + let range = Range::new(range_raw.0, range_raw.1); + (range.from(), range.to()) }; if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) { return Err(Error::CursorOverlap); @@ -288,7 +331,7 @@ mod test { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('('), 1).unwrap(), + get_surround_pos(None, doc.slice(..), &selection, Some('('), 1).unwrap(), expectations ); } @@ -303,7 +346,7 @@ mod test { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('('), 1), + get_surround_pos(None, doc.slice(..), &selection, Some('('), 1), Err(Error::PairNotFound) ); } @@ -318,7 +361,7 @@ mod test { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('('), 1), + get_surround_pos(None, doc.slice(..), &selection, Some('('), 1), Err(Error::PairNotFound) // overlapping surround chars ); } @@ -333,7 +376,7 @@ mod test { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('['), 1), + get_surround_pos(None, doc.slice(..), &selection, Some('['), 1), Err(Error::CursorOverlap) ); } @@ -397,7 +440,7 @@ mod test { ); assert_eq!( - find_nth_closest_pairs_pos(doc.slice(..), selection.primary(), 1), + find_nth_closest_pairs_pos(None, doc.slice(..), selection.primary(), 1), Err(Error::PairNotFound) ) } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 0e8917191..3cf818f60 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, @@ -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>, @@ -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 @@ -1720,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; @@ -2654,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(); @@ -2752,10 +2765,10 @@ mod test { ) }; - test("quantified_nodes", 1..36); + test("quantified_nodes", 1..37); // NOTE: Enable after implementing proper node group capturing - // test("quantified_nodes_grouped", 1..36); - // test("multiple_nodes_grouped", 1..36); + // test("quantified_nodes_grouped", 1..37); + // test("multiple_nodes_grouped", 1..37); } #[test] @@ -2926,7 +2939,7 @@ mod test { #[test] fn test_pretty_print() { - let source = r#"/// Hello"#; + let source = r#"// Hello"#; assert_pretty_print("rust", source, "(line_comment)", 0, source.len()); // A large tree should be indented with fields: @@ -2945,7 +2958,8 @@ mod test { " (macro_invocation\n", " macro: (identifier)\n", " (token_tree\n", - " (string_literal))))))", + " (string_literal\n", + " (string_content)))))))", ), 0, source.len(), @@ -2964,7 +2978,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 000000000..692d5890a --- /dev/null +++ b/helix-core/src/syntax/tree_cursor.rs @@ -0,0 +1,264 @@ +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 + } + + pub fn goto_parent_with

(&mut self, predicate: P) -> bool + where + P: Fn(&Node) -> bool, + { + while self.goto_parent() { + if predicate(&self.node()) { + return true; + } + } + + false + } + + /// 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)) + } + + fn goto_first_child_impl(&mut self, named: bool) -> 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(); + return true; + } + + let child = if named { + self.cursor.named_child(0) + } else { + self.cursor.child(0) + }; + + if let Some(child) = child { + // Otherwise descend in the current tree. + self.cursor = child; + true + } else { + false + } + } + + pub fn goto_first_child(&mut self) -> bool { + self.goto_first_child_impl(false) + } + + pub fn goto_first_named_child(&mut self) -> bool { + self.goto_first_child_impl(true) + } + + fn goto_next_sibling_impl(&mut self, named: bool) -> bool { + let sibling = if named { + self.cursor.next_named_sibling() + } else { + self.cursor.next_sibling() + }; + + if let Some(sibling) = sibling { + self.cursor = sibling; + true + } else { + false + } + } + + pub fn goto_next_sibling(&mut self) -> bool { + self.goto_next_sibling_impl(false) + } + + pub fn goto_next_named_sibling(&mut self) -> bool { + self.goto_next_sibling_impl(true) + } + + fn goto_prev_sibling_impl(&mut self, named: bool) -> bool { + let sibling = if named { + self.cursor.prev_named_sibling() + } else { + self.cursor.prev_sibling() + }; + + if let Some(sibling) = sibling { + self.cursor = sibling; + true + } else { + false + } + } + + pub fn goto_prev_sibling(&mut self) -> bool { + self.goto_prev_sibling_impl(false) + } + + pub fn goto_prev_named_sibling(&mut self) -> bool { + self.goto_prev_sibling_impl(true) + } + + /// 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); + } + + /// Returns an iterator over the children of the node the TreeCursor is on + /// at the time this is called. + pub fn children(&'a mut self) -> ChildIter { + let parent = self.node(); + + ChildIter { + cursor: self, + parent, + named: false, + } + } + + /// Returns an iterator over the named children of the node the TreeCursor is on + /// at the time this is called. + pub fn named_children(&'a mut self) -> ChildIter { + let parent = self.node(); + + ChildIter { + cursor: self, + parent, + named: true, + } + } +} + +pub struct ChildIter<'n> { + cursor: &'n mut TreeCursor<'n>, + parent: Node<'n>, + named: bool, +} + +impl<'n> Iterator for ChildIter<'n> { + type Item = Node<'n>; + + fn next(&mut self) -> Option { + // first iteration, just visit the first child + if self.cursor.node() == self.parent { + self.cursor + .goto_first_child_impl(self.named) + .then(|| self.cursor.node()) + } else { + self.cursor + .goto_next_sibling_impl(self.named) + .then(|| self.cursor.node()) + } + } +} diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs index 11d19d485..1576914e3 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-core/src/textobject.rs b/helix-core/src/textobject.rs index bf00a4580..7576b3a78 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -7,9 +7,9 @@ use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; use crate::line_ending::rope_is_line_ending; use crate::movement::Direction; -use crate::surround; use crate::syntax::LanguageConfiguration; use crate::Range; +use crate::{surround, Syntax}; fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize { use CharCategory::{Eol, Whitespace}; @@ -199,25 +199,28 @@ pub fn textobject_paragraph( } pub fn textobject_pair_surround( + syntax: Option<&Syntax>, slice: RopeSlice, range: Range, textobject: TextObject, ch: char, count: usize, ) -> Range { - textobject_pair_surround_impl(slice, range, textobject, Some(ch), count) + textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count) } pub fn textobject_pair_surround_closest( + syntax: Option<&Syntax>, slice: RopeSlice, range: Range, textobject: TextObject, count: usize, ) -> Range { - textobject_pair_surround_impl(slice, range, textobject, None, count) + textobject_pair_surround_impl(syntax, slice, range, textobject, None, count) } fn textobject_pair_surround_impl( + syntax: Option<&Syntax>, slice: RopeSlice, range: Range, textobject: TextObject, @@ -226,8 +229,7 @@ fn textobject_pair_surround_impl( ) -> Range { let pair_pos = match ch { Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count), - // Automatically find the closest surround pairs - None => surround::find_nth_closest_pairs_pos(slice, range, count), + None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count), }; pair_pos .map(|(anchor, head)| match textobject { @@ -574,7 +576,8 @@ mod test { let slice = doc.slice(..); for &case in scenario { let (pos, objtype, expected_range, ch, count) = case; - let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count); + let result = + textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count); assert_eq!( result, expected_range.into(), diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index 18af13ae7..2f5b3d0fc 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -2,7 +2,7 @@ use crate::{ requests::DisconnectArguments, transport::{Payload, Request, Response, Transport}, types::*, - Error, Result, ThreadId, + Error, Result, }; use helix_core::syntax::DebuggerQuirks; diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 93488e452..badb9bd64 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -126,7 +126,7 @@ pub fn config_dir() -> PathBuf { pub fn cache_dir() -> PathBuf { // TODO: allow env var override - let strategy = choose_base_strategy().expect("Unable to find the config directory!"); + let strategy = choose_base_strategy().expect("Unable to find the cache directory!"); let mut path = strategy.cache_dir(); path.push("helix"); path diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 88af10388..f87f15f21 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -27,7 +27,8 @@ lsp-types = { version = "0.95" } 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 = { version = "1.37", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.15" parking_lot = "0.12.1" arc-swap = "1" +slotmap.workspace = true diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index a7b3989dd..254625a3a 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -2,11 +2,11 @@ use crate::{ file_operations::FileOperationsInterest, find_lsp_workspace, jsonrpc, transport::{Payload, Transport}, - Call, Error, OffsetEncoding, Result, + Call, Error, LanguageServerId, OffsetEncoding, Result, }; use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope}; -use helix_loader::{self, VERSION_AND_GIT_HASH}; +use helix_loader::VERSION_AND_GIT_HASH; use helix_stdx::path; use lsp::{ notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport, @@ -46,7 +46,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder { #[derive(Debug)] pub struct Client { - id: usize, + id: LanguageServerId, name: String, _process: Child, server_tx: UnboundedSender, @@ -179,10 +179,14 @@ impl Client { server_environment: HashMap, root_path: PathBuf, root_uri: Option, - id: usize, + id: LanguageServerId, name: String, req_timeout: u64, - ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { + ) -> Result<( + Self, + UnboundedReceiver<(LanguageServerId, Call)>, + Arc, + )> { // Resolve path to the binary let cmd = helix_stdx::env::which(cmd)?; @@ -234,7 +238,7 @@ impl Client { &self.name } - pub fn id(&self) -> usize { + pub fn id(&self) -> LanguageServerId { self.id } @@ -393,6 +397,16 @@ impl Client { &self, params: R::Params, ) -> impl Future> + where + R::Params: serde::Serialize, + { + self.call_with_ref::(¶ms) + } + + fn call_with_ref( + &self, + params: &R::Params, + ) -> impl Future> where R::Params: serde::Serialize, { @@ -401,7 +415,7 @@ impl Client { fn call_with_timeout( &self, - params: R::Params, + params: &R::Params, timeout_secs: u64, ) -> impl Future> where @@ -410,17 +424,16 @@ impl Client { let server_tx = self.server_tx.clone(); let id = self.next_request_id(); + let params = serde_json::to_value(params); async move { use std::time::Duration; use tokio::time::timeout; - let params = serde_json::to_value(params)?; - let request = jsonrpc::MethodCall { jsonrpc: Some(jsonrpc::Version::V2), id: id.clone(), method: R::METHOD.to_string(), - params: Self::value_into_params(params), + params: Self::value_into_params(params?), }; let (tx, mut rx) = channel::>(1); @@ -737,7 +750,7 @@ impl Client { new_uri: url_from_path(new_path)?, }]; let request = self.call_with_timeout::( - lsp::RenameFilesParams { files }, + &lsp::RenameFilesParams { files }, 5, ); @@ -1022,21 +1035,10 @@ impl Client { pub fn resolve_completion_item( &self, - completion_item: lsp::CompletionItem, - ) -> Option>> { - let capabilities = self.capabilities.get().unwrap(); - - // Return early if the server does not support resolving completion items. - match capabilities.completion_provider { - Some(lsp::CompletionOptions { - resolve_provider: Some(true), - .. - }) => (), - _ => return None, - } - - let res = self.call::(completion_item); - Some(async move { Ok(serde_json::from_value(res.await?)?) }) + completion_item: &lsp::CompletionItem, + ) -> impl Future> { + let res = self.call_with_ref::(completion_item); + async move { Ok(serde_json::from_value(res.await?)?) } } pub fn resolve_code_action( diff --git a/helix-lsp/src/file_event.rs b/helix-lsp/src/file_event.rs index 93457fa55..8f4c16756 100644 --- a/helix-lsp/src/file_event.rs +++ b/helix-lsp/src/file_event.rs @@ -3,24 +3,24 @@ use std::{collections::HashMap, path::PathBuf, sync::Weak}; use globset::{GlobBuilder, GlobSetBuilder}; use tokio::sync::mpsc; -use crate::{lsp, Client}; +use crate::{lsp, Client, LanguageServerId}; enum Event { FileChanged { path: PathBuf, }, Register { - client_id: usize, + client_id: LanguageServerId, client: Weak, registration_id: String, options: lsp::DidChangeWatchedFilesRegistrationOptions, }, Unregister { - client_id: usize, + client_id: LanguageServerId, registration_id: String, }, RemoveClient { - client_id: usize, + client_id: LanguageServerId, }, } @@ -59,7 +59,7 @@ impl Handler { pub fn register( &self, - client_id: usize, + client_id: LanguageServerId, client: Weak, registration_id: String, options: lsp::DidChangeWatchedFilesRegistrationOptions, @@ -72,7 +72,7 @@ impl Handler { }); } - pub fn unregister(&self, client_id: usize, registration_id: String) { + pub fn unregister(&self, client_id: LanguageServerId, registration_id: String) { let _ = self.tx.send(Event::Unregister { client_id, registration_id, @@ -83,12 +83,12 @@ impl Handler { let _ = self.tx.send(Event::FileChanged { path }); } - pub fn remove_client(&self, client_id: usize) { + pub fn remove_client(&self, client_id: LanguageServerId) { let _ = self.tx.send(Event::RemoveClient { client_id }); } async fn run(mut rx: mpsc::UnboundedReceiver) { - let mut state: HashMap = HashMap::new(); + let mut state: HashMap = HashMap::new(); while let Some(event) = rx.recv().await { match event { Event::FileChanged { path } => { diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index c58d967b6..0a3c6a33d 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -17,6 +17,7 @@ use helix_core::syntax::{ LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures, }; use helix_stdx::path; +use slotmap::SlotMap; use tokio::sync::mpsc::UnboundedReceiver; use std::{ @@ -28,8 +29,9 @@ use std::{ use thiserror::Error; use tokio_stream::wrappers::UnboundedReceiverStream; -pub type Result = core::result::Result; +pub type Result = core::result::Result; pub type LanguageServerName = String; +pub use helix_core::diagnostic::LanguageServerId; #[derive(Error, Debug)] pub enum Error { @@ -284,7 +286,8 @@ pub mod util { .chars_at(cursor) .skip(1) .take_while(|ch| chars::char_is_word(*ch)) - .count(); + .count() + + 1; } (start, end) } @@ -539,6 +542,16 @@ pub mod util { } else { return (0, 0, None); }; + + if start > end { + log::error!( + "Invalid LSP text edit start {:?} > end {:?}, discarding", + start, + end + ); + return (0, 0, None); + } + (start, end, replacement) }), ) @@ -640,38 +653,42 @@ impl Notification { #[derive(Debug)] pub struct Registry { - inner: HashMap>>, + inner: SlotMap>, + inner_by_name: HashMap>>, syn_loader: Arc>, - counter: usize, - pub incoming: SelectAll>, + pub incoming: SelectAll>, pub file_event_handler: file_event::Handler, } impl Registry { pub fn new(syn_loader: Arc>) -> Self { Self { - inner: HashMap::new(), + inner: SlotMap::with_key(), + inner_by_name: HashMap::new(), syn_loader, - counter: 0, incoming: SelectAll::new(), file_event_handler: file_event::Handler::new(), } } - pub fn get_by_id(&self, id: usize) -> Option<&Client> { - self.inner - .values() - .flatten() - .find(|client| client.id() == id) - .map(|client| &**client) + pub fn get_by_id(&self, id: LanguageServerId) -> Option<&Arc> { + self.inner.get(id) } - pub fn remove_by_id(&mut self, id: usize) { + pub fn remove_by_id(&mut self, id: LanguageServerId) { + let Some(client) = self.inner.remove(id) else { + log::error!("client was already removed"); + return + }; self.file_event_handler.remove_client(id); - self.inner.retain(|_, language_servers| { - language_servers.retain(|ls| id != ls.id()); - !language_servers.is_empty() - }); + let instances = self + .inner_by_name + .get_mut(client.name()) + .expect("inner and inner_by_name must be synced"); + instances.retain(|ls| id != ls.id()); + if instances.is_empty() { + self.inner_by_name.remove(client.name()); + } } fn start_client( @@ -681,28 +698,28 @@ impl Registry { doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result>> { + ) -> Result, StartupError> { let syn_loader = self.syn_loader.load(); let config = syn_loader .language_server_configs() .get(&name) .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?; - let id = self.counter; - self.counter += 1; - if let Some(NewClient(client, incoming)) = start_client( - id, - name, - ls_config, - config, - doc_path, - root_dirs, - enable_snippets, - )? { - self.incoming.push(UnboundedReceiverStream::new(incoming)); - Ok(Some(client)) - } else { - Ok(None) - } + let id = self.inner.try_insert_with_key(|id| { + start_client( + id, + name, + ls_config, + config, + doc_path, + root_dirs, + enable_snippets, + ) + .map(|client| { + self.incoming.push(UnboundedReceiverStream::new(client.1)); + client.0 + }) + })?; + Ok(self.inner[id].clone()) } /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers, @@ -719,41 +736,39 @@ impl Registry { .language_servers .iter() .filter_map(|LanguageServerFeatures { name, .. }| { - if self.inner.contains_key(name) { - let client = match self.start_client( - name.clone(), - language_config, - doc_path, - root_dirs, - enable_snippets, - ) { - Ok(client) => client?, - Err(error) => return Some(Err(error)), - }; - let old_clients = self - .inner - .insert(name.clone(), vec![client.clone()]) - .unwrap(); - + if let Some(old_clients) = self.inner_by_name.remove(name) { for old_client in old_clients { self.file_event_handler.remove_client(old_client.id()); + self.inner.remove(old_client.id()); tokio::spawn(async move { let _ = old_client.force_shutdown().await; }); } - - Some(Ok(client)) - } else { - None } + let client = match self.start_client( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + ) { + Ok(client) => client, + Err(StartupError::NoRequiredRootFound) => return None, + Err(StartupError::Error(err)) => return Some(Err(err)), + }; + self.inner_by_name + .insert(name.to_owned(), vec![client.clone()]); + + Some(Ok(client)) }) .collect() } pub fn stop(&mut self, name: &str) { - if let Some(clients) = self.inner.remove(name) { + if let Some(clients) = self.inner_by_name.remove(name) { for client in clients { self.file_event_handler.remove_client(client.id()); + self.inner.remove(client.id()); tokio::spawn(async move { let _ = client.force_shutdown().await; }); @@ -770,7 +785,7 @@ impl Registry { ) -> impl Iterator>)> + 'a { language_config.language_servers.iter().filter_map( move |LanguageServerFeatures { name, .. }| { - if let Some(clients) = self.inner.get(name) { + if let Some(clients) = self.inner_by_name.get(name) { if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| { client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) }) { @@ -785,21 +800,21 @@ impl Registry { enable_snippets, ) { Ok(client) => { - let client = client?; - self.inner + self.inner_by_name .entry(name.to_owned()) .or_default() .push(client.clone()); Some((name.clone(), Ok(client))) } - Err(err) => Some((name.to_owned(), Err(err))), + Err(StartupError::NoRequiredRootFound) => None, + Err(StartupError::Error(err)) => Some((name.to_owned(), Err(err))), } }, ) } pub fn iter_clients(&self) -> impl Iterator> { - self.inner.values().flatten() + self.inner.values() } } @@ -822,7 +837,7 @@ impl ProgressStatus { /// Acts as a container for progress reported by language servers. Each server /// has a unique id assigned at creation through [`Registry`]. This id is then used /// to store the progress in this map. -pub struct LspProgressMap(HashMap>); +pub struct LspProgressMap(HashMap>); impl LspProgressMap { pub fn new() -> Self { @@ -830,28 +845,35 @@ impl LspProgressMap { } /// Returns a map of all tokens corresponding to the language server with `id`. - pub fn progress_map(&self, id: usize) -> Option<&HashMap> { + pub fn progress_map( + &self, + id: LanguageServerId, + ) -> Option<&HashMap> { self.0.get(&id) } - pub fn is_progressing(&self, id: usize) -> bool { + pub fn is_progressing(&self, id: LanguageServerId) -> bool { self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default() } /// Returns last progress status for a given server with `id` and `token`. - pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> { + pub fn progress( + &self, + id: LanguageServerId, + token: &lsp::ProgressToken, + ) -> Option<&ProgressStatus> { self.0.get(&id).and_then(|values| values.get(token)) } /// Checks if progress `token` for server with `id` is created. - pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool { + pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool { self.0 .get(&id) .map(|values| values.get(token).is_some()) .unwrap_or_default() } - pub fn create(&mut self, id: usize, token: lsp::ProgressToken) { + pub fn create(&mut self, id: LanguageServerId, token: lsp::ProgressToken) { self.0 .entry(id) .or_default() @@ -861,7 +883,7 @@ impl LspProgressMap { /// Ends the progress by removing the `token` from server with `id`, if removed returns the value. pub fn end_progress( &mut self, - id: usize, + id: LanguageServerId, token: &lsp::ProgressToken, ) -> Option { self.0.get_mut(&id).and_then(|vals| vals.remove(token)) @@ -870,7 +892,7 @@ impl LspProgressMap { /// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`. pub fn update( &mut self, - id: usize, + id: LanguageServerId, token: lsp::ProgressToken, status: lsp::WorkDoneProgress, ) -> Option { @@ -881,19 +903,30 @@ impl LspProgressMap { } } -struct NewClient(Arc, UnboundedReceiver<(usize, Call)>); +struct NewClient(Arc, UnboundedReceiver<(LanguageServerId, Call)>); + +enum StartupError { + NoRequiredRootFound, + Error(Error), +} + +impl> From for StartupError { + fn from(value: T) -> Self { + StartupError::Error(value.into()) + } +} /// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that /// it is only called when it makes sense. fn start_client( - id: usize, + id: LanguageServerId, name: String, config: &LanguageConfiguration, ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, -) -> Result> { +) -> Result { let (workspace, workspace_is_cwd) = helix_loader::find_workspace(); let workspace = path::normalize(workspace); let root = find_lsp_workspace( @@ -918,7 +951,7 @@ fn start_client( .map(|entry| entry.file_name()) .any(|entry| globset.is_match(entry)) { - return Ok(None); + return Err(StartupError::NoRequiredRootFound); } } @@ -970,7 +1003,7 @@ fn start_client( initialize_notify.notify_one(); }); - Ok(Some(NewClient(client, incoming))) + Ok(NewClient(client, incoming)) } /// Find an LSP workspace of a file using the following mechanism: diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index f2f35d6ab..bd671abe1 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,4 +1,4 @@ -use crate::{jsonrpc, Error, Result}; +use crate::{jsonrpc, Error, LanguageServerId, Result}; use anyhow::Context; use log::{error, info}; use serde::{Deserialize, Serialize}; @@ -37,7 +37,7 @@ enum ServerMessage { #[derive(Debug)] pub struct Transport { - id: usize, + id: LanguageServerId, name: String, pending_requests: Mutex>>>, } @@ -47,10 +47,10 @@ impl Transport { server_stdout: BufReader, server_stdin: BufWriter, server_stderr: BufReader, - id: usize, + id: LanguageServerId, name: String, ) -> ( - UnboundedReceiver<(usize, jsonrpc::Call)>, + UnboundedReceiver<(LanguageServerId, jsonrpc::Call)>, UnboundedSender, Arc, ) { @@ -194,7 +194,7 @@ impl Transport { async fn process_server_message( &self, - client_tx: &UnboundedSender<(usize, jsonrpc::Call)>, + client_tx: &UnboundedSender<(LanguageServerId, jsonrpc::Call)>, msg: ServerMessage, language_server_name: &str, ) -> Result<()> { @@ -251,7 +251,7 @@ impl Transport { async fn recv( transport: Arc, mut server_stdout: BufReader, - client_tx: UnboundedSender<(usize, jsonrpc::Call)>, + client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>, ) { let mut recv_buffer = String::new(); loop { @@ -329,7 +329,7 @@ impl Transport { async fn send( transport: Arc, mut server_stdin: BufWriter, - client_tx: UnboundedSender<(usize, jsonrpc::Call)>, + client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>, mut client_rx: UnboundedReceiver, initialize_notify: Arc, ) { diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index ed23f4e4f..f17e08854 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -17,6 +17,13 @@ etcetera = "0.8" ropey = { version = "1.6.1", default-features = false } which = "6.0" regex-cursor = "0.1.4" +bitflags = "2.4" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.52", features = ["Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading"] } + +[target.'cfg(unix)'.dependencies] +rustix = { version = "0.38", features = ["fs"] } [dev-dependencies] tempfile = "3.10" diff --git a/helix-stdx/src/faccess.rs b/helix-stdx/src/faccess.rs new file mode 100644 index 000000000..0270c1f8a --- /dev/null +++ b/helix-stdx/src/faccess.rs @@ -0,0 +1,459 @@ +//! From + +use std::io; +use std::path::Path; + +use bitflags::bitflags; + +// Licensed under MIT from faccess +bitflags! { + /// Access mode flags for `access` function to test for. + pub struct AccessMode: u8 { + /// Path exists + const EXISTS = 0b0001; + /// Path can likely be read + const READ = 0b0010; + /// Path can likely be written to + const WRITE = 0b0100; + /// Path can likely be executed + const EXECUTE = 0b1000; + } +} + +#[cfg(unix)] +mod imp { + use super::*; + + use rustix::fs::Access; + use std::os::unix::fs::{MetadataExt, PermissionsExt}; + + pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> { + let mut imode = Access::empty(); + + if mode.contains(AccessMode::EXISTS) { + imode |= Access::EXISTS; + } + + if mode.contains(AccessMode::READ) { + imode |= Access::READ_OK; + } + + if mode.contains(AccessMode::WRITE) { + imode |= Access::WRITE_OK; + } + + if mode.contains(AccessMode::EXECUTE) { + imode |= Access::EXEC_OK; + } + + rustix::fs::access(p, imode)?; + Ok(()) + } + + fn chown(p: &Path, uid: Option, gid: Option) -> io::Result<()> { + let uid = uid.map(|n| unsafe { rustix::fs::Uid::from_raw(n) }); + let gid = gid.map(|n| unsafe { rustix::fs::Gid::from_raw(n) }); + rustix::fs::chown(p, uid, gid)?; + Ok(()) + } + + pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> { + let from_meta = std::fs::metadata(from)?; + let to_meta = std::fs::metadata(to)?; + let from_gid = from_meta.gid(); + let to_gid = to_meta.gid(); + + let mut perms = from_meta.permissions(); + perms.set_mode(perms.mode() & 0o0777); + if from_gid != to_gid && chown(to, None, Some(from_gid)).is_err() { + let new_perms = (perms.mode() & 0o0707) | ((perms.mode() & 0o07) << 3); + perms.set_mode(new_perms); + } + + std::fs::set_permissions(to, perms)?; + + Ok(()) + } +} + +// Licensed under MIT from faccess except for `chown`, `copy_metadata` and `is_acl_inherited` +#[cfg(windows)] +mod imp { + + use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, ERROR_SUCCESS, HANDLE, PSID}; + use windows_sys::Win32::Security::Authorization::{ + GetNamedSecurityInfoW, SetNamedSecurityInfoW, SE_FILE_OBJECT, + }; + use windows_sys::Win32::Security::{ + AccessCheck, AclSizeInformation, GetAce, GetAclInformation, GetSidIdentifierAuthority, + ImpersonateSelf, IsValidAcl, IsValidSid, MapGenericMask, RevertToSelf, + SecurityImpersonation, ACCESS_ALLOWED_CALLBACK_ACE, ACL, ACL_SIZE_INFORMATION, + DACL_SECURITY_INFORMATION, GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, INHERITED_ACE, + LABEL_SECURITY_INFORMATION, OBJECT_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, + PRIVILEGE_SET, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, + SID_IDENTIFIER_AUTHORITY, TOKEN_DUPLICATE, TOKEN_QUERY, + }; + use windows_sys::Win32::Storage::FileSystem::{ + FILE_ACCESS_RIGHTS, FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, + FILE_GENERIC_WRITE, + }; + use windows_sys::Win32::System::Threading::{GetCurrentThread, OpenThreadToken}; + + use super::*; + + use std::ffi::c_void; + + use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt}; + + struct SecurityDescriptor { + sd: PSECURITY_DESCRIPTOR, + owner: PSID, + group: PSID, + dacl: *mut ACL, + } + + impl Drop for SecurityDescriptor { + fn drop(&mut self) { + if !self.sd.is_null() { + unsafe { + LocalFree(self.sd); + } + } + } + } + + impl SecurityDescriptor { + fn for_path(p: &Path) -> io::Result { + let path = std::fs::canonicalize(p)?; + let pathos = path.into_os_string(); + let mut pathw: Vec = Vec::with_capacity(pathos.len() + 1); + pathw.extend(pathos.encode_wide()); + pathw.push(0); + + let mut sd = std::ptr::null_mut(); + let mut owner = std::ptr::null_mut(); + let mut group = std::ptr::null_mut(); + let mut dacl = std::ptr::null_mut(); + + let err = unsafe { + GetNamedSecurityInfoW( + pathw.as_ptr(), + SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION + | GROUP_SECURITY_INFORMATION + | DACL_SECURITY_INFORMATION + | LABEL_SECURITY_INFORMATION, + &mut owner, + &mut group, + &mut dacl, + std::ptr::null_mut(), + &mut sd, + ) + }; + + if err == ERROR_SUCCESS { + Ok(SecurityDescriptor { + sd, + owner, + group, + dacl, + }) + } else { + Err(io::Error::last_os_error()) + } + } + + fn is_acl_inherited(&self) -> bool { + let mut acl_info: ACL_SIZE_INFORMATION = unsafe { ::core::mem::zeroed() }; + let acl_info_ptr: *mut c_void = &mut acl_info as *mut _ as *mut c_void; + let mut ace: ACCESS_ALLOWED_CALLBACK_ACE = unsafe { ::core::mem::zeroed() }; + + unsafe { + GetAclInformation( + self.dacl, + acl_info_ptr, + std::mem::size_of_val(&acl_info) as u32, + AclSizeInformation, + ) + }; + + for i in 0..acl_info.AceCount { + let mut ptr = &mut ace as *mut _ as *mut c_void; + unsafe { GetAce(self.dacl, i, &mut ptr) }; + if (ace.Header.AceFlags as u32 & INHERITED_ACE) != 0 { + return true; + } + } + + false + } + + fn descriptor(&self) -> &PSECURITY_DESCRIPTOR { + &self.sd + } + + fn owner(&self) -> &PSID { + &self.owner + } + } + + struct ThreadToken(HANDLE); + impl Drop for ThreadToken { + fn drop(&mut self) { + unsafe { + CloseHandle(self.0); + } + } + } + + impl ThreadToken { + fn new() -> io::Result { + unsafe { + if ImpersonateSelf(SecurityImpersonation) == 0 { + return Err(io::Error::last_os_error()); + } + + let token: *mut HANDLE = std::ptr::null_mut(); + let err = + OpenThreadToken(GetCurrentThread(), TOKEN_DUPLICATE | TOKEN_QUERY, 0, token); + + RevertToSelf(); + + if err == 0 { + return Err(io::Error::last_os_error()); + } + + Ok(Self(*token)) + } + } + + fn as_handle(&self) -> &HANDLE { + &self.0 + } + } + + // Based roughly on Tcl's NativeAccess() + // https://github.com/tcltk/tcl/blob/2ee77587e4dc2150deb06b48f69db948b4ab0584/win/tclWinFile.c + fn eaccess(p: &Path, mut mode: FILE_ACCESS_RIGHTS) -> io::Result<()> { + let md = p.metadata()?; + + if !md.is_dir() { + // Read Only is ignored for directories + if mode & FILE_GENERIC_WRITE == FILE_GENERIC_WRITE && md.permissions().readonly() { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "File is read only", + )); + } + + // If it doesn't have the correct extension it isn't executable + if mode & FILE_GENERIC_EXECUTE == FILE_GENERIC_EXECUTE { + if let Some(ext) = p.extension().and_then(|s| s.to_str()) { + match ext { + "exe" | "com" | "bat" | "cmd" => (), + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "File not executable", + )) + } + } + } + } + + return std::fs::OpenOptions::new() + .access_mode(mode) + .open(p) + .map(|_| ()); + } + + let sd = SecurityDescriptor::for_path(p)?; + + // Unmapped Samba users are assigned a top level authority of 22 + // ACL tests are likely to be misleading + const SAMBA_UNMAPPED: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY { + Value: [0, 0, 0, 0, 0, 22], + }; + unsafe { + let owner = sd.owner(); + if IsValidSid(*owner) != 0 + && (*GetSidIdentifierAuthority(*owner)).Value == SAMBA_UNMAPPED.Value + { + return Ok(()); + } + } + + let token = ThreadToken::new()?; + + let mut privileges: PRIVILEGE_SET = unsafe { std::mem::zeroed() }; + let mut granted_access: u32 = 0; + let mut privileges_length = std::mem::size_of::() as u32; + let mut result = 0; + + let mut mapping = GENERIC_MAPPING { + GenericRead: FILE_GENERIC_READ, + GenericWrite: FILE_GENERIC_WRITE, + GenericExecute: FILE_GENERIC_EXECUTE, + GenericAll: FILE_ALL_ACCESS, + }; + + unsafe { MapGenericMask(&mut mode, &mut mapping) }; + + if unsafe { + AccessCheck( + *sd.descriptor(), + *token.as_handle(), + mode, + &mut mapping, + &mut privileges, + &mut privileges_length, + &mut granted_access, + &mut result, + ) + } != 0 + { + if result == 0 { + Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "Permission Denied", + )) + } else { + Ok(()) + } + } else { + Err(io::Error::last_os_error()) + } + } + + pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> { + let mut imode = 0; + + if mode.contains(AccessMode::READ) { + imode |= FILE_GENERIC_READ; + } + + if mode.contains(AccessMode::WRITE) { + imode |= FILE_GENERIC_WRITE; + } + + if mode.contains(AccessMode::EXECUTE) { + imode |= FILE_GENERIC_EXECUTE; + } + + if imode == 0 { + if p.exists() { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "Not Found")) + } + } else { + eaccess(p, imode) + } + } + + fn chown(p: &Path, sd: SecurityDescriptor) -> io::Result<()> { + let path = std::fs::canonicalize(p)?; + let pathos = path.as_os_str(); + let mut pathw = Vec::with_capacity(pathos.len() + 1); + pathw.extend(pathos.encode_wide()); + pathw.push(0); + + let mut owner = std::ptr::null_mut(); + let mut group = std::ptr::null_mut(); + let mut dacl = std::ptr::null(); + + let mut si = OBJECT_SECURITY_INFORMATION::default(); + if unsafe { IsValidSid(sd.owner) } != 0 { + si |= OWNER_SECURITY_INFORMATION; + owner = sd.owner; + } + + if unsafe { IsValidSid(sd.group) } != 0 { + si |= GROUP_SECURITY_INFORMATION; + group = sd.group; + } + + if unsafe { IsValidAcl(sd.dacl) } != 0 { + si |= DACL_SECURITY_INFORMATION; + if !sd.is_acl_inherited() { + si |= PROTECTED_DACL_SECURITY_INFORMATION; + } + dacl = sd.dacl as *const _; + } + + let err = unsafe { + SetNamedSecurityInfoW( + pathw.as_ptr(), + SE_FILE_OBJECT, + si, + owner, + group, + dacl, + std::ptr::null(), + ) + }; + + if err == ERROR_SUCCESS { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } + + pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> { + let sd = SecurityDescriptor::for_path(from)?; + chown(to, sd)?; + + let meta = std::fs::metadata(from)?; + let perms = meta.permissions(); + + std::fs::set_permissions(to, perms)?; + + Ok(()) + } +} + +// Licensed under MIT from faccess except for `copy_metadata` +#[cfg(not(any(unix, windows)))] +mod imp { + use super::*; + + pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> { + if mode.contains(AccessMode::WRITE) { + if std::fs::metadata(p)?.permissions().readonly() { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "Path is read only", + )); + } else { + return Ok(()); + } + } + + if p.exists() { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "Path not found")) + } + } + + pub fn copy_metadata(from: &path, to: &Path) -> io::Result<()> { + let meta = std::fs::metadata(from)?; + let perms = meta.permissions(); + std::fs::set_permissions(to, perms)?; + + Ok(()) + } +} + +pub fn readonly(p: &Path) -> bool { + match imp::access(p, AccessMode::WRITE) { + Ok(_) => false, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, + Err(_) => true, + } +} + +pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> { + imp::copy_metadata(from, to) +} diff --git a/helix-stdx/src/lib.rs b/helix-stdx/src/lib.rs index 68fe3ec37..19602c205 100644 --- a/helix-stdx/src/lib.rs +++ b/helix-stdx/src/lib.rs @@ -1,3 +1,4 @@ pub mod env; +pub mod faccess; pub mod path; pub mod rope; diff --git a/helix-stdx/src/path.rs b/helix-stdx/src/path.rs index ff2bffae5..968596a70 100644 --- a/helix-stdx/src/path.rs +++ b/helix-stdx/src/path.rs @@ -3,7 +3,7 @@ pub use etcetera::home_dir; use std::{ borrow::Cow, ffi::OsString, - path::{Component, Path, PathBuf}, + path::{Component, Path, PathBuf, MAIN_SEPARATOR_STR}, }; use crate::env::current_working_dir; @@ -18,7 +18,8 @@ where if let Ok(home) = home_dir() { if let Ok(stripped) = path.strip_prefix(&home) { let mut path = OsString::with_capacity(2 + stripped.as_os_str().len()); - path.push("~/"); + path.push("~"); + path.push(MAIN_SEPARATOR_STR); path.push(stripped); return Cow::Owned(PathBuf::from(path)); } diff --git a/helix-stdx/src/rope.rs b/helix-stdx/src/rope.rs index 7e2549f5a..2695555e3 100644 --- a/helix-stdx/src/rope.rs +++ b/helix-stdx/src/rope.rs @@ -3,6 +3,7 @@ use std::ops::{Bound, RangeBounds}; pub use regex_cursor::engines::meta::{Builder as RegexBuilder, Regex}; pub use regex_cursor::regex_automata::util::syntax::Config; use regex_cursor::{Input as RegexInput, RopeyCursor}; +use ropey::str_utils::byte_to_char_idx; use ropey::RopeSlice; pub trait RopeSliceExt<'a>: Sized { @@ -16,6 +17,23 @@ pub trait RopeSliceExt<'a>: Sized { fn regex_input_at>(self, char_range: R) -> RegexInput>; fn first_non_whitespace_char(self) -> Option; fn last_non_whitespace_char(self) -> Option; + /// returns the char idx of `byte_idx`, if `byte_idx` is a char boundary + /// this function behaves the same as `byte_to_char` but if `byte_idx` is + /// not a valid char boundary (so within a char) this will return the next + /// char index. + /// + /// # Example + /// + /// ``` + /// # use ropey::RopeSlice; + /// # use helix_stdx::rope::RopeSliceExt; + /// let text = RopeSlice::from("😆"); + /// for i in 1..text.len_bytes() { + /// assert_eq!(text.byte_to_char(i), 0); + /// assert_eq!(text.byte_to_next_char(i), 1); + /// } + /// ``` + fn byte_to_next_char(self, byte_idx: usize) -> usize; } impl<'a> RopeSliceExt<'a> for RopeSlice<'a> { @@ -75,4 +93,48 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> { .position(|ch| !ch.is_whitespace()) .map(|pos| self.len_chars() - pos - 1) } + + /// returns the char idx of `byte_idx`, if `byte_idx` is + /// a char boundary this function behaves the same as `byte_to_char` + fn byte_to_next_char(self, mut byte_idx: usize) -> usize { + let (chunk, chunk_byte_off, chunk_char_off, _) = self.chunk_at_byte(byte_idx); + byte_idx -= chunk_byte_off; + let is_char_boundary = + is_utf8_char_boundary(chunk.as_bytes().get(byte_idx).copied().unwrap_or(0)); + chunk_char_off + byte_to_char_idx(chunk, byte_idx) + !is_char_boundary as usize + } +} + +// copied from std +#[inline] +const fn is_utf8_char_boundary(b: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (b as i8) >= -0x40 +} + +#[cfg(test)] +mod tests { + use ropey::RopeSlice; + + use crate::rope::RopeSliceExt; + + #[test] + fn next_char_at_byte() { + for i in 0..=6 { + assert_eq!(RopeSlice::from("foobar").byte_to_next_char(i), i); + } + for char_idx in 0..10 { + let len = "😆".len(); + assert_eq!( + RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len), + char_idx + ); + for i in 1..=len { + assert_eq!( + RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len + i), + char_idx + 1 + ); + } + } + } } diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index bc3117d20..b00e97488 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/application.rs b/helix-term/src/application.rs index 809393c7f..6bdf60bca 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -4,7 +4,7 @@ use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection}; use helix_lsp::{ lsp::{self, notification::Notification}, util::lsp_range_to_range, - LspProgressMap, + LanguageServerId, LspProgressMap, }; use helix_stdx::path::get_relative_path; use helix_view::{ @@ -655,7 +655,7 @@ impl Application { pub async fn handle_language_server_message( &mut self, call: helix_lsp::Call, - server_id: usize, + server_id: LanguageServerId, ) { use helix_lsp::{Call, MethodCall, Notification}; @@ -1030,12 +1030,7 @@ impl Application { Ok(json!(result)) } Ok(MethodCall::RegisterCapability(params)) => { - if let Some(client) = self - .editor - .language_servers - .iter_clients() - .find(|client| client.id() == server_id) - { + if let Some(client) = self.editor.language_servers.get_by_id(server_id) { for reg in params.registrations { match reg.method.as_str() { lsp::notification::DidChangeWatchedFiles::METHOD => { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a5e79a539..7be2ea095 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,16 +3,26 @@ pub(crate) mod lsp; pub(crate) mod typed; pub use dap::*; -use helix_stdx::rope::{self, RopeSliceExt}; -use helix_vcs::Hunk; +use helix_event::status; +use helix_stdx::{ + path::expand_tilde, + rope::{self, RopeSliceExt}, +}; +use helix_vcs::{FileChange, Hunk}; pub use lsp::*; -use tui::widgets::Row; +use tui::{ + text::Span, + widgets::{Cell, 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,12 +34,11 @@ 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, + RopeReader, RopeSlice, Selection, SmallVec, Syntax, Tendril, Transaction, }; use helix_view::{ document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, @@ -37,6 +46,7 @@ use helix_view::{ info::Info, input::KeyEvent, keyboard::KeyCode, + theme::Style, tree, view::View, Document, DocumentId, Editor, ViewId, @@ -52,7 +62,7 @@ use crate::{ filter_picker_entry, job::Callback, keymap::ReverseKeymap, - ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent}, + ui::{self, menu::Item, overlay::overlaid, Picker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Jobs}; @@ -322,6 +332,7 @@ impl MappableCommand { buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", symbol_picker, "Open symbol picker", + changed_file_picker, "Open changed file picker", select_references_to_symbol_under_cursor, "Select symbol references", workspace_symbol_picker, "Open workspace symbol picker", diagnostics_picker, "Open diagnostic picker", @@ -427,8 +438,10 @@ impl MappableCommand { reverse_selection_contents, "Reverse selections contents", expand_selection, "Expand selection to parent syntax node", shrink_selection, "Shrink selection to previously expanded syntax node", - select_next_sibling, "Select next sibling in syntax tree", - select_prev_sibling, "Select previous sibling in syntax tree", + select_next_sibling, "Select next sibling in the syntax tree", + select_prev_sibling, "Select previous sibling the in syntax tree", + select_all_siblings, "Select all siblings of the current node", + select_all_children, "Select all children of the current node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save current selection to jumplist", @@ -473,6 +486,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", @@ -503,6 +518,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", ); } @@ -618,6 +635,7 @@ fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movem &mut annotations, ) }); + drop(annotations); doc.set_selection(view.id, selection); } @@ -781,28 +799,29 @@ fn goto_line_start(cx: &mut Context) { } fn goto_next_buffer(cx: &mut Context) { - goto_buffer(cx.editor, Direction::Forward); + goto_buffer(cx.editor, Direction::Forward, cx.count()); } fn goto_previous_buffer(cx: &mut Context) { - goto_buffer(cx.editor, Direction::Backward); + goto_buffer(cx.editor, Direction::Backward, cx.count()); } -fn goto_buffer(editor: &mut Editor, direction: Direction) { +fn goto_buffer(editor: &mut Editor, direction: Direction, count: usize) { let current = view!(editor).doc; let id = match direction { Direction::Forward => { let iter = editor.documents.keys(); - let mut iter = iter.skip_while(|id| *id != ¤t); - iter.next(); // skip current item - iter.next().or_else(|| editor.documents.keys().next()) + // skip 'count' times past current buffer + iter.cycle().skip_while(|id| *id != ¤t).nth(count) } Direction::Backward => { let iter = editor.documents.keys(); - let mut iter = iter.rev().skip_while(|id| *id != ¤t); - iter.next(); // skip current item - iter.next().or_else(|| editor.documents.keys().next_back()) + // skip 'count' times past current buffer + iter.rev() + .cycle() + .skip_while(|id| *id != ¤t) + .nth(count) } } .unwrap(); @@ -1183,25 +1202,51 @@ fn goto_file_impl(cx: &mut Context, action: Action) { let primary = selections.primary(); // Checks whether there is only one selection with a width of 1 if selections.len() == 1 && primary.len() == 1 { - let count = cx.count(); - let text_slice = text.slice(..); - // In this case it selects the WORD under the cursor - let current_word = textobject::textobject_word( - text_slice, - primary, - textobject::TextObject::Inside, - count, - true, - ); - // Trims some surrounding chars so that the actual file is opened. - let surrounding_chars: &[_] = &['\'', '"', '(', ')']; paths.clear(); - paths.push( - current_word - .fragment(text_slice) - .trim_matches(surrounding_chars) - .to_string(), - ); + + let is_valid_path_char = |c: &char| { + #[cfg(target_os = "windows")] + let valid_chars = &[ + '@', '/', '\\', '.', '-', '_', '+', '#', '$', '%', '{', '}', '[', ']', ':', '!', + '~', '=', + ]; + #[cfg(not(target_os = "windows"))] + let valid_chars = &['@', '/', '.', '-', '_', '+', '#', '$', '%', '~', '=', ':']; + + valid_chars.contains(c) || c.is_alphabetic() || c.is_numeric() + }; + + let cursor_pos = primary.cursor(text.slice(..)); + let pre_cursor_pos = cursor_pos.saturating_sub(1); + let post_cursor_pos = cursor_pos + 1; + let start_pos = if is_valid_path_char(&text.char(cursor_pos)) { + cursor_pos + } else if is_valid_path_char(&text.char(pre_cursor_pos)) { + pre_cursor_pos + } else { + post_cursor_pos + }; + + let prefix_len = text + .chars_at(start_pos) + .reversed() + .take_while(is_valid_path_char) + .count(); + + let postfix_len = text + .chars_at(start_pos) + .take_while(is_valid_path_char) + .count(); + + let path: Cow = text + .slice((start_pos - prefix_len)..(start_pos + postfix_len)) + .into(); + log::debug!("Goto file path: {}", path); + + match expand_tilde(PathBuf::from(path.as_ref())).to_str() { + Some(path) => paths.push(path.to_string()), + None => cx.editor.set_error("Couldn't get string out of path."), + }; } for sel in paths { @@ -1211,7 +1256,8 @@ fn goto_file_impl(cx: &mut Context, action: Action) { } if let Ok(url) = Url::parse(p) { - return open_url(cx, url, action); + open_url(cx, url, action); + continue; } let path = &rel_path.join(p); @@ -1638,7 +1684,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, @@ -1716,6 +1762,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); } @@ -2034,6 +2081,11 @@ fn searcher(cx: &mut Context, direction: Direction) { let config = cx.editor.config(); let scrolloff = config.scrolloff; let wrap_around = config.search.wrap_around; + let movement = if cx.editor.mode() == Mode::Select { + Movement::Extend + } else { + Movement::Move + }; // TODO: could probably share with select_on_matches? let completions = search_completions(cx, Some(reg)); @@ -2058,7 +2110,7 @@ fn searcher(cx: &mut Context, direction: Direction) { search_impl( cx.editor, ®ex, - Movement::Move, + movement, direction, scrolloff, wrap_around, @@ -2603,14 +2655,19 @@ fn selection_is_linewise(selection: &Selection, text: &Rope) -> bool { }) } -fn delete_selection_impl(cx: &mut Context, op: Operation) { +enum YankAction { + Yank, + NoYank, +} + +fn delete_selection_impl(cx: &mut Context, op: Operation, yank: YankAction) { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); let only_whole_lines = selection_is_linewise(selection, doc.text()); - if cx.register != Some('_') { - // first yank the selection + if cx.register != Some('_') && matches!(yank, YankAction::Yank) { + // yank the selection let text = doc.text().slice(..); let values: Vec = selection.fragments(text).map(Cow::into_owned).collect(); let reg_name = cx.register.unwrap_or('"'); @@ -2618,9 +2675,9 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) { cx.editor.set_error(err.to_string()); return; } - }; + } - // then delete + // delete the selection let transaction = Transaction::delete_by_selection(doc.text(), selection, |range| (range.from(), range.to())); doc.apply(&transaction, view.id); @@ -2686,21 +2743,19 @@ fn delete_by_selection_insert_mode( } fn delete_selection(cx: &mut Context) { - delete_selection_impl(cx, Operation::Delete); + delete_selection_impl(cx, Operation::Delete, YankAction::Yank); } fn delete_selection_noyank(cx: &mut Context) { - cx.register = Some('_'); - delete_selection_impl(cx, Operation::Delete); + delete_selection_impl(cx, Operation::Delete, YankAction::NoYank); } fn change_selection(cx: &mut Context) { - delete_selection_impl(cx, Operation::Change); + delete_selection_impl(cx, Operation::Change, YankAction::Yank); } fn change_selection_noyank(cx: &mut Context) { - cx.register = Some('_'); - delete_selection_impl(cx, Operation::Change); + delete_selection_impl(cx, Operation::Change, YankAction::NoYank); } fn collapse_selection(cx: &mut Context) { @@ -2966,6 +3021,7 @@ fn jumplist_picker(cx: &mut Context) { .flat_map(|(view, _)| { view.jumps .iter() + .rev() .map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone())) }) .collect(), @@ -2988,6 +3044,94 @@ fn jumplist_picker(cx: &mut Context) { cx.push_layer(Box::new(overlaid(picker))); } +fn changed_file_picker(cx: &mut Context) { + pub struct FileChangeData { + cwd: PathBuf, + style_untracked: Style, + style_modified: Style, + style_conflict: Style, + style_deleted: Style, + style_renamed: Style, + } + + impl Item for FileChange { + type Data = FileChangeData; + + fn format(&self, data: &Self::Data) -> Row { + let process_path = |path: &PathBuf| { + path.strip_prefix(&data.cwd) + .unwrap_or(path) + .display() + .to_string() + }; + + let (sign, style, content) = match self { + Self::Untracked { path } => ("[+]", data.style_untracked, process_path(path)), + Self::Modified { path } => ("[~]", data.style_modified, process_path(path)), + Self::Conflict { path } => ("[x]", data.style_conflict, process_path(path)), + Self::Deleted { path } => ("[-]", data.style_deleted, process_path(path)), + Self::Renamed { from_path, to_path } => ( + "[>]", + data.style_renamed, + format!("{} -> {}", process_path(from_path), process_path(to_path)), + ), + }; + + Row::new([Cell::from(Span::styled(sign, style)), Cell::from(content)]) + } + } + + let cwd = helix_stdx::env::current_working_dir(); + if !cwd.exists() { + cx.editor + .set_error("Current working directory does not exist"); + return; + } + + let added = cx.editor.theme.get("diff.plus"); + let modified = cx.editor.theme.get("diff.delta"); + let conflict = cx.editor.theme.get("diff.delta.conflict"); + let deleted = cx.editor.theme.get("diff.minus"); + let renamed = cx.editor.theme.get("diff.delta.moved"); + + let picker = Picker::new( + Vec::new(), + FileChangeData { + cwd: cwd.clone(), + style_untracked: added, + style_modified: modified, + style_conflict: conflict, + style_deleted: deleted, + style_renamed: renamed, + }, + |cx, meta: &FileChange, action| { + let path_to_open = meta.path(); + if let Err(e) = cx.editor.open(path_to_open, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path_to_open.display()) + }; + cx.editor.set_error(err); + } + }, + ) + .with_preview(|_editor, meta| Some((meta.path().to_path_buf().into(), None))); + let injector = picker.injector(); + + cx.editor + .diff_providers + .clone() + .for_each_changed_file(cwd, move |change| match change { + Ok(change) => injector.push(change).is_ok(), + Err(err) => { + status::report_blocking(err); + true + } + }); + cx.push_layer(Box::new(overlaid(picker))); +} + impl ui::menu::Item for MappableCommand { type Data = ReverseKeymap; @@ -3439,48 +3583,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) { @@ -4768,18 +4919,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); } }; @@ -4787,11 +4937,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) { @@ -4833,6 +4983,36 @@ pub fn extend_parent_node_start(cx: &mut Context) { move_node_bound_impl(cx, Direction::Backward, Movement::Extend) } +fn select_all_impl(editor: &mut Editor, select_fn: F) +where + F: Fn(&Syntax, RopeSlice, Selection) -> Selection, +{ + 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 = select_fn(syntax, text, current_selection.clone()); + doc.set_selection(view.id, selection); + } +} + +fn select_all_siblings(cx: &mut Context) { + let motion = |editor: &mut Editor| { + select_all_impl(editor, object::select_all_siblings); + }; + + cx.editor.apply_motion(motion); +} + +fn select_all_children(cx: &mut Context) { + let motion = |editor: &mut Editor| { + select_all_impl(editor, object::select_all_children); + }; + + cx.editor.apply_motion(motion); +} + fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); let is_select = cx.editor.mode == Mode::Select; @@ -5155,6 +5335,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); } @@ -5219,15 +5407,25 @@ 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, + doc.syntax(), + text, + range, + objtype, + count, ), 'g' => textobject_change(range), // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_pair_surround(text, range, objtype, ch, count) - } + ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround( + doc.syntax(), + text, + range, + objtype, + ch, + count, + ), _ => range, } }); @@ -5251,7 +5449,9 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { ("a", "Argument/parameter (tree-sitter)"), ("c", "Comment (tree-sitter)"), ("T", "Test (tree-sitter)"), - ("m", "Closest surrounding pair"), + ("e", "Data structure entry (tree-sitter)"), + ("m", "Closest surrounding pair (tree-sitter)"), + ("g", "Change"), (" ", "... or any character acting as a pair"), ]; @@ -5264,7 +5464,7 @@ fn surround_add(cx: &mut Context) { // surround_len is the number of new characters being added. let (open, close, surround_len) = match event.char() { Some(ch) => { - let (o, c) = surround::get_pair(ch); + let (o, c) = match_brackets::get_pair(ch); let mut open = Tendril::new(); open.push(o); let mut close = Tendril::new(); @@ -5315,13 +5515,14 @@ fn surround_replace(cx: &mut Context) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - let change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) { - Ok(c) => c, - Err(err) => { - cx.editor.set_error(err.to_string()); - return; - } - }; + let change_pos = + match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) { + Ok(c) => c, + Err(err) => { + cx.editor.set_error(err.to_string()); + return; + } + }; let selection = selection.clone(); let ranges: SmallVec<[Range; 1]> = change_pos.iter().map(|&p| Range::point(p)).collect(); @@ -5336,7 +5537,7 @@ fn surround_replace(cx: &mut Context) { Some(to) => to, None => return doc.set_selection(view.id, selection), }; - let (open, close) = surround::get_pair(to); + let (open, close) = match_brackets::get_pair(to); // the changeset has to be sorted to allow nested surrounds let mut sorted_pos: Vec<(usize, char)> = Vec::new(); @@ -5373,13 +5574,14 @@ fn surround_delete(cx: &mut Context) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - let mut change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) { - Ok(c) => c, - Err(err) => { - cx.editor.set_error(err.to_string()); - return; - } - }; + let mut change_pos = + match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) { + Ok(c) => c, + Err(err) => { + cx.editor.set_error(err.to_string()); + return; + } + }; change_pos.sort_unstable(); // the changeset has to be sorted to allow nested surrounds let transaction = Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None))); @@ -5807,3 +6009,188 @@ 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/dap.rs b/helix-term/src/commands/dap.rs index d62b0a4e5..eda94c44f 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_dap::{self as dap, Client}; use helix_lsp::block_on; -use helix_view::{editor::Breakpoint, graphics::Margin}; +use helix_view::editor::Breakpoint; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -581,12 +581,7 @@ pub fn dap_variables(cx: &mut Context) { } let contents = Text::from(tui::text::Text::from(variables)); - let margin = if cx.editor.popup_border() { - Margin::all(1) - } else { - Margin::none() - }; - let popup = Popup::new("dap-variables", contents).margin(margin); + let popup = Popup::new("dap-variables", contents); cx.replace_or_push_layer("dap-variables", popup); } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index a3168dc2d..e7ba9f0fc 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,4 +1,4 @@ -use futures_util::{stream::FuturesUnordered, FutureExt}; +use futures_util::{stream::FuturesOrdered, FutureExt}; use helix_lsp::{ block_on, lsp::{ @@ -6,7 +6,7 @@ use helix_lsp::{ NumberOrString, }, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, - Client, OffsetEncoding, + Client, LanguageServerId, OffsetEncoding, }; use tokio_stream::StreamExt; use tui::{ @@ -21,7 +21,6 @@ use helix_stdx::path; use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId}, editor::Action, - graphics::Margin, handlers::lsp::SignatureHelpInvoked, theme::Style, Document, View, @@ -266,7 +265,7 @@ enum DiagnosticsFormat { fn diag_picker( cx: &Context, - diagnostics: BTreeMap>, + diagnostics: BTreeMap>, format: DiagnosticsFormat, ) -> Picker { // TODO: drop current_path comparison and instead use workspace: bool flag? @@ -341,7 +340,7 @@ pub fn symbol_picker(cx: &mut Context) { let mut seen_language_servers = HashSet::new(); - let mut futures: FuturesUnordered<_> = doc + let mut futures: FuturesOrdered<_> = doc .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) .map(|language_server| { @@ -416,7 +415,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { let get_symbols = move |pattern: String, editor: &mut Editor| { let doc = doc!(editor); let mut seen_language_servers = HashSet::new(); - let mut futures: FuturesUnordered<_> = doc + let mut futures: FuturesOrdered<_> = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) .map(|language_server| { @@ -497,7 +496,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { struct CodeActionOrCommandItem { lsp_item: lsp::CodeActionOrCommand, - language_server_id: usize, + language_server_id: LanguageServerId, } impl ui::menu::Item for CodeActionOrCommandItem { @@ -574,7 +573,7 @@ pub fn code_action(cx: &mut Context) { let mut seen_language_servers = HashSet::new(); - let mut futures: FuturesUnordered<_> = doc + let mut futures: FuturesOrdered<_> = doc .language_servers_with_feature(LanguageServerFeature::CodeAction) .filter(|ls| seen_language_servers.insert(ls.id())) // TODO this should probably already been filtered in something like "language_servers_with_feature" @@ -733,15 +732,7 @@ pub fn code_action(cx: &mut Context) { }); picker.move_down(); // pre-select the first item - let margin = if editor.menu_border() { - Margin::vertical(1) - } else { - Margin::none() - }; - - let popup = Popup::new("code-action", picker) - .with_scrollbar(false) - .margin(margin); + let popup = Popup::new("code-action", picker).with_scrollbar(false); compositor.replace_or_push("code-action", popup); }; @@ -757,7 +748,11 @@ impl ui::menu::Item for lsp::Command { } } -pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) { +pub fn execute_lsp_command( + editor: &mut Editor, + language_server_id: LanguageServerId, + cmd: lsp::Command, +) { // the command is executed on the server and communicated back // to the client asynchronously using workspace edits let future = match editor @@ -1034,7 +1029,7 @@ pub fn rename_symbol(cx: &mut Context) { fn create_rename_prompt( editor: &Editor, prefill: String, - language_server_id: Option, + language_server_id: Option, ) -> Box { let prompt = ui::Prompt::new( "rename-to:".into(), @@ -1315,11 +1310,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 0dfcdb6e9..f38ae6bba 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,4 +1,5 @@ use std::fmt::Write; +use std::io::BufReader; use std::ops::Deref; use crate::job::Job; @@ -7,9 +8,9 @@ use super::*; use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; -use helix_core::{encoding, line_ending, shellwords::Shellwords}; -use helix_view::document::DEFAULT_LANGUAGE_NAME; -use helix_view::editor::{Action, CloseError, ConfigEvent}; +use helix_core::{line_ending, shellwords::Shellwords}; +use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; +use helix_view::editor::{CloseError, ConfigEvent}; use serde_json::Value; use ui::completers::{self, Completer}; @@ -309,7 +310,7 @@ fn buffer_next( return Ok(()); } - goto_buffer(cx.editor, Direction::Forward); + goto_buffer(cx.editor, Direction::Forward, 1); Ok(()) } @@ -322,7 +323,7 @@ fn buffer_previous( return Ok(()); } - goto_buffer(cx.editor, Direction::Backward); + goto_buffer(cx.editor, Direction::Backward, 1); Ok(()) } @@ -2454,6 +2455,39 @@ fn yank_diagnostic( Ok(()) } +fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let scrolloff = cx.editor.config().scrolloff; + let (view, doc) = current!(cx.editor); + + ensure!(!args.is_empty(), "file name is expected"); + ensure!(args.len() == 1, "only the file name is expected"); + + let filename = args.get(0).unwrap(); + let path = PathBuf::from(filename.to_string()); + ensure!( + path.exists() && path.is_file(), + "path is not a file: {:?}", + path + ); + + let file = std::fs::File::open(path).map_err(|err| anyhow!("error opening file: {}", err))?; + let mut reader = BufReader::new(file); + let (contents, _, _) = read_to_string(&mut reader, Some(doc.encoding())) + .map_err(|err| anyhow!("error reading file: {}", err))?; + let contents = Tendril::from(contents); + let selection = doc.selection(view.id); + let transaction = Transaction::insert(doc.text(), selection, contents); + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view); + view.ensure_cursor_in_view(doc, scrolloff); + + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -3068,6 +3102,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: yank_diagnostic, signature: CommandSignature::all(completers::register), }, + TypableCommand { + name: "read", + aliases: &["r"], + doc: "Load a file into buffer", + fun: read, + signature: CommandSignature::positional(&[completers::filename]), + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index ef5369f85..d45809d36 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -9,10 +9,9 @@ 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; +pub mod completion; mod signature_help; pub fn setup(config: Arc>) -> Handlers { diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs index 491ca5638..68956c85f 100644 --- a/helix-term/src/handlers/completion.rs +++ b/helix-term/src/handlers/completion.rs @@ -30,6 +30,8 @@ use crate::ui::lsp::SignatureHelp; use crate::ui::{self, CompletionItem, Popup}; use super::Handlers; +pub use resolve::ResolveHandler; +mod resolve; #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum TriggerKind { @@ -251,7 +253,7 @@ fn request_completion( .into_iter() .map(|item| CompletionItem { item, - language_server_id, + provider: language_server_id, resolved: false, }) .collect(); diff --git a/helix-term/src/handlers/completion/resolve.rs b/helix-term/src/handlers/completion/resolve.rs new file mode 100644 index 000000000..fb5179e13 --- /dev/null +++ b/helix-term/src/handlers/completion/resolve.rs @@ -0,0 +1,153 @@ +use std::sync::Arc; + +use helix_lsp::lsp; +use tokio::sync::mpsc::Sender; +use tokio::time::{Duration, Instant}; + +use helix_event::{send_blocking, AsyncHook, CancelRx}; +use helix_view::Editor; + +use crate::handlers::completion::CompletionItem; +use crate::job; + +/// A hook for resolving incomplete completion items. +/// +/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion): +/// +/// > If computing full completion items is expensive, servers can additionally provide a +/// > handler for the completion item resolve request. ... +/// > A typical use case is for example: the `textDocument/completion` request doesn't fill +/// > in the `documentation` property for returned completion items since it is expensive +/// > to compute. When the item is selected in the user interface then a +/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter. +/// > The returned completion item should have the documentation property filled in. +pub struct ResolveHandler { + last_request: Option>, + resolver: Sender, +} + +impl ResolveHandler { + pub fn new() -> ResolveHandler { + ResolveHandler { + last_request: None, + resolver: ResolveTimeout { + next_request: None, + in_flight: None, + } + .spawn(), + } + } + + pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut CompletionItem) { + if item.resolved { + return; + } + let needs_resolve = item.item.documentation.is_none() + || item.item.detail.is_none() + || item.item.additional_text_edits.is_none(); + if !needs_resolve { + item.resolved = true; + return; + } + if self.last_request.as_deref().is_some_and(|it| it == item) { + return; + } + let Some(ls) = editor.language_servers.get_by_id(item.provider).cloned() else { + item.resolved = true; + return; + }; + if matches!( + ls.capabilities().completion_provider, + Some(lsp::CompletionOptions { + resolve_provider: Some(true), + .. + }) + ) { + let item = Arc::new(item.clone()); + self.last_request = Some(item.clone()); + send_blocking(&self.resolver, ResolveRequest { item, ls }) + } else { + item.resolved = true; + } + } +} + +struct ResolveRequest { + item: Arc, + ls: Arc, +} + +#[derive(Default)] +struct ResolveTimeout { + next_request: Option, + in_flight: Option<(helix_event::CancelTx, Arc)>, +} + +impl AsyncHook for ResolveTimeout { + type Event = ResolveRequest; + + fn handle_event( + &mut self, + request: Self::Event, + timeout: Option, + ) -> Option { + if self + .next_request + .as_ref() + .is_some_and(|old_request| old_request.item == request.item) + { + timeout + } else if self + .in_flight + .as_ref() + .is_some_and(|(_, old_request)| old_request.item == request.item.item) + { + self.next_request = None; + None + } else { + self.next_request = Some(request); + Some(Instant::now() + Duration::from_millis(150)) + } + } + + fn finish_debounce(&mut self) { + let Some(request) = self.next_request.take() else { return }; + let (tx, rx) = helix_event::cancelation(); + self.in_flight = Some((tx, request.item.clone())); + tokio::spawn(request.execute(rx)); + } +} + +impl ResolveRequest { + async fn execute(self, cancel: CancelRx) { + let future = self.ls.resolve_completion_item(&self.item.item); + let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else { + return; + }; + job::dispatch(move |_, compositor| { + if let Some(completion) = &mut compositor + .find::() + .unwrap() + .completion + { + let resolved_item = match resolved_item { + Ok(item) => CompletionItem { + item, + resolved: true, + ..*self.item + }, + Err(err) => { + log::error!("completion resolve request failed: {err}"); + // set item to resolved so we don't request it again + // we could also remove it but that oculd be odd ui + let mut item = (*self.item).clone(); + item.resolved = true; + item + } + }; + completion.replace_item(&self.item, resolved_item); + }; + }) + .await + } +} diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index 3c746548a..0bb1d3d16 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -5,7 +5,7 @@ use helix_core::syntax::LanguageServerFeature; use helix_event::{ cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx, }; -use helix_lsp::lsp; +use helix_lsp::lsp::{self, SignatureInformation}; use helix_stdx::rope::RopeSliceExt; use helix_view::document::Mode; use helix_view::events::{DocumentDidChange, SelectionDidChange}; @@ -18,7 +18,7 @@ use crate::commands::Open; use crate::compositor::Compositor; use crate::events::{OnModeSwitch, PostInsertChar}; use crate::handlers::Handlers; -use crate::ui::lsp::SignatureHelp; +use crate::ui::lsp::{Signature, SignatureHelp}; use crate::ui::Popup; use crate::{job, ui}; @@ -82,6 +82,7 @@ impl helix_event::AsyncHook for SignatureHelpHandler { } } self.state = if open { State::Open } else { State::Closed }; + return timeout; } } @@ -138,6 +139,31 @@ pub fn request_signature_help( }); } +fn active_param_range( + signature: &SignatureInformation, + response_active_parameter: Option, +) -> Option<(usize, usize)> { + let param_idx = signature + .active_parameter + .or(response_active_parameter) + .unwrap_or(0) as usize; + let param = signature.parameters.as_ref()?.get(param_idx)?; + match ¶m.label { + lsp::ParameterLabel::Simple(string) => { + let start = signature.label.find(string.as_str())?; + Some((start, start + string.len())) + } + lsp::ParameterLabel::LabelOffsets([start, end]) => { + // LS sends offsets based on utf-16 based string representation + // but highlighting in helix is done using byte offset. + use helix_core::str_utils::char_to_byte_idx; + let from = char_to_byte_idx(&signature.label, *start as usize); + let to = char_to_byte_idx(&signature.label, *end as usize); + Some((from, to)) + } + } +} + pub fn show_signature_help( editor: &mut Editor, compositor: &mut Compositor, @@ -184,54 +210,50 @@ pub fn show_signature_help( let doc = doc!(editor); let language = doc.language_name().unwrap_or(""); - let signature = match response + if response.signatures.is_empty() { + return; + } + + let signatures: Vec = response .signatures - .get(response.active_signature.unwrap_or(0) as usize) - { - Some(s) => s, - None => return, - }; - let mut contents = SignatureHelp::new( - signature.label.clone(), + .into_iter() + .map(|s| { + let active_param_range = active_param_range(&s, response.active_parameter); + + let signature_doc = if config.lsp.display_signature_help_docs { + s.documentation.map(|doc| match doc { + lsp::Documentation::String(s) => s, + lsp::Documentation::MarkupContent(markup) => markup.value, + }) + } else { + None + }; + + Signature { + signature: s.label, + signature_doc, + active_param_range, + } + }) + .collect(); + + let old_popup = compositor.find_id::>(SignatureHelp::ID); + let mut active_signature = old_popup + .as_ref() + .map(|popup| popup.contents().active_signature()) + .unwrap_or_else(|| response.active_signature.unwrap_or_default() as usize); + + if active_signature >= signatures.len() { + active_signature = signatures.len() - 1; + } + + let contents = SignatureHelp::new( language.to_string(), Arc::clone(&editor.syn_loader), + active_signature, + signatures, ); - let signature_doc = if config.lsp.display_signature_help_docs { - signature.documentation.as_ref().map(|doc| match doc { - lsp::Documentation::String(s) => s.clone(), - lsp::Documentation::MarkupContent(markup) => markup.value.clone(), - }) - } else { - None - }; - - contents.set_signature_doc(signature_doc); - - let active_param_range = || -> Option<(usize, usize)> { - let param_idx = signature - .active_parameter - .or(response.active_parameter) - .unwrap_or(0) as usize; - let param = signature.parameters.as_ref()?.get(param_idx)?; - match ¶m.label { - lsp::ParameterLabel::Simple(string) => { - let start = signature.label.find(string.as_str())?; - Some((start, start + string.len())) - } - lsp::ParameterLabel::LabelOffsets([start, end]) => { - // LS sends offsets based on utf-16 based string representation - // but highlighting in helix is done using byte offset. - use helix_core::str_utils::char_to_byte_idx; - let from = char_to_byte_idx(&signature.label, *start as usize); - let to = char_to_byte_idx(&signature.label, *end as usize); - Some((from, to)) - } - } - }; - contents.set_active_param_range(active_param_range()); - - let old_popup = compositor.find_id::>(SignatureHelp::ID); let mut popup = Popup::new(SignatureHelp::ID, contents) .position(old_popup.and_then(|p| p.get_position())) .position_bias(Open::Above) diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index bab662b04..5a3e8eed4 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, @@ -86,10 +87,12 @@ pub fn default() -> HashMap { "A-;" => flip_selections, "A-o" | "A-up" => expand_selection, "A-i" | "A-down" => shrink_selection, + "A-I" | "A-S-down" => select_all_children, "A-p" | "A-left" => select_prev_sibling, "A-n" | "A-right" => select_next_sibling, "A-e" => move_parent_node_end, "A-b" => move_parent_node_start, + "A-a" => select_all_siblings, "%" => select_all, "x" => extend_line_below, @@ -113,6 +116,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 +130,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, @@ -222,9 +227,10 @@ pub fn default() -> HashMap { "S" => workspace_symbol_picker, "d" => diagnostics_picker, "D" => workspace_diagnostics_picker, + "g" => changed_file_picker, "a" => code_action, "'" => last_picker, - "g" => { "Debug (experimental)" sticky=true + "G" => { "Debug (experimental)" sticky=true "l" => dap_launch, "r" => dap_restart, "b" => dap_toggle_breakpoint, @@ -360,6 +366,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 8b7919951..cf4fbd9fa 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 @@ -53,7 +51,7 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b // in our picker. if matches!( entry.file_name().to_str(), - Some(".git" | ".pijul" | ".jj" | ".hg") + Some(".git" | ".pijul" | ".jj" | ".hg" | ".svn") ) { return false; } diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 6cbb5b109..f793dd484 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,28 +1,24 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, - handlers::trigger_auto_completion, - job, + handlers::{completion::ResolveHandler, trigger_auto_completion}, }; -use helix_event::AsyncHook; use helix_view::{ document::SavePoint, editor::CompleteAction, - graphics::Margin, handlers::lsp::SignatureHelpInvoked, theme::{Modifier, Style}, ViewId, }; -use tokio::time::Instant; use tui::{buffer::Buffer as Surface, text::Span}; -use std::{borrow::Cow, sync::Arc, time::Duration}; +use std::{borrow::Cow, sync::Arc}; use helix_core::{chars, Change, Transaction}; use helix_view::{graphics::Rect, Document, Editor}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; -use helix_lsp::{lsp, util, OffsetEncoding}; +use helix_lsp::{lsp, util, LanguageServerId, OffsetEncoding}; impl menu::Item for CompletionItem { type Data = (); @@ -94,7 +90,7 @@ impl menu::Item for CompletionItem { #[derive(Debug, PartialEq, Default, Clone)] pub struct CompletionItem { pub item: lsp::CompletionItem, - pub language_server_id: usize, + pub provider: LanguageServerId, pub resolved: bool, } @@ -104,7 +100,7 @@ pub struct Completion { #[allow(dead_code)] trigger_offset: usize, filter: String, - resolve_handler: tokio::sync::mpsc::Sender, + resolve_handler: ResolveHandler, } impl Completion { @@ -224,7 +220,7 @@ impl Completion { ($item:expr) => { match editor .language_servers - .get_by_id($item.language_server_id) + .get_by_id($item.provider) { Some(ls) => ls, None => { @@ -285,12 +281,6 @@ impl Completion { let language_server = language_server!(item); let offset_encoding = language_server.offset_encoding(); - let language_server = editor - .language_servers - .get_by_id(item.language_server_id) - .unwrap(); - - // resolve item if not yet resolved if !item.resolved { if let Some(resolved) = Self::resolve_completion_item(language_server, item.item.clone()) @@ -343,16 +333,9 @@ impl Completion { } }); - let margin = if editor.menu_border() { - Margin::vertical(1) - } else { - Margin::none() - }; - let popup = Popup::new(Self::ID, menu) .with_scrollbar(false) - .ignore_escape_key(true) - .margin(margin); + .ignore_escape_key(true); let (view, doc) = current_ref!(editor); let text = doc.text().slice(..); @@ -371,7 +354,7 @@ impl Completion { // TODO: expand nucleo api to allow moving straight to a Utf32String here // and avoid allocation during matching filter: String::from(fragment), - resolve_handler: ResolveHandler::default().spawn(), + resolve_handler: ResolveHandler::new(), }; // need to recompute immediately in case start_offset != trigger_offset @@ -389,7 +372,16 @@ impl Completion { language_server: &helix_lsp::Client, completion_item: lsp::CompletionItem, ) -> Option { - let future = language_server.resolve_completion_item(completion_item)?; + if !matches!( + language_server.capabilities().completion_provider, + Some(lsp::CompletionOptions { + resolve_provider: Some(true), + .. + }) + ) { + return None; + } + let future = language_server.resolve_completion_item(&completion_item); let response = helix_lsp::block_on(future); match response { Ok(item) => Some(item), @@ -422,7 +414,7 @@ impl Completion { self.popup.contents().is_empty() } - fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) { + pub fn replace_item(&mut self, old_item: &CompletionItem, new_item: CompletionItem) { self.popup.contents_mut().replace_option(old_item, new_item); } @@ -444,12 +436,12 @@ impl Component for Completion { self.popup.render(area, surface, cx); // if we have a selection, render a markdown popup on top/below with info - let option = match self.popup.contents().selection() { + let option = match self.popup.contents_mut().selection_mut() { Some(option) => option, None => return, }; if !option.resolved { - helix_event::send_blocking(&self.resolve_handler, option.clone()); + self.resolve_handler.ensure_item_resolved(cx.editor, option); } // need to render: // option.detail @@ -498,12 +490,7 @@ impl Component for Completion { None => return, }; - let popup_area = { - let (popup_x, popup_y) = self.popup.get_rel_position(area, cx.editor); - let (popup_width, popup_height) = self.popup.get_size(); - Rect::new(popup_x, popup_y, popup_width, popup_height) - }; - + let popup_area = self.popup.area(area, cx.editor); let doc_width_available = area.width.saturating_sub(popup_area.right()); let doc_area = if doc_width_available > 30 { let mut doc_width = doc_width_available; @@ -552,88 +539,3 @@ impl Component for Completion { markdown_doc.render(doc_area, surface, cx); } } - -/// A hook for resolving incomplete completion items. -/// -/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion): -/// -/// > If computing full completion items is expensive, servers can additionally provide a -/// > handler for the completion item resolve request. ... -/// > A typical use case is for example: the `textDocument/completion` request doesn't fill -/// > in the `documentation` property for returned completion items since it is expensive -/// > to compute. When the item is selected in the user interface then a -/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter. -/// > The returned completion item should have the documentation property filled in. -#[derive(Debug, Default)] -struct ResolveHandler { - trigger: Option, - request: Option, -} - -impl AsyncHook for ResolveHandler { - type Event = CompletionItem; - - fn handle_event( - &mut self, - item: Self::Event, - timeout: Option, - ) -> Option { - if self - .trigger - .as_ref() - .is_some_and(|trigger| trigger == &item) - { - timeout - } else { - self.trigger = Some(item); - self.request = None; - Some(Instant::now() + Duration::from_millis(150)) - } - } - - fn finish_debounce(&mut self) { - let Some(item) = self.trigger.take() else { return }; - let (tx, rx) = helix_event::cancelation(); - self.request = Some(tx); - job::dispatch_blocking(move |editor, _| resolve_completion_item(editor, item, rx)) - } -} - -fn resolve_completion_item( - editor: &mut Editor, - item: CompletionItem, - cancel: helix_event::CancelRx, -) { - let Some(language_server) = editor.language_server_by_id(item.language_server_id) else { - return; - }; - - let Some(future) = language_server.resolve_completion_item(item.item.clone()) else { - return; - }; - - tokio::spawn(async move { - match helix_event::cancelable_future(future, cancel).await { - Some(Ok(resolved_item)) => { - job::dispatch(move |_, compositor| { - if let Some(completion) = &mut compositor - .find::() - .unwrap() - .completion - { - let resolved_item = CompletionItem { - item: resolved_item, - language_server_id: item.language_server_id, - resolved: true, - }; - - completion.replace_item(item, resolved_item); - }; - }) - .await - } - Some(Err(err)) => log::error!("completion resolve request failed: {err}"), - None => (), - } - }); -} diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index dc61ca2e3..bcbaa3519 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -7,6 +7,7 @@ use helix_core::syntax::Highlight; use helix_core::syntax::HighlightEvent; use helix_core::text_annotations::TextAnnotations; use helix_core::{visual_offset_from_block, Position, RopeSlice}; +use helix_stdx::rope::RopeSliceExt; use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue}; use helix_view::graphics::Rect; use helix_view::theme::Style; @@ -32,14 +33,27 @@ impl LineDecoration for F { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum StyleIterKind { + /// base highlights (usually emitted by TS), byte indices (potentially not codepoint aligned) + BaseHighlights, + /// overlay highlights (emitted by custom code from selections), char indices + Overlay, +} + /// A wrapper around a HighlightIterator /// that merges the layered highlights to create the final text style /// and yields the active text style and the char_idx where the active /// style will have to be recomputed. +/// +/// TODO(ropey2): hopefully one day helix and ropey will operate entirely +/// on byte ranges and we can remove this struct StyleIter<'a, H: Iterator> { text_style: Style, active_highlights: Vec, highlight_iter: H, + kind: StyleIterKind, + text: RopeSlice<'a>, theme: &'a Theme, } @@ -54,7 +68,7 @@ impl> Iterator for StyleIter<'_, H> { HighlightEvent::HighlightEnd => { self.active_highlights.pop(); } - HighlightEvent::Source { start, end } => { + HighlightEvent::Source { start, mut end } => { if start == end { continue; } @@ -64,6 +78,9 @@ impl> Iterator for StyleIter<'_, H> { .fold(self.text_style, |acc, span| { acc.patch(self.theme.highlight(span.0)) }); + if self.kind == StyleIterKind::BaseHighlights { + end = self.text.byte_to_next_char(end); + } return Some((style, end)); } } @@ -185,13 +202,17 @@ pub fn render_text<'t>( text_style: renderer.text_style, active_highlights: Vec::with_capacity(64), highlight_iter: syntax_highlight_iter, + kind: StyleIterKind::BaseHighlights, theme, + text, }; let mut overlay_styles = StyleIter { text_style: Style::default(), active_highlights: Vec::with_capacity(64), highlight_iter: overlay_highlight_iter, + kind: StyleIterKind::Overlay, theme, + text, }; let mut last_line_pos = LinePos { @@ -341,6 +362,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 +417,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 +432,7 @@ impl<'a> TextRenderer<'a> { indent_guide_char: editor_config.indent_guides.character.into(), newline, nbsp, + nnbsp, space, tab, virtual_tab, @@ -448,6 +476,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 +490,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 c1e36bbdd..97f90f625 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -12,9 +12,7 @@ use crate::{ use helix_core::{ diagnostic::NumberOrString, - graphemes::{ - ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, - }, + graphemes::{next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, syntax::{self, HighlightEvent}, text_annotations::TextAnnotations, @@ -315,26 +313,14 @@ impl EditorView { let iter = syntax // TODO: range doesn't actually restrict source, just highlight range .highlight_iter(text.slice(..), Some(range), None) - .map(|event| event.unwrap()) - .map(move |event| match event { - // TODO: use byte slices directly - // convert byte offsets to char offset - HighlightEvent::Source { start, end } => { - let start = - text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start)); - let end = - text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end)); - HighlightEvent::Source { start, end } - } - event => event, - }); + .map(|event| event.unwrap()); Box::new(iter) } None => Box::new( [HighlightEvent::Source { - start: text.byte_to_char(range.start), - end: text.byte_to_char(range.end), + start: range.start, + end: range.end, }] .into_iter(), ), @@ -350,7 +336,8 @@ impl EditorView { let text = doc.text().slice(..); let row = text.char_to_line(anchor.min(text.len_chars())); - let range = Self::viewport_byte_range(text, row, height); + let mut range = Self::viewport_byte_range(text, row, height); + range = text.byte_to_char(range.start)..text.byte_to_char(range.end); text_annotations.collect_overlay_highlights(range) } @@ -359,8 +346,8 @@ impl EditorView { pub fn doc_diagnostics_highlights( doc: &Document, theme: &Theme, - ) -> [Vec<(usize, std::ops::Range)>; 5] { - use helix_core::diagnostic::{DiagnosticTag, Severity}; + ) -> [Vec<(usize, std::ops::Range)>; 7] { + use helix_core::diagnostic::{DiagnosticTag, Range, Severity}; let get_scope_of = |scope| { theme .find_scope_index_exact(scope) @@ -389,6 +376,25 @@ impl EditorView { let mut hint_vec = Vec::new(); let mut warning_vec = Vec::new(); let mut error_vec = Vec::new(); + let mut unnecessary_vec = Vec::new(); + let mut deprecated_vec = Vec::new(); + + let push_diagnostic = + |vec: &mut Vec<(usize, std::ops::Range)>, scope, range: Range| { + // If any diagnostic overlaps ranges with the prior diagnostic, + // merge the two together. Otherwise push a new span. + match vec.last_mut() { + Some((_, existing_range)) if range.start <= existing_range.end => { + // This branch merges overlapping diagnostics, assuming that the current + // diagnostic starts on range.start or later. If this assertion fails, + // we will discard some part of `diagnostic`. This implies that + // `doc.diagnostics()` is not sorted by `diagnostic.range`. + debug_assert!(existing_range.start <= range.start); + existing_range.end = range.end.max(existing_range.end) + } + _ => vec.push((scope, range.start..range.end)), + } + }; for diagnostic in doc.diagnostics() { // Separate diagnostics into different Vecs by severity. @@ -400,31 +406,44 @@ impl EditorView { _ => (&mut default_vec, r#default), }; - let scope = diagnostic - .tags - .first() - .and_then(|tag| match tag { - DiagnosticTag::Unnecessary => unnecessary, - DiagnosticTag::Deprecated => deprecated, - }) - .unwrap_or(scope); - - // If any diagnostic overlaps ranges with the prior diagnostic, - // merge the two together. Otherwise push a new span. - match vec.last_mut() { - Some((_, range)) if diagnostic.range.start <= range.end => { - // This branch merges overlapping diagnostics, assuming that the current - // diagnostic starts on range.start or later. If this assertion fails, - // we will discard some part of `diagnostic`. This implies that - // `doc.diagnostics()` is not sorted by `diagnostic.range`. - debug_assert!(range.start <= diagnostic.range.start); - range.end = diagnostic.range.end.max(range.end) + // If the diagnostic has tags and a non-warning/error severity, skip rendering + // the diagnostic as info/hint/default and only render it as unnecessary/deprecated + // instead. For warning/error diagnostics, render both the severity highlight and + // the tag highlight. + if diagnostic.tags.is_empty() + || matches!( + diagnostic.severity, + Some(Severity::Warning | Severity::Error) + ) + { + push_diagnostic(vec, scope, diagnostic.range); + } + + for tag in &diagnostic.tags { + match tag { + DiagnosticTag::Unnecessary => { + if let Some(scope) = unnecessary { + push_diagnostic(&mut unnecessary_vec, scope, diagnostic.range) + } + } + DiagnosticTag::Deprecated => { + if let Some(scope) = deprecated { + push_diagnostic(&mut deprecated_vec, scope, diagnostic.range) + } + } } - _ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)), } } - [default_vec, info_vec, hint_vec, warning_vec, error_vec] + [ + default_vec, + unnecessary_vec, + deprecated_vec, + info_vec, + hint_vec, + warning_vec, + error_vec, + ] } /// Get highlight spans for selections in a document view. @@ -1015,7 +1034,6 @@ impl EditorView { self.last_insert.1.push(InsertEvent::TriggerCompletion); // TODO : propagate required size on resize to completion too - completion.required_size((size.width, size.height)); self.completion = Some(completion); Some(area) } @@ -1048,13 +1066,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(); @@ -1211,24 +1249,28 @@ impl EditorView { } MouseEventKind::Up(MouseButton::Right) => { - if let Some((coords, view_id)) = gutter_coords_and_view(cxt.editor, row, column) { + if let Some((pos, view_id)) = gutter_coords_and_view(cxt.editor, row, column) { cxt.editor.focus(view_id); - let (view, doc) = current!(cxt.editor); - if let Some(pos) = - view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true) - { - doc.set_selection(view_id, Selection::point(pos)); - if modifiers == KeyModifiers::ALT { - commands::MappableCommand::dap_edit_log.execute(cxt); - } else { - commands::MappableCommand::dap_edit_condition.execute(cxt); - } + if let Some((pos, _)) = pos_and_view(cxt.editor, row, column, true) { + doc_mut!(cxt.editor).set_selection(view_id, Selection::point(pos)); + } else { + let (view, doc) = current!(cxt.editor); - return EventResult::Consumed(None); + if let Some(pos) = view.pos_at_visual_coords(doc, pos.row as u16, 0, true) { + doc.set_selection(view_id, Selection::point(pos)); + match modifiers { + KeyModifiers::ALT => { + commands::MappableCommand::dap_edit_log.execute(cxt) + } + _ => commands::MappableCommand::dap_edit_condition.execute(cxt), + }; + } } - } + cxt.editor.ensure_cursor_in_view(view_id); + return EventResult::Consumed(None); + } EventResult::Ignored(None) } @@ -1279,6 +1321,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/lsp.rs b/helix-term/src/ui/lsp.rs index a3698e38d..b82f7be29 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -3,60 +3,95 @@ use std::sync::Arc; use arc_swap::ArcSwap; use helix_core::syntax; use helix_view::graphics::{Margin, Rect, Style}; +use helix_view::input::Event; use tui::buffer::Buffer; +use tui::layout::Alignment; +use tui::text::Text; use tui::widgets::{BorderType, Paragraph, Widget, Wrap}; -use crate::compositor::{Component, Compositor, Context}; +use crate::compositor::{Component, Compositor, Context, EventResult}; +use crate::alt; use crate::ui::Markdown; use super::Popup; -pub struct SignatureHelp { - signature: String, - signature_doc: Option, +pub struct Signature { + pub signature: String, + pub signature_doc: Option, /// Part of signature text - active_param_range: Option<(usize, usize)>, + pub active_param_range: Option<(usize, usize)>, +} +pub struct SignatureHelp { language: String, config_loader: Arc>, + active_signature: usize, + signatures: Vec, } impl SignatureHelp { pub const ID: &'static str = "signature-help"; pub fn new( - signature: String, language: String, config_loader: Arc>, + active_signature: usize, + signatures: Vec, ) -> Self { Self { - signature, - signature_doc: None, - active_param_range: None, language, config_loader, + active_signature, + signatures, } } - pub fn set_signature_doc(&mut self, signature_doc: Option) { - self.signature_doc = signature_doc; - } - - pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) { - self.active_param_range = offset; + pub fn active_signature(&self) -> usize { + self.active_signature } pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup> { compositor.find_id::>(Self::ID) } + + fn signature_index(&self) -> String { + format!("({}/{})", self.active_signature + 1, self.signatures.len()) + } } impl Component for SignatureHelp { + fn handle_event(&mut self, event: &Event, _cx: &mut Context) -> EventResult { + let Event::Key(event) = event else { + return EventResult::Ignored(None); + }; + + if self.signatures.len() <= 1 { + return EventResult::Ignored(None); + } + + match event { + alt!('p') => { + self.active_signature = self + .active_signature + .checked_sub(1) + .unwrap_or(self.signatures.len() - 1); + EventResult::Consumed(None) + } + alt!('n') => { + self.active_signature = (self.active_signature + 1) % self.signatures.len(); + EventResult::Consumed(None) + } + _ => EventResult::Ignored(None), + } + } + fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { let margin = Margin::horizontal(1); - let active_param_span = self.active_param_range.map(|(start, end)| { + let signature = &self.signatures[self.active_signature]; + + let active_param_span = signature.active_param_range.map(|(start, end)| { vec![( cx.editor .theme @@ -66,21 +101,29 @@ impl Component for SignatureHelp { )] }); + let sig = &self.signatures[self.active_signature]; let sig_text = crate::ui::markdown::highlighted_code_block( - &self.signature, + sig.signature.as_str(), &self.language, Some(&cx.editor.theme), Arc::clone(&self.config_loader), active_param_span, ); + if self.signatures.len() > 1 { + let signature_index = self.signature_index(); + let text = Text::from(signature_index); + let paragraph = Paragraph::new(&text).alignment(Alignment::Right); + paragraph.render(area.clip_top(1).with_height(1).clip_right(1), surface); + } + let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width); let sig_text_area = area.clip_top(1).with_height(sig_text_height); let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area); let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false }); sig_text_para.render(sig_text_area, surface); - if self.signature_doc.is_none() { + if sig.signature_doc.is_none() { return; } @@ -92,7 +135,7 @@ impl Component for SignatureHelp { } } - let sig_doc = match &self.signature_doc { + let sig_doc = match &sig.signature_doc { None => return, Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), }; @@ -110,13 +153,12 @@ impl Component for SignatureHelp { const PADDING: u16 = 2; const SEPARATOR_HEIGHT: u16 = 1; - if PADDING >= viewport.1 || PADDING >= viewport.0 { - return None; - } - let max_text_width = (viewport.0 - PADDING).min(120); + let sig = &self.signatures[self.active_signature]; + + let max_text_width = viewport.0.saturating_sub(PADDING).clamp(10, 120); let signature_text = crate::ui::markdown::highlighted_code_block( - &self.signature, + sig.signature.as_str(), &self.language, None, Arc::clone(&self.config_loader), @@ -125,7 +167,7 @@ impl Component for SignatureHelp { let (sig_width, sig_height) = crate::ui::text::required_size(&signature_text, max_text_width); - let (width, height) = match self.signature_doc { + let (width, height) = match sig.signature_doc { Some(ref doc) => { let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader)); let doc_text = doc_md.parse(None); @@ -139,6 +181,12 @@ impl Component for SignatureHelp { None => (sig_width, sig_height), }; - Some((width + PADDING, height + PADDING)) + let sig_index_width = if self.signatures.len() > 1 { + self.signature_index().len() + 1 + } else { + 0 + }; + + Some((width + PADDING + sig_index_width as u16, height + PADDING)) } } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index c0e60b33e..c5006f958 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -7,18 +7,11 @@ use crate::{ use helix_core::fuzzy::MATCHER; use nucleo::pattern::{Atom, AtomKind, CaseMatching}; use nucleo::{Config, Utf32Str}; -use tui::{ - buffer::Buffer as Surface, - widgets::{Block, Borders, Table, Widget}, -}; +use tui::{buffer::Buffer as Surface, widgets::Table}; pub use tui::widgets::{Cell, Row}; -use helix_view::{ - editor::SmartTabConfig, - graphics::{Margin, Rect}, - Editor, -}; +use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item: Sync + Send + 'static { @@ -241,9 +234,9 @@ impl Menu { } impl Menu { - pub fn replace_option(&mut self, old_option: T, new_option: T) { + pub fn replace_option(&mut self, old_option: &T, new_option: T) { for option in &mut self.options { - if old_option == *option { + if old_option == option { *option = new_option; break; } @@ -341,16 +334,8 @@ impl Component for Menu { .try_get("ui.menu") .unwrap_or_else(|| theme.get("ui.text")); let selected = theme.get("ui.menu.selected"); - surface.clear_with(area, style); - - let render_borders = cx.editor.menu_border(); - let area = if render_borders { - Widget::render(Block::default().borders(Borders::ALL), area, surface); - area.inner(&Margin::vertical(1)) - } else { - area - }; + surface.clear_with(area, style); let scroll = self.scroll; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index b5969818c..5211c2e27 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -13,7 +13,7 @@ mod spinner; mod statusline; mod text; -use crate::compositor::{Component, Compositor}; +use crate::compositor::Compositor; use crate::filter_picker_entry; use crate::job::{self, Callback}; pub use completion::{Completion, CompletionItem}; @@ -143,14 +143,12 @@ pub fn raw_regex_prompt( move |_editor: &mut Editor, compositor: &mut Compositor| { let contents = Text::new(format!("{}", err)); let size = compositor.size(); - let mut popup = Popup::new("invalid-regex", contents) + let popup = Popup::new("invalid-regex", contents) .position(Some(helix_core::Position::new( size.height as usize - 2, // 2 = statusline + commandline 0, ))) .auto_close(true); - popup.required_size((size.width, size.height)); - compositor.replace_or_push("invalid-regex", popup); }, )); diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index b38b8b6e3..5362bdc7f 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -11,20 +11,30 @@ use tui::{ use helix_core::Position; use helix_view::{ graphics::{Margin, Rect}, + input::{MouseEvent, MouseEventKind}, Editor, }; +const MIN_HEIGHT: u16 = 6; +const MAX_HEIGHT: u16 = 26; +const MAX_WIDTH: u16 = 120; + +struct RenderInfo { + area: Rect, + child_height: u16, + render_borders: bool, + is_menu: bool, +} + // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // a width/height hint. maybe Popup(Box) pub struct Popup { contents: T, position: Option, - margin: Margin, - size: (u16, u16), - child_size: (u16, u16), + area: Rect, position_bias: Open, - scroll: usize, + scroll_half_pages: usize, auto_close: bool, ignore_escape_key: bool, id: &'static str, @@ -36,11 +46,9 @@ impl Popup { Self { contents, position: None, - margin: Margin::none(), - size: (0, 0), position_bias: Open::Below, - child_size: (0, 0), - scroll: 0, + area: Rect::new(0, 0, 0, 0), + scroll_half_pages: 0, auto_close: false, ignore_escape_key: false, id, @@ -70,11 +78,6 @@ impl Popup { self } - pub fn margin(mut self, margin: Margin) -> Self { - self.margin = margin; - self - } - pub fn auto_close(mut self, auto_close: bool) -> Self { self.auto_close = auto_close; self @@ -92,28 +95,65 @@ impl Popup { self } - /// Calculate the position where the popup should be rendered and return the coordinates of the - /// top left corner. - pub fn get_rel_position(&mut self, viewport: Rect, editor: &Editor) -> (u16, u16) { - let position = self + pub fn scroll_half_page_down(&mut self) { + self.scroll_half_pages += 1; + } + + pub fn scroll_half_page_up(&mut self) { + self.scroll_half_pages = self.scroll_half_pages.saturating_sub(1); + } + + /// Toggles the Popup's scrollbar. + /// Consider disabling the scrollbar in case the child + /// already has its own. + pub fn with_scrollbar(mut self, enable_scrollbar: bool) -> Self { + self.has_scrollbar = enable_scrollbar; + self + } + + pub fn contents(&self) -> &T { + &self.contents + } + + pub fn contents_mut(&mut self) -> &mut T { + &mut self.contents + } + + pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { + self.render_info(viewport, editor).area + } + + fn render_info(&mut self, viewport: Rect, editor: &Editor) -> RenderInfo { + let mut position = editor.cursor().0.unwrap_or_default(); + if let Some(old_position) = self .position - .get_or_insert_with(|| editor.cursor().0.unwrap_or_default()); + .filter(|old_position| old_position.row == position.row) + { + position = old_position; + } else { + self.position = Some(position); + } - let (width, height) = self.size; + let is_menu = self + .contents + .type_name() + .starts_with("helix_term::ui::menu::Menu"); - // if there's a orientation preference, use that - // if we're on the top part of the screen, do below - // if we're on the bottom part, do above + let mut render_borders = if is_menu { + editor.menu_border() + } else { + editor.popup_border() + }; // -- make sure frame doesn't stick out of bounds let mut rel_x = position.col as u16; let mut rel_y = position.row as u16; - if viewport.width <= rel_x + width { - rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - } - let can_put_below = viewport.height > rel_y + height; - let can_put_above = rel_y.checked_sub(height).is_some(); + // if there's a orientation preference, use that + // if we're on the top part of the screen, do below + // if we're on the bottom part, do above + let can_put_below = viewport.height > rel_y + MIN_HEIGHT; + let can_put_above = rel_y.checked_sub(MIN_HEIGHT).is_some(); let final_pos = match self.position_bias { Open::Below => match can_put_below { true => Open::Below, @@ -125,51 +165,87 @@ impl Popup { }, }; - rel_y = match final_pos { - Open::Above => rel_y.saturating_sub(height), - Open::Below => rel_y + 1, + // compute maximum space available for child + let mut max_height = match final_pos { + Open::Above => rel_y, + Open::Below => viewport.height.saturating_sub(1 + rel_y), }; + max_height = max_height.min(MAX_HEIGHT); + let mut max_width = viewport.width.saturating_sub(2).min(MAX_WIDTH); + render_borders = render_borders && max_height > 3 && max_width > 3; + if render_borders { + max_width -= 2; + max_height -= 2; + } - (rel_x, rel_y) - } - - pub fn get_size(&self) -> (u16, u16) { - (self.size.0, self.size.1) - } + // compute required child size and reclamp + let (mut width, child_height) = self + .contents + .required_size((max_width, max_height)) + .expect("Component needs required_size implemented in order to be embedded in a popup"); - pub fn scroll(&mut self, offset: usize, direction: bool) { - if direction { - let max_offset = self.child_size.1.saturating_sub(self.size.1); - self.scroll = (self.scroll + offset).min(max_offset as usize); + width = width.min(MAX_WIDTH); + let height = if render_borders { + (child_height + 2).min(MAX_HEIGHT) } else { - self.scroll = self.scroll.saturating_sub(offset); + child_height.min(MAX_HEIGHT) + }; + if render_borders { + width += 2; + } + if viewport.width <= rel_x + width + 2 { + rel_x = viewport.width.saturating_sub(width + 2); + width = viewport.width.saturating_sub(rel_x + 2) } - } - - /// Toggles the Popup's scrollbar. - /// Consider disabling the scrollbar in case the child - /// already has its own. - pub fn with_scrollbar(mut self, enable_scrollbar: bool) -> Self { - self.has_scrollbar = enable_scrollbar; - self - } - - pub fn contents(&self) -> &T { - &self.contents - } - pub fn contents_mut(&mut self) -> &mut T { - &mut self.contents + let area = match final_pos { + Open::Above => { + rel_y = rel_y.saturating_sub(height); + Rect::new(rel_x, rel_y, width, position.row as u16 - rel_y) + } + Open::Below => { + rel_y += 1; + let y_max = viewport.bottom().min(height + rel_y); + Rect::new(rel_x, rel_y, width, y_max - rel_y) + } + }; + RenderInfo { + area, + child_height, + render_borders, + is_menu, + } } - pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { - // trigger required_size so we recalculate if the child changed - self.required_size((viewport.width, viewport.height)); - - let (rel_x, rel_y) = self.get_rel_position(viewport, editor); + 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); + } - // clip to viewport - viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)) + 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), + } } } @@ -177,6 +253,7 @@ 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 +277,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) } _ => { @@ -223,63 +300,48 @@ impl Component for Popup { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } - fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let max_width = 120.min(viewport.0); - let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport - - let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin); - - let (width, height) = self - .contents - .required_size((inner.width, inner.height)) - .expect("Component needs required_size implemented in order to be embedded in a popup"); - - self.child_size = (width, height); - self.size = ( - (width + self.margin.width()).min(max_width), - (height + self.margin.height()).min(max_height), - ); - - // re-clamp scroll offset - let max_offset = self.child_size.1.saturating_sub(self.size.1); - self.scroll = self.scroll.min(max_offset as usize); - - Some(self.size) - } - fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - let area = self.area(viewport, cx.editor); - cx.scroll = Some(self.scroll); + let RenderInfo { + area, + child_height, + render_borders, + is_menu, + } = self.render_info(viewport, cx.editor); + self.area = area; // clear area - let background = cx.editor.theme.get("ui.popup"); - surface.clear_with(area, background); - - let render_borders = cx.editor.popup_border(); - - let inner = if self - .contents - .type_name() - .starts_with("helix_term::ui::menu::Menu") - { - area + let background = if is_menu { + // TODO: consistently style menu + cx.editor + .theme + .try_get("ui.menu") + .unwrap_or_else(|| cx.editor.theme.get("ui.text")) } else { - area.inner(&self.margin) + cx.editor.theme.get("ui.popup") }; + surface.clear_with(area, background); - let border = usize::from(render_borders); + let mut inner = area; if render_borders { + inner = area.inner(&Margin::all(1)); Widget::render(Block::default().borders(Borders::ALL), area, surface); } + let border = usize::from(render_borders); + let max_offset = child_height.saturating_sub(inner.height) as usize; + let half_page_size = (inner.height / 2) as usize; + let scroll = max_offset.min(self.scroll_half_pages * half_page_size); + if half_page_size > 0 { + self.scroll_half_pages = scroll / half_page_size; + } + cx.scroll = Some(scroll); self.contents.render(inner, surface, cx); // render scrollbar if contents do not fit if self.has_scrollbar { - let win_height = (inner.height as usize).saturating_sub(2 * border); - let len = (self.child_size.1 as usize).saturating_sub(2 * border); + let win_height = inner.height as usize; + let len = child_height as usize; let fits = len <= win_height; - let scroll = self.scroll; let scroll_style = cx.editor.theme.get("ui.menu.scroll"); const fn div_ceil(a: usize, b: usize) -> usize { @@ -293,7 +355,8 @@ impl Component for Popup { let mut cell; for i in 0..win_height { - cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)]; + cell = + &mut surface[(inner.right() - 1 + border as u16, inner.top() + i as u16)]; let half_block = if render_borders { "▌" } else { "▐" }; diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index d46c13138..a6ee7f05d 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/spinner.rs b/helix-term/src/ui/spinner.rs index 379c4489f..9ce610555 100644 --- a/helix-term/src/ui/spinner.rs +++ b/helix-term/src/ui/spinner.rs @@ -1,16 +1,18 @@ use std::{collections::HashMap, time::Instant}; +use helix_lsp::LanguageServerId; + #[derive(Default, Debug)] pub struct ProgressSpinners { - inner: HashMap, + inner: HashMap, } impl ProgressSpinners { - pub fn get(&self, id: usize) -> Option<&Spinner> { + pub fn get(&self, id: LanguageServerId) -> Option<&Spinner> { self.inner.get(&id) } - pub fn get_or_create(&mut self, id: usize) -> &mut Spinner { + pub fn get_or_create(&mut self, id: LanguageServerId) -> &mut Spinner { self.inner.entry(id).or_default() } } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 2939a2573..8a87242f9 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -173,7 +173,7 @@ fn render_mode<'a>(context: &RenderContext) -> Spans<'a> { " ".into() }; let modename = format!(" {} ", modename); - if config.color_modes { + if visible && config.color_modes { Span::styled( modename, match context.editor.mode() { diff --git a/helix-term/tests/test/auto_indent.rs b/helix-term/tests/test/auto_indent.rs index 5132d44d9..5ea1024b7 100644 --- a/helix-term/tests/test/auto_indent.rs +++ b/helix-term/tests/test/auto_indent.rs @@ -6,13 +6,13 @@ async fn auto_indent_c() -> anyhow::Result<()> { AppBuilder::new().with_file("foo.c", None), // switches to append mode? ( - helpers::platform_line("void foo() {#[|}]#"), + "void foo() {#[|}]#", "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ void foo() { #[|\n]#\ } - "}), + "}, ), ) .await?; diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs index ada488fc9..c921e2ae7 100644 --- a/helix-term/tests/test/auto_pairs.rs +++ b/helix-term/tests/test/auto_pairs.rs @@ -19,6 +19,7 @@ async fn insert_basic() -> anyhow::Result<()> { format!("#[{}|]#", LINE_END), format!("i{}", pair.0), format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -46,6 +47,7 @@ async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> { format!("#[{}|]#", LINE_END), format!("i{}", open), format!("{}#[|{}]#{}", open, close, LINE_END), + LineFeedHandling::AsIs, ), ) .await?; @@ -56,6 +58,7 @@ async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> { format!("{}#[{}|]#{}", open, close, LINE_END), format!("i{}", close), format!("{}{}#[|{}]#", open, close, LINE_END), + LineFeedHandling::AsIs, ), ) .await?; @@ -71,6 +74,7 @@ async fn insert_after_word() -> anyhow::Result<()> { format!("foo#[{}|]#", LINE_END), format!("i{}", pair.0), format!("foo{}#[|{}]#{}", pair.0, pair.1, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -80,6 +84,7 @@ async fn insert_after_word() -> anyhow::Result<()> { format!("foo#[{}|]#", LINE_END), format!("i{}", pair.0), format!("foo{}#[|{}]#", pair.0, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -94,6 +99,7 @@ async fn insert_before_word() -> anyhow::Result<()> { format!("#[f|]#oo{}", LINE_END), format!("i{}", pair.0), format!("{}#[|f]#oo{}", pair.0, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -108,6 +114,7 @@ async fn insert_before_word_selection() -> anyhow::Result<()> { format!("#[foo|]#{}", LINE_END), format!("i{}", pair.0), format!("{}#[|foo]#{}", pair.0, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -122,6 +129,7 @@ async fn insert_before_word_selection_trailing_word() -> anyhow::Result<()> { format!("foo#[ wor|]#{}", LINE_END), format!("i{}", pair.0), format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -136,6 +144,7 @@ async fn insert_closer_selection_trailing_word() -> anyhow::Result<()> { format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END), format!("i{}", pair.1), format!("foo{}{}#[| wor]#{}", pair.0, pair.1, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -155,6 +164,7 @@ async fn insert_before_eol() -> anyhow::Result<()> { open = pair.0, close = pair.1 ), + LineFeedHandling::AsIs, )) .await?; } @@ -177,6 +187,7 @@ async fn insert_auto_pairs_disabled() -> anyhow::Result<()> { format!("#[{}|]#", LINE_END), format!("i{}", pair.0), format!("{}#[|{}]#", pair.0, LINE_END), + LineFeedHandling::AsIs, ), ) .await?; @@ -197,6 +208,7 @@ async fn insert_multi_range() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -211,6 +223,7 @@ async fn insert_before_multi_code_point_graphemes() -> anyhow::Result<()> { format!("hello #[👨‍👩‍👧‍👦|]# goodbye{}", LINE_END), format!("i{}", pair.1), format!("hello {}#[|👨‍👩‍👧‍👦]# goodbye{}", pair.1, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -226,6 +239,7 @@ async fn insert_at_end_of_document() -> anyhow::Result<()> { in_keys: format!("i{}", pair.0), out_text: format!("{}{}{}", LINE_END, pair.0, pair.1), out_selection: Selection::single(LINE_END.len() + 1, LINE_END.len() + 2), + line_feed_handling: LineFeedHandling::AsIs, }) .await?; @@ -235,6 +249,7 @@ async fn insert_at_end_of_document() -> anyhow::Result<()> { in_keys: format!("i{}", pair.0), out_text: format!("foo{}{}{}", LINE_END, pair.0, pair.1), out_selection: Selection::single(LINE_END.len() + 4, LINE_END.len() + 5), + line_feed_handling: LineFeedHandling::AsIs, }) .await?; } @@ -259,6 +274,7 @@ async fn insert_close_inside_pair() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -283,6 +299,7 @@ async fn insert_close_inside_pair_multi() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -307,6 +324,7 @@ async fn insert_nested_open_inside_pair() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -338,6 +356,7 @@ async fn insert_nested_open_inside_pair_multi() -> anyhow::Result<()> { inner_close = inner_pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -358,6 +377,7 @@ async fn append_basic() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -377,6 +397,7 @@ async fn append_multi_range() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -401,6 +422,7 @@ async fn append_close_inside_pair() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -425,6 +447,7 @@ async fn append_close_inside_pair_multi() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -444,6 +467,7 @@ async fn append_end_of_word() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -458,6 +482,7 @@ async fn append_middle_of_word() -> anyhow::Result<()> { format!("#[wo|]#rd{}", LINE_END), format!("a{}", pair.1), format!("#[wo{}r|]#d{}", pair.1, LINE_END), + LineFeedHandling::AsIs, )) .await?; } @@ -477,6 +502,7 @@ async fn append_end_of_word_multi() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -501,6 +527,7 @@ async fn append_inside_nested_pair() -> anyhow::Result<()> { close = pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } @@ -532,6 +559,7 @@ async fn append_inside_nested_pair_multi() -> anyhow::Result<()> { inner_close = inner_pair.1, eol = LINE_END ), + LineFeedHandling::AsIs, )) .await?; } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 1172a7981..1c5c6196b 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -9,89 +9,89 @@ mod write; async fn test_selection_duplication() -> anyhow::Result<()> { // Forward test(( - platform_line(indoc! {"\ + indoc! {"\ #[lo|]#rem ipsum dolor - "}), + "}, "CC", - platform_line(indoc! {"\ + indoc! {"\ #(lo|)#rem #(ip|)#sum #[do|]#lor - "}), + "}, )) .await?; // Backward test(( - platform_line(indoc! {"\ + indoc! {"\ #[|lo]#rem ipsum dolor - "}), + "}, "CC", - platform_line(indoc! {"\ + indoc! {"\ #(|lo)#rem #(|ip)#sum #[|do]#lor - "}), + "}, )) .await?; // Copy the selection to previous line, skipping the first line in the file test(( - platform_line(indoc! {"\ + indoc! {"\ test #[testitem|]# - "}), + "}, "", - platform_line(indoc! {"\ + indoc! {"\ test #[testitem|]# - "}), + "}, )) .await?; // Copy the selection to previous line, including the first line in the file test(( - platform_line(indoc! {"\ + indoc! {"\ test #[test|]# - "}), + "}, "", - platform_line(indoc! {"\ + indoc! {"\ #[test|]# #(test|)# - "}), + "}, )) .await?; // Copy the selection to next line, skipping the last line in the file test(( - platform_line(indoc! {"\ + indoc! {"\ #[testitem|]# test - "}), + "}, "C", - platform_line(indoc! {"\ + indoc! {"\ #[testitem|]# test - "}), + "}, )) .await?; // Copy the selection to next line, including the last line in the file test(( - platform_line(indoc! {"\ + indoc! {"\ #[test|]# test - "}), + "}, "C", - platform_line(indoc! {"\ + indoc! {"\ #(test|)# #[test|]# - "}), + "}, )) .await?; Ok(()) @@ -153,23 +153,45 @@ async fn test_goto_file_impl() -> anyhow::Result<()> { ) .await?; + // ';' is behind the path + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one.js';B;gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + // allow numeric values in path + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one123.js'B;gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one123.js"])); + }), + false, + ) + .await?; + Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_multi_selection_paste() -> anyhow::Result<()> { test(( - platform_line(indoc! {"\ + indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}), + "}, "yp", - platform_line(indoc! {"\ + indoc! {"\ lorem#[|lorem]# ipsum#(|ipsum)# dolor#(|dolor)# - "}), + "}, )) .await?; @@ -180,58 +202,58 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> { async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { // pipe test(( - platform_line(indoc! {"\ + indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}), + "}, "|echo foo", - platform_line(indoc! {"\ + indoc! {"\ #[|foo\n]# #(|foo\n)# #(|foo\n)# - "}), + "}, )) .await?; // insert-output test(( - platform_line(indoc! {"\ + indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}), + "}, "!echo foo", - platform_line(indoc! {"\ + indoc! {"\ #[|foo\n]# lorem #(|foo\n)# ipsum #(|foo\n)# dolor - "}), + "}, )) .await?; // append-output test(( - platform_line(indoc! {"\ + indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}), + "}, "echo foo", - platform_line(indoc! {"\ + indoc! {"\ lorem#[|foo\n]# ipsum#(|foo\n)# dolor#(|foo\n)# - "}), + "}, )) .await?; @@ -247,7 +269,13 @@ async fn test_undo_redo() -> anyhow::Result<()> { // * u Undo the two newlines. We're now on line 1. // * Jump forward an back again in the jumplist. This would panic // if the jumplist were not being updated correctly. - test(("#[|]#", "2[u", "#[|]#")).await?; + test(( + "#[|]#", + "2[u", + "#[|]#", + LineFeedHandling::AsIs, + )) + .await?; // A jumplist selection is passed through an edit and then an undo and then a redo. // @@ -258,10 +286,22 @@ async fn test_undo_redo() -> anyhow::Result<()> { // * Jump back in the jumplist. This would panic if the jumplist were not being // updated correctly. // * Jump forward to line 1. - test(("#[|]#", "[kduU", "#[|]#")).await?; + test(( + "#[|]#", + "[kduU", + "#[|]#", + LineFeedHandling::AsIs, + )) + .await?; // In this case we 'redo' manually to ensure that the transactions are composing correctly. - test(("#[|]#", "[u[u", "#[|]#")).await?; + test(( + "#[|]#", + "[u[u", + "#[|]#", + LineFeedHandling::AsIs, + )) + .await?; Ok(()) } @@ -270,35 +310,35 @@ async fn test_undo_redo() -> anyhow::Result<()> { async fn test_extend_line() -> anyhow::Result<()> { // extend with line selected then count test(( - platform_line(indoc! {"\ + indoc! {"\ #[l|]#orem ipsum dolor - "}), + "}, "x2x", - platform_line(indoc! {"\ + indoc! {"\ #[lorem ipsum dolor\n|]# - "}), + "}, )) .await?; // extend with count on partial selection test(( - platform_line(indoc! {"\ + indoc! {"\ #[l|]#orem ipsum - "}), + "}, "2x", - platform_line(indoc! {"\ + indoc! {"\ #[lorem ipsum\n|]# - "}), + "}, )) .await?; @@ -366,16 +406,11 @@ async fn test_character_info() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_delete_char_backward() -> anyhow::Result<()> { // don't panic when deleting overlapping ranges + test(("#(x|)# #[x|]#", "c", "#[\n|]#")).await?; test(( - platform_line("#(x|)# #[x|]#"), - "c", - platform_line("#[\n|]#"), - )) - .await?; - test(( - platform_line("#( |)##( |)#a#( |)#axx#[x|]#a"), + "#( |)##( |)#a#( |)#axx#[x|]#a", "li", - platform_line("#(a|)##(|a)#xx#[|a]#"), + "#(a|)##(|a)#xx#[|a]#", )) .await?; @@ -385,43 +420,33 @@ async fn test_delete_char_backward() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_delete_word_backward() -> anyhow::Result<()> { // don't panic when deleting overlapping ranges - test(( - platform_line("fo#[o|]#ba#(r|)#"), - "a", - platform_line("#[\n|]#"), - )) - .await?; + test(("fo#[o|]#ba#(r|)#", "a", "#[\n|]#")).await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_delete_word_forward() -> anyhow::Result<()> { // don't panic when deleting overlapping ranges - test(( - platform_line("fo#[o|]#b#(|ar)#"), - "i", - platform_line("fo#[\n|]#"), - )) - .await?; + test(("fo#[o|]#b#(|ar)#", "i", "fo#[\n|]#")).await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_delete_char_forward() -> anyhow::Result<()> { test(( - platform_line(indoc! {"\ + indoc! {"\ #[abc|]#def #(abc|)#ef #(abc|)#f #(abc|)# - "}), + "}, "a", - platform_line(indoc! {"\ + indoc! {"\ #[abc|]#ef #(abc|)#f #(abc|)# #(abc|)# - "}), + "}, )) .await?; @@ -430,33 +455,37 @@ async fn test_delete_char_forward() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_insert_with_indent() -> anyhow::Result<()> { - const INPUT: &str = "\ -#[f|]#n foo() { - if let Some(_) = None { + const INPUT: &str = indoc! { " + #[f|]#n foo() { + if let Some(_) = None { - } -\x20 -} + } + + } -fn bar() { + fn bar() { -}"; + } + " + }; // insert_at_line_start test(( INPUT, ":lang rust%I", - "\ -#[f|]#n foo() { - #(i|)#f let Some(_) = None { - #(\n|)#\ -\x20 #(}|)# -#(\x20|)# -#(}|)# -#(\n|)#\ -#(f|)#n bar() { - #(\n|)#\ -#(}|)#", + indoc! { " + #[f|]#n foo() { + #(i|)#f let Some(_) = None { + #(\n|)# + #(}|)# + #( |)# + #(}|)# + #(\n|)# + #(f|)#n bar() { + #(\n|)# + #(}|)# + " + }, )) .await?; @@ -464,17 +493,19 @@ fn bar() { test(( INPUT, ":lang rust%A", - "\ -fn foo() {#[\n|]#\ -\x20 if let Some(_) = None {#(\n|)#\ -\x20 #(\n|)#\ -\x20 }#(\n|)#\ -\x20#(\n|)#\ -}#(\n|)#\ -#(\n|)#\ -fn bar() {#(\n|)#\ -\x20 #(\n|)#\ -}#(|)#", + indoc! { " + fn foo() {#[\n|]# + if let Some(_) = None {#(\n|)# + #(\n|)# + }#(\n|)# + #(\n|)# + }#(\n|)# + #(\n|)# + fn bar() {#(\n|)# + #(\n|)# + }#(\n|)# + " + }, )) .await?; @@ -485,42 +516,42 @@ fn bar() {#(\n|)#\ async fn test_join_selections() -> anyhow::Result<()> { // normal join test(( - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, "J", - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, )) .await?; // join with empty line test(( - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, "JJ", - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, )) .await?; // join with additional space in non-empty line test(( - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, "JJ", - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, )) .await?; @@ -531,7 +562,7 @@ async fn test_join_selections() -> anyhow::Result<()> { async fn test_join_selections_space() -> anyhow::Result<()> { // join with empty lines panic test(( - platform_line(indoc! {"\ + indoc! {"\ #[a b @@ -541,69 +572,153 @@ async fn test_join_selections_space() -> anyhow::Result<()> { d e|]# - "}), + "}, "", - platform_line(indoc! {"\ + indoc! {"\ a#[ |]#b#( |)#c#( |)#d#( |)#e - "}), + "}, )) .await?; // normal join test(( - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, "", - platform_line(indoc! {"\ + indoc! {"\ abc#[ |]#def - "}), + "}, )) .await?; // join with empty line test(( - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, "", - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, )) .await?; // join with additional space in non-empty line test(( - platform_line(indoc! {"\ + indoc! {"\ #[a|]#bc def - "}), + "}, "", - platform_line(indoc! {"\ + indoc! {"\ abc#[ |]#def - "}), + "}, )) .await?; // join with retained trailing spaces test(( - platform_line(indoc! {"\ + indoc! {"\ #[aaa bb c |]# - "}), + "}, "", - platform_line(indoc! {"\ + indoc! {"\ aaa #[ |]#bb #( |)#c - "}), + "}, + )) + .await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_read_file() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let contents_to_read = "some contents"; + let output_file = helpers::temp_file_with_contents(contents_to_read)?; + + test_key_sequence( + &mut helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?, + Some(&format!(":r {:?}:w", output_file.path())), + Some(&|app| { + assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); + }), + false, + ) + .await?; + + let expected_contents = LineFeedHandling::Native.apply(contents_to_read); + helpers::assert_file_has_content(&mut file, &expected_contents)?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn surround_delete() -> anyhow::Result<()> { + // Test `surround_delete` when head < anchor + test(("(#[| ]#)", "mdm", "#[| ]#")).await?; + test(("(#[| ]#)", "md(", "#[| ]#")).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn surround_replace_ts() -> anyhow::Result<()> { + const INPUT: &str = r#"\ +fn foo() { + if let Some(_) = None { + todo!("f#[|o]#o)"); + } +} +"#; + test(( + INPUT, + ":lang rustmrm'", + r#"\ +fn foo() { + if let Some(_) = None { + todo!('f#[|o]#o)'); + } +} +"#, + )) + .await?; + + test(( + INPUT, + ":lang rust3mrm[", + r#"\ +fn foo() { + if let Some(_) = None [ + todo!("f#[|o]#o)"); + ] +} +"#, + )) + .await?; + + test(( + INPUT, + ":lang rust2mrm{", + r#"\ +fn foo() { + if let Some(_) = None { + todo!{"f#[|o]#o)"}; + } +} +"#, )) .await?; diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 5be68837c..b2471f635 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -6,7 +6,7 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { // single cursor stays single cursor, first goes to end of current // node, then parent ( - helpers::platform_line(indoc! {r##" + indoc! {r##" fn foo() { let result = if true { "yes" @@ -14,9 +14,9 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { "no#["|]# } } - "##}), + "##}, "", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -24,10 +24,10 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { \"no\"#[\n|]# } } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -35,9 +35,9 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { \"no\"#[\n|]# } } - "}), + "}, "", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -45,11 +45,11 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { \"no\" }#[\n|]# } - "}), + "}, ), // select mode extends ( - helpers::platform_line(indoc! {r##" + indoc! {r##" fn foo() { let result = if true { "yes" @@ -57,9 +57,9 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { #["no"|]# } } - "##}), + "##}, "v", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -67,7 +67,7 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { #[\"no\" }\n|]# } - "}), + "}, ), ]; @@ -84,7 +84,7 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { // single cursor stays single cursor, first goes to end of current // node, then parent ( - helpers::platform_line(indoc! {r##" + indoc! {r##" fn foo() { let result = if true { "yes" @@ -92,9 +92,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { "no#["|]# } } - "##}), + "##}, "", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -102,10 +102,10 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { #[\"|]#no\" } } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -113,9 +113,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { \"no\"#[\n|]# } } - "}), + "}, "", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -123,10 +123,10 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { \"no\" } } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -134,9 +134,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { \"no\" } } - "}), + "}, "", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -144,11 +144,11 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { \"no\" } } - "}), + "}, ), // select mode extends ( - helpers::platform_line(indoc! {r##" + indoc! {r##" fn foo() { let result = if true { "yes" @@ -156,9 +156,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { #["no"|]# } } - "##}), + "##}, "v", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -166,10 +166,10 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { ]#\"no\" } } - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" fn foo() { let result = if true { "yes" @@ -177,9 +177,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { #["no"|]# } } - "##}), + "##}, "v", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -187,7 +187,7 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> { ]#\"no\" } } - "}), + "}, ), ]; @@ -204,7 +204,7 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { // single cursor stays single cursor, first goes to end of current // node, then parent ( - helpers::platform_line(indoc! {r##" + indoc! {r##" fn foo() { let result = if true { "yes" @@ -212,9 +212,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { "no#["|]# } } - "##}), + "##}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -222,10 +222,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\"#[|\n]# } } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -233,9 +233,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\"#[\n|]# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -243,12 +243,12 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\" }#[|\n]# } - "}), + "}, ), // appending to the end of a line should still look at the current // line, not the next one ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -256,9 +256,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no#[\"|]# } } - "}), + "}, "a", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -266,11 +266,11 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\" }#[\n|]# } - "}), + "}, ), // before cursor is all whitespace, so insert tab ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -278,9 +278,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { #[\"no\"|]# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -288,12 +288,12 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { #[|\"no\"]# } } - "}), + "}, ), // if selection spans multiple lines, it should still only look at the // line on which the head is ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[\"yes\" @@ -301,9 +301,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\"|]# } } - "}), + "}, "a", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -311,10 +311,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\" }#[\n|]# } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[\"yes\" @@ -322,9 +322,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\"|]# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[|\"yes\" @@ -332,10 +332,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\"]# } } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { #[l|]#et result = if true { #(\"yes\" @@ -343,9 +343,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\"|)# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { #[|l]#et result = if true { #(|\"yes\" @@ -353,10 +353,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\")# } } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\"#[\n|]# @@ -364,9 +364,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\"#(\n|)# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -374,10 +374,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\" }#(|\n)# } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[\"yes\"|]# @@ -385,9 +385,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { #(\"no\"|)# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[|\"yes\"]# @@ -395,12 +395,12 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { #(|\"no\")# } } - "}), + "}, ), // if any cursors are not preceded by all whitespace, then do the // smart_tab action ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[\"yes\"\n|]# @@ -408,9 +408,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no#(\"\n|)# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { \"yes\" @@ -418,11 +418,11 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no\" }#(|\n)# } - "}), + "}, ), // Ctrl-tab always inserts a tab ( - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[\"yes\"\n|]# @@ -430,9 +430,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no#(\"\n|)# } } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ fn foo() { let result = if true { #[|\"yes\"\n]# @@ -440,7 +440,363 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> { \"no #(|\"\n)# } } - "}), + "}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn select_all_siblings() -> anyhow::Result<()> { + let tests = vec![ + // basic tests + ( + indoc! {r##" + let foo = bar(#[a|]#, b, c); + "##}, + "", + indoc! {r##" + let foo = bar(#[a|]#, #(b|)#, #(c|)#); + "##}, + ), + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + 3, + 4, + 5, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // direction is preserved + ( + indoc! {r##" + let a = [ + #[|1]#, + 2, + 3, + 4, + 5, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + "##}, + ), + // can't pick any more siblings - selection stays the same + ( + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // each cursor does the sibling select independently + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + 3, + 4, + 5, + ]; + + let b = [ + #("one"|)#, + "two", + "three", + "four", + "five", + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + + let b = [ + #("one"|)#, + #("two"|)#, + #("three"|)#, + #("four"|)#, + #("five"|)#, + ]; + "##}, + ), + // conflicting sibling selections get normalized. Here, the primary + // selection would choose every list item, but because the secondary + // range covers more than one item, the descendent is the entire list, + // which means the sibling is the assignment. The list item ranges just + // get normalized out since the list itself becomes selected. + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + #(3, + 4|)#, + 5, + ]; + "##}, + "", + indoc! {r##" + let #(a|)# = #[[ + 1, + 2, + 3, + 4, + 5, + ]|]#; + "##}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn select_all_children() -> anyhow::Result<()> { + let tests = vec![ + // basic tests + ( + indoc! {r##" + let foo = bar#[(a, b, c)|]#; + "##}, + "", + indoc! {r##" + let foo = bar(#[a|]#, #(b|)#, #(c|)#); + "##}, + ), + ( + indoc! {r##" + let a = #[[ + 1, + 2, + 3, + 4, + 5, + ]|]#; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // direction is preserved + ( + indoc! {r##" + let a = #[|[ + 1, + 2, + 3, + 4, + 5, + ]]#; + "##}, + "", + indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + "##}, + ), + // can't pick any more children - selection stays the same + ( + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // each cursor does the sibling select independently + ( + indoc! {r##" + let a = #[|[ + 1, + 2, + 3, + 4, + 5, + ]]#; + + let b = #([ + "one", + "two", + "three", + "four", + "five", + ]|)#; + "##}, + "", + indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + + let b = [ + #("one"|)#, + #("two"|)#, + #("three"|)#, + #("four"|)#, + #("five"|)#, + ]; + "##}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_select_next_sibling() -> anyhow::Result<()> { + let tests = vec![ + // basic test + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 #[}|]# + fn dec(x: usize) -> usize { x - 1 } + fn ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + #[fn dec(x: usize) -> usize { x - 1 }|]# + fn ident(x: usize) -> usize { x } + "##}, + ), + // direction is not preserved and is always forward. + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 #[}|]# + fn dec(x: usize) -> usize { x - 1 } + fn ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + fn dec(x: usize) -> usize { x - 1 } + #[fn ident(x: usize) -> usize { x }|]# + "##}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_select_prev_sibling() -> anyhow::Result<()> { + let tests = vec![ + // basic test + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + fn dec(x: usize) -> usize { x - 1 } + #[|f]#n ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + #[|fn dec(x: usize) -> usize { x - 1 }]# + fn ident(x: usize) -> usize { x } + "##}, + ), + // direction is not preserved and is always backward. + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + fn dec(x: usize) -> usize { x - 1 } + #[|f]#n ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + #[|fn inc(x: usize) -> usize { x + 1 }]# + fn dec(x: usize) -> usize { x - 1 } + fn ident(x: usize) -> usize { x } + "##}, ), ]; diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index f65352c7e..350f34aab 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -94,7 +94,10 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { ) .await?; - helpers::assert_file_has_content(file.as_file_mut(), &platform_line(&RANGE.end().to_string()))?; + helpers::assert_file_has_content( + &mut file, + &LineFeedHandling::Native.apply(&RANGE.end().to_string()), + )?; Ok(()) } @@ -114,14 +117,12 @@ async fn test_write() -> anyhow::Result<()> { ) .await?; - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; - + reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; assert_eq!( - helpers::platform_line("the gostak distims the doshes"), + LineFeedHandling::Native.apply("the gostak distims the doshes"), file_content ); @@ -138,24 +139,18 @@ async fn test_overwrite_protection() -> anyhow::Result<()> { helpers::run_event_loop_until_idle(&mut app).await; file.as_file_mut() - .write_all(helpers::platform_line("extremely important content").as_bytes())?; + .write_all("extremely important content".as_bytes())?; file.as_file_mut().flush()?; file.as_file_mut().sync_all()?; test_key_sequence(&mut app, Some(":x"), None, false).await?; - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; - - file.rewind()?; + reload_file(&mut file).unwrap(); let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; + file.read_to_string(&mut file_content)?; - assert_eq!( - helpers::platform_line("extremely important content"), - file_content - ); + assert_eq!("extremely important content", file_content); Ok(()) } @@ -175,14 +170,13 @@ async fn test_write_quit() -> anyhow::Result<()> { ) .await?; - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; + reload_file(&mut file).unwrap(); let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; + file.read_to_string(&mut file_content)?; assert_eq!( - helpers::platform_line("the gostak distims the doshes"), + LineFeedHandling::Native.apply("the gostak distims the doshes"), file_content ); @@ -205,12 +199,13 @@ async fn test_write_concurrent() -> anyhow::Result<()> { test_key_sequence(&mut app, Some(&command), None, false).await?; - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; - + reload_file(&mut file).unwrap(); let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!(platform_line(&RANGE.end().to_string()), file_content); + file.read_to_string(&mut file_content)?; + assert_eq!( + LineFeedHandling::Native.apply(&RANGE.end().to_string()), + file_content + ); Ok(()) } @@ -276,7 +271,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> { ) .await?; - helpers::assert_file_has_content(file.as_file_mut(), &helpers::platform_line("hello"))?; + helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("hello"))?; Ok(()) } @@ -321,7 +316,7 @@ async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> { test_key_sequences(&mut app, vec![(Some(":w"), None)], false).await?; // file still saves - helpers::assert_file_has_content(file.as_file_mut(), "let foo = 0;\n")?; + helpers::assert_file_has_content(&mut file, "let foo = 0;\n")?; Ok(()) } @@ -360,13 +355,13 @@ async fn test_write_new_path() -> anyhow::Result<()> { .await?; helpers::assert_file_has_content( - file1.as_file_mut(), - &helpers::platform_line("i can eat glass, it will not hurt me\n"), + &mut file1, + &LineFeedHandling::Native.apply("i can eat glass, it will not hurt me\n"), )?; helpers::assert_file_has_content( - file2.as_file_mut(), - &helpers::platform_line("i can eat glass, it will not hurt me\n"), + &mut file2, + &LineFeedHandling::Native.apply("i can eat glass, it will not hurt me\n"), )?; Ok(()) @@ -436,8 +431,8 @@ async fn test_write_insert_final_newline_added_if_missing() -> anyhow::Result<() test_key_sequence(&mut app, Some(":w"), None, false).await?; helpers::assert_file_has_content( - file.as_file_mut(), - &helpers::platform_line("have you tried chamomile tea?\n"), + &mut file, + &LineFeedHandling::Native.apply("have you tried chamomile tea?\n"), )?; Ok(()) @@ -448,14 +443,14 @@ async fn test_write_insert_final_newline_unchanged_if_not_missing() -> anyhow::R let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) - .with_input_text(&helpers::platform_line("#[t|]#en minutes, please\n")) + .with_input_text(LineFeedHandling::Native.apply("#[t|]#en minutes, please\n")) .build()?; test_key_sequence(&mut app, Some(":w"), None, false).await?; helpers::assert_file_has_content( - file.as_file_mut(), - &helpers::platform_line("ten minutes, please\n"), + &mut file, + &LineFeedHandling::Native.apply("ten minutes, please\n"), )?; Ok(()) @@ -478,10 +473,8 @@ async fn test_write_insert_final_newline_unchanged_if_missing_and_false() -> any test_key_sequence(&mut app, Some(":w"), None, false).await?; - helpers::assert_file_has_content( - file.as_file_mut(), - "the quiet rain continued through the night", - )?; + reload_file(&mut file).unwrap(); + helpers::assert_file_has_content(&mut file, "the quiet rain continued through the night")?; Ok(()) } @@ -507,13 +500,13 @@ async fn test_write_all_insert_final_newline_add_if_missing_and_modified() -> an .await?; helpers::assert_file_has_content( - file1.as_file_mut(), - &helpers::platform_line("we don't serve time travelers here\n"), + &mut file1, + &LineFeedHandling::Native.apply("we don't serve time travelers here\n"), )?; helpers::assert_file_has_content( - file2.as_file_mut(), - &helpers::platform_line("a time traveler walks into a bar\n"), + &mut file2, + &LineFeedHandling::Native.apply("a time traveler walks into a bar\n"), )?; Ok(()) @@ -531,7 +524,82 @@ async fn test_write_all_insert_final_newline_do_not_add_if_unmodified() -> anyho test_key_sequence(&mut app, Some(":wa"), None, false).await?; - helpers::assert_file_has_content(file.as_file_mut(), "i lost on Jeopardy!")?; + helpers::assert_file_has_content(&mut file, "i lost on Jeopardy!")?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_symlink_write() -> anyhow::Result<()> { + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(not(unix))] + use std::os::windows::fs::symlink_file as symlink; + + let dir = tempfile::tempdir()?; + + let mut file = tempfile::NamedTempFile::new_in(&dir)?; + let symlink_path = dir.path().join("linked"); + symlink(file.path(), &symlink_path)?; + + let mut app = helpers::AppBuilder::new() + .with_file(&symlink_path, None) + .build()?; + + test_key_sequence( + &mut app, + Some("ithe gostak distims the doshes:w"), + None, + false, + ) + .await?; + + reload_file(&mut file).unwrap(); + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + + assert_eq!( + LineFeedHandling::Native.apply("the gostak distims the doshes"), + file_content + ); + assert!(symlink_path.is_symlink()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_symlink_write_fail() -> anyhow::Result<()> { + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(not(unix))] + use std::os::windows::fs::symlink_file as symlink; + + let dir = tempfile::tempdir()?; + + let file = helpers::new_readonly_tempfile_in_dir(&dir)?; + let symlink_path = dir.path().join("linked"); + symlink(file.path(), &symlink_path)?; + + let mut app = helpers::AppBuilder::new() + .with_file(&symlink_path, None) + .build()?; + + test_key_sequence( + &mut app, + Some("ihello:wq"), + Some(&|app| { + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + assert_eq!(Some(&path::normalize(&symlink_path)), doc.path()); + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + }), + false, + ) + .await?; + + assert!(symlink_path.is_symlink()); Ok(()) } @@ -557,7 +625,7 @@ async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> { ) .await?; - file.rewind()?; + reload_file(&mut file).unwrap(); let mut new_file_content: Vec = Vec::new(); file.read_to_end(&mut new_file_content)?; diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index a978f386e..70b3f4022 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -1,5 +1,4 @@ use std::{ - fs::File, io::{Read, Write}, mem::replace, path::PathBuf, @@ -14,6 +13,46 @@ use helix_view::{current_ref, doc, editor::LspConfig, input::parse_macro, Editor use tempfile::NamedTempFile; use tokio_stream::wrappers::UnboundedReceiverStream; +/// Specify how to set up the input text with line feeds +#[derive(Clone, Debug)] +pub enum LineFeedHandling { + /// Replaces all LF chars with the system's appropriate line feed character, + /// and if one doesn't exist already, appends the system's appropriate line + /// ending to the end of a string. + Native, + + /// Do not modify the input text in any way. What you give is what you test. + AsIs, +} + +impl LineFeedHandling { + /// Apply the line feed handling to the input string, yielding a set of + /// resulting texts with the appropriate line feed substitutions. + pub fn apply(&self, text: &str) -> String { + let line_end = match self { + LineFeedHandling::Native => helix_core::NATIVE_LINE_ENDING, + LineFeedHandling::AsIs => return text.into(), + } + .as_str(); + + // we can assume that the source files in this code base will always + // be LF, so indoc strings will always insert LF + let mut output = text.replace('\n', line_end); + + if !output.ends_with(line_end) { + output.push_str(line_end); + } + + output + } +} + +impl Default for LineFeedHandling { + fn default() -> Self { + Self::Native + } +} + #[derive(Clone, Debug)] pub struct TestCase { pub in_text: String, @@ -21,6 +60,8 @@ pub struct TestCase { pub in_keys: String, pub out_text: String, pub out_selection: Selection, + + pub line_feed_handling: LineFeedHandling, } impl From<(S, R, V)> for TestCase @@ -30,8 +71,19 @@ where V: Into, { fn from((input, keys, output): (S, R, V)) -> Self { - let (in_text, in_selection) = test::print(&input.into()); - let (out_text, out_selection) = test::print(&output.into()); + TestCase::from((input, keys, output, LineFeedHandling::default())) + } +} + +impl From<(S, R, V, LineFeedHandling)> for TestCase +where + S: Into, + R: Into, + V: Into, +{ + fn from((input, keys, output, line_feed_handling): (S, R, V, LineFeedHandling)) -> Self { + let (in_text, in_selection) = test::print(&line_feed_handling.apply(&input.into())); + let (out_text, out_selection) = test::print(&line_feed_handling.apply(&output.into())); TestCase { in_text, @@ -39,6 +91,7 @@ where in_keys: keys.into(), out_text, out_selection, + line_feed_handling, } } } @@ -137,6 +190,7 @@ pub async fn test_key_sequence_with_input_text>( should_exit: bool, ) -> anyhow::Result<()> { let test_case = test_case.into(); + let mut app = match app { Some(app) => app, None => Application::new(Args::default(), test_config(), test_syntax_loader(None))?, @@ -240,23 +294,6 @@ pub fn test_editor_config() -> helix_view::editor::Config { } } -/// Replaces all LF chars with the system's appropriate line feed -/// character, and if one doesn't exist already, appends the system's -/// appropriate line ending to the end of a string. -pub fn platform_line(input: &str) -> String { - let line_end = helix_core::NATIVE_LINE_ENDING.as_str(); - - // we can assume that the source files in this code base will always - // be LF, so indoc strings will always insert LF - let mut output = input.replace('\n', line_end); - - if !output.ends_with(line_end) { - output.push_str(line_end); - } - - output -} - /// Creates a new temporary file that is set to read only. Useful for /// testing write failures. pub fn new_readonly_tempfile() -> anyhow::Result { @@ -268,6 +305,18 @@ pub fn new_readonly_tempfile() -> anyhow::Result { Ok(file) } +/// Creates a new temporary file in the directory that is set to read only. Useful for +/// testing write failures. +pub fn new_readonly_tempfile_in_dir( + dir: impl AsRef, +) -> anyhow::Result { + let mut file = tempfile::NamedTempFile::new_in(dir)?; + let metadata = file.as_file().metadata()?; + let mut perms = metadata.permissions(); + perms.set_readonly(true); + file.as_file_mut().set_permissions(perms)?; + Ok(file) +} pub struct AppBuilder { args: Args, config: Config, @@ -352,9 +401,8 @@ pub async fn run_event_loop_until_idle(app: &mut Application) { app.event_loop_until_idle(&mut rx_stream).await; } -pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> { - file.flush()?; - file.sync_all()?; +pub fn assert_file_has_content(file: &mut NamedTempFile, content: &str) -> anyhow::Result<()> { + reload_file(file)?; let mut file_content = String::new(); file.read_to_string(&mut file_content)?; @@ -368,3 +416,13 @@ pub fn assert_status_not_error(editor: &Editor) { assert_ne!(&Severity::Error, sev); } } + +pub fn reload_file(file: &mut NamedTempFile) -> anyhow::Result<()> { + let path = file.path(); + let f = std::fs::OpenOptions::new() + .write(true) + .read(true) + .open(&path)?; + *file.as_file_mut() = f; + Ok(()) +} diff --git a/helix-term/tests/test/languages/go.rs b/helix-term/tests/test/languages/go.rs index 7bb3651e6..57ef92f82 100644 --- a/helix-term/tests/test/languages/go.rs +++ b/helix-term/tests/test/languages/go.rs @@ -6,30 +6,30 @@ async fn auto_indent() -> anyhow::Result<()> { let enter_tests = [ ( - helpers::platform_line(indoc! {r##" + indoc! {r##" type Test struct {#[}|]# - "##}), + "##}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ type Test struct { \t#[|\n]# } - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ func main() { \tswitch nil {#[}|]# } - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ func main() { \tswitch nil { \t\t#[|\n]# \t} } - "}), + "}, ), ]; diff --git a/helix-term/tests/test/languages/yaml.rs b/helix-term/tests/test/languages/yaml.rs index 7669e8a27..10e1861d4 100644 --- a/helix-term/tests/test/languages/yaml.rs +++ b/helix-term/tests/test/languages/yaml.rs @@ -6,7 +6,7 @@ async fn auto_indent() -> anyhow::Result<()> { let below_tests = [ ( - helpers::platform_line(indoc! {r##" + indoc! {r##" #[t|]#op: baz: foo bazi: @@ -17,9 +17,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: #[\n|]# baz: foo @@ -31,10 +31,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: b#[a|]#z: foo bazi: @@ -45,9 +45,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo #[\n|]# @@ -59,10 +59,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: baz: foo bazi#[:|]# @@ -73,9 +73,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -87,10 +87,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: baz: foo bazi: @@ -101,9 +101,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -115,10 +115,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: baz: foo bazi: @@ -129,9 +129,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -143,10 +143,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -157,9 +157,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -171,10 +171,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -185,9 +185,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -199,10 +199,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -213,9 +213,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook:#[\n|]# - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -227,10 +227,10 @@ async fn auto_indent() -> anyhow::Result<()> { bax: foox fook: #[\n|]# - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: | @@ -239,9 +239,9 @@ async fn auto_indent() -> anyhow::Result<()> { line string#[\n|]# fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: | @@ -251,10 +251,10 @@ async fn auto_indent() -> anyhow::Result<()> { string #[\n|]# fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: > @@ -263,9 +263,9 @@ async fn auto_indent() -> anyhow::Result<()> { line#[\n|]# string fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: > @@ -275,74 +275,74 @@ async fn auto_indent() -> anyhow::Result<()> { #[\n|]# string fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: >#[\n|]# fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: > #[\n|]# fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ - top:#[\n|]# baz: foo bax: foox fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: #[\n|]# baz: foo bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: baz: foo#[\n|]# bax: foox fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: baz: foo #[\n|]# bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: baz: foo bax: foox#[\n|]# fook: - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: baz: foo bax: foox #[\n|]# fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: - one: two#[\n|]# @@ -350,9 +350,9 @@ async fn auto_indent() -> anyhow::Result<()> { - top: baz: foo bax: foox - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: - one: two @@ -361,42 +361,42 @@ async fn auto_indent() -> anyhow::Result<()> { - top: baz: foo bax: foox - "}), + "}, ), // yaml map without a key ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top:#[\n|]# - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: #[\n|]# - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top#[:|]# bottom: withvalue - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: #[\n|]# bottom: withvalue - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ bottom: withvalue top#[:|]# - "}), + "}, "o", - helpers::platform_line(indoc! {"\ + indoc! {"\ bottom: withvalue top: #[\n|]# - "}), + "}, ), ]; @@ -406,7 +406,7 @@ async fn auto_indent() -> anyhow::Result<()> { let above_tests = [ ( - helpers::platform_line(indoc! {r##" + indoc! {r##" #[t|]#op: baz: foo bazi: @@ -417,9 +417,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ #[\n|]# top: baz: foo @@ -431,10 +431,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: b#[a|]#z: foo bazi: @@ -445,9 +445,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: #[\n|]# baz: foo @@ -459,10 +459,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: baz: foo bazi#[:|]# @@ -473,9 +473,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo #[\n|]# @@ -487,10 +487,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: baz: foo bazi: @@ -501,9 +501,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -515,10 +515,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" top: baz: foo bazi: @@ -529,9 +529,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "##}), + "##}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -543,10 +543,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -557,9 +557,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -571,10 +571,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -585,9 +585,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -599,10 +599,10 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -613,9 +613,9 @@ async fn auto_indent() -> anyhow::Result<()> { - 2 bax: foox fook:#[\n|]# - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bazi: @@ -627,10 +627,10 @@ async fn auto_indent() -> anyhow::Result<()> { bax: foox #[\n|]# fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: | @@ -639,9 +639,9 @@ async fn auto_indent() -> anyhow::Result<()> { line string#[\n|]# fook: - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: | @@ -651,10 +651,10 @@ async fn auto_indent() -> anyhow::Result<()> { #[\n|]# string fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: > @@ -663,9 +663,9 @@ async fn auto_indent() -> anyhow::Result<()> { line string fook: - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: > @@ -675,58 +675,58 @@ async fn auto_indent() -> anyhow::Result<()> { line string fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: > fook:#[\n|]# - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: foo bax: > #[\n|]# fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: baz: foo#[\n|]# bax: foox fook: - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: #[\n|]# baz: foo bax: foox fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: baz: foo bax: foox fook:#[\n|]# - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ - top: baz: foo bax: foox #[\n|]# fook: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: - one: two#[\n|]# @@ -734,9 +734,9 @@ async fn auto_indent() -> anyhow::Result<()> { - top: baz: foo bax: foox - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: baz: #[\n|]# @@ -745,42 +745,42 @@ async fn auto_indent() -> anyhow::Result<()> { - top: baz: foo bax: foox - "}), + "}, ), // yaml map without a key ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top:#[\n|]# - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ #[\n|]# top: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ bottom: withvalue top#[:|]# - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ bottom: withvalue #[\n|]# top: - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ top: bottom:#[ |]#withvalue - "}), + "}, "O", - helpers::platform_line(indoc! {"\ + indoc! {"\ top: #[\n|]# bottom: withvalue - "}), + "}, ), ]; @@ -790,24 +790,24 @@ async fn auto_indent() -> anyhow::Result<()> { let enter_tests = [ ( - helpers::platform_line(indoc! {r##" + indoc! {r##" foo: #[b|]#ar - "##}), + "##}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ foo: #[|b]#ar - "}), + "}, ), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ foo:#[\n|]# - "}), + "}, "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ foo: #[|\n]# - "}), + "}, ), ]; diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 4ebaae854..77098a336 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -8,6 +8,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { in_keys: "i".into(), out_text: String::new(), out_selection: Selection::single(0, 0), + line_feed_handling: LineFeedHandling::AsIs, }) .await?; @@ -106,6 +107,14 @@ async fn surround_by_character() -> anyhow::Result<()> { )) .await?; + // Selection direction is preserved + test(( + "(so [many {go#[|od]#} text] here)", + "mi{", + "(so [many {#[|good]#} text] here)", + )) + .await?; + Ok(()) } @@ -365,6 +374,41 @@ async fn surround_around_pair() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn match_around_closest_ts() -> anyhow::Result<()> { + test_with_config( + AppBuilder::new().with_file("foo.rs", None), + ( + r#"fn main() {todo!{"f#[|oo]#)"};}"#, + "mam", + r#"fn main() {todo!{#[|"foo)"]#};}"#, + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.rs", None), + ( + r##"fn main() { let _ = ("#[|1]#23", "#(|1)#23"); } "##, + "3mam", + r##"fn main() #[|{ let _ = ("123", "123"); }]# "##, + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.rs", None), + ( + r##" fn main() { let _ = ("12#[|3", "12]#3"); } "##, + "1mam", + r##" fn main() { let _ = #[|("123", "123")]#; } "##, + ), + ) + .await?; + + Ok(()) +} + /// Ensure the very initial cursor in an opened file is the width of /// the first grapheme #[tokio::test(flavor = "multi_thread")] @@ -392,20 +436,10 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn cursor_position_append_eof() -> anyhow::Result<()> { // Selection is forwards - test(( - "#[foo|]#", - "abar", - helpers::platform_line("#[foobar|]#\n"), - )) - .await?; + test(("#[foo|]#", "abar", "#[foobar|]#\n")).await?; // Selection is backwards - test(( - "#[|foo]#", - "abar", - helpers::platform_line("#[foobar|]#\n"), - )) - .await?; + test(("#[|foo]#", "abar", "#[foobar|]#\n")).await?; Ok(()) } @@ -415,19 +449,19 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow:: test_with_config( AppBuilder::new().with_file("foo.rs", None), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ #[/|]#// Increments fn inc(x: usize) -> usize { x + 1 } /// Decrements fn dec(x: usize) -> usize { x - 1 } - "}), + "}, "]fv]f", - helpers::platform_line(indoc! {"\ + indoc! {"\ /// Increments #[fn inc(x: usize) -> usize { x + 1 } /// Decrements fn dec(x: usize) -> usize { x - 1 }|]# - "}), + "}, ), ) .await?; @@ -440,19 +474,19 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res test_with_config( AppBuilder::new().with_file("foo.rs", None), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ /// Increments #[fn inc(x: usize) -> usize { x + 1 } /// Decrements fn dec(x: usize) -> usize { x - 1 }|]# - "}), + "}, "v[f", - helpers::platform_line(indoc! {"\ + indoc! {"\ /// Increments #[fn inc(x: usize) -> usize { x + 1 }|]# /// Decrements fn dec(x: usize) -> usize { x - 1 } - "}), + "}, ), ) .await?; @@ -466,23 +500,23 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any test_with_config( AppBuilder::new().with_file("foo.rs", None), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ /// Increments fn inc(x: usize) -> usize { x + 1 } /// Decrements fn dec(x: usize) -> usize { x - 1 } /// Identity #[fn ident(x: usize) -> usize { x }|]# - "}), + "}, "v[f", - helpers::platform_line(indoc! {"\ + indoc! {"\ /// Increments fn inc(x: usize) -> usize { x + 1 } /// Decrements #[|fn dec(x: usize) -> usize { x - 1 } /// Identity ]#fn ident(x: usize) -> usize { x } - "}), + "}, ), ) .await?; @@ -490,23 +524,23 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any test_with_config( AppBuilder::new().with_file("foo.rs", None), ( - helpers::platform_line(indoc! {"\ + indoc! {"\ /// Increments fn inc(x: usize) -> usize { x + 1 } /// Decrements fn dec(x: usize) -> usize { x - 1 } /// Identity #[fn ident(x: usize) -> usize { x }|]# - "}), + "}, "v[f[f", - helpers::platform_line(indoc! {"\ + indoc! {"\ /// Increments #[|fn inc(x: usize) -> usize { x + 1 } /// Decrements fn dec(x: usize) -> usize { x - 1 } /// Identity ]#fn ident(x: usize) -> usize { x } - "}), + "}, ), ) .await?; @@ -517,36 +551,36 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any #[tokio::test(flavor = "multi_thread")] async fn find_char_line_ending() -> anyhow::Result<()> { test(( - helpers::platform_line(indoc! { + indoc! { "\ one #[|t]#wo three" - }), + }, "Tgll2f", - helpers::platform_line(indoc! { + indoc! { "\ one two#[ |]#three" - }), + }, )) .await?; test(( - helpers::platform_line(indoc! { + indoc! { "\ #[|o]#ne two three" - }), + }, "f2tghTF", - helpers::platform_line(indoc! { + indoc! { "\ one#[| t]#wo three" - }), + }, )) .await?; @@ -556,41 +590,41 @@ async fn find_char_line_ending() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_surround_replace() -> anyhow::Result<()> { test(( - platform_line(indoc! {"\ + indoc! {"\ (#[|a]#) - "}), + "}, "mrm{", - platform_line(indoc! {"\ + indoc! {"\ {#[|a]#} - "}), + "}, )) .await?; test(( - platform_line(indoc! {"\ + indoc! {"\ (#[a|]#) - "}), + "}, "mrm{", - platform_line(indoc! {"\ + indoc! {"\ {#[a|]#} - "}), + "}, )) .await?; test(( - platform_line(indoc! {"\ + indoc! {"\ {{ #(}|)# #[}|]# - "}), + "}, "mrm)", - platform_line(indoc! {"\ + indoc! {"\ (( #()|)# #[)|]# - "}), + "}, )) .await?; @@ -600,38 +634,95 @@ async fn test_surround_replace() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_surround_delete() -> anyhow::Result<()> { test(( - platform_line(indoc! {"\ + indoc! {"\ (#[|a]#) - "}), + "}, "mdm", - platform_line(indoc! {"\ + indoc! {"\ #[|a]# - "}), + "}, )) .await?; test(( - platform_line(indoc! {"\ + indoc! {"\ (#[a|]#) - "}), + "}, "mdm", - platform_line(indoc! {"\ + indoc! {"\ #[a|]# - "}), + "}, )) .await?; test(( - platform_line(indoc! {"\ + indoc! {"\ {{ #(}|)# #[}|]# - "}), + "}, "mdm", - platform_line("\n\n#(\n|)##[\n|]#"), + "\n\n#(\n|)##[\n|]#", )) .await?; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn tree_sitter_motions_work_across_injections() -> anyhow::Result<()> { + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + // When the full injected layer is selected, expand_selection jumps to + // a more shallow layer. + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "#[|]#", + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.html", None), + ( + "", + "", + "", + ), + ) + .await?; + + Ok(()) +} diff --git a/helix-term/tests/test/splits.rs b/helix-term/tests/test/splits.rs index 3b66c0486..3ba5a504b 100644 --- a/helix-term/tests/test/splits.rs +++ b/helix-term/tests/test/splits.rs @@ -62,9 +62,9 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> { ) .await?; - helpers::assert_file_has_content(file1.as_file_mut(), &platform_line("hello1"))?; - helpers::assert_file_has_content(file2.as_file_mut(), &platform_line("hello2"))?; - helpers::assert_file_has_content(file3.as_file_mut(), &platform_line("hello3"))?; + helpers::assert_file_has_content(&mut file1, &LineFeedHandling::Native.apply("hello1"))?; + helpers::assert_file_has_content(&mut file2, &LineFeedHandling::Native.apply("hello2"))?; + helpers::assert_file_has_content(&mut file3, &LineFeedHandling::Native.apply("hello3"))?; Ok(()) } @@ -91,7 +91,7 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> { let doc = docs.pop().unwrap(); assert_eq!( - helpers::platform_line("hello\ngoodbye"), + LineFeedHandling::Native.apply("hello\ngoodbye"), doc.text().to_string() ); @@ -110,7 +110,7 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> { let doc = docs.pop().unwrap(); assert_eq!( - helpers::platform_line("hello\ngoodbye"), + LineFeedHandling::Native.apply("hello\ngoodbye"), doc.text().to_string() ); @@ -122,10 +122,7 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> { ) .await?; - helpers::assert_file_has_content( - file.as_file_mut(), - &helpers::platform_line("hello\ngoodbye"), - )?; + helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("hello\ngoodbye"))?; Ok(()) } @@ -151,7 +148,13 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> { // // This panicked in the past because the jumplist entry on line 2 of window 2 // was not updated and after the `kd` step, pointed outside of the document. - test(("#[|]#", "v[wkdqd", "#[|]#")).await?; + test(( + "#[|]#", + "v[wkdqd", + "#[|]#", + LineFeedHandling::AsIs, + )) + .await?; // Transactions are applied to the views for windows lazily when they are focused. // This case panics if the transactions and inversions are not applied in the @@ -160,6 +163,7 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> { "#[|]#", "[[[vuuuwUUUquuu", "#[|]#", + LineFeedHandling::AsIs, )) .await?; @@ -185,6 +189,7 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> { "#[|]#", "3[vwuu3[q%d", "#[|]#", + LineFeedHandling::AsIs, )) .await?; diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 37721e896..c5c95bff0 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -91,6 +91,10 @@ where W: Write, { pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend { + // helix is not usable without colors, but crossterm will disable + // them by default if NO_COLOR is set in the environment. Override + // this behaviour. + crossterm::style::force_color_output(true); CrosstermBackend { buffer, capabilities: Capabilities::from_env_or_default(config), diff --git a/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs index c30aa6e03..67c4db443 100644 --- a/helix-tui/src/widgets/reflow.rs +++ b/helix-tui/src/widgets/reflow.rs @@ -4,6 +4,7 @@ use helix_core::unicode::width::UnicodeWidthStr; use unicode_segmentation::UnicodeSegmentation; const NBSP: &str = "\u{00a0}"; +const NNBSP: &str = "\u{202f}"; /// A state machine to pack styled symbols into lines. /// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming @@ -58,7 +59,8 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> { let mut symbols_exhausted = true; for StyledGrapheme { symbol, style } in &mut self.symbols { symbols_exhausted = false; - let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP; + let symbol_whitespace = + symbol.chars().all(&char::is_whitespace) && symbol != NBSP && symbol != NNBSP; // Ignore characters wider that the total max width. if symbol.width() as u16 > self.max_line_width @@ -496,6 +498,20 @@ mod test { assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]); } + #[test] + fn line_composer_word_wrapper_nnbsp() { + let width = 20; + let text = "AAAAAAAAAAAAAAA AAAA\u{202f}AAA"; + let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width); + assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{202f}AAA",]); + + // Ensure that if the character was a regular space, it would be wrapped differently. + let text_space = text.replace('\u{202f}', " "); + let (word_wrapper_space, _) = + run_composer(Composer::WordWrapper { trim: true }, &text_space, width); + assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]); + } + #[test] fn line_composer_word_wrapper_preserve_indentation() { let width = 20; diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 49b3661e2..a9529ceab 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -17,9 +17,9 @@ helix-event = { path = "../helix-event" } tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } parking_lot = "0.12" -arc-swap = { version = "1.7.0" } +arc-swap = { version = "1.7.1" } -gix = { version = "0.61.0", features = ["attributes"], default-features = false, optional = true } +gix = { version = "0.62.0", features = ["attributes", "status"], default-features = false, optional = true } imara-diff = "0.1.5" anyhow = "1" diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 995bade06..979f8726e 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -5,113 +5,183 @@ use std::io::Read; use std::path::Path; use std::sync::Arc; +use gix::bstr::ByteSlice; +use gix::diff::Rewrites; +use gix::dir::entry::Status; use gix::objs::tree::EntryKind; use gix::sec::trust::DefaultForLevel; +use gix::status::{ + index_worktree::iter::Item, + plumbing::index_as_worktree::{Change, EntryStatus}, + UntrackedFiles, +}; use gix::{Commit, ObjectId, Repository, ThreadSafeRepository}; -use crate::DiffProvider; +use crate::FileChange; #[cfg(test)] mod test; -pub struct Git; - -impl Git { - fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Result { - // custom open options - let mut git_open_opts_map = gix::sec::trust::Mapping::::default(); - - // On windows various configuration options are bundled as part of the installations - // This path depends on the install location of git and therefore requires some overhead to lookup - // This is basically only used on windows and has some overhead hence it's disabled on other platforms. - // `gitoxide` doesn't use this as default - let config = gix::open::permissions::Config { - system: true, - git: true, - user: true, - env: true, - includes: true, - git_binary: cfg!(windows), - }; - // change options for config permissions without touching anything else - git_open_opts_map.reduced = git_open_opts_map - .reduced - .permissions(gix::open::Permissions { - config, - ..gix::open::Permissions::default_for_level(gix::sec::Trust::Reduced) - }); - git_open_opts_map.full = git_open_opts_map.full.permissions(gix::open::Permissions { - config, - ..gix::open::Permissions::default_for_level(gix::sec::Trust::Full) - }); +pub fn get_diff_base(file: &Path) -> Result> { + debug_assert!(!file.exists() || file.is_file()); + debug_assert!(file.is_absolute()); + + // TODO cache repository lookup + + let repo_dir = file.parent().context("file has no parent directory")?; + let repo = open_repo(repo_dir) + .context("failed to open git repo")? + .to_thread_local(); + let head = repo.head_commit()?; + let file_oid = find_file_in_commit(&repo, &head, file)?; + + let file_object = repo.find_object(file_oid)?; + let data = file_object.detach().data; + // Get the actual data that git would make out of the git object. + // This will apply the user's git config or attributes like crlf conversions. + if let Some(work_dir) = repo.work_dir() { + let rela_path = file.strip_prefix(work_dir)?; + let rela_path = gix::path::try_into_bstr(rela_path)?; + let (mut pipeline, _) = repo.filter_pipeline(None)?; + let mut worktree_outcome = + pipeline.convert_to_worktree(&data, rela_path.as_ref(), Delay::Forbid)?; + let mut buf = Vec::with_capacity(data.len()); + worktree_outcome.read_to_end(&mut buf)?; + Ok(buf) + } else { + Ok(data) + } +} - let open_options = gix::discover::upwards::Options { - ceiling_dirs: ceiling_dir - .map(|dir| vec![dir.to_owned()]) - .unwrap_or_default(), - dot_git_only: true, - ..Default::default() - }; +pub fn get_current_head_name(file: &Path) -> Result>>> { + debug_assert!(!file.exists() || file.is_file()); + debug_assert!(file.is_absolute()); + let repo_dir = file.parent().context("file has no parent directory")?; + let repo = open_repo(repo_dir) + .context("failed to open git repo")? + .to_thread_local(); + let head_ref = repo.head_ref()?; + let head_commit = repo.head_commit()?; + + let name = match head_ref { + Some(reference) => reference.name().shorten().to_string(), + None => head_commit.id.to_hex_with_len(8).to_string(), + }; + + Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) +} - let res = ThreadSafeRepository::discover_with_environment_overrides_opts( - path, - open_options, - git_open_opts_map, - )?; +pub fn for_each_changed_file(cwd: &Path, f: impl Fn(Result) -> bool) -> Result<()> { + status(&open_repo(cwd)?.to_thread_local(), f) +} - Ok(res) - } +fn open_repo(path: &Path) -> Result { + // custom open options + let mut git_open_opts_map = gix::sec::trust::Mapping::::default(); + + // On windows various configuration options are bundled as part of the installations + // This path depends on the install location of git and therefore requires some overhead to lookup + // This is basically only used on windows and has some overhead hence it's disabled on other platforms. + // `gitoxide` doesn't use this as default + let config = gix::open::permissions::Config { + system: true, + git: true, + user: true, + env: true, + includes: true, + git_binary: cfg!(windows), + }; + // change options for config permissions without touching anything else + git_open_opts_map.reduced = git_open_opts_map + .reduced + .permissions(gix::open::Permissions { + config, + ..gix::open::Permissions::default_for_level(gix::sec::Trust::Reduced) + }); + git_open_opts_map.full = git_open_opts_map.full.permissions(gix::open::Permissions { + config, + ..gix::open::Permissions::default_for_level(gix::sec::Trust::Full) + }); + + let open_options = gix::discover::upwards::Options { + dot_git_only: true, + ..Default::default() + }; + + let res = ThreadSafeRepository::discover_with_environment_overrides_opts( + path, + open_options, + git_open_opts_map, + )?; + + Ok(res) } -impl DiffProvider for Git { - fn get_diff_base(&self, file: &Path) -> Result> { - debug_assert!(!file.exists() || file.is_file()); - debug_assert!(file.is_absolute()); - - // TODO cache repository lookup - - let repo_dir = file.parent().context("file has no parent directory")?; - let repo = Git::open_repo(repo_dir, None) - .context("failed to open git repo")? - .to_thread_local(); - let head = repo.head_commit()?; - let file_oid = find_file_in_commit(&repo, &head, file)?; - - let file_object = repo.find_object(file_oid)?; - let data = file_object.detach().data; - // Get the actual data that git would make out of the git object. - // This will apply the user's git config or attributes like crlf conversions. - if let Some(work_dir) = repo.work_dir() { - let rela_path = file.strip_prefix(work_dir)?; - let rela_path = gix::path::try_into_bstr(rela_path)?; - let (mut pipeline, _) = repo.filter_pipeline(None)?; - let mut worktree_outcome = - pipeline.convert_to_worktree(&data, rela_path.as_ref(), Delay::Forbid)?; - let mut buf = Vec::with_capacity(data.len()); - worktree_outcome.read_to_end(&mut buf)?; - Ok(buf) - } else { - Ok(data) +/// Emulates the result of running `git status` from the command line. +fn status(repo: &Repository, f: impl Fn(Result) -> bool) -> Result<()> { + let work_dir = repo + .work_dir() + .ok_or_else(|| anyhow::anyhow!("working tree not found"))? + .to_path_buf(); + + let status_platform = repo + .status(gix::progress::Discard)? + // Here we discard the `status.showUntrackedFiles` config, as it makes little sense in + // our case to not list new (untracked) files. We could have respected this config + // if the default value weren't `Collapsed` though, as this default value would render + // the feature unusable to many. + .untracked_files(UntrackedFiles::Files) + // Turn on file rename detection, which is off by default. + .index_worktree_rewrites(Some(Rewrites { + copies: None, + percentage: Some(0.5), + limit: 1000, + })); + + // No filtering based on path + let empty_patterns = vec![]; + + let status_iter = status_platform.into_index_worktree_iter(empty_patterns)?; + + for item in status_iter { + let Ok(item) = item.map_err(|err| f(Err(err.into()))) else { + continue; + }; + let change = match item { + Item::Modification { + rela_path, status, .. + } => { + let path = work_dir.join(rela_path.to_path()?); + match status { + EntryStatus::Conflict(_) => FileChange::Conflict { path }, + EntryStatus::Change(Change::Removed) => FileChange::Deleted { path }, + EntryStatus::Change(Change::Modification { .. }) => { + FileChange::Modified { path } + } + _ => continue, + } + } + Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => { + FileChange::Untracked { + path: work_dir.join(entry.rela_path.to_path()?), + } + } + Item::Rewrite { + source, + dirwalk_entry, + .. + } => FileChange::Renamed { + from_path: work_dir.join(source.rela_path().to_path()?), + to_path: work_dir.join(dirwalk_entry.rela_path.to_path()?), + }, + _ => continue, + }; + if !f(Ok(change)) { + break; } } - fn get_current_head_name(&self, file: &Path) -> Result>>> { - debug_assert!(!file.exists() || file.is_file()); - debug_assert!(file.is_absolute()); - let repo_dir = file.parent().context("file has no parent directory")?; - let repo = Git::open_repo(repo_dir, None) - .context("failed to open git repo")? - .to_thread_local(); - let head_ref = repo.head_ref()?; - let head_commit = repo.head_commit()?; - - let name = match head_ref { - Some(reference) => reference.name().shorten().to_string(), - None => head_commit.id.to_hex_with_len(8).to_string(), - }; - - Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) - } + Ok(()) } /// Finds the object that contains the contents of a file at a specific commit. diff --git a/helix-vcs/src/git/test.rs b/helix-vcs/src/git/test.rs index 9c67d2c33..95ff10b23 100644 --- a/helix-vcs/src/git/test.rs +++ b/helix-vcs/src/git/test.rs @@ -2,7 +2,7 @@ use std::{fs::File, io::Write, path::Path, process::Command}; use tempfile::TempDir; -use crate::{DiffProvider, Git}; +use crate::git; fn exec_git_cmd(args: &str, git_dir: &Path) { let res = Command::new("git") @@ -54,7 +54,7 @@ fn missing_file() { let file = temp_git.path().join("file.txt"); File::create(&file).unwrap().write_all(b"foo").unwrap(); - assert!(Git.get_diff_base(&file).is_err()); + assert!(git::get_diff_base(&file).is_err()); } #[test] @@ -64,7 +64,7 @@ fn unmodified_file() { let contents = b"foo".as_slice(); File::create(&file).unwrap().write_all(contents).unwrap(); create_commit(temp_git.path(), true); - assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); + assert_eq!(git::get_diff_base(&file).unwrap(), Vec::from(contents)); } #[test] @@ -76,7 +76,7 @@ fn modified_file() { create_commit(temp_git.path(), true); File::create(&file).unwrap().write_all(b"bar").unwrap(); - assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); + assert_eq!(git::get_diff_base(&file).unwrap(), Vec::from(contents)); } /// Test that `get_file_head` does not return content for a directory. @@ -95,7 +95,7 @@ fn directory() { std::fs::remove_dir_all(&dir).unwrap(); File::create(&dir).unwrap().write_all(b"bar").unwrap(); - assert!(Git.get_diff_base(&dir).is_err()); + assert!(git::get_diff_base(&dir).is_err()); } /// Test that `get_file_head` does not return content for a symlink. @@ -116,6 +116,6 @@ fn symlink() { symlink("file.txt", &file_link).unwrap(); create_commit(temp_git.path(), true); - assert!(Git.get_diff_base(&file_link).is_err()); - assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); + assert!(git::get_diff_base(&file_link).is_err()); + assert_eq!(git::get_diff_base(&file).unwrap(), Vec::from(contents)); } diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs index 63487fbcd..539be779a 100644 --- a/helix-vcs/src/lib.rs +++ b/helix-vcs/src/lib.rs @@ -1,11 +1,9 @@ -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use arc_swap::ArcSwap; -use std::{path::Path, sync::Arc}; - -#[cfg(feature = "git")] -pub use git::Git; -#[cfg(not(feature = "git"))] -pub use Dummy as Git; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; #[cfg(feature = "git")] mod git; @@ -14,29 +12,13 @@ mod diff; pub use diff::{DiffHandle, Hunk}; -pub trait DiffProvider { - /// Returns the data that a diff should be computed against - /// if this provider is used. - /// The data is returned as raw byte without any decoding or encoding performed - /// to ensure all file encodings are handled correctly. - fn get_diff_base(&self, file: &Path) -> Result>; - fn get_current_head_name(&self, file: &Path) -> Result>>>; -} +mod status; -#[doc(hidden)] -pub struct Dummy; -impl DiffProvider for Dummy { - fn get_diff_base(&self, _file: &Path) -> Result> { - bail!("helix was compiled without git support") - } - - fn get_current_head_name(&self, _file: &Path) -> Result>>> { - bail!("helix was compiled without git support") - } -} +pub use status::FileChange; +#[derive(Clone)] pub struct DiffProviderRegistry { - providers: Vec>, + providers: Vec, } impl DiffProviderRegistry { @@ -46,8 +28,8 @@ impl DiffProviderRegistry { .find_map(|provider| match provider.get_diff_base(file) { Ok(res) => Some(res), Err(err) => { - log::info!("{err:#?}"); - log::info!("failed to open diff base for {}", file.display()); + log::debug!("{err:#?}"); + log::debug!("failed to open diff base for {}", file.display()); None } }) @@ -59,20 +41,82 @@ impl DiffProviderRegistry { .find_map(|provider| match provider.get_current_head_name(file) { Ok(res) => Some(res), Err(err) => { - log::info!("{err:#?}"); - log::info!("failed to obtain current head name for {}", file.display()); + log::debug!("{err:#?}"); + log::debug!("failed to obtain current head name for {}", file.display()); None } }) } + + /// Fire-and-forget changed file iteration. Runs everything in a background task. Keeps + /// iteration until `on_change` returns `false`. + pub fn for_each_changed_file( + self, + cwd: PathBuf, + f: impl Fn(Result) -> bool + Send + 'static, + ) { + tokio::task::spawn_blocking(move || { + if self + .providers + .iter() + .find_map(|provider| provider.for_each_changed_file(&cwd, &f).ok()) + .is_none() + { + f(Err(anyhow!("no diff provider returns success"))); + } + }); + } } impl Default for DiffProviderRegistry { fn default() -> Self { // currently only git is supported // TODO make this configurable when more providers are added - let git: Box = Box::new(Git); - let providers = vec![git]; + let providers = vec![ + #[cfg(feature = "git")] + DiffProvider::Git, + ]; DiffProviderRegistry { providers } } } + +/// A union type that includes all types that implement [DiffProvider]. We need this type to allow +/// cloning [DiffProviderRegistry] as `Clone` cannot be used in trait objects. +/// +/// `Copy` is simply to ensure the `clone()` call is the simplest it can be. +#[derive(Copy, Clone)] +pub enum DiffProvider { + #[cfg(feature = "git")] + Git, + None, +} + +impl DiffProvider { + fn get_diff_base(&self, file: &Path) -> Result> { + match self { + #[cfg(feature = "git")] + Self::Git => git::get_diff_base(file), + Self::None => bail!("No diff support compiled in"), + } + } + + fn get_current_head_name(&self, file: &Path) -> Result>>> { + match self { + #[cfg(feature = "git")] + Self::Git => git::get_current_head_name(file), + Self::None => bail!("No diff support compiled in"), + } + } + + fn for_each_changed_file( + &self, + cwd: &Path, + f: impl Fn(Result) -> bool, + ) -> Result<()> { + match self { + #[cfg(feature = "git")] + Self::Git => git::for_each_changed_file(cwd, f), + Self::None => bail!("No diff support compiled in"), + } + } +} diff --git a/helix-vcs/src/status.rs b/helix-vcs/src/status.rs new file mode 100644 index 000000000..f34334909 --- /dev/null +++ b/helix-vcs/src/status.rs @@ -0,0 +1,32 @@ +use std::path::{Path, PathBuf}; + +pub enum FileChange { + Untracked { + path: PathBuf, + }, + Modified { + path: PathBuf, + }, + Conflict { + path: PathBuf, + }, + Deleted { + path: PathBuf, + }, + Renamed { + from_path: PathBuf, + to_path: PathBuf, + }, +} + +impl FileChange { + pub fn path(&self) -> &Path { + match self { + Self::Untracked { path } => path, + Self::Modified { path } => path, + Self::Conflict { path } => path, + Self::Deleted { path } => path, + Self::Renamed { to_path, .. } => to_path, + } + } +} diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 6eab0b5e7..be0edc96e 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -27,11 +27,13 @@ bitflags = "2.5" anyhow = "1" crossterm = { version = "0.27", optional = true } +tempfile = "3.9" + # Conversion traits once_cell = "1.19" url = "2.5.0" -arc-swap = { version = "1.7.0" } +arc-swap = { version = "1.7.1" } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio-stream = "0.1" diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index 9ff2fd788..8dd7ebb16 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -156,7 +156,7 @@ pub mod provider { #[cfg(feature = "term")] mod osc52 { - use {super::ClipboardType, crate::base64, crossterm}; + use {super::ClipboardType, crate::base64}; #[derive(Debug)] pub struct SetClipboardCommand { @@ -255,7 +255,7 @@ pub mod provider { #[cfg(not(target_arch = "wasm32"))] pub mod command { use super::*; - use anyhow::{bail, Context as _, Result}; + use anyhow::{bail, Context as _}; #[cfg(not(any(windows, target_os = "macos")))] pub fn is_exit_success(program: &str, args: &[&str]) -> bool { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f813c7424..3393fbed7 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -8,8 +8,9 @@ use helix_core::chars::char_is_word; use helix_core::doc_formatter::TextFormat; use helix_core::encoding::Encoding; use helix_core::syntax::{Highlight, LanguageServerFeature}; -use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; +use helix_core::text_annotations::{InlineAnnotation, Overlay}; use helix_lsp::util::lsp_pos_to_pos; +use helix_stdx::faccess::{copy_metadata, readonly}; use helix_vcs::{DiffHandle, DiffProviderRegistry}; use ::parking_lot::Mutex; @@ -21,7 +22,6 @@ use std::collections::HashMap; use std::fmt::Display; use std::future::Future; use std::path::{Path, PathBuf}; -use std::rc::Rc; use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::SystemTime; @@ -126,6 +126,7 @@ pub struct Document { /// /// To know if they're up-to-date, check the `id` field in `DocumentInlayHints`. pub(crate) inlay_hints: HashMap, + pub(crate) jump_labels: HashMap>, /// Set to `true` when the document is updated, reset to `false` on the next inlay hints /// update from the LSP pub inlay_hints_oudated: bool, @@ -200,22 +201,22 @@ pub struct DocumentInlayHints { pub id: DocumentInlayHintsId, /// Inlay hints of `TYPE` kind, if any. - pub type_inlay_hints: Rc<[InlineAnnotation]>, + pub type_inlay_hints: Vec, /// Inlay hints of `PARAMETER` kind, if any. - pub parameter_inlay_hints: Rc<[InlineAnnotation]>, + pub parameter_inlay_hints: Vec, /// Inlay hints that are neither `TYPE` nor `PARAMETER`. /// /// LSPs are not required to associate a kind to their inlay hints, for example Rust-Analyzer /// currently never does (February 2023) and the LSP spec may add new kinds in the future that /// we want to display even if we don't have some special highlighting for them. - pub other_inlay_hints: Rc<[InlineAnnotation]>, + pub other_inlay_hints: Vec, /// Inlay hint padding. When creating the final `TextAnnotations`, the `before` padding must be /// added first, then the regular inlay hints, then the `after` padding. - pub padding_before_inlay_hints: Rc<[InlineAnnotation]>, - pub padding_after_inlay_hints: Rc<[InlineAnnotation]>, + pub padding_before_inlay_hints: Vec, + pub padding_after_inlay_hints: Vec, } impl DocumentInlayHints { @@ -223,11 +224,11 @@ impl DocumentInlayHints { pub fn empty_with_id(id: DocumentInlayHintsId) -> Self { Self { id, - type_inlay_hints: Rc::new([]), - parameter_inlay_hints: Rc::new([]), - other_inlay_hints: Rc::new([]), - padding_before_inlay_hints: Rc::new([]), - padding_after_inlay_hints: Rc::new([]), + type_inlay_hints: Vec::new(), + parameter_inlay_hints: Vec::new(), + other_inlay_hints: Vec::new(), + padding_before_inlay_hints: Vec::new(), + padding_after_inlay_hints: Vec::new(), } } } @@ -623,7 +624,7 @@ where *mut_ref = f(mem::take(mut_ref)); } -use helix_lsp::{lsp, Client, LanguageServerName}; +use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName}; use url::Url; impl Document { @@ -666,6 +667,7 @@ impl Document { version_control_head: None, focused_at: std::time::Instant::now(), readonly: false, + jump_labels: HashMap::new(), } } @@ -869,7 +871,7 @@ impl Document { // We encode the file according to the `Document`'s encoding. let future = async move { - use tokio::{fs, fs::File}; + use tokio::fs; if let Some(parent) = path.parent() { // TODO: display a prompt asking the user if the directories should be created if !parent.exists() { @@ -891,9 +893,66 @@ impl Document { } } } + let write_path = tokio::fs::read_link(&path) + .await + .unwrap_or_else(|_| path.clone()); + + if readonly(&write_path) { + bail!(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Path is read only" + )); + } + let backup = if path.exists() { + let path_ = write_path.clone(); + // hacks: we use tempfile to handle the complex task of creating + // non clobbered temporary path for us we don't want + // the whole automatically delete path on drop thing + // since the path doesn't exist yet, we just want + // the path + tokio::task::spawn_blocking(move || -> Option { + tempfile::Builder::new() + .prefix(path_.file_name()?) + .suffix(".bck") + .make_in(path_.parent()?, |backup| std::fs::rename(&path_, backup)) + .ok()? + .into_temp_path() + .keep() + .ok() + }) + .await + .ok() + .flatten() + } else { + None + }; - let mut file = File::create(&path).await?; - to_writer(&mut file, encoding_with_bom_info, &text).await?; + let write_result: anyhow::Result<_> = async { + let mut dst = tokio::fs::File::create(&write_path).await?; + to_writer(&mut dst, encoding_with_bom_info, &text).await?; + Ok(()) + } + .await; + + if let Some(backup) = backup { + if write_result.is_err() { + // restore backup + let _ = tokio::fs::rename(&backup, &write_path) + .await + .map_err(|e| log::error!("Failed to restore backup on write failure: {e}")); + } else { + // copy metadata and delete backup + let _ = tokio::task::spawn_blocking(move || { + let _ = copy_metadata(&backup, &write_path) + .map_err(|e| log::error!("Failed to copy metadata on write: {e}")); + let _ = std::fs::remove_file(backup) + .map_err(|e| log::error!("Failed to remove backup file on write: {e}")); + }) + .await; + } + } + + write_result?; let event = DocumentSavedEvent { revision: current_rev, @@ -904,13 +963,14 @@ impl Document { for (_, language_server) in language_servers { if !language_server.is_initialized() { - return Ok(event); + continue; } - if let Some(identifier) = &identifier { - if let Some(notification) = - language_server.text_document_did_save(identifier.clone(), &text) - { - notification.await?; + if let Some(notification) = identifier + .clone() + .and_then(|id| language_server.text_document_did_save(id, &text)) + { + if let Err(err) = notification.await { + log::error!("Failed to send textDocument/didSave: {err}"); } } } @@ -954,35 +1014,12 @@ impl Document { } } - #[cfg(unix)] // Detect if the file is readonly and change the readonly field if necessary (unix only) pub fn detect_readonly(&mut self) { - use rustix::fs::{access, Access}; // Allows setting the flag for files the user cannot modify, like root files self.readonly = match &self.path { None => false, - Some(p) => match access(p, Access::WRITE_OK) { - Ok(_) => false, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, - Err(_) => true, - }, - }; - } - - #[cfg(not(unix))] - // Detect if the file is readonly and change the readonly field if necessary (non-unix os) - pub fn detect_readonly(&mut self) { - // TODO Use the Windows' function `CreateFileW` to check if a file is readonly - // Discussion: https://github.com/helix-editor/helix/pull/7740#issuecomment-1656806459 - // Vim implementation: https://github.com/vim/vim/blob/4c0089d696b8d1d5dc40568f25ea5738fa5bbffb/src/os_win32.c#L7665 - // Windows binding: https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/FileSystem/fn.CreateFileW.html - self.readonly = match &self.path { - None => false, - Some(p) => match std::fs::metadata(p) { - Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, - Err(_) => false, - Ok(metadata) => metadata.permissions().readonly(), - }, + Some(p) => readonly(p), }; } @@ -1139,6 +1176,7 @@ impl Document { pub fn remove_view(&mut self, view_id: ViewId) { self.selections.remove(&view_id); self.inlay_hints.remove(&view_id); + self.jump_labels.remove(&view_id); } /// Apply a [`Transaction`] to the [`Document`] to change its text. @@ -1258,21 +1296,16 @@ impl Document { }); self.diagnostics.sort_unstable_by_key(|diagnostic| { - ( - diagnostic.range, - diagnostic.severity, - diagnostic.language_server_id, - ) + (diagnostic.range, diagnostic.severity, diagnostic.provider) }); // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place - let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| { - if let Some(data) = Rc::get_mut(annotations) { - changes.update_positions( - data.iter_mut() - .map(|annotation| (&mut annotation.char_idx, Assoc::After)), - ); - } + let apply_inlay_hint_changes = |annotations: &mut Vec| { + changes.update_positions( + annotations + .iter_mut() + .map(|annotation| (&mut annotation.char_idx, Assoc::After)), + ); }; self.inlay_hints_oudated = true; @@ -1607,7 +1640,7 @@ impl Document { }) } - pub fn supports_language_server(&self, id: usize) -> bool { + pub fn supports_language_server(&self, id: LanguageServerId) -> bool { self.language_servers().any(|l| l.id() == id) } @@ -1730,7 +1763,7 @@ impl Document { text: &Rope, language_config: Option<&LanguageConfiguration>, diagnostic: &helix_lsp::lsp::Diagnostic, - language_server_id: usize, + language_server_id: LanguageServerId, offset_encoding: helix_lsp::OffsetEncoding, ) -> Option { use helix_core::diagnostic::{Range, Severity::*}; @@ -1807,7 +1840,7 @@ impl Document { tags, source: diagnostic.source.clone(), data: diagnostic.data.clone(), - language_server_id, + provider: language_server_id, }) } @@ -1820,13 +1853,13 @@ impl Document { &mut self, diagnostics: impl IntoIterator, unchanged_sources: &[String], - language_server_id: Option, + language_server_id: Option, ) { if unchanged_sources.is_empty() { self.clear_diagnostics(language_server_id); } else { self.diagnostics.retain(|d| { - if language_server_id.map_or(false, |id| id != d.language_server_id) { + if language_server_id.map_or(false, |id| id != d.provider) { return true; } @@ -1839,18 +1872,14 @@ impl Document { } self.diagnostics.extend(diagnostics); self.diagnostics.sort_unstable_by_key(|diagnostic| { - ( - diagnostic.range, - diagnostic.severity, - diagnostic.language_server_id, - ) + (diagnostic.range, diagnostic.severity, diagnostic.provider) }); } /// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared - pub fn clear_diagnostics(&mut self, language_server_id: Option) { + pub fn clear_diagnostics(&mut self, language_server_id: Option) { if let Some(id) = language_server_id { - self.diagnostics.retain(|d| d.language_server_id != id); + self.diagnostics.retain(|d| d.provider != id); } else { self.diagnostics.clear(); } @@ -1940,17 +1969,19 @@ impl Document { } } - /// Get the text annotations that apply to the whole document, those that do not apply to any - /// specific view. - pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations { - TextAnnotations::default() - } - /// Set the inlay hints for this document and `view_id`. pub fn set_inlay_hints(&mut self, view_id: ViewId, inlay_hints: DocumentInlayHints) { self.inlay_hints.insert(view_id, inlay_hints); } + pub fn set_jump_labels(&mut self, view_id: ViewId, labels: Vec) { + self.jump_labels.insert(view_id, labels); + } + + pub fn remove_jump_labels(&mut self, view_id: ViewId) { + self.jump_labels.remove(&view_id); + } + /// Get the inlay hints for this document and `view_id`. pub fn inlay_hints(&self, view_id: ViewId) -> Option<&DocumentInlayHints> { self.inlay_hints.get(&view_id) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index bad61052e..2e04037a8 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -16,13 +16,13 @@ use helix_vcs::DiffProviderRegistry; use futures_util::stream::select_all::SelectAll; use futures_util::{future, StreamExt}; -use helix_lsp::Call; +use helix_lsp::{Call, LanguageServerId}; use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ borrow::Cow, cell::Cell, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, fs, io::{self, stdin}, num::NonZeroUsize, @@ -212,6 +212,31 @@ impl Default for FilePickerConfig { } } +fn serialize_alphabet(alphabet: &[char], serializer: S) -> Result +where + S: Serializer, +{ + let alphabet: String = alphabet.iter().collect(); + serializer.serialize_str(&alphabet) +} + +fn deserialize_alphabet<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::Error; + + let str = String::deserialize(deserializer)?; + let chars: Vec<_> = str.chars().collect(); + let unique_chars: HashSet<_> = chars.iter().copied().collect(); + if unique_chars.len() != chars.len() { + return Err(::custom( + "jump-label-alphabet must contain unique characters", + )); + } + Ok(chars) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { @@ -305,6 +330,12 @@ pub struct Config { /// Which indent heuristic to use when a new line is inserted #[serde(default)] pub indent_heuristic: IndentationHeuristic, + /// labels characters used in jumpmode + #[serde( + serialize_with = "serialize_alphabet", + deserialize_with = "deserialize_alphabet" + )] + pub jump_label_alphabet: Vec, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] @@ -682,6 +713,7 @@ pub enum WhitespaceRender { default: Option, space: Option, nbsp: Option, + nnbsp: Option, tab: Option, newline: Option, }, @@ -713,6 +745,14 @@ impl WhitespaceRender { } } } + pub fn nnbsp(&self) -> WhitespaceRenderValue { + match *self { + Self::Basic(val) => val, + Self::Specific { default, nnbsp, .. } => { + nnbsp.or(default).unwrap_or(WhitespaceRenderValue::None) + } + } + } pub fn tab(&self) -> WhitespaceRenderValue { match *self { Self::Basic(val) => val, @@ -736,6 +776,7 @@ impl WhitespaceRender { pub struct WhitespaceCharacters { pub space: char, pub nbsp: char, + pub nnbsp: char, pub tab: char, pub tabpad: char, pub newline: char, @@ -746,6 +787,7 @@ impl Default for WhitespaceCharacters { Self { space: '·', // U+00B7 nbsp: '⍽', // U+237D + nnbsp: '␣', // U+2423 tab: '→', // U+2192 newline: '⏎', // U+23CE tabpad: ' ', @@ -870,6 +912,7 @@ impl Default for Config { smart_tab: Some(SmartTabConfig::default()), popup_border: PopupBorderConfig::None, indent_heuristic: IndentationHeuristic::default(), + jump_label_alphabet: ('a'..='z').collect(), } } } @@ -917,7 +960,7 @@ pub struct Editor { pub macro_recording: Option<(char, Vec)>, pub macro_replaying: Vec, pub language_servers: helix_lsp::Registry, - pub diagnostics: BTreeMap>, + pub diagnostics: BTreeMap>, pub diff_providers: DiffProviderRegistry, pub debugger: Option, @@ -977,7 +1020,7 @@ pub type Motion = Box; pub enum EditorEvent { DocumentSaved(DocumentSavedEventResult), ConfigEvent(ConfigEvent), - LanguageServerMessage((usize, Call)), + LanguageServerMessage((LanguageServerId, Call)), DebuggerEvent(dap::Payload), IdleTimer, Redraw, @@ -1217,8 +1260,13 @@ impl Editor { } #[inline] - pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> { - self.language_servers.get_by_id(language_server_id) + pub fn language_server_by_id( + &self, + language_server_id: LanguageServerId, + ) -> Option<&helix_lsp::Client> { + self.language_servers + .get_by_id(language_server_id) + .map(|client| &**client) } /// Refreshes the language server for a given document @@ -1818,7 +1866,7 @@ impl Editor { /// Returns all supported diagnostics for the document pub fn doc_diagnostics<'a>( language_servers: &'a helix_lsp::Registry, - diagnostics: &'a BTreeMap>, + diagnostics: &'a BTreeMap>, document: &Document, ) -> impl Iterator + 'a { Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true) @@ -1828,10 +1876,9 @@ impl Editor { /// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from pub fn doc_diagnostics_with_filter<'a>( language_servers: &'a helix_lsp::Registry, - diagnostics: &'a BTreeMap>, - + diagnostics: &'a BTreeMap>, document: &Document, - filter: impl Fn(&lsp::Diagnostic, usize) -> bool + 'a, + filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a, ) -> impl Iterator + 'a { let text = document.text().clone(); let language_config = document.language.clone(); diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index ebdac9e23..36f719f79 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -71,7 +71,7 @@ pub fn diagnostic<'doc>( d.line == line && doc .language_servers_with_feature(LanguageServerFeature::Diagnostics) - .any(|ls| ls.id() == d.language_server_id) + .any(|ls| ls.id() == d.provider) }); diagnostics_on_line.max_by_key(|d| d.severity).map(|d| { write!(out, "●").ok(); diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 4c9eba0fd..307dbc71d 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -214,33 +214,56 @@ impl Tree { node } - pub fn remove(&mut self, index: ViewId) { - let mut stack = Vec::new(); + /// Get a mutable reference to a [Container] by index. + /// # Panics + /// Panics if `index` is not in self.nodes, or if the node's content is not a [Content::Container]. + fn container_mut(&mut self, index: ViewId) -> &mut Container { + match &mut self.nodes[index] { + Node { + content: Content::Container(container), + .. + } => container, + _ => unreachable!(), + } + } + + fn remove_or_replace(&mut self, child: ViewId, replacement: Option) { + let parent = self.nodes[child].parent; + + self.nodes.remove(child); + + let container = self.container_mut(parent); + let pos = container + .children + .iter() + .position(|&item| item == child) + .unwrap(); + + if let Some(new) = replacement { + container.children[pos] = new; + self.nodes[new].parent = parent; + } else { + container.children.remove(pos); + } + } + pub fn remove(&mut self, index: ViewId) { if self.focus == index { // focus on something else self.focus = self.prev(); } - stack.push(index); + let parent = self.nodes[index].parent; + let parent_is_root = parent == self.root; - while let Some(index) = stack.pop() { - let parent_id = self.nodes[index].parent; - if let Node { - content: Content::Container(container), - .. - } = &mut self.nodes[parent_id] - { - if let Some(pos) = container.children.iter().position(|&child| child == index) { - container.children.remove(pos); - // TODO: if container now only has one child, remove it and place child in parent - if container.children.is_empty() && parent_id != self.root { - // if container now empty, remove it - stack.push(parent_id); - } - } - } - self.nodes.remove(index); + self.remove_or_replace(index, None); + + let parent_container = self.container_mut(parent); + if parent_container.children.len() == 1 && !parent_is_root { + // Lets merge the only child back to its grandparent so that Views + // are equally spaced. + let sibling = parent_container.children.pop().unwrap(); + self.remove_or_replace(parent, Some(sibling)); } self.recalculate() @@ -384,11 +407,13 @@ impl Tree { } Layout::Vertical => { let len = container.children.len(); - - let width = area.width / len as u16; + let len_u16 = len as u16; let inner_gap = 1u16; - // let total_gap = inner_gap * (len as u16 - 1); + let total_gap = inner_gap * len_u16.saturating_sub(2); + + let used_area = area.width.saturating_sub(total_gap); + let width = used_area / len_u16; let mut child_x = area.x; @@ -873,4 +898,72 @@ mod test { assert_eq!(doc_id(&tree, l2), Some(doc_r0)); assert_eq!(doc_id(&tree, r0), Some(doc_l0)); } + + #[test] + fn all_vertical_views_have_same_width() { + let tree_area_width = 180; + let mut tree = Tree::new(Rect { + x: 0, + y: 0, + width: tree_area_width, + height: 80, + }); + let mut view = View::new(DocumentId::default(), GutterConfig::default()); + view.area = Rect::new(0, 0, 180, 80); + tree.insert(view); + + let view = View::new(DocumentId::default(), GutterConfig::default()); + tree.split(view, Layout::Vertical); + + let view = View::new(DocumentId::default(), GutterConfig::default()); + tree.split(view, Layout::Horizontal); + + tree.remove(tree.focus); + + let view = View::new(DocumentId::default(), GutterConfig::default()); + tree.split(view, Layout::Vertical); + + // Make sure that we only have one level in the tree. + assert_eq!(3, tree.views().count()); + assert_eq!( + vec![ + tree_area_width / 3 - 1, // gap here + tree_area_width / 3 - 1, // gap here + tree_area_width / 3 + ], + tree.views() + .map(|(view, _)| view.area.width) + .collect::>() + ); + } + + #[test] + fn vsplit_gap_rounding() { + let (tree_area_width, tree_area_height) = (80, 24); + let mut tree = Tree::new(Rect { + x: 0, + y: 0, + width: tree_area_width, + height: tree_area_height, + }); + let mut view = View::new(DocumentId::default(), GutterConfig::default()); + view.area = Rect::new(0, 0, tree_area_width, tree_area_height); + tree.insert(view); + + for _ in 0..9 { + let view = View::new(DocumentId::default(), GutterConfig::default()); + tree.split(view, Layout::Vertical); + } + + assert_eq!(10, tree.views().count()); + assert_eq!( + std::iter::repeat(7) + .take(9) + .chain(Some(8)) // Rounding in `recalculate`. + .collect::>(), + tree.views() + .map(|(view, _)| view.area.width) + .collect::>() + ); + } } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index e5e2641a8..5b283b98f 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -19,7 +19,6 @@ use helix_core::{ use std::{ collections::{HashMap, VecDeque}, fmt, - rc::Rc, }; const JUMP_LIST_CAPACITY: usize = 30; @@ -80,7 +79,7 @@ impl JumpList { self.jumps.retain(|(other_id, _)| other_id != doc_id); } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl DoubleEndedIterator { self.jumps.iter() } @@ -409,10 +408,19 @@ impl View { } /// Get the text annotations to display in the current view for the given document and theme. - pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations { - // TODO custom annotations for custom views like side by side diffs - - let mut text_annotations = doc.text_annotations(theme); + pub fn text_annotations<'a>( + &self, + doc: &'a Document, + theme: Option<&Theme>, + ) -> TextAnnotations<'a> { + let mut text_annotations = TextAnnotations::default(); + + if let Some(labels) = doc.jump_labels.get(&self.id) { + let style = theme + .and_then(|t| t.find_scope_index("ui.virtual.jump-label")) + .map(Highlight); + text_annotations.add_overlay(labels, style); + } let DocumentInlayHints { id: _, @@ -436,20 +444,15 @@ impl View { .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint")) .map(Highlight); - let mut add_annotations = |annotations: &Rc<[_]>, style| { - if !annotations.is_empty() { - text_annotations.add_inline_annotations(Rc::clone(annotations), style); - } - }; - // Overlapping annotations are ignored apart from the first so the order here is not random: // types -> parameters -> others should hopefully be the "correct" order for most use cases, // with the padding coming before and after as expected. - add_annotations(padding_before_inlay_hints, None); - add_annotations(type_inlay_hints, type_style); - add_annotations(parameter_inlay_hints, parameter_style); - add_annotations(other_inlay_hints, other_style); - add_annotations(padding_after_inlay_hints, None); + text_annotations + .add_inline_annotations(padding_before_inlay_hints, None) + .add_inline_annotations(type_inlay_hints, type_style) + .add_inline_annotations(parameter_inlay_hints, parameter_style) + .add_inline_annotations(other_inlay_hints, other_style) + .add_inline_annotations(padding_after_inlay_hints, None); text_annotations } diff --git a/languages.toml b/languages.toml index cbbfc5a91..ecf1b49fd 100644 --- a/languages.toml +++ b/languages.toml @@ -8,10 +8,12 @@ use-grammars = { except = [ "hare", "wren", "gemini" ] } als = { command = "als" } ada-language-server = { command = "ada_language_server" } ada-gpr-language-server = {command = "ada_language_server", args = ["--language-gpr"]} +angular = {command = "ngserver", args = ["--stdio", "--tsProbeLocations", ".", "--ngProbeLocations", ".",]} awk-language-server = { command = "awk-language-server" } bash-language-server = { command = "bash-language-server", args = ["start"] } bass = { command = "bass", args = ["--lsp"] } bicep-langserver = { command = "bicep-langserver" } +bitbake-language-server = { command = "bitbake-language-server" } bufls = { command = "bufls", args = ["serve"] } cairo-language-server = { command = "cairo-language-server", args = [] } cl-lsp = { command = "cl-lsp", args = [ "stdio" ] } @@ -27,6 +29,7 @@ dhall-lsp-server = { command = "dhall-lsp-server" } docker-langserver = { command = "docker-langserver", args = ["--stdio"] } docker-compose-langserver = { command = "docker-compose-langserver", args = ["--stdio"]} dot-language-server = { command = "dot-language-server", args = ["--stdio"] } +earthlyls = { command = "earthlyls" } elixir-ls = { command = "elixir-ls", config = { elixirLS.dialyzerEnabled = false } } elm-language-server = { command = "elm-language-server" } elvish = { command = "elvish", args = ["-lsp"] } @@ -43,13 +46,14 @@ intelephense = { command = "intelephense", args = ["--stdio"] } jdtls = { command = "jdtls" } jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] } julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] } +koka = { command = "koka", args = ["--language-server", "--lsstdio"] } kotlin-language-server = { command = "kotlin-language-server" } lean = { command = "lean", args = [ "--server" ] } ltex-ls = { command = "ltex-ls" } markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] } markdown-oxide = { command = "markdown-oxide" } marksman = { command = "marksman", args = ["server"] } -metals = { command = "metals", config = { "isHttpEnabled" = true } } +metals = { command = "metals", config = { "isHttpEnabled" = true, metals = { inlayHints = { typeParameters = {enable = true} , hintsInPatternMatch = {enable = true} } } } } mint = { command = "mint", args = ["ls"] } nil = { command = "nil" } nimlangserver = { command = "nimlangserver" } @@ -63,6 +67,7 @@ openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] } pasls = { command = "pasls", args = [] } pbkit = { command = "pb", args = [ "lsp" ] } perlnavigator = { command = "perlnavigator", args= ["--stdio"] } +pest-language-server = { command = "pest-language-server" } prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] } purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] } pylsp = { command = "pylsp" } @@ -98,6 +103,7 @@ yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] } zls = { command = "zls" } blueprint-compiler = { command = "blueprint-compiler", args = ["lsp"] } typst-lsp = { command = "typst-lsp" } +tinymist = { command = "tinymist" } pkgbuild-language-server = { command = "pkgbuild-language-server" } helm_ls = { command = "helm_ls", args = ["serve"] } ember-language-server = { command = "ember-language-server", args = ["--stdio"] } @@ -197,6 +203,7 @@ scope = "source.rust" injection-regex = "rust" file-types = ["rs"] roots = ["Cargo.toml", "Cargo.lock"] +shebangs = ["rust-script", "cargo"] auto-format = true comment-tokens = ["//", "///", "//!"] block-comment-tokens = [ @@ -216,9 +223,9 @@ persistent-diagnostic-sources = ["rustc", "clippy"] '`' = '`' [language.debugger] -name = "lldb-vscode" +name = "lldb-dap" transport = "stdio" -command = "lldb-vscode" +command = "lldb-dap" [[language.debugger.templates]] name = "binary" @@ -246,7 +253,7 @@ args = { attachCommands = [ "platform select remote-gdb-server", "platform conne [[grammar]] name = "rust" -source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" } +source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "473634230435c18033384bebaa6d6a17c2523281" } [[language]] name = "sway" @@ -435,9 +442,9 @@ language-servers = [ "clangd" ] indent = { tab-width = 2, unit = " " } [language.debugger] -name = "lldb-vscode" +name = "lldb-dap" transport = "stdio" -command = "lldb-vscode" +command = "lldb-dap" [[language.debugger.templates]] name = "binary" @@ -472,9 +479,9 @@ language-servers = [ "clangd" ] indent = { tab-width = 2, unit = " " } [language.debugger] -name = "lldb-vscode" +name = "lldb-dap" transport = "stdio" -command = "lldb-vscode" +command = "lldb-dap" [[language.debugger.templates]] name = "binary" @@ -667,7 +674,7 @@ name = "javascript" scope = "source.js" injection-regex = "(js|javascript)" language-id = "javascript" -file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", { glob = "jakefile" }] +file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", { glob = ".node_repl_history" }, { glob = "jakefile" }] shebangs = ["node"] comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } @@ -706,9 +713,9 @@ grammar = "javascript" name = "typescript" scope = "source.ts" injection-regex = "(ts|typescript)" -file-types = ["ts", "mts", "cts"] language-id = "typescript" -shebangs = ["deno", "ts-node"] +file-types = ["ts", "mts", "cts"] +shebangs = ["deno", "bun", "ts-node"] comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } language-servers = [ "typescript-language-server" ] @@ -779,7 +786,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53 name = "python" scope = "source.python" injection-regex = "python" -file-types = ["py", "pyi", "py3", "pyw", "ptl", "rpy", "cpy", "ipy", "pyt", { glob = ".pythonstartup" }, { glob = ".pythonrc" }, { glob = "SConstruct" }, { glob = "SConscript" }] +file-types = ["py", "pyi", "py3", "pyw", "ptl", "rpy", "cpy", "ipy", "pyt", { glob = ".python_history" }, { glob = ".pythonstartup" }, { glob = ".pythonrc" }, { glob = "SConstruct" }, { glob = "SConscript" }] shebangs = ["python"] roots = ["pyproject.toml", "setup.py", "poetry.lock", "pyrightconfig.json"] comment-token = "#" @@ -881,6 +888,10 @@ injection-regex = "(shell|bash|zsh|sh)" file-types = [ "sh", "bash", + "ash", + "dash", + "ksh", + "mksh", "zsh", "zshenv", "zlogin", @@ -892,7 +903,6 @@ file-types = [ "bazelrc", "Renviron", "zsh-theme", - "ksh", "cshrc", "tcshrc", "bashrc_Apple_Terminal", @@ -927,7 +937,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "bash" -source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "275effdfc0edce774acf7d481f9ea195c6c403cd" } +source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "f8fb3274f72a30896075585b32b0c54cad65c086" } [[language]] name = "php" @@ -1262,9 +1272,9 @@ indent = { tab-width = 4, unit = " " } formatter = { command = "zig" , args = ["fmt", "--stdin"] } [language.debugger] -name = "lldb-vscode" +name = "lldb-dap" transport = "stdio" -command = "lldb-vscode" +command = "lldb-dap" [[language.debugger.templates]] name = "binary" @@ -1473,7 +1483,7 @@ source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "568dd name = "markdown" scope = "source.md" injection-regex = "md|markdown" -file-types = ["md", "markdown", "mkd", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }] +file-types = ["md", "markdown", "mkd", "mkdn", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }] roots = [".marksman.toml"] language-servers = [ "marksman", "markdown-oxide" ] indent = { tab-width = 2, unit = " " } @@ -1481,7 +1491,7 @@ block-comment-tokens = { start = "" } [[grammar]] name = "markdown" -source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "aaf76797aa8ecd9a5e78e0ec3681941de6c945ee", subpath = "tree-sitter-markdown" } +source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "62516e8c78380e3b51d5b55727995d2c511436d8", subpath = "tree-sitter-markdown" } [[language]] name = "markdown.inline" @@ -1492,7 +1502,7 @@ grammar = "markdown_inline" [[grammar]] name = "markdown_inline" -source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "aaf76797aa8ecd9a5e78e0ec3681941de6c945ee", subpath = "tree-sitter-markdown-inline" } +source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "62516e8c78380e3b51d5b55727995d2c511436d8", subpath = "tree-sitter-markdown-inline" } [[language]] name = "dart" @@ -1634,7 +1644,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev = [[language]] name = "git-ignore" scope = "source.gitignore" -file-types = [{ glob = ".gitignore" }, { glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = ".prettierignore" }, { glob = ".eslintignore" }, { glob = ".npmignore"}, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }] +file-types = [{ glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }, { glob = ".*ignore" }] injection-regex = "git-ignore" comment-token = "#" grammar = "gitignore" @@ -1780,7 +1790,7 @@ language-servers = [ "solc" ] [[grammar]] name = "solidity" -source = { git = "https://github.com/JoranHonig/tree-sitter-solidity", rev = "9004b86531cb424bd379424cf7266a4585f2af7d" } +source = { git = "https://github.com/JoranHonig/tree-sitter-solidity", rev = "08338dcee32603383fcef08f36321900bb7a354b" } [[language]] name = "gleam" @@ -2038,13 +2048,36 @@ name = "odin" auto-format = true scope = "source.odin" file-types = ["odin"] -roots = ["ols.json"] +roots = ["ols.json", "main.odin"] language-servers = [ "ols" ] comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } indent = { tab-width = 4, unit = "\t" } formatter = { command = "odinfmt", args = [ "-stdin", "true" ] } +[language.debugger] +name = "lldb-dap" +transport = "stdio" +command = "lldb-dap" + +[[language.debugger.templates]] +name = "binary" +request = "launch" +completion = [ { name = "binary", completion = "filename" } ] +args = { console = "internalConsole", program = "{0}" } + +[[language.debugger.templates]] +name = "attach" +request = "attach" +completion = [ "pid" ] +args = { console = "internalConsole", pid = "{0}" } + +[[language.debugger.templates]] +name = "gdbserver attach" +request = "attach" +completion = [ { name = "lldb connect url", default = "connect://localhost:3333" }, { name = "file", completion = "filename" }, "pid" ] +args = { console = "internalConsole", attachCommands = [ "platform select remote-gdb-server", "platform connect {0}", "file {1}", "attach {2}" ] } + [[grammar]] name = "odin" source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e49ffca2952529d33e94ed63b1b75c4f1" } @@ -2053,7 +2086,7 @@ source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e4 name = "meson" scope = "source.meson" injection-regex = "meson" -file-types = [{ glob = "meson.build" }, { glob = "meson_options.txt" }] +file-types = [{ glob = "meson.build" }, { glob = "meson.options" }, { glob = "meson_options.txt" }] comment-token = "#" indent = { tab-width = 2, unit = " " } @@ -2188,7 +2221,7 @@ source = { git = "https://github.com/sogaiu/tree-sitter-clojure", rev = "e57c569 name = "starlark" scope = "source.starlark" injection-regex = "(starlark|bzl|bazel)" -file-types = ["bzl", "bazel", "star", { glob = "BUILD" }, { glob = "BUILD.*" }] +file-types = ["bzl", "bazel", "star", { glob = "BUILD" }, { glob = "BUILD.*" }, { glob = "Tiltfile" }] comment-token = "#" indent = { tab-width = 4, unit = " " } grammar = "python" @@ -3028,6 +3061,7 @@ roots = ["sln", "fsproj"] injection-regex = "fsharp" file-types = ["fs", "fsx", "fsi", "fsscript"] comment-token = "//" +block-comment-tokens = { start = "(*", end = "*)" } indent = { tab-width = 4, unit = " " } auto-format = true language-servers = ["fsharp-ls"] @@ -3063,7 +3097,7 @@ scope = "source.typst" injection-regex = "typst" file-types = ["typst", "typ"] comment-token = "//" -language-servers = ["typst-lsp"] +language-servers = ["tinymist", "typst-lsp"] indent = { tab-width = 2, unit = " " } [language.auto-pairs] @@ -3075,7 +3109,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "typst" -source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "ecf8596336857adfcd5f7cbb3b2aa11a67badc37" } +source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "13863ddcbaa7b68ee6221cea2e3143415e64aea4" } [[language]] name = "nunjucks" @@ -3132,7 +3166,8 @@ source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "1f505 [[language]] name = "todotxt" scope = "text.todotxt" -file-types = [{ glob = "todo.txt" }, { glob = "*.todo.txt" }, "todotxt"] +# glob = "todo.txt" is too common and can conflict regular files, define in user config if necessary +file-types = [{ glob = "*.todo.txt" }, "todotxt"] formatter = { command = "sort" } auto-format = true @@ -3195,7 +3230,7 @@ language-servers = [ "templ" ] [[grammar]] name = "templ" -source = { git = "https://github.com/vrischmann/tree-sitter-templ", rev = "ea56ac0655243490a4929a988f4eaa91dfccc995" } +source = { git = "https://github.com/vrischmann/tree-sitter-templ", rev = "db662414ccd6f7c78b1e834e7abe11c224b04759" } [[language]] name = "dbml" @@ -3209,6 +3244,17 @@ indent = { tab-width = 2, unit = " " } name = "dbml" source = { git = "https://github.com/dynamotn/tree-sitter-dbml", rev = "2e2fa5640268c33c3d3f27f7e676f631a9c68fd9" } +[[language]] +name = "bitbake" +language-servers = [ "bitbake-language-server" ] +scope = "source.bitbake" +file-types = ["bb", "bbappend", "bbclass", {glob = "conf/*.conf" }, {glob = "conf/*/*.{inc,conf}" }, { glob = "recipe-*/*/*.inc" }] +comment-token = "#" + +[[grammar]] +name = "bitbake" +source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-bitbake", rev = "10bacac929ff36a1e8f4056503fe4f8717b21b94" } + [[language]] name = "log" scope = "source.log" @@ -3249,10 +3295,11 @@ injection-regex = "koka" file-types = ["kk"] comment-token = "//" indent = { tab-width = 8, unit = " " } +language-servers = ["koka"] [[grammar]] name = "koka" -source = { git = "https://github.com/mtoohey31/tree-sitter-koka", rev = "2527e152d4b6a79fd50aebd8d0b4b4336c94a034" } +source = { git = "https://github.com/mtoohey31/tree-sitter-koka", rev = "96d070c3700692858035f3524cc0ad944cef2594" } [[language]] name = "tact" @@ -3339,13 +3386,13 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "ld" -source = { git = "https://github.com/mtoohey31/tree-sitter-ld", rev = "81978cde3844bfc199851e39c80a20ec6444d35e" } +source = { git = "https://github.com/mtoohey31/tree-sitter-ld", rev = "0e9695ae0ede47b8744a8e2ad44d4d40c5d4e4c9" } [[language]] name = "hyprlang" scope = "source.hyprlang" roots = ["hyprland.conf"] -file-types = [ { glob = "hyprland.conf"} ] +file-types = [ { glob = "hyprland.conf" }, { glob = "hyprpaper.conf" }, { glob = "hypridle.conf" }, { glob = "hyprlock.conf" } ] comment-token = "#" grammar = "hyprlang" @@ -3353,6 +3400,18 @@ grammar = "hyprlang" name = "hyprlang" source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-hyprlang", rev = "27af9b74acf89fa6bed4fb8cb8631994fcb2e6f3"} +[[language]] +name = "tcl" +scope = "source.tcl" +injection-regex = "tcl" +file-types = [ "tcl" ] +shebangs = [ "tclish", "jimsh", "wish" ] +comment-token = '#' + +[[grammar]] +name = "tcl" +source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-tcl", rev = "56ad1fa6a34ba800e5495d1025a9b0fda338d5b8" } + [[language]] name = "supercollider" scope = "source.supercollider" @@ -3385,7 +3444,7 @@ scope = "source.helm" roots = ["Chart.yaml"] comment-token = "#" language-servers = ["helm_ls"] -file-types = [ { glob = "templates/*.yaml" }, { glob = "templates/_helpers.tpl"}, { glob = "templates/NOTES.txt" } ] +file-types = [ { glob = "templates/*.yaml" }, { glob = "templates/_*.tpl"}, { glob = "templates/NOTES.txt" } ] [[language]] name = "glimmer" @@ -3408,3 +3467,115 @@ formatter = { command = "prettier", args = ['--parser', 'glimmer'] } [[grammar]] name = "glimmer" source = { git = "https://github.com/ember-tooling/tree-sitter-glimmer", rev = "5dc6d1040e8ff8978ff3680e818d85447bbc10aa" } + +[[language]] +name = "ohm" +scope = "source.ohm" +injection-regex = "ohm" +file-types = ["ohm"] +comment-token = "//" +block-comment-tokens = [ + { start = "/*", end = "*/" }, + { start = "/**", end = "*/" }, +] +indent = { tab-width = 2, unit = " " } + +[language.auto-pairs] +'"' = '"' +'{' = '}' +'(' = ')' +'<' = '>' + +[[grammar]] +name = "ohm" +source = { git = "https://github.com/novusnota/tree-sitter-ohm", rev = "80f14f0e477ddacc1e137d5ed8e830329e3fb7a3" } + +[[language]] +name = "earthfile" +scope = "source.earthfile" +injection-regex = "earthfile" +roots = ["Earthfile"] +file-types = [ + { glob = "Earthfile" }, +] +comment-token = "#" +indent = { tab-width = 2, unit = " " } +language-servers = ["earthlyls"] + +[[grammar]] +name = "earthfile" +source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "a079e6c472eeedd6b9a1e03ca0b6c82cd6a112a4" } + +[[language]] +name = "adl" +scope = "source.adl" +injection-regex = "adl" +file-types = ["adl"] +roots = [] +comment-token = "//" +indent = { tab-width = 2, unit = " " } + +[language.auto-pairs] +'"' = '"' +'{' = '}' +'<' = '>' + +[[grammar]] +name = "adl" +source = { git = "https://github.com/adl-lang/tree-sitter-adl", rev = "2787d04beadfbe154d3f2da6e98dc45a1b134bbf" } + +[[language]] +name = "ldif" +scope = "source.ldif" +injection-regex = "ldif" +file-types = ["ldif"] +comment-token = "#" + +[[grammar]] +name = "ldif" +source = { git = "https://github.com/kepet19/tree-sitter-ldif", rev = "0a917207f65ba3e3acfa9cda16142ee39c4c1aaa" } + +[[language]] +name = "xtc" +scope = "source.xtc" +# Accept Xena Traffic Configuration, Xena Port Configuration and Xena OpenAutomation +file-types = [ "xtc", "xpc", "xoa" ] +comment-token = ";" + +[[grammar]] +name = "xtc" +source = { git = "https://github.com/Alexis-Lapierre/tree-sitter-xtc", rev = "7bc11b736250c45e25cfb0215db2f8393779957e" } + +[[language]] +name = "move" +scope = "source.move" +injection-regex = "move" +roots = ["Move.toml"] +file-types = ["move"] +comment-token = "//" +indent = { tab-width = 4, unit = " " } +language-servers = [] + +[[grammar]] +name = "move" +source = { git = "https://github.com/tzakian/tree-sitter-move", rev = "8bc0d1692caa8763fef54d48068238d9bf3c0264" } + +[[language]] +name = "pest" +scope = "source.pest" +injection-regex = "pest" +file-types = ["pest"] +comment-tokens = ["//", "///", "//!"] +block-comment-tokens = { start = "/*", end = "*/" } +indent = { tab-width = 4, unit = " " } +language-servers = ["pest-language-server"] + +[language.auto-pairs] +'(' = ')' +'{' = '}' +'[' = ']' +'"' = '"' + +[[grammar]] +name = "pest" +source = { git = "https://github.com/pest-parser/tree-sitter-pest", rev = "a8a98a824452b1ec4da7f508386a187a2f234b85" } diff --git a/runtime/queries/_typescript/textobjects.scm b/runtime/queries/_typescript/textobjects.scm index c248aeade..09198519f 100644 --- a/runtime/queries/_typescript/textobjects.scm +++ b/runtime/queries/_typescript/textobjects.scm @@ -4,3 +4,9 @@ (type_alias_declaration value: (_) @class.inside) ] @class.around + +(enum_body + (_) @entry.around) + +(enum_assignment (_) @entry.inside) + diff --git a/runtime/queries/adl/highlights.scm b/runtime/queries/adl/highlights.scm new file mode 100644 index 000000000..88e00ad7f --- /dev/null +++ b/runtime/queries/adl/highlights.scm @@ -0,0 +1,37 @@ +; adl + +[ +"module" +"struct" +"union" +"type" +"newtype" +"annotation" +] @keyword + +(adl (scoped_name)) @namespace +(comment) @comment +(doc_comment) @comment.block.documentation +(name) @type + +(fname) @variable.other.member + +(type_expr (scoped_name) @type) + +(type_expr_params (param (scoped_name) @type.parameter)) + +; json +(key) @string.special + +(string) @string + +(number) @constant.numeric + +[ + (null) + (true) + (false) +] @constant.builtin + +(escape_sequence) @constant.character.escape + diff --git a/runtime/queries/adl/indents.scm b/runtime/queries/adl/indents.scm new file mode 100644 index 000000000..1200d4d42 --- /dev/null +++ b/runtime/queries/adl/indents.scm @@ -0,0 +1,12 @@ +[ + (struct) + (union) + + (array) + (object) +] @indent + +; [ +; "}" +; "]" +; ] @outdent diff --git a/runtime/queries/adl/textobjects.scm b/runtime/queries/adl/textobjects.scm new file mode 100644 index 000000000..36e39a2d4 --- /dev/null +++ b/runtime/queries/adl/textobjects.scm @@ -0,0 +1 @@ +(struct (_) @function.inside) @funtion.around diff --git a/runtime/queries/bash/highlights.scm b/runtime/queries/bash/highlights.scm index 92d61e8b8..1aa35aa7c 100644 --- a/runtime/queries/bash/highlights.scm +++ b/runtime/queries/bash/highlights.scm @@ -60,7 +60,6 @@ ">>" "<" "|" - (expansion_flags) ] @operator ( diff --git a/runtime/queries/bash/textobjects.scm b/runtime/queries/bash/textobjects.scm index 59983b30c..09536a6b5 100644 --- a/runtime/queries/bash/textobjects.scm +++ b/runtime/queries/bash/textobjects.scm @@ -7,3 +7,6 @@ (comment) @comment.inside (comment)+ @comment.around + +(array + (_) @entry.around) diff --git a/runtime/queries/bitbake/highlights.scm b/runtime/queries/bitbake/highlights.scm new file mode 100644 index 000000000..6e8ab0cf3 --- /dev/null +++ b/runtime/queries/bitbake/highlights.scm @@ -0,0 +1,82 @@ + +; variables +(variable_assignment (identifier) @variable.other.member) +(variable_assignment (concatenation (identifier) @variable.other.member)) +(unset_statement (identifier) @variable.other.member) +(export_statement (identifier) @variable.other.member) +(variable_expansion (identifier) @variable.other.member) +(python_function_definition (parameters (python_identifier) @variable.other.member)) + +(variable_assignment (override) @keyword.storage.modifier) +(overrides_statement (identifier) @keyword.storage.modifier) +(flag) @keyword.storage.modifier + +[ + "=" + "?=" + "??=" + ":=" + "=+" + "+=" + ".=" + "=." + +] @operator + +(variable_expansion [ "${" "}" ] @punctuation.special) +[ "(" ")" "{" "}" "[" "]" ] @punctuation.bracket + +[ + "noexec" + "INHERIT" + "OVERRIDES" + "$BB_ENV_PASSTHROUGH" + "$BB_ENV_PASSTHROUGH_ADDITIONS" +] @variable.builtin + +; functions + +(python_function_definition (python_identifier) @function) +(anonymous_python_function (identifier) @function) +(function_definition (identifier) @function) +(export_functions_statement (identifier) @function) +(addtask_statement (identifier) @function) +(deltask_statement (identifier) @function) +(addhandler_statement (identifier) @function) +(function_definition (override) @keyword.storage.modifier) + +[ + "addtask" + "deltask" + "addhandler" + "unset" + "EXPORT_FUNCTIONS" + "python" + "def" +] @keyword.function + +[ + "append" + "prepend" + "remove" + + "before" + "after" +] @keyword.operator + +; imports + +[ + "inherit" + "include" + "require" + "export" + "import" +] @keyword.control.import + +(inherit_path) @namespace +(include_path) @namespace + + +(string) @string +(comment) @comment diff --git a/runtime/queries/bitbake/injections.scm b/runtime/queries/bitbake/injections.scm new file mode 100644 index 000000000..adae990e4 --- /dev/null +++ b/runtime/queries/bitbake/injections.scm @@ -0,0 +1,18 @@ +((python_function_definition) @injection.content + (#set! injection.language "python") + (#set! injection.include-children)) + +((anonymous_python_function (block) @injection.content) + (#set! injection.language "python") + (#set! injection.include-children)) + +((inline_python) @injection.content + (#set! injection.language "python") + (#set! injection.include-children)) + +((function_definition) @injection.content + (#set! injection.language "bash") + (#set! injection.include-children)) + +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/c/textobjects.scm b/runtime/queries/c/textobjects.scm index 2a3da66f3..63ec4a567 100644 --- a/runtime/queries/c/textobjects.scm +++ b/runtime/queries/c/textobjects.scm @@ -19,3 +19,9 @@ (comment) @comment.inside (comment)+ @comment.around + +(enumerator + (_) @entry.inside) @entry.around + +(initializer_list + (_) @entry.around) diff --git a/runtime/queries/comment/highlights.scm b/runtime/queries/comment/highlights.scm index ba26ca0bf..e0916cece 100644 --- a/runtime/queries/comment/highlights.scm +++ b/runtime/queries/comment/highlights.scm @@ -1,5 +1,3 @@ -":" @punctuation.delimiter - ; Hint level tags ((tag (name) @hint) (#match? @hint "^(HINT|MARK|PASSED|STUB|MOCK)$")) diff --git a/runtime/queries/earthfile/highlights.scm b/runtime/queries/earthfile/highlights.scm new file mode 100644 index 000000000..2de8670f8 --- /dev/null +++ b/runtime/queries/earthfile/highlights.scm @@ -0,0 +1,74 @@ +(string_array "," @punctuation.delimiter) +(string_array ["[" "]"] @punctuation.bracket) + +[ + "ARG" + "AS LOCAL" + "BUILD" + "CACHE" + "CMD" + "COPY" + "DO" + "ENTRYPOINT" + "ENV" + "EXPOSE" + "FROM DOCKERFILE" + "FROM" + "FUNCTION" + "GIT CLONE" + "HOST" + "IMPORT" + "LABEL" + "LET" + "PROJECT" + "RUN" + "SAVE ARTIFACT" + "SAVE IMAGE" + "SET" + "USER" + "VERSION" + "VOLUME" + "WORKDIR" +] @keyword + +(for_command ["FOR" "IN" "END"] @keyword.control.repeat) + +(if_command ["IF" "END"] @keyword.control.conditional) +(elif_block ["ELSE IF"] @keyword.control.conditional) +(else_block ["ELSE"] @keyword.control.conditional) + +(import_command ["IMPORT" "AS"] @keyword.control.import) + +(try_command ["TRY" "FINALLY" "END"] @keyword.control.exception) + +(wait_command ["WAIT" "END"] @keyword.control) +(with_docker_command ["WITH DOCKER" "END"] @keyword.control) + +[ + (comment) + (line_continuation_comment) +] @comment + +(line_continuation) @operator + +[ + (target_ref) + (target_artifact) + (function_ref) +] @function + +(target (identifier) @function) + +[ + (double_quoted_string) + (single_quoted_string) +] @string +(unquoted_string) @string.special +(escape_sequence) @constant.character.escape + +(variable) @variable +(expansion ["$" "{" "}" "(" ")"] @punctuation.special) +(build_arg) @variable +(options (_) @variable.parameter) + +"=" @operator diff --git a/runtime/queries/earthfile/indents.scm b/runtime/queries/earthfile/indents.scm new file mode 100644 index 000000000..2bee23e84 --- /dev/null +++ b/runtime/queries/earthfile/indents.scm @@ -0,0 +1 @@ +(target) @indent diff --git a/runtime/queries/earthfile/injections.scm b/runtime/queries/earthfile/injections.scm new file mode 100644 index 000000000..7435a400e --- /dev/null +++ b/runtime/queries/earthfile/injections.scm @@ -0,0 +1,9 @@ +((comment) @injection.content + (#set! injection.language "comment")) + +((line_continuation_comment) @injection.content + (#set! injection.language "comment")) + +((shell_fragment) @injection.content + (#set! injection.language "bash") + (#set! injection.include-children)) diff --git a/runtime/queries/earthfile/textobjects.scm b/runtime/queries/earthfile/textobjects.scm new file mode 100644 index 000000000..ae0364a2d --- /dev/null +++ b/runtime/queries/earthfile/textobjects.scm @@ -0,0 +1 @@ +(target (block) @function.inside) @function.around diff --git a/runtime/queries/ecma/highlights.scm b/runtime/queries/ecma/highlights.scm index 07f7dfeda..dc8ce5e73 100644 --- a/runtime/queries/ecma/highlights.scm +++ b/runtime/queries/ecma/highlights.scm @@ -29,15 +29,24 @@ name: (identifier) @function) (method_definition name: (property_identifier) @function.method) +(method_definition + name: (private_property_identifier) @function.method.private) (pair key: (property_identifier) @function.method value: [(function) (arrow_function)]) +(pair + key: (private_property_identifier) @function.method.private + value: [(function) (arrow_function)]) (assignment_expression left: (member_expression property: (property_identifier) @function.method) right: [(function) (arrow_function)]) +(assignment_expression + left: (member_expression + property: (private_property_identifier) @function.method.private) + right: [(function) (arrow_function)]) (variable_declarator name: (identifier) @function @@ -64,6 +73,9 @@ (call_expression function: (member_expression property: (property_identifier) @function.method)) +(call_expression + function: (member_expression + property: (private_property_identifier) @function.method.private)) ; Variables ;---------- @@ -74,6 +86,7 @@ ;----------- (property_identifier) @variable.other.member +(private_property_identifier) @variable.other.member.private (shorthand_property_identifier) @variable.other.member (shorthand_property_identifier_pattern) @variable.other.member diff --git a/runtime/queries/ecma/textobjects.scm b/runtime/queries/ecma/textobjects.scm index c80dc81b4..a19eb25bb 100644 --- a/runtime/queries/ecma/textobjects.scm +++ b/runtime/queries/ecma/textobjects.scm @@ -34,3 +34,12 @@ (comment) @comment.inside (comment)+ @comment.around + +(array + (_) @entry.around) + +(pair + (_) @entry.inside) @entry.around + +(pair_pattern + (_) @entry.inside) @entry.around diff --git a/runtime/queries/gleam/injections.scm b/runtime/queries/gleam/injections.scm new file mode 100644 index 000000000..ac82a3e5f --- /dev/null +++ b/runtime/queries/gleam/injections.scm @@ -0,0 +1,3 @@ +((comment) @injection.content + (#set! injection.language "comment")) + diff --git a/runtime/queries/graphql/textobjects.scm b/runtime/queries/graphql/textobjects.scm new file mode 100644 index 000000000..b60353672 --- /dev/null +++ b/runtime/queries/graphql/textobjects.scm @@ -0,0 +1,23 @@ +(type_definition) @class.around + +(executable_definition) @function.around + +(arguments_definition + (input_value_definition) @parameter.inside @parameter.movement) + +(arguments + (argument) @parameter.inside @parameter.movement) + +(selection + [(field) (fragment_spread)] @entry.around) + +(selection + (field (selection_set) @entry.inside)) + +(field_definition + (_) @entry.inside) @entry.around + +(input_fields_definition + (input_value_definition ) @entry.around) + +(enum_value) @entry.around diff --git a/runtime/queries/hurl/textobjects.scm b/runtime/queries/hurl/textobjects.scm new file mode 100644 index 000000000..ac96f9555 --- /dev/null +++ b/runtime/queries/hurl/textobjects.scm @@ -0,0 +1,5 @@ +(comment) @comment.inside + +(comment)+ @comment.around + +(entry (_) @function.inside) @function.around diff --git a/runtime/queries/java/textobjects.scm b/runtime/queries/java/textobjects.scm index b0e73a0a7..1fe0726a8 100644 --- a/runtime/queries/java/textobjects.scm +++ b/runtime/queries/java/textobjects.scm @@ -36,3 +36,9 @@ (line_comment)+ @comment.around (block_comment) @comment.around + +(array_initializer + (_) @entry.around) + +(enum_body + (enum_constant) @entry.around) diff --git a/runtime/queries/json/textobjects.scm b/runtime/queries/json/textobjects.scm new file mode 100644 index 000000000..a8fd57c99 --- /dev/null +++ b/runtime/queries/json/textobjects.scm @@ -0,0 +1,5 @@ +(pair + (_) @entry.inside) @entry.around + +(array + (_) @entry.around) diff --git a/runtime/queries/koka/highlights.scm b/runtime/queries/koka/highlights.scm index fead98384..1ef409278 100644 --- a/runtime/queries/koka/highlights.scm +++ b/runtime/queries/koka/highlights.scm @@ -12,18 +12,6 @@ ]))) ["(" (block) (fnexpr)]) -(ntlappexpr - function: (ntlappexpr - (atom - (qidentifier - [ - (qvarid) @function - (qidop) @function - (identifier - [(varid) (idop)] @function) - ]))) - ["(" (block) (fnexpr)]) - (appexpr field: (atom (qidentifier @@ -46,28 +34,6 @@ ]))) "[") -(ntlappexpr - field: (atom - (qidentifier - [ - (qvarid) @function - (qidop) @function - (identifier - [(varid) (idop)] @function) - ]))) - -(ntlappexpr - (ntlappexpr - field: (atom - (qidentifier - [ - (qvarid) @variable - (qidop) @variable - (identifier - [(varid) (idop)] @variable) - ]))) - "[") - [ "initially" "finally" diff --git a/runtime/queries/koka/indents.scm b/runtime/queries/koka/indents.scm index 0a47bcbbd..6045373db 100644 --- a/runtime/queries/koka/indents.scm +++ b/runtime/queries/koka/indents.scm @@ -1,6 +1,5 @@ [ (appexpr ["[" "("]) ; Applications. - (ntlappexpr ["[" "("]) (atom ["[" "("]) ; Lists and tuples. (program (moduledecl "{")) ; Braced module declarations. (funbody) diff --git a/runtime/queries/ld/highlights.scm b/runtime/queries/ld/highlights.scm index 4f935e753..e0c9dd1e1 100644 --- a/runtime/queries/ld/highlights.scm +++ b/runtime/queries/ld/highlights.scm @@ -4,7 +4,7 @@ . (NAME) @namespace) -(NAME) @variable +[(NAME) (SYMBOLNAME)] @variable ; Operators diff --git a/runtime/queries/ldif/highlights.scm b/runtime/queries/ldif/highlights.scm new file mode 100644 index 000000000..882ace9fc --- /dev/null +++ b/runtime/queries/ldif/highlights.scm @@ -0,0 +1,19 @@ +(comment) @comment + +((distinguishedName + (name + (name_componet + (attributeTypeAndValue + (attributeType) @comment + (string) @type.parameter + )))) @comment) + + +(dn_spec) @constant +(changerecord) @constant +(mod_spec) @constant + +(attributeType) @type.parameter +(change_modify) @string + +(value_spec) @keyword diff --git a/runtime/queries/lua/textobjects.scm b/runtime/queries/lua/textobjects.scm index 6fb2000df..305c3ae7d 100644 --- a/runtime/queries/lua/textobjects.scm +++ b/runtime/queries/lua/textobjects.scm @@ -13,3 +13,6 @@ (comment) @comment.inside (comment)+ @comment.around + +(table_constructor + (field (_) @entry.inside) @entry.around) diff --git a/runtime/queries/move/highlights.scm b/runtime/queries/move/highlights.scm new file mode 100644 index 000000000..86a79dc22 --- /dev/null +++ b/runtime/queries/move/highlights.scm @@ -0,0 +1,157 @@ +(ability) @keyword + +; --- +; Primitives +; --- + +(address_literal) @constant +(bool_literal) @constant.builtin.boolean +(num_literal) @constant.numeric +[ + (hex_string_literal) + (byte_string_literal) +] @string +; TODO: vector_literal + +[ + (line_comment) + (block_comment) +] @comment + +(annotation) @function.macro + +(borrow_expression "&" @keyword.storage.modifier.ref) +(borrow_expression "&mut" @keyword.storage.modifier.mut) + +(constant_identifier) @constant +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +(function_identifier) @function + +(struct_identifier) @type +(pack_expression + access: (module_access + member: (identifier) @type)) +(apply_type + (module_access + member: (identifier) @type)) +(field_identifier) @variable.other.member + +; ------- +; Functions +; ------- + +(call_expression + access: (module_access + member: (identifier) @function)) + +(macro_call_expression + access: (macro_module_access + access: (module_access + member: [(identifier) @function.macro]) + "!" @function.macro)) + +; ------- +; Paths +; ------- + +(module_identifier) @namespace + +; ------- +; Operators +; ------- + +[ + "*" + "=" + "!" +] @operator +(binary_operator) @operator + +; --- +; Punctuation +; --- + +[ + "::" + "." + ";" + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "abort" + ; "acquires" + "as" + "break" + "const" + "continue" + "copy" + "else" + "false" + "friend" + "fun" + "has" + "if" + ; "invariant" + "let" + "loop" + "module" + "move" + "native" + "public" + "return" + ; "script" + "spec" + "struct" + "true" + "use" + "while" + + "entry" + + ; "aborts_if" + ; "aborts_with" + "address" + "apply" + "assume" + ; "axiom" + ; "choose" + "decreases" + ; "emits" + "ensures" + "except" + ; "forall" + "global" + "include" + "internal" + "local" + ; "min" + ; "modifies" + "mut" + "phantom" + "post" + "pragma" + ; "requires" + ; "Self" + "schema" + "succeeds_if" + "to" + ; "update" + "where" + "with" +] @keyword + +(primitive_type) @type.buildin + +(identifier) @variable diff --git a/runtime/queries/ohm/highlights.scm b/runtime/queries/ohm/highlights.scm new file mode 100644 index 000000000..68477aacc --- /dev/null +++ b/runtime/queries/ohm/highlights.scm @@ -0,0 +1,122 @@ +; See: https://docs.helix-editor.com/master/themes.html#syntax-highlighting + +; attribute +; --------- + +(case_name) @attribute + +; comment.line +; ------------ + +[ + (singleline_comment) + (rule_descr) +] @comment.line + +; comment.block +; ------------- + +(multiline_comment) @comment.block + +; function.method +; --------------- + +(rule + name: (identifier) @function.method) + +; function.builtin +; ---------------- + +; Lexical +((identifier) @function.builtin + (#any-of? @function.builtin + "any" + "alnum" + "end" + "digit" "hexDigit" + "letter" + "space" + "lower" "upper" "caseInsensitive" + "listOf" "nonemptyListOf" "emptyListOf" + "applySyntactic") + (#is-not? local)) + +; Syntactic +((identifier) @function.builtin + (#any-of? @function.builtin "ListOf" "NonemptyListOf" "EmptyListOf") + (#is-not? local)) + +; function.method (continuing) +; --------------- + +(term + base: (identifier) @function.method) + +; string.special +; -------------- + +(escape_char) @constant.character.escape + +; string +; ------ + +[ + (terminal_string) + (one_char_terminal) +] @string + +; type +; ---- + +(super_grammar + name: (identifier) @type) + +(grammar + name: (identifier) @type) + +; operator +; -------- + +[ + ; "=" ":=" "+=" + (define) (override) (extend) + + ; "&" "~" + (lookahead) (negative_lookahead) + + ; "#" + (lexification) + + ; "*" "+" "?" + (zero_or_more) (one_or_more) (zero_or_one) + + ; "..." + (super_splice) + + "<:" ".." "|" +] @operator + +; punctuation.bracket +; ------------------- + +[ + "<" + ">" + "{" + "}" +] @punctuation.bracket + +(alt + "(" @punctuation.bracket + ")" @punctuation.bracket) + +; punctuation.delimiter +; --------------------- + +"," @punctuation.delimiter + +; variable.parameter +; ------------------ + +(formals + (identifier) @variable.parameter) diff --git a/runtime/queries/ohm/indents.scm b/runtime/queries/ohm/indents.scm new file mode 100644 index 000000000..f56119aec --- /dev/null +++ b/runtime/queries/ohm/indents.scm @@ -0,0 +1,37 @@ +; See: https://docs.helix-editor.com/guides/indent.html + +; indent +; ------ + +[ + ; <..., ...> + (formals) + (params) + + ; (...| ...) + (alt) +] @indent + +; outdent +; ------- + +[ + "}" + ")" + ">" +] @outdent + +; align +; ----- + +; | ... | ... +(rule_body + . (top_level_term) @anchor + (#set! "scope" "tail")) @align + +; N/A or unused: +; -------------- +; indent.always +; outdent.always +; extend +; extend.prevent-once diff --git a/runtime/queries/ohm/injections.scm b/runtime/queries/ohm/injections.scm new file mode 100644 index 000000000..dc50a7654 --- /dev/null +++ b/runtime/queries/ohm/injections.scm @@ -0,0 +1,7 @@ +; See: https://docs.helix-editor.com/guides/injection.html + +((singleline_comment) @injection.content + (#set! injection.language "comment")) + +((multiline_comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/ohm/textobjects.scm b/runtime/queries/ohm/textobjects.scm new file mode 100644 index 000000000..855ad8de7 --- /dev/null +++ b/runtime/queries/ohm/textobjects.scm @@ -0,0 +1,40 @@ +; See: https://docs.helix-editor.com/guides/textobject.html + +; function.inside & around +; ------------------------ + +(rule + body: (_) @function.inside) @function.around + +; class.inside & around +; --------------------- + +(grammar + body: (_) @class.inside) @class.around + +; parameter.inside & around +; ------------------------- + +(formals + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(rule_body + ((_) @parameter.inside . "|"? @parameter.around) @parameter.around) + +(params + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(alt + ((_) @parameter.inside . "|"? @parameter.around) @parameter.around) + +; comment.inside +; -------------- + +(multiline_comment)+ @comment.inside +(singleline_comment)+ @comment.inside + +; comment.around +; -------------- + +(multiline_comment)+ @comment.around +(singleline_comment)+ @comment.around diff --git a/runtime/queries/pest/highlights.scm b/runtime/queries/pest/highlights.scm new file mode 100644 index 000000000..9d6f13c2a --- /dev/null +++ b/runtime/queries/pest/highlights.scm @@ -0,0 +1,49 @@ +(line_comment) @comment +(block_comment) @comment + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +((identifier) @variable) +((builtin) @type.builtin) +((const) @constant) + +[ + (string) + (character) +] @string + +[ + "_" + "@" + "$" +]@keyword.storage.modifier + +[ + "~" + "|" + "=" + "+" + "*" + "&" + "^" + "!" + "?" + ".." +] @operator + +[ + "PUSH" + "PEEK" + "POP" + "SOI" + "EOI" + "ANY" +] @keyword + diff --git a/runtime/queries/pest/indents.scm b/runtime/queries/pest/indents.scm new file mode 100644 index 000000000..51042f905 --- /dev/null +++ b/runtime/queries/pest/indents.scm @@ -0,0 +1,9 @@ +[ + (expression) +] @indent + +[ + "]" + "}" + ")" +] @outdent diff --git a/runtime/queries/pest/injections.scm b/runtime/queries/pest/injections.scm new file mode 100644 index 000000000..0a79c8f83 --- /dev/null +++ b/runtime/queries/pest/injections.scm @@ -0,0 +1,7 @@ +((line_comment) @injection.content + (#set! injection.language "comment") + (#set! injection.include-children)) + +((block_comment) @injection.content + (#set! injection.language "comment") + (#set! injection.include-children)) diff --git a/runtime/queries/pest/textobjects.scm b/runtime/queries/pest/textobjects.scm new file mode 100644 index 000000000..75ccd09eb --- /dev/null +++ b/runtime/queries/pest/textobjects.scm @@ -0,0 +1,8 @@ +(grammar_rule (_) @class.inside) @class.around +(term (_) @entry.inside) @entry.around + +(line_comment) @comment.inside +(line_comment)+ @comment.around + +(block_comment) @comment.inside +(block_comment)+ @comment.around diff --git a/runtime/queries/php/textobjects.scm b/runtime/queries/php/textobjects.scm index e35eebd76..e721b864d 100644 --- a/runtime/queries/php/textobjects.scm +++ b/runtime/queries/php/textobjects.scm @@ -38,3 +38,15 @@ (comment) @comment.inside (comment)+ @comment.around + +(array_creation_expression + (array_element_initializer + (_) @entry.inside + ) @entry.around @entry.movement) + +(list_literal + (_) @entry.inside @entry.around @entry.movement) + +[ + (enum_case) +] @entry.around @entry.movement diff --git a/runtime/queries/python/highlights.scm b/runtime/queries/python/highlights.scm index 2666402b8..0a082f2fd 100644 --- a/runtime/queries/python/highlights.scm +++ b/runtime/queries/python/highlights.scm @@ -204,13 +204,13 @@ (for_in_clause "in" @keyword.control) [ - "and" "async" "class" "exec" "global" "nonlocal" "print" + "type" ] @keyword [ "and" diff --git a/runtime/queries/python/textobjects.scm b/runtime/queries/python/textobjects.scm index 966e47446..2b9556fca 100644 --- a/runtime/queries/python/textobjects.scm +++ b/runtime/queries/python/textobjects.scm @@ -21,3 +21,15 @@ name: (identifier) @_name body: (block)? @test.inside) @test.around (#match? @_name "^test_")) + +(list + (_) @entry.around) + +(tuple + (_) @entry.around) + +(set + (_) @entry.around) + +(pair + (_) @entry.inside) @entry.around diff --git a/runtime/queries/ruby/injections.scm b/runtime/queries/ruby/injections.scm index 1a865df19..3e0d8bb83 100644 --- a/runtime/queries/ruby/injections.scm +++ b/runtime/queries/ruby/injections.scm @@ -6,3 +6,18 @@ (heredoc_end) @name (#set! injection.language "sql")) (#eq? @name "SQL")) + +((heredoc_body + (heredoc_content) @injection.content + (heredoc_end) @name + (#set! injection.language "graphql")) + (#any-of? @name + "GQL" + "GRAPHQL")) + +((heredoc_body + (heredoc_content) @injection.content + (heredoc_end) @name + (#set! injection.language "erb")) + (#eq? @name "ERB")) + diff --git a/runtime/queries/ruby/textobjects.scm b/runtime/queries/ruby/textobjects.scm index 2d48fa6fc..123c55678 100644 --- a/runtime/queries/ruby/textobjects.scm +++ b/runtime/queries/ruby/textobjects.scm @@ -42,3 +42,15 @@ ; Comments (comment) @comment.inside (comment)+ @comment.around + +(pair + (_) @entry.inside) @entry.around + +(array + (_) @entry.around) + +(string_array + (_) @entry.around) + +(symbol_array + (_) @entry.around) diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 09068b4f5..7997c5ea0 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -51,7 +51,7 @@ (lifetime "'" @label (identifier) @label) -(loop_label +(label "'" @label (identifier) @label) @@ -125,6 +125,7 @@ "match" "if" "else" + "try" ] @keyword.control.conditional [ diff --git a/runtime/queries/rust/textobjects.scm b/runtime/queries/rust/textobjects.scm index df26331d8..de517d362 100644 --- a/runtime/queries/rust/textobjects.scm +++ b/runtime/queries/rust/textobjects.scm @@ -59,3 +59,27 @@ (function_item body: (_) @test.inside) @test.around (#eq? @_test_attribute "test")) + +(array_expression + (_) @entry.around) + +(tuple_expression + (_) @entry.around) + +(tuple_pattern + (_) @entry.around) + +; Commonly used vec macro intializer is special cased +(macro_invocation + (identifier) @_id (token_tree (_) @entry.around) + (#eq? @_id "vec")) + +(enum_variant) @entry.around + +(field_declaration + (_) @entry.inside) @entry.around + +(field_initializer + (_) @entry.inside) @entry.around + +(shorthand_field_initializer) @entry.around diff --git a/runtime/queries/solidity/highlights.scm b/runtime/queries/solidity/highlights.scm index 08178c362..fcc8d3e31 100644 --- a/runtime/queries/solidity/highlights.scm +++ b/runtime/queries/solidity/highlights.scm @@ -1,11 +1,6 @@ -; identifiers -; ----------- -(identifier) @variable -(yul_identifier) @variable - ; Pragma -(pragma_directive) @tag -(solidity_version_comparison_operator _ @tag) +(pragma_directive) @keyword.directive +(solidity_version_comparison_operator _ @keyword.directive) ; Literals @@ -25,7 +20,7 @@ [ (true) (false) -] @constant.builtin +] @constant.builtin.boolean (comment) @comment @@ -34,8 +29,14 @@ ; ----------- (type_name) @type -(primitive_type) @type + +[ + (primitive_type) + (number_unit) +] @type.builtin + (user_defined_type (identifier) @type) +(type_alias (identifier) @type) ; Color payable in payable address conversion as type and not as keyword (payable_conversion_expression "payable" @type) @@ -65,6 +66,7 @@ ; Use constructor coloring for special functions (constructor_definition "constructor" @constructor) +(error_declaration "error" @constructor) (fallback_receive_definition "receive" @constructor) (fallback_receive_definition "fallback" @constructor) @@ -80,7 +82,7 @@ ; Function parameters (call_struct_argument name: (identifier) @field) -(event_paramater name: (identifier) @variable.parameter) +(event_parameter name: (identifier) @variable.parameter) (parameter name: (identifier) @variable.parameter) ; Yul functions @@ -99,7 +101,7 @@ ; Keywords (meta_type_expression "type" @keyword) [ - "pragma" + "abstract" "contract" "interface" "library" @@ -107,9 +109,9 @@ "struct" "enum" "event" - "using" "assembly" "emit" + "public" "internal" "private" @@ -117,17 +119,22 @@ "pure" "view" "payable" + "modifier" - "memory" - "storage" - "calldata" "var" - "constant" + "let" (virtual) (override_specifier) (yul_leave) ] @keyword +[ + "memory" + "storage" + "calldata" + "constant" +] @keyword.storage.modifier + [ "for" "while" @@ -147,6 +154,7 @@ [ "try" "catch" + "revert" ] @keyword.control.exception [ @@ -157,9 +165,10 @@ "function" @keyword.function "import" @keyword.control.import +"using" @keyword.control.import (import_directive "as" @keyword.control.import) (import_directive "from" @keyword.control.import) -(event_paramater "indexed" @keyword) ; TODO fix spelling once fixed upstream +(event_parameter "indexed" @keyword) ; Punctuation @@ -176,6 +185,9 @@ [ "." "," + ":" + "->" + "=>" ] @punctuation.delimiter @@ -217,3 +229,11 @@ "delete" "new" ] @keyword.operator + +; TODO: move to top when order swapped +; identifiers +; ----------- +((identifier) @variable.builtin + (#match? @variable.builtin "^(this|msg|block|tx)$")) +(identifier) @variable +(yul_identifier) @variable diff --git a/runtime/queries/solidity/textobjects.scm b/runtime/queries/solidity/textobjects.scm new file mode 100644 index 000000000..4e5ffdd12 --- /dev/null +++ b/runtime/queries/solidity/textobjects.scm @@ -0,0 +1,54 @@ +(function_definition + body: (_) @function.inside) @function.around + +(constructor_definition + body: (_) @function.inside) @function.around + +(fallback_receive_definition + body: (_) @function.inside) @function.around + +(yul_function_definition + (yul_block) @function.inside) @function.around + +(function_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(constructor_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(return_type_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(modifier_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(event_definition + ((event_parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(error_declaration + ((error_parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(call_argument + ((call_struct_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(call_expression + ((call_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(variable_declaration_tuple + ((variable_declaration) @parameter.inside . ","? @parameter.around) @parameter.around) + +(emit_statement + ((call_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(revert_arguments + ((call_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(struct_declaration + body: (_) @class.inside) @class.around + +(enum_declaration + body: (_) @class.inside) @class.around + +(comment) @comment.inside + +(comment)+ @comment.around diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm index 22b0c551e..7ab729030 100644 --- a/runtime/queries/svelte/highlights.scm +++ b/runtime/queries/svelte/highlights.scm @@ -1,6 +1,11 @@ ; Special identifiers ;-------------------- +(tag_name) @tag +(attribute_name) @variable.other.member +(erroneous_end_tag_name) @error +(comment) @comment + ; TODO: ((element (start_tag (tag_name) @_tag) (text) @markup.heading) (#match? @_tag "^(h[0-9]|title)$")) @@ -28,11 +33,6 @@ (quoted_attribute_value (attribute_value) @markup.link.url)) (#match? @_attr "^(href|src)$")) -(tag_name) @tag -(attribute_name) @variable.other.member -(erroneous_end_tag_name) @error -(comment) @comment - [ (attribute_value) (quoted_attribute_value) diff --git a/runtime/queries/tcl/folds.scm b/runtime/queries/tcl/folds.scm new file mode 100644 index 000000000..9084c662c --- /dev/null +++ b/runtime/queries/tcl/folds.scm @@ -0,0 +1 @@ +(braced_word) @fold diff --git a/runtime/queries/tcl/highlights.scm b/runtime/queries/tcl/highlights.scm new file mode 100644 index 000000000..27b2d81e6 --- /dev/null +++ b/runtime/queries/tcl/highlights.scm @@ -0,0 +1,153 @@ + +(comment) @comment + +(command name: (simple_word) @function) + +"proc" @keyword.function + +(procedure + name: (_) @variable +) + +(set (simple_word) @variable) + +(argument + name: (_) @variable.parameter +) + +((simple_word) @variable.builtin + (#any-of? @variable.builtin + "argc" + "argv" + "argv0" + "auto_path" + "env" + "errorCode" + "errorInfo" + "tcl_interactive" + "tcl_library" + "tcl_nonwordchars" + "tcl_patchLevel" + "tcl_pkgPath" + "tcl_platform" + "tcl_precision" + "tcl_rcFileName" + "tcl_traceCompile" + "tcl_traceExec" + "tcl_wordchars" + "tcl_version")) + + +"expr" @function.builtin + +(command + name: (simple_word) @function.builtin + (#any-of? @function.builtin + "cd" + "exec" + "exit" + "incr" + "info" + "join" + "puts" + "regexp" + "regsub" + "split" + "subst" + "trace" + "source")) + +(command name: (simple_word) @keyword + (#any-of? @keyword + "append" + "break" + "catch" + "continue" + "default" + "dict" + "error" + "eval" + "global" + "lappend" + "lassign" + "lindex" + "linsert" + "list" + "llength" + "lmap" + "lrange" + "lrepeat" + "lreplace" + "lreverse" + "lsearch" + "lset" + "lsort" + "package" + "return" + "switch" + "throw" + "unset" + "variable")) + +[ + "error" + "namespace" + "on" + "set" + "try" + ] @keyword + +(unpack) @operator + +[ + "while" + "foreach" + ; "for" + ] @keyword.control.repeat + +[ + "if" + "else" + "elseif" + ] @keyword.control.conditional + +[ + "**" + "/" "*" "%" "+" "-" + "<<" ">>" + ">" "<" ">=" "<=" + "==" "!=" + "eq" "ne" + "in" "ni" + "&" + "^" + "|" + "&&" + "||" + ] @operator + +(variable_substitution) @variable +(quoted_word) @string +(escaped_character) @constant.character.escape + +[ + "{" "}" + "[" "]" + ";" + ] @punctuation.delimiter + +((simple_word) @constant.numeric + (#match? @constant.numeric "^[0-9]+$")) + +((simple_word) @constant.builtin.boolean + (#any-of? @constant.builtin.boolean "true" "false")) + +; after apply array auto_execok auto_import auto_load auto_mkindex auto_qualify +; auto_reset bgerror binary chan clock close coroutine dde encoding eof fblocked +; fconfigure fcopy file fileevent filename flush format gets glob history http +; interp load mathfunc mathop memory msgcat my next nextto open parray pid +; pkg::create pkg_mkIndex platform platform::shell pwd re_syntax read refchan +; registry rename safe scan seek self socket source string tailcall tcl::prefix +; tcl_endOfWord tcl_findLibrary tcl_startOfNextWord tcl_startOfPreviousWord +; tcl_wordBreakAfter tcl_wordBreakBefore tcltest tell time timerate tm +; transchan unknown unload update uplevel upvar vwait yield yieldto zlib diff --git a/runtime/queries/tcl/indents.scm b/runtime/queries/tcl/indents.scm new file mode 100644 index 000000000..c0652fe78 --- /dev/null +++ b/runtime/queries/tcl/indents.scm @@ -0,0 +1,13 @@ +[ + (braced_word_simple) + (namespace) + (command) + (conditional) + (foreach) + (while) + (try) + (procedure) + (command_substitution) + ] @indent + +[ "}" "]" ] @outdent diff --git a/runtime/queries/templ/highlights.scm b/runtime/queries/templ/highlights.scm index e5b2ede01..d015970c0 100644 --- a/runtime/queries/templ/highlights.scm +++ b/runtime/queries/templ/highlights.scm @@ -1,28 +1,9 @@ -(package_identifier) @namespace +; inherits: go -(parameter_declaration (identifier) @variable.parameter) -(variadic_parameter_declaration (identifier) @variable.parameter) - -(function_declaration - name: (identifier) @function) - -(type_spec name: (type_identifier) @type) -(type_identifier) @type -(field_identifier) @variable.other.member -(identifier) @variable - -; Function calls - -(call_expression - function: (identifier) @function) - -(call_expression - function: (selector_expression - field: (field_identifier) @function)) - -; -; These are Templ specific -; +(css_declaration + name: (css_identifier) @function) +(script_declaration + name: (script_identifier) @function) (component_declaration name: (component_identifier) @function) @@ -42,6 +23,8 @@ (css_property name: (css_property_name) @attribute) +(css_property + value: (css_property_value) @constant) (expression) @function.method (dynamic_class_attribute_value) @function.method @@ -56,36 +39,12 @@ ] @operator [ - "func" - "var" - "const" "templ" "css" "type" - "struct" - "range" "script" ] @keyword.storage.type -[ - "return" -] @keyword.control.return - -[ - "import" - "package" -] @keyword.control.import - -[ - "else" - "case" - "switch" - "if" - "default" -] @keyword.control.conditional - -"for" @keyword.control.repeat - [ (interpreted_string_literal) (raw_string_literal) diff --git a/runtime/queries/toml/textobjects.scm b/runtime/queries/toml/textobjects.scm new file mode 100644 index 000000000..a8fd57c99 --- /dev/null +++ b/runtime/queries/toml/textobjects.scm @@ -0,0 +1,5 @@ +(pair + (_) @entry.inside) @entry.around + +(array + (_) @entry.around) diff --git a/runtime/queries/typst/highlights.scm b/runtime/queries/typst/highlights.scm index 0bbccede0..4e012a2fe 100644 --- a/runtime/queries/typst/highlights.scm +++ b/runtime/queries/typst/highlights.scm @@ -21,6 +21,7 @@ ; OPERATOR (in ["in" "not"] @keyword.operator) +(context "context" @keyword.control) (and "and" @keyword.operator) (or "or" @keyword.operator) (not "not" @keyword.operator) @@ -45,12 +46,9 @@ (string) @string (content ["[" "]"] @operator) (bool) @constant.builtin.boolean -(builtin) @constant.builtin (none) @constant.builtin (auto) @constant.builtin (ident) @variable -(call - item: (builtin) @function.builtin) ; MARKUP (item "-" @markup.list) diff --git a/runtime/queries/typst/injections.scm b/runtime/queries/typst/injections.scm index 06a250979..fb4fee5b2 100644 --- a/runtime/queries/typst/injections.scm +++ b/runtime/queries/typst/injections.scm @@ -5,3 +5,6 @@ lang: (ident) @injection.language (blob) @injection.content) +((comment) + @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/xtc/highlights.scm b/runtime/queries/xtc/highlights.scm new file mode 100644 index 000000000..cb0bec6ae --- /dev/null +++ b/runtime/queries/xtc/highlights.scm @@ -0,0 +1,27 @@ +(parameter) @keyword + +(change_port) @function.special + +(template) @variable + +[ + (hex_argument) + (ipv4_argument) +] @attribute + +(numeric_argument) @constant.numeric + +(index) @tag + +(string_literal_argument) @string + +(string_argument) @constant.character + +(comment) @comment + +(port_comment) @label + +[ +("[") +("]") +] @punctuation.bracket diff --git a/runtime/queries/yaml/injections.scm b/runtime/queries/yaml/injections.scm index 52b437a4e..46c4a9c0f 100644 --- a/runtime/queries/yaml/injections.scm +++ b/runtime/queries/yaml/injections.scm @@ -20,22 +20,24 @@ ; Modified for Helix from https://github.com/nvim-treesitter/nvim-treesitter/blob/master/queries/yaml/injections.scm -;; Github actions ("run") / Gitlab CI ("scripts") +;; Github actions: run +;; Gitlab CI: scripts, before_script, after_script +;; Buildkite: command, commands (block_mapping_pair - key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + key: (flow_node) @_run (#any-of? @_run "run" "script" "before_script" "after_script" "command" "commands") value: (flow_node (plain_scalar (string_scalar) @injection.content) (#set! injection.language "bash"))) (block_mapping_pair - key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + key: (flow_node) @_run (#any-of? @_run "run" "script" "before_script" "after_script" "command" "commands") value: (block_node (block_scalar) @injection.content (#set! injection.language "bash"))) (block_mapping_pair - key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + key: (flow_node) @_run (#any-of? @_run "run" "script" "before_script" "after_script" "command" "commands") value: (block_node (block_sequence (block_sequence_item @@ -45,7 +47,7 @@ (#set! injection.language "bash"))))) (block_mapping_pair - key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + key: (flow_node) @_run (#any-of? @_run "run" "script" "before_script" "after_script" "command" "commands") value: (block_node (block_sequence (block_sequence_item diff --git a/runtime/themes/acme.toml b/runtime/themes/acme.toml index 650924741..195e06354 100644 --- a/runtime/themes/acme.toml +++ b/runtime/themes/acme.toml @@ -24,6 +24,8 @@ "diagnostic.error" = {bg="red", fg="white", modifiers=["bold"]} "diagnostic.warning" = {bg="orange", fg="black", modifiers=["bold"]} "diagnostic.hint" = {fg="gray", modifiers=["bold"]} +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.bufferline" = { fg = "black", bg = "acme_bar_bg" } "ui.bufferline.active" = { fg = "black", bg = "acme_bg" } "diff.plus" = {fg = "green"} diff --git a/runtime/themes/adwaita-dark.toml b/runtime/themes/adwaita-dark.toml index 2ecfe08e0..44ae0063b 100644 --- a/runtime/themes/adwaita-dark.toml +++ b/runtime/themes/adwaita-dark.toml @@ -82,6 +82,7 @@ "ui.help" = { bg = "libadwaita_dark_alt" } "ui.text" = "light_4" "ui.virtual" = "dark_1" +"ui.virtual.ruler" = { bg = "libadwaita_popup"} "ui.menu" = { fg = "light_4", bg = "libadwaita_popup" } "ui.menu.selected" = { fg = "light_4", bg = "blue_5" } "ui.menu.scroll" = { fg = "light_7", bg = "dark_3" } @@ -98,6 +99,8 @@ "diagnostic.info" = { fg = "purple_2", modifiers = ["dim"] } "diagnostic.error" = { fg = "red_4", modifiers = ["underlined"] } "diagnostic.warning" = { fg = "yellow_2", modifiers = ["underlined"] } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.bufferline" = { fg = "dark_2", bg = "libadwaita_dark" } "ui.bufferline.active" = { fg = "light_4", bg = "libadwaita_dark_alt" } diff --git a/runtime/themes/amberwood.toml b/runtime/themes/amberwood.toml index 142ded369..30b4169ab 100644 --- a/runtime/themes/amberwood.toml +++ b/runtime/themes/amberwood.toml @@ -95,6 +95,8 @@ "ui.menu.scroll" = { fg = "gray04", bg = "gray01" } diagnostic = { modifiers = ["underlined"] } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } warning = "yellow" error = "error" diff --git a/runtime/themes/autumn.toml b/runtime/themes/autumn.toml index ce684a6a6..7e9b3a7e1 100644 --- a/runtime/themes/autumn.toml +++ b/runtime/themes/autumn.toml @@ -19,6 +19,8 @@ "diagnostic.info" = { underline = { style = "line" } } "diagnostic" = { underline = { style = "line", color = "my_gray5" }, bg = "my_black"} "diagnostic.warning" = { underline = { style = "curl", color = "my_yellow2" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "diff.delta" = "my_gray5" "diff.minus" = "my_red" "diff.plus" = "my_green" @@ -61,14 +63,15 @@ "ui.selection" = { bg = "my_gray3" } "ui.statusline" = { fg = "my_gray7", bg = "my_gray2" } "ui.statusline.inactive" = { fg = 'my_gray5', bg = 'my_gray2' } -"ui.statusline.insert" = {fg = "my_black", bg = "my_gray6", modifiers = ["bold"]} -"ui.statusline.normal" = {fg = "my_gray7", bg = "my_gray2"} -"ui.statusline.select" = {fg = "my_gray7", bg = "my_black", modifiers = ["bold"]} +"ui.statusline.insert" = { fg = "my_black", bg = "my_gray6", modifiers = ["bold"] } +"ui.statusline.normal" = { fg = "my_gray7", bg = "my_gray2" } +"ui.statusline.select" = { fg = "my_gray7", bg = "my_black", modifiers = ["bold"] } "ui.text.focus" = "my_white1" "ui.text" = "my_white1" "ui.virtual.inlay-hint" = { fg = "my_gray4", bg="my_black", modifiers = ["normal"] } "ui.virtual.inlay-hint.parameter" = { fg = "my_gray4", modifiers = ["normal"] } "ui.virtual.inlay-hint.type" = { fg = "my_gray4", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "my_yellow2", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "my_gray1" } "ui.virtual.whitespace" = { fg = "my_gray6" } "ui.virtual.wrap" = "my_gray4" diff --git a/runtime/themes/ayu_dark.toml b/runtime/themes/ayu_dark.toml index 0b011b0ea..a2fbc3161 100644 --- a/runtime/themes/ayu_dark.toml +++ b/runtime/themes/ayu_dark.toml @@ -66,10 +66,13 @@ "diagnostic.info"= { underline = { color = "blue", style="curl"} } "diagnostic.warning"= { underline = { color = "yellow", style="curl"} } "diagnostic.error"= { underline = { color = "red", style="curl"} } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.bufferline" = { fg = "gray", bg = "background" } "ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" } "ui.debug" = { fg = "orange", bg = "background" } "ui.highlight.frameline" = { bg = "#0067a3" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["italic", "bold"] } "special" = "orange" diff --git a/runtime/themes/ayu_evolve.toml b/runtime/themes/ayu_evolve.toml index b57235a6a..b9c5d8221 100644 --- a/runtime/themes/ayu_evolve.toml +++ b/runtime/themes/ayu_evolve.toml @@ -27,6 +27,7 @@ inherits = 'ayu_dark' "ui.text.inactive" = "gray" "ui.bufferline" = { fg = "light_gray", bg = "background" } "ui.bufferline.active" = { fg = "light_gray", bg = "dark_gray" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["italic", "bold"] } [palette] background = '#020202' diff --git a/runtime/themes/ayu_light.toml b/runtime/themes/ayu_light.toml index 6c405381d..c3ab1d4f5 100644 --- a/runtime/themes/ayu_light.toml +++ b/runtime/themes/ayu_light.toml @@ -66,10 +66,13 @@ "diagnostic.info"= { underline = { color = "blue", style = "curl" } } "diagnostic.warning"= { underline = { color = "yellow", style = "curl" } } "diagnostic.error"= { underline = { color = "red", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.bufferline" = { fg = "gray", bg = "background" } "ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" } "ui.debug" = { fg = "orange", bg = "background" } "ui.highlight.frameline" = { bg = "#cfe0f2" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["italic", "bold"] } "special" = "orange" diff --git a/runtime/themes/ayu_mirage.toml b/runtime/themes/ayu_mirage.toml index a4b74fcfb..0fd53a4b0 100644 --- a/runtime/themes/ayu_mirage.toml +++ b/runtime/themes/ayu_mirage.toml @@ -66,10 +66,13 @@ "diagnostic.info"= { underline = { color = "blue", style = "curl" } } "diagnostic.warning"= { underline = { color = "yellow", style = "curl" } } "diagnostic.error"= { underline = { color = "red", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.bufferline" = { fg = "gray", bg = "background" } "ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" } "ui.debug" = { fg = "orange", bg = "background" } "ui.highlight.frameline" = { bg = "#0067a3" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["italic", "bold"] } "special" = "orange" diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml index 1a38a6aeb..389c94821 100644 --- a/runtime/themes/base16_default_dark.toml +++ b/runtime/themes/base16_default_dark.toml @@ -2,6 +2,8 @@ "ui.background" = { bg = "base00" } "ui.virtual.whitespace" = "base03" +"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "base01" } "ui.menu" = { fg = "base05", bg = "base01" } "ui.menu.selected" = { fg = "base01", bg = "base04" } "ui.linenr" = { fg = "base03", bg = "base01" } diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml index 84dab5305..16e38e749 100644 --- a/runtime/themes/base16_default_light.toml +++ b/runtime/themes/base16_default_light.toml @@ -13,6 +13,8 @@ "ui.cursor" = { fg = "base04", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] } "ui.virtual.whitespace" = "base03" +"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "base01" } "ui.text" = "base05" "operator" = "base05" "ui.text.focus" = "base05" diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml index f39758611..c8530c79d 100644 --- a/runtime/themes/base16_terminal.toml +++ b/runtime/themes/base16_terminal.toml @@ -14,6 +14,8 @@ "ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "light-gray", modifiers = ["reversed"] } "ui.virtual.whitespace" = "light-gray" +"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "black" } "variable" = "light-red" "constant.numeric" = "yellow" "constant" = "yellow" diff --git a/runtime/themes/base16_transparent.toml b/runtime/themes/base16_transparent.toml index c314ae8c5..70452366f 100644 --- a/runtime/themes/base16_transparent.toml +++ b/runtime/themes/base16_transparent.toml @@ -31,6 +31,7 @@ "ui.virtual.inlay-hint.parameter" = { fg = "white", bg = "gray"} "ui.virtual.inlay-hint.type" = { fg = "white", bg = "gray"} "ui.virtual.wrap" = "gray" +"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } "variable" = "light-red" "constant.numeric" = "yellow" diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml index 1ea13f1f5..f7d08daa9 100644 --- a/runtime/themes/bogster.toml +++ b/runtime/themes/bogster.toml @@ -77,6 +77,8 @@ "diagnostic.error" = { underline = { color = "bogster-lred", style = "curl"} } "diagnostic.info" = { underline = { color = "bogster-teal", style = "curl"} } "diagnostic.hint" = { underline = { color = "bogster-blue", style = "curl"} } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] bogster-yellow = "#dcb659" diff --git a/runtime/themes/bogster_light.toml b/runtime/themes/bogster_light.toml index e528d89d9..3259d6777 100644 --- a/runtime/themes/bogster_light.toml +++ b/runtime/themes/bogster_light.toml @@ -77,6 +77,8 @@ "diagnostic.error" = { underline = { color = "bogster-lred", style = "curl"} } "diagnostic.info" = { underline = { color = "bogster-teal", style = "curl"} } "diagnostic.hint" = { underline = { color = "bogster-blue", style = "curl"} } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] bogster-orange = "#dc7759" diff --git a/runtime/themes/boo_berry.toml b/runtime/themes/boo_berry.toml index d3a3e2232..929bbc742 100644 --- a/runtime/themes/boo_berry.toml +++ b/runtime/themes/boo_berry.toml @@ -67,6 +67,8 @@ "diagnostic.error" = { underline = { color = "gold", style = "curl"} } "diagnostic.info" = { underline = { color = "lilac", style = "curl"} } "diagnostic.hint" = { underline = { color = "lilac", style = "curl"} } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] berry = "#3A2A4D" diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 65ba76e4b..58173dab1 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -89,6 +89,7 @@ "ui.virtual.ruler" = { bg = "surface0" } "ui.virtual.indent-guide" = "surface0" "ui.virtual.inlay-hint" = { fg = "overlay0", bg = "base" } +"ui.virtual.jump-label" = { fg = "rosewater", modifiers = ["bold"] } "ui.selection" = { bg = "surface1" } @@ -115,6 +116,8 @@ "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.info" = { underline = { color = "sky", style = "curl" } } "diagnostic.hint" = { underline = { color = "teal", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } error = "red" warning = "yellow" diff --git a/runtime/themes/curzon.toml b/runtime/themes/curzon.toml index 1303519fb..d5a608f5f 100644 --- a/runtime/themes/curzon.toml +++ b/runtime/themes/curzon.toml @@ -75,6 +75,8 @@ label = "label" "diagnostic.info" = { underline = { color = "info", style = "curl" } } "diagnostic.warning" = { underline = { color = "warning", style = "curl" } } "diagnostic.error" = { underline = { color = "error", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } warning = "warning" error = "#f43f5e" diff --git a/runtime/themes/cyan_light.toml b/runtime/themes/cyan_light.toml index a35ad5847..2ac57cd32 100644 --- a/runtime/themes/cyan_light.toml +++ b/runtime/themes/cyan_light.toml @@ -98,9 +98,12 @@ "ui.help" = { fg = "shade06", bg = "shade01" } "ui.text" = "shade05" "ui.text.focus" = { fg = "shade07", bg = "light_blue" } + "ui.virtual" = "shade03" "ui.virtual.ruler" = { bg = "shade01" } "ui.virtual.inlay-hint" = { fg = "shade03_darker" } +"ui.virtual.jump-label" = { fg = "shade07", bg = "shade01", modifiers = ["bold" ] } + "ui.menu" = { fg = "shade05", bg = "shade01" } "ui.menu.selected" = { fg = "shade07", bg = "light_blue" } @@ -108,11 +111,14 @@ "info" = "light_blue" "warning" = "orange" "error" = "red" + "diagnostic" = { modifiers = [] } "diagnostic.hint" = { underline = { color = "shade04", style = "line" } } "diagnostic.info" = { underline = { color = "light_blue", style = "line" } } "diagnostic.warning" = { underline = { color = "orange", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] shade00 = "#f2f3f7" diff --git a/runtime/themes/darcula.toml b/runtime/themes/darcula.toml index b704d60ee..7e0907c09 100644 --- a/runtime/themes/darcula.toml +++ b/runtime/themes/darcula.toml @@ -25,6 +25,7 @@ "ui.virtual.indent-guide" = "grey02" "ui.virtual.whitespace" = "grey03" "ui.virtual.inlay-hint" = { fg = "grey03", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "lightblue", modifiers = ["italic", "bold"] } "ui.bufferline" = { fg = "grey04", bg = "grey00" } "ui.bufferline.active" = { fg = "grey07", bg = "grey02" } @@ -76,6 +77,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl"} } "diagnostic.info" = { underline = { color = "grey05", style = "curl"} } "diagnostic.hint" = { underline = { color = "grey05", style = "curl"} } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "info" = "grey05" "hint" = "grey05" diff --git a/runtime/themes/dark_high_contrast.toml b/runtime/themes/dark_high_contrast.toml index 51701cfce..9e2328450 100644 --- a/runtime/themes/dark_high_contrast.toml +++ b/runtime/themes/dark_high_contrast.toml @@ -3,11 +3,14 @@ # Interface "ui.background" = { bg = "black" } "ui.window" = { fg = "aqua" } -"special" = "orange" # file picker fuzzy match +"special" = "orange" # file picker fuzzy match "ui.background.separator" = { fg = "white" } "ui.text" = "white" "ui.text.focus" = { modifiers = ["reversed"] } # file picker selected +"ui.virtual.jump-label" = { fg = "deep_red", modifiers = [ + "bold", +], underline = { color = "deep_red", style = "line" } } "ui.virtual" = "gray" "ui.virtual.whitespace" = "gray" "ui.virtual.ruler" = { fg = "white", bg = "gray" } @@ -54,14 +57,16 @@ "diagnostic.hint" = { underline = { color = "yellow", style = "dashed" } } "diagnostic.warning" = { underline = { color = "orange", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } -"info" = "white" +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"info" = "white" "hint" = "yellow" "warning" = "orange" "error" = "red" "debug" = "red" "diff.plus" = "green" -"diff.delta" ="blue" +"diff.delta" = "blue" "diff.minus" = "pink" # Syntax high light @@ -85,7 +90,9 @@ "label" = "blue" # Markup -"markup.heading" = { fg = "yellow", modifiers = ["bold"], underline = { color = "yellow", style = "double_line"} } +"markup.heading" = { fg = "yellow", modifiers = [ + "bold", +], underline = { color = "yellow", style = "double_line" } } "markup.list" = "pink" "markup.bold" = { fg = "emerald_green", modifiers = ["bold"] } "markup.italic" = { fg = "blue", modifiers = ["italic"] } @@ -105,6 +112,7 @@ dark_blue = "#0d1a2d" aqua = "#6fc3df" purple = "#c586c0" red = "#b65f5f" +deep_red = "#fd0004" pink = "#ff5c8d" orange = "#f38518" brown = "#ce9178" diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 819cd62aa..77b9b3e7c 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -1,105 +1,93 @@ -# Author: Shafkath Shuhan - -"namespace" = { fg = "type" } -"module" = { fg = "type" } - -"type" = { fg = "type" } -"type.builtin" = { fg = "type" } -"type.enum.variant" = { fg = "constant" } -"constructor" = { fg = "type" } -"variable.other.member" = { fg = "variable" } - -"keyword" = { fg = "blue2" } -"keyword.directive" = { fg = "blue2" } -"keyword.control" = { fg = "special" } -"label" = { fg = "blue2" } -"tag" = "blue2" - -"special" = { fg = "text" } -"operator" = { fg = "text" } - -"punctuation" = { fg = "text" } -"punctuation.delimiter" = { fg = "text" } - -"variable" = { fg = "variable" } -"variable.parameter" = { fg = "variable" } -"variable.builtin" = { fg = "blue2" } -"constant" = { fg = "constant" } -"constant.builtin" = { fg = "blue2" } - -"function" = { fg = "fn_declaration" } -"function.builtin" = { fg = "fn_declaration" } -"function.macro" = { fg = "blue2" } -"attribute" = { fg = "fn_declaration" } - -"comment" = { fg = "dark_green" } - -"string" = { fg = "orange" } -"constant.character" = { fg = "orange" } -"string.regexp" = { fg = "gold" } -"constant.numeric" = { fg = "pale_green" } -"constant.character.escape" = { fg = "gold" } - -"markup.heading" = { fg = "blue2", modifiers = ["bold"] } -"markup.list" = "blue3" -"markup.bold" = { fg = "blue2", modifiers = ["bold"] } -"markup.italic" = { modifiers = ["italic"] } +# Author: David Else <12832280+David-Else@users.noreply.github.com> + +# SYNTAX +"attribute" = "fn_declaration" +"comment" = "dark_green" +"constant" = "constant" +"constant.builtin" = "blue2" +"constant.character" = "orange" +"constant.character.escape" = "gold" +"constant.numeric" = "pale_green" +"constructor" = "type" +"diff.delta" = "blue4" +"diff.minus" = "orange_red" +"diff.plus" = "dark_green2" +"function" = "fn_declaration" +"function.builtin" = "fn_declaration" +"function.macro" = "blue2" +"keyword" = "blue2" +"keyword.control" = "special" +"keyword.directive" = "special" +"label" = "blue2" +"module" = "type" +"namespace" = "type" +"operator" = "text" +"punctuation" = "text" +"punctuation.delimiter" = "text" +"special" = "text" +"string" = "orange" +"string.regexp" = "gold" +"tag" = "blue2" +"type" = "type" +"type.builtin" = "type" +"type.enum.variant" = "constant" +"variable" = "variable" +"variable.builtin" = "blue2" +"variable.other.member" = "variable" +"variable.parameter" = "variable" + +# MARKUP +"markup.heading" = { fg = "blue2", modifiers = ["bold"] } +"markup.list" = "blue3" +"markup.bold" = { fg = "blue2", modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { modifiers = ["underlined"] } -"markup.link.text" = "orange" -"markup.quote" = "dark_green" -"markup.raw" = "orange" - -"diff.plus" = { fg = "dark_green2" } -"diff.delta" = { fg = "blue4" } -"diff.minus" = { fg = "orange_red" } - -"ui.background" = { fg = "light_gray", bg = "dark_gray2" } - -"ui.window" = { bg = "widget" } -"ui.popup" = { fg = "text", bg = "widget" } -"ui.help" = { fg = "text", bg = "widget" } -"ui.menu" = { fg = "text", bg = "widget" } -"ui.menu.selected" = { bg = "dark_blue2" } - +"markup.link.url" = { modifiers = ["underlined"] } +"markup.link.text" = "orange" +"markup.quote" = "dark_green" +"markup.raw" = "orange" + +# UI +"ui.background" = { fg = "light_gray", bg = "dark_gray2" } +"ui.window" = { bg = "widget" } +"ui.popup" = { fg = "text", bg = "widget" } +"ui.help" = { fg = "text", bg = "widget" } +"ui.menu" = { fg = "text", bg = "widget" } +"ui.menu.selected" = { bg = "dark_blue2" } # TODO: Alternate bg colour for `ui.cursor.match` and `ui.selection`. -"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] } -"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] } -"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] } - -"ui.selection" = { bg = "#3a3d41" } -"ui.selection.primary" = { bg = "dark_blue" } - -"ui.linenr" = { fg = "dark_gray" } -"ui.linenr.selected" = { fg = "light_gray2" } - -"ui.cursorline.primary" = { bg = "dark_gray3" } -"ui.statusline" = { fg = "white", bg = "blue" } -"ui.statusline.inactive" = { fg = "white", bg = "blue" } -"ui.statusline.insert" = { fg = "white", bg = "yellow" } -"ui.statusline.select" = { fg = "white", bg = "magenta" } - -"ui.bufferline" = { fg = "text", bg = "widget" } -"ui.bufferline.active" = { fg = "white", bg = "blue" } -"ui.bufferline.background" = { bg = "background" } - -"ui.text" = { fg = "text" } -"ui.text.focus" = { fg = "white" } - -"ui.virtual.whitespace" = { fg = "dark_gray" } -"ui.virtual.ruler" = { bg = "borders" } -"ui.virtual.indent-guide" = { fg = "dark_gray4" } -"ui.virtual.inlay-hint" = { fg = "dark_gray5"} - -"warning" = { fg = "gold2" } -"error" = { fg = "red" } -"info" = { fg = "light_blue" } -"hint" = { fg = "light_gray3" } - +"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] } +"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] } +"ui.selection" = { bg = "#3a3d41" } +"ui.selection.primary" = { bg = "dark_blue" } +"ui.linenr" = { fg = "dark_gray" } +"ui.linenr.selected" = { fg = "light_gray2" } +"ui.cursorline.primary" = { bg = "dark_gray3" } +"ui.statusline" = { fg = "white", bg = "blue" } +"ui.statusline.inactive" = { fg = "white", bg = "widget" } +"ui.statusline.insert" = { fg = "white", bg = "yellow" } +"ui.statusline.select" = { fg = "white", bg = "magenta" } +"ui.bufferline" = { fg = "text", bg = "widget" } +"ui.bufferline.active" = { fg = "white", bg = "blue" } +"ui.bufferline.background" = { bg = "background" } +"ui.text" = { fg = "text" } +"ui.text.focus" = { fg = "white" } +"ui.virtual.whitespace" = { fg = "#3e3e3d" } +"ui.virtual.ruler" = { bg = "borders" } +"ui.virtual.indent-guide" = { fg = "dark_gray4" } +"ui.virtual.inlay-hint" = { fg = "dark_gray5"} +"ui.virtual.jump-label" = { fg = "dark_gray", modifiers = ["bold"] } +"ui.highlight.frameline" = { bg = "#4b4b18" } +"ui.debug.active" = { fg = "#ffcc00" } +"ui.debug.breakpoint" = { fg = "#e51400" } +"warning" = { fg = "gold2" } +"error" = { fg = "red" } +"info" = { fg = "light_blue" } +"hint" = { fg = "light_gray3" } "diagnostic.error".underline = { color = "red", style = "curl" } -"diagnostic".underline = { color = "gold", style = "curl" } -"diagnostic.unnecessary" = { modifiers = ["dim"] } -"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"diagnostic".underline = { color = "gold", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] white = "#ffffff" diff --git a/runtime/themes/doom_acario_dark.toml b/runtime/themes/doom_acario_dark.toml index 47f1dfca8..5c4bd0dd8 100644 --- a/runtime/themes/doom_acario_dark.toml +++ b/runtime/themes/doom_acario_dark.toml @@ -78,6 +78,8 @@ 'diagnostic.error'= { underline = { color = 'red', style = "curl"} } 'diagnostic.info'= { underline = { color = 'blue', style = "curl"} } 'diagnostic.warning'= { underline = { color = 'yellow', style = "curl"} } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } 'special' = { fg = 'orange' } diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml index 32d9c8f5d..c1a9a12bc 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -26,6 +26,8 @@ "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "cyan", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "error" = { fg = "red" } "hint" = { fg = "purple" } @@ -120,6 +122,7 @@ "ui.virtual.inlay-hint" = { fg = "cyan" } "ui.virtual.inlay-hint.parameter" = { fg = "cyan", modifiers = ["italic", "dim"] } "ui.virtual.inlay-hint.type" = { fg = "cyan", modifiers = ["italic", "dim"] } +"ui.virtual.jump-label" = { fg = "pink", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "black" } "ui.virtual.whitespace" = { fg = "whitespace" } "ui.virtual.wrap" = { fg = "current_line" } diff --git a/runtime/themes/dracula_at_night.toml b/runtime/themes/dracula_at_night.toml index b2e3b9a9f..55f261539 100644 --- a/runtime/themes/dracula_at_night.toml +++ b/runtime/themes/dracula_at_night.toml @@ -43,11 +43,15 @@ "ui.text" = { fg = "foreground" } "ui.text.focus" = { fg = "cyan" } "ui.window" = { fg = "foreground" } +"ui.virtual.jump-label" = { fg = "pink", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "background_dark" } "error" = { fg = "red" } "warning" = { fg = "cyan" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } + "markup.heading" = { fg = "purple", modifiers = ["bold"] } "markup.list" = "cyan" "markup.bold" = { fg = "orange", modifiers = ["bold"] } diff --git a/runtime/themes/emacs.toml b/runtime/themes/emacs.toml index 513af60e1..c60afe99e 100644 --- a/runtime/themes/emacs.toml +++ b/runtime/themes/emacs.toml @@ -87,6 +87,8 @@ "diagnostic.warning" = { underline = { color = "dark_orange", style = "curl" } } "diagnostic.info" = { underline = { color = "forest_green", style = "curl" } } "diagnostic.hint" = { underline = { color = "dark_cyan", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] black = "#000000" diff --git a/runtime/themes/everblush.toml b/runtime/themes/everblush.toml index b49bf0494..ac0c2ae05 100644 --- a/runtime/themes/everblush.toml +++ b/runtime/themes/everblush.toml @@ -61,7 +61,7 @@ "ui.statusline.inactive" = { fg = "foreground", bg = "background" } "ui.statusline.normal" = { fg = "white", bg = "background" } "ui.statusline.insert" = { fg = "blue", bg = "background" } -"ui.statusline.select" = { fg = "cyan", bg = "magenta" } +"ui.statusline.select" = { fg = "magenta", bg = "background" } "ui.text" = { fg = "foreground" } "ui.text.focus" = { fg = "blue" } "ui.virtual.ruler" = { bg = "cursorline" } @@ -79,6 +79,8 @@ "diagnostic.warning" = { underline = { style = "curl", color = "yellow" } } "diagnostic.info" = { underline = { style = "curl", color = "blue" } } "diagnostic.hint" = { underline = { style = "curl", color = "green" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "special" = { fg = "red-light" } "markup.heading" = { fg = "blue", modifiers = ["bold"] } diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml index 25a491345..37b6756f5 100644 --- a/runtime/themes/everforest_dark.toml +++ b/runtime/themes/everforest_dark.toml @@ -40,6 +40,7 @@ "constructor" = "green" "module" = "yellow" "special" = "blue" +"ui.virtual.jump-label" = { fg = "#00dfff", modifiers = ["bold"] } "markup.heading.marker" = "grey1" "markup.heading.1" = { fg = "red", modifiers = ["bold"] } @@ -110,6 +111,8 @@ "diagnostic.info" = { underline = { color = "blue", style = "curl" } } "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] diff --git a/runtime/themes/everforest_light.toml b/runtime/themes/everforest_light.toml index cf39e23d6..b7448455c 100644 --- a/runtime/themes/everforest_light.toml +++ b/runtime/themes/everforest_light.toml @@ -110,6 +110,8 @@ "diagnostic.info" = { underline = { color = "blue", style = "curl" } } "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] diff --git a/runtime/themes/ferra.toml b/runtime/themes/ferra.toml index 0734f2407..212fb5105 100644 --- a/runtime/themes/ferra.toml +++ b/runtime/themes/ferra.toml @@ -68,6 +68,8 @@ "diagnostic.error" = { underline = { color = "ferra_ember", style = "curl" } } "diagnostic.info" = { underline = { color = "ferra_blush", style = "curl" } } "diagnostic.hint" = { underline = { color = "ferra_blush", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] ferra_night = "#2b292d" diff --git a/runtime/themes/flatwhite.toml b/runtime/themes/flatwhite.toml index d833d5c66..cc8f536cb 100644 --- a/runtime/themes/flatwhite.toml +++ b/runtime/themes/flatwhite.toml @@ -25,6 +25,8 @@ "diagnostic.info" = { underline = { color = "orange_text", style = "curl" } } "diagnostic.warning" = { underline = { color = "orange_text", style = "curl" } } "diagnostic.error" = { underline = { color = "diff_delete", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "info" = { fg = "orange_text", bg = "orange_bg" } "hint" = { modifiers = ["bold"] } diff --git a/runtime/themes/fleet_dark.toml b/runtime/themes/fleet_dark.toml index b5ad1aa3e..7fd26c1f8 100644 --- a/runtime/themes/fleet_dark.toml +++ b/runtime/themes/fleet_dark.toml @@ -105,6 +105,8 @@ "diagnostic.info" = { underline = { color = "#A366C4", style = "line" } } "diagnostic.warning" = { underline = { color = "#FACB66", style = "line" } } "diagnostic.error" = { underline = { color = "#FF5269", style = "line" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] "White" = "#ffffff" diff --git a/runtime/themes/flexoki_dark.toml b/runtime/themes/flexoki_dark.toml new file mode 100644 index 000000000..759f019b8 --- /dev/null +++ b/runtime/themes/flexoki_dark.toml @@ -0,0 +1,20 @@ +inherits = "flexoki_light" + +[palette] +tx = "#CECDC3" +tx-2 = "#878580" +tx-3 = "#575653" +ui-3 = "#403E3C" +ui-2 = "#343331" +ui = "#282726" +bg-2 = "#1C1B1A" +bg = "#100F0F" + +re = "#D14D41" +or = "#DA702C" +ye = "#D0A215" +gr = "#879A39" +cy = "#3AA99F" +bl = "#4385BE" +pu = "#8B7EC8" +ma = "#CE5D97" diff --git a/runtime/themes/flexoki_light.toml b/runtime/themes/flexoki_light.toml new file mode 100644 index 000000000..8badd9222 --- /dev/null +++ b/runtime/themes/flexoki_light.toml @@ -0,0 +1,98 @@ +# Based on Flexoki: https://stephango.com/flexoki + +"ui.background" = { bg = "bg" } +"ui.cursor" = { fg = "tx", bg = "tx-3" } +"ui.cursor.primary" = { fg = "bg", bg = "tx" } +"ui.cursor.match" = { modifiers = ["bold"] } +"ui.linenr" = "tx-3" +"ui.linenr.selected" = "tx" +"ui.selection" = { bg = "ui-2" } +"ui.statusline" = { fg = "tx", bg = "bg-2" } +"ui.statusline.normal" = { fg = "bg", bg = "bl" } +"ui.statusline.insert" = { fg = "bg", bg = "or" } +"ui.statusline.select" = { fg = "bg", bg = "ma" } +"ui.cursorline" = { bg = "bg-2" } +"ui.popup" = { fg = "tx", bg = "bg-2" } +"ui.window" = "tx" +"ui.help" = { fg = "tx", bg = "bg" } +"ui.text" = "tx" +"ui.text.focus" = { bg = "bg-2", fg = "tx" } +"ui.text.info" = "tx" +"ui.virtual.whitespace" = "bg-2" +"ui.virtual.ruler" = { bg = "bg-2" } +"ui.virtual.inlay-hint" = { fg = "tx-3", bg = "bg" } +"ui.virtual.jump-label" = { bg = "bg-2", modifiers = ["bold"]} +"ui.menu" = { fg = "tx", bg = "bg" } +"ui.menu.selected" = { bg = "ui", fg = "tx" } +"ui.debug" = { fg = "or", bg = "bg" } +"ui.highlight.frameline" = { bg = "ye" } +"diagnostic.hint" = { underline = { color = "bl", style = "curl" } } +"diagnostic.info" = { underline = { color = "bl", style = "curl" } } +"diagnostic.warning" = { underline = { color = "ye", style = "curl" } } +"diagnostic.error" = { underline = { color = "re", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"hint" = { fg = "bl", modifiers = ["bold"] } +"info" = { fg = "ye", modifiers = ["bold"] } +"warning" = { fg = "or", modifiers = ["bold"] } +"error" = { fg = "re", modifiers = ["bold"] } + +"attribute" = "ye" +"type" = "ye" +"constructor" = "gr" +"constant" = "pu" +"constant.builtin" = "ye" +"string" = "cy" +"string.regexp" = "or" +"string.special" = "ye" +"comment" = "tx-3" +"variable" = "tx" +"variable.builtin" = "ma" +"variable.other" = "bl" +"punctuation" = "tx-2" +"keyword" = "gr" +"keyword.control" = "re" +"keyword.control.conditional" = "gr" +"keyword.control.import" = "ye" +"keyword.control.return" = "gr" +"keyword.function" = "gr" +"keyword.storage.type" = "bl" +"keyword.storage.modifier" = "bl" +"operator" = "tx-2" +"function" = "or" +"tag" = "bl" +"namespace" = "re" + +"markup.heading" = "or" +"markup.list" = "ye" +"markup.bold" = { fg = "or", modifiers = ["bold"] } +"markup.italic" = { fg = "or", modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.raw.block" = "or" +"markup.link.url" = "bl" +"markup.link.text" = "ye" +"markup.link.label" = "gr" +"markup.quote" = "ye" +"markup.raw" = "bl" +"diff.plus" = "gr" +"diff.minus" = "re" +"diff.delta" = "ye" + +[palette] +tx = "#100F0F" +tx-2 = "#6F6E69" +tx-3 = "#B7B5AC" +ui-3 = "#CECDC3" +ui-2 = "#DAD8CE" +ui = "#E6E4D9" +bg-2 = "#F2F0E5" +bg = "#FFFCF0" + +re = "#AF3029" +or = "#BC5215" +ye = "#AD8301" +gr = "#66800B" +cy = "#24837B" +bl = "#205EA6" +pu = "#5E409D" +ma = "#A02F6F" diff --git a/runtime/themes/github_dark.toml b/runtime/themes/github_dark.toml index 6b3354848..dafba559d 100644 --- a/runtime/themes/github_dark.toml +++ b/runtime/themes/github_dark.toml @@ -78,6 +78,8 @@ label = "scale.red.3" "diagnostic.info" = { underline = { color = "accent.fg", style = "curl" } } "diagnostic.warning" = { underline = { color = "attention.fg", style = "curl" } } "diagnostic.error" = { underline = { color = "danger.fg", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } hint = "success.fg" info = "accent.fg" diff --git a/runtime/themes/github_light.toml b/runtime/themes/github_light.toml index e6912a987..cd3f1e5d1 100644 --- a/runtime/themes/github_light.toml +++ b/runtime/themes/github_light.toml @@ -78,6 +78,8 @@ label = "scale.red.5" "diagnostic.info" = { underline = { color = "accent.fg", style = "curl" } } "diagnostic.warning" = { underline = { color = "attention.fg", style = "curl" } } "diagnostic.error" = { underline = { color = "danger.fg", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } hint = "success.fg" info = "accent.fg" diff --git a/runtime/themes/gruber-darker.toml b/runtime/themes/gruber-darker.toml index 355a3be83..4737f52f7 100644 --- a/runtime/themes/gruber-darker.toml +++ b/runtime/themes/gruber-darker.toml @@ -72,6 +72,8 @@ "diagnostic.error" = { underline = { color = "red3", style = "dashed" } } "diagnostic.info" = { underline = { color = "aqua1", style = "dashed" } } "diagnostic.hint" = { underline = { color = "blue0", style = "dashed" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "markup.heading" = { fg = "aqua1", modifiers = ["bold"] } "markup.bold" = { modifiers = ["bold"] } diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index e3eef1855..67ca066f7 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -33,7 +33,7 @@ "diff.delta" = "orange1" "diff.minus" = "red1" -"warning" = "orange1" +"warning" = "yellow1" "error" = "red1" "info" = "aqua1" "hint" = "blue1" @@ -65,11 +65,14 @@ "ui.virtual.ruler" = { bg = "bg1" } "ui.virtual.inlay-hint" = { fg = "gray1" } "ui.virtual.wrap" = { fg = "bg2" } +"ui.virtual.jump-label" = { fg = "purple0", modifiers = ["bold"] } -"diagnostic.warning" = { underline = { color = "orange1", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow1", style = "curl" } } "diagnostic.error" = { underline = { color = "red1", style = "curl" } } "diagnostic.info" = { underline = { color = "aqua1", style = "curl" } } "diagnostic.hint" = { underline = { color = "blue1", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "markup.heading" = "aqua1" "markup.bold" = { modifiers = ["bold"] } diff --git a/runtime/themes/heisenberg.toml b/runtime/themes/heisenberg.toml index 3c127a3e4..9f8ca1832 100644 --- a/runtime/themes/heisenberg.toml +++ b/runtime/themes/heisenberg.toml @@ -63,6 +63,8 @@ "diagnostic.info" = { underline = {color = "crystal_blue", style = "curl" } } "diagnostic.warning" = { underline = {color = "vapor_yellow", style = "curl" } } "diagnostic.error" = { underline = {color = "chili_powder_red", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.bufferline" = { fg = "gray", bg = "background" } "ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" } diff --git a/runtime/themes/hex_steel.toml b/runtime/themes/hex_steel.toml index c81df4097..fb4fc1fd8 100644 --- a/runtime/themes/hex_steel.toml +++ b/runtime/themes/hex_steel.toml @@ -64,6 +64,8 @@ "diagnostic.warning" = { underline = { color = "warning", style = "curl" } } "diagnostic.info" = { underline = { color = "info", style = "curl" } } "diagnostic.hint" = { underline = { color = "display", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "error" = { fg = "error", modifiers = ["bold"] } "warning" = { fg = "warning", modifiers = ["bold"] } diff --git a/runtime/themes/horizon-dark.toml b/runtime/themes/horizon-dark.toml index 2ab10e911..e51ec09f5 100644 --- a/runtime/themes/horizon-dark.toml +++ b/runtime/themes/horizon-dark.toml @@ -4,15 +4,19 @@ constant = "purple" "constant.numeric" = "orange" "constant.builtin" = "orange" variable = "red" +attribute = "brown" comment = "light-gray" special = "purple" +"punctuation" = "red" "punctuation.bracket" = "purple" +"punctuation.delimiter" = "white" keyword = "purple" function = "blue" label = "orange" type = "orange" constructor = "orange" namespace = "orange" +tag = "red" # User Interface "ui.background" = { bg = "bg", fg = "gray" } @@ -27,8 +31,9 @@ namespace = "orange" "ui.gutter" = "gray" "ui.gutter.selected" = "light-gray" "ui.selection" = { bg = "selection" } -"ui.virtual.indent-guide" = { fg = "gray"} -"ui.virtual.whitespace" = { fg = "light-gray"} +"ui.virtual.indent-guide" = { fg = "gray" } +"ui.virtual.whitespace" = { fg = "light-gray" } +"ui.virtual.ruler" = { bg ="dark-bg" } "ui.statusline" = { bg = "dark-bg", fg = "light-gray" } "ui.popup" = { bg = "dark-bg", fg = "orange" } "ui.help" = { bg = "dark-bg", fg = "orange" } @@ -37,13 +42,16 @@ namespace = "orange" "ui.window" = "selection" "ui.bufferline" = { bg = "dark-bg", fg = "light-gray" } "ui.bufferline.active" = { bg = "dark-bg", fg = "orange" } +"ui.virtual.jump-label" = { fg = "pink", modifiers = ["bold"] } # Diagnostics "diagnostic" = { underline = { style = "curl" } } "diagnostic.hint" = { underline = { color = "green", style = "curl" } } "diagnostic.info" = { underline = { color = "blue", style = "curl" } } -"diagnostics.error" = { underline = { color = "red", style = "curl"} } -"diagnostics.warning" = { underline = { color = "orange", style = "curl"} } +"diagnostics.error" = { underline = { color = "red", style = "curl" } } +"diagnostics.warning" = { underline = { color = "orange", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } warning = { fg = "orange", modifiers = ["bold"] } error = { fg = "red", modifiers = ["bold"] } @@ -62,7 +70,7 @@ hint = { fg = "green", modifiers = ["bold"] } "markup.quote" = "orange" "markup.raw" = "orange" -"diff.plus" = "green" +"diff.plus" = "green" "diff.minus" = "red" "diff.delta" = "orange" @@ -76,6 +84,7 @@ pink = "#EE64AE" selection = "#353747" green = "#27D796" orange = "#FAB795" +brown = "#F09383" purple = "#B877DB" red = "#E95678" blue = "#25B2BC" diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml index 8942c3ca2..975f7a733 100644 --- a/runtime/themes/ingrid.toml +++ b/runtime/themes/ingrid.toml @@ -73,3 +73,5 @@ "diagnostic.error" = { underline = { color = "#D74E50", style = "curl" } } "diagnostic.info" = { underline = { color = "#839A53", style = "curl" } } "diagnostic.hint" = { underline = { color = "#A6B6CE", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } diff --git a/runtime/themes/jellybeans.toml b/runtime/themes/jellybeans.toml index 55088c298..6f8c326f4 100644 --- a/runtime/themes/jellybeans.toml +++ b/runtime/themes/jellybeans.toml @@ -99,6 +99,8 @@ "diagnostic.info" = { underline = { color = "blue_accent", style = "line" } } "diagnostic.warning" = { underline = { color = "yellow_accent", style = "line" } } "diagnostic.error" = { underline = { color = "red_error", style = "line" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] background = "#151515" diff --git a/runtime/themes/jetbrains_dark.toml b/runtime/themes/jetbrains_dark.toml new file mode 100644 index 000000000..c73fe93af --- /dev/null +++ b/runtime/themes/jetbrains_dark.toml @@ -0,0 +1,99 @@ +"type.builtin" = "red207" +"type.enum" = "red199" +"constant.builtin" = "red207" +"constant.numeric" = "blue184" +string = "green171" +comment = "blue133" +"comment.block.documentation" = "green130" +"variable.builtin" = "red207" +"variable.other.member" = "red199" +label = "red199" +keyword = "red207" +function = "blue245" +"function.macro" = "red179" +tag = "red213" + +"markup.heading" = { fg = "red199", modifiers = ["italic"] } +"markup.heading.marker" = "red207" +"markup.list" = "red207" +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link" = { fg = "blue247", underline = { style = "line" } } +"markup.raw" = { bg = "blue64" } + +"diff.plus" = "green145" +"diff.minus" = "blue145" +"diff.delta" = "blue173" + +"ui.background" = { fg = "blue196", bg = "blue34" } +"ui.gutter" = "blue56" +"ui.gutter.selected" = { fg ="blue56", bg = "blue46" } +"ui.linenr" = "blue89" +"ui.linenr.selected" = "blue171" +"ui.statusline" = { bg = "blue48" } +"ui.statusline.inactive" = "blue89" +"ui.statusline.normal" = { fg = "blue48", bg = "green156", modifiers = ["bold"] } +"ui.statusline.insert" = { fg = "blue48", bg = "blue247", modifiers = ["bold"] } +"ui.statusline.select" = { fg = "blue48", bg = "red194", modifiers = ["bold"] } +"ui.popup" = { bg = "blue48" } +"ui.text.focus" = "blue247" +"ui.virtual" = "blue122" +"ui.virtual.ruler" = { bg = "blue64" } +"ui.virtual.whitespace" = "blue122" +"ui.virtual.indent-guide" = "blue56" +"ui.virtual.inlay-hint" = { fg = "blue145", bg = "blue50" } +"ui.menu" = { bg = "blue48" } +"ui.menu.selected" = { bg = "blue110" } +"ui.menu.scroll" = { fg = "blue81", bg = "blue48" } +"ui.selection" = { bg = "blue131" } +"ui.cursorline.primary" = { bg = "blue46" } +"ui.cursorline.secondary" = { bg = "blue46" } +"ui.cursorcolumn.primary" = { bg = "blue46" } +"ui.cursorcolumn.secondary" = { bg = "blue46" } + +warning = "red194" +error = "red214" +info = "blue247" +hint = "green156" + +"diagnostic" = { underline = { style = "curl" } } +"diagnostic.hint" = { underline = { color = "green156", style = "curl" } } +"diagnostic.info" = { underline = { color = "blue247", style = "curl" } } +"diagnostic.warning" = { underline = { color = "red194", style = "curl" } } +"diagnostic.error" = { underline = { color = "red214", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } + +[palette] +red179 = "#b3ae60" +red194 = "#c29e4a" +red199 = "#c77dbb" +red207 = "#cf8e6d" +red213 = "#d5b778" +red214 = "#d64d5b" + +green130 = "#5f826b" +green145 = "#549159" +green156 = "#499c54" +green171 = "#6aab73" + +blue34 = "#1e1f22" +blue46 = "#26282e" +blue48 = "#2b2d30" +blue50 = "#2d2e32" +blue56 = "#313438" +blue64 = "#293c40" +blue81 = "#4d4e51" +blue89 = "#4b5059" +blue110 = "#2e436e" +blue122 = "#6f737a" +blue131 = "#214283" +blue133 = "#7a7e85" +blue145 = "#868a91" +blue171 = "#A1A3AB" +blue173 = "#375fad" +blue184 = "#2aacb8" +blue196 = "#bcbec4" +blue245 = "#56a8f5" +blue247 = "#57aaf7" diff --git a/runtime/themes/kanagawa.toml b/runtime/themes/kanagawa.toml index d0576a4e3..02b5271ec 100644 --- a/runtime/themes/kanagawa.toml +++ b/runtime/themes/kanagawa.toml @@ -8,7 +8,7 @@ ## User interface "ui.selection" = { bg = "waveBlue2" } -"ui.selection.primary" = { bg = "waveBlue1" } +"ui.selection.primary" = { bg = "waveBlue2" } "ui.background" = { fg = "fujiWhite", bg = "sumiInk1" } "ui.linenr" = { fg = "sumiInk4" } @@ -19,6 +19,7 @@ "ui.virtual.inlay-hint" = "fujiGray" "ui.virtual.inlay-hint.parameter" = { fg = "carpYellow", modifiers = ["dim"] } "ui.virtual.inlay-hint.type" = { fg = "waveAqua2", modifiers = ["dim"] } +"ui.virtual.jump-label" = { fg = "peachRed", modifiers = ["bold"] } "ui.statusline" = { fg = "oldWhite", bg = "sumiInk0" } "ui.statusline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } @@ -36,7 +37,7 @@ "ui.text" = "fujiWhite" "ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } -"ui.cursor" = { fg = "waveBlue1", bg = "waveAqua2"} +"ui.cursor" = { fg = "waveBlue1", bg = "waveAqua2" } "ui.cursor.primary" = { fg = "waveBlue1", bg = "fujiWhite" } "ui.cursor.match" = { fg = "waveRed", modifiers = ["bold"] } "ui.highlight" = { fg = "fujiWhite", bg = "waveBlue2" } @@ -44,13 +45,15 @@ "ui.menu.selected" = { fg = "fujiWhite", bg = "waveBlue2", modifiers = ["bold"] } "ui.menu.scroll" = { fg = "oldWhite", bg = "waveBlue1" } -"ui.cursorline.primary" = { bg = "sumiInk3"} +"ui.cursorline.primary" = { bg = "sumiInk3" } "ui.cursorcolumn.primary" = { bg = "sumiInk3" } "diagnostic.error" = { underline = { color = "samuraiRed", style = "curl" } } "diagnostic.warning" = { underline = { color = "roninYellow", style = "curl" } } "diagnostic.info" = { underline = { color = "waveAqua1", style = "curl" } } "diagnostic.hint" = { underline = { color = "dragonBlue", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } error = "samuraiRed" warning = "roninYellow" @@ -112,41 +115,41 @@ hint = "dragonBlue" "markup.raw" = "springGreen" [palette] -seaFoam = "#C7CCD1" # custom lighter foreground -fujiWhite = "#DCD7BA" # default foreground -oldWhite = "#C8C093" # dark foreground, e.g. statuslines -sumiInk0 = "#16161D" # dark background, e.g. statuslines, floating windows -sumiInk1 = "#1F1F28" # default background -sumiInk2 = "#2A2A37" # lighter background, e.g. colorcolumns, folds -sumiInk3 = "#363646" # lighter background, e.g. cursorline -sumiInk4 = "#54546D" # darker foreground, e.g. linenumbers, fold column -waveBlue1 = "#223249" # popup background, visual selection background -waveBlue2 = "#2D4F67" # popup selection background, search background -winterGreen = "#2B3328" # diff add background -winterYellow = "#49443C" # diff change background -winterRed = "#43242B" # diff deleted background -winterBlue = "#252535" # diff line background -autumnGreen = "#76946A" # git add -autumnRed = "#C34043" # git delete -autumnYellow = "#DCA561" # git change -samuraiRed = "#E82424" # diagnostic error -roninYellow = "#FF9E3B" # diagnostic warning -waveAqua1 = "#6A9589" # diagnostic info -dragonBlue = "#658594" # diagnostic hint -fujiGray = "#727169" # comments +seaFoam = "#C7CCD1" # custom lighter foreground +fujiWhite = "#DCD7BA" # default foreground +oldWhite = "#C8C093" # dark foreground, e.g. statuslines +sumiInk0 = "#16161D" # dark background, e.g. statuslines, floating windows +sumiInk1 = "#1F1F28" # default background +sumiInk2 = "#2A2A37" # lighter background, e.g. colorcolumns, folds +sumiInk3 = "#363646" # lighter background, e.g. cursorline +sumiInk4 = "#54546D" # darker foreground, e.g. linenumbers, fold column +waveBlue1 = "#223249" # popup background, visual selection background +waveBlue2 = "#2D4F67" # popup selection background, search background +winterGreen = "#2B3328" # diff add background +winterYellow = "#49443C" # diff change background +winterRed = "#43242B" # diff deleted background +winterBlue = "#252535" # diff line background +autumnGreen = "#76946A" # git add +autumnRed = "#C34043" # git delete +autumnYellow = "#DCA561" # git change +samuraiRed = "#E82424" # diagnostic error +roninYellow = "#FF9E3B" # diagnostic warning +waveAqua1 = "#6A9589" # diagnostic info +dragonBlue = "#658594" # diagnostic hint +fujiGray = "#727169" # comments springViolet1 = "#938AA9" # light foreground -oniViolet = "#957FB8" # statements and keywords -crystalBlue = "#7E9CD8" # functions and titles +oniViolet = "#957FB8" # statements and keywords +crystalBlue = "#7E9CD8" # functions and titles springViolet2 = "#9CABCA" # brackets and punctuation -springBlue = "#7FB4CA" # specials and builtins -lightBlue = "#A3D4D5" # not used! -waveAqua2 = "#7AA89F" # types -springGreen = "#98BB6C" # strings -boatYellow1 = "#938056" # not used -boatYellow2 = "#C0A36E" # operators, regex -carpYellow = "#E6C384" # identifiers -sakuraPink = "#D27E99" # numbers -waveRed = "#E46876" # standout specials 1, e.g. builtin variables -peachRed = "#FF5D62" # standout specials 2, e.g. exception handling, returns -surimiOrange = "#FFA066" # constants, imports, booleans -katanaGray = "#717C7C" # deprecated +springBlue = "#7FB4CA" # specials and builtins +lightBlue = "#A3D4D5" # not used! +waveAqua2 = "#7AA89F" # types +springGreen = "#98BB6C" # strings +boatYellow1 = "#938056" # not used +boatYellow2 = "#C0A36E" # operators, regex +carpYellow = "#E6C384" # identifiers +sakuraPink = "#D27E99" # numbers +waveRed = "#E46876" # standout specials 1, e.g. builtin variables +peachRed = "#FF5D62" # standout specials 2, e.g. exception handling, returns +surimiOrange = "#FFA066" # constants, imports, booleans +katanaGray = "#717C7C" # deprecated diff --git a/runtime/themes/kaolin-dark.toml b/runtime/themes/kaolin-dark.toml index b8188f769..d8c17391c 100644 --- a/runtime/themes/kaolin-dark.toml +++ b/runtime/themes/kaolin-dark.toml @@ -44,6 +44,8 @@ "diagnostic.warning" = { underline = { style = "curl", color = "warn" } } "diagnostic.info" = { underline = { style = "curl", color = "info" } } "diagnostic.hint" = { underline = { style = "curl", color = "hint" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.background" = { bg = "bg0" } "ui.linenr" = { fg = "gray0" } diff --git a/runtime/themes/material_darker.toml b/runtime/themes/material_darker.toml index f117f7194..52cea1d4c 100644 --- a/runtime/themes/material_darker.toml +++ b/runtime/themes/material_darker.toml @@ -13,6 +13,7 @@ disabled = "#474747" accent = "#ff9800" +active = "#323232" highlight = "#3f3f3f" comment = "#616161" diff --git a/runtime/themes/material_deep_ocean.toml b/runtime/themes/material_deep_ocean.toml index b98a32e55..8b1e3c861 100644 --- a/runtime/themes/material_deep_ocean.toml +++ b/runtime/themes/material_deep_ocean.toml @@ -63,9 +63,9 @@ "ui.statusline" = { bg = "bg", fg = "text" } "ui.statusline.inactive" = { bg = "bg", fg = "disabled" } -"ui.statusline.normal" = { bg = "accent", fg = "text" } -"ui.statusline.insert" = { bg = "green", fg = "text" } -"ui.statusline.select" = { bg = "purple", fg = "text" } +"ui.statusline.normal" = { bg = "blue", fg = "bg" } +"ui.statusline.insert" = { bg = "green", fg = "bg" } +"ui.statusline.select" = { bg = "purple", fg = "bg" } "ui.selection" = { bg = "selection" } @@ -76,7 +76,7 @@ "ui.cursor" = { bg = "highlight", fg = "white" } "ui.cursor.primary" = { bg = "white", fg = "gray" } -"ui.cursorline.primary" = { bg = "white" } +"ui.cursorline.primary" = { bg = "active" } "ui.virtual" = { fg = "gray" } "ui.virtual.ruler" = { bg = "highlight" } @@ -85,16 +85,28 @@ "ui.highlight" = { bg = "highlight" } "ui.menu" = { bg = "highlight", fg = "text" } +"ui.menu.selected" = { bg = "blue", fg = "bg" } "ui.help" = { bg = "highlight", fg = "text" } "ui.popup" = { bg = "highlight", fg = "text" } +"ui.virtual.jump-label" = { fg = "purple", modifiers = ["bold"] } + warning = "yellow" error = "error" info = "blue" hint = "purple" +"diagnostic" = { underline = { color = "error", style = "curl" } } +"diagnostic.info" = { underline = { color = "blue", style = "curl" } } +"diagnostic.hint" = { underline = { color = "purple", style = "curl" } } +"diagnostic.error" = { underline = { color = "error", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"]} +"diagnostic.deprecated" = { modifiers = ["crossed_out"]} + + [palette] bg = "#0f111a" text = "#a6accd" @@ -114,6 +126,7 @@ disabled = "#464b5d" accent = "#84ffff" +active = "#1a1c25" highlight = "#1f2233" comment = "#464b5d" diff --git a/runtime/themes/material_oceanic.toml b/runtime/themes/material_oceanic.toml index ea4930d30..fde54d7d4 100644 --- a/runtime/themes/material_oceanic.toml +++ b/runtime/themes/material_oceanic.toml @@ -12,10 +12,11 @@ disabled = "#415967" accent = "#009688" +active = "#314549" highlight = "#425b67" comment = "#546e7a" selection = "#395b65" -line-number = "#355058" \ No newline at end of file +line-number = "#355058" diff --git a/runtime/themes/material_palenight.toml b/runtime/themes/material_palenight.toml index ef9c27e26..f34517df2 100644 --- a/runtime/themes/material_palenight.toml +++ b/runtime/themes/material_palenight.toml @@ -10,10 +10,11 @@ disabled = "#515772" accent = "#ab47bc" +active = "#414863" highlight = "#444267" comment = "#676e95" selection = "#444267" -line-number = "#3a3f58" \ No newline at end of file +line-number = "#3a3f58" diff --git a/runtime/themes/meliora.toml b/runtime/themes/meliora.toml index e7037e18d..87f0cb092 100644 --- a/runtime/themes/meliora.toml +++ b/runtime/themes/meliora.toml @@ -58,6 +58,8 @@ "diagnostic.warning" = { underline = { color = "orange", style = "curl" } } "diagnostic.info" = { underline = { color = "blue", style = "curl" } } "diagnostic.hint" = { underline = { color = "magenta", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "error" = { fg = "red" } "warning" = { fg = "orange" } diff --git a/runtime/themes/mellow.toml b/runtime/themes/mellow.toml index 16d7b6086..db237f977 100644 --- a/runtime/themes/mellow.toml +++ b/runtime/themes/mellow.toml @@ -79,6 +79,7 @@ "ui.text.focus" = { fg = "fg" } "ui.virtual" = { fg = "gray02" } +"ui.virtual.ruler" = { bg ="gray02" } "ui.virtual.indent-guide" = { fg = "gray02" } "ui.virtual.inlay-hint" = { fg = "gray04" } @@ -99,6 +100,8 @@ "diagnostic.error" = { underline = { color = "bright_red", style = "curl" } } "diagnostic.info" = { underline = { color = "bright_blue", style = "curl" } } "diagnostic.hint" = { underline = { color = "bright_cyan", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } warning = "bright_yellow" error = "bright_red" diff --git a/runtime/themes/merionette.toml b/runtime/themes/merionette.toml index e4d5d6737..d8e7d7703 100644 --- a/runtime/themes/merionette.toml +++ b/runtime/themes/merionette.toml @@ -63,6 +63,8 @@ "diagnostic.hint" = { underline = { color = "white0", style = "double_line" } } "diagnostic.info" = { underline = { color = "blue0", style = "double_line" } } "diagnostic.warning" = { underline = { color = "green1", style = "double_line" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "markup.bold" = { modifiers = ["bold"] } "markup.heading" = { fg = "orange1", modifiers = ["bold"] } diff --git a/runtime/themes/modus_operandi.toml b/runtime/themes/modus_operandi.toml index ad4b3c85a..362f97778 100644 --- a/runtime/themes/modus_operandi.toml +++ b/runtime/themes/modus_operandi.toml @@ -80,6 +80,7 @@ punctuation = "fg-dim" "ui.virtual" = "bg-active" "ui.virtual.ruler" = { bg = "bg-dim" } "ui.virtual.inlay-hint" = { fg = "fg-dim", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "yellow-cooler", modifiers = ["bold"] } "ui.selection" = { fg = "fg-main", bg = "bg-inactive" } "ui.selection.primary" = { fg = "fg-main", bg = "bg-active" } @@ -99,6 +100,8 @@ punctuation = "fg-dim" "diagnostic.warning" = { underline = { color = "yellow-intense", style = "curl" } } "diagnostic.info" = { underline = { color = "cyan-intense", style = "curl" } } "diagnostic.hint" = { underline = { color = "blue-intense", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } error = "red" warning = "yellow-warmer" diff --git a/runtime/themes/modus_vivendi.toml b/runtime/themes/modus_vivendi.toml index a44fbe0f7..952683ee7 100644 --- a/runtime/themes/modus_vivendi.toml +++ b/runtime/themes/modus_vivendi.toml @@ -83,6 +83,7 @@ punctuation = "fg-dim" "ui.virtual" = "bg-active" "ui.virtual.ruler" = { bg = "bg-dim" } "ui.virtual.inlay-hint" = { fg = "fg-dim", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "yellow-cooler", modifiers = ["bold"] } "ui.selection" = { fg = "fg-main", bg = "bg-inactive" } "ui.selection.primary" = { fg = "fg-main", bg = "bg-active" } @@ -102,6 +103,8 @@ punctuation = "fg-dim" "diagnostic.warning" = { underline = { color = "yellow-intense", style = "curl" } } "diagnostic.info" = { underline = { color = "cyan-intense", style = "curl" } } "diagnostic.hint" = { underline = { color = "blue-intense", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } error = "red" warning = "yellow-warmer" diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index 66106d480..b0faac154 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -34,6 +34,7 @@ "comment" = { fg = "#88846F" } "ui.virtual.whitespace" = "#88846F" "ui.virtual.ruler" = { bg = "#24241e" } +"ui.virtual.jump-label" = { fg = "special", modifiers = ["bold"] } "string" = { fg = "#e6db74" } "constant.character" = { fg = "#e6db74" } @@ -82,6 +83,7 @@ "ui.bufferline.active" = { fg = "active_text", bg = "selection", modifiers = [ "bold", ] } +"ui.virtual.inlay-hint" = { fg = "#88846F" } "ui.text" = { fg = "text" } "ui.text.focus" = { fg = "active_text" } @@ -95,6 +97,8 @@ "diagnostic.error" = { underline = { color = "#f48771", style = "curl" } } "diagnostic.info" = { underline = { color = "#75beff", style = "curl" } } "diagnostic.hint" = { underline = { color = "#eeeeb3", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] type = "#A6E22E" diff --git a/runtime/themes/monokai_aqua.toml b/runtime/themes/monokai_aqua.toml index 0fe482546..0cb9300d0 100644 --- a/runtime/themes/monokai_aqua.toml +++ b/runtime/themes/monokai_aqua.toml @@ -12,6 +12,8 @@ inherits = "monokai" "ui.statusline.insert" = { fg = "light-black", bg = "green" } "ui.statusline.select" = { fg = "light-black", bg = "purple" } +"ui.virtual.jump-label" = { fg = "cyan", modifiers = ["bold"] } + [palette] cyan = "#66D9EF" type = "#66D9EF" diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml index a898671f6..97f2a57c5 100644 --- a/runtime/themes/monokai_pro.toml +++ b/runtime/themes/monokai_pro.toml @@ -8,6 +8,7 @@ "ui.menu.selected" = { fg = "base2", bg = "yellow" } "ui.virtual.whitespace" = "base5" "ui.virtual.ruler" = { bg = "base1" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } "info" = "base8" "hint" = "base8" @@ -99,6 +100,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "base8", style = "curl" } } "diagnostic.hint" = { underline = { color = "base8", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # markup highlight, no need for `markup.raw` and `markup.list`, make them to be default "markup.heading" = "green" diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml index 519c9ed39..e1f74b1cb 100644 --- a/runtime/themes/monokai_pro_machine.toml +++ b/runtime/themes/monokai_pro_machine.toml @@ -8,6 +8,7 @@ "ui.menu.selected" = { fg = "base2", bg = "yellow" } "ui.virtual.whitespace" = "base5" "ui.virtual.ruler" = { bg = "base1" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } "info" = "base8" "hint" = "base8" @@ -96,6 +97,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "base8", style = "curl" } } "diagnostic.hint" = { underline = { color = "base8", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # markup highlight, no need for `markup.raw` and `markup.list`, make them to be default "markup.heading" = "green" diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml index 9be6bdc00..cfa85b651 100644 --- a/runtime/themes/monokai_pro_octagon.toml +++ b/runtime/themes/monokai_pro_octagon.toml @@ -8,6 +8,7 @@ "ui.menu.selected" = { fg = "base2", bg = "yellow" } "ui.virtual.whitespace" = "base5" "ui.virtual.ruler" = { bg = "base1" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } "info" = "base8" "hint" = "base8" @@ -99,6 +100,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "base8", style = "curl" } } "diagnostic.hint" = { underline = { color = "base8", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # markup highlight, no need for `markup.raw` and `markup.list`, make them to be default "markup.heading" = "green" diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml index d002d13fa..a6ba6fcf3 100644 --- a/runtime/themes/monokai_pro_ristretto.toml +++ b/runtime/themes/monokai_pro_ristretto.toml @@ -8,6 +8,7 @@ "ui.menu.selected" = { fg = "base2", bg = "yellow" } "ui.virtual.whitespace" = "base5" "ui.virtual.ruler" = { bg = "base1" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } "info" = "base8" "hint" = "base8" @@ -96,6 +97,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "base8", style = "curl" } } "diagnostic.hint" = { underline = { color = "base8", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # markup highlight, no need for `markup.raw` and `markup.list`, make them to be default "markup.heading" = "green" diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml index 3cec2f1a3..1bfcb6889 100644 --- a/runtime/themes/monokai_pro_spectrum.toml +++ b/runtime/themes/monokai_pro_spectrum.toml @@ -8,6 +8,7 @@ "ui.menu.selected" = { fg = "base2", bg = "yellow" } "ui.virtual.whitespace" = "base4" "ui.virtual.ruler" = { bg = "base1" } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } "info" = "base8" "hint" = "base8" @@ -96,6 +97,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "base8", style = "curl" } } "diagnostic.hint" = { underline = { color = "base8", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # markup highlight, no need for `markup.raw` and `markup.list`, make them to be default "markup.heading" = "green" diff --git a/runtime/themes/monokai_soda.toml b/runtime/themes/monokai_soda.toml index 7e43247e3..ca36d4e93 100644 --- a/runtime/themes/monokai_soda.toml +++ b/runtime/themes/monokai_soda.toml @@ -14,6 +14,8 @@ "diagnostic.error" = { underline = { style = "curl", color = "pink" } } "diagnostic.warning" = { underline = { style = "curl", color = "orange" } } "diagnostic.info" = { underline = { style = "curl", color = "white" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } ## Diffs "diff.plus" = "green" @@ -102,6 +104,7 @@ "ui.text.focus" = { fg = "yellow", modifiers = ["bold"] } "ui.virtual" = "darkgray" "ui.virtual.ruler" = { bg = "darkgray" } +"ui.virtual.jump-label" = { fg = "softorange", modifiers = ["bold"] } # Palette diff --git a/runtime/themes/naysayer.toml b/runtime/themes/naysayer.toml index 41530d141..d85f31e2d 100644 --- a/runtime/themes/naysayer.toml +++ b/runtime/themes/naysayer.toml @@ -25,6 +25,8 @@ "diagnostic.error" = { bg = "error", fg = "text", modifiers = ["bold"] } "diagnostic.warning" = { bg = "warning", fg = "text", modifiers = ["bold"] } "diagnostic.hint" = { bg = "cyan", modifiers = ["bold"] } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.bufferline" = { fg = "text", bg = "bg" } "ui.bufferline.active" = { fg = "text", bg = "bg", modifiers = ['underlined'] } "ui.gutter.selected" = { bg = "highlight-line", modifiers = ["bold"] } diff --git a/runtime/themes/new_moon.toml b/runtime/themes/new_moon.toml index 4f878c7e0..2f0dbb0ad 100644 --- a/runtime/themes/new_moon.toml +++ b/runtime/themes/new_moon.toml @@ -96,6 +96,8 @@ "diagnostic.error".underline = { color = "red", style = "curl" } "diagnostic".underline = { color = "yellow", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] darkest = "#2d2d2d" diff --git a/runtime/themes/night_owl.toml b/runtime/themes/night_owl.toml index fc27dc7cc..ed3c02891 100644 --- a/runtime/themes/night_owl.toml +++ b/runtime/themes/night_owl.toml @@ -10,6 +10,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "blue", style = "curl" } } "diagnostic.hint" = { underline = { color = "paleblue", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # UI 'ui.background' = { fg = 'foreground', bg = 'background' } diff --git a/runtime/themes/nightfox.toml b/runtime/themes/nightfox.toml index 6c5ed3501..62db82c00 100644 --- a/runtime/themes/nightfox.toml +++ b/runtime/themes/nightfox.toml @@ -34,6 +34,7 @@ "ui.virtual.whitespace" = { fg = "bg3" } # Whitespace markers in editing area. "ui.virtual.indent-guide" = { fg = "black" } # Vertical indent width guides "ui.virtual.inlay-hint" = { fg = "comment", bg = "bg2" } # Default style for inlay hints of all kinds +"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold"] } # Style for virtual jump labels "ui.statusline" = { fg = "fg2", bg = "bg0" } # Status line. "ui.statusline.inactive" = { fg = "fg3", bg = "bg0" } # Status line in unfocused windows. @@ -75,6 +76,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } # Diagnostics error (editing area) "diagnostic.info" = { underline = { color = "blue", style = "curl" } } # Diagnostics info (editing area) "diagnostic.hint" = { underline = { color = "green", style = "curl" } } # Diagnostics hint (editing area) +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # SYNTAX HIGHLIGHTING diff --git a/runtime/themes/noctis.toml b/runtime/themes/noctis.toml index c9f95f7fe..db127229e 100644 --- a/runtime/themes/noctis.toml +++ b/runtime/themes/noctis.toml @@ -17,6 +17,8 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.info" = { underline = { color = "mid-blue", style = "curl" } } "diagnostic.hint" = { underline = { color = "light-green", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # UI ============================== # For styling helix itself. diff --git a/runtime/themes/noctis_bordo.toml b/runtime/themes/noctis_bordo.toml index c3bbe79c9..c5e04f2a1 100644 --- a/runtime/themes/noctis_bordo.toml +++ b/runtime/themes/noctis_bordo.toml @@ -59,6 +59,8 @@ "diagnostic.hint" = { underline = { color = "base03", style = "curl" } } "diagnostic.warning" = { underline = { color = "base09", style = "curl" } } "diagnostic.error" = { underline = { color = "base08", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.gutter" = { bg = "base01" } "info" = "base0D" diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml index dab31610f..b70f67a13 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -24,6 +24,8 @@ "info" = "nord8" "diagnostic.warning" = { underline = { color = "nord13", style = "curl" } } "warning" = "nord13" +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # Diffs "diff.delta" = "nord13" @@ -112,7 +114,7 @@ "ui.cursorcolumn.primary" = { bg = "nord1" } "ui.cursorline.primary" = { bg = "nord1" } -"ui.selection" = { bg = "nord2" } +"ui.selection" = { bg = "nord3" } "ui.highlight" = { fg = "nord8", bg = "nord2" } # Statusline @@ -123,9 +125,10 @@ "ui.statusline.select" = { fg = "nord1", bg = "nord7" } "ui.statusline.separator" = "nord3" -# Virtual/invisable text +# Virtual/invisible text "ui.virtual.indent-guide" = "nord3" "ui.virtual.inlay-hint" = { fg = "nord3", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "nord11", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "nord1" } "ui.virtual.whitespace" = "nord3" "ui.virtual.wrap" = "nord3" diff --git a/runtime/themes/nord_light.toml b/runtime/themes/nord_light.toml index b37d35962..6c27c507a 100644 --- a/runtime/themes/nord_light.toml +++ b/runtime/themes/nord_light.toml @@ -6,8 +6,6 @@ "ui.cursorline" = {bg="nord4"} "ui.statusline" = {bg="nord4", fg="nord0"} "ui.statusline.inactive" = {bg="nord5", fg="nord0"} -"ui.virtual" = "nord8" -"ui.virtual.ruler" = {bg="nord4"} "ui.cursor.match" = {bg="nord8"} "ui.cursor" = {bg="nord10", fg="nord6"} "ui.cursorline.primary" = {bg="nord5"} @@ -25,11 +23,20 @@ "ui.statusline.insert" = { fg = "nord0", bg = "nord13" } "ui.statusline.select" = { fg = "nord0", bg = "nord15" } +# Virtual/invisible text +"ui.virtual" = "nord8" +"ui.virtual.ruler" = {bg="nord4"} +"ui.virtual.inlay-hint" = { fg = "nord3", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "nord11", modifiers = ["bold"] } + + "diagnostic.error" = { underline = { color = "nord11", style = "curl" } } "diagnostic.warning" = { underline = { color = "nord13", style = "curl" } } "diagnostic.info" = { underline = { color = "nord13", style = "curl" } } "diagnostic.hint" = { underline = { color = "nord13", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "constant.numeric" = {fg="nord15"} "constant.builtin" = {fg="nord15"} diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index eae11172e..7cdeac095 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -45,6 +45,8 @@ "diagnostic.hint".underline = { color = "green", style = "curl" } "diagnostic.warning".underline = { color = "yellow", style = "curl" } "diagnostic.error".underline = { color = "red", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } @@ -56,6 +58,7 @@ "ui.virtual.whitespace" = { fg = "light-gray" } "ui.virtual.ruler" = { bg = "gray" } "ui.virtual.inlay-hint" = { fg = "light-gray" } +"ui.virtual.jump-label" = { fg = "light-gray", modifiers = ["bold"] } "ui.cursor" = { fg = "white", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] } diff --git a/runtime/themes/onedarker.toml b/runtime/themes/onedarker.toml index 307871445..ffa3ae2dc 100644 --- a/runtime/themes/onedarker.toml +++ b/runtime/themes/onedarker.toml @@ -46,6 +46,8 @@ "diagnostic.hint".underline = { color = "green", style = "curl" } "diagnostic.warning".underline = { color = "yellow", style = "curl" } "diagnostic.error".underline = { color = "red", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } diff --git a/runtime/themes/onelight.toml b/runtime/themes/onelight.toml index e35abdb37..e395716dd 100644 --- a/runtime/themes/onelight.toml +++ b/runtime/themes/onelight.toml @@ -17,12 +17,12 @@ "constant" = { fg = "cyan", modifiers = ["bold"] } "constant.builtin" = { fg = "deep-purple" } -"constant.builtin.boolean" = { fg = "purple" } +"constant.builtin.boolean" = { fg = "purple" , modifiers = ["bold"]} "constant.character" = { fg = "green" } -"constant.character.escape" = { fg = "brown" } -"constant.numeric" = { fg = "brown" } -"constant.numeric.integer" = { fg = "brown" } -"constant.numeric.float" = { fg = "brown" } +"constant.character.escape" = { fg = "brown" , modifiers = ["bold"]} +"constant.numeric" = { fg = "brown" , modifiers = ["bold"]} +"constant.numeric.integer" = { fg = "brown" , modifiers = ["bold"]} +"constant.numeric.float" = { fg = "brown" , modifiers = ["bold"]} "string" = { fg = "green" } "string.regexp" = { fg = "purple" } @@ -137,11 +137,12 @@ "ui.virtual" = { fg = "grey-500" } "ui.virtual.ruler" = { bg = "grey-200" } "ui.virtual.wrap" = { fg = "grey-500" } -"ui.virtual.whitespace" = { fg = "grey-300" } +"ui.virtual.whitespace" = { fg = "grey-400" } "ui.virtual.indent-guide" = { fg = "grey-500" } "ui.virtual.inlay-hint" = { fg = "grey-500" } "ui.virtual.inlay-hint.parameter" = { fg = "grey-500", modifiers = ["italic"] } "ui.virtual.inlay-hint.type" = { fg = "grey-500" } +"ui.virtual.jump-label" = { fg = "black", bg = "grey-200", modifiers = ["bold" ] } "ui.menu" = { fg = "black", bg = "grey-300" } "ui.menu.selected" = { fg = "white", bg = "light-blue" } @@ -162,6 +163,8 @@ "diagnostic.hint" = { underline = { color = "green", style = "dashed" } } "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } diff --git a/runtime/themes/papercolor-light.toml b/runtime/themes/papercolor-light.toml index 279fa2429..4f53c9e06 100644 --- a/runtime/themes/papercolor-light.toml +++ b/runtime/themes/papercolor-light.toml @@ -29,6 +29,7 @@ "ui.virtual.inlay-hint" = { fg = "regular5", modifiers = ["italic"] } "ui.virtual.inlay-hint.parameter" = { fg = "regular5", modifiers = ["italic"] } "ui.virtual.inlay-hint.type" = { fg = "regular5", modifiers = ["bold", "italic"] } +"ui.virtual.jump-label" = { fg = "bright1", modifiers = ["bold"] } "ui.cursor.match" = { bg = "matchparen_bg", fg = "matchparen_fg" } "ui.cursor" = { bg = "regular5", fg = "background" } "ui.cursor.primary" = { bg = "foreground", fg = "background" } @@ -49,6 +50,8 @@ "diagnostic.error".underline = { color = "bright1", style = "curl" } "diagnostic.info".underline = { color = "bright4", style = "curl" } "diagnostic.hint".underline = { color = "bright6", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } # Tree-sitter scopes for syntax highlighting "attribute" = "bright4" diff --git a/runtime/themes/penumbra+.toml b/runtime/themes/penumbra+.toml index bc53e82e0..728a80900 100644 --- a/runtime/themes/penumbra+.toml +++ b/runtime/themes/penumbra+.toml @@ -47,6 +47,8 @@ error = "red" "diagnostic.info".underline = { color = "sky", style = "curl" } "diagnostic.warning".underline = { color = "yellow", style = "curl" } "diagnostic.error".underline = { color = "red", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.background" = { bg = "shade" } "ui.background.separator" = "sky" diff --git a/runtime/themes/poimandres.toml b/runtime/themes/poimandres.toml index 5dbc799d5..7dc0d833a 100644 --- a/runtime/themes/poimandres.toml +++ b/runtime/themes/poimandres.toml @@ -58,6 +58,7 @@ string = { fg = "brightMint" } "ui.text.inactive" = "darkerGray" "ui.virtual" = { fg = "darkerGray.b0" } "ui.virtual.indent-guide" = "#303442" +"ui.virtual.ruler" = { bg ="selection" } "ui.selection" = { bg = "focus" } "ui.selection.primary" = { bg = "selection" } @@ -76,6 +77,8 @@ string = { fg = "brightMint" } "diagnostic.info" = { underline = { color = "lightBlue", style = "curl" } } "diagnostic.warning" = { underline = { color = "brightYellow", style = "curl" } } "diagnostic.error" = { underline = { color = "hotRed", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } hint = "lowerMint" info = "lightBlue" diff --git a/runtime/themes/pop-dark.toml b/runtime/themes/pop-dark.toml index 54db3d1b1..66436c153 100644 --- a/runtime/themes/pop-dark.toml +++ b/runtime/themes/pop-dark.toml @@ -12,6 +12,8 @@ error = { fg = 'redE', modifiers = ['bold'] } 'diagnostic.hint'.underline = { color = 'yellowH', style = 'curl' } 'diagnostic.warning'.underline = { color = 'orangeW', style = 'curl' } 'diagnostic.error'.underline = { color = 'redE', style = 'curl' } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } 'ui.background' = { bg = 'brownN' } 'ui.window' = { bg = 'brownH', fg = 'brownD' } 'ui.gutter' = { bg = 'brownU' } diff --git a/runtime/themes/rasmus.toml b/runtime/themes/rasmus.toml index a0b7e74d4..a05ef6330 100644 --- a/runtime/themes/rasmus.toml +++ b/runtime/themes/rasmus.toml @@ -107,6 +107,8 @@ "diagnostic.error".underline = { color = "bright_red", style = "curl" } "diagnostic.info".underline = { color = "bright_blue", style = "curl" } "diagnostic.hint".underline = { color = "bright_cyan", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } warning = "bright_yellow" error = "bright_red" diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index 456237e94..9a5ba450e 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -37,6 +37,7 @@ "ui.text.focus" = { bg = "overlay" } "ui.text.info" = { fg = "subtle" } +"ui.virtual.jump-label" = { fg = "love", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "overlay" } "ui.virtual.whitespace" = { fg = "highlight_high" } "ui.virtual.indent-guide" = { fg = "muted" } @@ -63,6 +64,8 @@ "diagnostic.info" = { underline = { color = "foam", style = "curl" } } "diagnostic.warning" = { underline = { color = "gold", style = "curl" } } "diagnostic.error" = { underline = { color = "love", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "special" = "rose" "attribute" = "iris" diff --git a/runtime/themes/serika-dark.toml b/runtime/themes/serika-dark.toml index d18419798..eb8d6c839 100644 --- a/runtime/themes/serika-dark.toml +++ b/runtime/themes/serika-dark.toml @@ -63,6 +63,8 @@ "diagnostic.info" = { underline = { style = "curl", color = "aqua" } } "diagnostic.warning" = { underline = { style = "curl", color = "yellow" } } "diagnostic.error" = { underline = { style = "curl", color = "nasty-red" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "diff.plus" = { fg = "green" } "diff.delta" = { fg = "orange" } diff --git a/runtime/themes/serika-light.toml b/runtime/themes/serika-light.toml index 1e3901ef2..00fa84cbb 100644 --- a/runtime/themes/serika-light.toml +++ b/runtime/themes/serika-light.toml @@ -63,6 +63,8 @@ "diagnostic.info" = { underline = { style = "curl", color = "aqua" } } "diagnostic.warning" = { underline = { style = "curl", color = "yellow" } } "diagnostic.error" = { underline = { style = "curl", color = "nasty-red" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "diff.plus" = { fg = "bg_green" } "diff.delta" = { fg = "bg_blue" } diff --git a/runtime/themes/snazzy.toml b/runtime/themes/snazzy.toml index eb88c5eb8..09d0cdd57 100644 --- a/runtime/themes/snazzy.toml +++ b/runtime/themes/snazzy.toml @@ -79,6 +79,9 @@ "error" = { fg = "red" } "warning" = { fg = "cyan" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } + "markup.heading" = { fg = "purple", modifiers = ["bold"] } "markup.link.label" = { fg = "blue", modifiers = ["italic"] } "markup.list" = "cyan" diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml index 88f857d3d..2be238bb5 100644 --- a/runtime/themes/solarized_dark.toml +++ b/runtime/themes/solarized_dark.toml @@ -42,6 +42,7 @@ "ui.virtual.whitespace" = { fg = "base01" } "ui.virtual.inlay-hint" = { fg = "base01", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } # 行号栏 "ui.linenr" = { fg = "base0", bg = "base02" } @@ -78,8 +79,6 @@ "ui.text" = { fg = "base1" } # 影响 picker列表选中, 快捷键帮助窗口文本 "ui.text.focus" = { fg = "blue", modifiers = ["bold"]} -# file picker中, 预览的当前选中项 -"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] } # 主光标/selectio "ui.cursor.primary" = { fg = "base03", bg = "base1" } @@ -91,7 +90,7 @@ "ui.selection.primary" = { bg = "base015" } "ui.virtual.indent-guide" = { fg = "base02" } -"ui.virtual.ruler" = { fg = "red" } +"ui.virtual.ruler" = { bg = "base02" } # normal模式的光标 "ui.cursor" = {fg = "base02", bg = "cyan"} @@ -108,6 +107,8 @@ "diagnostic.error" = { underline = { style = "curl", color = "red" } } "diagnostic.info" = { underline = { style = "curl", color = "blue" } } "diagnostic.hint" = { underline = { style = "curl", color = "base01" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] # 深色 越来越深 diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml index cd2d2b364..b0c0cb4cf 100644 --- a/runtime/themes/solarized_light.toml +++ b/runtime/themes/solarized_light.toml @@ -43,6 +43,7 @@ "ui.virtual.whitespace" = { fg = "base01" } "ui.virtual.inlay-hint" = { fg = "base01", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } # 行号栏 # line number column @@ -90,9 +91,6 @@ # 影响 picker列表选中, 快捷键帮助窗口文本 # Affects picker list selection, shortcut key help window text "ui.text.focus" = { fg = "blue", modifiers = ["bold"]} -# file picker中, 预览的当前选中项 -# In file picker, the currently selected item of the preview -"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] } # 主光标/selection # main cursor/selection @@ -106,7 +104,7 @@ "ui.selection.primary" = { bg = "base015" } "ui.virtual.indent-guide" = { fg = "base02" } -"ui.virtual.ruler" = { fg = "red" } +"ui.virtual.ruler" = { bg = "base02" } # normal模式的光标 # normal mode cursor @@ -125,6 +123,8 @@ "diagnostic.error" = { underline = { style = "curl", color = "red" } } "diagnostic.info" = { underline = { style = "curl", color = "blue" } } "diagnostic.hint" = { underline = { style = "curl", color = "base01" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] diff --git a/runtime/themes/sonokai.toml b/runtime/themes/sonokai.toml index 4bbdb4dbe..ebe1ae8d2 100644 --- a/runtime/themes/sonokai.toml +++ b/runtime/themes/sonokai.toml @@ -85,6 +85,8 @@ error = { fg = 'red', bg = 'bg2', modifiers = ['bold'] } "diagnostic.hint" = { underline = { style = "curl", color = "blue" } } "diagnostic.warning" = { underline = { style = "curl", color = "yellow" } } "diagnostic.error" = { underline = { style = "curl", color = "red" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } diff --git a/runtime/themes/spacebones_light.toml b/runtime/themes/spacebones_light.toml index ad2a04d5a..7870f67ca 100644 --- a/runtime/themes/spacebones_light.toml +++ b/runtime/themes/spacebones_light.toml @@ -78,6 +78,8 @@ "diagnostic.error" = { underline = { style = "curl", color = "#e0211d" } } "diagnostic.info" = { underline = { style = "curl", color = "theme_yellow" } } "diagnostic.hint" = { underline = { style = "curl", color = "bg2" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] base = "#655370" diff --git a/runtime/themes/starlight.toml b/runtime/themes/starlight.toml index 09db90fd6..335d1493a 100644 --- a/runtime/themes/starlight.toml +++ b/runtime/themes/starlight.toml @@ -85,6 +85,8 @@ "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.info" = { underline = { color = "blue", style = "curl" } } "diagnostic.hint" = { underline = { color = "blue", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "info" = "blue" "hint" = "blue" diff --git a/runtime/themes/tokyonight.toml b/runtime/themes/tokyonight.toml index be7968575..4e53e03b8 100644 --- a/runtime/themes/tokyonight.toml +++ b/runtime/themes/tokyonight.toml @@ -65,6 +65,8 @@ hint = { fg = "hint" } "diagnostic.warning" = { underline = { style = "curl", color = "yellow"} } "diagnostic.info" = { underline = { style = "curl", color = "info"} } "diagnostic.hint" = { underline = { style = "curl", color = "hint" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.background" = { bg = "bg", fg = "fg" } "ui.cursor" = { modifiers = ["reversed"] } @@ -90,6 +92,7 @@ hint = { fg = "hint" } "ui.virtual.ruler" = { bg = "fg-gutter" } "ui.virtual.whitespace" = { fg = "fg-gutter" } "ui.virtual.inlay-hint" = { bg = "bg-inlay", fg = "teal" } +"ui.virtual.jump-label" = { fg = "orange", modifiers = ["bold"] } "ui.window" = { fg = "border", modifiers = ["bold"] } [palette] diff --git a/runtime/themes/ttox.toml b/runtime/themes/ttox.toml index 16909cc70..0bc964b45 100644 --- a/runtime/themes/ttox.toml +++ b/runtime/themes/ttox.toml @@ -29,3 +29,5 @@ "diagnostic.warning" = { fg = "black", bg = "light-yellow" } "diagnostic.error" = { fg = "black", bg = "light-red" } "diagnostic.hint" = { fg = "black", bg = "light-blue" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } diff --git a/runtime/themes/varua.toml b/runtime/themes/varua.toml index b07ab08dc..34a7591de 100644 --- a/runtime/themes/varua.toml +++ b/runtime/themes/varua.toml @@ -67,6 +67,7 @@ "ui.statusline.select" = { bg = "blue", fg = "bg2" } "ui.virtual.wrap" = { fg = "grey0" } "ui.virtual.inlay-hint" = { fg = "grey1" } +"ui.virtual.ruler" = { bg = "bg2"} "hint" = "blue" "info" = "aqua" @@ -76,6 +77,9 @@ "diagnostic.info" = { underline = { style = "curl", color = "aqua" } } "diagnostic.warning" = { underline = { style = "curl", color = "yellow" } } "diagnostic.error" = { underline = { style = "curl", color = "red" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } [palette] bg0 = "#282828" diff --git a/runtime/themes/vim_dark_high_contrast.toml b/runtime/themes/vim_dark_high_contrast.toml index ecd6942b5..5e1a2a450 100644 --- a/runtime/themes/vim_dark_high_contrast.toml +++ b/runtime/themes/vim_dark_high_contrast.toml @@ -1,7 +1,10 @@ "ui.background" = { bg = "black" } +"ui.bufferline" = { bg = "black" } +"ui.bufferline.active" = { fg = "light-magenta", bg = "dark-magenta" } "ui.cursor" = { fg = "green", modifiers = ["reversed"] } "ui.cursor.match" = { fg = "light-cyan", bg = "dark-cyan" } "ui.cursor.primary" = { fg = "light-green", modifiers = ["reversed"] } +"ui.cursorline.primary" = { bg = "gray" } "ui.menu" = { bg = "dark-white" } "ui.menu.selected" = { fg = "yellow" } "ui.popup" = { bg = "dark-white" } @@ -15,12 +18,15 @@ "ui.text.focus" = { fg = "yellow" } "ui.virtual.wrap" = { fg = "dark-blue" } "ui.virtual.indent-guide" = { fg = "dark-blue" } +"ui.virtual.ruler" = { bg = "dark-white" } "ui.window" = { bg = "dark-white" } "diagnostic.error" = { bg = "dark-red" } "diagnostic.warning" = { bg = "dark-yellow" } "diagnostic.hint" = { bg = "dark-cyan" } "diagnostic.info" = { bg = "dark-white" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "warning" = { fg = "yellow", bg = "dark-yellow" } "error" = { fg = "red", bg = "dark-red" } "info" = { fg = "default", bg = "dark-white" } @@ -48,6 +54,7 @@ black = "#000000" red = "#ed5f74" green = "#1ea672" +gray = "#111111" yellow = "#d97917" blue = "#688ef1" magenta = "#c96ed0" diff --git a/runtime/themes/voxed.toml b/runtime/themes/voxed.toml index e55b46e5d..c2be9afef 100644 --- a/runtime/themes/voxed.toml +++ b/runtime/themes/voxed.toml @@ -75,6 +75,8 @@ label = "yellow" "diagnostic.info" = { underline = { color = "sglow", style = "curl" } } "diagnostic.warning" = { underline = { color = "redish", style = "curl" } } "diagnostic.error" = { underline = { color = "bpink", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } warning = "bpink" error = "bsienna" diff --git a/runtime/themes/yellowed.toml b/runtime/themes/yellowed.toml index 091b93eeb..d94a75fcb 100644 --- a/runtime/themes/yellowed.toml +++ b/runtime/themes/yellowed.toml @@ -111,6 +111,8 @@ "diagnostic.error" = { underline = { color = "error", style = "curl" } } "diagnostic.info" = { underline = { color = "info", style = "curl" } } "diagnostic.hint" = { underline = { color = "hint", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] # interface diff --git a/runtime/themes/zed_onedark.toml b/runtime/themes/zed_onedark.toml index 7ac1e73cc..3eeabab30 100644 --- a/runtime/themes/zed_onedark.toml +++ b/runtime/themes/zed_onedark.toml @@ -42,6 +42,8 @@ "diagnostic.hint".underline = { color = "green", style = "curl" } "diagnostic.warning".underline = { color = "yellow", style = "curl" } "diagnostic.error".underline = { color = "red", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } diff --git a/runtime/themes/zenburn.toml b/runtime/themes/zenburn.toml index 9a4a7abcc..27f941220 100644 --- a/runtime/themes/zenburn.toml +++ b/runtime/themes/zenburn.toml @@ -45,6 +45,8 @@ "diagnostic" = {bg = "statusbg"} "diagnostic.error" = { fg = "errorfg", bg = "errorbg"} +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } "ui.gutter" = { bg = "statusbg" } "hint" = {fg = "numeric", bg = "statusbg"} "warning" = "numeric" diff --git a/runtime/tutor b/runtime/tutor index f2535dd92..055718379 100644 --- a/runtime/tutor +++ b/runtime/tutor @@ -539,7 +539,7 @@ will now affect both cursors. 3. Use Insert mode to correct the lines. The two cursors will fix both lines simultaneously. - 4. Type , to remove the second cursor. + 4. Type , to remove the first cursor. --> Fix th two nes at same ime. --> diff --git a/theme.toml b/theme.toml index 8a5bfd72d..81c1a91d7 100644 --- a/theme.toml +++ b/theme.toml @@ -40,7 +40,7 @@ label = "honey" "diff.minus" = "#f22c86" "diff.delta" = "#6f44f0" -# TODO: diferentiate doc comment +# TODO: differentiate doc comment # concat (ERROR) @error.syntax and "MISSING ;" selectors for errors "ui.background" = { bg = "midnight" } @@ -52,11 +52,12 @@ label = "honey" "ui.popup" = { bg = "revolver" } "ui.window" = { fg = "bossanova" } "ui.help" = { bg = "#7958DC", fg = "#171452" } - "ui.text" = { fg = "lavender" } "ui.text.focus" = { fg = "white" } "ui.text.inactive" = "sirocco" "ui.virtual" = { fg = "comet" } +"ui.virtual.ruler" = { bg = "revolver" } +"ui.virtual.jump-label" = { fg = "apricot", modifiers = ["bold"] } "ui.virtual.indent-guide" = { fg = "comet" } @@ -65,6 +66,8 @@ label = "honey" # TODO: namespace ui.cursor as ui.selection.cursor? "ui.cursor.select" = { bg = "delta" } "ui.cursor.insert" = { bg = "white" } +"ui.cursor.primary.select" = { bg = "delta" } +"ui.cursor.primary.insert" = { bg = "white" } "ui.cursor.match" = { fg = "#212121", bg = "#6C6999" } "ui.cursor" = { modifiers = ["reversed"] } "ui.cursorline.primary" = { bg = "bossanova" } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1421fd1a1..aba5c4499 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -2,7 +2,6 @@ mod docgen; mod helpers; mod path; mod querycheck; -mod themelint; use std::{env, error::Error}; @@ -12,7 +11,6 @@ pub mod tasks { use crate::docgen::{lang_features, typable_commands, write}; use crate::docgen::{LANG_SUPPORT_MD_OUTPUT, TYPABLE_COMMANDS_MD_OUTPUT}; use crate::querycheck::query_check; - use crate::themelint::{lint, lint_all}; use crate::DynError; pub fn docgen() -> Result<(), DynError> { @@ -21,13 +19,6 @@ pub mod tasks { Ok(()) } - pub fn themelint(file: Option) -> Result<(), DynError> { - match file { - Some(file) => lint(file), - None => lint_all(), - } - } - pub fn querycheck() -> Result<(), DynError> { query_check() } @@ -39,7 +30,6 @@ Usage: Run with `cargo xtask `, eg. `cargo xtask docgen`. Tasks: docgen: Generate files to be included in the mdbook output. - themelint : Report errors for , or all themes if no theme is specified. query-check: Check that tree-sitter queries are valid. " ); @@ -52,7 +42,6 @@ fn main() -> Result<(), DynError> { None => tasks::print_help(), Some(t) => match t.as_str() { "docgen" => tasks::docgen()?, - "themelint" => tasks::themelint(env::args().nth(2))?, "query-check" => tasks::querycheck()?, invalid => return Err(format!("Invalid task name: {}", invalid).into()), }, diff --git a/xtask/src/path.rs b/xtask/src/path.rs index 6f4545c27..62d884f2e 100644 --- a/xtask/src/path.rs +++ b/xtask/src/path.rs @@ -18,7 +18,3 @@ pub fn ts_queries() -> PathBuf { pub fn lang_config() -> PathBuf { project_root().join("languages.toml") } - -pub fn themes() -> PathBuf { - project_root().join("runtime/themes") -} diff --git a/xtask/src/themelint.rs b/xtask/src/themelint.rs deleted file mode 100644 index f7efb7d9a..000000000 --- a/xtask/src/themelint.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::path; -use crate::DynError; -use helix_view::theme::Loader; -use helix_view::theme::Modifier; -use helix_view::Theme; - -struct Rule { - fg: Option<&'static str>, - bg: Option<&'static str>, - check_both: bool, -} - -enum Require { - Existence(Rule), - Difference(&'static str, &'static str), -} - -// Placed in an fn here, so it's the first thing you see -fn get_rules() -> Vec { - vec![ - // Check for ui.selection, which is required - Require::Existence(Rule::has_either("ui.selection")), - Require::Existence(Rule::has_either("ui.selection.primary")), - Require::Difference("ui.selection", "ui.selection.primary"), - // Check for planned readable text - Require::Existence(Rule::has_fg("ui.text")), - Require::Existence(Rule::has_bg("ui.background")), - // Check for complete editor.statusline bare minimum - Require::Existence(Rule::has_both("ui.statusline")), - Require::Existence(Rule::has_both("ui.statusline.inactive")), - // Check for editor.color-modes - Require::Existence(Rule::has_either("ui.statusline.normal")), - Require::Existence(Rule::has_either("ui.statusline.insert")), - Require::Existence(Rule::has_either("ui.statusline.select")), - Require::Difference("ui.statusline.normal", "ui.statusline.insert"), - Require::Difference("ui.statusline.normal", "ui.statusline.select"), - // Check for editor.cursorline - Require::Existence(Rule::has_bg("ui.cursorline.primary")), - // Check for general ui.virtual (such as inlay-hint) - Require::Existence(Rule::has_fg("ui.virtual")), - // Check for editor.whitespace - Require::Existence(Rule::has_fg("ui.virtual.whitespace")), - // Check fir rulers - Require::Existence(Rule::has_either("ui.virtual.indent-guide")), - // Check for editor.rulers - Require::Existence(Rule::has_either("ui.virtual.ruler")), - // Check for menus and prompts - Require::Existence(Rule::has_both("ui.menu")), - Require::Existence(Rule::has_both("ui.help")), - Require::Existence(Rule::has_bg("ui.popup")), - Require::Existence(Rule::has_either("ui.window")), - // Check for visible cursor - Require::Existence(Rule::has_bg("ui.cursor.primary")), - Require::Existence(Rule::has_either("ui.cursor.match")), - ] -} - -impl Rule { - fn has_bg(bg: &'static str) -> Rule { - Rule { - fg: None, - bg: Some(bg), - check_both: true, - } - } - fn has_fg(fg: &'static str) -> Rule { - Rule { - fg: Some(fg), - bg: None, - check_both: true, - } - } - fn has_either(item: &'static str) -> Rule { - Rule { - fg: Some(item), - bg: Some(item), - check_both: false, - } - } - fn has_both(item: &'static str) -> Rule { - Rule { - fg: Some(item), - bg: Some(item), - check_both: true, - } - } - fn found_fg(&self, theme: &Theme) -> bool { - if let Some(fg) = &self.fg { - if theme.get(fg).fg.is_none() && theme.get(fg).add_modifier == Modifier::empty() { - return false; - } - } - true - } - fn found_bg(&self, theme: &Theme) -> bool { - if let Some(bg) = &self.bg { - if theme.get(bg).bg.is_none() && theme.get(bg).add_modifier == Modifier::empty() { - return false; - } - } - true - } - fn rule_name(&self) -> &'static str { - if self.fg.is_some() { - self.fg.unwrap() - } else if self.bg.is_some() { - self.bg.unwrap() - } else { - "LINTER_ERROR_NO_RULE" - } - } - - fn check_difference( - theme: &Theme, - a: &'static str, - b: &'static str, - messages: &mut Vec, - ) { - let theme_a = theme.get(a); - let theme_b = theme.get(b); - if theme_a == theme_b { - messages.push(format!("$THEME: `{}` and `{}` cannot be equal", a, b)); - } - } - - fn check_existence(rule: &Rule, theme: &Theme, messages: &mut Vec) { - let found_fg = rule.found_fg(theme); - let found_bg = rule.found_bg(theme); - - if !rule.check_both && (found_fg || found_bg) { - return; - } - if !found_fg || !found_bg { - let mut missing = vec![]; - if !found_fg { - missing.push("`fg`"); - } - if !found_bg { - missing.push("`bg`"); - } - let entry = if !rule.check_both && !found_fg && !found_bg { - missing.join(" or ") - } else { - missing.join(" and ") - }; - messages.push(format!( - "$THEME: missing {} for `{}`", - entry, - rule.rule_name() - )) - } - } -} - -pub fn lint(file: String) -> Result<(), DynError> { - if file.contains("base16") { - println!("Skipping base16: {}", file); - return Ok(()); - } - let path = path::themes().join(file.clone() + ".toml"); - let theme = std::fs::read_to_string(path).unwrap(); - let theme: Theme = toml::from_str(&theme).expect("Failed to parse theme"); - - let mut messages: Vec = vec![]; - get_rules().iter().for_each(|lint| match lint { - Require::Existence(rule) => Rule::check_existence(rule, &theme, &mut messages), - Require::Difference(a, b) => Rule::check_difference(&theme, a, b, &mut messages), - }); - - if !messages.is_empty() { - messages.iter().for_each(|m| { - let theme = file.clone(); - let message = m.replace("$THEME", theme.as_str()); - println!("{}", message); - }); - Err(format!("{} has issues", file).into()) - } else { - Ok(()) - } -} - -pub fn lint_all() -> Result<(), DynError> { - let files = Loader::read_names(path::themes().as_path()); - let files_count = files.len(); - let ok_files_count = files - .into_iter() - .filter_map(|path| lint(path.replace(".toml", "")).ok()) - .count(); - - if files_count != ok_files_count { - Err(format!( - "{} of {} themes had issues", - files_count - ok_files_count, - files_count - ) - .into()) - } else { - Ok(()) - } -}