diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index ae2e5835e..39dc08982 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup mdBook
- uses: peaceiris/actions-mdbook@v1
+ uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'
# mdbook-version: '0.4.8'
@@ -27,14 +27,14 @@ jobs:
echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
- name: Deploy stable
- uses: peaceiris/actions-gh-pages@v3
+ uses: peaceiris/actions-gh-pages@v4
if: startswith(github.ref, 'refs/tags/')
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book
- name: Deploy
- uses: peaceiris/actions-gh-pages@v3
+ uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book
diff --git a/Cargo.lock b/Cargo.lock
index ad42dc7ae..017a248a8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -62,9 +62,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.81"
+version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "arc-swap"
@@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
-version = "1.0.90"
+version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
+checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
[[package]]
name = "cfg-if"
@@ -159,9 +159,9 @@ dependencies = [
[[package]]
name = "chrono"
-version = "0.4.37"
+version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -171,9 +171,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
-version = "5.3.0"
+version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee"
+checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad"
dependencies = [
"error-code",
]
@@ -365,9 +365,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encoding_rs"
-version = "0.8.33"
+version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [
"cfg-if",
]
@@ -538,9 +538,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "gix"
-version = "0.61.0"
+version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4e0e59a44bf00de058ee98d6ecf3c9ed8f8842c1da642258ae4120d41ded8f7"
+checksum = "5631c64fb4cd48eee767bf98a3cbc5c9318ef3bb71074d4c099a2371510282b6"
dependencies = [
"gix-actor",
"gix-attributes",
@@ -663,9 +663,9 @@ dependencies = [
[[package]]
name = "gix-config"
-version = "0.36.0"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62129c75e4b6229fe15fb9838cdc00c655e87105b651e4edd7c183fc5288b5d1"
+checksum = "7580e05996e893347ad04e1eaceb92e1c0e6a3ffe517171af99bf6b6df0ca6e5"
dependencies = [
"bstr",
"gix-config-value",
@@ -709,9 +709,9 @@ dependencies = [
[[package]]
name = "gix-diff"
-version = "0.42.0"
+version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4"
+checksum = "a5fbc24115b957346cd23fb0f47d830eb799c46c89cdcf2f5acc9bf2938c2d01"
dependencies = [
"bstr",
"gix-command",
@@ -729,9 +729,9 @@ dependencies = [
[[package]]
name = "gix-dir"
-version = "0.3.0"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3413ccd29130900c17574678aee640e4847909acae9febf6424dc77b782c6d32"
+checksum = "d6943a1f213ad7a060a0548ece229be53f3c2151534b126446ce3533eaf5f14c"
dependencies = [
"bstr",
"gix-discover",
@@ -784,9 +784,9 @@ dependencies = [
[[package]]
name = "gix-filter"
-version = "0.11.0"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd71bf3e64d8fb5d5635d4166ca5a36fe56b292ffff06eab1d93ea47fd5beb89"
+checksum = "5c0d1f01af62bfd2fb3dd291acc2b29d4ab3e96ad52a679174626508ce98ef12"
dependencies = [
"bstr",
"encoding_rs",
@@ -805,9 +805,9 @@ dependencies = [
[[package]]
name = "gix-fs"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634b8a743b0aae03c1a74ee0ea24e8c5136895efac64ce52b3ea106e1c6f0613"
+checksum = "e2184c40e7910529677831c8b481acf788ffd92427ed21fad65b6aa637e631b8"
dependencies = [
"gix-features",
"gix-utils",
@@ -861,9 +861,9 @@ dependencies = [
[[package]]
name = "gix-index"
-version = "0.31.1"
+version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "549621f13d9ccf325a7de45506a3266af0d08f915181c5687abb5e8669bfd2e6"
+checksum = "3383122cf18655ef4c097c0b935bba5eb56983947959aaf3b0ceb1949d4dd371"
dependencies = [
"bitflags 2.5.0",
"bstr",
@@ -929,9 +929,9 @@ dependencies = [
[[package]]
name = "gix-odb"
-version = "0.59.0"
+version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81b55378c719693380f66d9dd21ce46721eed2981d8789fc698ec1ada6fa176e"
+checksum = "e8bbb43d2fefdc4701ffdf9224844d05b136ae1b9a73c2f90710c8dd27a93503"
dependencies = [
"arc-swap",
"gix-date",
@@ -949,9 +949,9 @@ dependencies = [
[[package]]
name = "gix-pack"
-version = "0.49.0"
+version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6391aeaa030ad64aba346a9f5c69bb1c4e5c6fb4411705b03b40b49d8614ec30"
+checksum = "b58bad27c7677fa6b587aab3a1aca0b6c97373bd371a0a4290677c838c9bcaf1"
dependencies = [
"clru",
"gix-chunk",
@@ -969,9 +969,9 @@ dependencies = [
[[package]]
name = "gix-packetline-blocking"
-version = "0.17.3"
+version = "0.17.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca8ef6dd3ea50e26f3bf572e90c034d033c804d340cd1eb386392f184a9ba2f7"
+checksum = "c31d42378a3d284732e4d589979930d0d253360eccf7ec7a80332e5ccb77e14a"
dependencies = [
"bstr",
"faster-hex",
@@ -994,9 +994,9 @@ dependencies = [
[[package]]
name = "gix-pathspec"
-version = "0.7.2"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a96ed0e71ce9084a471fddfa74e842576a7cbf02fe8bd50388017ac461aed97"
+checksum = "d479789f3abd10f68a709454ce04cd68b54092ee882c8622ae3aa1bb9bf8496c"
dependencies = [
"bitflags 2.5.0",
"bstr",
@@ -1099,9 +1099,9 @@ dependencies = [
[[package]]
name = "gix-status"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca216db89947eca709f69ec5851aa76f9628e7c7aab7aa5a927d0c619d046bf2"
+checksum = "50c413bfd2952e4ee92e48438dac3c696f3555e586a34d184a427f6bedd1e4f9"
dependencies = [
"bstr",
"filetime",
@@ -1150,16 +1150,17 @@ dependencies = [
[[package]]
name = "gix-trace"
-version = "0.1.8"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b838b2db8f62c9447d483a4c28d251b67fee32741a82cb4d35e9eb4e9fdc5ab"
+checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e"
[[package]]
name = "gix-traverse"
-version = "0.38.0"
+version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95aef84bc777025403a09788b1e4815c06a19332e9e5d87a955e1ed7da9bf0cf"
+checksum = "f4029ec209b0cc480d209da3837a42c63801dd8548f09c1f4502c60accb62aeb"
dependencies = [
+ "bitflags 2.5.0",
"gix-commitgraph",
"gix-date",
"gix-hash",
@@ -1172,9 +1173,9 @@ dependencies = [
[[package]]
name = "gix-url"
-version = "0.27.2"
+version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f0b24f3ecc79a5a53539de9c2e99425d0ef23feacdcf3faac983aa9a2f26849"
+checksum = "0db829ebdca6180fbe32be7aed393591df6db4a72dbbc0b8369162390954d1cf"
dependencies = [
"bstr",
"gix-features",
@@ -1186,9 +1187,9 @@ dependencies = [
[[package]]
name = "gix-utils"
-version = "0.1.11"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92"
+checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc"
dependencies = [
"bstr",
"fastrand",
@@ -1207,9 +1208,9 @@ dependencies = [
[[package]]
name = "gix-worktree"
-version = "0.32.0"
+version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe78e03af9eec168eb187e05463a981c57f0a915f64b1788685a776bd2ef969c"
+checksum = "359a87dfef695b5f91abb9a424c947edca82768f34acfc269659f66174a510b4"
dependencies = [
"bstr",
"gix-attributes",
@@ -1392,6 +1393,7 @@ dependencies = [
"parking_lot",
"serde",
"serde_json",
+ "slotmap",
"thiserror",
"tokio",
"tokio-stream",
@@ -2092,18 +2094,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]]
name = "serde"
-version = "1.0.197"
+version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.197"
+version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
dependencies = [
"proc-macro2",
"quote",
@@ -2112,9 +2114,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.115"
+version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
+checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
dependencies = [
"itoa",
"ryu",
@@ -2471,9 +2473,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
-version = "0.22.2"
+version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdb9c9f15eae91dcd00ee0d86a281d16e6263786991b662b34fa9632c21a046b"
+checksum = "688200d842c76dd88f9a7719ecb0483f79f5a766fb1c100756d5d8a059abc71b"
dependencies = [
"cc",
"regex",
diff --git a/Cargo.toml b/Cargo.toml
index 3bfc4b8e1..6206281b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,6 +39,7 @@ package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.22" }
nucleo = "0.2.0"
+slotmap = "1.0.7"
[workspace.package]
version = "24.3.0"
diff --git a/base16_theme.toml b/base16_theme.toml
index 49f09afed..4a071a60b 100644
--- a/base16_theme.toml
+++ b/base16_theme.toml
@@ -29,6 +29,7 @@
"namespace" = "magenta"
"ui.help" = { fg = "white", bg = "black" }
"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] }
+"ui.virtual.ruler" = { bg = "black" }
"markup.heading" = "blue"
"markup.list" = "red"
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index ba330cf77..027b885a8 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -3,12 +3,19 @@ # Summary
[Helix](./title-page.md)
- [Installation](./install.md)
+ - [Package Managers](./package-managers.md)
+ - [Building from source](./building-from-source.md)
- [Usage](./usage.md)
+ - [Registers](./registers.md)
+ - [Surround](./surround.md)
+ - [Textobjects](./textobjects.md)
+ - [Syntax aware motions](./syntax-aware-motions.md)
- [Keymap](./keymap.md)
- [Commands](./commands.md)
- [Language support](./lang-support.md)
- [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md)
+ - [Editor](./editor.md)
- [Themes](./themes.md)
- [Key remapping](./remapping.md)
- [Languages](./languages.md)
diff --git a/book/src/building-from-source.md b/book/src/building-from-source.md
new file mode 100644
index 000000000..666917ea4
--- /dev/null
+++ b/book/src/building-from-source.md
@@ -0,0 +1,158 @@
+## Building from source
+
+- [Configuring Helix's runtime files](#configuring-helixs-runtime-files)
+ - [Linux and macOS](#linux-and-macos)
+ - [Windows](#windows)
+ - [Multiple runtime directories](#multiple-runtime-directories)
+ - [Note to packagers](#note-to-packagers)
+- [Validating the installation](#validating-the-installation)
+- [Configure the desktop shortcut](#configure-the-desktop-shortcut)
+
+Requirements:
+
+Clone the Helix GitHub repository into a directory of your choice. The
+examples in this documentation assume installation into either `~/src/` on
+Linux and macOS, or `%userprofile%\src\` on Windows.
+
+- The [Rust toolchain](https://www.rust-lang.org/tools/install)
+- The [Git version control system](https://git-scm.com/)
+- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang
+
+If you are using the `musl-libc` standard library 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"
+```
+
+1. Clone the repository:
+
+ ```sh
+ git clone https://github.com/helix-editor/helix
+ cd helix
+ ```
+
+2. Compile from source:
+
+ ```sh
+ cargo install --path helix-term --locked
+ ```
+
+ This command will create the `hx` executable and construct the tree-sitter
+ grammars in the local `runtime` folder.
+
+> ๐ก If you do not want to fetch or build grammars, set an environment variable `HELIX_DISABLE_AUTO_GRAMMAR_BUILD`
+
+> ๐ก Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
+> grammars with `hx --grammar fetch` and compile them with
+> `hx --grammar build`. This will install them in
+> the `runtime` directory within the user's helix config directory (more
+> [details below](#multiple-runtime-directories)).
+
+### Configuring Helix's runtime files
+
+#### Linux and macOS
+
+The **runtime** directory is one below the Helix source, so either export a
+`HELIX_RUNTIME` environment variable to point to that directory and add it to
+your `~/.bashrc` or equivalent:
+
+```sh
+export HELIX_RUNTIME=~/src/helix/runtime
+```
+
+Or, create a symbolic link:
+
+```sh
+ln -Ts $PWD/runtime ~/.config/helix/runtime
+```
+
+If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
+
+#### Windows
+
+Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
+`Edit environment variables for your account`) or use the `setx` command in
+Cmd:
+
+```sh
+setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
+```
+
+> ๐ก `%userprofile%` resolves to your user directory like
+> `C:\Users\Your-Name\` for example.
+
+Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
+
+| Method | Command |
+| ---------- | -------------------------------------------------------------------------------------- |
+| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
+| Cmd | `cd %appdata%\helix` `mklink /D runtime "%userprofile%\src\helix\runtime"` |
+
+> ๐ก On Windows, creating a symbolic link may require running PowerShell or
+> Cmd as an administrator.
+
+#### Multiple runtime directories
+
+When Helix finds multiple runtime directories it will search through them for files in the
+following order:
+
+1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
+ developing and testing helix only).
+2. `runtime/` subdirectory of OS-dependent helix user config directory.
+3. `$HELIX_RUNTIME`
+4. Distribution-specific fallback directory (set at compile timeโnot run timeโ
+ with the `HELIX_DEFAULT_RUNTIME` environment variable)
+5. `runtime/` subdirectory of path to Helix executable.
+
+This order also sets the priority for selecting which file will be used if multiple runtime
+directories have files with the same name.
+
+#### Note to packagers
+
+If you are making a package of Helix for end users, to provide a good out of
+the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment
+variable at build time (before invoking `cargo build`) to a directory which
+will store the final runtime files after installation. For example, say you want
+to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build
+script could follow are:
+
+1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime`
+1. `cargo build --profile opt --locked --path helix-term`
+1. `cp -r runtime $BUILD_DIR/usr/lib/helix/`
+1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx`
+
+This way the resulting `hx` binary will always look for its runtime directory in
+`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix`
+or `HELIX_RUNTIME`.
+
+### Validating the installation
+
+To make sure everything is set up as expected you should run the Helix health
+check:
+
+```sh
+hx --health
+```
+
+For more information on the health check results refer to
+[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck).
+
+### Configure the desktop shortcut
+
+If your desktop environment supports the
+[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:
+
+```sh
+cp contrib/Helix.desktop ~/.local/share/applications
+cp contrib/helix.png ~/.icons # or ~/.local/share/icons
+```
+
+To use another terminal than the system default, you can modify the `.desktop`
+file. For example, to use `kitty`:
+
+```sh
+sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
+sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
+```
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 5e22cebf8..0cd12568b 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -33,372 +33,3 @@ # Configuration
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
-## 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. | `250` |
-| `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |
-| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
-| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
-| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
-| `auto-info` | Whether to display info boxes | `true` |
-| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
-| `undercurl` | Set to `true` to override automatic detection of terminal undercurl 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` |
-| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
-| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
-| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
-| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
-| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
-| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
-| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
-
-### `[editor.statusline]` Section
-
-Allows configuring the statusline at the bottom of the editor.
-
-The configuration distinguishes between three areas of the status line:
-
-`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]`
-
-Statusline elements can be defined as follows:
-
-```toml
-[editor.statusline]
-left = ["mode", "spinner"]
-center = ["file-name"]
-right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
-separator = "โ"
-mode.normal = "NORMAL"
-mode.insert = "INSERT"
-mode.select = "SELECT"
-```
-The `[editor.statusline]` key takes the following sub-keys:
-
-| Key | Description | Default |
-| --- | --- | --- |
-| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
-| `center` | A list of elements aligned to the middle of the statusline | `[]` |
-| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
-| `separator` | The character used to separate elements in the statusline | `"โ"` |
-| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
-| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
-| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
-
-The following statusline elements can be configured:
-
-| Key | Description |
-| ------ | ----------- |
-| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
-| `spinner` | A progress spinner indicating LSP activity |
-| `file-name` | The path/name of the opened file |
-| `file-absolute-path` | The absolute path/name of the opened file |
-| `file-base-name` | The basename of the opened file |
-| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
-| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
-| `file-line-ending` | The file line endings (CRLF or LF) |
-| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
-| `total-line-numbers` | The total line numbers of the opened file |
-| `file-type` | The type of the opened file |
-| `diagnostics` | The number of warnings and/or errors |
-| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
-| `selections` | The number of active selections |
-| `primary-selection-length` | The number of characters currently in primary selection |
-| `position` | The cursor position |
-| `position-percentage` | The cursor position as a percentage of the total number of lines |
-| `separator` | The string defined in `editor.statusline.separator` (defaults to `"โ"`) |
-| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
-| `version-control` | The current branch name or detached commit hash of the opened workspace |
-| `register` | The current selected register |
-
-### `[editor.lsp]` Section
-
-| Key | Description | Default |
-| --- | ----------- | ------- |
-| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
-| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
-| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
-| `display-inlay-hints` | Display inlay hints[^2] | `false` |
-| `display-signature-help-docs` | Display docs under signature help popup | `true` |
-| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
-| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
-
-[^1]: By default, a progress spinner is shown in the statusline beside the file path.
-
-[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them!
-
-### `[editor.cursor-shape]` Section
-
-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
-> change shape.
-
-| Key | Description | Default |
-| --- | ----------- | ------- |
-| `normal` | Cursor shape in [normal mode][normal mode] | `block` |
-| `insert` | Cursor shape in [insert mode][insert mode] | `block` |
-| `select` | Cursor shape in [select mode][select mode] | `block` |
-
-[normal mode]: ./keymap.md#normal-mode
-[insert mode]: ./keymap.md#insert-mode
-[select mode]: ./keymap.md#select--extend-mode
-
-### `[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.
-
-All git related options are only enabled in a git repository.
-
-| Key | Description | Default |
-|--|--|---------|
-|`hidden` | Enables ignoring hidden files | `true`
-|`follow-symlinks` | Follow symlinks instead of ignoring them | `true`
-|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `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.excludesfile` option | `true`
-|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
-|`max-depth` | Set with an integer value for maximum depth to recurse | Unset by default
-
-Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files.
-
-Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory:
-- Linux and Mac: `~/.config/helix/ignore`
-- Windows: `%AppData%\helix\ignore`
-
-Example:
-
-```ini
-# unignore in file picker and global search
-!.github/
-!.gitignore
-!.gitattributes
-```
-
-### `[editor.auto-pairs]` Section
-
-Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
-simple boolean value, or a specific mapping of pairs of single characters.
-
-To disable auto-pairs altogether, set `auto-pairs` to `false`:
-
-```toml
-[editor]
-auto-pairs = false # defaults to `true`
-```
-
-The default pairs are (){}[]''""``, but these can be customized by
-setting `auto-pairs` to a TOML table:
-
-```toml
-[editor.auto-pairs]
-'(' = ')'
-'{' = '}'
-'[' = ']'
-'"' = '"'
-'`' = '`'
-'<' = '>'
-```
-
-Additionally, this setting can be used in a language config. Unless
-the editor setting is `false`, this will override the editor config in
-documents with this language.
-
-Example `languages.toml` that adds `<>` and removes `''`
-
-```toml
-[[language]]
-name = "rust"
-
-[language.auto-pairs]
-'(' = ')'
-'{' = '}'
-'[' = ']'
-'"' = '"'
-'`' = '`'
-'<' = '>'
-```
-
-### `[editor.search]` Section
-
-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` |
-
-### `[editor.whitespace]` Section
-
-Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace.
-
-| Key | Description | Default |
-|-----|-------------|---------|
-| `render` | Whether to render whitespace. May either be `all` or `none`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `none` |
-| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below |
-
-Example
-
-```toml
-[editor.whitespace]
-render = "all"
-# or control each character
-[editor.whitespace.render]
-space = "all"
-tab = "all"
-nbsp = "none"
-nnbsp = "none"
-newline = "none"
-
-[editor.whitespace.characters]
-space = "ยท"
-nbsp = "โฝ"
-nnbsp = "โฃ"
-tab = "โ"
-newline = "โ"
-tabpad = "ยท" # Tabs will look like "โยทยทยท" (depending on tab width)
-```
-
-### `[editor.indent-guides]` Section
-
-Options for rendering vertical indent guides.
-
-| Key | Description | Default |
-| --- | --- | --- |
-| `render` | Whether to render indent guides | `false` |
-| `character` | Literal character to use for rendering the indent guide | `โ` |
-| `skip-levels` | Number of indent levels to skip | `0` |
-
-Example:
-
-```toml
-[editor.indent-guides]
-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 subsections.
-
-| 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.diagnostics]` Section
-
-Currently unused
-
-#### `[editor.gutters.diff]` Section
-
-The `diff` gutter option displays colored bars indicating whether a `git` diff represents that a line was added, removed or changed.
-These colors are controlled by the theme attributes `diff.plus`, `diff.minus` and `diff.delta`.
-
-Other diff providers will eventually be supported by a future plugin system.
-
-There are currently no options for this section.
-
-#### `[editor.gutters.spacer]` Section
-
-Currently unused
-
-### `[editor.soft-wrap]` Section
-
-Options for soft wrapping lines that exceed the view width:
-
-| Key | Description | Default |
-| --- | --- | --- |
-| `enable` | Whether soft wrapping is enabled. | `false` |
-| `max-wrap` | Maximum free space left at the end of the line. | `20` |
-| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
-| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `โช ` |
-| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` |
-
-Example:
-
-```toml
-[editor.soft-wrap]
-enable = true
-max-wrap = 25 # increase value to reduce forced mid-word wrapping
-max-indent-retain = 0
-wrap-indicator = "" # set wrap-indicator to "" to hide it
-```
-
-### `[editor.smart-tab]` Section
-
-Options for navigating and editing using tab key.
-
-| Key | Description | Default |
-|------------|-------------|---------|
-| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
-| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |
-
-
-Due to lack of support for S-tab in some terminals, the default keybindings don't fully embrace smart-tab editing experience. If you enjoy smart-tab navigation and a terminal that supports the [Enhanced Keyboard protocol](https://github.com/helix-editor/helix/wiki/Terminal-Support#enhanced-keyboard-protocol), consider setting extra keybindings:
-
-```
-[keys.normal]
-tab = "move_parent_node_end"
-S-tab = "move_parent_node_start"
-
-[keys.insert]
-S-tab = "move_parent_node_start"
-
-[keys.select]
-tab = "extend_parent_node_end"
-S-tab = "extend_parent_node_start"
-```
diff --git a/book/src/editor.md b/book/src/editor.md
new file mode 100644
index 000000000..88541a7bc
--- /dev/null
+++ b/book/src/editor.md
@@ -0,0 +1,386 @@
+## Editor
+
+- [`[editor]` Section](#editor-section)
+- [`[editor.statusline]` Section](#editorstatusline-section)
+- [`[editor.lsp]` Section](#editorlsp-section)
+- [`[editor.cursor-shape]` Section](#editorcursor-shape-section)
+- [`[editor.file-picker]` Section](#editorfile-picker-section)
+- [`[editor.auto-pairs]` Section](#editorauto-pairs-section)
+- [`[editor.search]` Section](#editorsearch-section)
+- [`[editor.whitespace]` Section](#editorwhitespace-section)
+- [`[editor.indent-guides]` Section](#editorindent-guides-section)
+- [`[editor.gutters]` Section](#editorgutters-section)
+ - [`[editor.gutters.line-numbers]` Section](#editorguttersline-numbers-section)
+ - [`[editor.gutters.diagnostics]` Section](#editorguttersdiagnostics-section)
+ - [`[editor.gutters.diff]` Section](#editorguttersdiff-section)
+ - [`[editor.gutters.spacer]` Section](#editorguttersspacer-section)
+- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
+- [`[editor.smart-tab]` Section](#editorsmart-tab-section)
+
+### `[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. | `250` |
+| `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |
+| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
+| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
+| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
+| `auto-info` | Whether to display info boxes | `true` |
+| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
+| `undercurl` | Set to `true` to override automatic detection of terminal undercurl 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` |
+| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
+| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
+| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
+| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
+| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
+| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
+| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
+
+### `[editor.statusline]` Section
+
+Allows configuring the statusline at the bottom of the editor.
+
+The configuration distinguishes between three areas of the status line:
+
+`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]`
+
+Statusline elements can be defined as follows:
+
+```toml
+[editor.statusline]
+left = ["mode", "spinner"]
+center = ["file-name"]
+right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
+separator = "โ"
+mode.normal = "NORMAL"
+mode.insert = "INSERT"
+mode.select = "SELECT"
+```
+The `[editor.statusline]` key takes the following sub-keys:
+
+| Key | Description | Default |
+| --- | --- | --- |
+| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
+| `center` | A list of elements aligned to the middle of the statusline | `[]` |
+| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
+| `separator` | The character used to separate elements in the statusline | `"โ"` |
+| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
+| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
+| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
+
+The following statusline elements can be configured:
+
+| Key | Description |
+| ------ | ----------- |
+| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
+| `spinner` | A progress spinner indicating LSP activity |
+| `file-name` | The path/name of the opened file |
+| `file-absolute-path` | The absolute path/name of the opened file |
+| `file-base-name` | The basename of the opened file |
+| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
+| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
+| `file-line-ending` | The file line endings (CRLF or LF) |
+| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
+| `total-line-numbers` | The total line numbers of the opened file |
+| `file-type` | The type of the opened file |
+| `diagnostics` | The number of warnings and/or errors |
+| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
+| `selections` | The number of active selections |
+| `primary-selection-length` | The number of characters currently in primary selection |
+| `position` | The cursor position |
+| `position-percentage` | The cursor position as a percentage of the total number of lines |
+| `separator` | The string defined in `editor.statusline.separator` (defaults to `"โ"`) |
+| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
+| `version-control` | The current branch name or detached commit hash of the opened workspace |
+| `register` | The current selected register |
+
+### `[editor.lsp]` Section
+
+| Key | Description | Default |
+| --- | ----------- | ------- |
+| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
+| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
+| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
+| `display-inlay-hints` | Display inlay hints[^2] | `false` |
+| `display-signature-help-docs` | Display docs under signature help popup | `true` |
+| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
+| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
+
+[^1]: By default, a progress spinner is shown in the statusline beside the file path.
+
+[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them!
+
+### `[editor.cursor-shape]` Section
+
+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
+> change shape.
+
+| Key | Description | Default |
+| --- | ----------- | ------- |
+| `normal` | Cursor shape in [normal mode][normal mode] | `block` |
+| `insert` | Cursor shape in [insert mode][insert mode] | `block` |
+| `select` | Cursor shape in [select mode][select mode] | `block` |
+
+[normal mode]: ./keymap.md#normal-mode
+[insert mode]: ./keymap.md#insert-mode
+[select mode]: ./keymap.md#select--extend-mode
+
+### `[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.
+
+All git related options are only enabled in a git repository.
+
+| Key | Description | Default |
+|--|--|---------|
+|`hidden` | Enables ignoring hidden files | `true`
+|`follow-symlinks` | Follow symlinks instead of ignoring them | `true`
+|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `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.excludesfile` option | `true`
+|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
+|`max-depth` | Set with an integer value for maximum depth to recurse | Unset by default
+
+Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files.
+
+Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory:
+- Linux and Mac: `~/.config/helix/ignore`
+- Windows: `%AppData%\helix\ignore`
+
+Example:
+
+```ini
+# unignore in file picker and global search
+!.github/
+!.gitignore
+!.gitattributes
+```
+
+### `[editor.auto-pairs]` Section
+
+Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
+simple boolean value, or a specific mapping of pairs of single characters.
+
+To disable auto-pairs altogether, set `auto-pairs` to `false`:
+
+```toml
+[editor]
+auto-pairs = false # defaults to `true`
+```
+
+The default pairs are (){}[]''""``, but these can be customized by
+setting `auto-pairs` to a TOML table:
+
+```toml
+[editor.auto-pairs]
+'(' = ')'
+'{' = '}'
+'[' = ']'
+'"' = '"'
+'`' = '`'
+'<' = '>'
+```
+
+Additionally, this setting can be used in a language config. Unless
+the editor setting is `false`, this will override the editor config in
+documents with this language.
+
+Example `languages.toml` that adds `<>` and removes `''`
+
+```toml
+[[language]]
+name = "rust"
+
+[language.auto-pairs]
+'(' = ')'
+'{' = '}'
+'[' = ']'
+'"' = '"'
+'`' = '`'
+'<' = '>'
+```
+
+### `[editor.search]` Section
+
+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` |
+
+### `[editor.whitespace]` Section
+
+Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace.
+
+| Key | Description | Default |
+|-----|-------------|---------|
+| `render` | Whether to render whitespace. May either be `all` or `none`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `none` |
+| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below |
+
+Example
+
+```toml
+[editor.whitespace]
+render = "all"
+# or control each character
+[editor.whitespace.render]
+space = "all"
+tab = "all"
+nbsp = "none"
+nnbsp = "none"
+newline = "none"
+
+[editor.whitespace.characters]
+space = "ยท"
+nbsp = "โฝ"
+nnbsp = "โฃ"
+tab = "โ"
+newline = "โ"
+tabpad = "ยท" # Tabs will look like "โยทยทยท" (depending on tab width)
+```
+
+### `[editor.indent-guides]` Section
+
+Options for rendering vertical indent guides.
+
+| Key | Description | Default |
+| --- | --- | --- |
+| `render` | Whether to render indent guides | `false` |
+| `character` | Literal character to use for rendering the indent guide | `โ` |
+| `skip-levels` | Number of indent levels to skip | `0` |
+
+Example:
+
+```toml
+[editor.indent-guides]
+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 subsections.
+
+| 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.diagnostics]` Section
+
+Currently unused
+
+#### `[editor.gutters.diff]` Section
+
+The `diff` gutter option displays colored bars indicating whether a `git` diff represents that a line was added, removed or changed.
+These colors are controlled by the theme attributes `diff.plus`, `diff.minus` and `diff.delta`.
+
+Other diff providers will eventually be supported by a future plugin system.
+
+There are currently no options for this section.
+
+#### `[editor.gutters.spacer]` Section
+
+Currently unused
+
+### `[editor.soft-wrap]` Section
+
+Options for soft wrapping lines that exceed the view width:
+
+| Key | Description | Default |
+| --- | --- | --- |
+| `enable` | Whether soft wrapping is enabled. | `false` |
+| `max-wrap` | Maximum free space left at the end of the line. | `20` |
+| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
+| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `โช ` |
+| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` |
+
+Example:
+
+```toml
+[editor.soft-wrap]
+enable = true
+max-wrap = 25 # increase value to reduce forced mid-word wrapping
+max-indent-retain = 0
+wrap-indicator = "" # set wrap-indicator to "" to hide it
+```
+
+### `[editor.smart-tab]` Section
+
+Options for navigating and editing using tab key.
+
+| Key | Description | Default |
+|------------|-------------|---------|
+| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
+| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |
+
+
+Due to lack of support for S-tab in some terminals, the default keybindings don't fully embrace smart-tab editing experience. If you enjoy smart-tab navigation and a terminal that supports the [Enhanced Keyboard protocol](https://github.com/helix-editor/helix/wiki/Terminal-Support#enhanced-keyboard-protocol), consider setting extra keybindings:
+
+```
+[keys.normal]
+tab = "move_parent_node_end"
+S-tab = "move_parent_node_start"
+
+[keys.insert]
+S-tab = "move_parent_node_start"
+
+[keys.select]
+tab = "extend_parent_node_end"
+S-tab = "extend_parent_node_start"
+```
diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md
index efdc8b634..27fd583c2 100644
--- a/book/src/generated/lang-support.md
+++ b/book/src/generated/lang-support.md
@@ -1,6 +1,7 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| ada | โ | โ | | `ada_language_server`, `ada_language_server` |
+| adl | โ | โ | โ | |
| agda | โ | | | |
| astro | โ | | | |
| awk | โ | โ | | `awk-language-server` |
@@ -61,6 +62,7 @@
| git-config | โ | | | |
| git-ignore | โ | | | |
| git-rebase | โ | | | |
+| gjs | โ | โ | โ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| gleam | โ | โ | | `gleam` |
| glimmer | โ | | | `ember-language-server` |
| glsl | โ | โ | โ | |
@@ -72,6 +74,7 @@
| gowork | โ | | | `gopls` |
| graphql | โ | โ | | `graphql-lsp` |
| groovy | โ | | | |
+| gts | โ | โ | โ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| hare | โ | | | |
| haskell | โ | โ | | `haskell-language-server-wrapper` |
| haskell-persistent | โ | | | |
@@ -82,11 +85,12 @@
| hoon | โ | | | |
| hosts | โ | | | |
| html | โ | | | `vscode-html-language-server` |
-| hurl | โ | | โ | |
+| hurl | โ | โ | โ | |
| hyprlang | โ | | โ | |
| idris | | | | `idris2-lsp` |
| iex | โ | | | |
| ini | โ | | | |
+| inko | โ | โ | โ | |
| janet | โ | | | |
| java | โ | โ | โ | `jdtls` |
| javascript | โ | โ | โ | `typescript-language-server` |
@@ -100,10 +104,11 @@
| julia | โ | โ | โ | `julia` |
| just | โ | โ | โ | |
| kdl | โ | โ | โ | |
-| koka | โ | | โ | |
+| koka | โ | | โ | `koka` |
| kotlin | โ | | | `kotlin-language-server` |
| latex | โ | โ | | `texlab` |
| ld | โ | | โ | |
+| ldif | โ | | | |
| lean | โ | | | `lean` |
| ledger | โ | | | |
| llvm | โ | โ | โ | |
@@ -120,6 +125,7 @@
| mermaid | โ | | | |
| meson | โ | | โ | |
| mint | | | | `mint` |
+| move | โ | | | |
| msbuild | โ | | โ | |
| nasm | โ | โ | | |
| nickel | โ | | โ | `nls` |
@@ -138,6 +144,7 @@
| passwd | โ | | | |
| pem | โ | | | |
| perl | โ | โ | โ | `perlnavigator` |
+| pest | โ | โ | โ | `pest-language-server` |
| php | โ | โ | โ | `intelephense` |
| php-only | โ | | | |
| pkgbuild | โ | โ | โ | `pkgbuild-language-server`, `bash-language-server` |
@@ -172,7 +179,7 @@
| smali | โ | | โ | |
| smithy | โ | | | `cs` |
| sml | โ | | | |
-| solidity | โ | | | `solc` |
+| solidity | โ | โ | | `solc` |
| spicedb | โ | | | |
| sql | โ | | | |
| sshclientconfig | โ | | | |
@@ -195,7 +202,7 @@
| tsx | โ | โ | โ | `typescript-language-server` |
| twig | โ | | | |
| typescript | โ | โ | โ | `typescript-language-server` |
-| typst | โ | | | `typst-lsp` |
+| typst | โ | | | `tinymist`, `typst-lsp` |
| ungrammar | โ | | | |
| unison | โ | | โ | |
| uxntal | โ | | | |
@@ -213,6 +220,7 @@
| wren | โ | โ | โ | |
| xit | โ | | | |
| xml | โ | | โ | |
+| xtc | โ | | | |
| yaml | โ | | โ | `yaml-language-server`, `ansible-language-server` |
| yuck | โ | | | |
| zig | โ | โ | โ | `zls` |
diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md
index dbb8b5f38..cf1b550dc 100644
--- a/book/src/generated/typable-cmd.md
+++ b/book/src/generated/typable-cmd.md
@@ -87,3 +87,4 @@
| `:redraw` | Clear and re-render the whole UI |
| `:move` | Move the current buffer and its corresponding file to a different path |
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
+| `:read`, `:r` | Load a file into buffer |
diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md
index 0763a3c5c..1a47b79c6 100644
--- a/book/src/guides/adding_languages.md
+++ b/book/src/guides/adding_languages.md
@@ -16,7 +16,7 @@ ## Language configuration
> ๐ก If you are adding a new Language Server configuration, make sure to update
> the
-> [Language Server Wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
+> [Language Server Wiki](https://github.com/helix-editor/helix/wiki/Language-Server-Configurations)
> with the installation instructions.
## Grammar configuration
diff --git a/book/src/install.md b/book/src/install.md
index 07865e698..debc82b01 100644
--- a/book/src/install.md
+++ b/book/src/install.md
@@ -1,38 +1,10 @@
# Installing Helix
-
-- [Pre-built binaries](#pre-built-binaries)
-- [Linux, macOS, Windows and OpenBSD packaging status](#linux-macos-windows-and-openbsd-packaging-status)
-- [Linux](#linux)
- - [Ubuntu](#ubuntu)
- - [Fedora/RHEL](#fedorarhel)
- - [Arch Linux extra](#arch-linux-extra)
- - [NixOS](#nixos)
- - [Flatpak](#flatpak)
- - [Snap](#snap)
- - [AppImage](#appimage)
-- [macOS](#macos)
- - [Homebrew Core](#homebrew-core)
- - [MacPorts](#macports)
-- [Windows](#windows)
- - [Winget](#winget)
- - [Scoop](#scoop)
- - [Chocolatey](#chocolatey)
- - [MSYS2](#msys2)
-- [Building from source](#building-from-source)
- - [Configuring Helix's runtime files](#configuring-helixs-runtime-files)
- - [Linux and macOS](#linux-and-macos)
- - [Windows](#windows)
- - [Multiple runtime directories](#multiple-runtime-directories)
- - [Validating the installation](#validating-the-installation)
- - [Configure the desktop shortcut](#configure-the-desktop-shortcut)
-
-
To install Helix, follow the instructions specific to your operating system.
Note that:
- To get the latest nightly version of Helix, you need to
- [build from source](#building-from-source).
+ [build from source](./building-from-source.md).
- To take full advantage of Helix, install the language servers for your
preferred programming languages. See the
@@ -41,291 +13,7 @@ # Installing Helix
## Pre-built binaries
-Download pre-built binaries from the
-[GitHub Releases page](https://github.com/helix-editor/helix/releases). Add the binary to your system's `$PATH` to use it from the command
-line.
+Download pre-built binaries from the [GitHub Releases page](https://github.com/helix-editor/helix/releases).
+Add the `hx` binary to your system's `$PATH` to use it from the command line, and copy the `runtime` directory into the config directory (for example `~/.config/helix/runtime` on Linux/macOS).
+The runtime location can be overriden via the HELIX_RUNTIME environment variable.
-## Linux, macOS, Windows and OpenBSD packaging status
-
-[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
-
-## Linux
-
-The following third party repositories are available:
-
-### Ubuntu
-
-Add the `PPA` for Helix:
-
-```sh
-sudo add-apt-repository ppa:maveonair/helix-editor
-sudo apt update
-sudo apt install helix
-```
-
-### Fedora/RHEL
-
-```sh
-sudo dnf install helix
-```
-
-### Arch Linux extra
-
-Releases are available in the `extra` repository:
-
-```sh
-sudo pacman -S helix
-```
-
-> ๐ก When installed from the `extra` repository, run Helix with `helix` instead of `hx`.
->
-> For example:
-> ```sh
-> helix --health
-> ```
-> to check health
-
-Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
-in the AUR, which builds the master branch.
-
-### NixOS
-
-Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute,
-the unstable channel usually carries the latest release.
-
-Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project
-root. Use `nix develop` to spin up a reproducible development shell. Outputs are
-cached for each push to master using [Cachix](https://www.cachix.org/). The
-flake is configured to automatically make use of this cache assuming the user
-accepts the new settings on first use.
-
-If you are using a version of Nix without flakes enabled,
-[install Cachix CLI](https://docs.cachix.org/installation) and use
-`cachix use helix` to configure Nix to use cached outputs when possible.
-
-### Flatpak
-
-Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix):
-
-```sh
-flatpak install flathub com.helix_editor.Helix
-flatpak run com.helix_editor.Helix
-```
-
-### Snap
-
-Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with:
-
-```sh
-snap install --classic helix
-```
-
-This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`.
-
-### AppImage
-
-Install Helix using the Linux [AppImage](https://appimage.org/) format.
-Download the official Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page.
-
-```sh
-chmod +x helix-*.AppImage # change permission for executable mode
-./helix-*.AppImage # run helix
-```
-
-## macOS
-
-### Homebrew Core
-
-```sh
-brew install helix
-```
-
-### MacPorts
-
-```sh
-port install helix
-```
-
-## Windows
-
-Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
-or [MSYS2](https://msys2.org/).
-
-### Winget
-Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer.
-You can get [App Installer from the Microsoft Store](https://www.microsoft.com/p/app-installer/9nblggh4nns1#activetab=pivot:overviewtab). If it's already installed, make sure it is updated with the latest version.
-
-```sh
-winget install Helix.Helix
-```
-
-### Scoop
-
-```sh
-scoop install helix
-```
-
-### Chocolatey
-
-```sh
-choco install helix
-```
-
-### MSYS2
-
-For 64-bit Windows 8.1 or above:
-
-```sh
-pacman -S mingw-w64-ucrt-x86_64-helix
-```
-
-## Building from source
-
-Requirements:
-
-Clone the Helix GitHub repository into a directory of your choice. The
-examples in this documentation assume installation into either `~/src/` on
-Linux and macOS, or `%userprofile%\src\` on Windows.
-
-- The [Rust toolchain](https://www.rust-lang.org/tools/install)
-- The [Git version control system](https://git-scm.com/)
-- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang
-
-If you are using the `musl-libc` standard library 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"
-```
-
-1. Clone the repository:
-
- ```sh
- git clone https://github.com/helix-editor/helix
- cd helix
- ```
-
-2. Compile from source:
-
- ```sh
- cargo install --path helix-term --locked
- ```
-
- This command will create the `hx` executable and construct the tree-sitter
- grammars in the local `runtime` folder.
-
-> ๐ก If you do not want to fetch or build grammars, set an environment variable `HELIX_DISABLE_AUTO_GRAMMAR_BUILD`
-
-> ๐ก Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
-> grammars with `hx --grammar fetch` and compile them with
-> `hx --grammar build`. This will install them in
-> the `runtime` directory within the user's helix config directory (more
-> [details below](#multiple-runtime-directories)).
-
-### Configuring Helix's runtime files
-
-#### Linux and macOS
-
-The **runtime** directory is one below the Helix source, so either export a
-`HELIX_RUNTIME` environment variable to point to that directory and add it to
-your `~/.bashrc` or equivalent:
-
-```sh
-export HELIX_RUNTIME=~/src/helix/runtime
-```
-
-Or, create a symbolic link:
-
-```sh
-ln -Ts $PWD/runtime ~/.config/helix/runtime
-```
-
-If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
-
-#### Windows
-
-Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
-`Edit environment variables for your account`) or use the `setx` command in
-Cmd:
-
-```sh
-setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
-```
-
-> ๐ก `%userprofile%` resolves to your user directory like
-> `C:\Users\Your-Name\` for example.
-
-Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
-
-| Method | Command |
-| ---------- | -------------------------------------------------------------------------------------- |
-| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
-| Cmd | `cd %appdata%\helix` `mklink /D runtime "%userprofile%\src\helix\runtime"` |
-
-> ๐ก On Windows, creating a symbolic link may require running PowerShell or
-> Cmd as an administrator.
-
-#### Multiple runtime directories
-
-When Helix finds multiple runtime directories it will search through them for files in the
-following order:
-
-1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
- developing and testing helix only).
-2. `runtime/` subdirectory of OS-dependent helix user config directory.
-3. `$HELIX_RUNTIME`
-4. Distribution-specific fallback directory (set at compile timeโnot run timeโ
- with the `HELIX_DEFAULT_RUNTIME` environment variable)
-5. `runtime/` subdirectory of path to Helix executable.
-
-This order also sets the priority for selecting which file will be used if multiple runtime
-directories have files with the same name.
-
-#### Note to packagers
-
-If you are making a package of Helix for end users, to provide a good out of
-the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment
-variable at build time (before invoking `cargo build`) to a directory which
-will store the final runtime files after installation. For example, say you want
-to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build
-script could follow are:
-
-1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime`
-1. `cargo build --profile opt --locked --path helix-term`
-1. `cp -r runtime $BUILD_DIR/usr/lib/helix/`
-1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx`
-
-This way the resulting `hx` binary will always look for its runtime directory in
-`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix`
-or `HELIX_RUNTIME`.
-
-### Validating the installation
-
-To make sure everything is set up as expected you should run the Helix health
-check:
-
-```sh
-hx --health
-```
-
-For more information on the health check results refer to
-[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck).
-
-### Configure the desktop shortcut
-
-If your desktop environment supports the
-[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:
-
-```sh
-cp contrib/Helix.desktop ~/.local/share/applications
-cp contrib/helix.png ~/.icons # or ~/.local/share/icons
-```
-
-To use another terminal than the system default, you can modify the `.desktop`
-file. For example, to use `kitty`:
-
-```sh
-sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
-sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
-```
diff --git a/book/src/keymap.md b/book/src/keymap.md
index 73f7a8532..9a6fecbab 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -1,4 +1,4 @@
-# Keymap
+## Keymap
- [Normal mode](#normal-mode)
- [Movement](#movement)
@@ -12,8 +12,9 @@ # Keymap
- [Match mode](#match-mode)
- [Window mode](#window-mode)
- [Space mode](#space-mode)
- - [Comment mode](#comment-mode)
- [Popup](#popup)
+ - [Completion Menu](#completion-menu)
+ - [Signature-help Popup](#signature-help-popup)
- [Unimpaired](#unimpaired)
- [Insert mode](#insert-mode)
- [Select / extend mode](#select--extend-mode)
@@ -24,6 +25,8 @@ # Keymap
> ๐ก Mappings marked (**TS**) require a tree-sitter grammar for the file type.
+> โ ๏ธ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts.
+
## Normal mode
Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
@@ -307,13 +310,31 @@ #### Space mode
##### Popup
-Displays documentation for item under cursor.
+Displays documentation for item under cursor. Remapping currently not supported.
| Key | Description |
| ---- | ----------- |
| `Ctrl-u` | Scroll up |
| `Ctrl-d` | Scroll down |
+##### Completion Menu
+
+Displays documentation for the selected completion item. Remapping currently not supported.
+
+| Key | Description |
+| ---- | ----------- |
+| `Shift-Tab`, `Ctrl-p`, `Up` | Previous entry |
+| `Tab`, `Ctrl-n`, `Down` | Next entry |
+
+##### Signature-help Popup
+
+Displays the signature of the selected completion item. Remapping currently not supported.
+
+| Key | Description |
+| ---- | ----------- |
+| `Alt-p` | Previous signature |
+| `Alt-n` | Next signature |
+
#### Unimpaired
These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
diff --git a/book/src/lang-support.md b/book/src/lang-support.md
index 3f96673bc..aede33fa0 100644
--- a/book/src/lang-support.md
+++ b/book/src/lang-support.md
@@ -1,7 +1,7 @@
# Language Support
The following languages and Language Servers are supported. To use
-Language Server features, you must first [install][lsp-install-wiki] the
+Language Server features, you must first [configure][lsp-config-wiki] the
appropriate Language Server.
You can check the language support in your installed helix version with `hx --health`.
@@ -11,6 +11,6 @@ # Language Support
{{#include ./generated/lang-support.md}}
-[lsp-install-wiki]: https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers
+[lsp-config-wiki]: https://github.com/helix-editor/helix/wiki/Language-Server-Configurations
[lang-config]: ./languages.md
[adding-languages]: ./guides/adding_languages.md
diff --git a/book/src/languages.md b/book/src/languages.md
index dd93fec53..33ecbb92f 100644
--- a/book/src/languages.md
+++ b/book/src/languages.md
@@ -150,6 +150,8 @@ ### Configuring Language Servers for a language
Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
+The definition order of language servers affects the order in the results list of code action menu.
+
In case multiple language servers are specified in the `language-servers` attribute of a `language`,
it's often useful to only enable/disable certain language-server features for these language servers.
diff --git a/book/src/package-managers.md b/book/src/package-managers.md
new file mode 100644
index 000000000..99d2a3500
--- /dev/null
+++ b/book/src/package-managers.md
@@ -0,0 +1,150 @@
+## Package managers
+
+- [Linux](#linux)
+ - [Ubuntu](#ubuntu)
+ - [Fedora/RHEL](#fedorarhel)
+ - [Arch Linux extra](#arch-linux-extra)
+ - [NixOS](#nixos)
+ - [Flatpak](#flatpak)
+ - [Snap](#snap)
+ - [AppImage](#appimage)
+- [macOS](#macos)
+ - [Homebrew Core](#homebrew-core)
+ - [MacPorts](#macports)
+- [Windows](#windows)
+ - [Winget](#winget)
+ - [Scoop](#scoop)
+ - [Chocolatey](#chocolatey)
+ - [MSYS2](#msys2)
+
+[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
+
+## Linux
+
+The following third party repositories are available:
+
+### Ubuntu
+
+Add the `PPA` for Helix:
+
+```sh
+sudo add-apt-repository ppa:maveonair/helix-editor
+sudo apt update
+sudo apt install helix
+```
+
+### Fedora/RHEL
+
+```sh
+sudo dnf install helix
+```
+
+### Arch Linux extra
+
+Releases are available in the `extra` repository:
+
+```sh
+sudo pacman -S helix
+```
+
+> ๐ก When installed from the `extra` repository, run Helix with `helix` instead of `hx`.
+>
+> For example:
+> ```sh
+> helix --health
+> ```
+> to check health
+
+Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
+in the AUR, which builds the master branch.
+
+### NixOS
+
+Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute,
+the unstable channel usually carries the latest release.
+
+Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project
+root. Use `nix develop` to spin up a reproducible development shell. Outputs are
+cached for each push to master using [Cachix](https://www.cachix.org/). The
+flake is configured to automatically make use of this cache assuming the user
+accepts the new settings on first use.
+
+If you are using a version of Nix without flakes enabled,
+[install Cachix CLI](https://docs.cachix.org/installation) and use
+`cachix use helix` to configure Nix to use cached outputs when possible.
+
+### Flatpak
+
+Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix):
+
+```sh
+flatpak install flathub com.helix_editor.Helix
+flatpak run com.helix_editor.Helix
+```
+
+### Snap
+
+Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with:
+
+```sh
+snap install --classic helix
+```
+
+This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`.
+
+### AppImage
+
+Install Helix using the Linux [AppImage](https://appimage.org/) format.
+Download the official Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page.
+
+```sh
+chmod +x helix-*.AppImage # change permission for executable mode
+./helix-*.AppImage # run helix
+```
+
+## macOS
+
+### Homebrew Core
+
+```sh
+brew install helix
+```
+
+### MacPorts
+
+```sh
+port install helix
+```
+
+## Windows
+
+Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
+or [MSYS2](https://msys2.org/).
+
+### Winget
+Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer.
+You can get [App Installer from the Microsoft Store](https://www.microsoft.com/p/app-installer/9nblggh4nns1#activetab=pivot:overviewtab). If it's already installed, make sure it is updated with the latest version.
+
+```sh
+winget install Helix.Helix
+```
+
+### Scoop
+
+```sh
+scoop install helix
+```
+
+### Chocolatey
+
+```sh
+choco install helix
+```
+
+### MSYS2
+
+For 64-bit Windows 8.1 or above:
+
+```sh
+pacman -S mingw-w64-ucrt-x86_64-helix
+```
diff --git a/book/src/registers.md b/book/src/registers.md
new file mode 100644
index 000000000..2edf7e27a
--- /dev/null
+++ b/book/src/registers.md
@@ -0,0 +1,54 @@
+## Registers
+
+- [User-defined registers](#user-defined-registers)
+- [Default registers](#default-registers)
+- [Special registers](#special-registers)
+
+In Helix, registers are storage locations for text and other data, such as the
+result of a search. Registers can be used to cut, copy, and paste text, similar
+to the clipboard in other text editors. Usage is similar to Vim, with `"` being
+used to select a register.
+
+### User-defined registers
+
+Helix allows you to create your own named registers for storing text, for
+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:
+
+- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
+- `"md` - Store the selection in register `m` and delete it.
+
+### Default registers
+
+Commands that use registers, like yank (`y`), use a default register if none is specified.
+These registers are used as defaults:
+
+| Register character | Contains |
+| --- | --- |
+| `/` | Last search |
+| `:` | Last executed command |
+| `"` | Last yanked text |
+| `@` | Last recorded macro |
+
+### Special registers
+
+Some registers have special behavior when read from and written to.
+
+| Register character | When read | When written |
+| --- | --- | --- |
+| `_` | No values are returned | All values are discarded |
+| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable |
+| `.` | Contents of the current selections | This register is not writable |
+| `%` | Name of the current file | This register is not writable |
+| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard |
+| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard |
+
+When yanking multiple selections to the clipboard registers, the selections
+are joined with newlines. Pasting from these registers will paste multiple
+selections if the clipboard was last yanked to by the Helix session. Otherwise
+the clipboard contents are pasted as one selection.
+
diff --git a/book/src/surround.md b/book/src/surround.md
new file mode 100644
index 000000000..187d2876a
--- /dev/null
+++ b/book/src/surround.md
@@ -0,0 +1,24 @@
+## Surround
+
+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)
+
+| Key Sequence | Action |
+| --------------------------------- | --------------------------------------- |
+| `ms` (after selecting text) | Add surround characters to selection |
+| `mr` | Replace the closest surround characters |
+| `md` | Delete the closest surround characters |
+
+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]`:
+
+1. `%` to select the whole file
+2. `s` to split the selections on a search term
+3. Input `use` and hit Enter
+4. `mr([` to replace the parentheses with square brackets
+
+Multiple characters are currently not supported, but planned for future release.
+
diff --git a/book/src/syntax-aware-motions.md b/book/src/syntax-aware-motions.md
new file mode 100644
index 000000000..aab52b1b4
--- /dev/null
+++ b/book/src/syntax-aware-motions.md
@@ -0,0 +1,68 @@
+## Moving the selection with syntax-aware motions
+
+`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the
+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);
+```
+
+A function call might be parsed by tree-sitter into a tree like the following.
+
+```tsq
+(call
+ function: (identifier) ; func
+ arguments:
+ (arguments ; (arg1, arg2, arg3)
+ (identifier) ; arg1
+ (identifier) ; arg2
+ (identifier))) ; arg3
+```
+
+Use `:tree-sitter-subtree` to view the syntax tree of the primary selection. In
+a more intuitive tree format:
+
+```
+ โโโโโโ
+ โcallโ
+ โโโโโโโดโโโโโดโโโโโโ
+ โ โ
+โโโโโโโผโโโโโ โโโโโโผโโโโโ
+โidentifierโ โargumentsโ
+โ "func" โ โโโโโโดโโโโฌโโโโโโดโโโโ
+โโโโโโโโโโโโ โ โ โ
+ โ โ โ
+ โโโโโโโโโโโผโ โโโโโโผโโโโโโ โโผโโโโโโโโโโ
+ โidentifierโ โidentifierโ โidentifierโ
+ โ "arg1" โ โ "arg2" โ โ "arg3" โ
+ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
+```
+
+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
+// 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
+arguments node.
+
+```js
+func[(arg1, arg2, arg3)];
+```
+
+There is also some nuanced behavior that prevents you from getting stuck on a
+node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous
+child node will be selected. In the event that `arg1` does not have a previous
+sibling, the selection will move up the syntax tree and select the previous
+element. As a result, using `Alt-p` with a selection on `arg1` will move the
+selection to the "func" `identifier`.
+
+[lang-support]: ./lang-support.md
+[unimpaired-keybinds]: ./keymap.md#unimpaired
+[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif
diff --git a/book/src/textobjects.md b/book/src/textobjects.md
new file mode 100644
index 000000000..92ca5645b
--- /dev/null
+++ b/book/src/textobjects.md
@@ -0,0 +1,47 @@
+## Selecting and manipulating text with textobjects
+
+In Helix, textobjects are a way to select, manipulate and operate on a piece of
+text in a structured way. They allow you to refer to blocks of text based on
+their structure or purpose, such as a word, sentence, paragraph, or even a
+function or block of code.
+
+![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
+![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
+
+- `ma` - Select around the object (`va` in Vim, `` in Kakoune)
+- `mi` - Select inside the object (`vi` in Vim, `` in Kakoune)
+
+| Key after `mi` or `ma` | Textobject selected |
+| --- | --- |
+| `w` | Word |
+| `W` | WORD |
+| `p` | Paragraph |
+| `(`, `[`, `'`, etc. | Specified surround pairs |
+| `m` | The closest surround pair |
+| `f` | Function |
+| `t` | Type (or Class) |
+| `a` | Argument/parameter |
+| `c` | Comment |
+| `T` | Test |
+| `g` | Change |
+
+> ๐ก `f`, `t`, 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 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
+type use `[t`, 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.
+
+> ๐ก This feature relies on tree-sitter textobjects
+> and requires the corresponding query file to work properly.
+
diff --git a/book/src/themes.md b/book/src/themes.md
index 0a49053f5..e3b95c0a7 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -36,13 +36,6 @@ ### Overview
user-submitted themes
[here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
-### Using the linter
-
-Use the supplied linting tool to check for errors and missing scopes:
-
-```sh
-cargo xtask themelint onedark # replace onedark with
-```
## The details of theme creation
@@ -186,6 +179,7 @@ #### Syntax highlighting
- `parameter` - Function parameters
- `other`
- `member` - Fields of composite data types (e.g. structs, unions)
+ - `private` - Private fields that use a unique syntax (currently just ECMAScript-based languages)
- `label`
@@ -213,6 +207,7 @@ #### Syntax highlighting
- `function`
- `builtin`
- `method`
+ - `private` - Private methods that use a unique syntax (currently just ECMAScript-based languages)
- `macro`
- `special` (preprocessor in C)
diff --git a/book/src/usage.md b/book/src/usage.md
index e01482193..859cb6709 100644
--- a/book/src/usage.md
+++ b/book/src/usage.md
@@ -1,15 +1,5 @@
# Using Helix
-
-- [Registers](#registers)
- - [User-defined registers](#user-defined-registers)
- - [Special registers](#special-registers)
-- [Surround](#surround)
-- [Selecting and manipulating text with textobjects](#selecting-and-manipulating-text-with-textobjects)
-- [Navigating using tree-sitter textobjects](#navigating-using-tree-sitter-textobjects)
-- [Moving the selection with syntax-aware motions](#moving-the-selection-with-syntax-aware-motions)
-
-
For a full interactive introduction to Helix, refer to the
[tutor](https://github.com/helix-editor/helix/blob/master/runtime/tutor) which
can be accessed via the command `hx --tutor` or `:tutor`.
@@ -17,192 +7,3 @@ # Using Helix
> ๐ก Currently, not all functionality is fully documented, please refer to the
> [key mappings](./keymap.md) list.
-## Registers
-
-In Helix, registers are storage locations for text and other data, such as the
-result of a search. Registers can be used to cut, copy, and paste text, similar
-to the clipboard in other text editors. Usage is similar to Vim, with `"` being
-used to select a register.
-
-### User-defined registers
-
-Helix allows you to create your own named registers for storing text, for
-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:
-
-- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
-- `"md` - Store the selection in register `m` and delete it.
-
-### Default registers
-
-Commands that use registers, like yank (`y`), use a default register if none is specified.
-These registers are used as defaults:
-
-| Register character | Contains |
-| --- | --- |
-| `/` | Last search |
-| `:` | Last executed command |
-| `"` | Last yanked text |
-| `@` | Last recorded macro |
-
-### Special registers
-
-Some registers have special behavior when read from and written to.
-
-| Register character | When read | When written |
-| --- | --- | --- |
-| `_` | No values are returned | All values are discarded |
-| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable |
-| `.` | Contents of the current selections | This register is not writable |
-| `%` | Name of the current file | This register is not writable |
-| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard |
-| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard |
-
-When yanking multiple selections to the clipboard registers, the selections
-are joined with newlines. Pasting from these registers will paste multiple
-selections if the clipboard was last yanked to by the Helix session. Otherwise
-the clipboard contents are pasted as one selection.
-
-## Surround
-
-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)
-
-| Key Sequence | Action |
-| --------------------------------- | --------------------------------------- |
-| `ms` (after selecting text) | Add surround characters to selection |
-| `mr` | Replace the closest surround characters |
-| `md` | Delete the closest surround characters |
-
-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]`:
-
-1. `%` to select the whole file
-2. `s` to split the selections on a search term
-3. Input `use` and hit Enter
-4. `mr([` to replace the parentheses with square brackets
-
-Multiple characters are currently not supported, but planned for future release.
-
-## Selecting and manipulating text with textobjects
-
-In Helix, textobjects are a way to select, manipulate and operate on a piece of
-text in a structured way. They allow you to refer to blocks of text based on
-their structure or purpose, such as a word, sentence, paragraph, or even a
-function or block of code.
-
-![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
-![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
-
-- `ma` - Select around the object (`va` in Vim, `` in Kakoune)
-- `mi` - Select inside the object (`vi` in Vim, `` in Kakoune)
-
-| Key after `mi` or `ma` | Textobject selected |
-| --- | --- |
-| `w` | Word |
-| `W` | WORD |
-| `p` | Paragraph |
-| `(`, `[`, `'`, etc. | Specified surround pairs |
-| `m` | The closest surround pair |
-| `f` | Function |
-| `t` | Type (or Class) |
-| `a` | Argument/parameter |
-| `c` | Comment |
-| `T` | Test |
-| `g` | Change |
-
-> ๐ก `f`, `t`, 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 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
-type use `[t`, 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.
-
-> ๐ก This feature relies on tree-sitter textobjects
-> and requires the corresponding query file to work properly.
-
-## Moving the selection with syntax-aware motions
-
-`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the
-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);
-```
-
-A function call might be parsed by tree-sitter into a tree like the following.
-
-```tsq
-(call
- function: (identifier) ; func
- arguments:
- (arguments ; (arg1, arg2, arg3)
- (identifier) ; arg1
- (identifier) ; arg2
- (identifier))) ; arg3
-```
-
-Use `:tree-sitter-subtree` to view the syntax tree of the primary selection. In
-a more intuitive tree format:
-
-```
- โโโโโโ
- โcallโ
- โโโโโโโดโโโโโดโโโโโโ
- โ โ
-โโโโโโโผโโโโโ โโโโโโผโโโโโ
-โidentifierโ โargumentsโ
-โ "func" โ โโโโโโดโโโโฌโโโโโโดโโโโ
-โโโโโโโโโโโโ โ โ โ
- โ โ โ
- โโโโโโโโโโโผโ โโโโโโผโโโโโโ โโผโโโโโโโโโโ
- โidentifierโ โidentifierโ โidentifierโ
- โ "arg1" โ โ "arg2" โ โ "arg3" โ
- โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
-```
-
-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
-// 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
-arguments node.
-
-```js
-func[(arg1, arg2, arg3)];
-```
-
-There is also some nuanced behavior that prevents you from getting stuck on a
-node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous
-child node will be selected. In the event that `arg1` does not have a previous
-sibling, the selection will move up the syntax tree and select the previous
-element. As a result, using `Alt-p` with a selection on `arg1` will move the
-selection to the "func" `identifier`.
-
-[lang-support]: ./lang-support.md
-[unimpaired-keybinds]: ./keymap.md#unimpaired
-[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif
diff --git a/default.nix b/default.nix
index 50e7e6819..d2c51ec3a 100644
--- a/default.nix
+++ b/default.nix
@@ -5,4 +5,4 @@ let
sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7";
};
in
- (import compat {src = ./.;}).defaultNix.default
+ (import compat {src = ./.;}).defaultNix
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index 7482262eb..16d6b457f 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -25,8 +25,7 @@ smartstring = "1.0.1"
unicode-segmentation = "1.11"
unicode-width = "0.1"
unicode-general-category = "0.6"
-# slab = "0.4.2"
-slotmap = "1.0"
+slotmap.workspace = true
tree-sitter.workspace = true
once_cell = "1.19"
arc-swap = "1"
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index 52d77a9aa..d41119d38 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,4 +1,6 @@
//! LSP diagnostic utility types.
+use std::fmt;
+
use serde::{Deserialize, Serialize};
/// Describes the severity level of a [`Diagnostic`].
@@ -47,8 +49,25 @@ pub struct Diagnostic {
pub message: String,
pub severity: Option,
pub code: Option,
- pub language_server_id: usize,
+ pub provider: DiagnosticProvider,
pub tags: Vec,
pub source: Option,
pub data: Option,
}
+
+// TODO turn this into an enum + feature flag when lsp becomes optional
+pub type DiagnosticProvider = LanguageServerId;
+
+// while I would prefer having this in helix-lsp that necessitates a bunch of
+// conversions I would rather not add. I think its fine since this just a very
+// trivial newtype wrapper and we would need something similar once we define
+// completions in core
+slotmap::new_key_type! {
+ pub struct LanguageServerId;
+}
+
+impl fmt::Display for LanguageServerId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:?}", self.0)
+ }
+}
diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs
index 7cb5cd062..a02d6e4dd 100644
--- a/helix-core/src/graphemes.rs
+++ b/helix-core/src/graphemes.rs
@@ -278,23 +278,6 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
}
}
-/// Returns the passed byte index if it's already a grapheme boundary,
-/// or the next grapheme boundary byte index if not.
-#[must_use]
-#[inline]
-pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize {
- if byte_idx == 0 {
- byte_idx
- } else {
- // TODO: optimize so we're not constructing grapheme cursor twice
- if is_grapheme_boundary_byte(slice, byte_idx) {
- byte_idx
- } else {
- next_grapheme_boundary_byte(slice, byte_idx)
- }
- }
-}
-
/// Returns whether the given char position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 2a0a3876c..ca72fc3a2 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -247,41 +247,18 @@ fn add_indent_level(
}
}
-/// Computes for node and all ancestors whether they are the first node on their line.
-/// The first entry in the return value represents the root node, the last one the node itself
-fn get_first_in_line(mut node: Node, new_line_byte_pos: Option) -> Vec {
- let mut first_in_line = Vec::new();
- loop {
- if let Some(prev) = node.prev_sibling() {
- // If we insert a new line, the first node at/after the cursor is considered to be the first in its line
- let first = prev.end_position().row != node.start_position().row
- || new_line_byte_pos.map_or(false, |byte_pos| {
- node.start_byte() >= byte_pos && prev.start_byte() < byte_pos
- });
- first_in_line.push(Some(first));
- } else {
- // Nodes that have no previous siblings are first in their line if and only if their parent is
- // (which we don't know yet)
- first_in_line.push(None);
- }
- if let Some(parent) = node.parent() {
- node = parent;
- } else {
- break;
+/// Return true if only whitespace comes before the node on its line.
+/// If given, new_line_byte_pos is treated the same way as any existing newline.
+fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option) -> bool {
+ let mut line_start_byte_pos = text.line_to_byte(node.start_position().row);
+ if let Some(pos) = new_line_byte_pos {
+ if line_start_byte_pos < pos && pos <= node.start_byte() {
+ line_start_byte_pos = pos;
}
}
-
- let mut result = Vec::with_capacity(first_in_line.len());
- let mut parent_is_first = true; // The root node is by definition the first node in its line
- for first in first_in_line.into_iter().rev() {
- if let Some(first) = first {
- result.push(first);
- parent_is_first = first;
- } else {
- result.push(parent_is_first);
- }
- }
- result
+ text.byte_slice(line_start_byte_pos..node.start_byte())
+ .chars()
+ .all(|c| c.is_whitespace())
}
/// The total indent for some line of code.
@@ -852,7 +829,6 @@ pub fn treesitter_indent_for_pos<'a>(
byte_pos,
new_line_byte_pos,
)?;
- let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
let mut result = Indentation::default();
// We always keep track of all the indent changes on one line, in order to only indent once
@@ -861,9 +837,7 @@ pub fn treesitter_indent_for_pos<'a>(
let mut indent_for_line_below = Indentation::default();
loop {
- // This can safely be unwrapped because `first_in_line` contains
- // one entry for each ancestor of the node (which is what we iterate over)
- let is_first = *first_in_line.last().unwrap();
+ let is_first = is_first_in_line(node, text, new_line_byte_pos);
// Apply all indent definitions for this node.
// Since we only iterate over each node once, we can remove the
@@ -906,7 +880,6 @@ pub fn treesitter_indent_for_pos<'a>(
}
node = parent;
- first_in_line.pop();
} else {
// Only add the indentation for the line below if that line
// is not after the line that the indentation is calculated for.
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs
index b8bcc28ca..0e08c4898 100644
--- a/helix-core/src/match_brackets.rs
+++ b/helix-core/src/match_brackets.rs
@@ -9,16 +9,32 @@
const MAX_PLAINTEXT_SCAN: usize = 10000;
const MATCH_LIMIT: usize = 16;
-// Limit matching pairs to only ( ) { } [ ] < > ' ' " "
-const PAIRS: &[(char, char)] = &[
+pub const BRACKETS: [(char, char); 7] = [
('(', ')'),
('{', '}'),
('[', ']'),
('<', '>'),
- ('\'', '\''),
- ('\"', '\"'),
+ ('ยซ', 'ยป'),
+ ('ใ', 'ใ'),
+ ('๏ผ', '๏ผ'),
];
+// The difference between BRACKETS and PAIRS is that we can find matching
+// BRACKETS in a plain text file, but we can't do the same for PAIRs.
+// PAIRS also contains all BRACKETS.
+pub const PAIRS: [(char, char); BRACKETS.len() + 3] = {
+ let mut pairs = [(' ', ' '); BRACKETS.len() + 3];
+ let mut idx = 0;
+ while idx < BRACKETS.len() {
+ pairs[idx] = BRACKETS[idx];
+ idx += 1;
+ }
+ pairs[idx] = ('"', '"');
+ pairs[idx + 1] = ('\'', '\'');
+ pairs[idx + 2] = ('`', '`');
+ pairs
+};
+
/// Returns the position of the matching bracket under cursor.
///
/// If the cursor is on the opening bracket, the position of
@@ -30,7 +46,7 @@
/// If no matching bracket is found, `None` is returned.
#[must_use]
pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option {
- if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
+ if pos >= doc.len_chars() || !is_valid_pair(doc.char(pos)) {
return None;
}
find_pair(syntax, doc, pos, false)
@@ -59,60 +75,64 @@ fn find_pair(
) -> Option {
let pos = doc.char_to_byte(pos_);
- let root = syntax.tree_for_byte_range(pos, pos + 1).root_node();
- let mut node = root.descendant_for_byte_range(pos, pos + 1)?;
+ let root = syntax.tree_for_byte_range(pos, pos).root_node();
+ let mut node = root.descendant_for_byte_range(pos, pos)?;
loop {
- if node.is_named() {
- let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
- let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
+ if node.is_named() && node.child_count() >= 2 {
+ let open = node.child(0).unwrap();
+ let close = node.child(node.child_count() - 1).unwrap();
- if is_valid_pair(doc, start_char, end_char) {
- if end_byte == pos {
- return Some(start_char);
- }
+ if let (Some((start_pos, open)), Some((end_pos, close))) =
+ (as_char(doc, &open), as_char(doc, &close))
+ {
+ if PAIRS.contains(&(open, close)) {
+ if start_pos == pos_ {
+ return Some(start_pos);
+ }
- // We return the end char if the cursor is either on the start char
- // or at some arbitrary position between start and end char.
- if traverse_parents || start_byte == pos {
- return Some(end_char);
+ // We return the end char if the cursor is either on the start char
+ // or at some arbitrary position between start and end char.
+ if traverse_parents || end_pos == pos_ {
+ return Some(end_pos);
+ }
}
}
}
// this node itselt wasn't a pair but maybe its siblings are
- // check if we are *on* the pair (special cased so we don't look
- // at the current node twice and to jump to the start on that case)
- if let Some(open) = as_close_pair(doc, &node) {
- if let Some(pair_start) = find_pair_end(doc, node.prev_sibling(), open, Backward) {
+ if let Some((start_char, end_char)) = as_close_pair(doc, &node) {
+ if let Some(pair_start) =
+ find_pair_end(doc, node.prev_sibling(), start_char, end_char, Backward)
+ {
return Some(pair_start);
}
}
+ if let Some((start_char, end_char)) = as_open_pair(doc, &node) {
+ if let Some(pair_end) =
+ find_pair_end(doc, node.next_sibling(), start_char, end_char, Forward)
+ {
+ return Some(pair_end);
+ }
+ }
- if !traverse_parents {
- // check if we are *on* the opening pair (special cased here as
- // an opptimization since we only care about bracket on the cursor
- // here)
- if let Some(close) = as_open_pair(doc, &node) {
- if let Some(pair_end) = find_pair_end(doc, node.next_sibling(), close, Forward) {
- return Some(pair_end);
+ if traverse_parents {
+ for sibling in
+ iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
+ {
+ let Some((start_char, end_char)) = as_close_pair(doc, &sibling) else {
+ continue;
+ };
+ if find_pair_end(doc, sibling.prev_sibling(), start_char, end_char, Backward)
+ .is_some()
+ {
+ return doc.try_byte_to_char(sibling.start_byte()).ok();
}
}
- if node.is_named() {
- break;
- }
+ } else if node.is_named() {
+ break;
}
- for close in
- iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
- {
- let Some(open) = as_close_pair(doc, &close) else {
- continue;
- };
- if find_pair_end(doc, Some(node), open, Backward).is_some() {
- return doc.try_byte_to_char(close.start_byte()).ok();
- }
- }
let Some(parent) = node.parent() else {
break;
};
@@ -140,14 +160,22 @@ fn find_pair(
/// If no matching bracket is found, `None` is returned.
#[must_use]
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option {
- // Don't do anything when the cursor is not on top of a bracket.
let bracket = doc.get_char(cursor_pos)?;
+ let matching_bracket = {
+ let pair = get_pair(bracket);
+ if pair.0 == bracket {
+ pair.1
+ } else {
+ pair.0
+ }
+ };
+ // Don't do anything when the cursor is not on top of a bracket.
if !is_valid_bracket(bracket) {
return None;
}
// Determine the direction of the matching.
- let is_fwd = is_forward_bracket(bracket);
+ let is_fwd = is_open_bracket(bracket);
let chars_iter = if is_fwd {
doc.chars_at(cursor_pos + 1)
} else {
@@ -159,19 +187,7 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() {
if candidate == bracket {
open_cnt += 1;
- } else if is_valid_pair(
- doc,
- if is_fwd {
- cursor_pos
- } else {
- cursor_pos - i - 1
- },
- if is_fwd {
- cursor_pos + i + 1
- } else {
- cursor_pos
- },
- ) {
+ } else if candidate == matching_bracket {
// Return when all pending brackets have been closed.
if open_cnt == 1 {
return Some(if is_fwd {
@@ -187,37 +203,55 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
None
}
-fn is_valid_bracket(c: char) -> bool {
- PAIRS.iter().any(|(l, r)| *l == c || *r == c)
+/// Returns the open and closing chars pair. If not found in
+/// [`BRACKETS`] returns (ch, ch).
+///
+/// ```
+/// use helix_core::match_brackets::get_pair;
+///
+/// assert_eq!(get_pair('['), ('[', ']'));
+/// assert_eq!(get_pair('}'), ('{', '}'));
+/// assert_eq!(get_pair('"'), ('"', '"'));
+/// ```
+pub fn get_pair(ch: char) -> (char, char) {
+ PAIRS
+ .iter()
+ .find(|(open, close)| *open == ch || *close == ch)
+ .copied()
+ .unwrap_or((ch, ch))
}
-fn is_forward_bracket(c: char) -> bool {
- PAIRS.iter().any(|(l, _)| *l == c)
+pub fn is_open_bracket(ch: char) -> bool {
+ BRACKETS.iter().any(|(l, _)| *l == ch)
}
-fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool {
- PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
+pub fn is_close_bracket(ch: char) -> bool {
+ BRACKETS.iter().any(|(_, r)| *r == ch)
}
-fn surrounding_bytes(doc: RopeSlice, node: &Node) -> Option<(usize, usize)> {
- let len = doc.len_bytes();
+pub fn is_valid_bracket(ch: char) -> bool {
+ BRACKETS.iter().any(|(l, r)| *l == ch || *r == ch)
+}
- let start_byte = node.start_byte();
- let end_byte = node.end_byte().saturating_sub(1);
+pub fn is_open_pair(ch: char) -> bool {
+ PAIRS.iter().any(|(l, _)| *l == ch)
+}
- if start_byte >= len || end_byte >= len {
- return None;
- }
+pub fn is_close_pair(ch: char) -> bool {
+ PAIRS.iter().any(|(_, r)| *r == ch)
+}
- Some((start_byte, end_byte))
+pub fn is_valid_pair(ch: char) -> bool {
+ PAIRS.iter().any(|(l, r)| *l == ch || *r == ch)
}
/// Tests if this node is a pair close char and returns the expected open char
-fn as_close_pair(doc: RopeSlice, node: &Node) -> Option {
+/// and close char contained in this node
+fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<(char, char)> {
let close = as_char(doc, node)?.1;
PAIRS
.iter()
- .find_map(|&(open, close_)| (close_ == close).then_some(open))
+ .find_map(|&(open, close_)| (close_ == close).then_some((close, open)))
}
/// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char
@@ -228,6 +262,7 @@ fn as_close_pair(doc: RopeSlice, node: &Node) -> Option {
fn find_pair_end(
doc: RopeSlice,
node: Option,
+ start_char: char,
end_char: char,
direction: Direction,
) -> Option {
@@ -235,20 +270,30 @@ fn find_pair_end(
Forward => Node::next_sibling,
Backward => Node::prev_sibling,
};
+ let mut depth = 0;
iter::successors(node, advance)
.take(MATCH_LIMIT)
.find_map(|node| {
let (pos, c) = as_char(doc, &node)?;
- (end_char == c).then_some(pos)
+ if c == end_char {
+ if depth == 0 {
+ return Some(pos);
+ }
+ depth -= 1;
+ } else if c == start_char {
+ depth += 1;
+ }
+ None
})
}
-/// Tests if this node is a pair close char and returns the expected open char
-fn as_open_pair(doc: RopeSlice, node: &Node) -> Option {
+/// Tests if this node is a pair open char and returns the expected close char
+/// and open char contained in this node
+fn as_open_pair(doc: RopeSlice, node: &Node) -> Option<(char, char)> {
let open = as_char(doc, node)?.1;
PAIRS
.iter()
- .find_map(|&(open_, close)| (open_ == open).then_some(close))
+ .find_map(|&(open_, close)| (open_ == open).then_some((open, close)))
}
/// If node is a single char return it (and its char position)
diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs
index 0df105f1a..17a393caf 100644
--- a/helix-core/src/object.rs
+++ b/helix-core/src/object.rs
@@ -1,4 +1,4 @@
-use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
+use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
let cursor = &mut syntax.walk();
@@ -25,36 +25,95 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection)
}
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
- select_node_impl(syntax, text, selection, |cursor| {
- cursor.goto_first_child();
- })
+ select_node_impl(
+ syntax,
+ text,
+ selection,
+ |cursor| {
+ cursor.goto_first_child();
+ },
+ None,
+ )
}
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
- select_node_impl(syntax, text, selection, |cursor| {
- while !cursor.goto_next_sibling() {
- if !cursor.goto_parent() {
- break;
+ select_node_impl(
+ syntax,
+ text,
+ selection,
+ |cursor| {
+ while !cursor.goto_next_sibling() {
+ if !cursor.goto_parent() {
+ break;
+ }
}
+ },
+ Some(Direction::Forward),
+ )
+}
+
+pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+ selection.transform_iter(|range| {
+ let mut cursor = syntax.walk();
+ let (from, to) = range.into_byte_range(text);
+ cursor.reset_to_byte_range(from, to);
+
+ if !cursor.goto_parent_with(|parent| parent.child_count() > 1) {
+ return vec![range].into_iter();
}
+
+ select_children(&mut cursor, text, range).into_iter()
})
}
-pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
- select_node_impl(syntax, text, selection, |cursor| {
- while !cursor.goto_prev_sibling() {
- if !cursor.goto_parent() {
- break;
- }
- }
+pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+ selection.transform_iter(|range| {
+ let mut cursor = syntax.walk();
+ let (from, to) = range.into_byte_range(text);
+ cursor.reset_to_byte_range(from, to);
+ select_children(&mut cursor, text, range).into_iter()
})
}
+fn select_children<'n>(
+ cursor: &'n mut TreeCursor<'n>,
+ text: RopeSlice,
+ range: Range,
+) -> Vec {
+ let children = cursor
+ .named_children()
+ .map(|child| Range::from_node(child, text, range.direction()))
+ .collect::>();
+
+ if !children.is_empty() {
+ children
+ } else {
+ vec![range]
+ }
+}
+
+pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+ select_node_impl(
+ syntax,
+ text,
+ selection,
+ |cursor| {
+ while !cursor.goto_prev_sibling() {
+ if !cursor.goto_parent() {
+ break;
+ }
+ }
+ },
+ Some(Direction::Backward),
+ )
+}
+
fn select_node_impl(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
motion: F,
+ direction: Option,
) -> Selection
where
F: Fn(&mut TreeCursor),
@@ -73,6 +132,6 @@ fn select_node_impl(
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
- Range::new(from, to).with_direction(range.direction())
+ Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
})
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 579499de5..48eaf289c 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -14,6 +14,7 @@
use helix_stdx::rope::{self, RopeSliceExt};
use smallvec::{smallvec, SmallVec};
use std::borrow::Cow;
+use tree_sitter::Node;
/// A single selection range.
///
@@ -73,6 +74,12 @@ pub fn point(head: usize) -> Self {
Self::new(head, head)
}
+ pub fn from_node(node: Node, text: RopeSlice, direction: Direction) -> Self {
+ let from = text.byte_to_char(node.start_byte());
+ let to = text.byte_to_char(node.end_byte());
+ Range::new(from, to).with_direction(direction)
+ }
+
/// Start of the range.
#[inline]
#[must_use]
@@ -115,7 +122,7 @@ pub fn is_empty(&self) -> bool {
}
/// `Direction::Backward` when head < anchor.
- /// `Direction::Backward` otherwise.
+ /// `Direction::Forward` otherwise.
#[inline]
#[must_use]
pub fn direction(&self) -> Direction {
@@ -376,6 +383,12 @@ pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool {
let second = graphemes.next();
first.is_some() && second.is_none()
}
+
+ /// Converts this char range into an in order byte range, discarding
+ /// direction.
+ pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) {
+ (text.char_to_byte(self.from()), text.char_to_byte(self.to()))
+ }
}
impl From<(usize, usize)> for Range {
@@ -783,7 +796,9 @@ pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection {
let mut start = sel_start;
for line in sel.slice(text).lines() {
- let Some(line_ending) = get_line_ending(&line) else { break };
+ let Some(line_ending) = get_line_ending(&line) else {
+ break;
+ };
let line_end = start + line.len_chars();
// TODO: retain range direction
result.push(Range::new(start, line_end - line_ending.len_chars()));
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index ed9764883..e45346c92 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -1,18 +1,16 @@
use std::fmt::Display;
-use crate::{movement::Direction, search, Range, Selection};
+use crate::{
+ graphemes::next_grapheme_boundary,
+ match_brackets::{
+ find_matching_bracket, find_matching_bracket_fuzzy, get_pair, is_close_bracket,
+ is_open_bracket,
+ },
+ movement::Direction,
+ search, Range, Selection, Syntax,
+};
use ropey::RopeSlice;
-pub const PAIRS: &[(char, char)] = &[
- ('(', ')'),
- ('[', ']'),
- ('{', '}'),
- ('<', '>'),
- ('ยซ', 'ยป'),
- ('ใ', 'ใ'),
- ('๏ผ', '๏ผ'),
-];
-
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
PairNotFound,
@@ -34,32 +32,68 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Result = std::result::Result;
-/// Given any char in [PAIRS], return the open and closing chars. If not found in
-/// [PAIRS] return (ch, ch).
+/// Finds the position of surround pairs of any [`crate::match_brackets::PAIRS`]
+/// using tree-sitter when possible.
///
-/// ```
-/// use helix_core::surround::get_pair;
+/// # Returns
///
-/// assert_eq!(get_pair('['), ('[', ']'));
-/// assert_eq!(get_pair('}'), ('{', '}'));
-/// assert_eq!(get_pair('"'), ('"', '"'));
-/// ```
-pub fn get_pair(ch: char) -> (char, char) {
- PAIRS
- .iter()
- .find(|(open, close)| *open == ch || *close == ch)
- .copied()
- .unwrap_or((ch, ch))
+/// Tuple `(anchor, head)`, meaning it is not always ordered.
+pub fn find_nth_closest_pairs_pos(
+ syntax: Option<&Syntax>,
+ text: RopeSlice,
+ range: Range,
+ skip: usize,
+) -> Result<(usize, usize)> {
+ match syntax {
+ Some(syntax) => find_nth_closest_pairs_ts(syntax, text, range, skip),
+ None => find_nth_closest_pairs_plain(text, range, skip),
+ }
}
-pub fn find_nth_closest_pairs_pos(
+fn find_nth_closest_pairs_ts(
+ syntax: &Syntax,
text: RopeSlice,
range: Range,
mut skip: usize,
) -> Result<(usize, usize)> {
- let is_open_pair = |ch| PAIRS.iter().any(|(open, _)| *open == ch);
- let is_close_pair = |ch| PAIRS.iter().any(|(_, close)| *close == ch);
+ let mut opening = range.from();
+ // We want to expand the selection if we are already on the found pair,
+ // otherwise we would need to subtract "-1" from "range.to()".
+ let mut closing = range.to();
+ while skip > 0 {
+ closing = find_matching_bracket_fuzzy(syntax, text, closing).ok_or(Error::PairNotFound)?;
+ opening = find_matching_bracket(syntax, text, closing).ok_or(Error::PairNotFound)?;
+ // If we're already on a closing bracket "find_matching_bracket_fuzzy" will return
+ // the position of the opening bracket.
+ if closing < opening {
+ (opening, closing) = (closing, opening);
+ }
+
+ // In case found brackets are partially inside current selection.
+ if range.from() < opening || closing < range.to() - 1 {
+ closing = next_grapheme_boundary(text, closing);
+ } else {
+ skip -= 1;
+ if skip != 0 {
+ closing = next_grapheme_boundary(text, closing);
+ }
+ }
+ }
+
+ // Keep the original direction.
+ if let Direction::Forward = range.direction() {
+ Ok((opening, closing))
+ } else {
+ Ok((closing, opening))
+ }
+}
+
+fn find_nth_closest_pairs_plain(
+ text: RopeSlice,
+ range: Range,
+ mut skip: usize,
+) -> Result<(usize, usize)> {
let mut stack = Vec::with_capacity(2);
let pos = range.from();
let mut close_pos = pos.saturating_sub(1);
@@ -67,7 +101,7 @@ pub fn find_nth_closest_pairs_pos(
for ch in text.chars_at(pos) {
close_pos += 1;
- if is_open_pair(ch) {
+ if is_open_bracket(ch) {
// Track open pairs encountered so that we can step over
// the corresponding close pairs that will come up further
// down the loop. We want to find a lone close pair whose
@@ -76,7 +110,7 @@ pub fn find_nth_closest_pairs_pos(
continue;
}
- if !is_close_pair(ch) {
+ if !is_close_bracket(ch) {
// We don't care if this character isn't a brace pair item,
// so short circuit here.
continue;
@@ -157,7 +191,11 @@ pub fn find_nth_pairs_pos(
)
};
- Option::zip(open, close).ok_or(Error::PairNotFound)
+ // preserve original direction
+ match range.direction() {
+ Direction::Forward => Option::zip(open, close).ok_or(Error::PairNotFound),
+ Direction::Backward => Option::zip(close, open).ok_or(Error::PairNotFound),
+ }
}
fn find_nth_open_pair(
@@ -249,6 +287,7 @@ fn find_nth_close_pair(
/// are automatically detected around each cursor (note that this may result
/// in them selecting different surround characters for each selection).
pub fn get_surround_pos(
+ syntax: Option<&Syntax>,
text: RopeSlice,
selection: &Selection,
ch: Option,
@@ -257,9 +296,13 @@ pub fn get_surround_pos(
let mut change_pos = Vec::new();
for &range in selection {
- let (open_pos, close_pos) = match ch {
- Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?,
- None => find_nth_closest_pairs_pos(text, range, skip)?,
+ let (open_pos, close_pos) = {
+ let range_raw = match ch {
+ Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?,
+ None => find_nth_closest_pairs_pos(syntax, text, range, skip)?,
+ };
+ let range = Range::new(range_raw.0, range_raw.1);
+ (range.from(), range.to())
};
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return Err(Error::CursorOverlap);
@@ -288,7 +331,7 @@ fn test_get_surround_pos() {
);
assert_eq!(
- get_surround_pos(doc.slice(..), &selection, Some('('), 1).unwrap(),
+ get_surround_pos(None, doc.slice(..), &selection, Some('('), 1).unwrap(),
expectations
);
}
@@ -303,7 +346,7 @@ fn test_get_surround_pos_bail_different_surround_chars() {
);
assert_eq!(
- get_surround_pos(doc.slice(..), &selection, Some('('), 1),
+ get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
Err(Error::PairNotFound)
);
}
@@ -318,7 +361,7 @@ fn test_get_surround_pos_bail_overlapping_surround_chars() {
);
assert_eq!(
- get_surround_pos(doc.slice(..), &selection, Some('('), 1),
+ get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
Err(Error::PairNotFound) // overlapping surround chars
);
}
@@ -333,7 +376,7 @@ fn test_get_surround_pos_bail_cursor_overlap() {
);
assert_eq!(
- get_surround_pos(doc.slice(..), &selection, Some('['), 1),
+ get_surround_pos(None, doc.slice(..), &selection, Some('['), 1),
Err(Error::CursorOverlap)
);
}
@@ -397,7 +440,7 @@ fn test_find_nth_closest_pairs_pos_index_range_panic() {
);
assert_eq!(
- find_nth_closest_pairs_pos(doc.slice(..), selection.primary(), 1),
+ find_nth_closest_pairs_pos(None, doc.slice(..), selection.primary(), 1),
Err(Error::PairNotFound)
)
}
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 78abc0b0a..bbea9bbe7 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -510,6 +510,7 @@ pub enum DebugArgumentValue {
pub struct DebugTemplate {
pub name: String,
pub request: String,
+ #[serde(default)]
pub completion: Vec,
pub args: HashMap,
}
@@ -2765,10 +2766,10 @@ fn test_textobject_queries() {
)
};
- test("quantified_nodes", 1..36);
+ test("quantified_nodes", 1..37);
// NOTE: Enable after implementing proper node group capturing
- // test("quantified_nodes_grouped", 1..36);
- // test("multiple_nodes_grouped", 1..36);
+ // test("quantified_nodes_grouped", 1..37);
+ // test("multiple_nodes_grouped", 1..37);
}
#[test]
@@ -2939,7 +2940,7 @@ fn assert_pretty_print(
#[test]
fn test_pretty_print() {
- let source = r#"/// Hello"#;
+ let source = r#"// Hello"#;
assert_pretty_print("rust", source, "(line_comment)", 0, source.len());
// A large tree should be indented with fields:
@@ -2958,7 +2959,8 @@ fn test_pretty_print() {
" (macro_invocation\n",
" macro: (identifier)\n",
" (token_tree\n",
- " (string_literal))))))",
+ " (string_literal\n",
+ " (string_content)))))))",
),
0,
source.len(),
diff --git a/helix-core/src/syntax/tree_cursor.rs b/helix-core/src/syntax/tree_cursor.rs
index d9d140c9f..692d5890a 100644
--- a/helix-core/src/syntax/tree_cursor.rs
+++ b/helix-core/src/syntax/tree_cursor.rs
@@ -90,6 +90,19 @@ pub fn goto_parent(&mut self) -> bool {
true
}
+ pub fn goto_parent_with
(&mut self, predicate: P) -> bool
+ where
+ P: Fn(&Node) -> bool,
+ {
+ while self.goto_parent() {
+ if predicate(&self.node()) {
+ return true;
+ }
+ }
+
+ false
+ }
+
/// Finds the injection layer that has exactly the same range as the given `range`.
fn layer_id_of_byte_range(&self, search_range: Range) -> Option {
let start_idx = self
@@ -102,7 +115,7 @@ fn layer_id_of_byte_range(&self, search_range: Range) -> Option
.find_map(|range| (range.start == search_range.start).then_some(range.layer_id))
}
- pub fn goto_first_child(&mut self) -> bool {
+ fn goto_first_child_impl(&mut self, named: bool) -> bool {
// Check if the current node's range is an exact injection layer range.
if let Some(layer_id) = self
.layer_id_of_byte_range(self.node().byte_range())
@@ -111,8 +124,16 @@ pub fn goto_first_child(&mut self) -> bool {
// Switch to the child layer.
self.current = layer_id;
self.cursor = self.layers[self.current].tree().root_node();
- true
- } else if let Some(child) = self.cursor.child(0) {
+ return true;
+ }
+
+ let child = if named {
+ self.cursor.named_child(0)
+ } else {
+ self.cursor.child(0)
+ };
+
+ if let Some(child) = child {
// Otherwise descend in the current tree.
self.cursor = child;
true
@@ -121,8 +142,45 @@ pub fn goto_first_child(&mut self) -> bool {
}
}
+ pub fn goto_first_child(&mut self) -> bool {
+ self.goto_first_child_impl(false)
+ }
+
+ pub fn goto_first_named_child(&mut self) -> bool {
+ self.goto_first_child_impl(true)
+ }
+
+ fn goto_next_sibling_impl(&mut self, named: bool) -> bool {
+ let sibling = if named {
+ self.cursor.next_named_sibling()
+ } else {
+ self.cursor.next_sibling()
+ };
+
+ if let Some(sibling) = sibling {
+ self.cursor = sibling;
+ true
+ } else {
+ false
+ }
+ }
+
pub fn goto_next_sibling(&mut self) -> bool {
- if let Some(sibling) = self.cursor.next_sibling() {
+ self.goto_next_sibling_impl(false)
+ }
+
+ pub fn goto_next_named_sibling(&mut self) -> bool {
+ self.goto_next_sibling_impl(true)
+ }
+
+ fn goto_prev_sibling_impl(&mut self, named: bool) -> bool {
+ let sibling = if named {
+ self.cursor.prev_named_sibling()
+ } else {
+ self.cursor.prev_sibling()
+ };
+
+ if let Some(sibling) = sibling {
self.cursor = sibling;
true
} else {
@@ -131,12 +189,11 @@ pub fn goto_next_sibling(&mut self) -> bool {
}
pub fn goto_prev_sibling(&mut self) -> bool {
- if let Some(sibling) = self.cursor.prev_sibling() {
- self.cursor = sibling;
- true
- } else {
- false
- }
+ self.goto_prev_sibling_impl(false)
+ }
+
+ pub fn goto_prev_named_sibling(&mut self) -> bool {
+ self.goto_prev_sibling_impl(true)
}
/// Finds the injection layer that contains the given start-end range.
@@ -157,4 +214,51 @@ pub fn reset_to_byte_range(&mut self, start: usize, end: usize) {
let root = self.layers[self.current].tree().root_node();
self.cursor = root.descendant_for_byte_range(start, end).unwrap_or(root);
}
+
+ /// Returns an iterator over the children of the node the TreeCursor is on
+ /// at the time this is called.
+ pub fn children(&'a mut self) -> ChildIter {
+ let parent = self.node();
+
+ ChildIter {
+ cursor: self,
+ parent,
+ named: false,
+ }
+ }
+
+ /// Returns an iterator over the named children of the node the TreeCursor is on
+ /// at the time this is called.
+ pub fn named_children(&'a mut self) -> ChildIter {
+ let parent = self.node();
+
+ ChildIter {
+ cursor: self,
+ parent,
+ named: true,
+ }
+ }
+}
+
+pub struct ChildIter<'n> {
+ cursor: &'n mut TreeCursor<'n>,
+ parent: Node<'n>,
+ named: bool,
+}
+
+impl<'n> Iterator for ChildIter<'n> {
+ type Item = Node<'n>;
+
+ fn next(&mut self) -> Option {
+ // first iteration, just visit the first child
+ if self.cursor.node() == self.parent {
+ self.cursor
+ .goto_first_child_impl(self.named)
+ .then(|| self.cursor.node())
+ } else {
+ self.cursor
+ .goto_next_sibling_impl(self.named)
+ .then(|| self.cursor.node())
+ }
+ }
}
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index bf00a4580..7576b3a78 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -7,9 +7,9 @@
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
use crate::line_ending::rope_is_line_ending;
use crate::movement::Direction;
-use crate::surround;
use crate::syntax::LanguageConfiguration;
use crate::Range;
+use crate::{surround, Syntax};
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
use CharCategory::{Eol, Whitespace};
@@ -199,25 +199,28 @@ pub fn textobject_paragraph(
}
pub fn textobject_pair_surround(
+ syntax: Option<&Syntax>,
slice: RopeSlice,
range: Range,
textobject: TextObject,
ch: char,
count: usize,
) -> Range {
- textobject_pair_surround_impl(slice, range, textobject, Some(ch), count)
+ textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count)
}
pub fn textobject_pair_surround_closest(
+ syntax: Option<&Syntax>,
slice: RopeSlice,
range: Range,
textobject: TextObject,
count: usize,
) -> Range {
- textobject_pair_surround_impl(slice, range, textobject, None, count)
+ textobject_pair_surround_impl(syntax, slice, range, textobject, None, count)
}
fn textobject_pair_surround_impl(
+ syntax: Option<&Syntax>,
slice: RopeSlice,
range: Range,
textobject: TextObject,
@@ -226,8 +229,7 @@ fn textobject_pair_surround_impl(
) -> Range {
let pair_pos = match ch {
Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count),
- // Automatically find the closest surround pairs
- None => surround::find_nth_closest_pairs_pos(slice, range, count),
+ None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count),
};
pair_pos
.map(|(anchor, head)| match textobject {
@@ -574,7 +576,8 @@ fn test_textobject_surround() {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range, ch, count) = case;
- let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count);
+ let result =
+ textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count);
assert_eq!(
result,
expected_range.into(),
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index 0b502f768..f87f15f21 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -31,3 +31,4 @@ tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "io-util", "io-
tokio-stream = "0.1.15"
parking_lot = "0.12.1"
arc-swap = "1"
+slotmap.workspace = true
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index b2290e031..254625a3a 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -2,7 +2,7 @@
file_operations::FileOperationsInterest,
find_lsp_workspace, jsonrpc,
transport::{Payload, Transport},
- Call, Error, OffsetEncoding, Result,
+ Call, Error, LanguageServerId, OffsetEncoding, Result,
};
use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope};
@@ -46,7 +46,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
#[derive(Debug)]
pub struct Client {
- id: usize,
+ id: LanguageServerId,
name: String,
_process: Child,
server_tx: UnboundedSender,
@@ -179,10 +179,14 @@ pub fn start(
server_environment: HashMap,
root_path: PathBuf,
root_uri: Option,
- id: usize,
+ id: LanguageServerId,
name: String,
req_timeout: u64,
- ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> {
+ ) -> Result<(
+ Self,
+ UnboundedReceiver<(LanguageServerId, Call)>,
+ Arc,
+ )> {
// Resolve path to the binary
let cmd = helix_stdx::env::which(cmd)?;
@@ -234,7 +238,7 @@ pub fn name(&self) -> &str {
&self.name
}
- pub fn id(&self) -> usize {
+ pub fn id(&self) -> LanguageServerId {
self.id
}
@@ -393,6 +397,16 @@ fn call(
&self,
params: R::Params,
) -> impl Future