diff --git a/Cargo.lock b/Cargo.lock index ede4ae0d38d5..f7812f4d4e08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,7 +728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c52b625ad8cc360a0b7f426266f21fb07bd49b8f4ccf1b3ca7bc89424db1dec4" dependencies = [ "git-hash", - "hashbrown 0.13.1", + "hashbrown 0.13.2", ] [[package]] @@ -1104,9 +1104,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash 0.8.2", ] @@ -1121,7 +1121,7 @@ dependencies = [ "chrono", "encoding_rs", "etcetera", - "hashbrown 0.13.1", + "hashbrown 0.13.2", "helix-loader", "imara-diff", "log", @@ -1379,6 +1379,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indoc" version = "1.0.8" @@ -1427,9 +1437,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libloading" @@ -1546,6 +1556,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1867,6 +1886,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c68e921cef53841b8925c2abadd27c9b891d9613bdc43d6b823062866df38e8" +dependencies = [ + "serde", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -2122,9 +2150,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.24.1" +version = "1.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" dependencies = [ "autocfg", "bytes", @@ -2164,11 +2192,36 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.10" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +checksum = "729bfd096e40da9c001f778f5cdecbd2957929a24e10e5883d9392220a751581" dependencies = [ + "indexmap", + "nom8", "serde", + "serde_spanned", + "toml_datetime", ] [[package]] @@ -2336,9 +2389,9 @@ checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "which" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", diff --git a/book/src/commands.md b/book/src/commands.md index b18ed1a5d7bb..6ae4185048c1 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -1,6 +1,5 @@ # Commands -Command mode, similar to Vim, can be activated by pressing `:`. The built-in -commands are: +Command mode, similar to Vim, can be activated by pressing `:`. The built-in commands are: {{#include ./generated/typable-cmd.md}} diff --git a/book/src/configuration.md b/book/src/configuration.md index 604bfdb3ff2a..a3d017e13dc8 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -6,8 +6,7 @@ in your config directory: - Linux and Mac: `~/.config/helix/config.toml` - Windows: `%AppData%\helix\config.toml` -> πŸ’‘ You can easily open the config file by typing `:config-open` within Helix -> normal mode. +> πŸ’‘ You can easily open the config file by typing `:config-open` within Helix normal mode. Example config: @@ -28,36 +27,35 @@ hidden = false ``` You can use a custom configuration file by specifying it with the `-c` or -`--config` command line argument, for example -`hx -c path/to/custom-config.toml`. Additionally, you can reload the -configuration file by sending the USR1 signal to the Helix process on Unix -operating systems, such as by using the command `pkill -USR1 hx`. +`--config` command line argument, for example `hx -c path/to/custom-config.toml`. +Additionally, you can reload the configuration file by sending the USR1 +signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`. ## Editor ### `[editor]` Section -| Key | Description | Default | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `5` | -| `mouse` | Enable mouse mode. | `true` | -| `middle-click-paste` | Middle click paste support. | `true` | -| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | -| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | -| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` | -| `cursorline` | Highlight all lines with a cursor. | `false` | -| `cursorcolumn` | Highlight all columns with a cursor. | `false` | -| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | -| `auto-completion` | Enable automatic pop up of auto-completion. | `true` | -| `auto-format` | Enable automatic formatting on save. | `true` | -| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` | -| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | -| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | -| `auto-info` | Whether to display infoboxes | `true` | -| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | -| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | -| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | -| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | +| Key | Description | Default | +|--|--|---------| +| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `5` | +| `mouse` | Enable mouse mode. | `true` | +| `middle-click-paste` | Middle click paste support. | `true` | +| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | +| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | +| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` | +| `cursorline` | Highlight all lines with a cursor. | `false` | +| `cursorcolumn` | Highlight all columns with a cursor. | `false` | +| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | +| `auto-completion` | Enable automatic pop up of auto-completion. | `true` | +| `auto-format` | Enable automatic formatting on save. | `true` | +| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` | +| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | +| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | +| `auto-info` | Whether to display infoboxes | `true` | +| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | +| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | +| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | +| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | ### `[editor.statusline]` Section @@ -127,10 +125,10 @@ The following statusline elements can be configured: ### `[editor.cursor-shape]` Section -Defines the shape of cursor in each mode. Valid values for these options are -`block`, `bar`, `underline`,or `hidden`. +Defines the shape of cursor in each mode. +Valid values for these options are `block`, `bar`, `underline`, or `hidden`. -> πŸ’‘Due to limitations of the terminal environment, only the primary cursor can +> πŸ’‘ Due to limitations of the terminal environment, only the primary cursor can > change shape. | Key | Description | Default | @@ -145,20 +143,20 @@ Defines the shape of cursor in each mode. Valid values for these options are ### `[editor.file-picker]` Section -Set options for file picker and global search. Ignoring a file means it is not -visible in the Helix file picker and global search. +Sets options for file picker and global search. Ignoring a file means it is +not visible in the Helix file picker and global search. All git related options are only enabled in a git repository. -| Key | Description | Default | -| ------------- | -------------------------------------------------------------------------------------------------------- | ------------------- | -| `hidden` | Enables ignoring hidden files. | true | -| `parents` | Enables reading ignore files from parent directories. | true | -| `ignore` | Enables reading `.ignore` files. | true | -| `git-ignore` | Enables reading `.gitignore` files. | true | -| `git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludefile` 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`. | +| Key | Description | Default | +|--|--|---------| +|`hidden` | Enables ignoring hidden files. | true +|`parents` | Enables reading ignore files from parent directories. | true +|`ignore` | Enables reading `.ignore` files. | true +|`git-ignore` | Enables reading `.gitignore` files. | true +|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludefile` 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`. ### `[editor.auto-pairs]` Section @@ -209,10 +207,10 @@ name = "rust" Search specific options. -| Key | Description | Default | -| ------------- | -------------------------------------------------------------------------------------------------- | ------- | -| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` | -| `wrap-around` | Whether the search should wrap after depleting the matches | `true` | +| Key | Description | Default | +|--|--|---------| +| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` | +| `wrap-around`| Whether the search should wrap after depleting the matches | `true` | ### `[editor.whitespace]` Section @@ -262,3 +260,54 @@ render = true character = "β•Ž" # Some characters that work well: "▏", "┆", "β”Š", "βΈ½" skip-levels = 1 ``` + +### `[editor.gutters]` Section + +For simplicity, `editor.gutters` accepts an array of gutter types, which will +use default settings for all gutter components. + +```toml +[editor] +gutters = ["diff", "diagnostics", "line-numbers", "spacer"] +``` + +To customize the behavior of gutters, the `[editor.gutters]` section must +be used. This section contains top level settings, as well as settings for +specific gutter components as sub-sections. + +| Key | Description | Default | +| --- | --- | --- | +| `layout` | A vector of gutters to display | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | + +Example: + +```toml +[editor.gutters] +layout = ["diff", "diagnostics", "line-numbers", "spacer"] +``` + +#### `[editor.gutters.line-numbers]` Section + +Options for the line number gutter + +| Key | Description | Default | +| --- | --- | --- | +| `min-width` | The minimum number of characters to use | `3` | + +Example: + +```toml +[editor.gutters.line-numbers] +min-width = 1 +``` + +#### `[editor.gutters.diagnotics]` Section + +Currently unused + +#### `[editor.gutters.diff]` Section + +Currently unused + +#### `[editor.gutters.spacer]` Section + diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 00e6a91e2a1c..1711ec36d5bf 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -109,6 +109,7 @@ | ron | βœ“ | | βœ“ | | | ruby | βœ“ | βœ“ | βœ“ | `solargraph` | | rust | βœ“ | βœ“ | βœ“ | `rust-analyzer` | +| sage | βœ“ | βœ“ | | | | scala | βœ“ | | βœ“ | `metals` | | scheme | βœ“ | | | | | scss | βœ“ | | | `vscode-css-language-server` | @@ -129,7 +130,7 @@ | twig | βœ“ | | | | | typescript | βœ“ | βœ“ | βœ“ | `typescript-language-server` | | ungrammar | βœ“ | | | | -| v | βœ“ | | | `vls` | +| v | βœ“ | | | `v` | | vala | βœ“ | | | `vala-language-server` | | verilog | βœ“ | βœ“ | | `svlangserver` | | vhs | βœ“ | | | | diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 3a59b5fdabf2..816826cfc125 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -50,10 +50,3 @@ below. grammars. - If a parser is causing a segfault or you want to remove it, make sure to remove the compiled parser located at `runtime/grammar/.so`. - -[language configuration section]: ../languages.md -[neovim-query-precedence]: - https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090 -[install-lsp-wiki]: - https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers -[lang-support]: ../lang-support.md diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md index be8d797a3612..be4b140d57f0 100644 --- a/book/src/guides/textobject.md +++ b/book/src/guides/textobject.md @@ -1,15 +1,15 @@ # Adding Textobject Queries -Helix supports textobjects that are language specific, such as functions, -classes, etc. These textobjects require an accompanying tree-sitter grammar and -a `textobjects.scm` query file to work properly. Tree-sitter allows us to query -the source code syntax tree and capture specific parts of it. The queries are -written in a lisp dialect. More information on how to write queries can be found -in the [official tree-sitter documentation][tree-sitter-queries]. +Helix supports textobjects that are language specific, such as functions, classes, etc. +These textobjects require an accompanying tree-sitter grammar and a `textobjects.scm` query file +to work properly. Tree-sitter allows us to query the source code syntax tree +and capture specific parts of it. The queries are written in a lisp dialect. +More information on how to write queries can be found in the [official tree-sitter +documentation][tree-sitter-queries]. Query files should be placed in `runtime/queries/{language}/textobjects.scm` -when contributing to Helix. Note that to test the query files locally you should -put them under your local runtime directory (`~/.config/helix/runtime` on Linux +when contributing to Helix. Note that to test the query files locally you should put +them under your local runtime directory (`~/.config/helix/runtime` on Linux for example). The following [captures][tree-sitter-captures] are recognized: @@ -31,23 +31,18 @@ repository. ## Queries for Textobject Based Navigation -Tree-sitter based navigation in Helix is done using captures in the following -order: +Tree-sitter based navigation in Helix is done using captures in the +following order: - `object.movement` - `object.around` - `object.inside` -For example if a `function.around` capture has been already defined for a -language in its `textobjects.scm` file, function navigation should also work -automatically. `function.movement` should be defined only if the node captured -by `function.around` doesn't make sense in a navigation context. - -[textobjects]: ../usage.md#textobjects -[textobjects-nav]: ../usage.md#tree-sitter-textobject-based-navigation -[tree-sitter-queries]: - https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax -[tree-sitter-captures]: - https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes -[textobject-examples]: - https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l= +For example if a `function.around` capture has been already defined for a language +in its `textobjects.scm` file, function navigation should also work automatically. +`function.movement` should be defined only if the node captured by `function.around` +doesn't make sense in a navigation context. + +[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax +[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes +[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l= diff --git a/book/src/install.md b/book/src/install.md index c7565fb252d0..cbf405cf0749 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -1,7 +1,6 @@ # Installing Helix - - [Installing Helix](#installing-helix) - [Using the Pre-built Binaries](#using-the-pre-built-binaries) - [Installing Helix on Linux through the Official Package Manager](#installing-helix-on-linux-through-the-official-package-manager) @@ -33,7 +32,7 @@ line. ## Installing Helix on Linux through the Official Package Manager If your Linux distribution has Helix available through its official package -manager, install it through that. The following list shows availability +manager, install it through that. The following shows availability throughout the Linux ecosystem: [![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) @@ -51,8 +50,7 @@ Helix is available for the following versions of Ubuntu: - 22.04 LTS (Jammy Jellyfish) - 22.10 (Kinetic Kudu) -Via -[Maveonair's PPA](https://launchpad.net/~maveonair/+archive/ubuntu/helix-editor): +Via [Maveonair's PPA](https://launchpad.net/~maveonair/+archive/ubuntu/helix-editor) ```sh sudo add-apt-repository ppa:maveonair/helix-editor @@ -97,8 +95,8 @@ brew install helix ## Installing Helix on Windows -Install on Windows using [Scoop](https://scoop.sh/), -[Chocolatey](https://chocolatey.org/) or [MSYS2](https://msys2.org/). +Install on Windows using [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/) +or [MSYS2](https://msys2.org/). **Scoop:** @@ -120,7 +118,7 @@ For 64-bit Windows 8.1 or above: pacman -S mingw-w64-ucrt-x86_64-helix ``` -## Building from Source +2. Compile Helix: 1. Clone the repository: @@ -139,7 +137,14 @@ This command will create the `hx` executable and construct the tree-sitter grammars in the `runtime` folder, or in the folder specified in `HELIX_RUNTIME` (as described below). -3. Configure Helix's runtime files +> πŸ’‘ If you are using the musl-libc instead of glibc the following environment variable must be set during the build +> to ensure tree sitter grammars can be loaded correctly: +> +> ```sh +> RUSTFLAGS="-C target-feature=-crt-static" +> ``` + +3. Configure Helix's runtime files **IMPORTANT**: The runtime files must be accessible to the newly created binary. They are currently located in the source code `runtime` directory. To make them @@ -162,7 +167,7 @@ Either, Or, -2. Create a symlink in `~/.config/helix/` that links to the source code +2. Create a symlink in `~/.config/helix` that links to the source code directory. ```sh @@ -174,7 +179,7 @@ And optionally: 3. Configure the Desktop Shortcut If your desktop environment supports the -[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), +[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html) you can configure Helix to show up in the application menu by copying the provided `.desktop` and icon files to their correct folders: @@ -198,7 +203,7 @@ Either, 1. Set the `HELIX_RUNTIME` environment variable on your system to tell Helix where to find the runtime files. - You can either do this using the Windows settings (search for + You can either do this using the Windows setting (search for `Edit environment variables for your account`) or use the `setx` command in Cmd: diff --git a/book/src/keymap.md b/book/src/keymap.md index 019073e3ea0e..72a8d54c5520 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -27,35 +27,35 @@ ### Movement -> πŸ’‘ Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line. - -| Key | Description | Command | -| -------------------- | ------------------------------------------- | --------------------------- | -| `h`, `Left` | Move left | `move_char_left` | -| `j`, `Down` | Move down | `move_line_down` | -| `k`, `Up` | Move up | `move_line_up` | -| `l`, `Right` | Move right | `move_char_right` | -| `w` | Move next word start | `move_next_word_start` | -| `b` | Move previous word start | `move_prev_word_start` | -| `e` | Move next word end | `move_next_word_end` | -| `W` | Move next WORD start | `move_next_long_word_start` | -| `B` | Move previous WORD start | `move_prev_long_word_start` | -| `E` | Move next WORD end | `move_next_long_word_end` | -| `t` | Find 'till next char | `find_till_char` | -| `f` | Find next char | `find_next_char` | -| `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` | -| `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` | -| `Ctrl-f`, `PageDown` | Move page down | `page_down` | -| `Ctrl-u` | Move half page up | `half_page_up` | -| `Ctrl-d` | Move half page down | `half_page_down` | -| `Ctrl-i` | Jump forward on the jump list | `jump_forward` | -| `Ctrl-o` | Jump backward on the jump list | `jump_backward` | -| `Ctrl-s` | Save the current selection to the jump list | `save_selection` | +> NOTE: Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line. + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `h`, `Left` | Move left | `move_char_left` | +| `j`, `Down` | Move down | `move_line_down` | +| `k`, `Up` | Move up | `move_line_up` | +| `l`, `Right` | Move right | `move_char_right` | +| `w` | Move next word start | `move_next_word_start` | +| `b` | Move previous word start | `move_prev_word_start` | +| `e` | Move next word end | `move_next_word_end` | +| `W` | Move next WORD start | `move_next_long_word_start` | +| `B` | Move previous WORD start | `move_prev_long_word_start` | +| `E` | Move next WORD end | `move_next_long_word_end` | +| `t` | Find 'till next char | `find_till_char` | +| `f` | Find next char | `find_next_char` | +| `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` | +| `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` | +| `Ctrl-f`, `PageDown` | Move page down | `page_down` | +| `Ctrl-u` | Move half page up | `half_page_up` | +| `Ctrl-d` | Move half page down | `half_page_down` | +| `Ctrl-i` | Jump forward on the jump list | `jump_forward` | +| `Ctrl-o` | Jump backward on the jump list | `jump_backward` | +| `Ctrl-s` | Save the current selection to the jump list | `save_selection` | ### Changes @@ -105,43 +105,42 @@ ### Selection manipulation -| Key | Description | Command | -| -------------------- | ------------------------------------------------------------- | ------------------------------------ | -| `s` | Select all regex matches inside selections | `select_regex` | -| `S` | Split selection into sub selections on regex matches | `split_selection` | -| `Alt-s` | Split selection on newlines | `split_selection_on_newline` | -| `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` | -| `&` | Align selection in columns | `align_selections` | -| `_` | Trim whitespace from the selection | `trim_selections` | -| `;` | Collapse selection onto a single cursor | `collapse_selection` | -| `Alt-;` | Flip selection cursor and anchor | `flip_selections` | -| `Alt-:` | Ensures the selection is in forward direction | `ensure_selections_forward` | -| `,` | Keep only the primary selection | `keep_primary_selection` | -| `Alt-,` | Remove the primary selection | `remove_primary_selection` | -| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` | -| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` | -| `(` | Rotate main selection backward | `rotate_selections_backward` | -| `)` | Rotate main selection forward | `rotate_selections_forward` | -| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` | -| `Alt-)` | Rotate selection contents forward | `rotate_selection_contents_forward` | -| `%` | Select entire file | `select_all` | -| `x` | Select current line, if already selected, extend to next line | `extend_line_below` | -| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | -| `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` | -| `J` | Join lines inside selection | `join_selections` | -| `Alt-J` | Join lines inside selection and select space | `join_selections_space` | -| `K` | Keep selections matching the regex | `keep_selections` | -| `Alt-K` | Remove selections matching the regex | `remove_selections` | -| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | -| `Alt-o`, `Alt-up` | Expand selection to parent syntax node (**TS**) | `expand_selection` | -| `Alt-i`, `Alt-down` | Shrink syntax tree object selection (**TS**) | `shrink_selection` | -| `Alt-p`, `Alt-left` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` | -| `Alt-n`, `Alt-right` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `s` | Select all regex matches inside selections | `select_regex` | +| `S` | Split selection into sub selections on regex matches | `split_selection` | +| `Alt-s` | Split selection on newlines | `split_selection_on_newline` | +| `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` | +| `&` | Align selection in columns | `align_selections` | +| `_` | Trim whitespace from the selection | `trim_selections` | +| `;` | Collapse selection onto a single cursor | `collapse_selection` | +| `Alt-;` | Flip selection cursor and anchor | `flip_selections` | +| `Alt-:` | Ensures the selection is in forward direction | `ensure_selections_forward` | +| `,` | Keep only the primary selection | `keep_primary_selection` | +| `Alt-,` | Remove the primary selection | `remove_primary_selection` | +| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` | +| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` | +| `(` | Rotate main selection backward | `rotate_selections_backward` | +| `)` | Rotate main selection forward | `rotate_selections_forward` | +| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` | +| `Alt-)` | Rotate selection contents forward | `rotate_selection_contents_forward` | +| `%` | Select entire file | `select_all` | +| `x` | Select current line, if already selected, extend to next line | `extend_line_below` | +| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | +| `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` | +| `J` | Join lines inside selection | `join_selections` | +| `Alt-J` | Join lines inside selection and select space | `join_selections_space` | +| `K` | Keep selections matching the regex | `keep_selections` | +| `Alt-K` | Remove selections matching the regex | `remove_selections` | +| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | +| `Alt-o`, `Alt-up` | Expand selection to parent syntax node (**TS**) | `expand_selection` | +| `Alt-i`, `Alt-down` | Shrink syntax tree object selection (**TS**) | `shrink_selection` | +| `Alt-p`, `Alt-left` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` | +| `Alt-n`, `Alt-right` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` | ### Search -Search commands operate on the `/` register by default. To use a different -register, use `".` +Search commands all operate on the `/` register by default. To use a different register, use `"`. | Key | Description | Command | | --- | ------------------------------------------- | ------------------ | @@ -153,8 +152,7 @@ register, use `".` ### Minor modes -Minor modes are accessible from normal mode and typically switch back to normal -mode after a command. +Minor modes are accessible from normal mode and typically switch back to normal mode after a command. | Key | Description | Command | | -------- | -------------------------------------------------- | -------------- | @@ -167,13 +165,17 @@ mode after a command. | `Ctrl-w` | Enter [window mode](#window-mode) | N/A | | `Space` | Enter [space mode](#space-mode) | N/A | +These modes (except command mode) can be configured by +[remapping keys](https://docs.helix-editor.com/remapping.html#minor-modes). + #### View mode -View mode is accessed by typing `z` in [normal mode](#normal-mode) and is -intended for scrolling and manipulating the view without changing the selection. -The "sticky" variant of this mode (accessed by typing `Z` in normal mode) is -persistent and can be exited using the escape key. This is useful when you're -simply looking over text and not actively editing it. +View mode is access by typing `z` in [normal mode](#normal-mode) +and is intended for scrolling and manipulating the view without changing +the selection. The "sticky" variant of this mode (accessed by typing `Z` in +normal mode) is persistent and can be exited using the escape key. This is +useful when you're simply looking over text and not actively editing it. + | Key | Description | Command | | -------------------- | --------------------------------------------------------- | ------------------- | @@ -193,26 +195,26 @@ simply looking over text and not actively editing it. Goto mode is accessed by typing `g` in [normal mode](#normal-mode), it jumps to various locations. -| Key | Description | Command | -| --- | ------------------------------------------------ | -------------------------- | -| `g` | Go to line number `` else start of file | `goto_file_start` | -| `e` | Go to the end of the file | `goto_last_line` | -| `f` | Go to files in the selection | `goto_file` | -| `h` | Go to the start of the line | `goto_line_start` | -| `l` | Go to the end of the line | `goto_line_end` | -| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` | -| `t` | Go to the top of the screen | `goto_window_top` | -| `c` | Go to the middle of the screen | `goto_window_center` | -| `b` | Go to the bottom of the screen | `goto_window_bottom` | -| `d` | Go to definition (**LSP**) | `goto_definition` | -| `y` | Go to type definition (**LSP**) | `goto_type_definition` | -| `r` | Go to references (**LSP**) | `goto_reference` | -| `i` | Go to implementation (**LSP**) | `goto_implementation` | -| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | -| `m` | Go to the last modified/alternate file | `goto_last_modified_file` | -| `n` | Go to next buffer | `goto_next_buffer` | -| `p` | Go to previous buffer | `goto_previous_buffer` | -| `.` | Go to last modification in current file | `goto_last_modification` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `g` | Go to line number `` else start of file | `goto_file_start` | +| `e` | Go to the end of the file | `goto_last_line` | +| `f` | Go to files in the selection | `goto_file` | +| `h` | Go to the start of the line | `goto_line_start` | +| `l` | Go to the end of the line | `goto_line_end` | +| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` | +| `t` | Go to the top of the screen | `goto_window_top` | +| `c` | Go to the middle of the screen | `goto_window_center` | +| `b` | Go to the bottom of the screen | `goto_window_bottom` | +| `d` | Go to definition (**LSP**) | `goto_definition` | +| `y` | Go to type definition (**LSP**) | `goto_type_definition` | +| `r` | Go to references (**LSP**) | `goto_reference` | +| `i` | Go to implementation (**LSP**) | `goto_implementation` | +| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | +| `m` | Go to the last modified/alternate file | `goto_last_modified_file` | +| `n` | Go to next buffer | `goto_next_buffer` | +| `p` | Go to previous buffer | `goto_previous_buffer` | +| `.` | Go to last modification in current file | `goto_last_modification` | #### Match mode @@ -234,8 +236,8 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`). #### Window mode -Window mode is accessed by typing `Ctrl-w` in [normal mode](#normal-mode), this -layer is similar to Vim keybindings as Kakoune does not support window. +Window mode is accessed by typing `Ctrl-w` in [normal mode](#normal-mode), +this layer is similar to Vim keybindings as Kakoune does not support windows. | Key | Description | Command | | ---------------------- | ---------------------------------------------------- | ----------------- | @@ -261,31 +263,30 @@ Space mode is accessed by typing `Space` in [normal mode](#normal-mode). This layer is a kludge of mappings, mostly pickers. -| Key | Description | Command | -| --- | ----------------------------------------------------------------------- | ----------------------------------- | -| `f` | Open file picker | `file_picker` | -| `F` | Open file picker at current working directory | `file_picker_in_current_directory` | -| `b` | Open buffer picker | `buffer_picker` | -| `j` | Open jump list picker | `jumplist_picker` | -| `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` | -| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | -| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` | -| `r` | Rename symbol (**LSP**) | `rename_symbol` | -| `a` | Apply code action (**LSP**) | `code_action` | -| `'` | Open last fuzzy picker | `last_picker` | -| `w` | Enter [window mode](#window-mode) | N/A | -| `p` | Paste system clipboard after selections | `paste_clipboard_after` | -| `P` | Paste system clipboard before selections | `paste_clipboard_before` | -| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | -| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | -| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | -| `/` | Global search in workspace folder | `global_search` | -| `?` | Open command palette | `command_palette` | - -> πŸ’‘ Global search displays results in a fuzzy picker, use `Space + '` to bring -> it back up after opening a file. +| Key | Description | Command | +| ----- | ----------- | ------- | +| `f` | Open file picker | `file_picker` | +| `F` | Open file picker at current working directory | `file_picker_in_current_directory` | +| `b` | Open buffer picker | `buffer_picker` | +| `j` | Open jump list picker | `jump list_picker` | +| `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` | +| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | +| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` | +| `r` | Rename symbol (**LSP**) | `rename_symbol` | +| `a` | Apply code action (**LSP**) | `code_action` | +| `'` | Open last fuzzy picker | `last_picker` | +| `w` | Enter [window mode](#window-mode) | N/A | +| `p` | Paste system clipboard after selections | `paste_clipboard_after` | +| `P` | Paste system clipboard before selections | `paste_clipboard_before` | +| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | +| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | +| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | +| `/` | Global search in workspace folder | `global_search` | +| `?` | Open command palette | `command_palette` | + +> TIP: Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file. ##### Popup @@ -299,15 +300,14 @@ content than fits on the screen, you can use scroll: #### Unimpaired -These mappings are in the style of -[vim-unimpaired](https://github.com/tpope/vim-unimpaired). +These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). | Key | Description | Command | -| -------- | -------------------------------------------- | --------------------- | -| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` | +| ----- | ----------- | ------- | | `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` | -| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` | +| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` | | `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` | +| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` | | `]f` | Go to next function (**TS**) | `goto_next_function` | | `[f` | Go to previous function (**TS**) | `goto_prev_function` | | `]t` | Go to next type definition (**TS**) | `goto_next_class` | @@ -322,34 +322,34 @@ These mappings are in the style of | `[p` | Go to previous paragraph | `goto_prev_paragraph` | | `]g` | Go to next change | `goto_next_change` | | `[g` | Go to previous change | `goto_prev_change` | -| `[G` | Go to first change | `goto_first_change` | | `]G` | Go to last change | `goto_last_change` | -| `[Space` | Add newline above | `add_newline_above` | +| `[G` | Go to first change | `goto_first_change` | | `]Space` | Add newline below | `add_newline_below` | +| `[Space` | Add newline above | `add_newline_above` | ## Insert mode -Insert mode bindings are minimal by default. Helix is designed to be a modal -editor, and this is reflected in the user experience and internal mechanics. -Changes to the text are only saved for undos when escaping from insert mode to -normal mode. - -> πŸ’‘ New users are strongly encouraged to learn the modal editing paradigm to -> get the smoothest experience. - -| Key | Description | Command | -| ------------------------- | ------------------------- | ------------------------ | -| `Escape` | Switch to normal mode | `normal_mode` | -| `Ctrl-s` | Commit undo checkpoint | `commit_undo_checkpoint` | -| `Ctrl-x` | Autocomplete | `completion` | -| `Ctrl-r` | Insert a register content | `insert_register` | -| `Ctrl-w`, `Alt-Backspace` | Delete previous word | `delete_word_backward` | -| `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` | -| `Ctrl-u` | Delete to start of line | `kill_to_line_start` | -| `Ctrl-k` | Delete to end of line | `kill_to_line_end` | -| `Ctrl-h`, `Backspace` | Delete previous char | `delete_char_backward` | -| `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` | -| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` | +Insert mode bindings are minimal by default. Helix is designed to +be a modal editor, and this is reflected in the user experience and internal +mechanics. Changes to the text are only saved for undos when +escaping from insert mode to normal mode. + +> πŸ’‘ New users are strongly encouraged to learn the modal editing paradigm +> to get the smoothest experience. + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `Escape` | Switch to normal mode | `normal_mode` | +| `Ctrl-s` | Commit undo checkpoint | `commit_undo_checkpoint` | +| `Ctrl-x` | Autocomplete | `completion` | +| `Ctrl-r` | Insert a register content | `insert_register` | +| `Ctrl-w`, `Alt-Backspace` | Delete previous word | `delete_word_backward` | +| `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` | +| `Ctrl-u` | Delete to start of line | `kill_to_line_start` | +| `Ctrl-k` | Delete to end of line | `kill_to_line_end` | +| `Ctrl-h`, `Backspace` | Delete previous char | `delete_char_backward` | +| `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` | +| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` | These keys are not recommended, but are included for new users less familiar with modal editors. @@ -382,9 +382,10 @@ end = "no_op" ## Select / extend mode -Select mode echoes Normal mode, but changes any movements to extend selections -rather than replace them. Goto motions are also changed to extend, so that `vgl` -for example extends the selection to the end of the line. +Select mode echoes Normal mode, but changes any movements to extend +selections rather than replace them. Goto motions are also changed to +extend, so that `vgl` for example extends the selection to the end of +the line. Search is also affected. By default, `n` and `N` will remove the current selection and select the next instance of the search term. Toggling this mode diff --git a/book/src/lang-support.md b/book/src/lang-support.md index 930c9a20057e..add64a0a9571 100644 --- a/book/src/lang-support.md +++ b/book/src/lang-support.md @@ -1,11 +1,10 @@ # Language Support -The following languages and Language Servers are supported. To use Language -Server features, you must first [install][lsp-install-wiki] the appropriate -Language Server. +The following languages and Language Servers are supported. To use +Language Server features, you must first [install][lsp-install-wiki] the +appropriate Language Server. -You can check the language support in your installed Helix version with -`hx --health`. +You can check the language support in your installed helix version with `hx --health`. Also see the [Language Configuration][lang-config] docs and the [Adding Languages][adding-languages] guide for more language configuration information. diff --git a/book/src/languages.md b/book/src/languages.md index e2af50d0c2c8..963827a2d4ba 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -8,7 +8,7 @@ Language-specific settings and settings for language servers are configured in There are three possible locations for a `languages.toml` file: 1. In the Helix source code, this lives in the - [Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml). + [Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml) It provides the default configurations for languages and language servers. 2. In your [configuration directory](./configuration.md). This overrides values @@ -67,8 +67,8 @@ These configuration keys are available: ### File-type detection and the `file-types` key -Helix determines which language configuration to use based on the `file-types` -key from the above section. `file-types` is a list of strings or tables, for +Helix determines which language configuration to use based on the `file-types` key +from the above section. `file-types` is a list of strings or tables, for example: ```toml diff --git a/book/src/remapping.md b/book/src/remapping.md index b08f8f25f291..5b081cf16308 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -1,10 +1,10 @@ # Key Remapping -Helix currently supports one-way key remapping through a simple TOML -configuration file. (More powerful solutions such as rebinding via commands will -be available in the future). +Helix currently supports one-way key remapping through a simple TOML configuration +file. (More powerful solutions such as rebinding via commands will be +available in the future). -To remap keys, create a `config.toml` file in your `Helix` configuration +To remap keys, create a `config.toml` file in your `helix` configuration directory (default `~/.config/helix` on Linux systems) with a structure like this: @@ -30,8 +30,31 @@ j = { k = "normal_mode" } # Maps `jk` to exit insert mode ``` -> NOTE: Bindings can be nested, to create (or edit) minor modes: -> `g = { a = "code_action"}` adds a new entry to the `goto` mode. +## Minor modes + +Minor modes are accessed by pressing a key (usually from normal mode), giving access to dedicated bindings. Bindings +can be modified or added by nesting definitions. + +```toml +[keys.insert.j] +k = "normal_mode" # Maps `jk` to exit insert mode + +[keys.normal.g] +a = "code_action" # Maps `ga` to show possible code actions + +# invert `j` and `k` in view mode +[keys.normal.z] +j = "scroll_up" +k = "scroll_down" + +# create a new minor mode bound to `+` +[keys.normal."+"] +m = ":run-shell-command make" +c = ":run-shell-command cargo build" +t = ":run-shell-command cargo test" +``` + +## Special keys and modifiers Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes `C-`, `S-` and `A-`. Special keys are encoded as follows: @@ -58,9 +81,6 @@ Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes `C-`, Keys can be disabled by binding them to the `no_op` command. -You can find a list of available commands at -[Keymap](https://docs.helix-editor.com/keymap.html) +You can find a list of available commands in the [Keymap](https://docs.helix-editor.com/keymap.html) documentation. -> Commands can also be found in the source code at -> [`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 and the `TypableCommandList`. +> Commands can also be found in the source code at [`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 and the `TypableCommandList`. diff --git a/book/src/themes.md b/book/src/themes.md index 33d319bd6bb2..6af7d8c69eaf 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -1,8 +1,6 @@ # Themes -To use a theme, add `theme = ""` to the top of your -[`config.toml`](./configuration.md) file, or select it during runtime using -`:theme `. +To use a theme add `theme = ""` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme `. ## Creating a Theme @@ -11,13 +9,18 @@ To use a theme, add `theme = ""` to the top of your To create a theme file: 1. Create a 'themes' folder in your user configuration folder (e.g. - `~/.config/helix/themes`) + `~/.config/helix/themes`). 2. Create a file with the name of your theme as the file name (e.g. `mytheme.toml`) and place it in your `themes` folder. > πŸ’‘ The names "default" and "base16_default" are reserved for built-in themes > and cannot be overridden by user-defined themes. +### An overview of the Theme File Format + +> πŸ’‘ The names "default" and "base16_default" are reserved for built-in themes +> and cannot be overridden by user-defined themes. + ### An Overview of the Theme File Format Each line in the theme file is specified as follows: @@ -26,10 +29,7 @@ Each line in the theme file is specified as follows: key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] } ``` -Where `key` represents what you want to style, `fg` specifies the foreground -color, `bg` the background color, `underline` the underline `style`/`color`, and -`modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can -be omitted to defer to the defaults. +Where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline `style`/`color`, and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. To specify only the foreground color: @@ -37,8 +37,7 @@ To specify only the foreground color: key = "#ffffff" ``` -If the key contains a dot `'.'`, it must be quoted to prevent it being parsed as -a [dotted key](https://toml.io/en/v1.0.0#keys). +If the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys). ```toml "key.key" = "#ffffff" @@ -55,16 +54,16 @@ If you plan to submit your theme for inclusion in Helix, it is recommended to use the supplied linting tool to ensure compliance with the specifications: ```sh -cargo xtask themelint onedark # replace onedark with +cargo xtask themelint onedark # replace onedark with ``` ## The Details of Theme Creation ### Color palettes -It's recommended to define a palette of named colors and refer to them in the -configuration values in your theme. To do this, add a table called `palette` to -your theme file: +It's recommended to define a palette of named colors, and refer to them in the +configuration values in your theme. To do this, add a table called +`palette` to your theme file: ```toml "ui.background" = "white" @@ -75,8 +74,8 @@ white = "#ffffff" black = "#000000" ``` -Keep in mind that the [palette] table includes all keys after its header, so it -should be defined after the normal theme options. +Keep in mind that the `[palette]` table includes all keys after its header, +so it should be defined after the normal theme options. The default palette uses the terminal's default 16 colors, and the colors names are listed below. The `[palette]` section in the config file takes precedence @@ -103,29 +102,37 @@ over it and is merged into the default palette. ### Modifiers -The following values can be used as modifiers, providing they are supported by +The following values may be used as modifier, provided they are supported by your terminal emulator. -| Modifier | -| ------------- | -| `bold` | -| `dim` | -| `italic` | -| `underlined` | -| `slow_blink` | -| `rapid_blink` | -| `reversed` | -| `hidden` | -| `crossed_out` | - -> πŸ’‘ The `underlined` modifier is deprecated and only available for backwards -> compatibility. Its behavior is equivalent to setting `underline.style="line"`. +| Modifier | +| --- | +| `bold` | +| `dim` | +| `italic` | +| `underlined` | +| `slow_blink` | +| `rapid_blink` | +| `reversed` | +| `hidden` | +| `crossed_out` | + +> πŸ’‘ The `underlined` modifier is deprecated and only available for backwards compatibility. +> Its behavior is equivalent to setting `underline.style="line"`. ### Underline Style -One of the following values can be used for `underline.style`, providing it is +One of the following values may be used as a value for `underline.style`, providing it is supported by your terminal emulator. +| Modifier | +| --- | +| `line` | +| `curl` | +| `dashed` | +| `dotted` | +| `double_line` | + | Modifier | | ------------- | | `line` | @@ -136,7 +143,7 @@ supported by your terminal emulator. ### Inheritance -Extends other themes by setting the `inherits` property to an existing theme. +Extend other themes by setting the `inherits` property to an existing theme. ```toml inherits = "boo_berry" @@ -158,9 +165,7 @@ The following is a list of scopes available to use for styling: These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). -When determining styling for a highlight, the longest matching theme key will be -used. For example, if the highlight is `function.builtin.static,` the key -`function.builtin` will be used instead of function. +When determining styling for a highlight, the longest matching theme key will be used. For example, if the highlight is `function.builtin.static`, the key `function.builtin` will be used instead of `function`. We use a similar set of scopes as [Sublime Text](https://www.sublimetext.com/docs/scope_naming.html). See also @@ -174,10 +179,8 @@ We use a similar set of scopes as - `variant` - `constructor` -- `constant` (TODO: constant.other.placeholder for `%v)` - - - `builtin` Special constants provided by the language (`true`, `false`, `nil` - etc.) +- `constant` (TODO: constant.other.placeholder for `%v`) + - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) - `boolean` - `character` - `escape` @@ -285,53 +288,58 @@ These scopes are used for theming the editor interface: - `completion` - for completion doc popup UI - `hover` - for hover popup UI -| Key | Notes | -| --------------------------- | ------------------------------------------------------------------------------------------------ | -| `ui.background` | | -| `ui.background.separator` | Picker separator below input line | -| `ui.cursor` | | -| `ui.cursor.insert` | | -| `ui.cursor.select` | | -| `ui.cursor.match` | Matching bracket etc. | -| `ui.cursor.primary` | Cursor with primary selection | -| `ui.gutter` | Gutter | -| `ui.gutter.selected` | Gutter for the line the cursor is on | -| `ui.linenr` | Line numbers | -| `ui.linenr.selected` | Line number for the line the cursor is on | -| `ui.statusline` | `statusline` | -| `ui.statusline.inactive` | `statusline` (unfocused document) | -| `ui.statusline.normal` | `statusline` mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) | -| `ui.statusline.insert` | `statusline` mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) | -| `ui.statusline.select` | `statusline` mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) | -| `ui.statusline.separator` | Separator character in `statusline` | -| `ui.popup` | Documentation popups (e.g. Space + k) | -| `ui.popup.info` | Prompt for multiple key options | -| `ui.window` | Borderlines separating splits | -| `ui.help` | Description box for commands | -| `ui.text` | Command prompts, popup text, etc. | -| `ui.text.focus` | | -| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) | -| `ui.text.info` | The key: command text in `ui.popup.info` boxes | -| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | -| `ui.virtual.whitespace` | Visible whitespace characters | -| `ui.virtual.indent-guide` | Vertical indent width guides | -| `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 | -| `ui.selection` | For selections in the editing area | -| `ui.selection.primary` | | -| `ui.cursorline.primary` | The line of the primary cursor ([if `cursorline` is enabled][editor-section]) | -| `ui.cursorline.secondary` | The lines of any other cursors ([if `cursorline` is enabled][editor-section]) | -| `ui.cursorcolumn.primary` | The column of the primary cursor ([if `cursorcolumn` is enabled][editor-section]) | -| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if `cursorcolumn` is enabled][editor-section]) | -| `warning` | Diagnostics warning (gutter) | -| `error` | Diagnostics error (gutter) | -| `info` | Diagnostics info (gutter) | -| `hint` | Diagnostics hint (gutter) | -| `diagnostic` | Diagnostics fallback style (editing area) | -| `diagnostic.hint` | Diagnostics hint (editing area) | -| `diagnostic.info` | Diagnostics info (editing area) | -| `diagnostic.warning` | Diagnostics warning (editing area) | -| `diagnostic.error` | Diagnostics error (editing area) | + +| Key | Notes | +| --- | --- | +| `ui.background` | | +| `ui.background.separator` | Picker separator below input line | +| `ui.cursor` | | +| `ui.cursor.normal` | | +| `ui.cursor.insert` | | +| `ui.cursor.select` | | +| `ui.cursor.match` | Matching bracket etc. | +| `ui.cursor.primary` | Cursor with primary selection | +| `ui.cursor.primary.normal` | | +| `ui.cursor.primary.insert` | | +| `ui.cursor.primary.select` | | +| `ui.gutter` | Gutter | +| `ui.gutter.selected` | Gutter for the line the cursor is on | +| `ui.linenr` | Line numbers | +| `ui.linenr.selected` | Line number for the line the cursor is on | +| `ui.statusline` | Statusline | +| `ui.statusline.inactive` | Statusline (unfocused document) | +| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) | +| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) | +| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) | +| `ui.statusline.separator` | Separator character in statusline | +| `ui.popup` | Documentation popups (e.g Space + k) | +| `ui.popup.info` | Prompt for multiple key options | +| `ui.window` | Border lines separating splits | +| `ui.help` | Description box for commands | +| `ui.text` | Command prompts, popup text, etc. | +| `ui.text.focus` | | +| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) | +| `ui.text.info` | The key: command text in `ui.popup.info` boxes | +| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | +| `ui.virtual.whitespace` | Visible whitespace characters | +| `ui.virtual.indent-guide` | Vertical indent width guides | +| `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 | +| `ui.selection` | For selections in the editing area | +| `ui.selection.primary` | | +| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) | +| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) | +| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) | +| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) | +| `warning` | Diagnostics warning (gutter) | +| `error` | Diagnostics error (gutter) | +| `info` | Diagnostics info (gutter) | +| `hint` | Diagnostics hint (gutter) | +| `diagnostic` | Diagnostics fallback style (editing area) | +| `diagnostic.hint` | Diagnostics hint (editing area) | +| `diagnostic.info` | Diagnostics info (editing area) | +| `diagnostic.warning` | Diagnostics warning (editing area) | +| `diagnostic.error` | Diagnostics error (editing area) | [editor-section]: ./configuration.md#editor-section diff --git a/book/src/usage.md b/book/src/usage.md index 747c05d532d6..8cac7149fe50 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -1,7 +1,6 @@ # Using Helix - - [Using Helix](#using-helix) - [Registers](#registers) - [User-defined Registers](#user-defined-registers) @@ -34,8 +33,7 @@ example: - `"ay` - Yank the current selection to register `a`. - `"op` - Paste the text in register `o` after the selection. -If a register is selected before invoking a change or delete command, the -selection will be stored in the register and the action will be carried out: +If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out: - `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode). @@ -50,19 +48,15 @@ selection will be stored in the register and the action will be carried out: | `"` | Last yanked text | | `_` | Black hole | -The system clipboard is not directly supported by a built-in register. Instead, -special commands and keybindings are provided. Refer to the +The system clipboard is not directly supported by a built-in register. Instead, special commands and keybindings are provided. Refer to the [key map](keymap.md#space-mode) for more details. -The black hole register is a no-op register, meaning that no data will be read -or written to it. +The black hole register is a no-op register, meaning that no data will be read or written to it. ## Surround -Helix includes built-in functionality similar to -[vim-surround](https://github.com/tpope/vim-surround). The key mappings for this -functionality have been inspired by -[vim-sandwich](https://github.com/machakann/vim-sandwich). +Helix includes built-in functionality similar to [vim-surround](https://github.com/tpope/vim-surround). +The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich): ![Surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif) @@ -74,8 +68,7 @@ functionality have been inspired by You can use counts to act on outer pairs. -Surround can also act on multiple selections. For example, to change every -occurrence of `(use)` to `[use]`: +Surround can also act on multiple selections. For example, to change every occurrence of `(use)` to `[use]`: 1. `%` to select the whole file 2. `s` to split the selections on a search term @@ -86,9 +79,9 @@ Multiple characters are currently not supported, but planned for future release. ## Moving the Primary Selection with Syntax-tree Motions -`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to -move the primary selection according to its location in the syntax tree. For -example, many languages have the following syntax for function calls: +`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the primary +selection according to its location in the syntax tree. For example, many languages have the +following syntax for function calls: ```js func(arg1, arg2, arg3); @@ -129,7 +122,10 @@ If you have a selection that wraps `arg1` (see the tree above), and you use Alt-n, it will select the next sibling in the syntax tree: `arg2`. ```js -func([arg1], arg2, arg3) > func(arg1, [arg2], arg3); +// before +func([arg1], arg2, arg3) +// after +func(arg1, [arg2], arg3); ``` Similarly, Alt-o will expand the selection to the parent node, in this case, the @@ -173,24 +169,25 @@ function or block of code. | `t` | Test | | `g` | Change | -> πŸ’‘`f`, `c`, etc. need a tree-sitter grammar active for the current document -> and a special tree-sitter query file to work properly. [Only some -> grammars][lang-support] currently have the query file implemented. -> Contributions are welcome! +> πŸ’‘ `f`, `c`, etc need a tree-sitter grammar active for the current +document and a special tree-sitter query file to work properly. [Only +some grammars][lang-support] currently have the query file implemented. +Contributions are welcome! -## Navigating Using Tree-sitter Textobjects +## Navigating Using Tree-sitter Textobject Navigating between functions, classes, parameters, and other elements is -possible using tree-sitter and Textobject queries. For example, to move to the -next function use `]f`, to move to previous class use `[c`, and so on. +possible using tree-sitter and Textobject queries. For +example to move to the next function use `]f`, to move to previous +class use `[c`, and so on. ![tree-sitter-nav-demo][tree-sitter-nav-demo] -For the full reference see the [unimpaired][unimpaired-keybinds] section of the -key bind documentation. +For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind +documentation. -> πŸ’‘ This feature relies on tree-sitter Textobjects and requires the -> corresponding query file to work properly. +> πŸ’‘ This feature relies on tree-sitter Textobjects +> and requires the corresponding query file to work properly. [lang-support]: ./lang-support.md [unimpaired-keybinds]: ./keymap.md#unimpaired diff --git a/book/theme/css/variables.css b/book/theme/css/variables.css index 1bf91b19aeda..5d0978cc3e5f 100644 --- a/book/theme/css/variables.css +++ b/book/theme/css/variables.css @@ -48,6 +48,18 @@ --searchresults-border-color: #888; --searchresults-li-bg: #252932; --search-mark-bg: #e3b171; + --hljs-background: #191f26; + --hljs-color: #e6e1cf; + --hljs-quote: #5c6773; + --hljs-variable: #ff7733; + --hljs-type: #ffee99; + --hljs-title: #b8cc52; + --hljs-symbol: #ffb454; + --hljs-selector-tag: #ff7733; + --hljs-selector-tag: #36a3d9; + --hljs-selector-tag: #00568d; + --hljs-selector-tag: #91b362; + --hljs-selector-tag: #d96c75; } .coal { @@ -88,6 +100,18 @@ --searchresults-border-color: #98a3ad; --searchresults-li-bg: #2b2b2f; --search-mark-bg: #355c7d; + --hljs-background: #969896; + --hljs-color: #cc6666; + --hljs-quote: #de935f; + --hljs-variable: #f0c674; + --hljs-type: #b5bd68; + --hljs-title: #8abeb7; + --hljs-symbol: #81a2be; + --hljs-selector-tag: #b294bb; + --hljs-selector-tag: #1d1f21; + --hljs-selector-tag: #c5c8c6; + --hljs-selector-tag: #718c00; + --hljs-selector-tag: #c82829; } .light { @@ -128,6 +152,14 @@ --searchresults-border-color: #888; --searchresults-li-bg: #e4f2fe; --search-mark-bg: #a2cff5; + --hljs-background: #f6f7f6; + --hljs-color: #000; + --hljs-quote: #575757; + --hljs-variable: #d70025; + --hljs-type: #b21e00; + --hljs-title: #0030f2; + --hljs-symbol: #008200; + --hljs-selector-tag: #9d00ec; } .navy { @@ -168,6 +200,19 @@ --searchresults-border-color: #5c5c68; --searchresults-li-bg: #242430; --search-mark-bg: #a2cff5; + + --hljs-background: #969896; + --hljs-color: #cc6666; + --hljs-quote: #de935f; + --hljs-variable: #f0c674; + --hljs-type: #b5bd68; + --hljs-title: #8abeb7; + --hljs-symbol: #81a2be; + --hljs-selector-tag: #b294bb; + --hljs-selector-tag: #1d1f21; + --hljs-selector-tag: #c5c8c6; + --hljs-selector-tag: #718c00; + --hljs-selector-tag: #c82829; } .rust { @@ -208,6 +253,14 @@ --searchresults-border-color: #888; --searchresults-li-bg: #dec2a2; --search-mark-bg: #e69f67; + --hljs-background: #f6f7f6; + --hljs-color: #000; + --hljs-quote: #575757; + --hljs-variable: #d70025; + --hljs-type: #b21e00; + --hljs-title: #0030f2; + --hljs-symbol: #008200; + --hljs-selector-tag: #9d00ec; } @media (prefers-color-scheme: dark) { @@ -292,7 +345,15 @@ --searchresults-header-fg: #5f5f71; --searchresults-border-color: #5c5c68; --searchresults-li-bg: #242430; - --search-mark-bg: #a2cff5; + --search-mark-bg: #acff5; + --hljs-background: #2f1e2e; + --hljs-color: #a39e9b; + --hljs-quote: #8d8687; + --hljs-variable: #ef6155; + --hljs-type: #f99b15; + --hljs-title: #fec418; + --hljs-symbol: #48b685; + --hljs-selector-tag: #815ba4; } .colibri { @@ -338,5 +399,13 @@ --searchresults-border-color: #5c5c68; --searchresults-li-bg: #242430; --search-mark-bg: #a2cff5; + --hljs-background: #TODO; + --hljs-color: #TODO; + --hljs-quote: #TODO; + --hljs-variable: #TODO; + --hljs-type: #TODO; + --hljs-title: #TODO; + --hljs-symbol: #TODO; + --hljs-selector-tag: #TODO; */ } diff --git a/book/theme/highlight.css b/book/theme/highlight.css index 8dce7d65f5bd..a2db0500af48 100644 --- a/book/theme/highlight.css +++ b/book/theme/highlight.css @@ -7,12 +7,12 @@ code.hljs { padding:3px 5px } .hljs { - background:#2f1e2e; - color:#a39e9b + background: var(--hljs-background); + color: var(--hljs-color); } .hljs-comment, .hljs-quote { - color:#8d8687 + color: var(--hljs-quote) } .hljs-link, .hljs-meta, @@ -23,7 +23,7 @@ code.hljs { .hljs-tag, .hljs-template-variable, .hljs-variable { - color:#ef6155 + color: var(--hljs-variable) } .hljs-built_in, .hljs-deletion, @@ -31,22 +31,22 @@ code.hljs { .hljs-number, .hljs-params, .hljs-type { - color:#f99b15 + color: var(--hljs-type) } .hljs-attribute, .hljs-section, .hljs-title { - color:#fec418 + color: var(--hljs-title) } .hljs-addition, .hljs-bullet, .hljs-string, .hljs-symbol { - color:#48b685 + color: var(--hljs-symbol) } .hljs-keyword, .hljs-selector-tag { - color:#815ba4 + color: var(--hljs-selector-tag) } .hljs-emphasis { font-style:italic diff --git a/contrib/Helix.appdata.xml b/contrib/Helix.appdata.xml new file mode 100644 index 000000000000..a242849751cd --- /dev/null +++ b/contrib/Helix.appdata.xml @@ -0,0 +1,87 @@ + + + com.helix_editor.Helix + CC0-1.0 + MPL-2.0 + Helix + A post-modern text editor + + +

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

+
    +
  • Vim-like modal editing
  • +
  • Multiple selections
  • +
  • Built-in language server support
  • +
  • Smart, incremental syntax highlighting and code editing via tree-sitter
  • +
+
+ + Helix.desktop + + + + Helix with default theme + https://github.com/helix-editor/helix/raw/d4565b4404cabc522bd60822abd374755581d751/screenshot.png + + + + https://helix-editor.com/ + https://opencollective.com/helix-editor + https://docs.helix-editor.com/ + https://github.com/helix-editor/helix + https://github.com/helix-editor/helix/issues + + + + + + https://helix-editor.com/news/release-22-12-highlights/ + + + https://helix-editor.com/news/release-22-08-highlights/ + + + https://helix-editor.com/news/release-22-05-highlights/ + + + https://helix-editor.com/news/release-22-03-highlights/ + + + + + keyboard + + + + Utility + TextEditor + + + + text + editor + development + programming + + + + hx + 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/docs/releases.md b/docs/releases.md index ec0b72704c44..6e7c37c6ebe5 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -5,6 +5,7 @@ Helix releases are versioned in the Calendar Versioning scheme: we'll use `` as a placeholder for the tag being published. * Merge the changelog PR +* Add new `` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html) * Tag and push * `git tag -s -m "" -a && git push` * Make sure to switch to master and pull first diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index ca6cd51e354e..bfe1106d7254 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -31,12 +31,12 @@ arc-swap = "1" regex = "1" bitflags = "1.3" ahash = "0.8.2" -hashbrown = { version = "0.13.1", features = ["raw"] } +hashbrown = { version = "0.13.2", features = ["raw"] } log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -toml = "0.5" +toml = "0.6" imara-diff = "0.1.0" diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs index e1114f4a9e25..f74b576ac996 100644 --- a/helix-core/tests/indent.rs +++ b/helix-core/tests/indent.rs @@ -28,8 +28,8 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) { let mut config_file = test_dir; config_file.push("languages.toml"); - let config = std::fs::read(config_file).unwrap(); - let config = toml::from_slice(&config).unwrap(); + let config = std::fs::read_to_string(config_file).unwrap(); + let config = toml::from_str(&config).unwrap(); let loader = Loader::new(config); // set runtime path so we can find the queries diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml index 95a059052775..d42ce23ffd16 100644 --- a/helix-dap/Cargo.toml +++ b/helix-dap/Cargo.toml @@ -19,7 +19,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] } -which = "4.2" +which = "4.4" [dev-dependencies] fern = "0.6" diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index a3d1458424d7..5d0f00139837 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] anyhow = "1" serde = { version = "1.0", features = ["derive"] } -toml = "0.5" +toml = "0.6" etcetera = "0.4" tree-sitter = "0.20" once_cell = "1.17" diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index 259b1318ea00..0f329d217c0f 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -1,6 +1,9 @@ +use std::str::from_utf8; + /// Default built-in languages.toml. pub fn default_lang_config() -> toml::Value { - toml::from_slice(include_bytes!("../../languages.toml")) + let default_config = include_bytes!("../../languages.toml"); + toml::from_str(from_utf8(default_config).unwrap()) .expect("Could not parse built-in languages.toml to valid toml") } @@ -11,8 +14,8 @@ pub fn user_lang_config() -> Result { .chain([crate::config_dir()].into_iter()) .map(|path| path.join("languages.toml")) .filter_map(|file| { - std::fs::read(&file) - .map(|config| toml::from_slice(&config)) + std::fs::read_to_string(file) + .map(|config| toml::from_str(&config)) .ok() }) .collect::, _>>()? diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 2aa924755112..01c966c8c4bd 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -515,5 +515,5 @@ pub fn load_runtime_file(language: &str, filename: &str) -> Result) -> std::fmt::Result { - f.debug_tuple("MappableCommand") - .field(&self.name()) - .finish() + match self { + MappableCommand::Static { name, .. } => { + f.debug_tuple("MappableCommand").field(name).finish() + } + MappableCommand::Typable { name, args, .. } => f + .debug_tuple("MappableCommand") + .field(name) + .field(args) + .finish(), + } } } @@ -505,12 +512,16 @@ impl PartialEq for MappableCommand { match (self, other) { ( MappableCommand::Typable { - name: first_name, .. + name: first_name, + args: first_args, + .. }, MappableCommand::Typable { - name: second_name, .. + name: second_name, + args: second_args, + .. }, - ) => first_name == second_name, + ) => first_name == second_name && first_args == second_args, ( MappableCommand::Static { name: first_name, .. @@ -863,7 +874,7 @@ fn align_selections(cx: &mut Context) { changes.sort_unstable_by_key(|(from, _, _)| *from); let transaction = Transaction::change(doc.text(), changes.into_iter()); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn goto_window(cx: &mut Context, align: Align) { @@ -1315,7 +1326,7 @@ fn replace(cx: &mut Context) { } }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); exit_select_mode(cx); } }) @@ -1333,7 +1344,7 @@ where (range.from(), range.to(), Some(text)) }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn switch_case(cx: &mut Context) { @@ -1863,7 +1874,7 @@ fn global_search(cx: &mut Context) { impl ui::menu::Item for FileResult { type Data = Option; - fn label(&self, current_path: &Self::Data) -> Spans { + fn format(&self, current_path: &Self::Data) -> Row { let relative_path = helix_core::path::get_relative_path(&self.path) .to_string_lossy() .into_owned(); @@ -2003,6 +2014,10 @@ fn global_search(cx: &mut Context) { let line_num = *line_num; let (view, doc) = current!(cx.editor); let text = doc.text(); + if line_num >= text.len_lines() { + cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed."); + return; + } let start = text.line_to_char(line_num); let end = text.line_to_char((line_num + 1).min(text.len_lines())); @@ -2158,7 +2173,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) { let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { (range.from(), range.to(), None) }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); match op { Operation::Delete => { @@ -2176,7 +2191,7 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &mut View, selection: let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { (range.from(), range.to(), None) }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn delete_selection(cx: &mut Context) { @@ -2272,7 +2287,7 @@ fn append_mode(cx: &mut Context) { doc.text(), [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(), ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } let selection = doc.selection(view.id).clone().transform(|range| { @@ -2311,7 +2326,7 @@ fn buffer_picker(cx: &mut Context) { impl ui::menu::Item for BufferMeta { type Data = (); - fn label(&self, _data: &Self::Data) -> Spans { + fn format(&self, _data: &Self::Data) -> Row { let path = self .path .as_deref() @@ -2380,7 +2395,7 @@ fn jumplist_picker(cx: &mut Context) { impl ui::menu::Item for JumpMeta { type Data = (); - fn label(&self, _data: &Self::Data) -> Spans { + fn format(&self, _data: &Self::Data) -> Row { let path = self .path .as_deref() @@ -2453,7 +2468,7 @@ fn jumplist_picker(cx: &mut Context) { impl ui::menu::Item for MappableCommand { type Data = ReverseKeymap; - fn label(&self, keymap: &Self::Data) -> Spans { + fn format(&self, keymap: &Self::Data) -> Row { let fmt_binding = |bindings: &Vec>| -> String { bindings.iter().fold(String::new(), |mut acc, bind| { if !acc.is_empty() { @@ -2582,7 +2597,7 @@ async fn make_format_callback( if let Ok(format) = format { if doc.version() == doc_version { - apply_transaction(&format, doc, view); + doc.apply(&format, view.id); doc.append_changes_to_history(view); doc.detect_indent_and_line_ending(); view.ensure_cursor_in_view(doc, scrolloff); @@ -2675,7 +2690,7 @@ fn open(cx: &mut Context, open: Open) { transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } // o inserts a new line after each line with a selection @@ -3103,7 +3118,7 @@ pub mod insert { let (view, doc) = current!(cx.editor); if let Some(t) = transaction { - apply_transaction(&t, doc, view); + doc.apply(&t, view.id); } // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) @@ -3125,7 +3140,7 @@ pub mod insert { &doc.selection(view.id).clone().cursors(doc.text().slice(..)), indent, ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } pub fn insert_newline(cx: &mut Context) { @@ -3230,7 +3245,7 @@ pub mod insert { transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); let (view, doc) = current!(cx.editor); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } pub fn delete_char_backward(cx: &mut Context) { @@ -3325,7 +3340,7 @@ pub mod insert { } }); let (view, doc) = current!(cx.editor); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); } @@ -3343,7 +3358,7 @@ pub mod insert { None, ) }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); } @@ -3624,7 +3639,7 @@ fn paste_impl( transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); } - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) { @@ -3716,7 +3731,7 @@ fn replace_with_yanked(cx: &mut Context) { } }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); exit_select_mode(cx); } } @@ -3740,7 +3755,7 @@ fn replace_selections_with_clipboard_impl( ) }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); doc.append_changes_to_history(view); } Err(e) => return Err(e.context("Couldn't get system clipboard contents")), @@ -3812,7 +3827,7 @@ fn indent(cx: &mut Context) { Some((pos, pos, Some(indent.clone()))) }), ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn unindent(cx: &mut Context) { @@ -3851,7 +3866,7 @@ fn unindent(cx: &mut Context) { let transaction = Transaction::change(doc.text(), changes.into_iter()); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn format_selections(cx: &mut Context) { @@ -3906,7 +3921,7 @@ fn format_selections(cx: &mut Context) { language_server.offset_encoding(), ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn join_selections_impl(cx: &mut Context, select_space: bool) { @@ -3938,6 +3953,11 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) { } } + // nothing to do, bail out early to avoid crashes later + if changes.is_empty() { + return; + } + changes.sort_unstable_by_key(|(from, _to, _text)| *from); changes.dedup(); @@ -3960,7 +3980,7 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) { Transaction::change(doc.text(), changes.into_iter()) }; - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { @@ -4103,7 +4123,7 @@ fn toggle_comments(cx: &mut Context) { .map(|tc| tc.as_ref()); let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); exit_select_mode(cx); } @@ -4159,7 +4179,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { .map(|(range, fragment)| (range.from(), range.to(), Some(fragment))), ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } fn rotate_selection_contents_forward(cx: &mut Context) { @@ -4317,6 +4337,10 @@ fn rotate_view(cx: &mut Context) { cx.editor.focus_next() } +fn rotate_view_reverse(cx: &mut Context) { + cx.editor.focus_prev() +} + fn jump_view_right(cx: &mut Context) { cx.editor.focus_direction(tree::Direction::Right) } @@ -4688,7 +4712,7 @@ fn surround_add(cx: &mut Context) { let transaction = Transaction::change(doc.text(), changes.into_iter()) .with_selection(Selection::new(ranges, selection.primary_index())); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); exit_select_mode(cx); }) } @@ -4728,7 +4752,7 @@ fn surround_replace(cx: &mut Context) { (pos, pos + 1, Some(t)) }), ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); exit_select_mode(cx); }); }) @@ -4756,7 +4780,7 @@ fn surround_delete(cx: &mut Context) { let transaction = Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None))); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); exit_select_mode(cx); }) } @@ -4971,7 +4995,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { if behavior != &ShellBehavior::Ignore { let transaction = Transaction::change(doc.text(), changes.into_iter()) .with_selection(Selection::new(ranges, selection.primary_index())); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); doc.append_changes_to_history(view); } @@ -5034,7 +5058,7 @@ fn add_newline_impl(cx: &mut Context, open: Open) { }); let transaction = Transaction::change(text, changes); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } enum IncrementDirection { @@ -5101,7 +5125,7 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) { let new_selection = Selection::new(new_selection_ranges, selection.primary_index()); let transaction = Transaction::change(doc.text(), changes.into_iter()); let transaction = transaction.with_selection(new_selection); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } } diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index b182f28c4284..b3166e395d90 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -12,7 +12,7 @@ use helix_view::editor::Breakpoint; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; -use tui::text::Spans; +use tui::{text::Spans, widgets::Row}; use std::collections::HashMap; use std::future::Future; @@ -25,7 +25,7 @@ use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select impl ui::menu::Item for StackFrame { type Data = (); - fn label(&self, _data: &Self::Data) -> Spans { + fn format(&self, _data: &Self::Data) -> Row { self.name.as_str().into() // TODO: include thread_states in the label } } @@ -33,7 +33,7 @@ impl ui::menu::Item for StackFrame { impl ui::menu::Item for DebugTemplate { type Data = (); - fn label(&self, _data: &Self::Data) -> Spans { + fn format(&self, _data: &Self::Data) -> Row { self.name.as_str().into() } } @@ -41,7 +41,7 @@ impl ui::menu::Item for DebugTemplate { impl ui::menu::Item for Thread { type Data = ThreadStates; - fn label(&self, thread_states: &Self::Data) -> Spans { + fn format(&self, thread_states: &Self::Data) -> Row { format!( "{} ({})", self.name, diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 86b0c5fa7417..578eb6084196 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -5,12 +5,15 @@ use helix_lsp::{ util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range}, OffsetEncoding, }; -use tui::text::{Span, Spans}; +use tui::{ + text::{Span, Spans}, + widgets::Row, +}; use super::{align_view, push_jump, Align, Context, Editor, Open}; use helix_core::{path, Selection}; -use helix_view::{apply_transaction, document::Mode, editor::Action, theme::Style}; +use helix_view::{document::Mode, editor::Action, theme::Style}; use crate::{ compositor::{self, Compositor}, @@ -46,7 +49,7 @@ impl ui::menu::Item for lsp::Location { /// Current working directory. type Data = PathBuf; - fn label(&self, cwdir: &Self::Data) -> Spans { + fn format(&self, cwdir: &Self::Data) -> Row { // The preallocation here will overallocate a few characters since it will account for the // URL's scheme, which is not used most of the time since that scheme will be "file://". // Those extra chars will be used to avoid allocating when writing the line number (in the @@ -80,7 +83,7 @@ impl ui::menu::Item for lsp::SymbolInformation { /// Path to currently focussed document type Data = Option; - fn label(&self, current_doc_path: &Self::Data) -> Spans { + fn format(&self, current_doc_path: &Self::Data) -> Row { if current_doc_path.as_ref() == Some(&self.location.uri) { self.name.as_str().into() } else { @@ -110,7 +113,7 @@ struct PickerDiagnostic { impl ui::menu::Item for PickerDiagnostic { type Data = (DiagnosticStyles, DiagnosticsFormat); - fn label(&self, (styles, format): &Self::Data) -> Spans { + fn format(&self, (styles, format): &Self::Data) -> Row { let mut style = self .diag .severity @@ -149,6 +152,7 @@ impl ui::menu::Item for PickerDiagnostic { Span::styled(&self.diag.message, style), Span::styled(code, style), ]) + .into() } } @@ -467,7 +471,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { impl ui::menu::Item for lsp::CodeActionOrCommand { type Data = (); - fn label(&self, _data: &Self::Data) -> Spans { + fn format(&self, _data: &Self::Data) -> Row { match self { lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(), lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), @@ -662,7 +666,7 @@ pub fn code_action(cx: &mut Context) { impl ui::menu::Item for lsp::Command { type Data = (); - fn label(&self, _data: &Self::Data) -> Spans { + fn format(&self, _data: &Self::Data) -> Row { self.title.as_str().into() } } @@ -796,7 +800,7 @@ pub fn apply_workspace_edit( offset_encoding, ); let view = view_mut!(editor, view_id); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); doc.append_changes_to_history(view); }; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index de24c4fba5ca..bd91df5ae62a 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -4,10 +4,7 @@ use crate::job::Job; use super::*; -use helix_view::{ - apply_transaction, - editor::{Action, CloseError, ConfigEvent}, -}; +use helix_view::editor::{Action, CloseError, ConfigEvent}; use ui::completers::{self, Completer}; #[derive(Clone)] @@ -480,7 +477,7 @@ fn set_line_ending( } }), ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); doc.append_changes_to_history(view); Ok(()) @@ -925,7 +922,7 @@ fn replace_selections_with_clipboard_impl( (range.from(), range.to(), Some(contents.as_str().into())) }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); doc.append_changes_to_history(view); Ok(()) } @@ -1596,7 +1593,7 @@ fn sort_impl( .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))), ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); doc.append_changes_to_history(view); Ok(()) @@ -1640,7 +1637,7 @@ fn reflow( (range.from(), range.to(), Some(reflowed_text)) }); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); doc.append_changes_to_history(view); view.ensure_cursor_in_view(doc, scrolloff); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 4a131f0a5217..e94a5f66b447 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -184,7 +184,7 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { S: serde::de::SeqAccess<'de>, { let mut commands = Vec::new(); - while let Some(command) = seq.next_element::<&str>()? { + while let Some(command) = seq.next_element::()? { commands.push( command .parse::() @@ -600,4 +600,43 @@ mod tests { "Mismatch" ) } + + #[test] + fn escaped_keymap() { + use crate::commands::MappableCommand; + use helix_view::input::{KeyCode, KeyEvent, KeyModifiers}; + + let keys = r#" +"+" = [ + "select_all", + ":pipe sed -E 's/\\s+$//g'", +] + "#; + + let key = KeyEvent { + code: KeyCode::Char('+'), + modifiers: KeyModifiers::NONE, + }; + + let expectation = Keymap::new(KeyTrie::Node(KeyTrieNode::new( + "", + hashmap! { + key => KeyTrie::Sequence(vec!{ + MappableCommand::select_all, + MappableCommand::Typable { + name: "pipe".to_string(), + args: vec!{ + "sed".to_string(), + "-E".to_string(), + "'s/\\s+$//g'".to_string() + }, + doc: "".to_string(), + }, + }) + }, + vec![key], + ))); + + assert_eq!(toml::from_str(keys), Ok(expectation)); + } } diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 11d7886a37d6..2eca709d181b 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,7 +1,6 @@ use crate::compositor::{Component, Context, Event, EventResult}; -use helix_view::{apply_transaction, editor::CompleteAction, ViewId}; +use helix_view::{editor::CompleteAction, ViewId}; use tui::buffer::Buffer as Surface; -use tui::text::Spans; use std::borrow::Cow; @@ -33,11 +32,7 @@ impl menu::Item for CompletionItem { .into() } - fn label(&self, _data: &Self::Data) -> Spans { - self.label.as_str().into() - } - - fn row(&self, _data: &Self::Data) -> menu::Row { + fn format(&self, _data: &Self::Data) -> menu::Row { menu::Row::new(vec![ menu::Cell::from(self.label.as_str()), menu::Cell::from(match self.kind { @@ -188,7 +183,7 @@ impl Completion { // initialize a savepoint doc.savepoint(); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); editor.last_completion = Some(CompleteAction { trigger_offset, @@ -208,7 +203,7 @@ impl Completion { trigger_offset, ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); editor.last_completion = Some(CompleteAction { trigger_offset, @@ -238,7 +233,7 @@ impl Completion { additional_edits.clone(), offset_encoding, // TODO: should probably transcode in Client ); - apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 35cf77abc9bf..a0518964e2a7 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -17,7 +17,6 @@ use helix_core::{ visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction, }; use helix_view::{ - apply_transaction, document::{Mode, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, @@ -342,23 +341,29 @@ impl EditorView { let selection_scope = theme .find_scope_index("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); + let primary_selection_scope = theme + .find_scope_index("ui.selection.primary") + .unwrap_or(selection_scope); let base_cursor_scope = theme .find_scope_index("ui.cursor") .unwrap_or(selection_scope); + let base_primary_cursor_scope = theme + .find_scope_index("ui.cursor.primary") + .unwrap_or(base_cursor_scope); let cursor_scope = match mode { Mode::Insert => theme.find_scope_index("ui.cursor.insert"), Mode::Select => theme.find_scope_index("ui.cursor.select"), - Mode::Normal => Some(base_cursor_scope), + Mode::Normal => theme.find_scope_index("ui.cursor.normal"), } .unwrap_or(base_cursor_scope); - let primary_cursor_scope = theme - .find_scope_index("ui.cursor.primary") - .unwrap_or(cursor_scope); - let primary_selection_scope = theme - .find_scope_index("ui.selection.primary") - .unwrap_or(selection_scope); + let primary_cursor_scope = match mode { + Mode::Insert => theme.find_scope_index("ui.cursor.primary.insert"), + Mode::Select => theme.find_scope_index("ui.cursor.primary.select"), + Mode::Normal => theme.find_scope_index("ui.cursor.primary.normal"), + } + .unwrap_or(base_primary_cursor_scope); let mut spans: Vec<(usize, std::ops::Range)> = Vec::new(); for (i, range) in selection.iter().enumerate() { @@ -386,7 +391,14 @@ impl EditorView { if range.head > range.anchor { // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); - spans.push((selection_scope, range.anchor..cursor_start)); + // non block cursors look like they exclude the cursor + let selection_end = + if selection_is_primary && !cursor_is_block && mode != Mode::Insert { + range.head + } else { + cursor_start + }; + spans.push((selection_scope, range.anchor..selection_end)); if !selection_is_primary || cursor_is_block { spans.push((cursor_scope, cursor_start..range.head)); } @@ -396,7 +408,16 @@ impl EditorView { if !selection_is_primary || cursor_is_block { spans.push((cursor_scope, range.head..cursor_end)); } - spans.push((selection_scope, cursor_end..range.anchor)); + // non block cursors look like they exclude the cursor + let selection_start = if selection_is_primary + && !cursor_is_block + && !(mode == Mode::Insert && cursor_end == range.anchor) + { + range.head + } else { + cursor_end + }; + spans.push((selection_scope, selection_start..range.anchor)); } } @@ -1026,7 +1047,7 @@ impl EditorView { (shift_position(start), shift_position(end), t) }), ); - apply_transaction(&tx, doc, view); + doc.apply(&tx, view.id); } InsertEvent::TriggerCompletion => { let (_, doc) = current!(cxt.editor); diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index b9c1f9ded2e1..da00aa89f9b1 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -4,7 +4,7 @@ use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; -use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table}; +use tui::{buffer::Buffer as Surface, widgets::Table}; pub use tui::widgets::{Cell, Row}; @@ -18,28 +18,24 @@ pub trait Item { /// Additional editor state that is used for label calculation. type Data; - fn label(&self, data: &Self::Data) -> Spans; + fn format(&self, data: &Self::Data) -> Row; fn sort_text(&self, data: &Self::Data) -> Cow { - let label: String = self.label(data).into(); + let label: String = self.format(data).cell_text().collect(); label.into() } fn filter_text(&self, data: &Self::Data) -> Cow { - let label: String = self.label(data).into(); + let label: String = self.format(data).cell_text().collect(); label.into() } - - fn row(&self, data: &Self::Data) -> Row { - Row::new(vec![Cell::from(self.label(data))]) - } } impl Item for PathBuf { /// Root prefix to strip. type Data = PathBuf; - fn label(&self, root_path: &Self::Data) -> Spans { + fn format(&self, root_path: &Self::Data) -> Row { self.strip_prefix(root_path) .unwrap_or(self) .to_string_lossy() @@ -81,7 +77,7 @@ impl Menu { Self { options, editor_data, - matcher: Box::new(Matcher::default()), + matcher: Box::default(), matches, cursor: None, widths: Vec::new(), @@ -144,10 +140,10 @@ impl Menu { let n = self .options .first() - .map(|option| option.row(&self.editor_data).cells.len()) + .map(|option| option.format(&self.editor_data).cells.len()) .unwrap_or_default(); let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.row(&self.editor_data); + let row = option.format(&self.editor_data); // maintain max for each column for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { let width = cell.content.width(); @@ -331,7 +327,9 @@ impl Component for Menu { (a + b - 1) / b } - let rows = options.iter().map(|option| option.row(&self.editor_data)); + let rows = options + .iter() + .map(|option| option.format(&self.editor_data)); let table = Table::new(rows) .style(style) .highlight_style(selected) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index eb935e5672e5..6bd64251740a 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -7,23 +7,23 @@ use crate::{ use futures_util::future::BoxFuture; use tui::{ buffer::Buffer as Surface, - widgets::{Block, BorderType, Borders}, + layout::Constraint, + text::{Span, Spans}, + widgets::{Block, BorderType, Borders, Cell, Table}, }; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use tui::widgets::Widget; -use std::{ - cmp::{self, Ordering}, - time::Instant, -}; +use std::cmp::{self, Ordering}; use std::{collections::HashMap, io::Read, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; -use helix_core::{movement::Direction, Position}; +use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position}; use helix_view::{ editor::Action, graphics::{CursorKind, Margin, Modifier, Rect}, + theme::Style, Document, DocumentId, Editor, }; @@ -389,6 +389,8 @@ pub struct Picker { pub truncate_start: bool, /// Whether to show the preview panel (default true) show_preview: bool, + /// Constraints for tabular formatting + widths: Vec, callback_fn: Box, } @@ -406,10 +408,30 @@ impl Picker { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, ); + let n = options + .first() + .map(|option| option.format(&editor_data).cells.len()) + .unwrap_or_default(); + let max_lens = options.iter().fold(vec![0; n], |mut acc, option| { + let row = option.format(&editor_data); + // maintain max for each column + for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { + let width = cell.content.width(); + if width > *acc { + *acc = width; + } + } + acc + }); + let widths = max_lens + .into_iter() + .map(|len| Constraint::Length(len as u16)) + .collect(); + let mut picker = Self { options, editor_data, - matcher: Box::new(Matcher::default()), + matcher: Box::default(), matches: Vec::new(), cursor: 0, prompt, @@ -418,6 +440,7 @@ impl Picker { show_preview: true, callback_fn: Box::new(callback_fn), completion_height: 0, + widths, }; // scoring on empty input: @@ -437,8 +460,6 @@ impl Picker { } pub fn score(&mut self) { - let now = Instant::now(); - let pattern = self.prompt.line(); if pattern == &self.previous_pattern { @@ -480,8 +501,6 @@ impl Picker { self.force_score(); } - log::debug!("picker score {:?}", Instant::now().duration_since(now)); - // reset cursor position self.cursor = 0; let pattern = self.prompt.line(); @@ -657,7 +676,7 @@ impl Component for Picker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let text_style = cx.editor.theme.get("ui.text"); let selected = cx.editor.theme.get("ui.text.focus"); - let highlighted = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); + let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); // -- Render the frame: // clear area @@ -697,61 +716,123 @@ impl Component for Picker { } // -- Render the contents: - // subtract area of prompt from top and current item marker " > " from left - let inner = inner.clip_top(2).clip_left(3); + // subtract area of prompt from top + let inner = inner.clip_top(2); let rows = inner.height; let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize)); + let cursor = self.cursor.saturating_sub(offset); - let files = self + let options = self .matches .iter() .skip(offset) - .map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap())); - - for (i, (_index, option)) in files.take(rows as usize).enumerate() { - let is_active = i == (self.cursor - offset); - if is_active { - surface.set_string( - inner.x.saturating_sub(3), - inner.y + i as u16, - " > ", - selected, - ); - surface.set_style( - Rect::new(inner.x, inner.y + i as u16, inner.width, 1), - selected, - ); - } + .take(rows as usize) + .map(|pmatch| &self.options[pmatch.index]) + .map(|option| option.format(&self.editor_data)) + .map(|mut row| { + const TEMP_CELL_SEP: &str = " "; + + let line = row.cell_text().fold(String::new(), |mut s, frag| { + s.push_str(&frag); + s.push_str(TEMP_CELL_SEP); + s + }); + + // Items are filtered by using the text returned by menu::Item::filter_text + // but we do highlighting here using the text in Row and therefore there + // might be inconsistencies. This is the best we can do since only the + // text in Row is displayed to the end user. + let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) + .fuzzy_indicies(&line, &self.matcher) + .unwrap_or_default(); + + let highlight_byte_ranges: Vec<_> = line + .char_indices() + .enumerate() + .filter_map(|(char_idx, (byte_offset, ch))| { + highlights + .contains(&char_idx) + .then(|| byte_offset..byte_offset + ch.len_utf8()) + }) + .collect(); + + // The starting byte index of the current (iterating) cell + let mut cell_start_byte_offset = 0; + for cell in row.cells.iter_mut() { + let spans = match cell.content.lines.get(0) { + Some(s) => s, + None => continue, + }; - let spans = option.label(&self.editor_data); - let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) - .fuzzy_indicies(&String::from(&spans), &self.matcher) - .unwrap_or_default(); - - spans.0.into_iter().fold(inner, |pos, span| { - let new_x = surface - .set_string_truncated( - pos.x, - pos.y + i as u16, - &span.content, - pos.width as usize, - |idx| { - if highlights.contains(&idx) { - highlighted.patch(span.style) - } else if is_active { - selected.patch(span.style) + let mut cell_len = 0; + + let graphemes_with_style: Vec<_> = spans + .0 + .iter() + .flat_map(|span| { + span.content + .grapheme_indices(true) + .zip(std::iter::repeat(span.style)) + }) + .map(|((grapheme_byte_offset, grapheme), style)| { + cell_len += grapheme.len(); + let start = cell_start_byte_offset; + + let grapheme_byte_range = + grapheme_byte_offset..grapheme_byte_offset + grapheme.len(); + + if highlight_byte_ranges.iter().any(|hl_rng| { + hl_rng.start >= start + grapheme_byte_range.start + && hl_rng.end <= start + grapheme_byte_range.end + }) { + (grapheme, style.patch(highlight_style)) } else { - text_style.patch(span.style) + (grapheme, style) } - }, - true, - self.truncate_start, - ) - .0; - pos.clip_left(new_x - pos.x) + }) + .collect(); + + let mut span_list: Vec<(String, Style)> = Vec::new(); + for (grapheme, style) in graphemes_with_style { + if span_list.last().map(|(_, sty)| sty) == Some(&style) { + let (string, _) = span_list.last_mut().unwrap(); + string.push_str(grapheme); + } else { + span_list.push((String::from(grapheme), style)) + } + } + + let spans: Vec = span_list + .into_iter() + .map(|(string, style)| Span::styled(string, style)) + .collect(); + let spans: Spans = spans.into(); + *cell = Cell::from(spans); + + cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len(); + } + + row }); - } + + let table = Table::new(options) + .style(text_style) + .highlight_style(selected) + .highlight_symbol(" > ") + .column_spacing(1) + .widths(&self.widths); + + use tui::widgets::TableState; + + table.render_table( + inner, + surface, + &mut TableState { + offset: 0, + selected: Some(cursor), + }, + ); } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 2c5043d68cf2..a0f3a32e4cba 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -130,7 +130,7 @@ pub async fn test_key_sequence_with_input_text>( }) .with_selection(test_case.in_selection.clone()); - helix_view::apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); test_key_sequence( &mut app, @@ -315,7 +315,7 @@ impl AppBuilder { .with_selection(selection); // replace the initial text with the input text - helix_view::apply_transaction(&trans, doc, view); + doc.apply(&trans, view.id); } Ok(app) diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 9b93c4050b75..b1fd44787f78 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -433,7 +433,7 @@ impl Buffer { (x_offset as u16, y) } - pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) { + pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { let mut remaining_width = width; let mut x = x; for span in &spans.0 { @@ -454,7 +454,7 @@ impl Buffer { (x, y) } - pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) { + pub fn set_span(&mut self, x: u16, y: u16, span: &Span, width: u16) -> (u16, u16) { self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style) } @@ -521,10 +521,10 @@ impl Buffer { pub fn merge(&mut self, other: &Buffer) { let area = self.area.union(other.area); let cell: Cell = Default::default(); - self.content.resize(area.area() as usize, cell.clone()); + self.content.resize(area.area(), cell.clone()); // Move original content to the appropriate space - let size = self.area.area() as usize; + let size = self.area.area(); for i in (0..size).rev() { let (x, y) = self.pos_of(i); // New index in content @@ -537,7 +537,7 @@ impl Buffer { // Push content of the other buffer into this one (may erase previous // data) - let size = other.area.area() as usize; + let size = other.area.area(); for i in 0..size { let (x, y) = other.pos_of(i); // New index in content diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index ccdafad5fdfe..a3e242feb6db 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -436,6 +436,32 @@ impl<'a> From>> for Text<'a> { } } +impl<'a> From> for String { + fn from(text: Text<'a>) -> String { + String::from(&text) + } +} + +impl<'a> From<&Text<'a>> for String { + fn from(text: &Text<'a>) -> String { + let size = text + .lines + .iter() + .flat_map(|spans| spans.0.iter().map(|span| span.content.len())) + .sum::() + + text.lines.len().saturating_sub(1); // for newline after each line + let mut output = String::with_capacity(size); + + for spans in &text.lines { + for span in &spans.0 { + output.push_str(&span.content); + } + output.push('\n'); + } + output + } +} + impl<'a> IntoIterator for Text<'a> { type Item = Spans<'a>; type IntoIter = std::vec::IntoIter; diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs index a8f428a7a65a..400f65e0ad98 100644 --- a/helix-tui/src/widgets/table.rs +++ b/helix-tui/src/widgets/table.rs @@ -4,14 +4,8 @@ use crate::{ text::Text, widgets::{Block, Widget}, }; -use cassowary::{ - strength::{MEDIUM, REQUIRED, WEAK}, - WeightedRelation::*, - {Expression, Solver}, -}; use helix_core::unicode::width::UnicodeWidthStr; use helix_view::graphics::{Rect, Style}; -use std::collections::HashMap; /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// @@ -126,6 +120,17 @@ impl<'a> Row<'a> { fn total_height(&self) -> u16 { self.height.saturating_add(self.bottom_margin) } + + /// Returns the contents of cells as plain text, without styles and colors. + pub fn cell_text(&self) -> impl Iterator + '_ { + self.cells.iter().map(|cell| String::from(&cell.content)) + } +} + +impl<'a, T: Into>> From for Row<'a> { + fn from(cell: T) -> Self { + Row::new(vec![cell.into()]) + } } /// A widget to display data in formatted columns. @@ -260,69 +265,32 @@ impl<'a> Table<'a> { } fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec { - let mut solver = Solver::new(); - let mut var_indices = HashMap::new(); - let mut ccs = Vec::new(); - let mut variables = Vec::new(); - for i in 0..self.widths.len() { - let var = cassowary::Variable::new(); - variables.push(var); - var_indices.insert(var, i); - } - let spacing_width = (variables.len() as u16).saturating_sub(1) * self.column_spacing; - let mut available_width = max_width.saturating_sub(spacing_width); + let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1); if has_selection { let highlight_symbol_width = self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0); - available_width = available_width.saturating_sub(highlight_symbol_width); + constraints.push(Constraint::Length(highlight_symbol_width)); } - for (i, constraint) in self.widths.iter().enumerate() { - ccs.push(variables[i] | GE(WEAK) | 0.); - ccs.push(match *constraint { - Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v), - Constraint::Percentage(v) => { - variables[i] | EQ(WEAK) | (f64::from(v * available_width) / 100.0) - } - Constraint::Ratio(n, d) => { - variables[i] - | EQ(WEAK) - | (f64::from(available_width) * f64::from(n) / f64::from(d)) - } - Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v), - Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v), - }) + for constraint in self.widths { + constraints.push(*constraint); + constraints.push(Constraint::Length(self.column_spacing)); } - solver - .add_constraint( - variables - .iter() - .fold(Expression::from_constant(0.), |acc, v| acc + *v) - | LE(REQUIRED) - | f64::from(available_width), - ) - .unwrap(); - solver.add_constraints(&ccs).unwrap(); - let mut widths = vec![0; variables.len()]; - for &(var, value) in solver.fetch_changes() { - let index = var_indices[&var]; - let value = if value.is_sign_negative() { - 0 - } else { - value.round() as u16 - }; - widths[index] = value; + if !self.widths.is_empty() { + constraints.pop(); } - // Cassowary could still return columns widths greater than the max width when there are - // fixed length constraints that cannot be satisfied. Therefore, we clamp the widths from - // left to right. - let mut available_width = max_width; - for w in &mut widths { - *w = available_width.min(*w); - available_width = available_width - .saturating_sub(*w) - .saturating_sub(self.column_spacing); + let mut chunks = crate::layout::Layout::default() + .direction(crate::layout::Direction::Horizontal) + .constraints(constraints) + .split(Rect { + x: 0, + y: 0, + width: max_width, + height: 1, + }); + if has_selection { + chunks.remove(0); } - widths + chunks.iter().step_by(2).map(|c| c.width).collect() } fn get_row_bounds( @@ -477,6 +445,9 @@ impl<'a> Table<'a> { }; let mut col = table_row_start_col; for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { + if is_selected { + buf.set_style(table_row_area, self.highlight_style); + } render_cell( buf, cell, @@ -489,9 +460,6 @@ impl<'a> Table<'a> { ); col += *width + self.column_spacing; } - if is_selected { - buf.set_style(table_row_area, self.highlight_style); - } } } } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 7d130317e410..a4b97f86e9a2 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -39,10 +39,10 @@ chardetng = "0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -toml = "0.5" +toml = "0.6" log = "~0.4" -which = "4.2" +which = "4.4" [target.'cfg(windows)'.dependencies] diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 856e5628ab42..6b33ea6aa308 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -27,7 +27,7 @@ use helix_core::{ }; use crate::editor::RedrawHandle; -use crate::{apply_transaction, DocumentId, Editor, View, ViewId}; +use crate::{DocumentId, Editor, View, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; @@ -650,7 +650,7 @@ impl Document { // This is not considered a modification of the contents of the file regardless // of the encoding. let transaction = helix_core::diff::compare_ropes(self.text(), &rope); - apply_transaction(&transaction, self, view); + self.apply(&transaction, view.id); self.append_changes_to_history(view); self.reset_modified(); @@ -852,9 +852,6 @@ impl Document { } /// Apply a [`Transaction`] to the [`Document`] to change its text. - /// Instead of calling this function directly, use [crate::apply_transaction] - /// to ensure that the transaction is applied to the appropriate [`View`] as - /// well. pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { // store the state just before any changes are made. This allows us to undo to the // state just before a transaction was applied. @@ -911,7 +908,7 @@ impl Document { pub fn restore(&mut self, view: &mut View) { if let Some(revert) = self.savepoint.take() { - apply_transaction(&revert, self, view); + self.apply(&revert, view.id); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 547a4ffbd40d..1029c14f3313 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -71,6 +71,96 @@ where ) } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +pub struct GutterConfig { + /// Gutter Layout + pub layout: Vec, + /// Options specific to the "line-numbers" gutter + pub line_numbers: GutterLineNumbersConfig, +} + +impl Default for GutterConfig { + fn default() -> Self { + Self { + layout: vec![ + GutterType::Diagnostics, + GutterType::Spacer, + GutterType::LineNumbers, + GutterType::Spacer, + GutterType::Diff, + ], + line_numbers: GutterLineNumbersConfig::default(), + } + } +} + +impl From> for GutterConfig { + fn from(x: Vec) -> Self { + GutterConfig { + layout: x, + ..Default::default() + } + } +} + +fn deserialize_gutter_seq_or_struct<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct GutterVisitor; + + impl<'de> serde::de::Visitor<'de> for GutterVisitor { + type Value = GutterConfig; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + "an array of gutter names or a detailed gutter configuration" + ) + } + + fn visit_seq(self, mut seq: S) -> Result + where + S: serde::de::SeqAccess<'de>, + { + let mut gutters = Vec::new(); + while let Some(gutter) = seq.next_element::()? { + gutters.push( + gutter + .parse::() + .map_err(serde::de::Error::custom)?, + ) + } + + Ok(gutters.into()) + } + + fn visit_map(self, map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let deserializer = serde::de::value::MapAccessDeserializer::new(map); + Deserialize::deserialize(deserializer) + } + } + + deserializer.deserialize_any(GutterVisitor) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +pub struct GutterLineNumbersConfig { + /// Minimum number of characters to use for line number gutter. Defaults to 3. + pub min_width: usize, +} + +impl Default for GutterLineNumbersConfig { + fn default() -> Self { + Self { min_width: 3 } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct FilePickerConfig { @@ -132,8 +222,8 @@ pub struct Config { pub cursorline: bool, /// Highlight the columns cursors are currently on. Defaults to false. pub cursorcolumn: bool, - /// Gutters. Default ["diagnostics", "line-numbers"] - pub gutters: Vec, + #[serde(deserialize_with = "deserialize_gutter_seq_or_struct")] + pub gutters: GutterConfig, /// Middle click paste support. Defaults to true. pub middle_click_paste: bool, /// Automatic insertion of pairs to parentheses, brackets, @@ -206,10 +296,10 @@ pub fn get_terminal_provider() -> Option { }); } - return Some(TerminalConfig { + Some(TerminalConfig { command: "conhost".to_string(), args: vec!["cmd".to_string(), "/C".to_string()], - }); + }) } #[cfg(not(any(windows, target_os = "wasm32")))] @@ -606,13 +696,7 @@ impl Default for Config { line_number: LineNumber::Absolute, cursorline: false, cursorcolumn: false, - gutters: vec![ - GutterType::Diagnostics, - GutterType::Spacer, - GutterType::LineNumbers, - GutterType::Spacer, - GutterType::Diff, - ], + gutters: GutterConfig::default(), middle_click_paste: true, auto_pairs: AutoPairConfig::default(), auto_completion: true, @@ -844,6 +928,7 @@ impl Editor { let config = self.config(); self.auto_pairs = (&config.auto_pairs).into(); self.reset_idle_timer(); + self._refresh(); } pub fn clear_idle_timer(&mut self) { @@ -984,6 +1069,7 @@ impl Editor { for (view, _) in self.tree.views_mut() { let doc = doc_mut!(self, &view.doc); view.sync_changes(doc); + view.gutters = config.gutters.clone(); view.ensure_cursor_in_view(doc, config.scrolloff) } } @@ -1285,6 +1371,10 @@ impl Editor { self.focus(self.tree.next()); } + pub fn focus_prev(&mut self) { + self.focus(self.tree.prev()); + } + pub fn focus_direction(&mut self, direction: tree::Direction) { let current_view = self.tree.focus; if let Some(id) = self.tree.find_split_in_direction(current_view, direction) { @@ -1498,6 +1588,6 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) { let line_start_pos = text.line_to_char(range.cursor_line(text)); (line_start_pos, pos, None) }); - crate::apply_transaction(&transaction, doc, view); + doc.apply(&transaction, view.id); } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 9264c50f8294..a0b645fae5b8 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -251,7 +251,6 @@ impl Rect { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Color { Reset, Black, @@ -353,7 +352,6 @@ bitflags! { /// /// let m = Modifier::BOLD | Modifier::ITALIC; /// ``` - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Modifier: u16 { const BOLD = 0b0000_0000_0001; const DIM = 0b0000_0000_0010; @@ -450,7 +448,6 @@ impl FromStr for Modifier { /// ); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Style { pub fg: Option, pub bg: Option, diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 377518fb5379..c1b5e2b16112 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -35,10 +35,10 @@ impl GutterType { } } - pub fn width(self, _view: &View, doc: &Document) -> usize { + pub fn width(self, view: &View, doc: &Document) -> usize { match self { GutterType::Diagnostics => 1, - GutterType::LineNumbers => line_numbers_width(_view, doc), + GutterType::LineNumbers => line_numbers_width(view, doc), GutterType::Spacer => 1, GutterType::Diff => 1, } @@ -140,12 +140,13 @@ pub fn line_numbers<'doc>( is_focused: bool, ) -> GutterFn<'doc> { let text = doc.text().slice(..); - let last_line = view.last_line(doc); - let width = GutterType::LineNumbers.width(view, doc); + let width = line_numbers_width(view, doc); + + let last_line_in_view = view.last_line(doc); // Whether to draw the line number for the last line of the // document or not. We only draw it if it's not an empty line. - let draw_last = text.line_to_byte(last_line) < text.len_bytes(); + let draw_last = text.line_to_byte(last_line_in_view) < text.len_bytes(); let linenr = theme.get("ui.linenr"); let linenr_select = theme.get("ui.linenr.selected"); @@ -158,7 +159,7 @@ pub fn line_numbers<'doc>( let mode = editor.mode; Box::new(move |line: usize, selected: bool, out: &mut String| { - if line == last_line && !draw_last { + if line == last_line_in_view && !draw_last { write!(out, "{:>1$}", '~', width).unwrap(); Some(linenr) } else { @@ -187,14 +188,19 @@ pub fn line_numbers<'doc>( }) } -pub fn line_numbers_width(_view: &View, doc: &Document) -> usize { +/// The width of a "line-numbers" gutter +/// +/// The width of the gutter depends on the number of lines in the document, +/// whether there is content on the last line (the `~` line), and the +/// `editor.gutters.line-numbers.min-width` settings. +fn line_numbers_width(view: &View, doc: &Document) -> usize { let text = doc.text(); let last_line = text.len_lines().saturating_sub(1); let draw_last = text.line_to_byte(last_line) < text.len_bytes(); let last_drawn = if draw_last { last_line + 1 } else { last_line }; - - // set a lower bound to 2-chars to minimize ambiguous relative line numbers - std::cmp::max(count_digits(last_drawn), 2) + let digits = count_digits(last_drawn); + let n_min = view.gutters.line_numbers.min_width; + digits.max(n_min) } pub fn padding<'doc>( @@ -282,3 +288,82 @@ pub fn diagnostics_or_breakpoints<'doc>( breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out)) }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::Document; + use crate::editor::{GutterConfig, GutterLineNumbersConfig}; + use crate::graphics::Rect; + use crate::DocumentId; + use helix_core::Rope; + + #[test] + fn test_default_gutter_widths() { + let mut view = View::new(DocumentId::default(), GutterConfig::default()); + view.area = Rect::new(40, 40, 40, 40); + + let rope = Rope::from_str("abc\n\tdef"); + let doc = Document::from(rope, None); + + assert_eq!(view.gutters.layout.len(), 5); + assert_eq!(view.gutters.layout[0].width(&view, &doc), 1); + assert_eq!(view.gutters.layout[1].width(&view, &doc), 1); + assert_eq!(view.gutters.layout[2].width(&view, &doc), 3); + assert_eq!(view.gutters.layout[3].width(&view, &doc), 1); + assert_eq!(view.gutters.layout[4].width(&view, &doc), 1); + } + + #[test] + fn test_configured_gutter_widths() { + let gutters = GutterConfig { + layout: vec![GutterType::Diagnostics], + ..Default::default() + }; + + let mut view = View::new(DocumentId::default(), gutters); + view.area = Rect::new(40, 40, 40, 40); + + let rope = Rope::from_str("abc\n\tdef"); + let doc = Document::from(rope, None); + + assert_eq!(view.gutters.layout.len(), 1); + assert_eq!(view.gutters.layout[0].width(&view, &doc), 1); + + let gutters = GutterConfig { + layout: vec![GutterType::Diagnostics, GutterType::LineNumbers], + line_numbers: GutterLineNumbersConfig { min_width: 10 }, + }; + + let mut view = View::new(DocumentId::default(), gutters); + view.area = Rect::new(40, 40, 40, 40); + + let rope = Rope::from_str("abc\n\tdef"); + let doc = Document::from(rope, None); + + assert_eq!(view.gutters.layout.len(), 2); + assert_eq!(view.gutters.layout[0].width(&view, &doc), 1); + assert_eq!(view.gutters.layout[1].width(&view, &doc), 10); + } + + #[test] + fn test_line_numbers_gutter_width_resizes() { + let gutters = GutterConfig { + layout: vec![GutterType::Diagnostics, GutterType::LineNumbers], + line_numbers: GutterLineNumbersConfig { min_width: 1 }, + }; + + let mut view = View::new(DocumentId::default(), gutters); + view.area = Rect::new(40, 40, 40, 40); + + let rope = Rope::from_str("a\nb"); + let doc_short = Document::from(rope, None); + + let rope = Rope::from_str("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np"); + let doc_long = Document::from(rope, None); + + assert_eq!(view.gutters.layout.len(), 2); + assert_eq!(view.gutters.layout[1].width(&view, &doc_short), 1); + assert_eq!(view.gutters.layout[1].width(&view, &doc_long), 2); + } +} diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index cf673e113ce2..04a9922a99bb 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -2,7 +2,6 @@ use bitflags::bitflags; bitflags! { /// Represents key modifiers (shift, control, alt). - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct KeyModifiers: u8 { const SHIFT = 0b0000_0001; const CONTROL = 0b0000_0010; diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 9cf36ae0505c..9a9804463953 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -66,17 +66,6 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) { view.offset.row = line.saturating_sub(relative); } -/// Applies a [`helix_core::Transaction`] to the given [`Document`] -/// and [`View`]. -pub fn apply_transaction( - transaction: &helix_core::Transaction, - doc: &mut Document, - view: &View, -) -> bool { - // TODO remove this helper function. Just call Document::apply everywhere directly. - doc.apply(transaction, view.id) -} - pub use document::Document; pub use editor::Editor; pub use theme::Theme; diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index cb0d3ac46d34..125725e08023 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,9 +1,10 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, + str, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use helix_core::hashmap; use helix_loader::merge_toml_values; use log::warn; @@ -15,12 +16,13 @@ use crate::graphics::UnderlineStyle; pub use crate::graphics::{Color, Modifier, Style}; pub static DEFAULT_THEME_DATA: Lazy = Lazy::new(|| { - toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") + let bytes = include_bytes!("../../theme.toml"); + toml::from_str(str::from_utf8(bytes).unwrap()).expect("Failed to parse base default theme") }); pub static BASE16_DEFAULT_THEME_DATA: Lazy = Lazy::new(|| { - toml::from_slice(include_bytes!("../../base16_theme.toml")) - .expect("Failed to parse base 16 default theme") + let bytes = include_bytes!("../../base16_theme.toml"); + toml::from_str(str::from_utf8(bytes).unwrap()).expect("Failed to parse base 16 default theme") }); pub static DEFAULT_THEME: Lazy = Lazy::new(|| Theme { @@ -70,7 +72,7 @@ impl Loader { fn load_theme( &self, name: &str, - base_them_name: &str, + base_theme_name: &str, only_default_dir: bool, ) -> Result { let path = self.path(name, only_default_dir); @@ -92,8 +94,8 @@ impl Loader { "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(), _ => self.load_theme( parent_theme_name, - base_them_name, - base_them_name == parent_theme_name, + base_theme_name, + base_theme_name == parent_theme_name, )?, }; @@ -148,8 +150,8 @@ impl Loader { // Loads the theme data as `toml::Value` first from the user_dir then in default_dir fn load_toml(&self, path: PathBuf) -> Result { - let data = std::fs::read(&path)?; - let value = toml::from_slice(data.as_slice())?; + let data = std::fs::read_to_string(path)?; + let value = toml::from_str(&data)?; Ok(value) } @@ -207,16 +209,18 @@ pub struct Theme { impl From for Theme { fn from(value: Value) -> Self { - let values: Result> = - toml::from_str(&value.to_string()).context("Failed to load theme"); - - let (styles, scopes, highlights) = build_theme_values(values); - - Self { - styles, - scopes, - highlights, - ..Default::default() + if let Value::Table(table) = value { + let (styles, scopes, highlights) = build_theme_values(table); + + Self { + styles, + scopes, + highlights, + ..Default::default() + } + } else { + warn!("Expected theme TOML value to be a table, found {:?}", value); + Default::default() } } } @@ -226,9 +230,9 @@ impl<'de> Deserialize<'de> for Theme { where D: Deserializer<'de>, { - let values = HashMap::::deserialize(deserializer)?; + let values = Map::::deserialize(deserializer)?; - let (styles, scopes, highlights) = build_theme_values(Ok(values)); + let (styles, scopes, highlights) = build_theme_values(values); Ok(Self { styles, @@ -240,39 +244,37 @@ impl<'de> Deserialize<'de> for Theme { } fn build_theme_values( - values: Result>, + mut values: Map, ) -> (HashMap, Vec, Vec