diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f712a5315..5cdc795fc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,8 +35,8 @@ jobs: uses: actions/cache@v4 with: path: runtime/grammars - key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} - restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- + key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} + restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- - name: Run cargo check run: cargo check @@ -65,8 +65,8 @@ jobs: uses: actions/cache@v4 with: path: runtime/grammars - key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} - restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- + key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} + restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- - name: Run cargo test run: cargo test --workspace @@ -76,7 +76,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-24.04-arm] lints: name: Lints @@ -100,8 +100,8 @@ jobs: uses: actions/cache@v4 with: path: runtime/grammars - key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} - restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- + key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} + restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- - name: Run cargo fmt run: cargo fmt --all --check @@ -135,8 +135,8 @@ jobs: uses: actions/cache@v4 with: path: runtime/grammars - key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} - restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- + key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} + restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars- - name: Validate queries run: cargo xtask query-check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de0a25f67..273680e49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,18 +58,18 @@ jobs: strategy: fail-fast: false # don't fail other jobs if one fails matrix: - build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc + build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc include: - build: x86_64-linux - os: ubuntu-22.04 + os: ubuntu-24.04 rust: stable target: x86_64-unknown-linux-gnu cross: false - build: aarch64-linux - os: ubuntu-22.04 + os: ubuntu-24.04-arm rust: stable target: aarch64-unknown-linux-gnu - cross: true + cross: false # - build: riscv64-linux # os: ubuntu-22.04 # rust: stable diff --git a/Cargo.toml b/Cargo.toml index 4b9e8fea6..667a83967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ tree-sitter = { version = "0.22" } nucleo = "0.5.0" slotmap = "1.0.7" thiserror = "2.0" -tempfile = "3.19.0" +tempfile = "3.19.1" bitflags = "2.9" unicode-segmentation = "1.2" ropey = { version = "1.6.1", default-features = false, features = ["simd"] } diff --git a/book/book.toml b/book/book.toml index 335b27af6..f1127f3c4 100644 --- a/book/book.toml +++ b/book/book.toml @@ -1,7 +1,6 @@ [book] authors = ["BlaΕΎ Hrastnik"] language = "en" -multilingual = false src = "src" [output.html] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 0aa705675..82715b7ef 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -15,7 +15,9 @@ - [Command line](./command-line.md) - [Commands](./commands.md) - [Language support](./lang-support.md) -- [Migrating from Vim](./from-vim.md) +- [Ecosystem](./ecosystem.md) + - [Migrating from Vim](./from-vim.md) + - [Helix mode in other software](./other-software.md) - [Configuration](./configuration.md) - [Editor](./editor.md) - [Themes](./themes.md) diff --git a/book/src/building-from-source.md b/book/src/building-from-source.md index fc8631b00..b2ed3f7fc 100644 --- a/book/src/building-from-source.md +++ b/book/src/building-from-source.md @@ -184,11 +184,11 @@ cargo deb -- --locked > πŸ’‘ This locks you into the `--release` profile. But you can also build helix in any way you like. > As long as you leave a `target/release/hx` file, it will get packaged with `cargo deb --no-build` -> πŸ’‘ Don't worry about the repeated +> πŸ’‘ Don't worry about the following: > ``` > warning: Failed to find dependency specification > ``` -> warnings. Cargo deb just reports which packaged files it didn't derive dependencies for. But +> Cargo deb just reports which packaged files it didn't derive dependencies for. But > so far the dependency deriving seams very good, even if some of the grammar files are skipped. You can find the resulted `.deb` in `target/debian/`. It should contain everything it needs, including the diff --git a/book/src/ecosystem.md b/book/src/ecosystem.md new file mode 100644 index 000000000..91d902806 --- /dev/null +++ b/book/src/ecosystem.md @@ -0,0 +1,3 @@ +# Ecosystem + +This section has information related to the wider Helix ecosystem. diff --git a/book/src/editor.md b/book/src/editor.md index 25d98b84b..d3160b6bb 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -105,6 +105,8 @@ separator = "β”‚" mode.normal = "NORMAL" mode.insert = "INSERT" mode.select = "SELECT" +diagnostics = ["warning", "error"] +workspace-diagnostics = ["warning", "error"] ``` The `[editor.statusline]` key takes the following sub-keys: @@ -117,6 +119,8 @@ The `[editor.statusline]` key takes the following sub-keys: | `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` | | `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` | | `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` | +| `diagnostics` | A list of severities which are displayed for the current buffer | `["warning", "error"]` | +| `workspace-diagnostics` | A list of severities which are displayed for the workspace | `["warning", "error"]` | The following statusline elements can be configured: diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index f6aee3fe3..d66f7bef1 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -36,6 +36,7 @@ | d | βœ“ | βœ“ | βœ“ | `serve-d` | | dart | βœ“ | βœ“ | βœ“ | `dart` | | dbml | βœ“ | | | | +| debian | βœ“ | | | | | devicetree | βœ“ | | | | | dhall | βœ“ | βœ“ | | `dhall-lsp-server` | | diff | βœ“ | | | | @@ -57,6 +58,7 @@ | erb | βœ“ | | | | | erlang | βœ“ | βœ“ | | `erlang_ls`, `elp` | | esdl | βœ“ | | | | +| fennel | βœ“ | | | `fennel-ls` | | fga | βœ“ | βœ“ | βœ“ | | | fidl | βœ“ | | | | | fish | βœ“ | βœ“ | βœ“ | `fish-lsp` | @@ -118,7 +120,7 @@ | jsonnet | βœ“ | | | `jsonnet-language-server` | | jsx | βœ“ | βœ“ | βœ“ | `typescript-language-server` | | julia | βœ“ | βœ“ | βœ“ | `julia` | -| just | βœ“ | βœ“ | βœ“ | | +| just | βœ“ | βœ“ | βœ“ | `just-lsp` | | kdl | βœ“ | βœ“ | βœ“ | | | koka | βœ“ | | βœ“ | `koka` | | kotlin | βœ“ | βœ“ | βœ“ | `kotlin-language-server` | @@ -175,12 +177,13 @@ | ponylang | βœ“ | βœ“ | βœ“ | | | powershell | βœ“ | | | | | prisma | βœ“ | βœ“ | | `prisma-language-server` | -| prolog | | | | `swipl` | +| prolog | βœ“ | | βœ“ | `swipl` | | protobuf | βœ“ | βœ“ | βœ“ | `buf`, `pb`, `protols` | | prql | βœ“ | | | | | purescript | βœ“ | βœ“ | | `purescript-language-server` | | python | βœ“ | βœ“ | βœ“ | `ruff`, `jedi-language-server`, `pylsp` | | qml | βœ“ | | βœ“ | `qmlls` | +| quarto | βœ“ | | βœ“ | | | quint | βœ“ | | | `quint-language-server` | | r | βœ“ | | | `R` | | racket | βœ“ | | βœ“ | `racket` | @@ -248,6 +251,7 @@ | wat | βœ“ | | | `wat_server` | | webc | βœ“ | | | | | werk | βœ“ | | | | +| wesl | βœ“ | βœ“ | | | | wgsl | βœ“ | | | `wgsl-analyzer` | | wit | βœ“ | | βœ“ | | | wren | βœ“ | βœ“ | βœ“ | | diff --git a/book/src/generated/static-cmd.md b/book/src/generated/static-cmd.md index 3efba2a66..dacdb0614 100644 --- a/book/src/generated/static-cmd.md +++ b/book/src/generated/static-cmd.md @@ -126,8 +126,10 @@ | `add_newline_below` | Add newline below | normal: `` ] ``, select: `` ] `` | | `goto_type_definition` | Goto type definition | normal: `` gy ``, select: `` gy `` | | `goto_implementation` | Goto implementation | normal: `` gi ``, select: `` gi `` | -| `goto_file_start` | Goto line number else file start | normal: `` gg ``, select: `` gg `` | +| `goto_file_start` | Goto line number else file start | normal: `` gg `` | | `goto_file_end` | Goto file end | | +| `extend_to_file_start` | Extend to line number else file start | select: `` gg `` | +| `extend_to_file_end` | Extend to file end | | | `goto_file` | Goto files/URLs in selections | normal: `` gf ``, select: `` gf `` | | `goto_file_hsplit` | Goto files in selections (hsplit) | normal: `` f ``, `` wf ``, select: `` f ``, `` wf `` | | `goto_file_vsplit` | Goto files in selections (vsplit) | normal: `` F ``, `` wF ``, select: `` F ``, `` wF `` | @@ -139,7 +141,8 @@ | `goto_last_modified_file` | Goto last modified file | normal: `` gm ``, select: `` gm `` | | `goto_last_modification` | Goto last modification | normal: `` g. ``, select: `` g. `` | | `goto_line` | Goto line | normal: `` G ``, select: `` G `` | -| `goto_last_line` | Goto last line | normal: `` ge ``, select: `` ge `` | +| `goto_last_line` | Goto last line | normal: `` ge `` | +| `extend_to_last_line` | Extend to last line | select: `` ge `` | | `goto_first_diag` | Goto first diagnostic | normal: `` [D ``, select: `` [D `` | | `goto_last_diag` | Goto last diagnostic | normal: `` ]D ``, select: `` ]D `` | | `goto_next_diag` | Goto next diagnostic | normal: `` ]d ``, select: `` ]d `` | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index dc5a6d08a..219f6b95f 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -78,9 +78,9 @@ | `:log-open` | Open the helix log file. | | `:insert-output` | Run shell command, inserting output before each selection. | | `:append-output` | Run shell command, appending output after each selection. | -| `:pipe` | Pipe each selection to the shell command. | +| `:pipe`, `:|` | Pipe each selection to the shell command. | | `:pipe-to` | Pipe each selection to the shell command, ignoring output. | -| `:run-shell-command`, `:sh` | Run a shell command | +| `:run-shell-command`, `:sh`, `:!` | Run a shell command | | `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | | `:clear-register` | Clear given register. If no argument is provided, clear all registers. | | `:redraw` | Clear and re-render the whole UI | diff --git a/book/src/guides/injection.md b/book/src/guides/injection.md index 729a2c99e..c94dadba1 100644 --- a/book/src/guides/injection.md +++ b/book/src/guides/injection.md @@ -4,11 +4,16 @@ Writing language injection queries allows one to highlight a specific node as a In addition to the [standard][upstream-docs] language injection options used by tree-sitter, there are a few Helix specific extensions that allow for more control. -And example of a simple query that would highlight all strings as bash in Nix: +An example of a simple query that would highlight all strings as bash in Nix: ```scm ((string_expression (string_fragment) @injection.content) (#set! injection.language "bash")) ``` +Another example is this query, which highlights links in comments and keywords like "TODO", by reusing the dedicated "comment" language: +``` +((comment) @injection.content + (#set! injection.language "comment")) +``` ## Capture Types diff --git a/book/src/other-software.md b/book/src/other-software.md new file mode 100644 index 000000000..a8a06259e --- /dev/null +++ b/book/src/other-software.md @@ -0,0 +1,33 @@ +# Helix mode in other software + +Helix' keymap and interaction model ([Using Helix](#usage.md)) is easier to adopt if it can be used consistently in many editing contexts. Yet, certain use cases cannot easily be addressed directly in Helix. Similar to vim, this leads to the creation of "Helix mode" in various other software products, allowing Helix-style editing for a greater variety of use cases. + +"Helix mode" is frequently still in early stages or missing entirely. For such cases, we also link to relevant bugs or discussions. + +## Other editors + +| Editor | Plugin or feature providing Helix editing | Comments +| --- | --- | --- | +| [Vim](https://www.vim.org/) | [helix.vim](https://github.com/chtenb/helix.vim) config | +| [IntelliJ IDEA](https://www.jetbrains.com/idea/) / [Android Studio](https://developer.android.com/studio)| [IdeaVim](https://plugins.jetbrains.com/plugin/164-ideavim) plugin + [helix.idea.vim](https://github.com/chtenb/helix.vim) config | Minimum recommended version is IdeaVim 2.19.0. +| [Visual Studio](https://visualstudio.microsoft.com/) | [VsVim](https://marketplace.visualstudio.com/items?itemName=JaredParMSFT.VsVim) plugin + [helix.vs.vim](https://github.com/chtenb/helix.vim) config | +| [Visual Studio Code](https://code.visualstudio.com/) | [Dance](https://marketplace.visualstudio.com/items?itemName=gregoire.dance) extension, or its [Helix fork](https://marketplace.visualstudio.com/items?itemName=kend.dancehelixkey) | The Helix fork has diverged. You can also use the original Dance and tweak its keybindings directly (try [this config](https://github.com/71/dance/issues/299#issuecomment-1655509531)). +| [Visual Studio Code](https://code.visualstudio.com/) | [Helix for VS Code](https://marketplace.visualstudio.com/items?itemName=jasew.vscode-helix-emulation) extension| +| [Zed](https://zed.dev/) | native via keybindings ([Bug](https://github.com/zed-industries/zed/issues/4642)) | +| [CodeMirror](https://codemirror.net/) | [codemirror-helix](https://gitlab.com/_rvidal/codemirror-helix) | + + +## Shells + +| Shell | Plugin or feature providing Helix editing +| --- | --- +| Fish | [Feature Request](https://github.com/fish-shell/fish-shell/issues/7748) +| Fish | [fish-helix](https://github.com/sshilovsky/fish-helix/tree/main) +| Zsh | [helix-zsh](https://github.com/john-h-k/helix-zsh) or [zsh-helix-mode](https://github.com/Multirious/zsh-helix-mode) +| Nushell | [Feature Request](https://github.com/nushell/reedline/issues/639) + +## Other software + +| Software | Plugin or feature providing Helix editing. | Comments +| --- | --- | --- | +| [Obsidian](https://obsidian.md/) | [Obsidian-Helix](https://github.com/Sinono3/obsidian-helix) | Uses `codemirror-helix` listed above. diff --git a/book/src/remapping.md b/book/src/remapping.md index 4eb14c558..23bb80c55 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -12,7 +12,7 @@ There are three kinds of commands that can be used in keymaps: in [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro. * Typable commands: commands that can be executed from command mode (`:`), for - example `:write!`. See the [Commands](./commands.html) documentation for a + example `:write!`. See the [Commands](./commands.md) documentation for a list of available typeable commands or the `TypableCommandList` declaration in the source code at [`helix-term/src/commands/typed.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands/typed.rs). * Macros: sequences of keys that are executed in order. These keybindings diff --git a/book/src/themes.md b/book/src/themes.md index 4201720e6..6cc67b3f9 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -181,7 +181,7 @@ We use a similar set of scopes as - `member` - Fields of composite data types (e.g. structs, unions) - `private` - Private fields that use a unique syntax (currently just ECMAScript-based languages) -- `label` +- `label` - `.class`, `#id` in CSS, etc. - `punctuation` - `delimiter` - Commas, colons @@ -216,7 +216,7 @@ We use a similar set of scopes as - `namespace` -- `special` +- `special` - `derive` in Rust, etc. - `markup` - `heading` diff --git a/contrib/Helix.desktop b/contrib/Helix.desktop index 25d5c3b1a..666828716 100644 --- a/contrib/Helix.desktop +++ b/contrib/Helix.desktop @@ -86,6 +86,6 @@ Keywords[ru]=тСкст;тСкстовый Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€; Keywords[sr]=ВСкст;Π΅Π΄ΠΈΡ‚ΠΎΡ€; Keywords[tr]=Metin;dΓΌzenleyici; Icon=helix -Categories=Utility;TextEditor; +Categories=Utility;TextEditor;ConsoleOnly StartupNotify=false MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++; diff --git a/default.nix b/default.nix index d94e707d3..45f34fff4 100644 --- a/default.nix +++ b/default.nix @@ -75,9 +75,9 @@ in mkdir -p $out/lib installShellCompletion ${./contrib/completion}/hx.{bash,fish,zsh} mkdir -p $out/share/{applications,icons/hicolor/{256x256,scalable}/apps} - cp ${./contrib/Helix.desktop} $out/share/applications + cp ${./contrib/Helix.desktop} $out/share/applications/Helix.desktop cp ${./logo.svg} $out/share/icons/hicolor/scalable/apps/helix.svg - cp ${./contrib/helix.png} $out/share/icons/hicolor/256x256/apps + cp ${./contrib/helix.png} $out/share/icons/hicolor/256x256/apps/helix.png ''; meta.mainProgram = "hx"; diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 10fb5a52c..fe9a8c939 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -21,7 +21,7 @@ helix-loader = { path = "../helix-loader" } helix-parsec = { path = "../helix-parsec" } ropey.workspace = true -smallvec = "1.14" +smallvec = "1.15" smartstring = "1.0.1" unicode-segmentation.workspace = true # unicode-width is changing width definitions diff --git a/helix-core/src/test.rs b/helix-core/src/test.rs index 7183302c6..71c71cce2 100644 --- a/helix-core/src/test.rs +++ b/helix-core/src/test.rs @@ -65,7 +65,7 @@ pub fn print(s: &str) -> (String, Selection) { let head_at_beg = iter.next_if_eq(&"|").is_some(); let last_grapheme = |s: &str| { UnicodeSegmentation::graphemes(s, true) - .last() + .next_back() .map(String::from) }; diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index d092d20f7..1f414de67 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -23,22 +23,6 @@ pub fn user_lang_config() -> Result { .collect::, _>>()? .into_iter() .fold(default_lang_config(), |a, b| { - // combines for example - // b: - // [[language]] - // name = "toml" - // language-server = { command = "taplo", args = ["lsp", "stdio"] } - // - // a: - // [[language]] - // language-server = { command = "/usr/bin/taplo" } - // - // into: - // [[language]] - // name = "toml" - // language-server = { command = "/usr/bin/taplo" } - // - // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values crate::merge_toml_values(a, b, 3) }); diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index ae9ffe550..54980dd7d 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -154,17 +154,36 @@ pub fn default_log_file() -> PathBuf { /// Merge two TOML documents, merging values from `right` onto `left` /// -/// When an array exists in both `left` and `right`, `right`'s array is -/// used. When a table exists in both `left` and `right`, the merged table -/// consists of all keys in `left`'s table unioned with all keys in `right` -/// with the values of `right` being merged recursively onto values of -/// `left`. +/// `merge_depth` sets the nesting depth up to which values are merged instead +/// of overridden. /// -/// `merge_toplevel_arrays` controls whether a top-level array in the TOML -/// document is merged instead of overridden. This is useful for TOML -/// documents that use a top-level array of values like the `languages.toml`, -/// where one usually wants to override or add to the array instead of -/// replacing it altogether. +/// When a table exists in both `left` and `right`, the merged table consists of +/// all keys in `left`'s table unioned with all keys in `right` with the values +/// of `right` being merged recursively onto values of `left`. +/// +/// `crate::merge_toml_values(a, b, 3)` combines, for example: +/// +/// b: +/// ```toml +/// [[language]] +/// name = "toml" +/// language-server = { command = "taplo", args = ["lsp", "stdio"] } +/// ``` +/// a: +/// ```toml +/// [[language]] +/// language-server = { command = "/usr/bin/taplo" } +/// ``` +/// +/// into: +/// ```toml +/// [[language]] +/// name = "toml" +/// language-server = { command = "/usr/bin/taplo" } +/// ``` +/// +/// thus it overrides the third depth-level of b with values of a if they exist, +/// but otherwise merges their values pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value { use toml::Value; @@ -174,11 +193,6 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi match (left, right) { (Value::Array(mut left_items), Value::Array(right_items)) => { - // The top-level arrays should be merged but nested arrays should - // act as overrides. For the `languages.toml` config, this means - // that you can specify a sub-set of languages in an overriding - // `languages.toml` but that nested arrays like Language Server - // arguments are replaced instead of merged. if merge_depth > 0 { left_items.reserve(right_items.len()); for rvalue in right_items { diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f2b78a118..f82751051 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -39,7 +39,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder { lsp::WorkspaceFolder { name: uri .path_segments() - .and_then(|segments| segments.last()) + .and_then(|mut segments| segments.next_back()) .map(|basename| basename.to_string()) .unwrap_or_default(), uri, diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index ba41cbc5a..1a2557de2 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -733,14 +733,17 @@ impl Registry { #[derive(Debug)] pub enum ProgressStatus { Created, - Started(lsp::WorkDoneProgress), + Started { + title: String, + progress: lsp::WorkDoneProgress, + }, } impl ProgressStatus { pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> { match &self { ProgressStatus::Created => None, - ProgressStatus::Started(progress) => Some(progress), + ProgressStatus::Started { title: _, progress } => Some(progress), } } } @@ -777,6 +780,13 @@ impl LspProgressMap { self.0.get(&id).and_then(|values| values.get(token)) } + pub fn title(&self, id: LanguageServerId, token: &lsp::ProgressToken) -> Option<&String> { + self.progress(id, token).and_then(|p| match p { + ProgressStatus::Created => None, + ProgressStatus::Started { title, .. } => Some(title), + }) + } + /// Checks if progress `token` for server with `id` is created. pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool { self.0 @@ -801,17 +811,39 @@ impl LspProgressMap { self.0.get_mut(&id).and_then(|vals| vals.remove(token)) } - /// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`. + /// Updates the progress of `token` for server with `id` to begin state `status` + pub fn begin( + &mut self, + id: LanguageServerId, + token: lsp::ProgressToken, + status: lsp::WorkDoneProgressBegin, + ) { + self.0.entry(id).or_default().insert( + token, + ProgressStatus::Started { + title: status.title.clone(), + progress: lsp::WorkDoneProgress::Begin(status), + }, + ); + } + + /// Updates the progress of `token` for server with `id` to report state `status`. pub fn update( &mut self, id: LanguageServerId, token: lsp::ProgressToken, - status: lsp::WorkDoneProgress, - ) -> Option { + status: lsp::WorkDoneProgressReport, + ) { self.0 .entry(id) .or_default() - .insert(token, ProgressStatus::Started(status)) + .entry(token) + .and_modify(|e| match e { + ProgressStatus::Created => (), + ProgressStatus::Started { progress, .. } => { + *progress = lsp::WorkDoneProgress::Report(status) + } + }); } } diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 9ea2d4589..7709d741b 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -61,7 +61,7 @@ tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } arc-swap = { version = "1.7.1" } termini = "1" -indexmap = "2.8" +indexmap = "2.9" # Logging fern = "0.7" @@ -102,7 +102,7 @@ crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc helix-loader = { path = "../helix-loader" } [dev-dependencies] -smallvec = "1.14" +smallvec = "1.15" indoc = "2.0.6" tempfile.workspace = true same-file = "1.0.1" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c9cdd6507..fd518b6b8 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -761,10 +761,11 @@ impl Application { .compositor .find::() .expect("expected at least one EditorView"); - let lsp::ProgressParams { token, value } = params; - - let lsp::ProgressParamsValue::WorkDone(work) = value; - let parts = match &work { + let lsp::ProgressParams { + token, + value: lsp::ProgressParamsValue::WorkDone(work), + } = params; + let (title, message, percentage) = match &work { lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin { title, message, @@ -792,47 +793,43 @@ impl Application { } }; - let token_d: &dyn std::fmt::Display = match &token { - lsp::NumberOrString::Number(n) => n, - lsp::NumberOrString::String(s) => s, - }; - - let status = match parts { - (Some(title), Some(message), Some(percentage)) => { - format!("[{}] {}% {} - {}", token_d, percentage, title, message) + if self.editor.config().lsp.display_progress_messages { + let title = + title.or_else(|| self.lsp_progress.title(server_id, &token)); + if title.is_some() || percentage.is_some() || message.is_some() { + use std::fmt::Write as _; + let mut status = format!("{}: ", language_server!().name()); + if let Some(percentage) = percentage { + write!(status, "{percentage:>2}% ").unwrap(); + } + if let Some(title) = title { + status.push_str(title); + } + if title.is_some() && message.is_some() { + status.push_str(" β‹… "); + } + if let Some(message) = message { + status.push_str(message); + } + self.editor.set_status(status); } - (Some(title), None, Some(percentage)) => { - format!("[{}] {}% {}", token_d, percentage, title) - } - (Some(title), Some(message), None) => { - format!("[{}] {} - {}", token_d, title, message) - } - (None, Some(message), Some(percentage)) => { - format!("[{}] {}% {}", token_d, percentage, message) - } - (Some(title), None, None) => { - format!("[{}] {}", token_d, title) - } - (None, Some(message), None) => { - format!("[{}] {}", token_d, message) - } - (None, None, Some(percentage)) => { - format!("[{}] {}%", token_d, percentage) - } - (None, None, None) => format!("[{}]", token_d), - }; - - if let lsp::WorkDoneProgress::End(_) = work { - self.lsp_progress.end_progress(server_id, &token); - if !self.lsp_progress.is_progressing(server_id) { - editor_view.spinners_mut().get_or_create(server_id).stop(); - } - } else { - self.lsp_progress.update(server_id, token, work); } - if self.config.load().editor.lsp.display_progress_messages { - self.editor.set_status(status); + match work { + lsp::WorkDoneProgress::Begin(begin_status) => { + self.lsp_progress + .begin(server_id, token.clone(), begin_status); + } + lsp::WorkDoneProgress::Report(report_status) => { + self.lsp_progress + .update(server_id, token.clone(), report_status); + } + lsp::WorkDoneProgress::End(_) => { + self.lsp_progress.end_progress(server_id, &token); + if !self.lsp_progress.is_progressing(server_id) { + editor_view.spinners_mut().get_or_create(server_id).stop(); + }; + } } } Notification::ProgressMessage(_params) => { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index df46b381d..595652892 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -427,6 +427,8 @@ impl MappableCommand { goto_implementation, "Goto implementation", goto_file_start, "Goto line number else file start", goto_file_end, "Goto file end", + extend_to_file_start, "Extend to line number else file start", + extend_to_file_end, "Extend to file end", goto_file, "Goto files/URLs in selections", goto_file_hsplit, "Goto files in selections (hsplit)", goto_file_vsplit, "Goto files in selections (vsplit)", @@ -439,6 +441,7 @@ impl MappableCommand { goto_last_modification, "Goto last modification", goto_line, "Goto line", goto_last_line, "Goto last line", + extend_to_last_line, "Extend to last line", goto_first_diag, "Goto first diagnostic", goto_last_diag, "Goto last diagnostic", goto_next_diag, "Goto next diagnostic", @@ -1255,28 +1258,44 @@ fn goto_next_paragraph(cx: &mut Context) { } fn goto_file_start(cx: &mut Context) { + goto_file_start_impl(cx, Movement::Move); +} + +fn extend_to_file_start(cx: &mut Context) { + goto_file_start_impl(cx, Movement::Extend); +} + +fn goto_file_start_impl(cx: &mut Context, movement: Movement) { if cx.count.is_some() { - goto_line(cx); + goto_line_impl(cx, movement); } else { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let selection = doc .selection(view.id) .clone() - .transform(|range| range.put_cursor(text, 0, cx.editor.mode == Mode::Select)); + .transform(|range| range.put_cursor(text, 0, movement == Movement::Extend)); push_jump(view, doc); doc.set_selection(view.id, selection); } } fn goto_file_end(cx: &mut Context) { + goto_file_end_impl(cx, Movement::Move); +} + +fn extend_to_file_end(cx: &mut Context) { + goto_file_end_impl(cx, Movement::Extend) +} + +fn goto_file_end_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let pos = doc.text().len_chars(); let selection = doc .selection(view.id) .clone() - .transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select)); + .transform(|range| range.put_cursor(text, pos, movement == Movement::Extend)); push_jump(view, doc); doc.set_selection(view.id, selection); } @@ -3799,15 +3818,23 @@ fn push_jump(view: &mut View, doc: &Document) { } fn goto_line(cx: &mut Context) { + goto_line_impl(cx, Movement::Move); +} + +fn goto_line_impl(cx: &mut Context, movement: Movement) { if cx.count.is_some() { let (view, doc) = current!(cx.editor); push_jump(view, doc); - goto_line_without_jumplist(cx.editor, cx.count); + goto_line_without_jumplist(cx.editor, cx.count, movement); } } -fn goto_line_without_jumplist(editor: &mut Editor, count: Option) { +fn goto_line_without_jumplist( + editor: &mut Editor, + count: Option, + movement: Movement, +) { if let Some(count) = count { let (view, doc) = current!(editor); let text = doc.text().slice(..); @@ -3822,13 +3849,21 @@ fn goto_line_without_jumplist(editor: &mut Editor, count: Option) let selection = doc .selection(view.id) .clone() - .transform(|range| range.put_cursor(text, pos, editor.mode == Mode::Select)); + .transform(|range| range.put_cursor(text, pos, movement == Movement::Extend)); doc.set_selection(view.id, selection); } } fn goto_last_line(cx: &mut Context) { + goto_last_line_impl(cx, Movement::Move) +} + +fn extend_to_last_line(cx: &mut Context) { + goto_last_line_impl(cx, Movement::Extend) +} + +fn goto_last_line_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let line_idx = if text.line(text.len_lines() - 1).len_chars() == 0 { @@ -3841,7 +3876,7 @@ fn goto_last_line(cx: &mut Context) { let selection = doc .selection(view.id) .clone() - .transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select)); + .transform(|range| range.put_cursor(text, pos, movement == Movement::Extend)); push_jump(view, doc); doc.set_selection(view.id, selection); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 8960093ab..1a197e0f1 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1914,7 +1914,15 @@ fn update_goto_line_number_preview(cx: &mut compositor::Context, args: Args) -> let scrolloff = cx.editor.config().scrolloff; let line = args[0].parse::()?; - goto_line_without_jumplist(cx.editor, NonZeroUsize::new(line)); + goto_line_without_jumplist( + cx.editor, + NonZeroUsize::new(line), + if cx.editor.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ); let (view, doc) = current!(cx.editor); view.ensure_cursor_in_view(doc, scrolloff); @@ -2958,7 +2966,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: theme, completer: CommandCompleter::positional(&[completers::theme]), signature: Signature { - positionals: (1, Some(1)), + positionals: (0, Some(1)), ..Signature::DEFAULT }, }, @@ -3476,7 +3484,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ }, TypableCommand { name: "pipe", - aliases: &[], + aliases: &["|"], doc: "Pipe each selection to the shell command.", fun: pipe, completer: SHELL_COMPLETER, @@ -3492,7 +3500,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ }, TypableCommand { name: "run-shell-command", - aliases: &["sh"], + aliases: &["sh", "!"], doc: "Run a shell command", fun: run_shell_command, completer: SHELL_COMPLETER, diff --git a/helix-term/src/handlers/document_colors.rs b/helix-term/src/handlers/document_colors.rs index cffe56888..956cecbfb 100644 --- a/helix-term/src/handlers/document_colors.rs +++ b/helix-term/src/handlers/document_colors.rs @@ -167,10 +167,14 @@ pub(super) fn register_hooks(handlers: &Handlers) { apply_color_swatch_changes(color_swatches_padding); } - // Cancel the ongoing request, if present. - event.doc.color_swatch_controller.cancel(); - - helix_event::send_blocking(&tx, DocumentColorsEvent(event.doc.id())); + // Avoid re-requesting document colors if the change is a ghost transaction (completion) + // because the language server will not know about the updates to the document and will + // give out-of-date locations. + if !event.ghost_transaction { + // Cancel the ongoing request, if present. + event.doc.color_swatch_controller.cancel(); + helix_event::send_blocking(&tx, DocumentColorsEvent(event.doc.id())); + } Ok(()) }); diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 63d09ffbd..92aa011d2 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -368,6 +368,8 @@ pub fn default() -> HashMap { "v" => normal_mode, "g" => { "Goto" + "g" => extend_to_file_start, + "e" => extend_to_last_line, "k" => extend_line_up, "j" => extend_line_down, "w" => extend_to_word, diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 15d2aa53b..9cddd8250 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -90,7 +90,7 @@ macro_rules! keymap { }; (@trie [$($cmd:ident),* $(,)?]) => { - $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*]) + $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::MappableCommand::$cmd),*]) }; ( diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 03adeb05b..3c97a93cd 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -732,7 +732,7 @@ impl Component for Prompt { fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { let area = area .clip_left(self.prompt.len() as u16) - .clip_right(if self.prompt.len() > 0 { 0 } else { 2 }); + .clip_right(if self.prompt.is_empty() { 2 } else { 0 }); let anchor = self.anchor.min(self.line.len().saturating_sub(1)); let mut col = area.left() as usize diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd07..adf01bdad 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -226,36 +226,58 @@ fn render_diagnostics(context: &mut RenderContext, write: F) where F: Fn(&mut RenderContext, String, Option