Merge remote-tracking branch 'origin/master' into goto_next_reference

pull/6465/head
Anthony Templeton 1 year ago
commit 5732304519

@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install nix - name: Install nix
uses: cachix/install-nix-action@v21 uses: cachix/install-nix-action@v22
- name: Authenticate with Cachix - name: Authenticate with Cachix
uses: cachix/cachix-action@v12 uses: cachix/cachix-action@v12

@ -160,7 +160,7 @@ jobs:
- name: Build AppImage - name: Build AppImage
shell: bash shell: bash
if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux' if: matrix.build == 'x86_64-linux'
run: | run: |
# Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE # Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo add-apt-repository universe sudo add-apt-repository universe
@ -263,7 +263,7 @@ jobs:
mv bins-$platform/hx$exe $pkgname mv bins-$platform/hx$exe $pkgname
chmod +x $pkgname/hx$exe chmod +x $pkgname/hx$exe
if [[ "$platform" = "aarch64-linux" || "$platform" = "x86_64-linux" ]]; then if [[ "$platform" = "x86_64-linux" ]]; then
mv bins-$platform/helix-*.AppImage* dist/ mv bins-$platform/helix-*.AppImage* dist/
fi fi

@ -1,5 +1,2 @@
# Things that we don't want ripgrep to search that we do want in git # Things that we don't want ripgrep to search that we do want in git
# https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering # https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering
# Minified JS vendored from mdbook
book/theme/highlight.js

@ -1596,7 +1596,7 @@ to distinguish it in bug reports..
- The `runtime/` directory is now properly detected on binary releases and - The `runtime/` directory is now properly detected on binary releases and
on cargo run. `~/.config/helix/runtime` can also be used. on cargo run. `~/.config/helix/runtime` can also be used.
- Registers can now be selected via " (for example `"ay`) - Registers can now be selected via " (for example, `"ay`)
- Support for Nix files was added - Support for Nix files was added
- Movement is now fully tested and matches Kakoune implementation - Movement is now fully tested and matches Kakoune implementation
- A per-file LSP symbol picker was added to space+s - A per-file LSP symbol picker was added to space+s

693
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -5,6 +5,7 @@ members = [
"helix-term", "helix-term",
"helix-tui", "helix-tui",
"helix-lsp", "helix-lsp",
"helix-event",
"helix-dap", "helix-dap",
"helix-loader", "helix-loader",
"helix-vcs", "helix-vcs",
@ -32,3 +33,7 @@ inherits = "test"
package.helix-core.opt-level = 2 package.helix-core.opt-level = 2
package.helix-tui.opt-level = 2 package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2 package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
nucleo = "0.2.0"

@ -61,4 +61,4 @@ Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-c
# Credits # Credits
Thanks to [@JakeHL](https://github.com/JakeHL) for designing the logo! Thanks to [@jakenvac](https://github.com/jakenvac) for designing the logo!

@ -3,10 +3,14 @@ authors = ["Blaž Hrastnik"]
language = "en" language = "en"
multilingual = false multilingual = false
src = "src" src = "src"
edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
[output.html] [output.html]
cname = "docs.helix-editor.com" cname = "docs.helix-editor.com"
default-theme = "colibri" default-theme = "colibri"
preferred-dark-theme = "colibri" preferred-dark-theme = "colibri"
git-repository-url = "https://github.com/helix-editor/helix" git-repository-url = "https://github.com/helix-editor/helix"
edit-url-template = "https://github.com/helix-editor/helix/edit/master/book/{path}"
additional-css = ["custom.css"]
[output.html.search]
use-boolean-and = true

@ -0,0 +1,231 @@
html {
font-family: "Inter", sans-serif;
}
.sidebar .sidebar-scrollbox {
padding: 0;
}
.chapter {
margin: 0.25rem 0;
}
.chapter li.chapter-item {
line-height: initial;
margin: 0;
padding: 1rem 1.5rem;
}
.chapter .section li.chapter-item {
line-height: inherit;
padding: .5rem .5rem 0 .5rem;
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
/* 2 1.75 1.5 1.25 1 .875 */
.content h1 { font-size: 2em }
.content h2 { font-size: 1.75em }
.content h3 { font-size: 1.5em }
.content h4 { font-size: 1.25em }
.content h5 { font-size: 1em }
.content h6 { font-size: .875em }
.content h1,
.content h2,
.content h3,
.content h4 {
font-weight: 500;
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p,
.content ol,
.content ul,
.content table {
margin-top: 0;
margin-bottom: .875em;
}
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul,
.content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-left: 4px solid var(--quote-border);
border-top: none;
border-bottom: none;
}
blockquote *:last-child {
margin-bottom: 0;
}
table {
width: 100%;
}
table thead th {
padding: .75rem;
text-align: left;
font-weight: 500;
line-height: 1.5;
width: auto;
}
table td {
padding: .75rem;
border: none;
}
table thead tr {
border: none;
border-bottom: 2px var(--table-border-color) solid;
}
table tbody tr {
border-bottom: 1px var(--table-border-line) solid;
}
table tbody tr:nth-child(2n) {
background: unset;
}
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.colibri {
--bg: #3b224c;
--fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
/* --links: #a4a0e8; */
--links: #ECCDBA;
--inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0, 0, 0, .2);
--quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #acff5;
}
.colibri .content .header {
color: #fff;
}
/* highlight.js theme, :where() is used to avoid increasing specificity */
:where(.colibri) .hljs {
background: #2f1e2e;
color: #a39e9b;
}
:where(.colibri) .hljs-comment,
:where(.colibri) .hljs-quote {
color: #8d8687;
}
:where(.colibri) .hljs-link,
:where(.colibri) .hljs-meta,
:where(.colibri) .hljs-name,
:where(.colibri) .hljs-regexp,
:where(.colibri) .hljs-selector-class,
:where(.colibri) .hljs-selector-id,
:where(.colibri) .hljs-tag,
:where(.colibri) .hljs-template-variable,
:where(.colibri) .hljs-variable {
color: #ef6155;
}
:where(.colibri) .hljs-built_in,
:where(.colibri) .hljs-deletion,
:where(.colibri) .hljs-literal,
:where(.colibri) .hljs-number,
:where(.colibri) .hljs-params,
:where(.colibri) .hljs-type {
color: #f99b15;
}
:where(.colibri) .hljs-attribute,
:where(.colibri) .hljs-section,
:where(.colibri) .hljs-title {
color: #fec418;
}
:where(.colibri) .hljs-addition,
:where(.colibri) .hljs-bullet,
:where(.colibri) .hljs-string,
:where(.colibri) .hljs-symbol {
color: #48b685;
}
:where(.colibri) .hljs-keyword,
:where(.colibri) .hljs-selector-tag {
color: #815ba4;
}
:where(.colibri) .hljs-emphasis {
font-style: italic;
}
:where(.colibri) .hljs-strong {
font-weight: 700;
}

@ -52,6 +52,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `auto-format` | Enable automatic formatting on save | `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` | | `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
| `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-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` | | `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` | | `auto-info` | Whether to display info boxes | `true` |
@ -62,6 +63,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | | `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` | | `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` | `[]` | | `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` |
### `[editor.statusline]` Section ### `[editor.statusline]` Section
@ -87,9 +89,9 @@ The `[editor.statusline]` key takes the following sub-keys:
| Key | Description | Default | | Key | Description | Default |
| --- | --- | --- | | --- | --- | --- |
| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name"]` | | `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 | `[]` | | `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", "position", "file-encoding"]` | | `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 | `"│"` | | `separator` | The character used to separate elements in the statusline | `"│"` |
| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` | | `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.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
@ -106,6 +108,7 @@ The following statusline elements can be configured:
| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) | | `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-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) | | `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 | | `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file | | `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors | | `diagnostics` | The number of warnings and/or errors |
@ -117,6 +120,7 @@ The following statusline elements can be configured:
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) | | `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |
| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) | | `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 | | `version-control` | The current branch name or detached commit hash of the opened workspace |
| `register` | The current selected register |
### `[editor.lsp]` Section ### `[editor.lsp]` Section
@ -131,8 +135,8 @@ The following statusline elements can be configured:
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `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. [^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! [^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 ### `[editor.cursor-shape]` Section
@ -344,3 +348,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0 max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it wrap-indicator = "" # set wrap-indicator to "" to hide it
``` ```
### `[editor.smart-tab]` Section
| 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` |

@ -2,7 +2,7 @@
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| astro | ✓ | | | | | astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` | | awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | | ✓ | `bash-language-server` | | bash | ✓ | | ✓ | `bash-language-server` |
| bass | ✓ | | | `bass` | | bass | ✓ | | | `bass` |
| beancount | ✓ | | | | | beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` | | bibtex | ✓ | | | `texlab` |
@ -41,9 +41,11 @@
| erlang | ✓ | ✓ | | `erlang_ls` | | erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | | | esdl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | | | fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | | | forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | ✓ | `fortls` | | fortran | ✓ | | ✓ | `fortls` |
| fsharp | ✓ | | | `fsautocomplete` |
| gdscript | ✓ | ✓ | ✓ | | | gdscript | ✓ | ✓ | ✓ | |
| gemini | ✓ | | | |
| git-attributes | ✓ | | | | | git-attributes | ✓ | | | |
| git-commit | ✓ | ✓ | | | | git-commit | ✓ | ✓ | | |
| git-config | ✓ | | | | | git-config | ✓ | | | |
@ -59,6 +61,7 @@
| graphql | ✓ | | | | | graphql | ✓ | | | |
| hare | ✓ | | | | | hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` | | haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | |
| hcl | ✓ | | ✓ | `terraform-ls` | | hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` | | heex | ✓ | ✓ | | `elixir-ls` |
| hosts | ✓ | | | | | hosts | ✓ | | | |
@ -67,8 +70,9 @@
| idris | | | | `idris2-lsp` | | idris | | | | `idris2-lsp` |
| iex | ✓ | | | | | iex | ✓ | | | |
| ini | ✓ | | | | | ini | ✓ | | | |
| java | ✓ | ✓ | | `jdtls` | | java | ✓ | ✓ | | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` | | javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | |
| jsdoc | ✓ | | | | | jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` | | json | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` | | jsonnet | ✓ | | | `jsonnet-language-server` |
@ -88,7 +92,7 @@
| markdoc | ✓ | | | `markdoc-ls` | | markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman` | | markdown | ✓ | | | `marksman` |
| markdown.inline | ✓ | | | | | markdown.inline | ✓ | | | |
| matlab | ✓ | | | | | matlab | ✓ | | | |
| mermaid | ✓ | | | | | mermaid | ✓ | | | |
| meson | ✓ | | ✓ | | | meson | ✓ | | ✓ | |
| mint | | | | `mint` | | mint | | | | `mint` |
@ -98,6 +102,7 @@
| nim | ✓ | ✓ | ✓ | `nimlangserver` | | nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | | | `nil` | | nix | ✓ | | | `nil` |
| nu | ✓ | | | | | nu | ✓ | | | |
| nunjucks | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` | | ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` | | ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | ✓ | `ols` | | odin | ✓ | | ✓ | `ols` |
@ -107,13 +112,14 @@
| pascal | ✓ | ✓ | | `pasls` | | pascal | ✓ | ✓ | | `pasls` |
| passwd | ✓ | | | | | passwd | ✓ | | | |
| pem | ✓ | | | | | pem | ✓ | | | |
| perl | ✓ | | | `perlnavigator` | | perl | ✓ | | | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` | | php | ✓ | ✓ | ✓ | `intelephense` |
| po | ✓ | ✓ | | | | po | ✓ | ✓ | | |
| pod | ✓ | | | |
| ponylang | ✓ | ✓ | ✓ | | | ponylang | ✓ | ✓ | ✓ | |
| prisma | ✓ | | | `prisma-language-server` | | prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` | | prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | | | protobuf | ✓ | | ✓ | `bufls`, `pb` |
| prql | ✓ | | | | | prql | ✓ | | | |
| purescript | ✓ | | | `purescript-language-server` | | purescript | ✓ | | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` | | python | ✓ | ✓ | ✓ | `pylsp` |
@ -140,29 +146,35 @@
| sql | ✓ | | | | | sql | ✓ | | | |
| sshclientconfig | ✓ | | | | | sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | | | starlark | ✓ | ✓ | | |
| svelte | ✓ | | | `svelteserver` | | strace | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` | | sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` | | swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | | | tablegen | ✓ | ✓ | ✓ | |
| task | ✓ | | | | | task | ✓ | | | |
| tfvars | ✓ | | ✓ | `terraform-ls` | | tfvars | ✓ | | ✓ | `terraform-ls` |
| todotxt | ✓ | | | |
| toml | ✓ | | | `taplo` | | toml | ✓ | | | `taplo` |
| tsq | ✓ | | | | | tsq | ✓ | | | |
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` | | tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| twig | ✓ | | | | | twig | ✓ | | | |
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` | | typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| ungrammar | ✓ | | | | | ungrammar | ✓ | | | |
| unison | ✓ | | | |
| uxntal | ✓ | | | | | uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v` | | v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` | | vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` | | verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` | | vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | | | vhs | ✓ | | | |
| vue | ✓ | | | `vls` | | vue | ✓ | | | `vue-language-server` |
| wast | ✓ | | | | | wast | ✓ | | | |
| wat | ✓ | | | | | wat | ✓ | | | |
| webc | ✓ | | | |
| wgsl | ✓ | | | `wgsl_analyzer` | | wgsl | ✓ | | | `wgsl_analyzer` |
| wit | ✓ | | ✓ | | | wit | ✓ | | ✓ | |
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | | | xit | ✓ | | | |
| xml | ✓ | | ✓ | | | xml | ✓ | | ✓ | |
| yaml | ✓ | | ✓ | `yaml-language-server` | | yaml | ✓ | | ✓ | `yaml-language-server` |

@ -24,6 +24,7 @@
| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) | | `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) | | `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
| `:write-all`, `:wa` | Write changes from all buffers to disk. | | `:write-all`, `:wa` | Write changes from all buffers to disk. |
| `:write-all!`, `:wa!` | Forcefully write changes from all buffers to disk creating necessary subdirectories. |
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. | | `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). | | `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). |
| `:quit-all`, `:qa` | Close all views. | | `:quit-all`, `:qa` | Close all views. |
@ -31,6 +32,7 @@
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | | `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). | | `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme (show current theme if no name specified). | | `:theme` | Change the editor theme (show current theme if no name specified). |
| `:yank-join` | Yank joined selections. A separator can be provided as first argument. Default value is newline. |
| `:clipboard-yank` | Yank main selection into system clipboard. | | `:clipboard-yank` | Yank main selection into system clipboard. |
| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. | | `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. | | `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
@ -46,8 +48,8 @@
| `:show-directory`, `:pwd` | Show the current working directory. | | `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. | | `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
| `:character-info`, `:char` | Get info about the character under the primary cursor. | | `:character-info`, `:char` | Get info about the character under the primary cursor. |
| `:reload` | Discard changes and reload from the source file. | | `:reload`, `:rl` | Discard changes and reload from the source file. |
| `:reload-all` | Discard changes and reload all documents from the source files. | | `:reload-all`, `:rla` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. | | `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker | | `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the language servers used by the current doc | | `:lsp-restart` | Restarts the language servers used by the current doc |

@ -1,76 +1,299 @@
# Adding indent queries # Adding indent queries
Helix uses tree-sitter to correctly indent new lines. This requires Helix uses tree-sitter to correctly indent new lines. This requires a tree-
a tree-sitter grammar and an `indent.scm` query file placed in sitter grammar and an `indent.scm` query file placed in `runtime/queries/
`runtime/queries/{language}/indents.scm`. The indentation for a line {language}/indents.scm`. The indentation for a line is calculated by traversing
is calculated by traversing the syntax tree from the lowest node at the the syntax tree from the lowest node at the beginning of the new line (see
beginning of the new line. Each of these nodes contributes to the total [Indent queries](#indent-queries)). Each of these nodes contributes to the total
indent when it is captured by the query (in what way depends on the name indent when it is captured by the query (in what way depends on the name of
of the capture). the capture.
Note that it matters where these added indents begin. For example, Note that it matters where these added indents begin. For example,
multiple indent level increases that start on the same line only increase multiple indent level increases that start on the same line only increase
the total indent level by 1. the total indent level by 1. See [Capture types](#capture-types).
## Scopes ## Indent queries
Added indents don't always apply to the whole node. For example, in most When Helix is inserting a new line through `o`, `O`, or `<ret>`, to determine
cases when a node should be indented, we actually only want everything the indent level for the new line, the query in `indents.scm` is run on the
except for its first line to be indented. For this, there are several document. The starting position of the query is the end of the line above where
scopes (more scopes may be added in the future if required): a new line will be inserted.
- `all`: For `o`, the inserted line is the line below the cursor, so that starting
This scope applies to the whole captured node. This is only different from position of the query is the end of the current line.
`tail` when the captured node is the first node on its line.
- `tail`: ```rust
This scope applies to everything except for the first line of the fn need_hero(some_hero: Hero, life: Life) -> {
captured node. matches!(some_hero, Hero { // ←─────────────────╮
strong: true,//←╮ ↑ ↑ │
fast: true, // │ │ ╰── query start │
sure: true, // │ ╰───── cursor ├─ traversal
soon: true, // ╰──────── new line inserted │ start node
}) && // │
// ↑ │
// ╰───────────────────────────────────────────────╯
some_hero > life
}
```
Every capture type has a default scope which should do the right thing For `O`, the newly inserted line is the *current* line, so the starting position
in most situations. When a different scope is required, this can be of the query is the end of the line above the cursor.
changed by using a `#set!` declaration anywhere in the pattern:
```scm ```rust
(assignment_expression fn need_hero(some_hero: Hero, life: Life) -> { // ←─╮
right: (_) @indent matches!(some_hero, Hero { // ←╮ ↑ │
(#set! "scope" "all")) strong: true,// ↑ ╭───╯ │ │
fast: true, // │ │ query start ─╯ │
sure: true, // ╰───┼ cursor ├─ traversal
soon: true, // ╰ new line inserted │ start node
}) && // │
some_hero > life // │
} // ←──────────────────────────────────────────────╯
``` ```
## Capture types From this starting node, the syntax tree is traversed up until the root node.
Each indent capture is collected along the way, and then combined according to
their [capture types](#capture-types) and [scopes](#scopes) to a final indent
level for the line.
- `@indent` (default scope `tail`): ### Capture types
Increase the indent level by 1. Multiple occurrences in the same line
don't stack. If there is at least one `@indent` and one `@outdent`
capture on the same line, the indent level isn't changed at all.
- `@indent` (default scope `tail`):
Increase the indent level by 1. Multiple occurrences in the same line *do not*
stack. If there is at least one `@indent` and one `@outdent` capture on the
same line, the indent level isn't changed at all.
- `@outdent` (default scope `all`): - `@outdent` (default scope `all`):
Decrease the indent level by 1. The same rules as for `@indent` apply. Decrease the indent level by 1. The same rules as for `@indent` apply.
- `@indent.always` (default scope `tail`):
Increase the indent level by 1. Multiple occurrences on the same line *do*
stack. The final indent level is `@indent.always` `@outdent.always`. If
an `@indent` and an `@indent.always` are on the same line, the `@indent` is
ignored.
- `@outdent.always` (default scope `all`):
Decrease the indent level by 1. The same rules as for `@indent.always` apply.
- `@align` (default scope `all`):
Align everything inside this node to some anchor. The anchor is given
by the start of the node captured by `@anchor` in the same pattern.
Every pattern with an `@align` should contain exactly one `@anchor`.
Indent (and outdent) for nodes below (in terms of their starting line)
the `@align` node is added to the indentation required for alignment.
- `@extend`: - `@extend`:
Extend the range of this node to the end of the line and to lines that Extend the range of this node to the end of the line and to lines that are
are indented more than the line that this node starts on. This is useful indented more than the line that this node starts on. This is useful for
for languages like Python, where for the purpose of indentation some nodes languages like Python, where for the purpose of indentation some nodes (like
(like functions or classes) should also contain indented lines that follow them. functions or classes) should also contain indented lines that follow them.
- `@extend.prevent-once`: - `@extend.prevent-once`:
Prevents the first extension of an ancestor of this node. For example, in Python Prevents the first extension of an ancestor of this node. For example, in Python
a return expression always ends the block that it is in. Note that this only stops the a return expression always ends the block that it is in. Note that this only
extension of the next `@extend` capture. If multiple ancestors are captured, stops the extension of the next `@extend` capture. If multiple ancestors are
only the extension of the innermost one is prevented. All other ancestors are unaffected captured, only the extension of the innermost one is prevented. All other
(regardless of whether the innermost ancestor would actually have been extended). ancestors are unaffected (regardless of whether the innermost ancestor would
actually have been extended).
#### `@indent` / `@outdent`
Consider this example:
```rust
fn shout(things: Vec<Thing>) {
// ↑
// ├───────────────────────╮ indent level
// @indent ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// │
let it_all = |out| { things.filter(|thing| { // │ 1
// ↑ ↑ │
// ├───────────────────────┼─────┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// @indent @indent
// │ 2
thing.can_do_with(out) // │
})}; // ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
//↑↑↑ │ 1
} //╰┼┴──────────────────────────────────────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// 3x @outdent
```
```scm
((block) @indent)
["}" ")"] @outdent
```
Note how on the second line, we have two blocks begin on the same line. In this
case, since both captures occur on the same line, they are combined and only
result in a net increase of 1. Also note that the closing `}`s are part of the
`@indent` captures, but the 3 `@outdent`s also combine into 1 and result in that
line losing one indent level.
#### `@extend` / `@extend.prevent-once`
For an example of where `@extend` can be useful, consider Python, which is
whitespace-sensitive.
```scm
]
(parenthesized_expression)
(function_definition)
(class_definition)
] @indent
```
```python
class Hero:
def __init__(self, strong, fast, sure, soon):# ←─╮
self.is_strong = strong # │
self.is_fast = fast # ╭─── query start │
self.is_sure = sure # │ ╭─ cursor │
self.is_soon = soon # │ │ │
# ↑ ↑ │ │ │
# │ ╰──────╯ │ │
# ╰─────────────────────╯ │
# ├─ traversal
def need_hero(self, life): # │ start node
return ( # │
self.is_strong # │
and self.is_fast # │
and self.is_sure # │
and self.is_soon # │
and self > life # │
) # ←─────────────────────────────────────────╯
```
Without braces to catch the scope of the function, the smallest descendant of
the cursor on a line feed ends up being the entire inside of the class. Because
of this, it will miss the entire function node and its indent capture, leading
to an indent level one too small.
To address this case, `@extend` tells helix to "extend" the captured node's span
to the line feed and every consecutive line that has a greater indent level than
the line of the node.
```scm
(parenthesized_expression) @indent
]
(function_definition)
(class_definition)
] @indent @extend
```
```python
class Hero:
def __init__(self, strong, fast, sure, soon):# ←─╮
self.is_strong = strong # │
self.is_fast = fast # ╭─── query start ├─ traversal
self.is_sure = sure # │ ╭─ cursor │ start node
self.is_soon = soon # │ │ ←───────────────╯
# ↑ ↑ │ │
# │ ╰──────╯ │
# ╰─────────────────────╯
def need_hero(self, life):
return (
self.is_strong
and self.is_fast
and self.is_sure
and self.is_soon
and self > life
)
```
Furthermore, there are some cases where extending to everything with a greater
indent level may not be desirable. Consider the `need_hero` function above. If
our cursor is on the last line of the returned expression.
```python
class Hero:
def __init__(self, strong, fast, sure, soon):
self.is_strong = strong
self.is_fast = fast
self.is_sure = sure
self.is_soon = soon
def need_hero(self, life):
return (
self.is_strong
and self.is_fast
and self.is_sure
and self.is_soon
and self > life
) # ←─── cursor
#←────────── where cursor should go on new line
```
In Python, the are a few tokens that will always end a scope, such as a return
statement. Since the scope ends, so should the indent level. But because the
function span is extended to every line with a greater indent level, a new line
would just continue on the same level. And an `@outdent` would not help us here
either, since it would cause everything in the parentheses to become outdented
as well.
To help, we need to signal an end to the extension. We can do this with
`@extend.prevent-once`.
```scm
(parenthesized_expression) @indent
]
(function_definition)
(class_definition)
] @indent @extend
(return_statement) @extend.prevent-once
```
#### `@indent.always` / `@outdent.always`
As mentioned before, normally if there is more than one `@indent` or `@outdent`
capture on the same line, they are combined.
Sometimes, there are cases when you may want to ensure that every indent capture
is additive, regardless of how many occur on the same line. Consider this
example in YAML.
```yaml
- foo: bar
# ↑ ↑
# │ ╰─────────────── start of map
# ╰───────────────── start of list element
baz: quux # ←─── cursor
# ←───────────── where the cursor should go on a new line
garply: waldo
- quux:
bar: baz
xyzzy: thud
fred: plugh
```
In YAML, you often have lists of maps. In these cases, the syntax is such that
the list element and the map both start on the same line. But we really do want
to start an indentation for each of these so that subsequent keys in the map
hang over the list and align properly. This is where `@indent.always` helps.
```scm
((block_sequence_item) @item @indent.always @extend
(#not-one-line? @item))
((block_mapping_pair
key: (_) @key
value: (_) @val
(#not-same-line? @key @val)
) @indent.always @extend
)
```
## Predicates ## Predicates
In some cases, an S-expression cannot express exactly what pattern should be matched. In some cases, an S-expression cannot express exactly what pattern should be matched.
For that, tree-sitter allows for predicates to appear anywhere within a pattern, For that, tree-sitter allows for predicates to appear anywhere within a pattern,
similar to how `#set!` declarations work: similar to how `#set!` declarations work:
```scm ```scm
(some_kind (some_kind
(child_kind) @indent (child_kind) @indent
(#predicate? arg1 arg2 ...) (#predicate? arg1 arg2 ...)
) )
``` ```
The number of arguments depends on the predicate that's used. The number of arguments depends on the predicate that's used.
Each argument is either a capture (`@name`) or a string (`"some string"`). Each argument is either a capture (`@name`) or a string (`"some string"`).
The following predicates are supported by tree-sitter: The following predicates are supported by tree-sitter:
@ -91,3 +314,47 @@ argument (a string).
- `#same-line?`/`#not-same-line?`: - `#same-line?`/`#not-same-line?`:
The captures given by the 2 arguments must/must not start on the same line. The captures given by the 2 arguments must/must not start on the same line.
- `#one-line?`/`#not-one-line?`:
The captures given by the fist argument must/must span a total of one line.
### Scopes
Added indents don't always apply to the whole node. For example, in most
cases when a node should be indented, we actually only want everything
except for its first line to be indented. For this, there are several
scopes (more scopes may be added in the future if required):
- `tail`:
This scope applies to everything except for the first line of the
captured node.
- `all`:
This scope applies to the whole captured node. This is only different from
`tail` when the captured node is the first node on its line.
For example, imagine we have the following function
```rust
fn aha() { // ←─────────────────────────────────────╮
let take = "on me"; // ←──────────────╮ scope: │
let take = "me on"; // ├─ "tail" ├─ (block) @indent
let ill = be_gone_days(1 || 2); // │ │
} // ←───────────────────────────────────┴──────────┴─ "}" @outdent
// scope: "all"
```
We can write the following query with the `#set!` declaration:
```scm
((block) @indent
(#set! "scope" "tail"))
("}" @outdent
(#set! "scope" "all"))
```
As we can see, the "tail" scope covers the node, except for the first line.
Everything up to and including the closing brace gets an indent level of 1.
Then, on the closing brace, we encounter an outdent with a scope of "all", which
means the first line is included, and the indent level is cancelled out on this
line. (Note these scopes are the defaults for `@indent` and `@outdent`—they are
written explicitly for demonstration.)

@ -6,9 +6,10 @@
- [Linux](#linux) - [Linux](#linux)
- [Ubuntu](#ubuntu) - [Ubuntu](#ubuntu)
- [Fedora/RHEL](#fedorarhel) - [Fedora/RHEL](#fedorarhel)
- [Arch Linux community](#arch-linux-community) - [Arch Linux extra](#arch-linux-extra)
- [NixOS](#nixos) - [NixOS](#nixos)
- [Flatpak](#flatpak) - [Flatpak](#flatpak)
- [Snap](#snap)
- [AppImage](#appimage) - [AppImage](#appimage)
- [macOS](#macos) - [macOS](#macos)
- [Homebrew Core](#homebrew-core) - [Homebrew Core](#homebrew-core)
@ -70,9 +71,9 @@ sudo dnf copr enable varlad/helix
sudo dnf install helix sudo dnf install helix
``` ```
### Arch Linux community ### Arch Linux extra
Releases are available in the `community` repository: Releases are available in the `extra` repository:
```sh ```sh
sudo pacman -S helix sudo pacman -S helix
@ -104,6 +105,16 @@ flatpak install flathub com.helix_editor.Helix
flatpak run 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 ### AppImage
Install Helix using the Linux [AppImage](https://appimage.org/) format. Install Helix using the Linux [AppImage](https://appimage.org/) format.
@ -159,9 +170,13 @@ pacman -S mingw-w64-ucrt-x86_64-helix
Requirements: 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 [Rust toolchain](https://www.rust-lang.org/tools/install)
- The [Git version control system](https://git-scm.com/) - 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 - 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: 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:
@ -171,19 +186,19 @@ RUSTFLAGS="-C target-feature=-crt-static"
1. Clone the repository: 1. Clone the repository:
```sh ```sh
git clone https://github.com/helix-editor/helix git clone https://github.com/helix-editor/helix
cd helix cd helix
``` ```
2. Compile from source: 2. Compile from source:
```sh ```sh
cargo install --path helix-term --locked cargo install --path helix-term --locked
``` ```
This command will create the `hx` executable and construct the tree-sitter This command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder. grammars in the local `runtime` folder.
> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch > 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
> grammars with `hx --grammar fetch` and compile them with > grammars with `hx --grammar fetch` and compile them with
@ -195,13 +210,15 @@ grammars in the local `runtime` folder.
#### Linux and macOS #### Linux and macOS
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files and add it to your `~/.bashrc` or equivalent: The **runtime** directory is one below the Helix source, so either set a
`HELIX_RUNTIME` environment variable to point to that directory and add it to
your `~/.bashrc` or equivalent:
```sh ```sh
HELIX_RUNTIME=/home/user-name/src/helix/runtime HELIX_RUNTIME=~/src/helix/runtime
``` ```
Or, create a symlink in `~/.config/helix` that links to the source code directory: Or, create a symbolic link:
```sh ```sh
ln -Ts $PWD/runtime ~/.config/helix/runtime ln -Ts $PWD/runtime ~/.config/helix/runtime

@ -25,6 +25,8 @@
## Normal mode ## Normal mode
Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`.
### Movement ### Movement
> NOTE: Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line. > NOTE: Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line.
@ -289,7 +291,7 @@ This layer is a kludge of mappings, mostly pickers.
| `w` | Enter [window mode](#window-mode) | N/A | | `w` | Enter [window mode](#window-mode) | N/A |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` | | `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` | | `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | | `y` | Yank selections to clipboard | `yank_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` | | `/` | Global search in workspace folder | `global_search` |
@ -339,6 +341,8 @@ These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim
## Insert mode ## Insert mode
Accessed by typing `i` in [normal mode](#normal-mode).
Insert mode bindings are minimal by default. Helix is designed to Insert mode bindings are minimal by default. Helix is designed to
be a modal editor, and this is reflected in the user experience and internal be a modal editor, and this is reflected in the user experience and internal
mechanics. Changes to the text are only saved for undos when mechanics. Changes to the text are only saved for undos when
@ -392,9 +396,11 @@ end = "no_op"
## Select / extend mode ## Select / extend mode
Accessed by typing `v` in [normal mode](#normal-mode).
Select mode echoes Normal mode, but changes any movements to extend Select mode echoes Normal mode, but changes any movements to extend
selections rather than replace them. Goto motions are also changed to selections rather than replace them. Goto motions are also changed to
extend, so that `vgl` for example extends the selection to the end of extend, so that `vgl`, for example, extends the selection to the end of
the line. the line.
Search is also affected. By default, `n` and `N` will remove the current Search is also affected. By default, `n` and `N` will remove the current
@ -407,19 +413,20 @@ you to selectively add search terms to your selections.
Keys to use within picker. Remapping currently not supported. Keys to use within picker. Remapping currently not supported.
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |
| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry | | `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
| `Tab`, `Down`, `Ctrl-n` | Next entry | | `Tab`, `Down`, `Ctrl-n` | Next entry |
| `PageUp`, `Ctrl-u` | Page up | | `PageUp`, `Ctrl-u` | Page up |
| `PageDown`, `Ctrl-d` | Page down | | `PageDown`, `Ctrl-d` | Page down |
| `Home` | Go to first entry | | `Home` | Go to first entry |
| `End` | Go to last entry | | `End` | Go to last entry |
| `Enter` | Open selected | | `Enter` | Open selected |
| `Ctrl-s` | Open horizontally | | `Alt-Enter` | Open selected in the background without closing the picker |
| `Ctrl-v` | Open vertically | | `Ctrl-s` | Open horizontally |
| `Ctrl-t` | Toggle preview | | `Ctrl-v` | Open vertically |
| `Escape`, `Ctrl-c` | Close picker | | `Ctrl-t` | Toggle preview |
| `Escape`, `Ctrl-c` | Close picker |
## Prompt ## Prompt

@ -7,24 +7,24 @@ in `languages.toml` files.
There are three possible locations for a `languages.toml` file: There are three possible locations for a `languages.toml` file:
1. In the Helix source code, this lives in the 1. In the Helix source code, which lives in the
[Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml). [Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml).
It provides the default configurations for languages and language servers. It provides the default configurations for languages and language servers.
2. In your [configuration directory](./configuration.md). This overrides values 2. In your [configuration directory](./configuration.md). This overrides values
from the built-in language configuration. For example to disable from the built-in language configuration. For example, to disable
auto-LSP-formatting in Rust: auto-LSP-formatting in Rust:
```toml ```toml
# in <config_dir>/helix/languages.toml # in <config_dir>/helix/languages.toml
[language-server.mylang-lsp] [language-server.mylang-lsp]
command = "mylang-lsp" command = "mylang-lsp"
[[language]] [[language]]
name = "rust" name = "rust"
auto-format = false auto-format = false
``` ```
3. In a `.helix` folder in your project. Language configuration may also be 3. In a `.helix` folder in your project. Language configuration may also be
overridden local to a project by creating a `languages.toml` file in a overridden local to a project by creating a `languages.toml` file in a
@ -128,7 +128,7 @@ These are the available options for a language server.
A `format` sub-table within `config` can be used to pass extra formatting options to A `format` sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook). [Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
For example with typescript: For example, with typescript:
```toml ```toml
[language-server.typescript-language-server] [language-server.typescript-language-server]
@ -147,8 +147,8 @@ Different languages can use the same language server instance, e.g. `typescript-
In case multiple language servers are specified in the `language-servers` attribute of a `language`, 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. it's often useful to only enable/disable certain language-server features for these language servers.
For example `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`, As an example, `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
so everything else should be handled by the `typescript-language-server` (which is configured by default) so everything else should be handled by the `typescript-language-server` (which is configured by default).
The language configuration for typescript could look like this: The language configuration for typescript could look like this:
```toml ```toml
@ -166,10 +166,10 @@ language-servers = [ { name = "typescript-language-server", except-features = [
``` ```
Each requested LSP feature is prioritized in the order of the `language-servers` array. Each requested LSP feature is prioritized in the order of the `language-servers` array.
For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`). For example, the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).
The features `diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols` are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language. The features `diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols` are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language.
If no `except-features` or `only-features` is given all features for the language server are enabled. If no `except-features` or `only-features` is given, all features for the language server are enabled.
If a language server itself doesn't support a feature the next language server array entry will be tried (and so on). If a language server itself doesn't support a feature, the next language server array entry will be tried (and so on).
The list of supported features is: The list of supported features is:

@ -70,6 +70,7 @@ over it and is merged into the default palette.
| Color Name | | Color Name |
| --- | | --- |
| `default` |
| `black` | | `black` |
| `red` | | `red` |
| `green` | | `green` |

@ -37,19 +37,35 @@ If a register is selected before invoking a change or delete command, the select
- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode). - `"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. - `"md` - Store the selection in register `m` and delete it.
### Special registers ### 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 | | Register character | Contains |
| --- | --- | | --- | --- |
| `/` | Last search | | `/` | Last search |
| `:` | Last executed command | | `:` | Last executed command |
| `"` | Last yanked text | | `"` | Last yanked text |
| `_` | Black hole | | `@` | Last recorded macro |
The system clipboard is not directly supported by a special register. Instead, special commands and keybindings are provided. Refer to the ### Special registers
[key map](keymap.md#space-mode) for more details.
The black hole register is a no-op register, meaning that no data will be read or written to it. 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 ## Surround

@ -1,660 +0,0 @@
"use strict";
// Fix back button cache problem
window.onunload = function () { };
// Global variable, shared between modules
function playground_text(playground) {
let code_block = playground.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
return editor.getValue();
} else {
return code_block.textContent;
}
}
(function codeSnippets() {
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
]);
}
var playgrounds = Array.from(document.querySelectorAll(".playground"));
if (playgrounds.length > 0) {
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
function handle_crate_list_update(playground_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playground_block, playground_crates);
// and install on change listener to dynamically update ACE editors
if (window.ace) {
let code_block = playground_block.querySelector("code");
if (code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
editor.addEventListener("change", function (e) {
update_play_button(playground_block, playground_crates);
});
// add Ctrl-Enter command to execute rust code
editor.commands.addCommand({
name: "run",
bindKey: {
win: "Ctrl-Enter",
mac: "Ctrl-Enter"
},
exec: _editor => run_rust_code(playground_block)
});
}
}
}
// updates the visibility of play button based on `no_run` class and
// used crates vs ones available on http://play.rust-lang.org
function update_play_button(pre_block, playground_crates) {
var play_button = pre_block.querySelector(".play-button");
// skip if code is `no_run`
if (pre_block.querySelector('code').classList.contains("no_run")) {
play_button.classList.add("hidden");
return;
}
// get list of `extern crate`'s from snippet
var txt = playground_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
var item;
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
var all_available = snippet_crates.every(function (elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.classList.remove("hidden");
} else {
play_button.classList.add("hidden");
}
}
function run_rust_code(code_block) {
var result_block = code_block.querySelector(".result");
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';
code_block.append(result_block);
}
let text = playground_text(code_block);
let classes = code_block.querySelector('code').classList;
let has_2018 = classes.contains("edition2018");
let edition = has_2018 ? "2018" : "2015";
var params = {
version: "stable",
optimize: "0",
code: text,
edition: edition
};
if (text.indexOf("#![feature") !== -1) {
params.version = "nightly";
}
result_block.innerText = "Running...";
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params)
})
.then(response => response.json())
.then(response => result_block.innerText = response.result)
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
}
// Syntax highlighting Configuration
hljs.configure({
tabReplace: ' ', // 4 spaces
languages: [], // Languages used for auto-detection
});
let code_nodes = Array
.from(document.querySelectorAll('code'))
// Don't highlight `inline code` blocks in headers.
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
Array
.from(document.querySelectorAll('code.editable'))
.forEach(function (block) { block.classList.remove('language-rust'); });
Array
.from(document.querySelectorAll('code:not(.editable)'))
.forEach(function (block) { hljs.highlightBlock(block); });
} else {
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
var lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return
if (!lines.length) { return; }
block.classList.add("hide-boring");
var buttons = document.createElement('div');
buttons.className = 'buttons';
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
// add expand button
var pre_block = block.parentNode;
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
if (e.target.classList.contains('fa-eye')) {
e.target.classList.remove('fa-eye');
e.target.classList.add('fa-eye-slash');
e.target.title = 'Hide lines';
e.target.setAttribute('aria-label', e.target.title);
block.classList.remove('hide-boring');
} else if (e.target.classList.contains('fa-eye-slash')) {
e.target.classList.remove('fa-eye-slash');
e.target.classList.add('fa-eye');
e.target.title = 'Show hidden lines';
e.target.setAttribute('aria-label', e.target.title);
block.classList.add('hide-boring');
}
});
});
if (window.playground_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
var pre_block = block.parentNode;
if (!pre_block.classList.contains('playground')) {
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var clipButton = document.createElement('button');
clipButton.className = 'fa fa-copy clip-button';
clipButton.title = 'Copy to clipboard';
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
});
}
// Process playground code blocks
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
// Add play button
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var runCodeButton = document.createElement('button');
runCodeButton.className = 'fa fa-play play-button';
runCodeButton.hidden = true;
runCodeButton.title = 'Run this code';
runCodeButton.setAttribute('aria-label', runCodeButton.title);
buttons.insertBefore(runCodeButton, buttons.firstChild);
runCodeButton.addEventListener('click', function (e) {
run_rust_code(pre_block);
});
if (window.playground_copyable) {
var copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
}
let code_block = pre_block.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
var undoChangesButton = document.createElement('button');
undoChangesButton.className = 'fa fa-history reset-button';
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
buttons.insertBefore(undoChangesButton, buttons.firstChild);
undoChangesButton.addEventListener('click', function () {
let editor = window.ace.edit(code_block);
editor.setValue(editor.originalCode);
editor.clearSelection();
});
}
});
})();
(function themes() {
var html = document.querySelector('html');
var themeToggleButton = document.getElementById('theme-toggle');
var themePopup = document.getElementById('theme-list');
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
var stylesheets = {
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
highlight: document.querySelector("[href$='highlight.css']"),
};
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector("button#" + get_theme()).focus();
}
function hideThemes() {
themePopup.style.display = 'none';
themeToggleButton.setAttribute('aria-expanded', false);
themeToggleButton.focus();
}
function get_theme() {
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (theme === null || theme === undefined) {
return default_theme;
} else {
return theme;
}
}
function set_theme(theme, store = true) {
let ace_theme;
if (theme == 'coal' || theme == 'navy') {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = false;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else if (theme == 'ayu') {
stylesheets.ayuHighlight.disabled = false;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = false;
ace_theme = "ace/theme/dawn";
}
setTimeout(function () {
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
}, 1);
if (window.ace && window.editors) {
window.editors.forEach(function (editor) {
editor.setTheme(ace_theme);
});
}
var previousTheme = get_theme();
if (store) {
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
}
html.classList.remove(previousTheme);
html.classList.add(theme);
}
// Set theme
var theme = get_theme();
set_theme(theme, false);
themeToggleButton.addEventListener('click', function () {
if (themePopup.style.display === 'block') {
hideThemes();
} else {
showThemes();
}
});
themePopup.addEventListener('click', function (e) {
var theme = e.target.id || e.target.parentElement.id;
set_theme(theme);
});
themePopup.addEventListener('focusout', function(e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
hideThemes();
}
});
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
hideThemes();
}
});
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (!themePopup.contains(e.target)) { return; }
switch (e.key) {
case 'Escape':
e.preventDefault();
hideThemes();
break;
case 'ArrowUp':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.previousElementSibling) {
li.previousElementSibling.querySelector('button').focus();
}
break;
case 'ArrowDown':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.nextElementSibling) {
li.nextElementSibling.querySelector('button').focus();
}
break;
case 'Home':
e.preventDefault();
themePopup.querySelector('li:first-child button').focus();
break;
case 'End':
e.preventDefault();
themePopup.querySelector('li:last-child button').focus();
break;
}
});
})();
(function sidebar() {
var html = document.querySelector("html");
var sidebar = document.getElementById("sidebar");
var sidebarLinks = document.querySelectorAll('#sidebar a');
var sidebarToggleButton = document.getElementById("sidebar-toggle");
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
var firstContact = null;
function showSidebar() {
html.classList.remove('sidebar-hidden')
html.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', 0);
});
sidebarToggleButton.setAttribute('aria-expanded', true);
sidebar.setAttribute('aria-hidden', false);
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
}
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
function toggleSection(ev) {
ev.currentTarget.parentElement.classList.toggle('expanded');
}
Array.from(sidebarAnchorToggles).forEach(function (el) {
el.addEventListener('click', toggleSection);
});
function hideSidebar() {
html.classList.remove('sidebar-visible')
html.classList.add('sidebar-hidden');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', -1);
});
sidebarToggleButton.setAttribute('aria-expanded', false);
sidebar.setAttribute('aria-hidden', true);
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
}
// Toggle sidebar
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
if (html.classList.contains("sidebar-hidden")) {
var current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-width', '150px');
}
showSidebar();
} else if (html.classList.contains("sidebar-visible")) {
hideSidebar();
} else {
if (getComputedStyle(sidebar)['transform'] === 'none') {
hideSidebar();
} else {
showSidebar();
}
}
});
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
function initResize(e) {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
html.classList.add('sidebar-resizing');
}
function resize(e) {
var pos = (e.clientX - sidebar.offsetLeft);
if (pos < 20) {
hideSidebar();
} else {
if (html.classList.contains("sidebar-hidden")) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize(e) {
html.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
document.addEventListener('touchstart', function (e) {
firstContact = {
x: e.touches[0].clientX,
time: Date.now()
};
}, { passive: true });
document.addEventListener('touchmove', function (e) {
if (!firstContact)
return;
var curX = e.touches[0].clientX;
var xDiff = curX - firstContact.x,
tDiff = Date.now() - firstContact.time;
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
showSidebar();
else if (xDiff < 0 && curX < 300)
hideSidebar();
firstContact = null;
}
}, { passive: true });
// Scroll sidebar to current active section
var activeSection = document.getElementById("sidebar").querySelector(".active");
if (activeSection) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
activeSection.scrollIntoView({ block: 'center' });
}
})();
(function chapterNavigation() {
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (window.search && window.search.hasFocus()) { return; }
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
var nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
break;
case 'ArrowLeft':
e.preventDefault();
var previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
window.location.href = previousButton.href;
}
break;
}
});
})();
(function clipboard() {
var clipButtons = document.querySelectorAll('.clip-button');
function hideTooltip(elem) {
elem.firstChild.innerText = "";
elem.className = 'fa fa-copy clip-button';
}
function showTooltip(elem, msg) {
elem.firstChild.innerText = msg;
elem.className = 'fa fa-copy tooltipped';
}
var clipboardSnippets = new ClipboardJS('.clip-button', {
text: function (trigger) {
hideTooltip(trigger);
let playground = trigger.closest("pre");
return playground_text(playground);
}
});
Array.from(clipButtons).forEach(function (clipButton) {
clipButton.addEventListener('mouseout', function (e) {
hideTooltip(e.currentTarget);
});
});
clipboardSnippets.on('success', function (e) {
e.clearSelection();
showTooltip(e.trigger, "Copied!");
});
clipboardSnippets.on('error', function (e) {
showTooltip(e.trigger, "Clipboard error!");
});
})();
(function scrollToTop () {
var menuTitle = document.querySelector('.menu-title');
menuTitle.addEventListener('click', function () {
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
});
})();
(function controlMenu() {
var menu = document.getElementById('menu-bar');
(function controlPosition() {
var scrollTop = document.scrollingElement.scrollTop;
var prevScrollTop = scrollTop;
var minMenuY = -menu.clientHeight - 50;
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
menu.style.top = scrollTop + 'px';
// Same as parseInt(menu.style.top.slice(0, -2), but faster
var topCache = menu.style.top.slice(0, -2);
menu.classList.remove('sticky');
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
document.addEventListener('scroll', function () {
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
// `null` means that it doesn't need to be updated
var nextSticky = null;
var nextTop = null;
var scrollDown = scrollTop > prevScrollTop;
var menuPosAbsoluteY = topCache - scrollTop;
if (scrollDown) {
nextSticky = false;
if (menuPosAbsoluteY > 0) {
nextTop = prevScrollTop;
}
} else {
if (menuPosAbsoluteY > 0) {
nextSticky = true;
} else if (menuPosAbsoluteY < minMenuY) {
nextTop = prevScrollTop + minMenuY;
}
}
if (nextSticky === true && stickyCache === false) {
menu.classList.add('sticky');
stickyCache = true;
} else if (nextSticky === false && stickyCache === true) {
menu.classList.remove('sticky');
stickyCache = false;
}
if (nextTop !== null) {
menu.style.top = nextTop + 'px';
topCache = nextTop;
}
prevScrollTop = scrollTop;
}, { passive: true });
})();
(function controlBorder() {
menu.classList.remove('bordered');
document.addEventListener('scroll', function () {
if (menu.offsetTop === 0) {
menu.classList.remove('bordered');
} else {
menu.classList.add('bordered');
}
}, { passive: true });
})();
})();

@ -1,499 +0,0 @@
/* CSS for UI elements (a.k.a. chrome) */
@import 'variables.css';
::-webkit-scrollbar {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
html {
scrollbar-color: var(--scrollbar) var(--bg);
}
#searchresults a,
.content a:link,
a:visited,
a > .hljs {
color: var(--links);
}
.content a:hover {
text-decoration: underline;
}
/* Menu Bar */
#menu-bar,
#menu-bar-hover-placeholder {
z-index: 101;
margin: auto calc(0px - var(--page-padding));
}
#menu-bar {
position: relative;
display: flex;
flex-wrap: wrap;
background-color: var(--bg);
border-bottom-color: var(--bg);
border-bottom-width: 1px;
border-bottom-style: solid;
}
#menu-bar.sticky,
.js #menu-bar-hover-placeholder:hover + #menu-bar,
.js #menu-bar:hover,
.js.sidebar-visible #menu-bar {
position: -webkit-sticky;
position: sticky;
top: 0 !important;
}
#menu-bar-hover-placeholder {
position: sticky;
position: -webkit-sticky;
top: 0;
height: var(--menu-bar-height);
}
#menu-bar.bordered {
border-bottom-color: var(--table-border-color);
}
#menu-bar i, #menu-bar .icon-button {
position: relative;
padding: 0 8px;
z-index: 10;
line-height: var(--menu-bar-height);
cursor: pointer;
transition: color 0.5s;
}
@media only screen and (max-width: 420px) {
#menu-bar i, #menu-bar .icon-button {
padding: 0 5px;
}
}
.icon-button {
border: none;
background: none;
padding: 0;
color: inherit;
}
.icon-button i {
margin: 0;
}
.right-buttons {
margin: 0 15px;
}
.right-buttons a {
text-decoration: none;
}
.left-buttons {
display: flex;
margin: 0 5px;
}
.no-js .left-buttons {
display: none;
}
.menu-title {
display: inline-block;
font-weight: 200;
font-size: 2.4rem;
line-height: var(--menu-bar-height);
text-align: center;
margin: 0;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.js .menu-title {
cursor: pointer;
}
.menu-bar,
.menu-bar:visited,
.nav-chapters,
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited,
.menu-bar .icon-button,
.menu-bar a i {
color: var(--icons);
}
.menu-bar i:hover,
.menu-bar .icon-button:hover,
.nav-chapters:hover,
.mobile-nav-chapters i:hover {
color: var(--icons-hover);
}
/* Nav Icons */
.nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
position: fixed;
top: 0;
bottom: 0;
margin: 0;
max-width: 150px;
min-width: 90px;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
transition: color 0.5s, background-color 0.5s;
}
.nav-chapters:hover {
text-decoration: none;
background-color: var(--theme-hover);
transition: background-color 0.15s, color 0.15s;
}
.nav-wrapper {
margin-top: 50px;
display: none;
}
.mobile-nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
width: 90px;
border-radius: 5px;
background-color: var(--sidebar-bg);
}
.previous {
float: left;
}
.next {
float: right;
right: var(--page-padding);
}
@media only screen and (max-width: 1080px) {
.nav-wide-wrapper { display: none; }
.nav-wrapper { display: block; }
}
@media only screen and (max-width: 1380px) {
.sidebar-visible .nav-wide-wrapper { display: none; }
.sidebar-visible .nav-wrapper { display: block; }
}
/* Inline code */
:not(pre) > .hljs {
display: inline;
padding: 0.1em 0.3em;
border-radius: 3px;
}
:not(pre):not(a):not(td):not(p) > .hljs {
color: var(--inline-code-color);
overflow-x: initial;
}
a:hover > .hljs {
text-decoration: underline;
}
pre {
position: relative;
}
pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: var(--sidebar-fg);
cursor: pointer;
}
pre > .buttons :hover {
color: var(--sidebar-active);
}
pre > .buttons i {
margin-left: 8px;
}
pre > .buttons button {
color: inherit;
background: transparent;
border: none;
cursor: inherit;
}
pre > .result {
margin-top: 10px;
}
/* Search */
#searchresults a {
text-decoration: none;
}
mark {
border-radius: 2px;
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
background-color: var(--search-mark-bg);
transition: background-color 300ms linear;
cursor: pointer;
}
mark.fade-out {
background-color: rgba(0,0,0,0) !important;
cursor: auto;
}
.searchbar-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
#searchbar {
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
transition: box-shadow 300ms ease-in-out;
border: 1px solid var(--searchbar-border-color);
border-radius: 3px;
background-color: var(--searchbar-bg);
color: var(--searchbar-fg);
}
#searchbar:focus,
#searchbar.active {
box-shadow: 0 0 3px var(--searchbar-shadow-color);
}
.searchresults-header {
font-weight: bold;
font-size: 1em;
padding: 18px 0 0 5px;
color: var(--searchresults-header-fg);
}
.searchresults-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
border-bottom: 1px dashed var(--searchresults-border-color);
}
ul#searchresults {
list-style: none;
padding-left: 20px;
}
ul#searchresults li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
ul#searchresults li.focus {
background-color: var(--searchresults-li-bg);
}
ul#searchresults span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
font-size: 0.8em;
}
ul#searchresults span.teaser em {
font-weight: bold;
font-style: normal;
}
/* Sidebar */
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: var(--sidebar-width);
font-size: 0.875em;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.sidebar-resizing {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.js:not(.sidebar-resizing) .sidebar {
transition: transform 0.3s; /* Animation: slide away */
}
.sidebar code {
line-height: 2em;
}
.sidebar .sidebar-scrollbox {
overflow-y: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.sidebar .sidebar-resize-handle {
position: absolute;
cursor: col-resize;
width: 0;
right: 0;
top: 0;
bottom: 0;
}
.js .sidebar .sidebar-resize-handle {
cursor: col-resize;
width: 5px;
}
.sidebar-hidden .sidebar {
transform: translateX(calc(0px - var(--sidebar-width)));
}
.sidebar::-webkit-scrollbar {
background: var(--sidebar-bg);
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
.sidebar-visible .page-wrapper {
transform: translateX(var(--sidebar-width));
}
@media only screen and (min-width: 620px) {
.sidebar-visible .page-wrapper {
transform: none;
margin-left: var(--sidebar-width);
}
}
.chapter {
list-style: none outside none;
padding-left: 0;
margin: .25rem 0;
}
.chapter ol {
width: 100%;
}
.chapter li {
display: flex;
color: var(--sidebar-non-existent);
}
.chapter li a {
display: block;
text-decoration: none;
color: var(--sidebar-fg);
}
.chapter li a:hover {
color: var(--sidebar-active);
}
.chapter li a.active {
color: var(--sidebar-active);
}
.chapter li > a.toggle {
cursor: pointer;
display: block;
margin-left: auto;
padding: 0 10px;
user-select: none;
opacity: 0.68;
}
.chapter li > a.toggle div {
transition: transform 0.5s;
}
/* collapse the section */
.chapter li:not(.expanded) + li > ol {
display: none;
}
.chapter li.chapter-item {
padding: 1rem 1.5rem;
}
.chapter .section li.chapter-item {
padding: .5rem .5rem 0 .5rem;
}
.chapter li.expanded > a.toggle div {
transform: rotate(90deg);
}
.spacer {
width: 100%;
height: 3px;
margin: 5px 0px;
}
.chapter .spacer {
background-color: var(--sidebar-spacer);
}
@media (-moz-touch-enabled: 1), (pointer: coarse) {
.chapter li a { padding: 5px 0; }
.spacer { margin: 10px 0; }
}
.section {
list-style: none outside none;
padding-left: 2rem;
line-height: 1.9em;
}
/* Theme Menu Popup */
.theme-popup {
position: absolute;
left: 10px;
top: var(--menu-bar-height);
z-index: 1000;
border-radius: 4px;
font-size: 0.7em;
color: var(--fg);
background: var(--theme-popup-bg);
border: 1px solid var(--theme-popup-border);
margin: 0;
padding: 0;
list-style: none;
display: none;
}
.theme-popup .default {
color: var(--icons);
}
.theme-popup .theme {
width: 100%;
border: 0;
margin: 0;
padding: 2px 10px;
line-height: 25px;
white-space: nowrap;
text-align: left;
cursor: pointer;
color: inherit;
background: inherit;
font-size: inherit;
}
.theme-popup .theme:hover {
background-color: var(--theme-hover);
}
.theme-popup .theme:hover:first-child,
.theme-popup .theme:hover:last-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}

@ -1,233 +0,0 @@
/* Base styles and content styles */
@import 'variables.css';
:root {
/* Browser default font-size is 16px, this way 1 rem = 10px */
font-size: 62.5%;
}
/* TODO: replace with self hosted fonts */
html {
font-family: "Inter", sans-serif;
color: var(--fg);
background-color: var(--bg);
text-size-adjust: none;
}
/* @supports (font-variation-settings: normal) { */
/* html { font-family: 'Inter var', sans-serif; } */
/* } */
body {
margin: 0;
font-size: 1.6rem;
overflow-x: hidden;
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
}
/* Don't change font size in headers. */
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
font-size: unset;
}
.left { float: left; }
.right { float: right; }
.boring { opacity: 0.6; }
.hide-boring .boring { display: none; }
.hidden { display: none !important; }
h2, h3 { margin-top: 2.5em; }
h4, h5 { margin-top: 2em; }
.header + .header h3,
.header + .header h4,
.header + .header h5 {
margin-top: 1em;
}
h1:target::before,
h2:target::before,
h3:target::before,
h4:target::before,
h5:target::before,
h6:target::before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
/* This is broken on Safari as of version 14, but is fixed
in Safari Technology Preview 117 which I think will be Safari 14.2.
https://bugs.webkit.org/show_bug.cgi?id=218076
*/
:target {
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
}
.page {
outline: 0;
padding: 0 var(--page-padding);
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
}
.page-wrapper {
box-sizing: border-box;
}
.js:not(.sidebar-resizing) .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
.content main {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
/* 2 1.75 1.5 1.25 1 .875 */
.content h1 { font-size: 2em }
.content h2 { font-size: 1.75em }
.content h3 { font-size: 1.5em }
.content h4 { font-size: 1.25em }
.content h5 { font-size: 1em }
.content h6 { font-size: .875em }
.content h1, .content h2, .content h3, .content h4 {
font-weight: 500;
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p, .content ol, .content ul, .content table {
margin-top: 0;
margin-bottom: .875em;
}
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul, .content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
.content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }
.content a { text-decoration: none; }
.content a:hover { text-decoration: underline; }
.content img { max-width: 100%; }
.content .header:link,
.content .header:visited {
color: var(--fg);
color: var(--heading-fg);
}
.content .header:link,
.content .header:visited:hover {
text-decoration: none;
}
table {
margin: 0 auto;
border-collapse: collapse;
width: 100%;
}
table td {
padding: .75rem;
width: auto;
}
table thead {
background: var(--table-header-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: .75rem;
text-align: left;
font-weight: 500;
line-height: 1.5;
width: auto;
}
table thead tr {
border-bottom: 2px var(--table-border-color) solid;
}
table tbody tr {
border-bottom: 1px var(--table-border-line) solid;
}
/* Alternate background colors for rows */
table tbody tr:nth-child(2n) {
/* background: var(--table-alternate-bg); */
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-left: 4px solid var(--quote-border);
}
blockquote *:last-child {
margin-bottom: 0;
}
:not(.footnote-definition) + .footnote-definition,
.footnote-definition + :not(.footnote-definition) {
margin-top: 2em;
}
.footnote-definition {
font-size: 0.9em;
margin: 0.5em 0;
}
.footnote-definition p {
display: inline;
}
.tooltiptext {
position: absolute;
visibility: hidden;
color: #fff;
background-color: #333;
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
left: -8px; /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
border-radius: 6px;
padding: 5px 8px;
margin: 5px;
z-index: 1000;
}
.tooltipped .tooltiptext {
visibility: visible;
}
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
font-weight: bold;
}
.result-no-output {
font-style: italic;
}

@ -1,54 +0,0 @@
#sidebar,
#menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none;
}
#page-wrapper.page-wrapper {
transform: none;
margin-left: 0px;
overflow-y: initial;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666666;
border-radius: 5px;
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact;
}
pre > .buttons {
z-index: 2;
}
a, a:visited, a:active, a:hover {
color: #4183c4;
text-decoration: none;
}
h1, h2, h3, h4, h5, h6 {
page-break-inside: avoid;
page-break-after: avoid;
}
pre, code {
page-break-inside: avoid;
white-space: pre-wrap;
}
.fa {
display: none !important;
}

@ -1,411 +0,0 @@
/* Globals */
:root {
--sidebar-width: 300px;
--page-padding: 15px;
--content-max-width: 750px;
--menu-bar-height: 50px;
}
/* Themes */
.ayu {
--bg: hsl(210, 25%, 8%);
--fg: #c5c5c5;
--sidebar-bg: #14191f;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #5c6773;
--sidebar-active: #ffb454;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #0096cf;
--inline-code-color: #ffb454;
--theme-popup-bg: #14191f;
--theme-popup-border: #5c6773;
--theme-hover: #191f26;
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(210, 25%, 13%);
--table-header-bg: hsl(210, 25%, 28%);
--table-alternate-bg: hsl(210, 25%, 11%);
--searchbar-border-color: #848484;
--searchbar-bg: #424242;
--searchbar-fg: #fff;
--searchbar-shadow-color: #d4c89f;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #252932;
--search-mark-bg: #e3b171;
--hljs-background: #191f26;
--hljs-color: #e6e1cf;
--hljs-quote: #5c6773;
--hljs-variable: #ff7733;
--hljs-type: #ffee99;
--hljs-title: #b8cc52;
--hljs-symbol: #ffb454;
--hljs-selector-tag: #ff7733;
--hljs-selector-tag: #36a3d9;
--hljs-selector-tag: #00568d;
--hljs-selector-tag: #91b362;
--hljs-selector-tag: #d96c75;
}
.coal {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existent: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
--hljs-background: #969896;
--hljs-color: #cc6666;
--hljs-quote: #de935f;
--hljs-variable: #f0c674;
--hljs-type: #b5bd68;
--hljs-title: #8abeb7;
--hljs-symbol: #81a2be;
--hljs-selector-tag: #b294bb;
--hljs-selector-tag: #1d1f21;
--hljs-selector-tag: #c5c8c6;
--hljs-selector-tag: #718c00;
--hljs-selector-tag: #c82829;
}
.light {
--bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 0%);
--sidebar-bg: #fafafa;
--sidebar-fg: hsl(0, 0%, 0%);
--sidebar-non-existent: #aaaaaa;
--sidebar-active: #1f1fff;
--sidebar-spacer: #f4f4f4;
--scrollbar: #8F8F8F;
--icons: #747474;
--icons-hover: #000000;
--links: #20609f;
--inline-code-color: #301900;
--theme-popup-bg: #fafafa;
--theme-popup-border: #cccccc;
--theme-hover: #e6e6e6;
--quote-bg: hsl(197, 37%, 96%);
--quote-border: hsl(197, 37%, 91%);
--table-border-color: hsl(0, 0%, 95%);
--table-header-bg: hsl(0, 0%, 80%);
--table-alternate-bg: hsl(0, 0%, 97%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #e4f2fe;
--search-mark-bg: #a2cff5;
--hljs-background: #f6f7f6;
--hljs-color: #000;
--hljs-quote: #575757;
--hljs-variable: #d70025;
--hljs-type: #b21e00;
--hljs-title: #0030f2;
--hljs-symbol: #008200;
--hljs-selector-tag: #9d00ec;
}
.navy {
--bg: hsl(226, 23%, 11%);
--fg: #bcbdd0;
--sidebar-bg: #282d3f;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #2b79a2;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: #282e40;
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 16%);
--table-header-bg: hsl(226, 23%, 31%);
--table-alternate-bg: hsl(226, 23%, 14%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
--hljs-background: #969896;
--hljs-color: #cc6666;
--hljs-quote: #de935f;
--hljs-variable: #f0c674;
--hljs-type: #b5bd68;
--hljs-title: #8abeb7;
--hljs-symbol: #81a2be;
--hljs-selector-tag: #b294bb;
--hljs-selector-tag: #1d1f21;
--hljs-selector-tag: #c5c8c6;
--hljs-selector-tag: #718c00;
--hljs-selector-tag: #c82829;
}
.rust {
--bg: hsl(60, 9%, 87%);
--fg: #262625;
--sidebar-bg: #3b2e2a;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505254;
--sidebar-active: #e69f67;
--sidebar-spacer: #45373a;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #262625;
--links: #2b79a2;
--inline-code-color: #6e6b5e;
--theme-popup-bg: #e1e1db;
--theme-popup-border: #b38f6b;
--theme-hover: #99908a;
--quote-bg: hsl(60, 5%, 75%);
--quote-border: hsl(60, 5%, 70%);
--table-border-color: hsl(60, 9%, 82%);
--table-header-bg: #b3a497;
--table-alternate-bg: hsl(60, 9%, 84%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #dec2a2;
--search-mark-bg: #e69f67;
--hljs-background: #f6f7f6;
--hljs-color: #000;
--hljs-quote: #575757;
--hljs-variable: #d70025;
--hljs-type: #b21e00;
--hljs-title: #0030f2;
--hljs-symbol: #008200;
--hljs-selector-tag: #9d00ec;
}
@media (prefers-color-scheme: dark) {
.light.no-js {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existent: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
}
}
.colibri {
--bg: #3b224c;
--fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
/* --links: #a4a0e8; */
--links: #ECCDBA;
--inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #acff5;
--hljs-background: #2f1e2e;
--hljs-color: #a39e9b;
--hljs-quote: #8d8687;
--hljs-variable: #ef6155;
--hljs-type: #f99b15;
--hljs-title: #fec418;
--hljs-symbol: #48b685;
--hljs-selector-tag: #815ba4;
}
.colibri {
/*
--bg: #ffffff;
--fg: #452859;
--fg: #5a5977;
--heading-fg: #281733;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #6F44F0;
--inline-code-color: #a39e9b;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: rgba(0, 0, 0, 0);
--quote-border: hsl(226, 15%, 75%);
--table-border-color: #5a5977;
--table-border-color: hsl(201deg 10% 67%);
--table-header-bg: hsl(0, 0%, 100%);
--table-alternate-bg: hsl(0, 0%, 97%);
--table-border-line: hsl(201deg, 20%, 92%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
--hljs-background: #TODO;
--hljs-color: #TODO;
--hljs-quote: #TODO;
--hljs-variable: #TODO;
--hljs-type: #TODO;
--hljs-title: #TODO;
--hljs-symbol: #TODO;
--hljs-selector-tag: #TODO;
*/
}

@ -1,56 +0,0 @@
pre code.hljs {
display:block;
overflow-x:auto;
padding:1em
}
code.hljs {
padding:3px 5px
}
.hljs {
background: var(--hljs-background);
color: var(--hljs-color);
}
.hljs-comment,
.hljs-quote {
color: var(--hljs-quote)
}
.hljs-link,
.hljs-meta,
.hljs-name,
.hljs-regexp,
.hljs-selector-class,
.hljs-selector-id,
.hljs-tag,
.hljs-template-variable,
.hljs-variable {
color: var(--hljs-variable)
}
.hljs-built_in,
.hljs-deletion,
.hljs-literal,
.hljs-number,
.hljs-params,
.hljs-type {
color: var(--hljs-type)
}
.hljs-attribute,
.hljs-section,
.hljs-title {
color: var(--hljs-title)
}
.hljs-addition,
.hljs-bullet,
.hljs-string,
.hljs-symbol {
color: var(--hljs-symbol)
}
.hljs-keyword,
.hljs-selector-tag {
color: var(--hljs-selector-tag)
}
.hljs-emphasis {
font-style:italic
}
.hljs-strong {
font-weight:700
}

File diff suppressed because one or more lines are too long

@ -15,7 +15,6 @@
<!-- Custom HTML head --> <!-- Custom HTML head -->
{{> head}} {{> head}}
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}"> <meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
@ -53,18 +52,19 @@
{{#if mathjax_support}} {{#if mathjax_support}}
<!-- MathJax --> <!-- MathJax -->
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}} {{/if}}
</head> </head>
<body> <body>
<div id="body-container">
<!-- Provide site root to javascript --> <!-- Provide site root to javascript -->
<script type="text/javascript"> <script>
var path_to_root = "{{ path_to_root }}"; var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}"; var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script> </script>
<!-- Work around some values being stored in localStorage wrapped in quotes --> <!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript"> <script>
try { try {
var theme = localStorage.getItem('mdbook-theme'); var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar'); var sidebar = localStorage.getItem('mdbook-sidebar');
@ -80,7 +80,7 @@
</script> </script>
<!-- Set the theme before any content is loaded, prevents flash --> <!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript"> <script>
var theme; var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { } try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; } if (theme === null || theme === undefined) { theme = default_theme; }
@ -92,12 +92,14 @@
</script> </script>
<!-- Hide / unhide sidebar before it is displayed --> <!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript"> <script>
var html = document.querySelector('html'); var html = document.querySelector('html');
var sidebar = 'hidden'; var sidebar = null;
if (document.body.clientWidth >= 1080) { if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { } try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible'; sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
} }
html.classList.remove('sidebar-visible'); html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar); html.classList.add("sidebar-" + sidebar);
@ -110,12 +112,34 @@
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div> <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav> </nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper"> <div id="page-wrapper" class="page-wrapper">
<div class="page"> <div class="page">
{{> header}} {{> header}}
<div id="menu-bar-hover-placeholder"></div> <div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered"> <div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons"> <div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar"> <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
@ -124,12 +148,12 @@
<i class="fa fa-paint-brush"></i> <i class="fa fa-paint-brush"></i>
</button> </button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu"> <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
<li role="none"><button role="menuitem" class="theme" id="colibri">{{ theme_option "Colibri" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="colibri">Colibri</button></li>
</ul> </ul>
{{#if search_enabled}} {{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar"> <button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
@ -151,13 +175,19 @@
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i> <i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a> </a>
{{/if}} {{/if}}
{{#if git_repository_edit_url}}
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
{{/if}}
</div> </div>
</div> </div>
{{#if search_enabled}} {{#if search_enabled}}
<div id="search-wrapper" class="hidden"> <div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer"> <form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header"> <input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form> </form>
<div id="searchresults-outer" class="searchresults-outer hidden"> <div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div> <div id="searchresults-header" class="searchresults-header"></div>
@ -168,7 +198,7 @@
{{/if}} {{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM --> <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript"> <script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible'); document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible'); document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) { Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
@ -216,10 +246,12 @@
</div> </div>
{{#if livereload}} {{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) --> <!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript"> <script>
var socket = new WebSocket("{{{livereload}}}"); const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) { socket.onmessage = function (event) {
if (event.data === "reload") { if (event.data === "reload") {
socket.close(); socket.close();
@ -235,7 +267,7 @@
{{#if google_analytics}} {{#if google_analytics}}
<!-- Google Analytics Tag --> <!-- Google Analytics Tag -->
<script type="text/javascript"> <script>
var localAddrs = ["localhost", "127.0.0.1", ""]; var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is // make sure we don't activate google analytics if the developer is
@ -253,43 +285,43 @@
{{/if}} {{/if}}
{{#if playground_line_numbers}} {{#if playground_line_numbers}}
<script type="text/javascript"> <script>
window.playground_line_numbers = true; window.playground_line_numbers = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_copyable}} {{#if playground_copyable}}
<script type="text/javascript"> <script>
window.playground_copyable = true; window.playground_copyable = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_js}} {{#if playground_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}ace.js"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}editor.js"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mode-rust.js"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}theme-dawn.js"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
{{/if}} {{/if}}
{{#if search_js}} {{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}elasticlunr.min.js"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mark.min.js"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}searcher.js"></script>
{{/if}} {{/if}}
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}clipboard.min.js"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}highlight.js"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}book.js"></script>
<!-- Custom JS scripts --> <!-- Custom JS scripts -->
{{#each additional_js}} {{#each additional_js}}
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script> <script src="{{ ../path_to_root }}{{this}}"></script>
{{/each}} {{/each}}
{{#if is_print}} {{#if is_print}}
{{#if mathjax_support}} {{#if mathjax_support}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() { MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
@ -297,7 +329,7 @@
}); });
</script> </script>
{{else}} {{else}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
}); });
@ -305,5 +337,6 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
</div>
</body> </body>
</html> </html>

@ -1,7 +1,7 @@
## Checklist ## Checklist
Helix releases are versioned in the Calendar Versioning scheme: Helix releases are versioned in the Calendar Versioning scheme:
`YY.0M(.MICRO)`, for example `22.05` for May of 2022. In these instructions `YY.0M(.MICRO)`, for example, `22.05` for May of 2022. In these instructions
we'll use `<tag>` as a placeholder for the tag being published. we'll use `<tag>` as a placeholder for the tag being published.
* Merge the changelog PR * Merge the changelog PR
@ -30,7 +30,7 @@ we'll use `<tag>` as a placeholder for the tag being published.
The changelog is currently created manually by reading through commits in the The changelog is currently created manually by reading through commits in the
log since the last release. GitHub's compare view is a nice way to approach log since the last release. GitHub's compare view is a nice way to approach
this. For example when creating the 22.07 release notes, this compare link this. For example, when creating the 22.07 release notes, this compare link
may be used may be used
``` ```

@ -20,5 +20,5 @@ Vision statements are all well and good, but are also vague and subjective. Her
* **Built-in tools** for working with code bases efficiently. Most projects aren't a single file, and an editor should handle that as a first-class use case. In Helix's case, this means (among other things) a fuzzy-search file navigator and LSP support. * **Built-in tools** for working with code bases efficiently. Most projects aren't a single file, and an editor should handle that as a first-class use case. In Helix's case, this means (among other things) a fuzzy-search file navigator and LSP support.
* **Edit anything** that comes up when coding, within reason. Whether it's a 200 MB XML file, a megabyte of minified javascript on a single line, or Japanese text encoded in ShiftJIS, you should be able to open it and edit it without problems. (Note: this doesn't mean handle every esoteric use case. Sometimes you do just need a specialized tool, and Helix isn't that.) * **Edit anything** that comes up when coding, within reason. Whether it's a 200 MB XML file, a megabyte of minified javascript on a single line, or Japanese text encoded in ShiftJIS, you should be able to open it and edit it without problems. (Note: this doesn't mean handle every esoteric use case. Sometimes you do just need a specialized tool, and Helix isn't that.)
* **Configurable**, within reason. Although the defaults should be good, not everyone will agree on what "good" is. Within the bounds of Helix's core interaction models, it should be reasonably configurable so that it can be "good" for more people. This means, for example, custom key maps among other things. * **Configurable**, within reason. Although the defaults should be good, not everyone will agree on what "good" is. Within the bounds of Helix's core interaction models, it should be reasonably configurable so that it can be "good" for more people. This means, for example, custom key maps among other things.
* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed. Right now we're thinking Wasm-based plugins. * **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed.
* **Clean code base.** Sometimes other factors (e.g. significant performance gains, important features, correctness, etc.) will trump strict readability, but we nevertheless want to keep the code base straightforward and easy to understand to the extent we can. * **Clean code base.** Sometimes other factors (e.g. significant performance gains, important features, correctness, etc.) will trump strict readability, but we nevertheless want to keep the code base straightforward and easy to understand to the extent we can.

@ -1,110 +1,29 @@
{ {
"nodes": { "nodes": {
"crane": { "crane": {
"flake": false,
"locked": {
"lastModified": 1681175776,
"narHash": "sha256-7SsUy9114fryHAZ8p1L6G6YSu7jjz55FddEwa2U8XZc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "445a3d222947632b5593112bb817850e8a9cf737",
"type": "github"
},
"original": {
"owner": "ipetkov",
"ref": "v0.12.1",
"repo": "crane",
"type": "github"
}
},
"dream2nix": {
"inputs": { "inputs": {
"all-cabal-json": [
"nci"
],
"crane": "crane",
"devshell": [
"nci"
],
"drv-parts": "drv-parts",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-parts": [ "flake-utils": [
"nci", "flake-utils"
"parts"
],
"flake-utils-pre-commit": [
"nci"
],
"ghc-utils": [
"nci"
],
"gomod2nix": [
"nci"
],
"mach-nix": [
"nci"
],
"nix-pypi-fetcher": [
"nci"
], ],
"nixpkgs": [ "nixpkgs": [
"nci",
"nixpkgs" "nixpkgs"
], ],
"nixpkgsV1": "nixpkgsV1", "rust-overlay": [
"poetry2nix": [ "rust-overlay"
"nci"
],
"pre-commit-hooks": [
"nci"
],
"pruned-racket-catalog": [
"nci"
]
},
"locked": {
"lastModified": 1683212002,
"narHash": "sha256-EObtqyQsv9v+inieRY5cvyCMCUI5zuU5qu+1axlJCPM=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "fbfb09d2ab5ff761d822dd40b4a1def81651d096",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "dream2nix",
"type": "github"
}
},
"drv-parts": {
"inputs": {
"flake-compat": [
"nci",
"dream2nix",
"flake-compat"
],
"flake-parts": [
"nci",
"dream2nix",
"flake-parts"
],
"nixpkgs": [
"nci",
"dream2nix",
"nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1680698112, "lastModified": 1688772518,
"narHash": "sha256-FgnobN/DvCjEsc0UAZEAdPLkL4IZi2ZMnu2K2bUaElc=", "narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=",
"owner": "davhau", "owner": "ipetkov",
"repo": "drv-parts", "repo": "crane",
"rev": "e8c2ec1157dc1edb002989669a0dbd935f430201", "rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "davhau", "owner": "ipetkov",
"repo": "drv-parts", "repo": "crane",
"type": "github" "type": "github"
} }
}, },
@ -129,11 +48,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1681202837, "lastModified": 1689068808,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401", "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -142,55 +61,13 @@
"type": "github" "type": "github"
} }
}, },
"mk-naked-shell": {
"flake": false,
"locked": {
"lastModified": 1681286841,
"narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
"owner": "yusdacra",
"repo": "mk-naked-shell",
"rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
"type": "github"
},
"original": {
"owner": "yusdacra",
"repo": "mk-naked-shell",
"type": "github"
}
},
"nci": {
"inputs": {
"dream2nix": "dream2nix",
"mk-naked-shell": "mk-naked-shell",
"nixpkgs": [
"nixpkgs"
],
"parts": "parts",
"rust-overlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1683699050,
"narHash": "sha256-UWKQpzVcSshB+sU2O8CCHjOSTQrNS7Kk9V3+UeBsJpg=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "ed27173cd1b223f598343ea3c15aacb1d140feac",
"type": "github"
},
"original": {
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1683408522, "lastModified": 1690272529,
"narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=", "narHash": "sha256-MakzcKXEdv/I4qJUtq/k/eG+rVmyOZLnYNC2w1mB59Y=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7", "rev": "ef99fa5c5ed624460217c31ac4271cfb5cb2502c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -200,99 +77,29 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgsV1": {
"locked": {
"lastModified": 1678500271,
"narHash": "sha256-tRBLElf6f02HJGG0ZR7znMNFv/Uf7b2fFInpTHiHaSE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5eb98948b66de29f899c7fe27ae112a47964baf8",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.11",
"type": "indirect"
}
},
"parts": {
"inputs": {
"nixpkgs-lib": [
"nci",
"nixpkgs"
]
},
"locked": {
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"nci": "nci", "crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"parts": "parts_2",
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
} }
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": [
"flake-utils"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1683771545, "lastModified": 1690424156,
"narHash": "sha256-we0GYcKTo2jRQGmUGrzQ9VH0OYAUsJMCsK8UkF+vZUA=", "narHash": "sha256-Bpml+L280tHTQpwpC5/BJbU4HSvEzMvW8IZ4gAXimhE=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "c57e210faf68e5d5386f18f1b17ad8365d25e4ed", "rev": "f335a0213504c7e6481c359dc1009be9cf34432c",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -3,166 +3,194 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
}; };
nci = { crane = {
url = "github:yusdacra/nix-cargo-integration"; url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
inputs.rust-overlay.follows = "rust-overlay"; inputs.rust-overlay.follows = "rust-overlay";
inputs.flake-utils.follows = "flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
}; };
parts.url = "github:hercules-ci/flake-parts";
}; };
outputs = inp: let outputs = {
mkRootPath = rel: self,
builtins.path { nixpkgs,
path = "${toString ./.}/${rel}"; crane,
name = rel; flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
}; };
filteredSource = let mkRootPath = rel:
pathsToIgnore = [ builtins.path {
".envrc" path = "${toString ./.}/${rel}";
".ignore" name = rel;
".github" };
".gitignore" filteredSource = let
"logo.svg" pathsToIgnore = [
"logo_dark.svg" ".envrc"
"logo_light.svg" ".ignore"
"rust-toolchain.toml" ".github"
"rustfmt.toml" ".gitignore"
"runtime" "logo_dark.svg"
"screenshot.png" "logo_light.svg"
"book" "rust-toolchain.toml"
"contrib" "rustfmt.toml"
"docs" "runtime"
"README.md" "screenshot.png"
"CHANGELOG.md" "book"
"shell.nix" "docs"
"default.nix" "README.md"
"grammars.nix" "CHANGELOG.md"
"flake.nix" "shell.nix"
"flake.lock" "default.nix"
]; "grammars.nix"
ignorePaths = path: type: let "flake.nix"
inherit (inp.nixpkgs) lib; "flake.lock"
# split the nix store path into its components ];
components = lib.splitString "/" path; ignorePaths = path: type: let
# drop off the `/nix/hash-source` section from the path inherit (nixpkgs) lib;
relPathComponents = lib.drop 4 components; # split the nix store path into its components
# reassemble the path components components = lib.splitString "/" path;
relPath = lib.concatStringsSep "/" relPathComponents; # drop off the `/nix/hash-source` section from the path
in relPathComponents = lib.drop 4 components;
lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore; # reassemble the path components
in relPath = lib.concatStringsSep "/" relPathComponents;
builtins.path {
name = "helix-source";
path = toString ./.;
# filter out unnecessary paths
filter = ignorePaths;
};
in
inp.parts.lib.mkFlake {inputs = inp;} {
imports = [inp.nci.flakeModule inp.parts.flakeModules.easyOverlay];
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-linux"
"aarch64-darwin"
"i686-linux"
];
perSystem = {
config,
pkgs,
lib,
...
}: let
makeOverridableHelix = old: config: let
grammars = pkgs.callPackage ./grammars.nix config;
runtimeDir = pkgs.runCommand "helix-runtime" {} ''
mkdir -p $out
ln -s ${mkRootPath "runtime"}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
helix-wrapped =
pkgs.runCommand
old.name
{
inherit (old) pname version;
meta = old.meta or {};
passthru =
(old.passthru or {})
// {
unwrapped = old;
};
nativeBuildInputs = [pkgs.makeWrapper];
makeWrapperArgs = config.makeWrapperArgs or [];
}
''
cp -rs --no-preserve=mode,ownership ${old} $out
wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
'';
in in
helix-wrapped lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
// { in
override = makeOverridableHelix old; builtins.path {
name = "helix-source";
path = toString ./.;
# filter out unnecessary paths
filter = ignorePaths;
};
makeOverridableHelix = old: config: let
grammars = pkgs.callPackage ./grammars.nix config;
runtimeDir = pkgs.runCommand "helix-runtime" {} ''
mkdir -p $out
ln -s ${mkRootPath "runtime"}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
helix-wrapped =
pkgs.runCommand
old.name
{
inherit (old) pname version;
meta = old.meta or {};
passthru = passthru =
helix-wrapped.passthru (old.passthru or {})
// { // {
wrapper = old: makeOverridableHelix old config; unwrapped = old;
}; };
}; nativeBuildInputs = [pkgs.makeWrapper];
stdenv = makeWrapperArgs = config.makeWrapperArgs or [];
if pkgs.stdenv.isLinux }
then pkgs.stdenv ''
else pkgs.clangStdenv; cp -rs --no-preserve=mode,ownership ${old} $out
rustFlagsEnv = wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
if stdenv.isLinux '';
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment'' in
else "$RUSTFLAGS"; helix-wrapped
in { // {
nci.projects."helix-project".relPath = ""; override = makeOverridableHelix old;
nci.crates."helix-term" = { passthru =
overrides = { helix-wrapped.passthru
add-meta.override = _: {meta.mainProgram = "hx";}; // {
add-inputs.overrideAttrs = prev: { wrapper = old: makeOverridableHelix old config;
buildInputs = (prev.buildInputs or []) ++ [stdenv.cc.cc.lib];
};
disable-grammar-builds = {
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
}; };
disable-tests = {checkPhase = ":";};
set-stdenv.override = _: {inherit stdenv;};
set-filtered-src.override = _: {src = filteredSource;};
};
}; };
stdenv =
if pkgs.stdenv.isLinux
then pkgs.stdenv
else pkgs.clangStdenv;
rustFlagsEnv =
if stdenv.isLinux
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
else "$RUSTFLAGS";
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
commonArgs =
{
inherit stdenv;
src = filteredSource;
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
buildInputs = [stdenv.cc.cc.lib];
# disable tests
doCheck = false;
meta.mainProgram = "hx";
}
// craneLib.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;};
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
in {
packages = {
helix-unwrapped = craneLib.buildPackage (commonArgs
// {
inherit cargoArtifacts;
postInstall = ''
mkdir -p $out/share/applications $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/256x256/apps
cp contrib/Helix.desktop $out/share/applications
cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg
cp contrib/helix.png $out/share/icons/hicolor/256x256/apps
'';
});
helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {};
default = self.packages.${system}.helix;
};
packages.helix-unwrapped = config.nci.outputs."helix-term".packages.release; checks = {
packages.helix-unwrapped-dev = config.nci.outputs."helix-term".packages.dev; # Build the crate itself
packages.helix = makeOverridableHelix config.packages.helix-unwrapped {}; inherit (self.packages.${system}) helix;
packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {};
packages.default = config.packages.helix;
overlayAttrs = { clippy = craneLib.cargoClippy (commonArgs
inherit (config.packages) helix; // {
}; inherit cargoArtifacts;
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
});
devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: { fmt = craneLib.cargoFmt commonArgs;
nativeBuildInputs =
(old.nativeBuildInputs or []) doc = craneLib.cargoDoc (commonArgs
++ (with pkgs; [lld_13 cargo-flamegraph rust-analyzer]) // {
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin) inherit cargoArtifacts;
++ (lib.optional stdenv.isLinux pkgs.lldb) });
++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
shellHook = '' test = craneLib.cargoTest (commonArgs
export HELIX_RUNTIME="$PWD/runtime" // {
export RUST_BACKTRACE="1" inherit cargoArtifacts;
export RUSTFLAGS="${rustFlagsEnv}" });
''; };
});
devShells.default = pkgs.mkShell {
inputsFrom = builtins.attrValues self.checks.${system};
nativeBuildInputs = with pkgs;
[lld_13 cargo-flamegraph rust-analyzer]
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
++ (lib.optional stdenv.isLinux pkgs.lldb)
++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
shellHook = ''
export HELIX_RUNTIME="$PWD/runtime"
export RUST_BACKTRACE="1"
export RUSTFLAGS="${rustFlagsEnv}"
'';
};
})
// {
overlays.default = final: prev: {
inherit (self.packages.${final.system}) helix;
}; };
}; };

@ -18,18 +18,18 @@ integration = []
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
ropey = { version = "1.6.0", default-features = false, features = ["simd"] } ropey = { version = "1.6.0", default-features = false, features = ["simd"] }
smallvec = "1.10" smallvec = "1.11"
smartstring = "1.0.1" smartstring = "1.0.1"
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
unicode-width = "0.1" unicode-width = "0.1"
unicode-general-category = "0.6" unicode-general-category = "0.6"
# slab = "0.4.2" # slab = "0.4.2"
slotmap = "1.0" slotmap = "1.0"
tree-sitter = "0.20" tree-sitter.workspace = true
once_cell = "1.18" once_cell = "1.18"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"
bitflags = "2.3" bitflags = "2.4"
ahash = "0.8.3" ahash = "0.8.3"
hashbrown = { version = "0.14.0", features = ["raw"] } hashbrown = { version = "0.14.0", features = ["raw"] }
dunce = "1.0" dunce = "1.0"
@ -48,6 +48,9 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
etcetera = "0.8" etcetera = "0.8"
textwrap = "0.16.0" textwrap = "0.16.0"
nucleo.workspace = true
parking_lot = "0.12"
[dev-dependencies] [dev-dependencies]
quickcheck = { version = "1", default-features = false } quickcheck = { version = "1", default-features = false }
indoc = "2.0.1" indoc = "2.0.3"

@ -0,0 +1,43 @@
use std::ops::DerefMut;
use nucleo::pattern::{AtomKind, CaseMatching, Pattern};
use nucleo::Config;
use parking_lot::Mutex;
pub struct LazyMutex<T> {
inner: Mutex<Option<T>>,
init: fn() -> T,
}
impl<T> LazyMutex<T> {
pub const fn new(init: fn() -> T) -> Self {
Self {
inner: Mutex::new(None),
init,
}
}
pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
}
}
pub static MATCHER: LazyMutex<nucleo::Matcher> = LazyMutex::new(nucleo::Matcher::default);
/// convenience function to easily fuzzy match
/// on a (relatively small list of inputs). This is not recommended for building a full tui
/// application that can match large numbers of matches as all matching is done on the current
/// thread, effectively blocking the UI
pub fn fuzzy_match<T: AsRef<str>>(
pattern: &str,
items: impl IntoIterator<Item = T>,
path: bool,
) -> Vec<(T, u32)> {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
if path {
matcher.config.set_match_paths();
}
let pattern = Pattern::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy);
pattern.match_list(items, &mut matcher)
}

@ -481,7 +481,7 @@ impl<'a> From<String> for GraphemeStr<'a> {
let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8; let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8;
GraphemeStr { GraphemeStr {
ptr: unsafe { NonNull::new_unchecked(ptr) }, ptr: unsafe { NonNull::new_unchecked(ptr) },
len: i32::try_from(len).unwrap() as u32, len: (i32::try_from(len).unwrap() as u32) | Self::MASK_OWNED,
phantom: PhantomData, phantom: PhantomData,
} }
} }

@ -72,8 +72,8 @@ impl Default for History {
revisions: vec![Revision { revisions: vec![Revision {
parent: 0, parent: 0,
last_child: None, last_child: None,
transaction: Transaction::from(ChangeSet::new(&Rope::new())), transaction: Transaction::from(ChangeSet::new("".into())),
inversion: Transaction::from(ChangeSet::new(&Rope::new())), inversion: Transaction::from(ChangeSet::new("".into())),
timestamp: Instant::now(), timestamp: Instant::now(),
}], }],
current: 0, current: 0,

@ -1,13 +1,13 @@
use std::collections::HashMap; use std::{borrow::Cow, collections::HashMap};
use tree_sitter::{Query, QueryCursor, QueryPredicateArg}; use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{ use crate::{
chars::{char_is_line_ending, char_is_whitespace}, chars::{char_is_line_ending, char_is_whitespace},
graphemes::tab_width_at, graphemes::{grapheme_width, tab_width_at},
syntax::{LanguageConfiguration, RopeProvider, Syntax}, syntax::{LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node, tree_sitter::Node,
Rope, RopeSlice, Rope, RopeGraphemes, RopeSlice,
}; };
/// Enum representing indentation style. /// Enum representing indentation style.
@ -19,6 +19,10 @@ pub enum IndentStyle {
Spaces(u8), Spaces(u8),
} }
// 16 spaces
const INDENTS: &str = " ";
const MAX_INDENT: u8 = 16;
impl IndentStyle { impl IndentStyle {
/// Creates an `IndentStyle` from an indentation string. /// Creates an `IndentStyle` from an indentation string.
/// ///
@ -27,10 +31,10 @@ impl IndentStyle {
#[inline] #[inline]
pub fn from_str(indent: &str) -> Self { pub fn from_str(indent: &str) -> Self {
// XXX: do we care about validating the input more than this? Probably not...? // XXX: do we care about validating the input more than this? Probably not...?
debug_assert!(!indent.is_empty() && indent.len() <= 8); debug_assert!(!indent.is_empty() && indent.len() <= MAX_INDENT as usize);
if indent.starts_with(' ') { if indent.starts_with(' ') {
IndentStyle::Spaces(indent.len() as u8) IndentStyle::Spaces(indent.len().clamp(1, MAX_INDENT as usize) as u8)
} else { } else {
IndentStyle::Tabs IndentStyle::Tabs
} }
@ -40,20 +44,13 @@ impl IndentStyle {
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
match *self { match *self {
IndentStyle::Tabs => "\t", IndentStyle::Tabs => "\t",
IndentStyle::Spaces(1) => " ",
IndentStyle::Spaces(2) => " ",
IndentStyle::Spaces(3) => " ",
IndentStyle::Spaces(4) => " ",
IndentStyle::Spaces(5) => " ",
IndentStyle::Spaces(6) => " ",
IndentStyle::Spaces(7) => " ",
IndentStyle::Spaces(8) => " ",
// Unsupported indentation style. This should never happen,
// but just in case fall back to two spaces.
IndentStyle::Spaces(n) => { IndentStyle::Spaces(n) => {
debug_assert!(n > 0 && n <= 8); // Always triggers. `debug_panic!()` wanted. // Unsupported indentation style. This should never happen,
" " debug_assert!(n > 0 && n <= MAX_INDENT);
// Either way, clamp to the nearest supported value
let closest_n = n.clamp(1, MAX_INDENT) as usize;
&INDENTS[0..closest_n]
} }
} }
} }
@ -75,9 +72,9 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
// Build a histogram of the indentation *increases* between // Build a histogram of the indentation *increases* between
// subsequent lines, ignoring lines that are all whitespace. // subsequent lines, ignoring lines that are all whitespace.
// //
// Index 0 is for tabs, the rest are 1-8 spaces. // Index 0 is for tabs, the rest are 1-MAX_INDENT spaces.
let histogram: [usize; 9] = { let histogram: [usize; MAX_INDENT as usize + 1] = {
let mut histogram = [0; 9]; let mut histogram = [0; MAX_INDENT as usize + 1];
let mut prev_line_is_tabs = false; let mut prev_line_is_tabs = false;
let mut prev_line_leading_count = 0usize; let mut prev_line_leading_count = 0usize;
@ -136,7 +133,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
histogram[0] += 1; histogram[0] += 1;
} else { } else {
let amount = leading_count - prev_line_leading_count; let amount = leading_count - prev_line_leading_count;
if amount <= 8 { if amount <= MAX_INDENT as usize {
histogram[amount] += 1; histogram[amount] += 1;
} }
} }
@ -240,68 +237,117 @@ fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bo
/// This is usually constructed in one of 2 ways: /// This is usually constructed in one of 2 ways:
/// - Successively add indent captures to get the (added) indent from a single line /// - Successively add indent captures to get the (added) indent from a single line
/// - Successively add the indent results for each line /// - Successively add the indent results for each line
#[derive(Default)] /// The string that this indentation defines starts with the string contained in the align field (unless it is None), followed by:
/// - max(0, indent - outdent) tabs, if tabs are used for indentation
/// - max(0, indent - outdent)*indent_width spaces, if spaces are used for indentation
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Indentation { pub struct Indentation {
/// The total indent (the number of indent levels) is defined as max(0, indent-outdent).
/// The string that this results in depends on the indent style (spaces or tabs, etc.)
indent: usize, indent: usize,
indent_always: usize,
outdent: usize, outdent: usize,
outdent_always: usize,
/// The alignment, as a string containing only tabs & spaces. Storing this as a string instead of e.g.
/// the (visual) width ensures that the alignment is preserved even if the tab width changes.
align: Option<String>,
} }
impl Indentation { impl Indentation {
/// Add some other [Indentation] to this. /// Add some other [Indentation] to this.
/// The added indent should be the total added indent from one line /// The added indent should be the total added indent from one line.
fn add_line(&mut self, added: &Indentation) { /// Indent should always be added starting from the bottom (or equivalently, the innermost tree-sitter node).
if added.indent > 0 && added.outdent == 0 { fn add_line(&mut self, added: Indentation) {
self.indent += 1; // Align overrides the indent from outer scopes.
} else if added.outdent > 0 && added.indent == 0 { if self.align.is_some() {
self.outdent += 1; return;
}
if added.align.is_some() {
self.align = added.align;
return;
} }
self.indent += added.indent;
self.indent_always += added.indent_always;
self.outdent += added.outdent;
self.outdent_always += added.outdent_always;
} }
/// Add an indent capture to this indent. /// Add an indent capture to this indent.
/// All the captures that are added in this way should be on the same line. /// All the captures that are added in this way should be on the same line.
fn add_capture(&mut self, added: IndentCaptureType) { fn add_capture(&mut self, added: IndentCaptureType) {
match added { match added {
IndentCaptureType::Indent => { IndentCaptureType::Indent => {
self.indent = 1; if self.indent_always == 0 {
self.indent = 1;
}
}
IndentCaptureType::IndentAlways => {
// any time we encounter an `indent.always` on the same line, we
// want to cancel out all regular indents
self.indent_always += 1;
self.indent = 0;
} }
IndentCaptureType::Outdent => { IndentCaptureType::Outdent => {
self.outdent = 1; if self.outdent_always == 0 {
self.outdent = 1;
}
}
IndentCaptureType::OutdentAlways => {
self.outdent_always += 1;
self.outdent = 0;
}
IndentCaptureType::Align(align) => {
self.align = Some(align);
} }
} }
} }
fn as_string(&self, indent_style: &IndentStyle) -> String { fn into_string(self, indent_style: &IndentStyle) -> String {
let indent_level = if self.indent >= self.outdent { let indent = self.indent_always + self.indent;
self.indent - self.outdent let outdent = self.outdent_always + self.outdent;
let indent_level = if indent >= outdent {
indent - outdent
} else { } else {
log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent); log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent);
0 0
}; };
indent_style.as_str().repeat(indent_level) let mut indent_string = if let Some(align) = self.align {
align
} else {
String::new()
};
indent_string.push_str(&indent_style.as_str().repeat(indent_level));
indent_string
} }
} }
/// An indent definition which corresponds to a capture from the indent query /// An indent definition which corresponds to a capture from the indent query
#[derive(Debug)]
struct IndentCapture { struct IndentCapture {
capture_type: IndentCaptureType, capture_type: IndentCaptureType,
scope: IndentScope, scope: IndentScope,
} }
#[derive(Clone, Copy)] #[derive(Debug, Clone, PartialEq)]
enum IndentCaptureType { enum IndentCaptureType {
Indent, Indent,
IndentAlways,
Outdent, Outdent,
OutdentAlways,
/// Alignment given as a string of whitespace
Align(String),
} }
impl IndentCaptureType { impl IndentCaptureType {
fn default_scope(&self) -> IndentScope { fn default_scope(&self) -> IndentScope {
match self { match self {
IndentCaptureType::Indent => IndentScope::Tail, IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
IndentCaptureType::Outdent => IndentScope::All, IndentCaptureType::Outdent | IndentCaptureType::OutdentAlways => IndentScope::All,
IndentCaptureType::Align(_) => IndentScope::All,
} }
} }
} }
/// This defines which part of a node an [IndentCapture] applies to. /// This defines which part of a node an [IndentCapture] applies to.
/// Each [IndentCaptureType] has a default scope, but the scope can be changed /// Each [IndentCaptureType] has a default scope, but the scope can be changed
/// with `#set!` property declarations. /// with `#set!` property declarations.
#[derive(Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum IndentScope { enum IndentScope {
/// The indent applies to the whole node /// The indent applies to the whole node
All, All,
@ -311,6 +357,7 @@ enum IndentScope {
/// A capture from the indent query which does not define an indent but extends /// A capture from the indent query which does not define an indent but extends
/// the range of a node. This is used before the indent is calculated. /// the range of a node. This is used before the indent is calculated.
#[derive(Debug)]
enum ExtendCapture { enum ExtendCapture {
Extend, Extend,
PreventOnce, PreventOnce,
@ -319,24 +366,41 @@ enum ExtendCapture {
/// The result of running a tree-sitter indent query. This stores for /// The result of running a tree-sitter indent query. This stores for
/// each node (identified by its ID) the relevant captures (already filtered /// each node (identified by its ID) the relevant captures (already filtered
/// by predicates). /// by predicates).
#[derive(Debug)]
struct IndentQueryResult { struct IndentQueryResult {
indent_captures: HashMap<usize, Vec<IndentCapture>>, indent_captures: HashMap<usize, Vec<IndentCapture>>,
extend_captures: HashMap<usize, Vec<ExtendCapture>>, extend_captures: HashMap<usize, Vec<ExtendCapture>>,
} }
fn get_node_start_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
let mut node_line = node.start_position().row;
// Adjust for the new line that will be inserted
if new_line_byte_pos.map_or(false, |pos| node.start_byte() >= pos) {
node_line += 1;
}
node_line
}
fn get_node_end_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
let mut node_line = node.end_position().row;
// Adjust for the new line that will be inserted (with a strict inequality since end_byte is exclusive)
if new_line_byte_pos.map_or(false, |pos| node.end_byte() > pos) {
node_line += 1;
}
node_line
}
fn query_indents( fn query_indents(
query: &Query, query: &Query,
syntax: &Syntax, syntax: &Syntax,
cursor: &mut QueryCursor, cursor: &mut QueryCursor,
text: RopeSlice, text: RopeSlice,
range: std::ops::Range<usize>, range: std::ops::Range<usize>,
// Position of the (optional) newly inserted line break. new_line_byte_pos: Option<usize>,
// Given as (line, byte_pos)
new_line_break: Option<(usize, usize)>,
) -> IndentQueryResult { ) -> IndentQueryResult {
let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new(); let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new(); let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
cursor.set_byte_range(range); cursor.set_byte_range(range);
// Iterate over all captures from the query // Iterate over all captures from the query
for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) { for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
// Skip matches where not all custom predicates are fulfilled // Skip matches where not all custom predicates are fulfilled
@ -363,21 +427,13 @@ fn query_indents(
Some(QueryPredicateArg::Capture(capt1)), Some(QueryPredicateArg::Capture(capt1)),
Some(QueryPredicateArg::Capture(capt2)) Some(QueryPredicateArg::Capture(capt2))
) => { ) => {
let get_line_num = |node: Node| {
let mut node_line = node.start_position().row;
// Adjust for the new line that will be inserted
if let Some((line, byte)) = new_line_break {
if node_line==line && node.start_byte()>=byte {
node_line += 1;
}
}
node_line
};
let n1 = m.nodes_for_capture_index(*capt1).next(); let n1 = m.nodes_for_capture_index(*capt1).next();
let n2 = m.nodes_for_capture_index(*capt2).next(); let n2 = m.nodes_for_capture_index(*capt2).next();
match (n1, n2) { match (n1, n2) {
(Some(n1), Some(n2)) => { (Some(n1), Some(n2)) => {
let same_line = get_line_num(n1)==get_line_num(n2); let n1_line = get_node_start_line(n1, new_line_byte_pos);
let n2_line = get_node_start_line(n2, new_line_byte_pos);
let same_line = n1_line == n2_line;
same_line==(pred.operator.as_ref()=="same-line?") same_line==(pred.operator.as_ref()=="same-line?")
} }
_ => true, _ => true,
@ -388,6 +444,23 @@ fn query_indents(
} }
} }
} }
"one-line?" | "not-one-line?" => match pred.args.get(0) {
Some(QueryPredicateArg::Capture(capture_idx)) => {
let node = m.nodes_for_capture_index(*capture_idx).next();
match node {
Some(node) => {
let (start_line, end_line) = (get_node_start_line(node,new_line_byte_pos), get_node_end_line(node, new_line_byte_pos));
let one_line = end_line == start_line;
one_line != (pred.operator.as_ref() == "not-one-line?")
},
_ => true,
}
}
_ => {
panic!("Invalid indent query: Arguments to \"not-kind-eq?\" must be a capture and a string");
}
},
_ => { _ => {
panic!( panic!(
"Invalid indent query: Unknown predicate (\"{}\")", "Invalid indent query: Unknown predicate (\"{}\")",
@ -398,11 +471,28 @@ fn query_indents(
}) { }) {
continue; continue;
} }
// A list of pairs (node_id, indent_capture) that are added by this match.
// They cannot be added to indent_captures immediately since they may depend on other captures (such as an @anchor).
let mut added_indent_captures: Vec<(usize, IndentCapture)> = Vec::new();
// The row/column position of the optional anchor in this query
let mut anchor: Option<tree_sitter::Node> = None;
for capture in m.captures { for capture in m.captures {
let capture_name = query.capture_names()[capture.index as usize].as_str(); let capture_name = query.capture_names()[capture.index as usize].as_str();
let capture_type = match capture_name { let capture_type = match capture_name {
"indent" => IndentCaptureType::Indent, "indent" => IndentCaptureType::Indent,
"indent.always" => IndentCaptureType::IndentAlways,
"outdent" => IndentCaptureType::Outdent, "outdent" => IndentCaptureType::Outdent,
"outdent.always" => IndentCaptureType::OutdentAlways,
// The alignment will be updated to the correct value at the end, when the anchor is known.
"align" => IndentCaptureType::Align(String::from("")),
"anchor" => {
if anchor.is_some() {
log::error!("Invalid indent query: Encountered more than one @anchor in the same match.")
} else {
anchor = Some(capture.node);
}
continue;
}
"extend" => { "extend" => {
extend_captures extend_captures
.entry(capture.node.id()) .entry(capture.node.id())
@ -452,17 +542,52 @@ fn query_indents(
} }
} }
} }
added_indent_captures.push((capture.node.id(), indent_capture))
}
for (node_id, mut capture) in added_indent_captures {
// Set the anchor for all align queries.
if let IndentCaptureType::Align(_) = capture.capture_type {
let anchor = match anchor {
None => {
log::error!(
"Invalid indent query: @align requires an accompanying @anchor."
);
continue;
}
Some(anchor) => anchor,
};
// Create a string of tabs & spaces that should have the same width
// as the string that precedes the anchor (independent of the tab width).
let mut align = String::new();
for grapheme in RopeGraphemes::new(
text.line(anchor.start_position().row)
.byte_slice(0..anchor.start_position().column),
) {
if grapheme == "\t" {
align.push('\t');
} else {
align.extend(
std::iter::repeat(' ').take(grapheme_width(&Cow::from(grapheme))),
);
}
}
capture.capture_type = IndentCaptureType::Align(align);
}
indent_captures indent_captures
.entry(capture.node.id()) .entry(node_id)
// Most entries only need to contain a single IndentCapture
.or_insert_with(|| Vec::with_capacity(1)) .or_insert_with(|| Vec::with_capacity(1))
.push(indent_capture); .push(capture);
} }
} }
IndentQueryResult {
let result = IndentQueryResult {
indent_captures, indent_captures,
extend_captures, extend_captures,
} };
log::trace!("indent result = {:?}", result);
result
} }
/// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position. /// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position.
@ -581,12 +706,14 @@ pub fn treesitter_indent_for_pos(
new_line: bool, new_line: bool,
) -> Option<String> { ) -> Option<String> {
let byte_pos = text.char_to_byte(pos); let byte_pos = text.char_to_byte(pos);
let new_line_byte_pos = new_line.then_some(byte_pos);
// The innermost tree-sitter node which is considered for the indent // The innermost tree-sitter node which is considered for the indent
// computation. It may change if some predeceding node is extended // computation. It may change if some predeceding node is extended
let mut node = syntax let mut node = syntax
.tree() .tree()
.root_node() .root_node()
.descendant_for_byte_range(byte_pos, byte_pos)?; .descendant_for_byte_range(byte_pos, byte_pos)?;
let (query_result, deepest_preceding) = { let (query_result, deepest_preceding) = {
// The query range should intersect with all nodes directly preceding // The query range should intersect with all nodes directly preceding
// the position of the indent query in case one of them is extended. // the position of the indent query in case one of them is extended.
@ -617,13 +744,13 @@ pub fn treesitter_indent_for_pos(
&mut cursor, &mut cursor,
text, text,
query_range, query_range,
new_line.then_some((line, byte_pos)), new_line_byte_pos,
); );
ts_parser.cursors.push(cursor); ts_parser.cursors.push(cursor);
(query_result, deepest_preceding) (query_result, deepest_preceding)
}) })
}; };
let indent_captures = query_result.indent_captures; let mut indent_captures = query_result.indent_captures;
let extend_captures = query_result.extend_captures; let extend_captures = query_result.extend_captures;
// Check for extend captures, potentially changing the node that the indent calculation starts with // Check for extend captures, potentially changing the node that the indent calculation starts with
@ -645,12 +772,16 @@ pub fn treesitter_indent_for_pos(
// even if there are multiple "indent" nodes on the same line // even if there are multiple "indent" nodes on the same line
let mut indent_for_line = Indentation::default(); let mut indent_for_line = Indentation::default();
let mut indent_for_line_below = Indentation::default(); let mut indent_for_line_below = Indentation::default();
loop { loop {
// This can safely be unwrapped because `first_in_line` contains // This can safely be unwrapped because `first_in_line` contains
// one entry for each ancestor of the node (which is what we iterate over) // 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 = *first_in_line.last().unwrap();
// Apply all indent definitions for this node
if let Some(definitions) = indent_captures.get(&node.id()) { // Apply all indent definitions for this node.
// Since we only iterate over each node once, we can remove the
// corresponding captures from the HashMap to avoid cloning them.
if let Some(definitions) = indent_captures.remove(&node.id()) {
for definition in definitions { for definition in definitions {
match definition.scope { match definition.scope {
IndentScope::All => { IndentScope::All => {
@ -668,28 +799,22 @@ pub fn treesitter_indent_for_pos(
} }
if let Some(parent) = node.parent() { if let Some(parent) = node.parent() {
let mut node_line = node.start_position().row; let node_line = get_node_start_line(node, new_line_byte_pos);
let mut parent_line = parent.start_position().row; let parent_line = get_node_start_line(parent, new_line_byte_pos);
if node_line == line && new_line {
// Also consider the line that will be inserted
if node.start_byte() >= byte_pos {
node_line += 1;
}
if parent.start_byte() >= byte_pos {
parent_line += 1;
}
};
if node_line != parent_line { if node_line != parent_line {
// Don't add indent for the line below the line of the query
if node_line < line + (new_line as usize) { if node_line < line + (new_line as usize) {
// Don't add indent for the line below the line of the query result.add_line(indent_for_line_below);
result.add_line(&indent_for_line_below);
} }
if node_line == parent_line + 1 { if node_line == parent_line + 1 {
indent_for_line_below = indent_for_line; indent_for_line_below = indent_for_line;
} else { } else {
result.add_line(&indent_for_line); result.add_line(indent_for_line);
indent_for_line_below = Indentation::default(); indent_for_line_below = Indentation::default();
} }
indent_for_line = Indentation::default(); indent_for_line = Indentation::default();
} }
@ -701,13 +826,13 @@ pub fn treesitter_indent_for_pos(
if (node.start_position().row < line) if (node.start_position().row < line)
|| (new_line && node.start_position().row == line && node.start_byte() < byte_pos) || (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
{ {
result.add_line(&indent_for_line_below); result.add_line(indent_for_line_below);
} }
result.add_line(&indent_for_line); result.add_line(indent_for_line);
break; break;
} }
} }
Some(result.as_string(indent_style)) Some(result.into_string(indent_style))
} }
/// Returns the indentation for a new line. /// Returns the indentation for a new line.
@ -797,4 +922,138 @@ mod test {
3 3
); );
} }
#[test]
fn test_large_indent_level() {
let tab_width = 16;
let indent_width = 16;
let line = Rope::from(" fn new"); // 16 spaces
assert_eq!(
indent_level_for_line(line.slice(..), tab_width, indent_width),
1
);
let line = Rope::from(" fn new"); // 32 spaces
assert_eq!(
indent_level_for_line(line.slice(..), tab_width, indent_width),
2
);
}
#[test]
fn add_capture() {
let indent = || Indentation {
indent: 1,
..Default::default()
};
let indent_always = || Indentation {
indent_always: 1,
..Default::default()
};
let outdent = || Indentation {
outdent: 1,
..Default::default()
};
let outdent_always = || Indentation {
outdent_always: 1,
..Default::default()
};
let add_capture = |mut indent: Indentation, capture| {
indent.add_capture(capture);
indent
};
// adding an indent to no indent makes an indent
assert_eq!(
indent(),
add_capture(Indentation::default(), IndentCaptureType::Indent)
);
assert_eq!(
indent_always(),
add_capture(Indentation::default(), IndentCaptureType::IndentAlways)
);
assert_eq!(
outdent(),
add_capture(Indentation::default(), IndentCaptureType::Outdent)
);
assert_eq!(
outdent_always(),
add_capture(Indentation::default(), IndentCaptureType::OutdentAlways)
);
// adding an indent to an already indented has no effect
assert_eq!(indent(), add_capture(indent(), IndentCaptureType::Indent));
assert_eq!(
outdent(),
add_capture(outdent(), IndentCaptureType::Outdent)
);
// adding an always to a regular makes it always
assert_eq!(
indent_always(),
add_capture(indent(), IndentCaptureType::IndentAlways)
);
assert_eq!(
outdent_always(),
add_capture(outdent(), IndentCaptureType::OutdentAlways)
);
// adding an always to an always is additive
assert_eq!(
Indentation {
indent_always: 2,
..Default::default()
},
add_capture(indent_always(), IndentCaptureType::IndentAlways)
);
assert_eq!(
Indentation {
outdent_always: 2,
..Default::default()
},
add_capture(outdent_always(), IndentCaptureType::OutdentAlways)
);
// adding regular to always should be associative
assert_eq!(
Indentation {
indent_always: 1,
..Default::default()
},
add_capture(
add_capture(indent(), IndentCaptureType::Indent),
IndentCaptureType::IndentAlways
)
);
assert_eq!(
Indentation {
indent_always: 1,
..Default::default()
},
add_capture(
add_capture(indent(), IndentCaptureType::IndentAlways),
IndentCaptureType::Indent
)
);
assert_eq!(
Indentation {
outdent_always: 1,
..Default::default()
},
add_capture(
add_capture(outdent(), IndentCaptureType::Outdent),
IndentCaptureType::OutdentAlways
)
);
assert_eq!(
Indentation {
outdent_always: 1,
..Default::default()
},
add_capture(
add_capture(outdent(), IndentCaptureType::OutdentAlways),
IndentCaptureType::Outdent
)
);
}
} }

@ -7,6 +7,7 @@ pub mod config;
pub mod diagnostic; pub mod diagnostic;
pub mod diff; pub mod diff;
pub mod doc_formatter; pub mod doc_formatter;
pub mod fuzzy;
pub mod graphemes; pub mod graphemes;
pub mod history; pub mod history;
pub mod increment; pub mod increment;
@ -18,7 +19,6 @@ pub mod movement;
pub mod object; pub mod object;
pub mod path; pub mod path;
mod position; mod position;
pub mod register;
pub mod search; pub mod search;
pub mod selection; pub mod selection;
pub mod shellwords; pub mod shellwords;
@ -41,7 +41,9 @@ pub use helix_loader::find_workspace;
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> { pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace()) line.chars().position(|ch| !ch.is_whitespace())
} }
mod rope_reader;
pub use rope_reader::RopeReader;
pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice}; pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
// pub use tendril::StrTendril as Tendril; // pub use tendril::StrTendril as Tendril;
@ -66,5 +68,5 @@ pub use syntax::Syntax;
pub use diagnostic::Diagnostic; pub use diagnostic::Diagnostic;
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING}; pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction}; pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};

@ -1,9 +1,9 @@
use crate::{Rope, RopeSlice}; use crate::{Rope, RopeSlice};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::Crlf; pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::Crlf;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::LF; pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::LF;
/// Represents one of the valid Unicode line endings. /// Represents one of the valid Unicode line endings.
#[derive(PartialEq, Eq, Copy, Clone, Debug)] #[derive(PartialEq, Eq, Copy, Clone, Debug)]

@ -1,8 +1,13 @@
use std::iter;
use ropey::RopeSlice;
use tree_sitter::Node; use tree_sitter::Node;
use crate::{Rope, Syntax}; use crate::movement::Direction::{self, Backward, Forward};
use crate::Syntax;
const MAX_PLAINTEXT_SCAN: usize = 10000; const MAX_PLAINTEXT_SCAN: usize = 10000;
const MATCH_LIMIT: usize = 16;
// Limit matching pairs to only ( ) { } [ ] < > ' ' " " // Limit matching pairs to only ( ) { } [ ] < > ' ' " "
const PAIRS: &[(char, char)] = &[ const PAIRS: &[(char, char)] = &[
@ -24,7 +29,7 @@ const PAIRS: &[(char, char)] = &[
/// ///
/// If no matching bracket is found, `None` is returned. /// If no matching bracket is found, `None` is returned.
#[must_use] #[must_use]
pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> { pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> {
if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) { if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
return None; return None;
} }
@ -42,35 +47,84 @@ pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<
// //
// If no surrounding scope is found, the function returns `None`. // If no surrounding scope is found, the function returns `None`.
#[must_use] #[must_use]
pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> { pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> {
find_pair(syntax, doc, pos, true) find_pair(syntax, doc, pos, true)
} }
fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> { fn find_pair(
syntax: &Syntax,
doc: RopeSlice,
pos_: usize,
traverse_parents: bool,
) -> Option<usize> {
let tree = syntax.tree(); let tree = syntax.tree();
let pos = doc.char_to_byte(pos); let pos = doc.char_to_byte(pos_);
let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?; let mut node = tree.root_node().descendant_for_byte_range(pos, pos)?;
loop { loop {
let (start_byte, end_byte) = surrounding_bytes(doc, &node)?; if node.is_named() {
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte)); let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
if is_valid_pair(doc, start_char, end_char) {
if end_byte == pos {
return Some(start_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 || start_byte == pos {
return Some(end_char);
}
}
}
// this node itselt wasn't a pair but maybe its siblings are
if is_valid_pair(doc, start_char, end_char) { // check if we are *on* the pair (special cased so we don't look
if end_byte == pos { // at the current node twice and to jump to the start on that case)
return Some(start_char); if let Some(open) = as_close_pair(doc, &node) {
if let Some(pair_start) = find_pair_end(doc, node.prev_sibling(), open, Backward) {
return Some(pair_start);
} }
// We return the end char if the cursor is either on the start char
// or at some arbitrary position between start and end char.
return Some(end_char);
} }
if traverse_parents { if !traverse_parents {
node = node.parent()?; // check if we are *on* the opening pair (special cased here as
} else { // an opptimization since we only care about bracket on the cursor
return None; // 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 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;
};
node = parent;
} }
let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
if node.child_count() != 0 {
return None;
}
let node_start = doc.byte_to_char(node.start_byte());
find_matching_bracket_plaintext(doc.byte_slice(node.byte_range()), pos_ - node_start)
.map(|pos| pos + node_start)
} }
/// Returns the position of the matching bracket under cursor. /// Returns the position of the matching bracket under cursor.
@ -85,10 +139,7 @@ fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) ->
/// ///
/// If no matching bracket is found, `None` is returned. /// If no matching bracket is found, `None` is returned.
#[must_use] #[must_use]
pub fn find_matching_bracket_current_line_plaintext( pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
doc: &Rope,
cursor_pos: usize,
) -> Option<usize> {
// Don't do anything when the cursor is not on top of a bracket. // Don't do anything when the cursor is not on top of a bracket.
let bracket = doc.char(cursor_pos); let bracket = doc.char(cursor_pos);
if !is_valid_bracket(bracket) { if !is_valid_bracket(bracket) {
@ -144,11 +195,11 @@ fn is_forward_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, _)| *l == c) PAIRS.iter().any(|(l, _)| *l == c)
} }
fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool { fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool {
PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
} }
fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> { fn surrounding_bytes(doc: RopeSlice, node: &Node) -> Option<(usize, usize)> {
let len = doc.len_bytes(); let len = doc.len_bytes();
let start_byte = node.start_byte(); let start_byte = node.start_byte();
@ -161,6 +212,55 @@ fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
Some((start_byte, end_byte)) Some((start_byte, end_byte))
} }
/// Tests if this node is a pair close char and returns the expected open char
fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<char> {
let close = as_char(doc, node)?.1;
PAIRS
.iter()
.find_map(|&(open, close_)| (close_ == close).then_some(open))
}
/// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char
///
/// # Returns
///
/// The position of the found node or `None` otherwise
fn find_pair_end(
doc: RopeSlice,
node: Option<Node>,
end_char: char,
direction: Direction,
) -> Option<usize> {
let advance = match direction {
Forward => Node::next_sibling,
Backward => Node::prev_sibling,
};
iter::successors(node, advance)
.take(MATCH_LIMIT)
.find_map(|node| {
let (pos, c) = as_char(doc, &node)?;
(end_char == c).then_some(pos)
})
}
/// Tests if this node is a pair close char and returns the expected open char
fn as_open_pair(doc: RopeSlice, node: &Node) -> Option<char> {
let open = as_char(doc, node)?.1;
PAIRS
.iter()
.find_map(|&(open_, close)| (open_ == open).then_some(close))
}
/// If node is a single char return it (and its char position)
fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
// TODO: multi char/non ASCII pairs
if node.byte_range().len() != 1 {
return None;
}
let pos = doc.try_byte_to_char(node.start_byte()).ok()?;
Some((pos, doc.char(pos)))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -168,11 +268,11 @@ mod tests {
#[test] #[test]
fn test_find_matching_bracket_current_line_plaintext() { fn test_find_matching_bracket_current_line_plaintext() {
let assert = |input: &str, pos, expected| { let assert = |input: &str, pos, expected| {
let input = &Rope::from(input); let input = RopeSlice::from(input);
let actual = find_matching_bracket_current_line_plaintext(input, pos); let actual = find_matching_bracket_plaintext(input, pos);
assert_eq!(expected, actual.unwrap()); assert_eq!(expected, actual.unwrap());
let actual = find_matching_bracket_current_line_plaintext(input, expected); let actual = find_matching_bracket_plaintext(input, expected);
assert_eq!(pos, actual.unwrap(), "expected symmetrical behaviour"); assert_eq!(pos, actual.unwrap(), "expected symmetrical behaviour");
}; };

@ -1,4 +1,4 @@
use std::iter; use std::{cmp::Reverse, iter};
use ropey::iter::Chars; use ropey::iter::Chars;
use tree_sitter::{Node, QueryCursor}; use tree_sitter::{Node, QueryCursor};
@ -16,7 +16,7 @@ use crate::{
syntax::LanguageConfiguration, syntax::LanguageConfiguration,
text_annotations::TextAnnotations, text_annotations::TextAnnotations,
textobject::TextObject, textobject::TextObject,
visual_offset_from_block, Range, RopeSlice, visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
}; };
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -177,6 +177,10 @@ pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Ran
word_move(slice, range, count, WordMotionTarget::PrevWordStart) word_move(slice, range, count, WordMotionTarget::PrevWordStart)
} }
pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevWordEnd)
}
pub fn move_next_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range { pub fn move_next_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextLongWordStart) word_move(slice, range, count, WordMotionTarget::NextLongWordStart)
} }
@ -189,8 +193,8 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) -
word_move(slice, range, count, WordMotionTarget::PrevLongWordStart) word_move(slice, range, count, WordMotionTarget::PrevLongWordStart)
} }
pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevWordEnd) word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd)
} }
fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range { fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range {
@ -199,6 +203,7 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar
WordMotionTarget::PrevWordStart WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevWordEnd | WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd
); );
// Special-case early-out. // Special-case early-out.
@ -377,6 +382,7 @@ pub enum WordMotionTarget {
NextLongWordStart, NextLongWordStart,
NextLongWordEnd, NextLongWordEnd,
PrevLongWordStart, PrevLongWordStart,
PrevLongWordEnd,
} }
pub trait CharHelpers { pub trait CharHelpers {
@ -393,6 +399,7 @@ impl CharHelpers for Chars<'_> {
WordMotionTarget::PrevWordStart WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevWordEnd | WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd
); );
// Reverse the iterator if needed for the motion direction. // Reverse the iterator if needed for the motion direction.
@ -479,7 +486,7 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
is_word_boundary(prev_ch, next_ch) is_word_boundary(prev_ch, next_ch)
&& (!prev_ch.is_whitespace() || char_is_line_ending(next_ch)) && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch))
} }
WordMotionTarget::NextLongWordStart => { WordMotionTarget::NextLongWordStart | WordMotionTarget::PrevLongWordEnd => {
is_long_word_boundary(prev_ch, next_ch) is_long_word_boundary(prev_ch, next_ch)
&& (char_is_line_ending(next_ch) || !next_ch.is_whitespace()) && (char_is_line_ending(next_ch) || !next_ch.is_whitespace())
} }
@ -520,10 +527,10 @@ pub fn goto_treesitter_object(
let node = match dir { let node = match dir {
Direction::Forward => nodes Direction::Forward => nodes
.filter(|n| n.start_byte() > byte_pos) .filter(|n| n.start_byte() > byte_pos)
.min_by_key(|n| n.start_byte())?, .min_by_key(|n| (n.start_byte(), Reverse(n.end_byte())))?,
Direction::Backward => nodes Direction::Backward => nodes
.filter(|n| n.end_byte() < byte_pos) .filter(|n| n.end_byte() < byte_pos)
.max_by_key(|n| n.end_byte())?, .max_by_key(|n| (n.end_byte(), Reverse(n.start_byte())))?,
}; };
let len = slice.len_bytes(); let len = slice.len_bytes();
@ -549,6 +556,85 @@ pub fn goto_treesitter_object(
last_range last_range
} }
fn find_parent_start(mut node: Node) -> Option<Node> {
let start = node.start_byte();
while node.start_byte() >= start || !node.is_named() {
node = node.parent()?;
}
Some(node)
}
pub fn move_parent_node_end(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
dir: Direction,
movement: Movement,
) -> Selection {
let tree = syntax.tree();
selection.transform(|range| {
let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to());
let mut node = match tree
.root_node()
.named_descendant_for_byte_range(start_from, start_to)
{
Some(node) => node,
None => {
log::debug!(
"no descendant found for byte range: {} - {}",
start_from,
start_to
);
return range;
}
};
let mut end_head = match dir {
// moving forward, we always want to move one past the end of the
// current node, so use the end byte of the current node, which is an exclusive
// end of the range
Direction::Forward => text.byte_to_char(node.end_byte()),
// moving backward, we want the cursor to land on the start char of
// the current node, or if it is already at the start of a node, to traverse up to
// the parent
Direction::Backward => {
let end_head = text.byte_to_char(node.start_byte());
// if we're already on the beginning, look up to the parent
if end_head == range.cursor(text) {
node = find_parent_start(node).unwrap_or(node);
text.byte_to_char(node.start_byte())
} else {
end_head
}
}
};
if movement == Movement::Move {
// preserve direction of original range
if range.direction() == Direction::Forward {
Range::new(end_head, end_head + 1)
} else {
Range::new(end_head + 1, end_head)
}
} else {
// if we end up with a forward range, then adjust it to be one past
// where we want
if end_head >= range.anchor {
end_head += 1;
}
Range::new(range.anchor, end_head)
}
})
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use ropey::Rope; use ropey::Rope;
@ -1445,6 +1531,100 @@ mod test {
} }
} }
#[test]
fn test_behaviour_when_moving_to_end_of_prev_long_words() {
let tests = [
(
"Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))],
),
("Starting from after boundary retreats the anchor",
vec![(1, Range::new(0, 9), Range::new(8, 0))],
),
(
"Jump to end of a word succeeded by whitespace",
vec![(1, Range::new(10, 10), Range::new(10, 4))],
),
(
" Jump to start of line from end of word preceded by whitespace",
vec![(1, Range::new(3, 4), Range::new(4, 0))],
),
("Previous anchor is irrelevant for backward motions",
vec![(1, Range::new(12, 5), Range::new(6, 0))]),
(
" Starting from whitespace moves to first space in sequence",
vec![(1, Range::new(0, 4), Range::new(4, 0))],
),
("Identifiers_with_underscores are considered a single word",
vec![(1, Range::new(0, 20), Range::new(20, 0))]),
(
"Jumping\n \nback through a newline selects whitespace",
vec![(1, Range::new(0, 13), Range::new(12, 8))],
),
(
"Jumping to start of word from the end selects the word",
vec![(1, Range::new(6, 7), Range::new(7, 0))],
),
(
"alphanumeric.!,and.?=punctuation are treated exactly the same",
vec![(1, Range::new(29, 30), Range::new(30, 0))],
),
(
"... ... punctuation and spaces behave as expected",
vec![
(1, Range::new(0, 10), Range::new(9, 3)),
(1, Range::new(10, 6), Range::new(7, 3)),
],
),
(".._.._ punctuation is joined by underscores into a single block",
vec![(1, Range::new(0, 6), Range::new(6, 0))]),
(
"Newlines\n\nare bridged seamlessly.",
vec![(1, Range::new(0, 10), Range::new(8, 0))],
),
(
"Jumping \n\n\n\n\nback from within a newline group selects previous block",
vec![(1, Range::new(0, 13), Range::new(11, 7))],
),
(
"Failed motions do not modify the range",
vec![(0, Range::new(3, 0), Range::new(3, 0))],
),
(
"Multiple motions at once resolve correctly",
vec![(3, Range::new(19, 19), Range::new(8, 0))],
),
(
"Excessive motions are performed partially",
vec![(999, Range::new(40, 40), Range::new(9, 0))],
),
(
"", // Edge case of moving backwards in empty string
vec![(1, Range::new(0, 0), Range::new(0, 0))],
),
(
"\n\n\n\n\n", // Edge case of moving backwards in all newlines
vec![(1, Range::new(5, 5), Range::new(0, 0))],
),
(" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
vec![
(1, Range::new(0, 8), Range::new(7, 4)),
(1, Range::new(7, 4), Range::new(3, 0)),
]),
("ヒーリ..クス multibyte characters behave as normal characters, including when interacting with punctuation",
vec![
(1, Range::new(0, 8), Range::new(7, 0)),
]),
];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_prev_long_word_end(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test] #[test]
fn test_behaviour_when_moving_to_prev_paragraph_single() { fn test_behaviour_when_moving_to_prev_paragraph_single() {
let tests = [ let tests = [

@ -85,23 +85,21 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
/// ///
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify /// This function is used instead of `std::fs::canonicalize` because we don't want to verify
/// here if the path exists, just normalize it's components. /// here if the path exists, just normalize it's components.
pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> { pub fn get_canonicalized_path(path: &Path) -> PathBuf {
let path = expand_tilde(path); let path = expand_tilde(path);
let path = if path.is_relative() { let path = if path.is_relative() {
std::env::current_dir().map(|current_dir| current_dir.join(path))? helix_loader::current_working_dir().join(path)
} else { } else {
path path
}; };
Ok(get_normalized_path(path.as_path())) get_normalized_path(path.as_path())
} }
pub fn get_relative_path(path: &Path) -> PathBuf { pub fn get_relative_path(path: &Path) -> PathBuf {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let path = if path.is_absolute() { let path = if path.is_absolute() {
let cwdir = std::env::current_dir() let cwdir = get_normalized_path(&helix_loader::current_working_dir());
.map(|path| get_normalized_path(&path))
.expect("couldn't determine current directory");
get_normalized_path(&path) get_normalized_path(&path)
.strip_prefix(cwdir) .strip_prefix(cwdir)
.map(PathBuf::from) .map(PathBuf::from)
@ -142,7 +140,7 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
/// ``` /// ```
/// ///
pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf { pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
let cwd = std::env::current_dir().unwrap_or_default(); let cwd = helix_loader::current_working_dir();
let path = path let path = path
.as_ref() .as_ref()
.strip_prefix(cwd) .strip_prefix(cwd)

@ -1,89 +0,0 @@
use std::collections::HashMap;
#[derive(Debug)]
pub struct Register {
name: char,
values: Vec<String>,
}
impl Register {
pub const fn new(name: char) -> Self {
Self {
name,
values: Vec::new(),
}
}
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
Self { name, values }
}
pub const fn name(&self) -> char {
self.name
}
pub fn read(&self) -> &[String] {
&self.values
}
pub fn write(&mut self, values: Vec<String>) {
self.values = values;
}
pub fn push(&mut self, value: String) {
self.values.push(value);
}
}
/// Currently just wraps a `HashMap` of `Register`s
#[derive(Debug, Default)]
pub struct Registers {
inner: HashMap<char, Register>,
}
impl Registers {
pub fn get(&self, name: char) -> Option<&Register> {
self.inner.get(&name)
}
pub fn read(&self, name: char) -> Option<&[String]> {
self.get(name).map(|reg| reg.read())
}
pub fn write(&mut self, name: char, values: Vec<String>) {
if name != '_' {
self.inner
.insert(name, Register::new_with_values(name, values));
}
}
pub fn push(&mut self, name: char, value: String) {
if name != '_' {
if let Some(r) = self.inner.get_mut(&name) {
r.push(value);
} else {
self.write(name, vec![value]);
}
}
}
pub fn first(&self, name: char) -> Option<&String> {
self.read(name).and_then(|entries| entries.first())
}
pub fn last(&self, name: char) -> Option<&String> {
self.read(name).and_then(|entries| entries.last())
}
pub fn inner(&self) -> &HashMap<char, Register> {
&self.inner
}
pub fn clear(&mut self) {
self.inner.clear();
}
pub fn remove(&mut self, name: char) -> Option<Register> {
self.inner.remove(&name)
}
}

@ -0,0 +1,37 @@
use std::io;
use ropey::iter::Chunks;
use ropey::RopeSlice;
pub struct RopeReader<'a> {
current_chunk: &'a [u8],
chunks: Chunks<'a>,
}
impl<'a> RopeReader<'a> {
pub fn new(rope: RopeSlice<'a>) -> RopeReader<'a> {
RopeReader {
current_chunk: &[],
chunks: rope.chunks(),
}
}
}
impl io::Read for RopeReader<'_> {
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
let buf_len = buf.len();
loop {
let read_bytes = self.current_chunk.read(buf)?;
buf = &mut buf[read_bytes..];
if buf.is_empty() {
return Ok(buf_len);
}
if let Some(next_chunk) = self.chunks.next() {
self.current_chunk = next_chunk.as_bytes();
} else {
return Ok(buf_len - buf.len());
}
}
}
}

@ -161,34 +161,35 @@ impl Range {
self.from() <= pos && pos < self.to() self.from() <= pos && pos < self.to()
} }
/// Map a range through a set of changes. Returns a new range representing the same position /// Map a range through a set of changes. Returns a new range representing
/// after the changes are applied. /// the same position after the changes are applied. Note that this
pub fn map(self, changes: &ChangeSet) -> Self { /// function runs in O(N) (N is number of changes) and can therefore
/// cause performance problems if run for a large number of ranges as the
/// complexity is then O(MN) (for multicuror M=N usually). Instead use
/// [Selection::map] or [ChangeSet::update_positions] instead
pub fn map(mut self, changes: &ChangeSet) -> Self {
use std::cmp::Ordering; use std::cmp::Ordering;
let (anchor, head) = match self.anchor.cmp(&self.head) { if changes.is_empty() {
Ordering::Equal => ( return self;
changes.map_pos(self.anchor, Assoc::After),
changes.map_pos(self.head, Assoc::After),
),
Ordering::Less => (
changes.map_pos(self.anchor, Assoc::After),
changes.map_pos(self.head, Assoc::Before),
),
Ordering::Greater => (
changes.map_pos(self.anchor, Assoc::Before),
changes.map_pos(self.head, Assoc::After),
),
};
// We want to return a new `Range` with `horiz == None` every time,
// even if the anchor and head haven't changed, because we don't
// know if the *visual* position hasn't changed due to
// character-width or grapheme changes earlier in the text.
Self {
anchor,
head,
old_visual_position: None,
} }
let positions_to_map = match self.anchor.cmp(&self.head) {
Ordering::Equal => [
(&mut self.anchor, Assoc::After),
(&mut self.head, Assoc::After),
],
Ordering::Less => [
(&mut self.anchor, Assoc::After),
(&mut self.head, Assoc::Before),
],
Ordering::Greater => [
(&mut self.head, Assoc::After),
(&mut self.anchor, Assoc::Before),
],
};
changes.update_positions(positions_to_map.into_iter());
self.old_visual_position = None;
self
} }
/// Extend the range to cover at least `from` `to`. /// Extend the range to cover at least `from` `to`.
@ -451,17 +452,36 @@ impl Selection {
/// Map selections over a set of changes. Useful for adjusting the selection position after /// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document. /// applying changes to a document.
pub fn map(self, changes: &ChangeSet) -> Self { pub fn map(self, changes: &ChangeSet) -> Self {
self.map_no_normalize(changes).normalize()
}
/// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document. Doesn't normalize the selection
pub fn map_no_normalize(mut self, changes: &ChangeSet) -> Self {
if changes.is_empty() { if changes.is_empty() {
return self; return self;
} }
Self::new( let positions_to_map = self.ranges.iter_mut().flat_map(|range| {
self.ranges use std::cmp::Ordering;
.into_iter() range.old_visual_position = None;
.map(|range| range.map(changes)) match range.anchor.cmp(&range.head) {
.collect(), Ordering::Equal => [
self.primary_index, (&mut range.anchor, Assoc::After),
) (&mut range.head, Assoc::After),
],
Ordering::Less => [
(&mut range.anchor, Assoc::After),
(&mut range.head, Assoc::Before),
],
Ordering::Greater => [
(&mut range.head, Assoc::After),
(&mut range.anchor, Assoc::Before),
],
}
});
changes.update_positions(positions_to_map);
self
} }
pub fn ranges(&self) -> &[Range] { pub fn ranges(&self) -> &[Range] {
@ -497,6 +517,9 @@ impl Selection {
/// Normalizes a `Selection`. /// Normalizes a `Selection`.
fn normalize(mut self) -> Self { fn normalize(mut self) -> Self {
if self.len() < 2 {
return self;
}
let mut primary = self.ranges[self.primary_index]; let mut primary = self.ranges[self.primary_index];
self.ranges.sort_unstable_by_key(Range::from); self.ranges.sort_unstable_by_key(Range::from);
@ -561,17 +584,12 @@ impl Selection {
assert!(!ranges.is_empty()); assert!(!ranges.is_empty());
debug_assert!(primary_index < ranges.len()); debug_assert!(primary_index < ranges.len());
let mut selection = Self { let selection = Self {
ranges, ranges,
primary_index, primary_index,
}; };
if selection.ranges.len() > 1 { selection.normalize()
// TODO: only normalize if needed (any ranges out of order)
selection = selection.normalize();
}
selection
} }
/// Takes a closure and maps each `Range` over the closure. /// Takes a closure and maps each `Range` over the closure.
@ -612,11 +630,19 @@ impl Selection {
self.transform(|range| Range::point(range.cursor(text))) self.transform(|range| Range::point(range.cursor(text)))
} }
pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a { pub fn fragments<'a>(
&'a self,
text: RopeSlice<'a>,
) -> impl DoubleEndedIterator<Item = Cow<'a, str>> + ExactSizeIterator<Item = Cow<str>> + 'a
{
self.ranges.iter().map(move |range| range.fragment(text)) self.ranges.iter().map(move |range| range.fragment(text))
} }
pub fn slices<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = RopeSlice> + 'a { pub fn slices<'a>(
&'a self,
text: RopeSlice<'a>,
) -> impl DoubleEndedIterator<Item = RopeSlice<'a>> + ExactSizeIterator<Item = RopeSlice<'a>> + 'a
{
self.ranges.iter().map(move |range| range.slice(text)) self.ranges.iter().map(move |range| range.slice(text))
} }

@ -4,7 +4,7 @@ use crate::{
diagnostic::Severity, diagnostic::Severity,
regex::Regex, regex::Regex,
transaction::{ChangeSet, Operation}, transaction::{ChangeSet, Operation},
Rope, RopeSlice, Tendril, RopeSlice, Tendril,
}; };
use ahash::RandomState; use ahash::RandomState;
@ -48,6 +48,21 @@ where
.transpose() .transpose()
} }
fn deserialize_tab_width<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
usize::deserialize(deserializer).and_then(|n| {
if n > 0 && n <= 16 {
Ok(n)
} else {
Err(serde::de::Error::custom(
"tab width must be a value from 1 to 16 inclusive",
))
}
})
}
pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result<Option<AutoPairs>, D::Error> pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result<Option<AutoPairs>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -424,6 +439,7 @@ pub struct DebuggerQuirks {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct IndentationConfiguration { pub struct IndentationConfiguration {
#[serde(deserialize_with = "deserialize_tab_width")]
pub tab_width: usize, pub tab_width: usize,
pub unit: String, pub unit: String,
} }
@ -802,7 +818,10 @@ impl Loader {
// TODO: content_regex handling conflict resolution // TODO: content_regex handling conflict resolution
} }
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> { pub fn language_config_for_shebang(
&self,
source: RopeSlice,
) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0)); let line = Cow::from(source.line(0));
static SHEBANG_REGEX: Lazy<Regex> = static SHEBANG_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(&["^", SHEBANG].concat()).unwrap()); Lazy::new(|| Regex::new(&["^", SHEBANG].concat()).unwrap());
@ -912,7 +931,7 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
impl Syntax { impl Syntax {
pub fn new( pub fn new(
source: &Rope, source: RopeSlice,
config: Arc<HighlightConfiguration>, config: Arc<HighlightConfiguration>,
loader: Arc<Loader>, loader: Arc<Loader>,
) -> Option<Self> { ) -> Option<Self> {
@ -951,8 +970,8 @@ impl Syntax {
pub fn update( pub fn update(
&mut self, &mut self,
old_source: &Rope, old_source: RopeSlice,
source: &Rope, source: RopeSlice,
changeset: &ChangeSet, changeset: &ChangeSet,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut queue = VecDeque::new(); let mut queue = VecDeque::new();
@ -1119,12 +1138,38 @@ impl Syntax {
layer.tree().root_node(), layer.tree().root_node(),
RopeProvider(source_slice), RopeProvider(source_slice),
); );
let mut combined_injections = vec![
(None, Vec::new(), IncludedChildren::default());
layer.config.combined_injections_patterns.len()
];
let mut injections = Vec::new(); let mut injections = Vec::new();
let mut last_injection_end = 0;
for mat in matches { for mat in matches {
let (injection_capture, content_node, included_children) = layer let (injection_capture, content_node, included_children) = layer
.config .config
.injection_for_match(&layer.config.injections_query, &mat, source_slice); .injection_for_match(&layer.config.injections_query, &mat, source_slice);
// in case this is a combined injection save it for more processing later
if let Some(combined_injection_idx) = layer
.config
.combined_injections_patterns
.iter()
.position(|&pattern| pattern == mat.pattern_index)
{
let entry = &mut combined_injections[combined_injection_idx];
if injection_capture.is_some() {
entry.0 = injection_capture;
}
if let Some(content_node) = content_node {
if content_node.start_byte() >= last_injection_end {
entry.1.push(content_node);
last_injection_end = content_node.end_byte();
}
}
entry.2 = included_children;
continue;
}
// Explicitly remove this match so that none of its other captures will remain // Explicitly remove this match so that none of its other captures will remain
// in the stream of captures. // in the stream of captures.
mat.remove(); mat.remove();
@ -1139,49 +1184,23 @@ impl Syntax {
intersect_ranges(&layer.ranges, &[content_node], included_children); intersect_ranges(&layer.ranges, &[content_node], included_children);
if !ranges.is_empty() { if !ranges.is_empty() {
if content_node.start_byte() < last_injection_end {
continue;
}
last_injection_end = content_node.end_byte();
injections.push((config, ranges)); injections.push((config, ranges));
} }
} }
} }
} }
// Process combined injections. for (lang_name, content_nodes, included_children) in combined_injections {
if let Some(combined_injections_query) = &layer.config.combined_injections_query { if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
let mut injections_by_pattern_index = if let Some(config) = (injection_callback)(&lang_name) {
vec![ let ranges =
(None, Vec::new(), IncludedChildren::default()); intersect_ranges(&layer.ranges, &content_nodes, included_children);
combined_injections_query.pattern_count() if !ranges.is_empty() {
]; injections.push((config, ranges));
let matches = cursor.matches(
combined_injections_query,
layer.tree().root_node(),
RopeProvider(source_slice),
);
for mat in matches {
let entry = &mut injections_by_pattern_index[mat.pattern_index];
let (injection_capture, content_node, included_children) = layer
.config
.injection_for_match(combined_injections_query, &mat, source_slice);
if injection_capture.is_some() {
entry.0 = injection_capture;
}
if let Some(content_node) = content_node {
entry.1.push(content_node);
}
entry.2 = included_children;
}
for (lang_name, content_nodes, included_children) in injections_by_pattern_index
{
if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
if let Some(config) = (injection_callback)(&lang_name) {
let ranges = intersect_ranges(
&layer.ranges,
&content_nodes,
included_children,
);
if !ranges.is_empty() {
injections.push((config, ranges));
}
} }
} }
} }
@ -1371,7 +1390,7 @@ impl LanguageLayer {
self.tree.as_ref().unwrap() self.tree.as_ref().unwrap()
} }
fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> { fn parse(&mut self, parser: &mut Parser, source: RopeSlice) -> Result<(), Error> {
parser parser
.set_included_ranges(&self.ranges) .set_included_ranges(&self.ranges)
.map_err(|_| Error::InvalidRanges)?; .map_err(|_| Error::InvalidRanges)?;
@ -1386,7 +1405,7 @@ impl LanguageLayer {
&mut |byte, _| { &mut |byte, _| {
if byte <= source.len_bytes() { if byte <= source.len_bytes() {
let (chunk, start_byte, _, _) = source.chunk_at_byte(byte); let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
chunk[byte - start_byte..].as_bytes() &chunk.as_bytes()[byte - start_byte..]
} else { } else {
// out of range // out of range
&[] &[]
@ -1402,7 +1421,7 @@ impl LanguageLayer {
} }
pub(crate) fn generate_edits( pub(crate) fn generate_edits(
old_text: &Rope, old_text: RopeSlice,
changeset: &ChangeSet, changeset: &ChangeSet,
) -> Vec<tree_sitter::InputEdit> { ) -> Vec<tree_sitter::InputEdit> {
use Operation::*; use Operation::*;
@ -1418,7 +1437,7 @@ pub(crate) fn generate_edits(
// TODO; this is a lot easier with Change instead of Operation. // TODO; this is a lot easier with Change instead of Operation.
fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) { fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) {
let byte = text.char_to_byte(pos); // <- attempted to index past end let byte = text.char_to_byte(pos); // <- attempted to index past end
let line = text.char_to_line(pos); let line = text.char_to_line(pos);
let line_start_byte = text.line_to_byte(line); let line_start_byte = text.line_to_byte(line);
@ -1544,7 +1563,7 @@ pub struct HighlightConfiguration {
pub language: Grammar, pub language: Grammar,
pub query: Query, pub query: Query,
injections_query: Query, injections_query: Query,
combined_injections_query: Option<Query>, combined_injections_patterns: Vec<usize>,
highlights_pattern_index: usize, highlights_pattern_index: usize,
highlight_indices: ArcSwap<Vec<Option<Highlight>>>, highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>, non_local_variable_patterns: Vec<bool>,
@ -1595,7 +1614,7 @@ impl<'a> Iterator for ChunksBytes<'a> {
} }
pub struct RopeProvider<'a>(pub RopeSlice<'a>); pub struct RopeProvider<'a>(pub RopeSlice<'a>);
impl<'a> TextProvider<'a> for RopeProvider<'a> { impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
type I = ChunksBytes<'a>; type I = ChunksBytes<'a>;
fn text(&mut self, node: Node) -> Self::I { fn text(&mut self, node: Node) -> Self::I {
@ -1609,7 +1628,7 @@ impl<'a> TextProvider<'a> for RopeProvider<'a> {
struct HighlightIterLayer<'a> { struct HighlightIterLayer<'a> {
_tree: Option<Tree>, _tree: Option<Tree>,
cursor: QueryCursor, cursor: QueryCursor,
captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>>, captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>>,
config: &'a HighlightConfiguration, config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>, highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>, scope_stack: Vec<LocalScope<'a>>,
@ -1660,26 +1679,15 @@ impl HighlightConfiguration {
} }
} }
let mut injections_query = Query::new(language, injection_query)?; let injections_query = Query::new(language, injection_query)?;
let combined_injections_patterns = (0..injections_query.pattern_count())
// Construct a separate query just for dealing with the 'combined injections'. .filter(|&i| {
// Disable the combined injection patterns in the main query. injections_query
let mut combined_injections_query = Query::new(language, injection_query)?; .property_settings(i)
let mut has_combined_queries = false; .iter()
for pattern_index in 0..injections_query.pattern_count() { .any(|s| &*s.key == "injection.combined")
let settings = injections_query.property_settings(pattern_index); })
if settings.iter().any(|s| &*s.key == "injection.combined") { .collect();
has_combined_queries = true;
injections_query.disable_pattern(pattern_index);
} else {
combined_injections_query.disable_pattern(pattern_index);
}
}
let combined_injections_query = if has_combined_queries {
Some(combined_injections_query)
} else {
None
};
// Find all of the highlighting patterns that are disabled for nodes that // Find all of the highlighting patterns that are disabled for nodes that
// have been identified as local variables. // have been identified as local variables.
@ -1728,7 +1736,7 @@ impl HighlightConfiguration {
language, language,
query, query,
injections_query, injections_query,
combined_injections_query, combined_injections_patterns,
highlights_pattern_index, highlights_pattern_index,
highlight_indices, highlight_indices,
non_local_variable_patterns, non_local_variable_patterns,
@ -2524,7 +2532,7 @@ mod test {
let mut cursor = QueryCursor::new(); let mut cursor = QueryCursor::new();
let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap(); let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let root = syntax.tree().root_node(); let root = syntax.tree().root_node();
let mut test = |capture, range| { let mut test = |capture, range| {
@ -2598,7 +2606,7 @@ mod test {
fn main() {} fn main() {}
", ",
); );
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap(); let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let tree = syntax.tree(); let tree = syntax.tree();
let root = tree.root_node(); let root = tree.root_node();
assert_eq!(root.kind(), "source_file"); assert_eq!(root.kind(), "source_file");
@ -2625,7 +2633,7 @@ mod test {
&doc, &doc,
vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(), vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
); );
let edits = generate_edits(&doc, transaction.changes()); let edits = generate_edits(doc.slice(..), transaction.changes());
// transaction.apply(&mut state); // transaction.apply(&mut state);
assert_eq!( assert_eq!(
@ -2654,7 +2662,7 @@ mod test {
let mut doc = Rope::from("fn test() {}"); let mut doc = Rope::from("fn test() {}");
let transaction = let transaction =
Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter()); Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
let edits = generate_edits(&doc, transaction.changes()); let edits = generate_edits(doc.slice(..), transaction.changes());
transaction.apply(&mut doc); transaction.apply(&mut doc);
assert_eq!(doc, "fn test(a: u32) {}"); assert_eq!(doc, "fn test(a: u32) {}");
@ -2688,7 +2696,7 @@ mod test {
let language = get_language(language_name).unwrap(); let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap(); let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let root = syntax let root = syntax
.tree() .tree()

@ -1,7 +1,8 @@
use ropey::RopeSlice;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{Range, Rope, Selection, Tendril}; use crate::{Range, Rope, Selection, Tendril};
use std::borrow::Cow; use std::{borrow::Cow, iter::once};
/// (from, to, replacement) /// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>); pub type Change = (usize, usize, Option<Tendril>);
@ -42,7 +43,7 @@ impl ChangeSet {
} }
#[must_use] #[must_use]
pub fn new(doc: &Rope) -> Self { pub fn new(doc: RopeSlice) -> Self {
let len = doc.len_chars(); let len = doc.len_chars();
Self { Self {
changes: Vec::new(), changes: Vec::new(),
@ -326,20 +327,75 @@ impl ChangeSet {
self.changes.is_empty() || self.changes == [Operation::Retain(self.len)] self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
} }
/// Map a position through the changes. /// Map a (mostly) *sorted* list of positions through the changes.
/// ///
/// `assoc` indicates which size to associate the position with. `Before` will keep the /// This is equivalent to updating each position with `map_pos`:
/// position close to the character before, and will place it before insertions over that ///
/// range, or at that point. `After` will move it forward, placing it at the end of such /// ``` no-compile
/// insertions. /// for (pos, assoc) in positions {
pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize { /// *pos = changes.map_pos(*pos, assoc);
/// }
/// ```
/// However this function is significantly faster for sorted lists running
/// in `O(N+M)` instead of `O(NM)`. This function also handles unsorted/
/// partially sorted lists. However, in that case worst case complexity is
/// again `O(MN)`. For lists that are often/mostly sorted (like the end of diagnostic ranges)
/// performance is usally close to `O(N + M)`
pub fn update_positions<'a>(&self, positions: impl Iterator<Item = (&'a mut usize, Assoc)>) {
use Operation::*; use Operation::*;
let mut positions = positions.peekable();
let mut old_pos = 0; let mut old_pos = 0;
let mut new_pos = 0; let mut new_pos = 0;
let mut iter = self.changes.iter().enumerate().peekable();
'outer: loop {
macro_rules! map {
($map: expr, $i: expr) => {
loop {
let Some((pos, assoc)) = positions.peek_mut() else { return; };
if **pos < old_pos {
// Positions are not sorted, revert to the last Operation that
// contains this position and continue iterating from there.
// We can unwrap here since `pos` can not be negative
// (unsigned integer) and iterating backwards to the start
// should always move us back to the start
for (i, change) in self.changes[..$i].iter().enumerate().rev() {
match change {
Retain(i) => {
old_pos -= i;
new_pos -= i;
}
Delete(i) => {
old_pos -= i;
}
Insert(ins) => {
new_pos -= ins.chars().count();
}
}
if old_pos <= **pos {
iter = self.changes[i..].iter().enumerate().peekable();
}
}
debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works");
continue 'outer;
}
let Some(new_pos) = $map(**pos, *assoc) else { break; };
**pos = new_pos;
positions.next();
}
};
}
let mut iter = self.changes.iter().peekable(); let Some((i, change)) = iter.next() else {
map!(
|pos, _| (old_pos == pos).then_some(new_pos),
self.changes.len()
);
break;
};
while let Some(change) = iter.next() {
let len = match change { let len = match change {
Delete(i) | Retain(i) => *i, Delete(i) | Retain(i) => *i,
Insert(_) => 0, Insert(_) => 0,
@ -348,46 +404,51 @@ impl ChangeSet {
match change { match change {
Retain(_) => { Retain(_) => {
if old_end > pos { map!(
return new_pos + (pos - old_pos); |pos, _| (old_end > pos).then_some(new_pos + (pos - old_pos)),
} i
);
new_pos += len; new_pos += len;
} }
Delete(_) => { Delete(_) => {
// in range // in range
if old_end > pos { map!(|pos, _| (old_end > pos).then_some(new_pos), i);
return new_pos;
}
} }
Insert(s) => { Insert(s) => {
let ins = s.chars().count(); let ins = s.chars().count();
// a subsequent delete means a replace, consume it // a subsequent delete means a replace, consume it
if let Some(Delete(len)) = iter.peek() { if let Some((_, Delete(len))) = iter.peek() {
iter.next(); iter.next();
old_end = old_pos + len; old_end = old_pos + len;
// in range of replaced text // in range of replaced text
if old_end > pos { map!(
// at point or tracking before |pos, assoc| (old_end > pos).then(|| {
if pos == old_pos || assoc == Assoc::Before { // at point or tracking before
return new_pos; if pos == old_pos || assoc == Assoc::Before {
} else { new_pos
// place to end of insert } else {
return new_pos + ins; // place to end of insert
} new_pos + ins
} }
}),
i
);
} else { } else {
// at insert point // at insert point
if old_pos == pos { map!(
// return position before inserted text |pos, assoc| (old_pos == pos).then(|| {
if assoc == Assoc::Before { // return position before inserted text
return new_pos; if assoc == Assoc::Before {
} else { new_pos
// after text } else {
return new_pos + ins; // after text
} new_pos + ins
} }
}),
i
);
} }
new_pos += ins; new_pos += ins;
@ -395,14 +456,20 @@ impl ChangeSet {
} }
old_pos = old_end; old_pos = old_end;
} }
let out_of_bounds: Vec<_> = positions.collect();
if pos > old_pos { panic!("Positions {out_of_bounds:?} are out of range for changeset len {old_pos}!",)
panic!( }
"Position {} is out of range for changeset len {}!",
pos, old_pos /// Map a position through the changes.
) ///
} /// `assoc` indicates which side to associate the position with. `Before` will keep the
new_pos /// position close to the character before, and will place it before insertions over that
/// range, or at that point. `After` will move it forward, placing it at the end of such
/// insertions.
pub fn map_pos(&self, mut pos: usize, assoc: Assoc) -> usize {
self.update_positions(once((&mut pos, assoc)));
pos
} }
pub fn changes_iter(&self) -> ChangeIterator { pub fn changes_iter(&self) -> ChangeIterator {
@ -422,7 +489,7 @@ impl Transaction {
/// Create a new, empty transaction. /// Create a new, empty transaction.
pub fn new(doc: &Rope) -> Self { pub fn new(doc: &Rope) -> Self {
Self { Self {
changes: ChangeSet::new(doc), changes: ChangeSet::new(doc.slice(..)),
selection: None, selection: None,
} }
} }
@ -803,6 +870,20 @@ mod test {
}; };
assert_eq!(cs.map_pos(2, Assoc::Before), 2); assert_eq!(cs.map_pos(2, Assoc::Before), 2);
assert_eq!(cs.map_pos(2, Assoc::After), 2); assert_eq!(cs.map_pos(2, Assoc::After), 2);
// unsorted selection
let cs = ChangeSet {
changes: vec![
Insert("ab".into()),
Delete(2),
Insert("cd".into()),
Delete(2),
],
len: 4,
len_after: 4,
};
let mut positions = [4, 2];
cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
assert_eq!(positions, [4, 2]);
} }
#[test] #[test]
@ -869,9 +950,9 @@ mod test {
#[test] #[test]
fn combine_with_empty() { fn combine_with_empty() {
let empty = Rope::from(""); let empty = Rope::from("");
let a = ChangeSet::new(&empty); let a = ChangeSet::new(empty.slice(..));
let mut b = ChangeSet::new(&empty); let mut b = ChangeSet::new(empty.slice(..));
b.insert("a".into()); b.insert("a".into());
let changes = a.compose(b); let changes = a.compose(b);
@ -885,9 +966,9 @@ mod test {
const TEST_CASE: &str = "Hello, これはヘリックスエディターです!"; const TEST_CASE: &str = "Hello, これはヘリックスエディターです!";
let empty = Rope::from(""); let empty = Rope::from("");
let a = ChangeSet::new(&empty); let a = ChangeSet::new(empty.slice(..));
let mut b = ChangeSet::new(&empty); let mut b = ChangeSet::new(empty.slice(..));
b.insert(TEST_CASE.into()); b.insert(TEST_CASE.into());
let changes = a.compose(b); let changes = a.compose(b);

@ -0,0 +1,48 @@
std::vector<std::string>
fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
char* parm5, bool parm6);
std::vector<std::string>
fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
char* parm5, bool parm6) {
auto lambda = []() {
return 0;
};
auto lambda_with_a_really_long_name_that_uses_a_whole_line
= [](int some_more_aligned_parameters,
std::string parm2) {
do_smth();
};
if (brace_on_same_line) {
do_smth();
} else if (brace_on_next_line)
{
do_smth();
} else if (another_condition) {
do_smth();
}
else {
do_smth();
}
if (inline_if_statement)
do_smth();
if (another_inline_if_statement)
return [](int parm1, char* parm2) {
this_is_a_really_pointless_lambda();
};
switch (var) {
case true:
return -1;
case false:
return 42;
}
}
class MyClass : public MyBaseClass {
public:
MyClass();
void public_fn();
private:
super_secret_private_fn();
}

@ -1 +0,0 @@
../../../src/indent.rs

@ -11,3 +11,16 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "rust" name = "rust"
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" } source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
[[language]]
name = "cpp"
scope = "source.cpp"
injection-regex = "cpp"
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"]
roots = []
comment-token = "//"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "cpp"
source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "2d2c4aee8672af4c7c8edff68e7dd4c07e88d2b1" }

@ -1,20 +1,122 @@
use helix_core::{ use helix_core::{
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle}, indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::Loader, syntax::{Configuration, Loader},
Syntax, Syntax,
}; };
use std::path::PathBuf; use ropey::Rope;
use std::{ops::Range, path::PathBuf, process::Command};
#[test] #[test]
fn test_treesitter_indent_rust() { fn test_treesitter_indent_rust() {
test_treesitter_indent("rust.rs", "source.rust"); standard_treesitter_test("rust.rs", "source.rust");
} }
#[test]
fn test_treesitter_indent_cpp() {
standard_treesitter_test("cpp.cpp", "source.cpp");
}
#[test] #[test]
fn test_treesitter_indent_rust_2() { fn test_treesitter_indent_rust_helix() {
test_treesitter_indent("indent.rs", "source.rust"); // We pin a specific git revision to prevent unrelated changes from causing the indent tests to fail.
// TODO Use commands.rs as indentation test. // Ideally, someone updates this once in a while and fixes any errors that occur.
// Currently this fails because we can't align the parameters of a closure yet let rev = "af382768cdaf89ff547dbd8f644a1bddd90e7c8f";
// test_treesitter_indent("commands.rs", "source.rust"); let files = Command::new("git")
.args([
"ls-tree",
"-r",
"--name-only",
"--full-tree",
rev,
"helix-term/src",
])
.output()
.unwrap();
let files = String::from_utf8(files.stdout).unwrap();
let ignored_files = vec![
// Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting
"helix-term/src/health.rs",
];
for file in files.split_whitespace() {
if ignored_files.contains(&file) {
continue;
}
let ignored_lines: Vec<Range<usize>> = match file {
"helix-term/src/application.rs" => vec![
// We can't handle complicated indent rules inside macros (`json!` in this case) since
// the tree-sitter grammar only parses them as `token_tree` and `identifier` nodes.
1045..1051,
],
"helix-term/src/commands.rs" => vec![
// This is broken because of the current handling of `call_expression`
// (i.e. having an indent query for it but outdenting again in specific cases).
// The indent query is needed to correctly handle multi-line arguments in function calls
// inside indented `field_expression` nodes (which occurs fairly often).
//
// Once we have the `@indent.always` capture type, it might be possible to just have an indent
// capture for the `arguments` field of a call expression. That could enable us to correctly
// handle this.
2226..2230,
],
"helix-term/src/commands/dap.rs" => vec![
// Complex `format!` macro
46..52,
],
"helix-term/src/commands/lsp.rs" => vec![
// Macro
624..627,
// Return type declaration of a closure. `cargo fmt` adds an additional space here,
// which we cannot (yet) model with our indent queries.
878..879,
// Same as in `helix-term/src/commands.rs`
1335..1343,
],
"helix-term/src/config.rs" => vec![
// Multiline string
146..152,
],
"helix-term/src/keymap.rs" => vec![
// Complex macro (see above)
456..470,
// Multiline string without indent
563..567,
],
"helix-term/src/main.rs" => vec![
// Multiline string
44..70,
],
"helix-term/src/ui/completion.rs" => vec![
// Macro
218..232,
],
"helix-term/src/ui/editor.rs" => vec![
// The chained function calls here are not indented, probably because of the comment
// in between. Since `cargo fmt` doesn't even attempt to format it, there's probably
// no point in trying to indent this correctly.
342..350,
],
"helix-term/src/ui/lsp.rs" => vec![
// Macro
56..61,
],
"helix-term/src/ui/statusline.rs" => vec![
// Same as in `helix-term/src/commands.rs`
436..442,
450..456,
],
_ => Vec::new(),
};
let git_object = rev.to_string() + ":" + file;
let content = Command::new("git")
.args(["cat-file", "blob", &git_object])
.output()
.unwrap();
let doc = Rope::from_reader(&mut content.stdout.as_slice()).unwrap();
test_treesitter_indent(file, doc, "source.rust", ignored_lines);
}
} }
#[test] #[test]
@ -50,20 +152,41 @@ fn test_indent_level_for_line_with_spaces_and_tabs() {
assert_eq!(indent_level, 2) assert_eq!(indent_level, 2)
} }
fn test_treesitter_indent(file_name: &str, lang_scope: &str) { fn indent_tests_dir() -> PathBuf {
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("tests/data/indent"); test_dir.push("tests/data/indent");
test_dir
}
let mut test_file = test_dir.clone(); fn indent_test_path(name: &str) -> PathBuf {
test_file.push(file_name); let mut path = indent_tests_dir();
let test_file = std::fs::File::open(test_file).unwrap(); path.push(name);
path
}
fn indent_tests_config() -> Configuration {
let mut config_path = indent_tests_dir();
config_path.push("languages.toml");
let config = std::fs::read_to_string(config_path).unwrap();
toml::from_str(&config).unwrap()
}
fn standard_treesitter_test(file_name: &str, lang_scope: &str) {
let test_path = indent_test_path(file_name);
let test_file = std::fs::File::open(test_path).unwrap();
let doc = ropey::Rope::from_reader(test_file).unwrap(); let doc = ropey::Rope::from_reader(test_file).unwrap();
test_treesitter_indent(file_name, doc, lang_scope, Vec::new())
}
let mut config_file = test_dir; /// Test that all the lines in the given file are indented as expected.
config_file.push("languages.toml"); /// ignored_lines is a list of (1-indexed) line ranges that are excluded from this test.
let config = std::fs::read_to_string(config_file).unwrap(); fn test_treesitter_indent(
let config = toml::from_str(&config).unwrap(); test_name: &str,
let loader = Loader::new(config); doc: Rope,
lang_scope: &str,
ignored_lines: Vec<std::ops::Range<usize>>,
) {
let loader = Loader::new(indent_tests_config());
// set runtime path so we can find the queries // set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@ -71,21 +194,25 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap()); std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
let language_config = loader.language_config_for_scope(lang_scope).unwrap(); let language_config = loader.language_config_for_scope(lang_scope).unwrap();
let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit);
let highlight_config = language_config.highlight_config(&[]).unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap();
let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)).unwrap();
let indent_query = language_config.indent_query().unwrap();
let text = doc.slice(..); let text = doc.slice(..);
let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
let indent_query = language_config.indent_query().unwrap();
for i in 0..doc.len_lines() { for i in 0..doc.len_lines() {
let line = text.line(i); let line = text.line(i);
if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
continue;
}
if let Some(pos) = helix_core::find_first_non_whitespace_char(line) { if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
let tab_and_indent_width: usize = 4; let tab_width: usize = 4;
let suggested_indent = treesitter_indent_for_pos( let suggested_indent = treesitter_indent_for_pos(
indent_query, indent_query,
&syntax, &syntax,
&IndentStyle::Spaces(tab_and_indent_width as u8), &indent_style,
tab_and_indent_width, tab_width,
tab_and_indent_width, indent_style.indent_width(tab_width),
text, text,
i, i,
text.line_to_char(i) + pos, text.line_to_char(i) + pos,
@ -94,7 +221,8 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
.unwrap(); .unwrap();
assert!( assert!(
line.get_slice(..pos).map_or(false, |s| s == suggested_indent), line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
"Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n", "Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
test_name,
i+1, i+1,
line.slice(..line.len_chars()-1), line.slice(..line.len_chars()-1),
suggested_indent, suggested_indent,

@ -0,0 +1,15 @@
[package]
name = "helix-event"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
parking_lot = { version = "0.12", features = ["send_guard"] }

@ -0,0 +1,8 @@
//! `helix-event` contains systems that allow (often async) communication between
//! different editor components without strongly coupling them. Currently this
//! crate only contains some smaller facilities but the intend is to add more
//! functionality in the future ( like a generic hook system)
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
mod redraw;

@ -0,0 +1,49 @@
//! Signals that control when/if the editor redraws
use std::future::Future;
use parking_lot::{RwLock, RwLockReadGuard};
use tokio::sync::Notify;
/// A `Notify` instance that can be used to (asynchronously) request
/// the editor the render a new frame.
static REDRAW_NOTIFY: Notify = Notify::const_new();
/// A `RwLock` that prevents the next frame from being
/// drawn until an exclusive (write) lock can be acquired.
/// This allows asynchsonous tasks to acquire `non-exclusive`
/// locks (read) to prevent the next frame from being drawn
/// until a certain computation has finished.
static RENDER_LOCK: RwLock<()> = RwLock::new(());
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
/// Requests that the editor is redrawn. The redraws are debounced (currently to
/// 30FPS) so this can be called many times without causing a ton of frames to
/// be rendered.
pub fn request_redraw() {
REDRAW_NOTIFY.notify_one();
}
/// Returns a future that will yield once a redraw has been asynchronously
/// requested using [`request_redraw`].
pub fn redraw_requested() -> impl Future<Output = ()> {
REDRAW_NOTIFY.notified()
}
/// Wait until all locks acquired with [`lock_frame`] have been released.
/// This function is called before rendering and is intended to allow the frame
/// to wait for async computations that should be included in the current frame.
pub fn start_frame() {
drop(RENDER_LOCK.write());
// exhaust any leftover redraw notifications
let notify = REDRAW_NOTIFY.notified();
tokio::pin!(notify);
notify.enable();
}
/// Acquires the render lock which will prevent the next frame from being drawn
/// until the returned guard is dropped.
pub fn lock_frame() -> RenderLockGuard {
RENDER_LOCK.read()
}

@ -18,16 +18,18 @@ anyhow = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.7" toml = "0.7"
etcetera = "0.8" etcetera = "0.8"
tree-sitter = "0.20" tree-sitter.workspace = true
once_cell = "1.18" once_cell = "1.18"
log = "0.4" log = "0.4"
which = "4.4"
# TODO: these two should be on !wasm32 only # TODO: these two should be on !wasm32 only
# cloning/compiling tree-sitter grammars # cloning/compiling tree-sitter grammars
cc = { version = "1" } cc = { version = "1" }
threadpool = { version = "1.0" } threadpool = { version = "1.0" }
tempfile = "3.5.0" tempfile = "3.8.0"
dunce = "1.0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
libloading = "0.8" libloading = "0.8"

@ -40,7 +40,9 @@ fn main() {
.ok() .ok()
.filter(|output| output.status.success()) .filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok()) .and_then(|x| String::from_utf8(x.stdout).ok())
else{ return; }; else {
return;
};
// If heads starts pointing at something else (different branch) // If heads starts pointing at something else (different branch)
// we need to return // we need to return
let head = Path::new(&git_dir).join("HEAD"); let head = Path::new(&git_dir).join("HEAD");
@ -55,7 +57,9 @@ fn main() {
.ok() .ok()
.filter(|output| output.status.success()) .filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok()) .and_then(|x| String::from_utf8(x.stdout).ok())
else{ return; }; else {
return;
};
let head_ref = Path::new(&git_dir).join(head_ref); let head_ref = Path::new(&git_dir).join(head_ref);
if head_ref.exists() { if head_ref.exists() {
println!("cargo:rerun-if-changed={}", head_ref.display()); println!("cargo:rerun-if-changed={}", head_ref.display());

@ -85,7 +85,16 @@ pub fn get_language(name: &str) -> Result<Language> {
Ok(language) Ok(language)
} }
fn ensure_git_is_available() -> Result<()> {
match which::which("git") {
Ok(_cmd) => Ok(()),
Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")),
}
}
pub fn fetch_grammars() -> Result<()> { pub fn fetch_grammars() -> Result<()> {
ensure_git_is_available()?;
// We do not need to fetch local grammars. // We do not need to fetch local grammars.
let mut grammars = get_grammar_configs()?; let mut grammars = get_grammar_configs()?;
grammars.retain(|grammar| !matches!(grammar.source, GrammarSource::Local { .. })); grammars.retain(|grammar| !matches!(grammar.source, GrammarSource::Local { .. }));
@ -145,6 +154,8 @@ pub fn fetch_grammars() -> Result<()> {
} }
pub fn build_grammars(target: Option<String>) -> Result<()> { pub fn build_grammars(target: Option<String>) -> Result<()> {
ensure_git_is_available()?;
let grammars = get_grammar_configs()?; let grammars = get_grammar_configs()?;
println!("Building {} grammars", grammars.len()); println!("Building {} grammars", grammars.len());
let results = run_parallel(grammars, move |grammar| { let results = run_parallel(grammars, move |grammar| {

@ -3,29 +3,56 @@ pub mod grammar;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> = static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
once_cell::sync::Lazy::new(prioritize_runtime_dirs); once_cell::sync::Lazy::new(prioritize_runtime_dirs);
static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new(); static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
pub fn initialize_config_file(specified_file: Option<PathBuf>) { static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
let config_file = specified_file.unwrap_or_else(|| {
let config_dir = config_dir();
if !config_dir.exists() { // Get the current working directory.
std::fs::create_dir_all(&config_dir).ok(); // This information is managed internally as the call to std::env::current_dir
} // might fail if the cwd has been deleted.
pub fn current_working_dir() -> PathBuf {
if let Some(path) = &*CWD.read().unwrap() {
return path.clone();
}
let path = std::env::current_dir()
.and_then(dunce::canonicalize)
.expect("Couldn't determine current working directory");
let mut cwd = CWD.write().unwrap();
*cwd = Some(path.clone());
path
}
config_dir.join("config.toml") pub fn set_current_working_dir(path: PathBuf) -> std::io::Result<()> {
}); let path = dunce::canonicalize(path)?;
std::env::set_current_dir(path.clone())?;
let mut cwd = CWD.write().unwrap();
*cwd = Some(path);
Ok(())
}
// We should only initialize this value once. pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(default_config_file);
ensure_parent_dir(&config_file);
CONFIG_FILE.set(config_file).ok(); CONFIG_FILE.set(config_file).ok();
} }
pub fn initialize_log_file(specified_file: Option<PathBuf>) {
let log_file = specified_file.unwrap_or_else(default_log_file);
ensure_parent_dir(&log_file);
LOG_FILE.set(log_file).ok();
}
/// A list of runtime directories from highest to lowest priority /// A list of runtime directories from highest to lowest priority
/// ///
/// The priority is: /// The priority is:
@ -122,10 +149,11 @@ pub fn cache_dir() -> PathBuf {
} }
pub fn config_file() -> PathBuf { pub fn config_file() -> PathBuf {
CONFIG_FILE CONFIG_FILE.get().map(|path| path.to_path_buf()).unwrap()
.get() }
.map(|path| path.to_path_buf())
.unwrap_or_else(|| config_dir().join("config.toml")) pub fn log_file() -> PathBuf {
LOG_FILE.get().map(|path| path.to_path_buf()).unwrap()
} }
pub fn workspace_config_file() -> PathBuf { pub fn workspace_config_file() -> PathBuf {
@ -136,7 +164,7 @@ pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml") config_dir().join("languages.toml")
} }
pub fn log_file() -> PathBuf { pub fn default_log_file() -> PathBuf {
cache_dir().join("helix.log") cache_dir().join("helix.log")
} }
@ -217,7 +245,7 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
/// If no workspace was found returns (CWD, true). /// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned /// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) { pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = std::env::current_dir().expect("unable to determine current directory"); let current_dir = current_working_dir();
for ancestor in current_dir.ancestors() { for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() { if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return (ancestor.to_owned(), false); return (ancestor.to_owned(), false);
@ -227,13 +255,37 @@ pub fn find_workspace() -> (PathBuf, bool) {
(current_dir, true) (current_dir, true)
} }
fn default_config_file() -> PathBuf {
config_dir().join("config.toml")
}
fn ensure_parent_dir(path: &Path) {
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).ok();
}
}
}
#[cfg(test)] #[cfg(test)]
mod merge_toml_tests { mod merge_toml_tests {
use std::str; use std::str;
use super::merge_toml_values; use super::{current_working_dir, merge_toml_values, set_current_working_dir};
use toml::Value; use toml::Value;
#[test]
fn current_dir_is_set() {
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
let cwd = current_working_dir();
assert_ne!(cwd, new_path);
set_current_working_dir(new_path.clone()).expect("Couldn't set new path");
let cwd = current_working_dir();
assert_eq!(cwd, new_path);
}
#[test] #[test]
fn language_toml_map_merges() { fn language_toml_map_merges() {
const USER: &str = r#" const USER: &str = r#"

@ -19,12 +19,13 @@ helix-parsec = { version = "0.6", path = "../helix-parsec" }
anyhow = "1.0" anyhow = "1.0"
futures-executor = "0.3" futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
globset = "0.4.13"
log = "0.4" log = "0.4"
lsp-types = { version = "0.94" } lsp-types = { version = "0.94" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.28", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio = { version = "1.32", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.14" tokio-stream = "0.1.14"
which = "4.4" which = "4.4"
parking_lot = "0.12.1" parking_lot = "0.12.1"

@ -7,8 +7,9 @@ use crate::{
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope}; use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::{ use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf, notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent, DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
WorkspaceFoldersChangeEvent,
}; };
use lsp_types as lsp; use lsp_types as lsp;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -543,6 +544,10 @@ impl Client {
normalizes_line_endings: Some(false), normalizes_line_endings: Some(false),
change_annotation_support: None, change_annotation_support: None,
}), }),
did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities {
dynamic_registration: Some(true),
relative_pattern_support: Some(false),
}),
..Default::default() ..Default::default()
}), }),
text_document: Some(lsp::TextDocumentClientCapabilities { text_document: Some(lsp::TextDocumentClientCapabilities {
@ -609,6 +614,12 @@ impl Client {
.collect(), .collect(),
}, },
}), }),
is_preferred_support: Some(true),
disabled_support: Some(true),
data_support: Some(true),
resolve_support: Some(CodeActionCapabilityResolveSupport {
properties: vec!["edit".to_owned(), "command".to_owned()],
}),
..Default::default() ..Default::default()
}), }),
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities { publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
@ -954,6 +965,24 @@ impl Client {
Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item)) Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item))
} }
pub fn resolve_code_action(
&self,
code_action: lsp::CodeAction,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support resolving code action.
match capabilities.completion_provider {
Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..
}) => (),
_ => return None,
}
Some(self.call::<lsp::request::CodeActionResolveRequest>(code_action))
}
pub fn text_document_signature_help( pub fn text_document_signature_help(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
@ -1428,4 +1457,13 @@ impl Client {
Some(self.call::<lsp::request::ExecuteCommand>(params)) Some(self.call::<lsp::request::ExecuteCommand>(params))
} }
pub fn did_change_watched_files(
&self,
changes: Vec<lsp::FileEvent>,
) -> impl Future<Output = std::result::Result<(), Error>> {
self.notify::<lsp::notification::DidChangeWatchedFiles>(lsp::DidChangeWatchedFilesParams {
changes,
})
}
} }

@ -0,0 +1,193 @@
use std::{collections::HashMap, path::PathBuf, sync::Weak};
use globset::{GlobBuilder, GlobSetBuilder};
use tokio::sync::mpsc;
use crate::{lsp, Client};
enum Event {
FileChanged {
path: PathBuf,
},
Register {
client_id: usize,
client: Weak<Client>,
registration_id: String,
options: lsp::DidChangeWatchedFilesRegistrationOptions,
},
Unregister {
client_id: usize,
registration_id: String,
},
RemoveClient {
client_id: usize,
},
}
#[derive(Default)]
struct ClientState {
client: Weak<Client>,
registered: HashMap<String, globset::GlobSet>,
}
/// The Handler uses a dedicated tokio task to respond to file change events by
/// forwarding changes to LSPs that have registered for notifications with a
/// matching glob.
///
/// When an LSP registers for the DidChangeWatchedFiles notification, the
/// Handler is notified by sending the registration details in addition to a
/// weak reference to the LSP client. This is done so that the Handler can have
/// access to the client without preventing the client from being dropped if it
/// is closed and the Handler isn't properly notified.
#[derive(Clone, Debug)]
pub struct Handler {
tx: mpsc::UnboundedSender<Event>,
}
impl Default for Handler {
fn default() -> Self {
Self::new()
}
}
impl Handler {
pub fn new() -> Self {
let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(Self::run(rx));
Self { tx }
}
pub fn register(
&self,
client_id: usize,
client: Weak<Client>,
registration_id: String,
options: lsp::DidChangeWatchedFilesRegistrationOptions,
) {
let _ = self.tx.send(Event::Register {
client_id,
client,
registration_id,
options,
});
}
pub fn unregister(&self, client_id: usize, registration_id: String) {
let _ = self.tx.send(Event::Unregister {
client_id,
registration_id,
});
}
pub fn file_changed(&self, path: PathBuf) {
let _ = self.tx.send(Event::FileChanged { path });
}
pub fn remove_client(&self, client_id: usize) {
let _ = self.tx.send(Event::RemoveClient { client_id });
}
async fn run(mut rx: mpsc::UnboundedReceiver<Event>) {
let mut state: HashMap<usize, ClientState> = HashMap::new();
while let Some(event) = rx.recv().await {
match event {
Event::FileChanged { path } => {
log::debug!("Received file event for {:?}", &path);
state.retain(|id, client_state| {
if !client_state
.registered
.values()
.any(|glob| glob.is_match(&path))
{
return true;
}
let Some(client) = client_state.client.upgrade() else {
log::warn!("LSP client was dropped: {id}");
return false;
};
let Ok(uri) = lsp::Url::from_file_path(&path) else {
return true;
};
log::debug!(
"Sending didChangeWatchedFiles notification to client '{}'",
client.name()
);
if let Err(err) = crate::block_on(client
.did_change_watched_files(vec![lsp::FileEvent {
uri,
// We currently always send the CHANGED state
// since we don't actually have more context at
// the moment.
typ: lsp::FileChangeType::CHANGED,
}]))
{
log::warn!("Failed to send didChangeWatchedFiles notification to client: {err}");
}
true
});
}
Event::Register {
client_id,
client,
registration_id,
options: ops,
} => {
log::debug!(
"Registering didChangeWatchedFiles for client '{}' with id '{}'",
client_id,
registration_id
);
let entry = state.entry(client_id).or_insert_with(ClientState::default);
entry.client = client;
let mut builder = GlobSetBuilder::new();
for watcher in ops.watchers {
if let lsp::GlobPattern::String(pattern) = watcher.glob_pattern {
if let Ok(glob) = GlobBuilder::new(&pattern).build() {
builder.add(glob);
}
}
}
match builder.build() {
Ok(globset) => {
entry.registered.insert(registration_id, globset);
}
Err(err) => {
// Remove any old state for that registration id and
// remove the entire client if it's now empty.
entry.registered.remove(&registration_id);
if entry.registered.is_empty() {
state.remove(&client_id);
}
log::warn!(
"Unable to build globset for LSP didChangeWatchedFiles {err}"
)
}
}
}
Event::Unregister {
client_id,
registration_id,
} => {
log::debug!(
"Unregistering didChangeWatchedFiles with id '{}' for client '{}'",
registration_id,
client_id
);
if let Some(client_state) = state.get_mut(&client_id) {
client_state.registered.remove(&registration_id);
if client_state.registered.is_empty() {
state.remove(&client_id);
}
}
}
Event::RemoveClient { client_id } => {
log::debug!("Removing LSP client: {client_id}");
state.remove(&client_id);
}
}
}
}
}

@ -1,4 +1,5 @@
mod client; mod client;
pub mod file_event;
pub mod jsonrpc; pub mod jsonrpc;
pub mod snippet; pub mod snippet;
mod transport; mod transport;
@ -377,7 +378,7 @@ pub mod util {
.expect("transaction must be valid for primary selection"); .expect("transaction must be valid for primary selection");
let removed_text = text.slice(removed_start..removed_end); let removed_text = text.slice(removed_start..removed_end);
let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping( let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
doc, doc,
selection, selection,
|range| { |range| {
@ -420,6 +421,11 @@ pub mod util {
return transaction; return transaction;
} }
// Don't normalize to avoid merging/reording selections which would
// break the association between tabstops and selections. Most ranges
// will be replaced by tabstops anyways and the final selection will be
// normalized anyways
selection = selection.map_no_normalize(changes);
let mut mapped_selection = SmallVec::with_capacity(selection.len()); let mut mapped_selection = SmallVec::with_capacity(selection.len());
let mut mapped_primary_idx = 0; let mut mapped_primary_idx = 0;
let primary_range = selection.primary(); let primary_range = selection.primary();
@ -428,9 +434,8 @@ pub mod util {
mapped_primary_idx = mapped_selection.len() mapped_primary_idx = mapped_selection.len()
} }
let range = range.map(changes);
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty()); let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
let Some(tabstops) = tabstops else{ let Some(tabstops) = tabstops else {
// no tabstop normal mapping // no tabstop normal mapping
mapped_selection.push(range); mapped_selection.push(range);
continue; continue;
@ -543,6 +548,7 @@ pub enum MethodCall {
WorkspaceFolders, WorkspaceFolders,
WorkspaceConfiguration(lsp::ConfigurationParams), WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams), RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
} }
impl MethodCall { impl MethodCall {
@ -566,6 +572,10 @@ impl MethodCall {
let params: lsp::RegistrationParams = params.parse()?; let params: lsp::RegistrationParams = params.parse()?;
Self::RegisterCapability(params) Self::RegisterCapability(params)
} }
lsp::request::UnregisterCapability::METHOD => {
let params: lsp::UnregistrationParams = params.parse()?;
Self::UnregisterCapability(params)
}
_ => { _ => {
return Err(Error::Unhandled); return Err(Error::Unhandled);
} }
@ -625,6 +635,7 @@ pub struct Registry {
syn_loader: Arc<helix_core::syntax::Loader>, syn_loader: Arc<helix_core::syntax::Loader>,
counter: usize, counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>, pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
pub file_event_handler: file_event::Handler,
} }
impl Registry { impl Registry {
@ -634,6 +645,7 @@ impl Registry {
syn_loader, syn_loader,
counter: 0, counter: 0,
incoming: SelectAll::new(), incoming: SelectAll::new(),
file_event_handler: file_event::Handler::new(),
} }
} }
@ -646,6 +658,7 @@ impl Registry {
} }
pub fn remove_by_id(&mut self, id: usize) { pub fn remove_by_id(&mut self, id: usize) {
self.file_event_handler.remove_client(id);
self.inner.retain(|_, language_servers| { self.inner.retain(|_, language_servers| {
language_servers.retain(|ls| id != ls.id()); language_servers.retain(|ls| id != ls.id());
!language_servers.is_empty() !language_servers.is_empty()
@ -711,6 +724,7 @@ impl Registry {
.unwrap(); .unwrap();
for old_client in old_clients { for old_client in old_clients {
self.file_event_handler.remove_client(old_client.id());
tokio::spawn(async move { tokio::spawn(async move {
let _ = old_client.force_shutdown().await; let _ = old_client.force_shutdown().await;
}); });
@ -727,6 +741,7 @@ impl Registry {
pub fn stop(&mut self, name: &str) { pub fn stop(&mut self, name: &str) {
if let Some(clients) = self.inner.remove(name) { if let Some(clients) = self.inner.remove(name) {
for client in clients { for client in clients {
self.file_event_handler.remove_client(client.id());
tokio::spawn(async move { tokio::spawn(async move {
let _ = client.force_shutdown().await; let _ = client.force_shutdown().await;
}); });
@ -927,7 +942,7 @@ pub fn find_lsp_workspace(
let mut file = if file.is_absolute() { let mut file = if file.is_absolute() {
file.to_path_buf() file.to_path_buf()
} else { } else {
let current_dir = std::env::current_dir().expect("unable to determine current directory"); let current_dir = helix_loader::current_working_dir();
current_dir.join(file) current_dir.join(file)
}; };
file = path::get_normalized_path(&file); file = path::get_normalized_path(&file);

@ -353,6 +353,11 @@ impl Transport {
} }
} }
fn is_shutdown(payload: &Payload) -> bool {
use lsp_types::request::{Request, Shutdown};
matches!(payload, Payload::Request { value: jsonrpc::MethodCall { method, .. }, .. } if method == Shutdown::METHOD)
}
// TODO: events that use capabilities need to do the right thing // TODO: events that use capabilities need to do the right thing
loop { loop {
@ -391,7 +396,10 @@ impl Transport {
} }
msg = client_rx.recv() => { msg = client_rx.recv() => {
if let Some(msg) = msg { if let Some(msg) = msg {
if is_pending && !is_initialize(&msg) { if is_pending && is_shutdown(&msg) {
log::info!("Language server not initialized, shutting down");
break;
} else if is_pending && !is_initialize(&msg) {
// ignore notifications // ignore notifications
if let Payload::Notification(_) = msg { if let Payload::Notification(_) = msg {
continue; continue;

@ -24,6 +24,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-event = { version = "0.6", path = "../helix-event" }
helix-view = { version = "0.6", path = "../helix-view" } helix-view = { version = "0.6", path = "../helix-view" }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-dap = { version = "0.6", path = "../helix-dap" }
@ -37,7 +38,7 @@ which = "4.4"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.26", features = ["event-stream"] } crossterm = { version = "0.27", features = ["event-stream"] }
signal-hook = "0.3" signal-hook = "0.3"
tokio-stream = "0.1" tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@ -49,7 +50,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4" log = "0.4"
# File picker # File picker
fuzzy-matcher = "0.3" nucleo.workspace = true
ignore = "0.4" ignore = "0.4"
# markdown doc rendering # markdown doc rendering
pulldown-cmark = { version = "0.9", default-features = false } pulldown-cmark = { version = "0.9", default-features = false }
@ -68,12 +69,15 @@ grep-searcher = "0.1.11"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.145" libc = "0.2.147"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
[build-dependencies] [build-dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
[dev-dependencies] [dev-dependencies]
smallvec = "1.10" smallvec = "1.11"
indoc = "2.0.1" indoc = "2.0.3"
tempfile = "3.4.0" tempfile = "3.8.0"

@ -5,7 +5,11 @@ use helix_core::{
path::get_relative_path, path::get_relative_path,
pos_at_coords, syntax, Selection, pos_at_coords, syntax, Selection,
}; };
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_pos_to_pos,
LspProgressMap,
};
use helix_view::{ use helix_view::{
align_view, align_view,
document::DocumentSavedEventResult, document::DocumentSavedEventResult,
@ -29,13 +33,9 @@ use crate::{
}; };
use log::{debug, error, warn}; use log::{debug, error, warn};
use std::{ #[cfg(not(feature = "integration"))]
collections::btree_map::Entry, use std::io::stdout;
io::{stdin, stdout}, use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
path::Path,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::{Context, Error}; use anyhow::{Context, Error};
@ -45,8 +45,6 @@ use {signal_hook::consts::signal, signal_hook_tokio::Signals};
#[cfg(windows)] #[cfg(windows)]
type Signals = futures_util::stream::Empty<()>; type Signals = futures_util::stream::Empty<()>;
const LSP_DEADLINE: Duration = Duration::from_millis(16);
#[cfg(not(feature = "integration"))] #[cfg(not(feature = "integration"))]
use tui::backend::CrosstermBackend; use tui::backend::CrosstermBackend;
@ -76,7 +74,6 @@ pub struct Application {
signals: Signals, signals: Signals,
jobs: Jobs, jobs: Jobs,
lsp_progress: LspProgressMap, lsp_progress: LspProgressMap,
last_render: Instant,
} }
#[cfg(feature = "integration")] #[cfg(feature = "integration")]
@ -163,11 +160,11 @@ impl Application {
let path = helix_loader::runtime_file(Path::new("tutor")); let path = helix_loader::runtime_file(Path::new("tutor"));
editor.open(&path, Action::VerticalSplit)?; editor.open(&path, Action::VerticalSplit)?;
// Unset path to prevent accidentally saving to the original tutor file. // Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?; doc_mut!(editor).set_path(None);
} else if !args.files.is_empty() { } else if !args.files.is_empty() {
let first = &args.files[0].0; // we know it's not empty let first = &args.files[0].0; // we know it's not empty
if first.is_dir() { if first.is_dir() {
std::env::set_current_dir(first).context("set current dir")?; helix_loader::set_current_working_dir(first.clone())?;
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.load().editor); let picker = ui::file_picker(".".into(), &config.load().editor);
compositor.push(Box::new(overlaid(picker))); compositor.push(Box::new(overlaid(picker)));
@ -215,11 +212,6 @@ impl Application {
} }
} else if stdin().is_tty() || cfg!(feature = "integration") { } else if stdin().is_tty() || cfg!(feature = "integration") {
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
} else if cfg!(target_os = "macos") {
// On Linux and Windows, we allow the output of a command to be piped into the new buffer.
// This doesn't currently work on macOS because of the following issue:
// https://github.com/crossterm-rs/crossterm/issues/500
anyhow::bail!("Piping into helix-term is currently not supported on macOS");
} else { } else {
editor editor
.new_file_from_stdin(Action::VerticalSplit) .new_file_from_stdin(Action::VerticalSplit)
@ -253,7 +245,6 @@ impl Application {
signals, signals,
jobs: Jobs::new(), jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(), lsp_progress: LspProgressMap::new(),
last_render: Instant::now(),
}; };
Ok(app) Ok(app)
@ -266,16 +257,8 @@ impl Application {
scroll: None, scroll: None,
}; };
// Acquire mutable access to the redraw_handle lock helix_event::start_frame();
// to ensure that there are no tasks running that want to block rendering
drop(cx.editor.redraw_handle.1.write().await);
cx.editor.needs_redraw = false; cx.editor.needs_redraw = false;
{
// exhaust any leftover redraw notifications
let notify = cx.editor.redraw_handle.0.notified();
tokio::pin!(notify);
notify.enable();
}
let area = self let area = self
.terminal .terminal
@ -297,10 +280,9 @@ impl Application {
pub async fn event_loop<S>(&mut self, input_stream: &mut S) pub async fn event_loop<S>(&mut self, input_stream: &mut S)
where where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin, S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{ {
self.render().await; self.render().await;
self.last_render = Instant::now();
loop { loop {
if !self.event_loop_until_idle(input_stream).await { if !self.event_loop_until_idle(input_stream).await {
@ -311,7 +293,7 @@ impl Application {
pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
where where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin, S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{ {
loop { loop {
if self.editor.should_close() { if self.editor.should_close() {
@ -564,16 +546,7 @@ impl Application {
let bytes = doc_save_event.text.len_bytes(); let bytes = doc_save_event.text.len_bytes();
if doc.path() != Some(&doc_save_event.path) { if doc.path() != Some(&doc_save_event.path) {
if let Err(err) = doc.set_path(Some(&doc_save_event.path)) { doc.set_path(Some(&doc_save_event.path));
log::error!(
"error setting path for doc '{:?}': {}",
doc.path(),
err.to_string(),
);
self.editor.set_error(err.to_string());
return;
}
let loader = self.editor.syn_loader.clone(); let loader = self.editor.syn_loader.clone();
@ -609,12 +582,7 @@ impl Application {
EditorEvent::LanguageServerMessage((id, call)) => { EditorEvent::LanguageServerMessage((id, call)) => {
self.handle_language_server_message(call, id).await; self.handle_language_server_message(call, id).await;
// limit render calls for fast language server messages // limit render calls for fast language server messages
let last = self.editor.language_servers.incoming.is_empty(); helix_event::request_redraw();
if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render().await;
self.last_render = Instant::now();
}
} }
EditorEvent::DebuggerEvent(payload) => { EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await; let needs_render = self.editor.handle_debugger_message(payload).await;
@ -622,6 +590,9 @@ impl Application {
self.render().await; self.render().await;
} }
} }
EditorEvent::Redraw => {
self.render().await;
}
EditorEvent::IdleTimer => { EditorEvent::IdleTimer => {
self.editor.clear_idle_timer(); self.editor.clear_idle_timer();
self.handle_idle_timeout().await; self.handle_idle_timeout().await;
@ -636,10 +607,7 @@ impl Application {
false false
} }
pub async fn handle_terminal_events( pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
&mut self,
event: Result<CrosstermEvent, crossterm::ErrorKind>,
) {
let mut cx = crate::compositor::Context { let mut cx = crate::compositor::Context {
editor: &mut self.editor, editor: &mut self.editor,
jobs: &mut self.jobs, jobs: &mut self.jobs,
@ -746,7 +714,12 @@ impl Application {
return; return;
} }
}; };
let offset_encoding = language_server!().offset_encoding(); let language_server = language_server!();
if !language_server.is_initialized() {
log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name());
return;
}
let offset_encoding = language_server.offset_encoding();
let doc = self.editor.document_by_path_mut(&path).filter(|doc| { let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version { if let Some(version) = params.version {
if version != doc.version() { if version != doc.version() {
@ -1042,20 +1015,31 @@ impl Application {
Ok(serde_json::Value::Null) Ok(serde_json::Value::Null)
} }
Ok(MethodCall::ApplyWorkspaceEdit(params)) => { Ok(MethodCall::ApplyWorkspaceEdit(params)) => {
let res = apply_workspace_edit( let language_server = language_server!();
&mut self.editor, if language_server.is_initialized() {
helix_lsp::OffsetEncoding::Utf8, let offset_encoding = language_server.offset_encoding();
&params.edit, let res = apply_workspace_edit(
); &mut self.editor,
offset_encoding,
Ok(json!(lsp::ApplyWorkspaceEditResponse { &params.edit,
applied: res.is_ok(), );
failure_reason: res.as_ref().err().map(|err| err.kind.to_string()),
failed_change: res Ok(json!(lsp::ApplyWorkspaceEditResponse {
.as_ref() applied: res.is_ok(),
.err() failure_reason: res.as_ref().err().map(|err| err.kind.to_string()),
.map(|err| err.failed_change_idx as u32), failed_change: res
})) .as_ref()
.err()
.map(|err| err.failed_change_idx as u32),
}))
} else {
Err(helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::InvalidRequest,
message: "Server must be initialized to request workspace edits"
.to_string(),
data: None,
})
}
} }
Ok(MethodCall::WorkspaceFolders) => { Ok(MethodCall::WorkspaceFolders) => {
Ok(json!(&*language_server!().workspace_folders().await)) Ok(json!(&*language_server!().workspace_folders().await))
@ -1080,17 +1064,65 @@ impl Application {
.collect(); .collect();
Ok(json!(result)) Ok(json!(result))
} }
Ok(MethodCall::RegisterCapability(_params)) => { Ok(MethodCall::RegisterCapability(params)) => {
log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server"); if let Some(client) = self
// Language Servers based on the `vscode-languageserver-node` library often send .editor
// client/registerCapability even though we do not enable dynamic registration .language_servers
// for any capabilities. We should send a MethodNotFound JSONRPC error in this .iter_clients()
// case but that rejects the registration promise in the server which causes an .find(|client| client.id() == server_id)
// exit. So we work around this by ignoring the request and sending back an OK {
// response. for reg in params.registrations {
match reg.method.as_str() {
lsp::notification::DidChangeWatchedFiles::METHOD => {
let Some(options) = reg.register_options else {
continue;
};
let ops: lsp::DidChangeWatchedFilesRegistrationOptions =
match serde_json::from_value(options) {
Ok(ops) => ops,
Err(err) => {
log::warn!("Failed to deserialize DidChangeWatchedFilesRegistrationOptions: {err}");
continue;
}
};
self.editor.language_servers.file_event_handler.register(
client.id(),
Arc::downgrade(client),
reg.id,
ops,
)
}
_ => {
// Language Servers based on the `vscode-languageserver-node` library often send
// client/registerCapability even though we do not enable dynamic registration
// for most capabilities. We should send a MethodNotFound JSONRPC error in this
// case but that rejects the registration promise in the server which causes an
// exit. So we work around this by ignoring the request and sending back an OK
// response.
log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
}
}
}
}
Ok(serde_json::Value::Null) Ok(serde_json::Value::Null)
} }
Ok(MethodCall::UnregisterCapability(params)) => {
for unreg in params.unregisterations {
match unreg.method.as_str() {
lsp::notification::DidChangeWatchedFiles::METHOD => {
self.editor
.language_servers
.file_event_handler
.unregister(server_id, unreg.id);
}
_ => {
log::warn!("Received unregistration request for unsupported method: {}", unreg.method);
}
}
}
Ok(serde_json::Value::Null)
}
}; };
tokio::spawn(language_server!().reply(id, reply)); tokio::spawn(language_server!().reply(id, reply));
@ -1116,7 +1148,7 @@ impl Application {
pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error> pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
where where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin, S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{ {
self.claim_term().await?; self.claim_term().await?;

File diff suppressed because it is too large Load Diff

@ -2,7 +2,7 @@ use super::{Context, Editor};
use crate::{ use crate::{
compositor::{self, Compositor}, compositor::{self, Compositor},
job::{Callback, Jobs}, job::{Callback, Jobs},
ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent, Text},
}; };
use dap::{StackFrame, Thread, ThreadStates}; use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
@ -73,21 +73,19 @@ fn thread_picker(
let debugger = debugger!(editor); let debugger = debugger!(editor);
let thread_states = debugger.thread_states.clone(); let thread_states = debugger.thread_states.clone();
let picker = FilePicker::new( let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
threads, callback_fn(cx.editor, thread)
thread_states, })
move |cx, thread, _action| callback_fn(cx.editor, thread), .with_preview(move |editor, thread| {
move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; let frame = frames.get(0)?;
let frame = frames.get(0)?; let path = frame.source.as_ref()?.path.clone()?;
let path = frame.source.as_ref()?.path.clone()?; let pos = Some((
let pos = Some(( frame.line.saturating_sub(1),
frame.line.saturating_sub(1), frame.end_line.unwrap_or(frame.line).saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1), ));
)); Some((path.into(), pos))
Some((path.into(), pos)) });
},
);
compositor.push(Box::new(picker)); compositor.push(Box::new(picker));
}, },
); );
@ -219,7 +217,7 @@ pub fn dap_start_impl(
} }
} }
args.insert("cwd", to_value(std::env::current_dir().unwrap())?); args.insert("cwd", to_value(helix_loader::current_working_dir())?);
let args = to_value(args).unwrap(); let args = to_value(args).unwrap();
@ -341,8 +339,12 @@ fn debug_parameter_prompt(
.to_owned(); .to_owned();
let completer = match field_type { let completer = match field_type {
"filename" => ui::completers::filename, "filename" => |editor: &Editor, input: &str| {
"directory" => ui::completers::directory, ui::completers::filename_with_git_ignore(editor, input, false)
},
"directory" => |editor: &Editor, input: &str| {
ui::completers::directory_with_git_ignore(editor, input, false)
},
_ => ui::completers::none, _ => ui::completers::none,
}; };
@ -728,39 +730,35 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let frames = debugger.stack_frames[&thread_id].clone(); let frames = debugger.stack_frames[&thread_id].clone();
let picker = FilePicker::new( let picker = Picker::new(frames, (), move |cx, frame, _action| {
frames, let debugger = debugger!(cx.editor);
(), // TODO: this should be simpler to find
move |cx, frame, _action| { let pos = debugger.stack_frames[&thread_id]
let debugger = debugger!(cx.editor); .iter()
// TODO: this should be simpler to find .position(|f| f.id == frame.id);
let pos = debugger.stack_frames[&thread_id] debugger.active_frame = pos;
.iter()
.position(|f| f.id == frame.id); let frame = debugger.stack_frames[&thread_id]
debugger.active_frame = pos; .get(pos.unwrap_or(0))
.cloned();
let frame = debugger.stack_frames[&thread_id] if let Some(frame) = &frame {
.get(pos.unwrap_or(0)) jump_to_stack_frame(cx.editor, frame);
.cloned(); }
if let Some(frame) = &frame { })
jump_to_stack_frame(cx.editor, frame); .with_preview(move |_editor, frame| {
} frame
}, .source
move |_editor, frame| { .as_ref()
frame .and_then(|source| source.path.clone())
.source .map(|path| {
.as_ref() (
.and_then(|source| source.path.clone()) path.into(),
.map(|path| { Some((
( frame.line.saturating_sub(1),
path.into(), frame.end_line.unwrap_or(frame.line).saturating_sub(1),
Some(( )),
frame.line.saturating_sub(1), )
frame.end_line.unwrap_or(frame.line).saturating_sub(1), })
)), });
)
})
},
);
cx.push_layer(Box::new(picker)) cx.push_layer(Box::new(picker))
} }

@ -32,8 +32,8 @@ use crate::{
compositor::{self, Compositor}, compositor::{self, Compositor},
job::Callback, job::Callback,
ui::{ ui::{
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker, self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
Popup, PromptEvent, PromptEvent,
}, },
}; };
@ -196,7 +196,6 @@ fn location_to_file_location(location: &lsp::Location) -> FileLocation {
(path.into(), line) (path.into(), line)
} }
// TODO: share with symbol picker(symbol.location)
fn jump_to_location( fn jump_to_location(
editor: &mut Editor, editor: &mut Editor,
location: &lsp::Location, location: &lsp::Location,
@ -214,15 +213,16 @@ fn jump_to_location(
return; return;
} }
}; };
match editor.open(&path, action) {
Ok(_) => (), let doc = match editor.open(&path, action) {
Ok(id) => doc_mut!(editor, &id),
Err(err) => { Err(err) => {
let err = format!("failed to open path: {:?}: {:?}", location.uri, err); let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
editor.set_error(err); editor.set_error(err);
return; return;
} }
} };
let (view, doc) = current!(editor); let view = view_mut!(editor);
// TODO: convert inside server // TODO: convert inside server
let new_range = let new_range =
if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) { if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) {
@ -234,51 +234,24 @@ fn jump_to_location(
// we flip the range so that the cursor sits on the start of the symbol // we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function). // (for example start of the function).
doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor)); doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
align_view(doc, view, Align::Center); if action.align_view(view, doc.id()) {
align_view(doc, view, Align::Center);
}
} }
type SymbolPicker = FilePicker<SymbolInformationItem>; type SymbolPicker = Picker<SymbolInformationItem>;
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker { fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag? // TODO: drop current_path comparison and instead use workspace: bool flag?
FilePicker::new( Picker::new(symbols, current_path, move |cx, item, action| {
symbols, jump_to_location(
current_path.clone(), cx.editor,
move |cx, item, action| { &item.symbol.location,
let (view, doc) = current!(cx.editor); item.offset_encoding,
push_jump(view, doc); action,
);
if current_path.as_ref() != Some(&item.symbol.location.uri) { })
let uri = &item.symbol.location.uri; .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
let path = match uri.to_file_path() {
Ok(path) => path,
Err(_) => {
let err = format!("unable to convert URI to filepath: {}", uri);
cx.editor.set_error(err);
return;
}
};
if let Err(err) = cx.editor.open(&path, action) {
let err = format!("failed to open document: {}: {}", uri, err);
log::error!("{}", err);
cx.editor.set_error(err);
return;
}
}
let (view, doc) = current!(cx.editor);
if let Some(range) =
lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
{
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
align_view(doc, view, Align::Center);
}
},
move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
)
.truncate_start(false) .truncate_start(false)
} }
@ -291,9 +264,9 @@ enum DiagnosticsFormat {
fn diag_picker( fn diag_picker(
cx: &Context, cx: &Context,
diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>, diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
current_path: Option<lsp::Url>, _current_path: Option<lsp::Url>,
format: DiagnosticsFormat, format: DiagnosticsFormat,
) -> FilePicker<PickerDiagnostic> { ) -> Picker<PickerDiagnostic> {
// TODO: drop current_path comparison and instead use workspace: bool flag? // TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs // flatten the map to a vec of (url, diag) pairs
@ -319,7 +292,7 @@ fn diag_picker(
error: cx.editor.theme.get("error"), error: cx.editor.theme.get("error"),
}; };
FilePicker::new( Picker::new(
flat_diag, flat_diag,
(styles, format), (styles, format),
move |cx, move |cx,
@ -329,28 +302,18 @@ fn diag_picker(
offset_encoding, offset_encoding,
}, },
action| { action| {
if current_path.as_ref() == Some(url) { jump_to_location(
let (view, doc) = current!(cx.editor); cx.editor,
push_jump(view, doc); &lsp::Location::new(url.clone(), diag.range),
} else { *offset_encoding,
let path = url.to_file_path().unwrap(); action,
cx.editor.open(&path, action).expect("editor.open failed"); )
}
let (view, doc) = current!(cx.editor);
if let Some(range) = lsp_range_to_range(doc.text(), diag.range, *offset_encoding) {
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
align_view(doc, view, Align::Center);
}
},
move |_editor, PickerDiagnostic { url, diag, .. }| {
let location = lsp::Location::new(url.clone(), diag.range);
Some(location_to_file_location(&location))
}, },
) )
.with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
let location = lsp::Location::new(url.clone(), diag.range);
Some(location_to_file_location(&location))
})
.truncate_start(false) .truncate_start(false)
} }
@ -443,6 +406,15 @@ pub fn symbol_picker(cx: &mut Context) {
pub fn workspace_symbol_picker(cx: &mut Context) { pub fn workspace_symbol_picker(cx: &mut Context) {
let doc = doc!(cx.editor); let doc = doc!(cx.editor);
if doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
.count()
== 0
{
cx.editor
.set_error("No configured language server supports workspace symbols");
return;
}
let get_symbols = move |pattern: String, editor: &mut Editor| { let get_symbols = move |pattern: String, editor: &mut Editor| {
let doc = doc!(editor); let doc = doc!(editor);
@ -726,7 +698,8 @@ pub fn code_action(cx: &mut Context) {
// always present here // always present here
let action = action.unwrap(); let action = action.unwrap();
let Some(language_server) = editor.language_server_by_id(action.language_server_id) else { let Some(language_server) = editor.language_server_by_id(action.language_server_id)
else {
editor.set_error("Language Server disappeared"); editor.set_error("Language Server disappeared");
return; return;
}; };
@ -739,7 +712,25 @@ pub fn code_action(cx: &mut Context) {
} }
lsp::CodeActionOrCommand::CodeAction(code_action) => { lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action); log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit { // we support lsp "codeAction/resolve" for `edit` and `command` fields
let mut resolved_code_action = None;
if code_action.edit.is_none() || code_action.command.is_none() {
if let Some(future) =
language_server.resolve_code_action(code_action.clone())
{
if let Ok(response) = helix_lsp::block_on(future) {
if let Ok(code_action) =
serde_json::from_value::<CodeAction>(response)
{
resolved_code_action = Some(code_action);
}
}
}
}
let resolved_code_action =
resolved_code_action.as_ref().unwrap_or(code_action);
if let Some(ref workspace_edit) = resolved_code_action.edit {
log::debug!("edit: {:?}", workspace_edit); log::debug!("edit: {:?}", workspace_edit);
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit); let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
} }
@ -1029,7 +1020,7 @@ fn goto_impl(
locations: Vec<lsp::Location>, locations: Vec<lsp::Location>,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
) { ) {
let cwdir = std::env::current_dir().unwrap_or_default(); let cwdir = helix_loader::current_working_dir();
match locations.as_slice() { match locations.as_slice() {
[location] => { [location] => {
@ -1039,14 +1030,10 @@ fn goto_impl(
editor.set_error("No definition found."); editor.set_error("No definition found.");
} }
_locations => { _locations => {
let picker = FilePicker::new( let picker = Picker::new(locations, cwdir, move |cx, location, action| {
locations, jump_to_location(cx.editor, location, offset_encoding, action)
cwdir, })
move |cx, location, action| { .with_preview(move |_editor, location| Some(location_to_file_location(location)));
jump_to_location(cx.editor, location, offset_encoding, action)
},
move |_editor, location| Some(location_to_file_location(location)),
);
compositor.push(Box::new(overlaid(picker))); compositor.push(Box::new(overlaid(picker)));
} }
} }
@ -1230,7 +1217,8 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
// Do not show the message if signature help was invoked // Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc. // automatically on backspace, trigger characters, etc.
if invoked == SignatureHelpInvoked::Manual { if invoked == SignatureHelpInvoked::Manual {
cx.editor.set_error("No configured language server supports signature-help"); cx.editor
.set_error("No configured language server supports signature-help");
} }
return; return;
}; };
@ -1455,7 +1443,8 @@ pub fn rename_symbol(cx: &mut Context) {
.language_servers_with_feature(LanguageServerFeature::RenameSymbol) .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
.find(|ls| language_server_id.map_or(true, |id| id == ls.id())) .find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
else { else {
cx.editor.set_error("No configured language server supports symbol renaming"); cx.editor
.set_error("No configured language server supports symbol renaming");
return; return;
}; };

File diff suppressed because it is too large Load Diff

@ -1,5 +1,5 @@
use crate::keymap; use crate::keymap;
use crate::keymap::{merge_keys, Keymap}; use crate::keymap::{merge_keys, KeyTrie};
use helix_loader::merge_toml_values; use helix_loader::merge_toml_values;
use helix_view::document::Mode; use helix_view::document::Mode;
use serde::Deserialize; use serde::Deserialize;
@ -12,7 +12,7 @@ use toml::de::Error as TomlError;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Config { pub struct Config {
pub theme: Option<String>, pub theme: Option<String>,
pub keys: HashMap<Mode, Keymap>, pub keys: HashMap<Mode, KeyTrie>,
pub editor: helix_view::editor::Config, pub editor: helix_view::editor::Config,
} }
@ -20,7 +20,7 @@ pub struct Config {
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ConfigRaw { pub struct ConfigRaw {
pub theme: Option<String>, pub theme: Option<String>,
pub keys: Option<HashMap<Mode, Keymap>>, pub keys: Option<HashMap<Mode, KeyTrie>>,
pub editor: Option<toml::Value>, pub editor: Option<toml::Value>,
} }
@ -109,6 +109,7 @@ impl Config {
)?, )?,
} }
} }
// these are just two io errors return the one for the global config // these are just two io errors return the one for the global config
(Err(err), Err(_)) => return Err(err), (Err(err), Err(_)) => return Err(err),
}; };
@ -138,7 +139,6 @@ mod tests {
#[test] #[test]
fn parsing_keymaps_config_file() { fn parsing_keymaps_config_file() {
use crate::keymap; use crate::keymap;
use crate::keymap::Keymap;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::document::Mode; use helix_view::document::Mode;
@ -155,13 +155,13 @@ mod tests {
merge_keys( merge_keys(
&mut keys, &mut keys,
hashmap! { hashmap! {
Mode::Insert => Keymap::new(keymap!({ "Insert mode" Mode::Insert => keymap!({ "Insert mode"
"y" => move_line_down, "y" => move_line_down,
"S-C-a" => delete_selection, "S-C-a" => delete_selection,
})), }),
Mode::Normal => Keymap::new(keymap!({ "Normal mode" Mode::Normal => keymap!({ "Normal mode"
"A-F12" => move_next_word_end, "A-F12" => move_next_word_end,
})), }),
}, },
); );

@ -18,7 +18,7 @@ use std::{
pub use default::default; pub use default::default;
use macros::key; use macros::key;
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct KeyTrieNode { pub struct KeyTrieNode {
/// A label for keys coming under this node, like "Goto mode" /// A label for keys coming under this node, like "Goto mode"
name: String, name: String,
@ -52,10 +52,6 @@ impl KeyTrieNode {
} }
} }
pub fn name(&self) -> &str {
&self.name
}
/// Merge another Node in. Leaves and subnodes from the other node replace /// Merge another Node in. Leaves and subnodes from the other node replace
/// corresponding keyevent in self, except when both other and self have /// corresponding keyevent in self, except when both other and self have
/// subnodes for same key. In that case the merge is recursive. /// subnodes for same key. In that case the merge is recursive.
@ -77,49 +73,40 @@ impl KeyTrieNode {
} }
pub fn infobox(&self) -> Info { pub fn infobox(&self) -> Info {
let mut body: Vec<(&str, BTreeSet<KeyEvent>)> = Vec::with_capacity(self.len()); let mut body: Vec<(BTreeSet<KeyEvent>, &str)> = Vec::with_capacity(self.len());
for (&key, trie) in self.iter() { for (&key, trie) in self.iter() {
let desc = match trie { let desc = match trie {
KeyTrie::Leaf(cmd) => { KeyTrie::MappableCommand(cmd) => {
if cmd.name() == "no_op" { if cmd.name() == "no_op" {
continue; continue;
} }
cmd.doc() cmd.doc()
} }
KeyTrie::Node(n) => n.name(), KeyTrie::Node(n) => &n.name,
KeyTrie::Sequence(_) => "[Multiple commands]", KeyTrie::Sequence(_) => "[Multiple commands]",
}; };
match body.iter().position(|(d, _)| d == &desc) { match body.iter().position(|(_, d)| d == &desc) {
Some(pos) => { Some(pos) => {
body[pos].1.insert(key); body[pos].0.insert(key);
} }
None => body.push((desc, BTreeSet::from([key]))), None => body.push((BTreeSet::from([key]), desc)),
} }
} }
body.sort_unstable_by_key(|(_, keys)| { body.sort_unstable_by_key(|(keys, _)| {
self.order self.order
.iter() .iter()
.position(|&k| k == *keys.iter().next().unwrap()) .position(|&k| k == *keys.iter().next().unwrap())
.unwrap() .unwrap()
}); });
let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body
.into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect();
}
Info::from_keymap(self.name(), body)
}
/// Get a reference to the key trie node's order.
pub fn order(&self) -> &[KeyEvent] {
self.order.as_slice()
}
}
impl Default for KeyTrieNode { let body: Vec<_> = body
fn default() -> Self { .into_iter()
Self::new("", HashMap::new(), Vec::new()) .map(|(events, desc)| {
let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
(events.join(", "), desc)
})
.collect();
Info::new(&self.name, &body)
} }
} }
@ -145,7 +132,7 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum KeyTrie { pub enum KeyTrie {
Leaf(MappableCommand), MappableCommand(MappableCommand),
Sequence(Vec<MappableCommand>), Sequence(Vec<MappableCommand>),
Node(KeyTrieNode), Node(KeyTrieNode),
} }
@ -174,7 +161,7 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
{ {
command command
.parse::<MappableCommand>() .parse::<MappableCommand>()
.map(KeyTrie::Leaf) .map(KeyTrie::MappableCommand)
.map_err(E::custom) .map_err(E::custom)
} }
@ -208,17 +195,43 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
} }
impl KeyTrie { impl KeyTrie {
pub fn reverse_map(&self) -> ReverseKeymap {
// recursively visit all nodes in keymap
fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
match node {
KeyTrie::MappableCommand(cmd) => {
let name = cmd.name();
if name != "no_op" {
cmd_map.entry(name.into()).or_default().push(keys.clone())
}
}
KeyTrie::Node(next) => {
for (key, trie) in &next.map {
keys.push(*key);
map_node(cmd_map, trie, keys);
keys.pop();
}
}
KeyTrie::Sequence(_) => {}
};
}
let mut res = HashMap::new();
map_node(&mut res, self, &mut Vec::new());
res
}
pub fn node(&self) -> Option<&KeyTrieNode> { pub fn node(&self) -> Option<&KeyTrieNode> {
match *self { match *self {
KeyTrie::Node(ref node) => Some(node), KeyTrie::Node(ref node) => Some(node),
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
} }
} }
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
match *self { match *self {
KeyTrie::Node(ref mut node) => Some(node), KeyTrie::Node(ref mut node) => Some(node),
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
} }
} }
@ -235,7 +248,7 @@ impl KeyTrie {
trie = match trie { trie = match trie {
KeyTrie::Node(map) => map.get(key), KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process // leaf encountered while keys left to process
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
}? }?
} }
Some(trie) Some(trie)
@ -256,75 +269,11 @@ pub enum KeymapResult {
Cancelled(Vec<KeyEvent>), Cancelled(Vec<KeyEvent>),
} }
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(transparent)]
pub struct Keymap {
/// Always a Node
root: KeyTrie,
}
/// A map of command names to keybinds that will execute the command. /// A map of command names to keybinds that will execute the command.
pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>; pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
Keymap { root }
}
pub fn reverse_map(&self) -> ReverseKeymap {
// recursively visit all nodes in keymap
fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
match node {
KeyTrie::Leaf(cmd) => match cmd {
MappableCommand::Typable { name, .. } => {
cmd_map.entry(name.into()).or_default().push(keys.clone())
}
MappableCommand::Static { name, .. } => cmd_map
.entry(name.to_string())
.or_default()
.push(keys.clone()),
},
KeyTrie::Node(next) => {
for (key, trie) in &next.map {
keys.push(*key);
map_node(cmd_map, trie, keys);
keys.pop();
}
}
KeyTrie::Sequence(_) => {}
};
}
let mut res = HashMap::new();
map_node(&mut res, &self.root, &mut Vec::new());
res
}
pub fn root(&self) -> &KeyTrie {
&self.root
}
pub fn merge(&mut self, other: Self) {
self.root.merge_nodes(other.root);
}
}
impl Deref for Keymap {
type Target = KeyTrieNode;
fn deref(&self) -> &Self::Target {
self.root.node().unwrap()
}
}
impl Default for Keymap {
fn default() -> Self {
Self::new(KeyTrie::Node(KeyTrieNode::default()))
}
}
pub struct Keymaps { pub struct Keymaps {
pub map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>, pub map: Box<dyn DynAccess<HashMap<Mode, KeyTrie>>>,
/// Stores pending keys waiting for the next key. This is relative to a /// Stores pending keys waiting for the next key. This is relative to a
/// sticky node if one is in use. /// sticky node if one is in use.
state: Vec<KeyEvent>, state: Vec<KeyEvent>,
@ -333,7 +282,7 @@ pub struct Keymaps {
} }
impl Keymaps { impl Keymaps {
pub fn new(map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>) -> Self { pub fn new(map: Box<dyn DynAccess<HashMap<Mode, KeyTrie>>>) -> Self {
Self { Self {
map, map,
state: Vec::new(), state: Vec::new(),
@ -341,7 +290,7 @@ impl Keymaps {
} }
} }
pub fn map(&self) -> DynGuard<HashMap<Mode, Keymap>> { pub fn map(&self) -> DynGuard<HashMap<Mode, KeyTrie>> {
self.map.load() self.map.load()
} }
@ -373,11 +322,11 @@ impl Keymaps {
let first = self.state.get(0).unwrap_or(&key); let first = self.state.get(0).unwrap_or(&key);
let trie_node = match self.sticky { let trie_node = match self.sticky {
Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())),
None => Cow::Borrowed(&keymap.root), None => Cow::Borrowed(keymap),
}; };
let trie = match trie_node.search(&[*first]) { let trie = match trie_node.search(&[*first]) {
Some(KeyTrie::Leaf(ref cmd)) => { Some(KeyTrie::MappableCommand(ref cmd)) => {
return KeymapResult::Matched(cmd.clone()); return KeymapResult::Matched(cmd.clone());
} }
Some(KeyTrie::Sequence(ref cmds)) => { Some(KeyTrie::Sequence(ref cmds)) => {
@ -396,7 +345,7 @@ impl Keymaps {
} }
KeymapResult::Pending(map.clone()) KeymapResult::Pending(map.clone())
} }
Some(KeyTrie::Leaf(cmd)) => { Some(KeyTrie::MappableCommand(cmd)) => {
self.state.clear(); self.state.clear();
KeymapResult::Matched(cmd.clone()) KeymapResult::Matched(cmd.clone())
} }
@ -416,9 +365,13 @@ impl Default for Keymaps {
} }
/// Merge default config keys with user overwritten keys for custom user config. /// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(dst: &mut HashMap<Mode, Keymap>, mut delta: HashMap<Mode, Keymap>) { pub fn merge_keys(dst: &mut HashMap<Mode, KeyTrie>, mut delta: HashMap<Mode, KeyTrie>) {
for (mode, keys) in dst { for (mode, keys) in dst {
keys.merge(delta.remove(mode).unwrap_or_default()) keys.merge_nodes(
delta
.remove(mode)
.unwrap_or_else(|| KeyTrie::Node(KeyTrieNode::default())),
)
} }
} }
@ -447,17 +400,15 @@ mod tests {
#[test] #[test]
fn merge_partial_keys() { fn merge_partial_keys() {
let keymap = hashmap! { let keymap = hashmap! {
Mode::Normal => Keymap::new( Mode::Normal => keymap!({ "Normal mode"
keymap!({ "Normal mode" "i" => normal_mode,
"i" => normal_mode, "无" => insert_mode,
"无" => insert_mode, "z" => jump_backward,
"z" => jump_backward, "g" => { "Merge into goto mode"
"g" => { "Merge into goto mode" "$" => goto_line_end,
"$" => goto_line_end, "g" => delete_char_forward,
"g" => delete_char_forward, },
}, })
})
)
}; };
let mut merged_keyamp = default(); let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone()); merge_keys(&mut merged_keyamp, keymap.clone());
@ -484,40 +435,52 @@ mod tests {
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap // Assumes that `g` is a node in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(), keymap.search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_line_end), &KeyTrie::MappableCommand(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode" "Leaf should be present in merged subnode"
); );
// Assumes that `gg` is in default keymap // Assumes that `gg` is in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(), keymap.search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::delete_char_forward), &KeyTrie::MappableCommand(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode" "Leaf should replace old leaf in merged subnode"
); );
// Assumes that `ge` is in default keymap // Assumes that `ge` is in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(), keymap.search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_last_line), &KeyTrie::MappableCommand(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node" "Old leaves in subnode should be present in merged node"
); );
assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1); assert!(
assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0); merged_keyamp
.get(&Mode::Normal)
.and_then(|key_trie| key_trie.node())
.unwrap()
.len()
> 1
);
assert!(
merged_keyamp
.get(&Mode::Insert)
.and_then(|key_trie| key_trie.node())
.unwrap()
.len()
> 0
);
} }
#[test] #[test]
fn order_should_be_set() { fn order_should_be_set() {
let keymap = hashmap! { let keymap = hashmap! {
Mode::Normal => Keymap::new( Mode::Normal => keymap!({ "Normal mode"
keymap!({ "Normal mode" "space" => { ""
"space" => { "" "s" => { ""
"s" => { "" "v" => vsplit,
"v" => vsplit, "c" => hsplit,
"c" => hsplit,
},
}, },
}) },
) })
}; };
let mut merged_keyamp = default(); let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone()); merge_keys(&mut merged_keyamp, keymap.clone());
@ -525,22 +488,19 @@ mod tests {
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works // Make sure mapping works
assert_eq!( assert_eq!(
keymap keymap.search(&[key!(' '), key!('s'), key!('v')]).unwrap(),
.root() &KeyTrie::MappableCommand(MappableCommand::vsplit),
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
&KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode" "Leaf should be present in merged subnode"
); );
// Make sure an order was set during merge // Make sure an order was set during merge
let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); let node = keymap.search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty()) assert!(!node.node().unwrap().order.as_slice().is_empty())
} }
#[test] #[test]
fn aliased_modes_are_same_in_default_keymap() { fn aliased_modes_are_same_in_default_keymap() {
let keymaps = Keymaps::default().map(); let keymaps = Keymaps::default().map();
let root = keymaps.get(&Mode::Normal).unwrap().root(); let root = keymaps.get(&Mode::Normal).unwrap();
assert_eq!( assert_eq!(
root.search(&[key!(' '), key!('w')]).unwrap(), root.search(&[key!(' '), key!('w')]).unwrap(),
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(), root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
@ -563,7 +523,7 @@ mod tests {
}, },
"j" | "k" => move_line_down, "j" | "k" => move_line_down,
}); });
let keymap = Keymap::new(normal_mode); let keymap = normal_mode;
let mut reverse_map = keymap.reverse_map(); let mut reverse_map = keymap.reverse_map();
// sort keybindings in order to have consistent tests // sort keybindings in order to have consistent tests
@ -611,7 +571,7 @@ mod tests {
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}; };
let expectation = Keymap::new(KeyTrie::Node(KeyTrieNode::new( let expectation = KeyTrie::Node(KeyTrieNode::new(
"", "",
hashmap! { hashmap! {
key => KeyTrie::Sequence(vec!{ key => KeyTrie::Sequence(vec!{
@ -628,7 +588,7 @@ mod tests {
}) })
}, },
vec![key], vec![key],
))); ));
assert_eq!(toml::from_str(keys), Ok(expectation)); assert_eq!(toml::from_str(keys), Ok(expectation));
} }

@ -1,10 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use super::macros::keymap; use super::macros::keymap;
use super::{Keymap, Mode}; use super::{KeyTrie, Mode};
use helix_core::hashmap; use helix_core::hashmap;
pub fn default() -> HashMap<Mode, Keymap> { pub fn default() -> HashMap<Mode, KeyTrie> {
let normal = keymap!({ "Normal mode" let normal = keymap!({ "Normal mode"
"h" | "left" => move_char_left, "h" | "left" => move_char_left,
"j" | "down" => move_visual_line_down, "j" | "down" => move_visual_line_down,
@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"A-i" | "A-down" => shrink_selection, "A-i" | "A-down" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling, "A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling, "A-n" | "A-right" => select_next_sibling,
"A-e" => move_parent_node_end,
"A-b" => move_parent_node_start,
"%" => select_all, "%" => select_all,
"x" => extend_line_below, "x" => extend_line_below,
@ -267,7 +269,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-v" | "v" => vsplit_new, "C-v" | "v" => vsplit_new,
}, },
}, },
"y" => yank_joined_to_clipboard, "y" => yank_to_clipboard,
"Y" => yank_main_selection_to_clipboard, "Y" => yank_main_selection_to_clipboard,
"p" => paste_clipboard_after, "p" => paste_clipboard_after,
"P" => paste_clipboard_before, "P" => paste_clipboard_before,
@ -338,6 +340,9 @@ pub fn default() -> HashMap<Mode, Keymap> {
"B" => extend_prev_long_word_start, "B" => extend_prev_long_word_start,
"E" => extend_next_long_word_end, "E" => extend_next_long_word_end,
"A-e" => extend_parent_node_end,
"A-b" => extend_parent_node_start,
"n" => extend_search_next, "n" => extend_search_next,
"N" => extend_search_prev, "N" => extend_search_prev,
@ -370,7 +375,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-h" | "backspace" | "S-backspace" => delete_char_backward, "C-h" | "backspace" | "S-backspace" => delete_char_backward,
"C-d" | "del" => delete_char_forward, "C-d" | "del" => delete_char_forward,
"C-j" | "ret" => insert_newline, "C-j" | "ret" => insert_newline,
"tab" => insert_tab, "tab" => smart_tab,
"S-tab" => insert_tab,
"up" => move_visual_line_up, "up" => move_visual_line_up,
"down" => move_visual_line_down, "down" => move_visual_line_down,
@ -382,8 +388,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"end" => goto_line_end_newline, "end" => goto_line_end_newline,
}); });
hashmap!( hashmap!(
Mode::Normal => Keymap::new(normal), Mode::Normal => normal,
Mode::Select => Keymap::new(select), Mode::Select => select,
Mode::Insert => Keymap::new(insert), Mode::Insert => insert,
) )
} }

@ -62,12 +62,11 @@ macro_rules! alt {
}; };
} }
/// Macro for defining the root of a `Keymap` object. Example: /// Macro for defining a `KeyTrie`. Example:
/// ///
/// ``` /// ```
/// # use helix_core::hashmap; /// # use helix_core::hashmap;
/// # use helix_term::keymap; /// # use helix_term::keymap;
/// # use helix_term::keymap::Keymap;
/// let normal_mode = keymap!({ "Normal mode" /// let normal_mode = keymap!({ "Normal mode"
/// "i" => insert_mode, /// "i" => insert_mode,
/// "g" => { "Goto" /// "g" => { "Goto"
@ -76,12 +75,12 @@ macro_rules! alt {
/// }, /// },
/// "j" | "down" => move_line_down, /// "j" | "down" => move_line_down,
/// }); /// });
/// let keymap = Keymap::new(normal_mode); /// let keymap = normal_mode;
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! keymap { macro_rules! keymap {
(@trie $cmd:ident) => { (@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd) $crate::keymap::KeyTrie::MappableCommand($crate::commands::MappableCommand::$cmd)
}; };
(@trie (@trie

@ -4,9 +4,8 @@ use helix_loader::VERSION_AND_GIT_HASH;
use helix_term::application::Application; use helix_term::application::Application;
use helix_term::args::Args; use helix_term::args::Args;
use helix_term::config::{Config, ConfigLoadError}; use helix_term::config::{Config, ConfigLoadError};
use std::path::PathBuf;
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { fn setup_logging(verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new(); let mut base_config = fern::Dispatch::new();
base_config = match verbosity { base_config = match verbosity {
@ -27,7 +26,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message message
)) ))
}) })
.chain(fern::log_file(logpath)?); .chain(fern::log_file(helix_loader::log_file())?);
base_config.chain(file_config).apply()?; base_config.chain(file_config).apply()?;
@ -41,12 +40,6 @@ fn main() -> Result<()> {
#[tokio::main] #[tokio::main]
async fn main_impl() -> Result<i32> { async fn main_impl() -> Result<i32> {
let logpath = helix_loader::log_file();
let parent = logpath.parent().unwrap();
if !parent.exists() {
std::fs::create_dir_all(parent).ok();
}
let help = format!( let help = format!(
"\ "\
{} {} {} {}
@ -68,7 +61,7 @@ FLAGS:
-g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml -g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml
-c, --config <file> Specifies a file to use for configuration -c, --config <file> Specifies a file to use for configuration
-v Increases logging verbosity each use for up to 3 times -v Increases logging verbosity each use for up to 3 times
--log Specifies a file to use for logging --log <file> Specifies a file to use for logging
(default file: {}) (default file: {})
-V, --version Prints version information -V, --version Prints version information
--vsplit Splits all given files vertically into different windows --vsplit Splits all given files vertically into different windows
@ -78,11 +71,14 @@ FLAGS:
VERSION_AND_GIT_HASH, VERSION_AND_GIT_HASH,
env!("CARGO_PKG_AUTHORS"), env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"), env!("CARGO_PKG_DESCRIPTION"),
logpath.display(), helix_loader::default_log_file().display(),
); );
let args = Args::parse_args().context("could not parse arguments")?; let args = Args::parse_args().context("could not parse arguments")?;
helix_loader::initialize_config_file(args.config_file.clone());
helix_loader::initialize_log_file(args.log_file.clone());
// Help has a higher priority and should be handled separately. // Help has a higher priority and should be handled separately.
if args.display_help { if args.display_help {
print!("{}", help); print!("{}", help);
@ -116,15 +112,7 @@ FLAGS:
return Ok(0); return Ok(0);
} }
let logpath = args.log_file.as_ref().cloned().unwrap_or(logpath); setup_logging(args.verbosity).context("failed to initialize logging")?;
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
let config_dir = helix_loader::config_dir();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
}
helix_loader::initialize_config_file(args.config_file.clone());
let config = match Config::load_default() { let config = match Config::load_default() {
Ok(config) => config, Ok(config) => config,

@ -110,6 +110,7 @@ impl Completion {
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> Self {
let preview_completion_insert = editor.config().preview_completion_insert;
let replace_mode = editor.config().completion_replace; let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server) // Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.item.preselect.unwrap_or(false)); items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
@ -143,7 +144,9 @@ impl Completion {
} }
}; };
let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{ let Some(range) =
util::lsp_range_to_range(doc.text(), edit.range, offset_encoding)
else {
return Transaction::new(doc.text()); return Transaction::new(doc.text());
}; };
@ -230,7 +233,7 @@ impl Completion {
match event { match event {
PromptEvent::Abort => {} PromptEvent::Abort => {}
PromptEvent::Update => { PromptEvent::Update if preview_completion_insert => {
// Update creates "ghost" transactions which are not sent to the // Update creates "ghost" transactions which are not sent to the
// lsp server to avoid messing up re-requesting completions. Once a // lsp server to avoid messing up re-requesting completions. Once a
// completion has been selected (with tab, c-n or c-p) it's always accepted whenever anything // completion has been selected (with tab, c-n or c-p) it's always accepted whenever anything
@ -263,6 +266,7 @@ impl Completion {
); );
doc.apply_temporary(&transaction, view.id); doc.apply_temporary(&transaction, view.id);
} }
PromptEvent::Update => {}
PromptEvent::Validate => { PromptEvent::Validate => {
if let Some(CompleteAction::Selected { savepoint }) = if let Some(CompleteAction::Selected { savepoint }) =
editor.last_completion.take() editor.last_completion.take()
@ -290,6 +294,8 @@ impl Completion {
}; };
// if more text was entered, remove it // if more text was entered, remove it
doc.restore(view, &savepoint, true); doc.restore(view, &savepoint, true);
// save an undo checkpoint before the completion
doc.append_changes_to_history(view);
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id, view.id,
@ -409,10 +415,18 @@ impl Completion {
_ => return false, _ => return false,
}; };
let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; }; let Some(language_server) = cx
.editor
.language_server_by_id(current_item.language_server_id)
else {
return false;
};
// This method should not block the compositor so we handle the response asynchronously. // This method should not block the compositor so we handle the response asynchronously.
let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; }; let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
else {
return false;
};
cx.callback( cx.callback(
future, future,

@ -43,6 +43,8 @@ pub struct EditorView {
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>), pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>, pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
/// Tracks if the terminal window is focused by reaction to terminal focus events
terminal_focused: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -71,6 +73,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
terminal_focused: true,
} }
} }
@ -163,15 +166,18 @@ impl EditorView {
Box::new(highlights) Box::new(highlights)
}; };
Self::render_gutter( let gutter_overflow = view.gutter_offset(doc) == 0;
editor, if !gutter_overflow {
doc, Self::render_gutter(
view, editor,
view.area, doc,
theme, view,
is_focused, view.area,
&mut line_decorations, theme,
); is_focused & self.terminal_focused,
&mut line_decorations,
);
}
if is_focused { if is_focused {
let cursor = doc let cursor = doc
@ -501,7 +507,9 @@ impl EditorView {
use helix_core::match_brackets; use helix_core::match_brackets;
let pos = doc.selection(view.id).primary().cursor(text); let pos = doc.selection(view.id).primary().cursor(text);
if let Some(pos) = match_brackets::find_matching_bracket(syntax, doc.text(), pos) { if let Some(pos) =
match_brackets::find_matching_bracket(syntax, doc.text().slice(..), pos)
{
// ensure col is on screen // ensure col is on screen
if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") { if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") {
return vec![(highlight, pos..pos + 1)]; return vec![(highlight, pos..pos + 1)];
@ -907,8 +915,9 @@ impl EditorView {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text); let cursor = doc.selection(view.id).primary().cursor(text);
let shift_position = let shift_position = |pos: usize| -> usize {
|pos: usize| -> usize { pos + cursor - trigger_offset }; (pos + cursor).saturating_sub(trigger_offset)
};
let tx = Transaction::change( let tx = Transaction::change(
doc.text(), doc.text(),
@ -943,6 +952,8 @@ impl EditorView {
self.handle_keymap_event(mode, cxt, event); self.handle_keymap_event(mode, cxt, event);
if self.keymaps.pending().is_empty() { if self.keymaps.pending().is_empty() {
cxt.editor.count = None cxt.editor.count = None
} else {
cxt.editor.selected_register = cxt.register.take();
} }
} }
} }
@ -1364,13 +1375,17 @@ impl Component for EditorView {
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::IdleTimeout => self.handle_idle_timeout(&mut cx), Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained => EventResult::Ignored(None), Event::FocusGained => {
self.terminal_focused = true;
EventResult::Consumed(None)
}
Event::FocusLost => { Event::FocusLost => {
if context.editor.config().auto_save { if context.editor.config().auto_save {
if let Err(e) = commands::typed::write_all_impl(context, false, false) { if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e)); context.editor.set_error(format!("{}", e));
} }
} }
self.terminal_focused = false;
EventResult::Consumed(None) EventResult::Consumed(None)
} }
} }

@ -1,239 +0,0 @@
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
#[cfg(test)]
mod test;
struct QueryAtom {
kind: QueryAtomKind,
atom: String,
ignore_case: bool,
inverse: bool,
}
impl QueryAtom {
fn new(atom: &str) -> Option<QueryAtom> {
let mut atom = atom.to_string();
let inverse = atom.starts_with('!');
if inverse {
atom.remove(0);
}
let mut kind = match atom.chars().next() {
Some('^') => QueryAtomKind::Prefix,
Some('\'') => QueryAtomKind::Substring,
_ if inverse => QueryAtomKind::Substring,
_ => QueryAtomKind::Fuzzy,
};
if atom.starts_with(['^', '\'']) {
atom.remove(0);
}
if atom.is_empty() {
return None;
}
if atom.ends_with('$') && !atom.ends_with("\\$") {
atom.pop();
kind = if kind == QueryAtomKind::Prefix {
QueryAtomKind::Exact
} else {
QueryAtomKind::Postfix
}
}
Some(QueryAtom {
kind,
atom: atom.replace('\\', ""),
// not ideal but fuzzy_matches only knows ascii uppercase so more consistent
// to behave the same
ignore_case: kind != QueryAtomKind::Fuzzy
&& atom.chars().all(|c| c.is_ascii_lowercase()),
inverse,
})
}
fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec<usize>) -> bool {
// for inverse there are no indices to return
// just return whether we matched
if self.inverse {
return self.matches(matcher, item);
}
let buf;
let item = if self.ignore_case {
buf = item.to_ascii_lowercase();
&buf
} else {
item
};
let off = match self.kind {
QueryAtomKind::Fuzzy => {
if let Some((_, fuzzy_indices)) = matcher.fuzzy_indices(item, &self.atom) {
indices.extend_from_slice(&fuzzy_indices);
return true;
} else {
return false;
}
}
QueryAtomKind::Substring => {
if let Some(off) = item.find(&self.atom) {
off
} else {
return false;
}
}
QueryAtomKind::Prefix if item.starts_with(&self.atom) => 0,
QueryAtomKind::Postfix if item.ends_with(&self.atom) => item.len() - self.atom.len(),
QueryAtomKind::Exact if item == self.atom => 0,
_ => return false,
};
indices.extend(off..(off + self.atom.len()));
true
}
fn matches(&self, matcher: &Matcher, item: &str) -> bool {
let buf;
let item = if self.ignore_case {
buf = item.to_ascii_lowercase();
&buf
} else {
item
};
let mut res = match self.kind {
QueryAtomKind::Fuzzy => matcher.fuzzy_match(item, &self.atom).is_some(),
QueryAtomKind::Substring => item.contains(&self.atom),
QueryAtomKind::Prefix => item.starts_with(&self.atom),
QueryAtomKind::Postfix => item.ends_with(&self.atom),
QueryAtomKind::Exact => item == self.atom,
};
if self.inverse {
res = !res;
}
res
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum QueryAtomKind {
/// Item is a fuzzy match of this behaviour
///
/// Usage: `foo`
Fuzzy,
/// Item contains query atom as a continuous substring
///
/// Usage `'foo`
Substring,
/// Item starts with query atom
///
/// Usage: `^foo`
Prefix,
/// Item ends with query atom
///
/// Usage: `foo$`
Postfix,
/// Item is equal to query atom
///
/// Usage `^foo$`
Exact,
}
#[derive(Default)]
pub struct FuzzyQuery {
first_fuzzy_atom: Option<String>,
query_atoms: Vec<QueryAtom>,
}
fn query_atoms(query: &str) -> impl Iterator<Item = &str> + '_ {
let mut saw_backslash = false;
query.split(move |c| {
saw_backslash = match c {
' ' if !saw_backslash => return true,
'\\' => true,
_ => false,
};
false
})
}
impl FuzzyQuery {
pub fn refine(&self, query: &str, old_query: &str) -> (FuzzyQuery, bool) {
// TODO: we could be a lot smarter about this
let new_query = Self::new(query);
let mut is_refinement = query.starts_with(old_query);
// if the last atom is an inverse atom adding more text to it
// will actually increase the number of matches and we can not refine
// the matches.
if is_refinement && !self.query_atoms.is_empty() {
let last_idx = self.query_atoms.len() - 1;
if self.query_atoms[last_idx].inverse
&& self.query_atoms[last_idx].atom != new_query.query_atoms[last_idx].atom
{
is_refinement = false;
}
}
(new_query, is_refinement)
}
pub fn new(query: &str) -> FuzzyQuery {
let mut first_fuzzy_query = None;
let query_atoms = query_atoms(query)
.filter_map(|atom| {
let atom = QueryAtom::new(atom)?;
if atom.kind == QueryAtomKind::Fuzzy && first_fuzzy_query.is_none() {
first_fuzzy_query = Some(atom.atom);
None
} else {
Some(atom)
}
})
.collect();
FuzzyQuery {
first_fuzzy_atom: first_fuzzy_query,
query_atoms,
}
}
pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
// use the rank of the first fuzzzy query for the rank, because merging ranks is not really possible
// this behaviour matches fzf and skim
let score = self
.first_fuzzy_atom
.as_ref()
.map_or(Some(0), |atom| matcher.fuzzy_match(item, atom))?;
if self
.query_atoms
.iter()
.any(|atom| !atom.matches(matcher, item))
{
return None;
}
Some(score)
}
pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else(
|| Some((0, Vec::new())),
|atom| matcher.fuzzy_indices(item, atom),
)?;
// fast path for the common case of just a single atom
if self.query_atoms.is_empty() {
return Some((score, indices));
}
for atom in &self.query_atoms {
if !atom.indices(matcher, item, &mut indices) {
return None;
}
}
// deadup and remove duplicate matches
indices.sort_unstable();
indices.dedup();
Some((score, indices))
}
}

@ -1,47 +0,0 @@
use crate::ui::fuzzy_match::FuzzyQuery;
use crate::ui::fuzzy_match::Matcher;
fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
let query = FuzzyQuery::new(query);
let matcher = Matcher::default();
items
.iter()
.filter_map(|item| {
let (_, indices) = query.fuzzy_indices(item, &matcher)?;
let matched_string = indices
.iter()
.map(|&pos| item.chars().nth(pos).unwrap())
.collect();
Some(matched_string)
})
.collect()
}
#[test]
fn match_single_value() {
let matches = run_test("foo", &["foobar", "foo", "bar"]);
assert_eq!(matches, &["foo", "foo"])
}
#[test]
fn match_multiple_values() {
let matches = run_test(
"foo bar",
&["foo bar", "foo bar", "bar foo", "bar", "foo"],
);
assert_eq!(matches, &["foobar", "foobar", "barfoo"])
}
#[test]
fn space_escape() {
let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["foo bar"])
}
#[test]
fn trim() {
let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["bar foo"])
}

@ -62,7 +62,7 @@ impl Component for SignatureHelp {
}); });
let sig_text = crate::ui::markdown::highlighted_code_block( let sig_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(), &self.signature,
&self.language, &self.language,
Some(&cx.editor.theme), Some(&cx.editor.theme),
Arc::clone(&self.config_loader), Arc::clone(&self.config_loader),
@ -109,7 +109,7 @@ impl Component for SignatureHelp {
let max_text_width = (viewport.0 - PADDING).min(120); let max_text_width = (viewport.0 - PADDING).min(120);
let signature_text = crate::ui::markdown::highlighted_code_block( let signature_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(), &self.signature,
&self.language, &self.language,
None, None,
Arc::clone(&self.config_loader), Arc::clone(&self.config_loader),

@ -10,14 +10,14 @@ use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use helix_core::{ use helix_core::{
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax}, syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
Rope, RopeSlice,
}; };
use helix_view::{ use helix_view::{
graphics::{Margin, Rect, Style}, graphics::{Margin, Rect, Style},
Theme, Theme,
}; };
fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> { fn styled_multiline_text<'a>(text: &str, style: Style) -> Text<'a> {
let spans: Vec<_> = text let spans: Vec<_> = text
.lines() .lines()
.map(|line| Span::styled(line.to_string(), style)) .map(|line| Span::styled(line.to_string(), style))
@ -27,7 +27,7 @@ fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
} }
pub fn highlighted_code_block<'a>( pub fn highlighted_code_block<'a>(
text: String, text: &str,
language: &str, language: &str,
theme: Option<&Theme>, theme: Option<&Theme>,
config_loader: Arc<syntax::Loader>, config_loader: Arc<syntax::Loader>,
@ -45,13 +45,13 @@ pub fn highlighted_code_block<'a>(
None => return styled_multiline_text(text, code_style), None => return styled_multiline_text(text, code_style),
}; };
let rope = Rope::from(text.as_ref()); let ropeslice = RopeSlice::from(text);
let syntax = config_loader let syntax = config_loader
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name( .language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(), language.into(),
)) ))
.and_then(|config| config.highlight_config(theme.scopes())) .and_then(|config| config.highlight_config(theme.scopes()))
.and_then(|config| Syntax::new(&rope, config, Arc::clone(&config_loader))); .and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
let syntax = match syntax { let syntax = match syntax {
Some(s) => s, Some(s) => s,
@ -59,7 +59,7 @@ pub fn highlighted_code_block<'a>(
}; };
let highlight_iter = syntax let highlight_iter = syntax
.highlight_iter(rope.slice(..), None, None) .highlight_iter(ropeslice, None, None)
.map(|e| e.unwrap()); .map(|e| e.unwrap());
let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> = let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
if let Some(spans) = additional_highlight_spans { if let Some(spans) = additional_highlight_spans {
@ -267,7 +267,7 @@ impl Markdown {
CodeBlockKind::Indented => "", CodeBlockKind::Indented => "",
}; };
let tui_text = highlighted_code_block( let tui_text = highlighted_code_block(
text.to_string(), &text,
language, language,
theme, theme,
Arc::clone(&self.config_loader), Arc::clone(&self.config_loader),

@ -1,22 +1,22 @@
use std::{borrow::Cow, path::PathBuf}; use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
use crate::{ use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult}, compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift, ctrl, key, shift,
}; };
use helix_core::fuzzy::MATCHER;
use nucleo::pattern::{AtomKind, CaseMatching, Pattern};
use nucleo::{Config, Utf32Str};
use tui::{buffer::Buffer as Surface, widgets::Table}; use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row}; pub use tui::widgets::{Cell, Row};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
use fuzzy_matcher::FuzzyMatcher;
use helix_view::{graphics::Rect, Editor};
use tui::layout::Constraint; use tui::layout::Constraint;
pub trait Item { pub trait Item: Sync + Send + 'static {
/// Additional editor state that is used for label calculation. /// Additional editor state that is used for label calculation.
type Data; type Data: Sync + Send + 'static;
fn format(&self, data: &Self::Data) -> Row; fn format(&self, data: &Self::Data) -> Row;
@ -51,9 +51,8 @@ pub struct Menu<T: Item> {
cursor: Option<usize>, cursor: Option<usize>,
matcher: Box<Matcher>,
/// (index, score) /// (index, score)
matches: Vec<(usize, i64)>, matches: Vec<(u32, u32)>,
widths: Vec<Constraint>, widths: Vec<Constraint>,
@ -75,11 +74,10 @@ impl<T: Item> Menu<T> {
editor_data: <T as Item>::Data, editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self { ) -> Self {
let matches = (0..options.len()).map(|i| (i, 0)).collect(); let matches = (0..options.len() as u32).map(|i| (i, 0)).collect();
Self { Self {
options, options,
editor_data, editor_data,
matcher: Box::new(Matcher::default().ignore_case()),
matches, matches,
cursor: None, cursor: None,
widths: Vec::new(), widths: Vec::new(),
@ -94,20 +92,19 @@ impl<T: Item> Menu<T> {
pub fn score(&mut self, pattern: &str) { pub fn score(&mut self, pattern: &str) {
// reuse the matches allocation // reuse the matches allocation
self.matches.clear(); self.matches.clear();
self.matches.extend( let mut matcher = MATCHER.lock();
self.options matcher.config = Config::DEFAULT;
.iter() let pattern = Pattern::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy);
.enumerate() let mut buf = Vec::new();
.filter_map(|(index, option)| { let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
let text = option.filter_text(&self.editor_data); let text = option.filter_text(&self.editor_data);
// TODO: using fuzzy_indices could give us the char idx for match highlighting pattern
self.matcher .score(Utf32Str::new(&text, &mut buf), &mut matcher)
.fuzzy_match(&text, pattern) .map(|score| (i as u32, score))
.map(|score| (index, score)) });
}), self.matches.extend(matches);
); self.matches
// Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority .sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
self.matches.sort_by_key(|(_, score)| -score);
// reset cursor position // reset cursor position
self.cursor = None; self.cursor = None;
@ -201,7 +198,7 @@ impl<T: Item> Menu<T> {
self.cursor.and_then(|cursor| { self.cursor.and_then(|cursor| {
self.matches self.matches
.get(cursor) .get(cursor)
.map(|(index, _score)| &self.options[*index]) .map(|(index, _score)| &self.options[*index as usize])
}) })
} }
@ -209,7 +206,7 @@ impl<T: Item> Menu<T> {
self.cursor.and_then(|cursor| { self.cursor.and_then(|cursor| {
self.matches self.matches
.get(cursor) .get(cursor)
.map(|(index, _score)| &mut self.options[*index]) .map(|(index, _score)| &mut self.options[*index as usize])
}) })
} }
@ -247,6 +244,21 @@ impl<T: Item + 'static> Component for Menu<T> {
compositor.pop(); compositor.pop();
})); }));
// Ignore tab key when supertab is turned on in order not to interfere
// with it. (Is there a better way to do this?)
if (event == key!(Tab) || event == shift!(Tab))
&& cx.editor.config().auto_completion
&& matches!(
cx.editor.config().smart_tab,
Some(SmartTabConfig {
enable: true,
supersede_menu: true,
})
)
{
return EventResult::Ignored(None);
}
match event { match event {
// esc or ctrl-c aborts the completion and closes the menu // esc or ctrl-c aborts the completion and closes the menu
key!(Esc) | ctrl!('c') => { key!(Esc) | ctrl!('c') => {
@ -317,7 +329,7 @@ impl<T: Item + 'static> Component for Menu<T> {
.iter() .iter()
.map(|(index, _score)| { .map(|(index, _score)| {
// (index, self.options.get(*index).unwrap()) // get_unchecked // (index, self.options.get(*index).unwrap()) // get_unchecked
&self.options[*index] // get_unchecked &self.options[*index as usize] // get_unchecked
}) })
.collect(); .collect();

@ -1,7 +1,6 @@
mod completion; mod completion;
mod document; mod document;
pub(crate) mod editor; pub(crate) mod editor;
mod fuzzy_match;
mod info; mod info;
pub mod lsp; pub mod lsp;
mod markdown; mod markdown;
@ -21,7 +20,7 @@ pub use completion::{Completion, CompletionItem};
pub use editor::EditorView; pub use editor::EditorView;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; pub use picker::{DynamicPicker, FileLocation, Picker};
pub use popup::Popup; pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent}; pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner}; pub use spinner::{ProgressSpinners, Spinner};
@ -64,7 +63,7 @@ pub fn regex_prompt(
prompt: std::borrow::Cow<'static, str>, prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static, fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
) { ) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let doc_id = view.doc; let doc_id = view.doc;
@ -111,7 +110,7 @@ pub fn regex_prompt(
view.jumps.push((doc_id, snapshot.clone())); view.jumps.push((doc_id, snapshot.clone()));
} }
fun(cx.editor, regex, event); fun(cx, regex, event);
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff); view.ensure_cursor_in_view(doc, config.scrolloff);
@ -142,23 +141,21 @@ pub fn regex_prompt(
}; };
cx.jobs.callback(callback); cx.jobs.callback(callback);
} else {
// Update
// TODO: mark command line as error
} }
} }
} }
} }
} }
}, },
); )
.with_language("regex", std::sync::Arc::clone(&cx.editor.syn_loader));
// Calculate initial completion // Calculate initial completion
prompt.recalculate_completion(cx.editor); prompt.recalculate_completion(cx.editor);
// prompt // prompt
cx.push_layer(Box::new(prompt)); cx.push_layer(Box::new(prompt));
} }
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> { pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder}; use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant; use std::time::Instant;
@ -176,6 +173,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
.git_ignore(config.file_picker.git_ignore) .git_ignore(config.file_picker.git_ignore)
.git_global(config.file_picker.git_global) .git_global(config.file_picker.git_global)
.git_exclude(config.file_picker.git_exclude) .git_exclude(config.file_picker.git_exclude)
.sort_by_file_name(|name1, name2| name1.cmp(name2))
.max_depth(config.file_picker.max_depth) .max_depth(config.file_picker.max_depth)
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks)); .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
@ -192,59 +190,46 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
.build() .build()
.expect("failed to build excluded_types"); .expect("failed to build excluded_types");
walk_builder.types(excluded_types); walk_builder.types(excluded_types);
// We want files along with their modification date for sorting
let files = walk_builder.build().filter_map(|entry| { let files = walk_builder.build().filter_map(|entry| {
let entry = entry.ok()?; let entry = entry.ok()?;
// This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir if !entry.file_type()?.is_file() {
if entry.file_type()?.is_file() { return None;
Some(entry.into_path())
} else {
None
} }
Some(entry.into_path())
}); });
// Cap the number of files if we aren't in a git project, preventing
// hangs when using the picker in your home directory
let mut files: Vec<PathBuf> = if root.join(".git").exists() {
files.collect()
} else {
// const MAX: usize = 8192;
const MAX: usize = 100_000;
files.take(MAX).collect()
};
files.sort();
log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
FilePicker::new( let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
files, if let Err(e) = cx.editor.open(path, action) {
root, let err = if let Some(err) = e.source() {
move |cx, path: &PathBuf, action| { format!("{}", err)
if let Err(e) = cx.editor.open(path, action) { } else {
let err = if let Some(err) = e.source() { format!("unable to open \"{}\"", path.display())
format!("{}", err) };
} else { cx.editor.set_error(err);
format!("unable to open \"{}\"", path.display()) }
}; })
cx.editor.set_error(err); .with_preview(|_editor, path| Some((path.clone().into(), None)));
let injector = picker.injector();
std::thread::spawn(move || {
for file in files {
if injector.push(file).is_err() {
break;
} }
}, }
|_editor, path| Some((path.clone().into(), None)), });
) picker
} }
pub mod completers { pub mod completers {
use crate::ui::prompt::Completion; use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use helix_core::fuzzy::fuzzy_match;
use fuzzy_matcher::FuzzyMatcher;
use helix_core::syntax::LanguageServerFeature; use helix_core::syntax::LanguageServerFeature;
use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme; use helix_view::theme;
use helix_view::{editor::Config, Editor}; use helix_view::{editor::Config, Editor};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Reverse;
pub type Completer = fn(&Editor, &str) -> Vec<Completion>; pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
@ -253,31 +238,16 @@ pub mod completers {
} }
pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> { pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
let mut names: Vec<_> = editor let names = editor.documents.values().map(|doc| {
.documents doc.relative_path()
.values() .map(|p| p.display().to_string().into())
.map(|doc| { .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME))
let name = doc });
.relative_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
((0..), Cow::from(name))
})
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(_range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names fuzzy_match(input, names, true)
.into_iter()
.map(|(name, _)| ((0..), name))
.collect()
} }
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> { pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@ -290,26 +260,10 @@ pub mod completers {
names.sort(); names.sort();
names.dedup(); names.dedup();
let mut names: Vec<_> = names fuzzy_match(input, names, false)
.into_iter() .into_iter()
.map(|name| ((0..), Cow::from(name))) .map(|(name, _)| ((0..), name.into()))
.collect(); .collect()
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(_range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by(|(name1, score1), (name2, score2)| {
(Reverse(*score1), name1).cmp(&(Reverse(*score2), name2))
});
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
} }
/// Recursive function to get all keys from this value and add them to vec /// Recursive function to get all keys from this value and add them to vec
@ -336,22 +290,22 @@ pub mod completers {
keys keys
}); });
let matcher = Matcher::default(); fuzzy_match(input, &*KEYS, false)
let mut matches: Vec<_> = KEYS
.iter()
.filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
matches
.into_iter() .into_iter()
.map(|(name, _)| ((0..), name.into())) .map(|(name, _)| ((0..), name.into()))
.collect() .collect()
} }
pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> { pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> {
filename_impl(editor, input, |entry| { filename_with_git_ignore(editor, input, true)
}
pub fn filename_with_git_ignore(
editor: &Editor,
input: &str,
git_ignore: bool,
) -> Vec<Completion> {
filename_impl(editor, input, git_ignore, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir { if is_dir {
@ -363,8 +317,6 @@ pub mod completers {
} }
pub fn language(editor: &Editor, input: &str) -> Vec<Completion> { pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
let text: String = "text".into(); let text: String = "text".into();
let language_ids = editor let language_ids = editor
@ -373,27 +325,13 @@ pub mod completers {
.map(|config| &config.language_id) .map(|config| &config.language_id)
.chain(std::iter::once(&text)); .chain(std::iter::once(&text));
let mut matches: Vec<_> = language_ids fuzzy_match(input, language_ids, false)
.filter_map(|language_id| {
matcher
.fuzzy_match(language_id, input)
.map(|score| (language_id, score))
})
.collect();
matches.sort_unstable_by(|(language1, score1), (language2, score2)| {
(Reverse(*score1), language1).cmp(&(Reverse(*score2), language2))
});
matches
.into_iter() .into_iter()
.map(|(language, _score)| ((0..), language.clone().into())) .map(|(name, _)| ((0..), name.to_owned().into()))
.collect() .collect()
} }
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
let Some(options) = doc!(editor) let Some(options) = doc!(editor)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
@ -401,28 +339,22 @@ pub mod completers {
return vec![]; return vec![];
}; };
let mut matches: Vec<_> = options fuzzy_match(input, &options.commands, false)
.commands
.iter()
.filter_map(|command| {
matcher
.fuzzy_match(command, input)
.map(|score| (command, score))
})
.collect();
matches.sort_unstable_by(|(command1, score1), (command2, score2)| {
(Reverse(*score1), command1).cmp(&(Reverse(*score2), command2))
});
matches
.into_iter() .into_iter()
.map(|(command, _score)| ((0..), command.clone().into())) .map(|(name, _)| ((0..), name.to_owned().into()))
.collect() .collect()
} }
pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> { pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
filename_impl(editor, input, |entry| { directory_with_git_ignore(editor, input, true)
}
pub fn directory_with_git_ignore(
editor: &Editor,
input: &str,
git_ignore: bool,
) -> Vec<Completion> {
filename_impl(editor, input, git_ignore, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir { if is_dir {
@ -445,7 +377,12 @@ pub mod completers {
} }
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs. // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
fn filename_impl<F>(_editor: &Editor, input: &str, filter_fn: F) -> Vec<Completion> fn filename_impl<F>(
_editor: &Editor,
input: &str,
git_ignore: bool,
filter_fn: F,
) -> Vec<Completion>
where where
F: Fn(&ignore::DirEntry) -> FileMatch, F: Fn(&ignore::DirEntry) -> FileMatch,
{ {
@ -476,7 +413,7 @@ pub mod completers {
match path.parent() { match path.parent() {
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(), Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
// Path::new("h")'s parent is Some("")... // Path::new("h")'s parent is Some("")...
_ => std::env::current_dir().expect("couldn't determine current directory"), _ => helix_loader::current_working_dir(),
} }
}; };
@ -485,9 +422,10 @@ pub mod completers {
let end = input.len()..; let end = input.len()..;
let mut files: Vec<_> = WalkBuilder::new(&dir) let files = WalkBuilder::new(&dir)
.hidden(false) .hidden(false)
.follow_links(false) // We're scanning over depth 1 .follow_links(false) // We're scanning over depth 1
.git_ignore(git_ignore)
.max_depth(Some(1)) .max_depth(Some(1))
.build() .build()
.filter_map(|file| { .filter_map(|file| {
@ -516,43 +454,25 @@ pub mod completers {
path.push(""); path.push("");
} }
let path = path.to_str()?.to_owned(); let path = path.into_os_string().into_string().ok()?;
Some((end.clone(), Cow::from(path))) Some(Cow::from(path))
}) })
}) // TODO: unwrap or skip }) // TODO: unwrap or skip
.filter(|(_, path)| !path.is_empty()) // TODO .filter(|path| !path.is_empty());
.collect();
// if empty, return a list of dirs and files in current dir // if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name { if let Some(file_name) = file_name {
let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort.
let mut matches: Vec<_> = files
.into_iter()
.filter_map(|(_range, file)| {
matcher
.fuzzy_match(&file, &file_name)
.map(|score| (file, score))
})
.collect();
let range = (input.len().saturating_sub(file_name.len()))..; let range = (input.len().saturating_sub(file_name.len()))..;
fuzzy_match(&file_name, files, true)
matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
(Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
});
files = matches
.into_iter() .into_iter()
.map(|(file, _)| (range.clone(), file)) .map(|(name, _)| (range.clone(), name))
.collect(); .collect()
// TODO: complete to longest common match // TODO: complete to longest common match
} else { } else {
let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2)); files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
files
} }
files
} }
} }

File diff suppressed because it is too large Load Diff

@ -1,7 +1,9 @@
use crate::compositor::{Component, Compositor, Context, Event, EventResult}; use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui}; use crate::{alt, ctrl, key, shift, ui};
use helix_core::syntax;
use helix_view::input::KeyEvent; use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode; use helix_view::keyboard::KeyCode;
use std::sync::Arc;
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Widget}; use tui::widgets::{Block, Borders, Widget};
@ -32,6 +34,7 @@ pub struct Prompt {
callback_fn: CallbackFn, callback_fn: CallbackFn,
pub doc_fn: DocFn, pub doc_fn: DocFn,
next_char_handler: Option<PromptCharHandler>, next_char_handler: Option<PromptCharHandler>,
language: Option<(&'static str, Arc<syntax::Loader>)>,
} }
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
@ -83,6 +86,7 @@ impl Prompt {
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
doc_fn: Box::new(|_| None), doc_fn: Box::new(|_| None),
next_char_handler: None, next_char_handler: None,
language: None,
} }
} }
@ -94,6 +98,11 @@ impl Prompt {
self self
} }
pub fn with_language(mut self, language: &'static str, loader: Arc<syntax::Loader>) -> Self {
self.language = Some((language, loader));
self
}
pub fn line(&self) -> &String { pub fn line(&self) -> &String {
&self.line &self.line
} }
@ -297,8 +306,8 @@ impl Prompt {
direction: CompletionDirection, direction: CompletionDirection,
) { ) {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort); (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
let values = match cx.editor.registers.read(register) { let mut values = match cx.editor.registers.read(register, cx.editor) {
Some(values) if !values.is_empty() => values, Some(values) if values.len() > 0 => values.rev(),
_ => return, _ => return,
}; };
@ -306,13 +315,16 @@ impl Prompt {
let index = match direction { let index = match direction {
CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1), CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
CompletionDirection::Backward => { CompletionDirection::Backward => self
self.history_pos.unwrap_or(values.len()).saturating_sub(1) .history_pos
} .unwrap_or_else(|| values.len())
.saturating_sub(1),
} }
.min(end); .min(end);
self.line = values[index].clone(); self.line = values.nth(index).unwrap().to_string();
// Appease the borrow checker.
drop(values);
self.history_pos = Some(index); self.history_pos = Some(index);
@ -356,6 +368,7 @@ impl Prompt {
let completion_color = theme.get("ui.menu"); let completion_color = theme.get("ui.menu");
let selected_color = theme.get("ui.menu.selected"); let selected_color = theme.get("ui.menu.selected");
let suggestion_color = theme.get("ui.text.inactive"); let suggestion_color = theme.get("ui.text.inactive");
let background = theme.get("ui.background");
// completion // completion
let max_len = self let max_len = self
@ -451,33 +464,32 @@ impl Prompt {
} }
let line = area.height - 1; let line = area.height - 1;
surface.clear_with(area.clip_top(line), background);
// render buffer text // render buffer text
surface.set_string(area.x, area.y + line, &self.prompt, prompt_color); surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
let (input, is_suggestion): (Cow<str>, bool) = if self.line.is_empty() { let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
// latest value in the register list if self.line.is_empty() {
match self // Show the most recently entered value as a suggestion.
if let Some(suggestion) = self
.history_register .history_register
.and_then(|reg| cx.editor.registers.last(reg)) .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
.map(|entry| entry.into())
{ {
Some(value) => (value, true), surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
None => (Cow::from(""), false),
} }
} else if let Some((language, loader)) = self.language.as_ref() {
let mut text: ui::text::Text = crate::ui::markdown::highlighted_code_block(
&self.line,
language,
Some(&cx.editor.theme),
loader.clone(),
None,
)
.into();
text.render(line_area, surface, cx);
} else { } else {
(self.line.as_str().into(), false) surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color);
}; }
surface.set_string(
area.x + self.prompt.len() as u16,
area.y + line,
&input,
if is_suggestion {
suggestion_color
} else {
prompt_color
},
);
} }
} }
@ -558,25 +570,29 @@ impl Component for Prompt {
} else { } else {
let last_item = self let last_item = self
.history_register .history_register
.and_then(|reg| cx.editor.registers.last(reg).cloned()) .and_then(|reg| cx.editor.registers.first(reg, cx.editor))
.map(|entry| entry.into()) .map(|entry| entry.to_string())
.unwrap_or_else(|| Cow::from("")); .unwrap_or_else(|| String::from(""));
// handle executing with last command in history if nothing entered // handle executing with last command in history if nothing entered
let input: Cow<str> = if self.line.is_empty() { let input = if self.line.is_empty() {
last_item &last_item
} else { } else {
if last_item != self.line { if last_item != self.line {
// store in history // store in history
if let Some(register) = self.history_register { if let Some(register) = self.history_register {
cx.editor.registers.push(register, self.line.clone()); if let Err(err) =
cx.editor.registers.push(register, self.line.clone())
{
cx.editor.set_error(err.to_string());
}
}; };
} }
self.line.as_str().into() &self.line
}; };
(self.callback_fn)(cx, &input, PromptEvent::Validate); (self.callback_fn)(cx, input, PromptEvent::Validate);
return close_fn; return close_fn;
} }
@ -608,25 +624,16 @@ impl Component for Prompt {
self.completion = cx self.completion = cx
.editor .editor
.registers .registers
.inner() .iter_preview()
.iter() .map(|(ch, preview)| (0.., format!("{} {}", ch, &preview).into()))
.map(|(ch, reg)| {
let content = reg
.read()
.get(0)
.and_then(|s| s.lines().next().to_owned())
.unwrap_or_default();
(0.., format!("{} {}", ch, &content).into())
})
.collect(); .collect();
self.next_char_handler = Some(Box::new(|prompt, c, context| { self.next_char_handler = Some(Box::new(|prompt, c, context| {
prompt.insert_str( prompt.insert_str(
context &context
.editor .editor
.registers .registers
.read(c) .first(c, context.editor)
.and_then(|r| r.first()) .unwrap_or_default(),
.map_or("", |r| r.as_str()),
context.editor, context.editor,
); );
})); }));

@ -145,6 +145,7 @@ where
helix_view::editor::StatusLineElement::FileModificationIndicator => { helix_view::editor::StatusLineElement::FileModificationIndicator => {
render_file_modification_indicator render_file_modification_indicator
} }
helix_view::editor::StatusLineElement::ReadOnlyIndicator => render_read_only_indicator,
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding, helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending, helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type, helix_view::editor::StatusLineElement::FileType => render_file_type,
@ -160,6 +161,7 @@ where
helix_view::editor::StatusLineElement::Separator => render_separator, helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer, helix_view::editor::StatusLineElement::Spacer => render_spacer,
helix_view::editor::StatusLineElement::VersionControl => render_version_control, helix_view::editor::StatusLineElement::VersionControl => render_version_control,
helix_view::editor::StatusLineElement::Register => render_register,
} }
} }
@ -441,6 +443,19 @@ where
write(context, title, None); write(context, title, None);
} }
fn render_read_only_indicator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = if context.doc.readonly {
" [readonly] "
} else {
""
}
.to_string();
write(context, title, None);
}
fn render_file_base_name<F>(context: &mut RenderContext, write: F) fn render_file_base_name<F>(context: &mut RenderContext, write: F)
where where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
@ -489,3 +504,12 @@ where
write(context, head, None); write(context, head, None);
} }
fn render_register<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
if let Some(reg) = context.editor.selected_register {
write(context, format!(" reg={} ", reg), None)
}
}

@ -18,6 +18,7 @@ mod test {
mod auto_indent; mod auto_indent;
mod auto_pairs; mod auto_pairs;
mod commands; mod commands;
mod languages;
mod movement; mod movement;
mod prompt; mod prompt;
mod splits; mod splits;

@ -2,7 +2,7 @@ use helix_core::{auto_pairs::DEFAULT_PAIRS, hashmap};
use super::*; use super::*;
const LINE_END: &str = helix_core::DEFAULT_LINE_ENDING.as_str(); const LINE_END: &str = helix_core::NATIVE_LINE_ENDING.as_str();
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> { fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
DEFAULT_PAIRS.iter().filter(|(open, close)| open != close) DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)

@ -2,6 +2,7 @@ use helix_term::application::Application;
use super::*; use super::*;
mod movement;
mod write; mod write;
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -426,3 +427,56 @@ async fn test_delete_char_forward() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_insert_with_indent() -> anyhow::Result<()> {
const INPUT: &str = "\
#[f|]#n foo() {
if let Some(_) = None {
}
\x20
}
fn bar() {
}";
// insert_at_line_start
test((
INPUT,
":lang rust<ret>%<A-s>I",
"\
#[f|]#n foo() {
#(i|)#f let Some(_) = None {
#(\n|)#\
\x20 #(}|)#
#(\x20|)#
#(}|)#
#(\n|)#\
#(f|)#n bar() {
#(\n|)#\
#(}|)#",
))
.await?;
// insert_at_line_end
test((
INPUT,
":lang rust<ret>%<A-s>A",
"\
fn foo() {#[\n|]#\
\x20 if let Some(_) = None {#(\n|)#\
\x20 #(\n|)#\
\x20 }#(\n|)#\
\x20#(\n|)#\
}#(\n|)#\
#(\n|)#\
fn bar() {#(\n|)#\
\x20 #(\n|)#\
}#(|)#",
))
.await?;
Ok(())
}

@ -0,0 +1,452 @@
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_move_parent_node_end() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
"no#["|]#
}
}
"##}),
"<A-e>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[\n|]#
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[\n|]#
}
}
"}),
"<A-e>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"
}#[\n|]#
}
"}),
),
// select mode extends
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
#["no"|]#
}
}
"##}),
"v<A-e><A-e>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
#[\"no\"
}\n|]#
}
"}),
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_move_parent_node_start() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
"no#["|]#
}
}
"##}),
"<A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
#[\"|]#no\"
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[\n|]#
}
}
"}),
"<A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else #[{|]#
\"no\"
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else #[{|]#
\"no\"
}
}
"}),
"<A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} #[e|]#lse {
\"no\"
}
}
"}),
),
// select mode extends
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
#["no"|]#
}
}
"##}),
"v<A-b><A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else #[|{
]#\"no\"
}
}
"}),
),
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
#["no"|]#
}
}
"##}),
"v<A-b><A-b><A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} #[|else {
]#\"no\"
}
}
"}),
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
"no#["|]#
}
}
"##}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[|\n]#
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[\n|]#
}
}
"}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"
}#[|\n]#
}
"}),
),
// appending to the end of a line should still look at the current
// line, not the next one
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no#[\"|]#
}
}
"}),
"a<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"
}#[\n|]#
}
"}),
),
// before cursor is all whitespace, so insert tab
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
#[\"no\"|]#
}
}
"}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
#[|\"no\"]#
}
}
"}),
),
// if selection spans multiple lines, it should still only look at the
// line on which the head is
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"
} else {
\"no\"|]#
}
}
"}),
"a<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"
}#[\n|]#
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"
} else {
\"no\"|]#
}
}
"}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[|\"yes\"
} else {
\"no\"]#
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
#[l|]#et result = if true {
#(\"yes\"
} else {
\"no\"|)#
}
}
"}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
#[|l]#et result = if true {
#(|\"yes\"
} else {
\"no\")#
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"#[\n|]#
} else {
\"no\"#(\n|)#
}
}
"}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
}#[| ]#else {
\"no\"
}#(|\n)#
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"|]#
} else {
#(\"no\"|)#
}
}
"}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[|\"yes\"]#
} else {
#(|\"no\")#
}
}
"}),
),
// if any cursors are not preceded by all whitespace, then do the
// smart_tab action
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"\n|]#
} else {
\"no#(\"\n|)#
}
}
"}),
"i<tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
}#[| ]#else {
\"no\"
}#(|\n)#
}
"}),
),
// Ctrl-tab always inserts a tab
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"\n|]#
} else {
\"no#(\"\n|)#
}
}
"}),
"i<S-tab>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
#[|\"yes\"\n]#
} else {
\"no #(|\"\n)#
}
}
"}),
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}

@ -244,7 +244,7 @@ pub fn test_editor_config() -> helix_view::editor::Config {
/// character, and if one doesn't exist already, appends the system's /// character, and if one doesn't exist already, appends the system's
/// appropriate line ending to the end of a string. /// appropriate line ending to the end of a string.
pub fn platform_line(input: &str) -> String { pub fn platform_line(input: &str) -> String {
let line_end = helix_core::DEFAULT_LINE_ENDING.as_str(); let line_end = helix_core::NATIVE_LINE_ENDING.as_str();
// we can assume that the source files in this code base will always // we can assume that the source files in this code base will always
// be LF, so indoc strings will always insert LF // be LF, so indoc strings will always insert LF

@ -0,0 +1,41 @@
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn auto_indent() -> anyhow::Result<()> {
let app = || AppBuilder::new().with_file("foo.go", None);
let enter_tests = [
(
helpers::platform_line(indoc! {r##"
type Test struct {#[}|]#
"##}),
"i<ret>",
helpers::platform_line(indoc! {"\
type Test struct {
\t#[|\n]#
}
"}),
),
(
helpers::platform_line(indoc! {"\
func main() {
\tswitch nil {#[}|]#
}
"}),
"i<ret>",
helpers::platform_line(indoc! {"\
func main() {
\tswitch nil {
\t\t#[|\n]#
\t}
}
"}),
),
];
for test in enter_tests {
test_with_config(app(), test).await?;
}
Ok(())
}

@ -0,0 +1,4 @@
use super::*;
mod go;
mod yaml;

@ -0,0 +1,819 @@
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn auto_indent() -> anyhow::Result<()> {
let app = || AppBuilder::new().with_file("foo.yaml", None);
let below_tests = [
(
helpers::platform_line(indoc! {r##"
#[t|]#op:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"o",
helpers::platform_line(indoc! {"\
top:
#[\n|]#
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
b#[a|]#z: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
#[\n|]#
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
baz: foo
bazi#[:|]#
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
#[\n|]#
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
baz: foo
bazi:
more: #[yes|]#
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
#[\n|]#
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
baz: foo
bazi:
more: yes
why: becaus#[e|]#
quux:
- 1
- 2
bax: foox
fook:
"##}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
#[\n|]#
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:#[\n|]#
- 1
- 2
bax: foox
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
#[\n|]#
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1#[\n|]#
- 2
bax: foox
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
#[\n|]#
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:#[\n|]#
"}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
#[\n|]#
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: |
some
multi
line
string#[\n|]#
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: |
some
multi
line
string
#[\n|]#
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >
some
multi
line#[\n|]#
string
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >
some
multi
line
#[\n|]#
string
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >#[\n|]#
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >
#[\n|]#
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
- top:#[\n|]#
baz: foo
bax: foox
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
- top:
#[\n|]#
baz: foo
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
- top:
baz: foo#[\n|]#
bax: foox
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
- top:
baz: foo
#[\n|]#
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
- top:
baz: foo
bax: foox#[\n|]#
fook:
"}),
"o",
helpers::platform_line(indoc! {"\
- top:
baz: foo
bax: foox
#[\n|]#
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz:
- one: two#[\n|]#
three: four
- top:
baz: foo
bax: foox
"}),
"o",
helpers::platform_line(indoc! {"\
top:
baz:
- one: two
#[\n|]#
three: four
- top:
baz: foo
bax: foox
"}),
),
// yaml map without a key
(
helpers::platform_line(indoc! {"\
top:#[\n|]#
"}),
"o",
helpers::platform_line(indoc! {"\
top:
#[\n|]#
"}),
),
(
helpers::platform_line(indoc! {"\
top#[:|]#
bottom: withvalue
"}),
"o",
helpers::platform_line(indoc! {"\
top:
#[\n|]#
bottom: withvalue
"}),
),
(
helpers::platform_line(indoc! {"\
bottom: withvalue
top#[:|]#
"}),
"o",
helpers::platform_line(indoc! {"\
bottom: withvalue
top:
#[\n|]#
"}),
),
];
for test in below_tests {
test_with_config(app(), test).await?;
}
let above_tests = [
(
helpers::platform_line(indoc! {r##"
#[t|]#op:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"O",
helpers::platform_line(indoc! {"\
#[\n|]#
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
b#[a|]#z: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"O",
helpers::platform_line(indoc! {"\
top:
#[\n|]#
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
baz: foo
bazi#[:|]#
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
#[\n|]#
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
baz: foo
bazi:
more: #[yes|]#
why: because
quux:
- 1
- 2
bax: foox
fook:
"##}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
#[\n|]#
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {r##"
top:
baz: foo
bazi:
more: yes
why: becaus#[e|]#
quux:
- 1
- 2
bax: foox
fook:
"##}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
#[\n|]#
why: because
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:#[\n|]#
- 1
- 2
bax: foox
fook:
"}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
#[\n|]#
quux:
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1#[\n|]#
- 2
bax: foox
fook:
"}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
#[\n|]#
- 1
- 2
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
fook:#[\n|]#
"}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bazi:
more: yes
why: because
quux:
- 1
- 2
bax: foox
#[\n|]#
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: |
some
multi
line
string#[\n|]#
fook:
"}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: |
some
multi
line
#[\n|]#
string
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >
some#[\n|]#
multi
line
string
fook:
"}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >
#[\n|]#
some
multi
line
string
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >
fook:#[\n|]#
"}),
"O",
helpers::platform_line(indoc! {"\
top:
baz: foo
bax: >
#[\n|]#
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
- top:
baz: foo#[\n|]#
bax: foox
fook:
"}),
"O",
helpers::platform_line(indoc! {"\
- top:
#[\n|]#
baz: foo
bax: foox
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
- top:
baz: foo
bax: foox
fook:#[\n|]#
"}),
"O",
helpers::platform_line(indoc! {"\
- top:
baz: foo
bax: foox
#[\n|]#
fook:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
baz:
- one: two#[\n|]#
three: four
- top:
baz: foo
bax: foox
"}),
"O",
helpers::platform_line(indoc! {"\
top:
baz:
#[\n|]#
- one: two
three: four
- top:
baz: foo
bax: foox
"}),
),
// yaml map without a key
(
helpers::platform_line(indoc! {"\
top:#[\n|]#
"}),
"O",
helpers::platform_line(indoc! {"\
#[\n|]#
top:
"}),
),
(
helpers::platform_line(indoc! {"\
bottom: withvalue
top#[:|]#
"}),
"O",
helpers::platform_line(indoc! {"\
bottom: withvalue
#[\n|]#
top:
"}),
),
(
helpers::platform_line(indoc! {"\
top:
bottom:#[ |]#withvalue
"}),
"O",
helpers::platform_line(indoc! {"\
top:
#[\n|]#
bottom: withvalue
"}),
),
];
for test in above_tests {
test_with_config(app(), test).await?;
}
let enter_tests = [
(
helpers::platform_line(indoc! {r##"
foo: #[b|]#ar
"##}),
"i<ret>",
helpers::platform_line(indoc! {"\
foo:
#[|b]#ar
"}),
),
(
helpers::platform_line(indoc! {"\
foo:#[\n|]#
"}),
"i<ret>",
helpers::platform_line(indoc! {"\
foo:
#[|\n]#
"}),
),
];
for test in enter_tests {
test_with_config(app(), test).await?;
}
Ok(())
}

@ -16,10 +16,10 @@ include = ["src/**/*", "README.md"]
default = ["crossterm"] default = ["crossterm"]
[dependencies] [dependencies]
bitflags = "2.3" bitflags = "2.4"
cassowary = "0.3" cassowary = "0.3"
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
crossterm = { version = "0.26", optional = true } crossterm = { version = "0.27", optional = true }
termini = "1.0" termini = "1.0"
serde = { version = "1", "optional" = true, features = ["derive"]} serde = { version = "1", "optional" = true, features = ["derive"]}
once_cell = "1.18" once_cell = "1.18"

@ -201,7 +201,7 @@ where
for (x, y, cell) in content { for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y) // Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) { if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
map_error(queue!(self.buffer, MoveTo(x, y)))?; queue!(self.buffer, MoveTo(x, y))?;
} }
last_pos = Some((x, y)); last_pos = Some((x, y));
if cell.modifier != modifier { if cell.modifier != modifier {
@ -214,12 +214,12 @@ where
} }
if cell.fg != fg { if cell.fg != fg {
let color = CColor::from(cell.fg); let color = CColor::from(cell.fg);
map_error(queue!(self.buffer, SetForegroundColor(color)))?; queue!(self.buffer, SetForegroundColor(color))?;
fg = cell.fg; fg = cell.fg;
} }
if cell.bg != bg { if cell.bg != bg {
let color = CColor::from(cell.bg); let color = CColor::from(cell.bg);
map_error(queue!(self.buffer, SetBackgroundColor(color)))?; queue!(self.buffer, SetBackgroundColor(color))?;
bg = cell.bg; bg = cell.bg;
} }
@ -227,7 +227,7 @@ where
if self.capabilities.has_extended_underlines { if self.capabilities.has_extended_underlines {
if cell.underline_color != underline_color { if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color); let color = CColor::from(cell.underline_color);
map_error(queue!(self.buffer, SetUnderlineColor(color)))?; queue!(self.buffer, SetUnderlineColor(color))?;
underline_color = cell.underline_color; underline_color = cell.underline_color;
} }
} else { } else {
@ -239,24 +239,24 @@ where
if new_underline_style != underline_style { if new_underline_style != underline_style {
let attr = CAttribute::from(new_underline_style); let attr = CAttribute::from(new_underline_style);
map_error(queue!(self.buffer, SetAttribute(attr)))?; queue!(self.buffer, SetAttribute(attr))?;
underline_style = new_underline_style; underline_style = new_underline_style;
} }
map_error(queue!(self.buffer, Print(&cell.symbol)))?; queue!(self.buffer, Print(&cell.symbol))?;
} }
map_error(queue!( queue!(
self.buffer, self.buffer,
SetUnderlineColor(CColor::Reset), SetUnderlineColor(CColor::Reset),
SetForegroundColor(CColor::Reset), SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset) SetAttribute(CAttribute::Reset)
)) )
} }
fn hide_cursor(&mut self) -> io::Result<()> { fn hide_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Hide)) execute!(self.buffer, Hide)
} }
fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> { fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
@ -266,7 +266,7 @@ where
CursorKind::Underline => SetCursorStyle::SteadyUnderScore, CursorKind::Underline => SetCursorStyle::SteadyUnderScore,
CursorKind::Hidden => unreachable!(), CursorKind::Hidden => unreachable!(),
}; };
map_error(execute!(self.buffer, Show, shape)) execute!(self.buffer, Show, shape)
} }
fn get_cursor(&mut self) -> io::Result<(u16, u16)> { fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@ -275,11 +275,11 @@ where
} }
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
map_error(execute!(self.buffer, MoveTo(x, y))) execute!(self.buffer, MoveTo(x, y))
} }
fn clear(&mut self) -> io::Result<()> { fn clear(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Clear(ClearType::All))) execute!(self.buffer, Clear(ClearType::All))
} }
fn size(&self) -> io::Result<Rect> { fn size(&self) -> io::Result<Rect> {
@ -294,10 +294,6 @@ where
} }
} }
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
#[derive(Debug)] #[derive(Debug)]
struct ModifierDiff { struct ModifierDiff {
pub from: Modifier, pub from: Modifier,
@ -312,48 +308,48 @@ impl ModifierDiff {
//use crossterm::Attribute; //use crossterm::Attribute;
let removed = self.from - self.to; let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) { if removed.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?; queue!(w, SetAttribute(CAttribute::NoReverse))?;
} }
if removed.contains(Modifier::BOLD) { if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
if self.to.contains(Modifier::DIM) { if self.to.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; queue!(w, SetAttribute(CAttribute::Dim))?;
} }
} }
if removed.contains(Modifier::ITALIC) { if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; queue!(w, SetAttribute(CAttribute::NoItalic))?;
} }
if removed.contains(Modifier::DIM) { if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
} }
if removed.contains(Modifier::CROSSED_OUT) { if removed.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?; queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
} }
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; queue!(w, SetAttribute(CAttribute::NoBlink))?;
} }
let added = self.to - self.from; let added = self.to - self.from;
if added.contains(Modifier::REVERSED) { if added.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; queue!(w, SetAttribute(CAttribute::Reverse))?;
} }
if added.contains(Modifier::BOLD) { if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?; queue!(w, SetAttribute(CAttribute::Bold))?;
} }
if added.contains(Modifier::ITALIC) { if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?; queue!(w, SetAttribute(CAttribute::Italic))?;
} }
if added.contains(Modifier::DIM) { if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; queue!(w, SetAttribute(CAttribute::Dim))?;
} }
if added.contains(Modifier::CROSSED_OUT) { if added.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?; queue!(w, SetAttribute(CAttribute::CrossedOut))?;
} }
if added.contains(Modifier::SLOW_BLINK) { if added.contains(Modifier::SLOW_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?; queue!(w, SetAttribute(CAttribute::SlowBlink))?;
} }
if added.contains(Modifier::RAPID_BLINK) { if added.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?; queue!(w, SetAttribute(CAttribute::RapidBlink))?;
} }
Ok(()) Ok(())
@ -407,7 +403,7 @@ impl Command for SetUnderlineColor {
} }
#[cfg(windows)] #[cfg(windows)]
fn execute_winapi(&self) -> crossterm::Result<()> { fn execute_winapi(&self) -> io::Result<()> {
Err(std::io::Error::new( Err(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
"SetUnderlineColor not supported by winapi.", "SetUnderlineColor not supported by winapi.",

@ -65,20 +65,6 @@ where
viewport: Viewport, viewport: Viewport,
} }
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.cursor_kind == CursorKind::Hidden {
if let Err(err) = self.show_cursor(CursorKind::Block) {
eprintln!("Failed to show the cursor: {}", err);
}
}
}
}
impl<B> Terminal<B> impl<B> Terminal<B>
where where
B: Backend, B: Backend,

@ -450,11 +450,11 @@ impl<'a> Table<'a> {
} else { } else {
col col
}; };
if is_selected {
buf.set_style(table_row_area, self.highlight_style);
}
let mut col = table_row_start_col; let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
if is_selected {
buf.set_style(table_row_area, self.highlight_style);
}
render_cell( render_cell(
buf, buf,
cell, cell,

@ -12,12 +12,13 @@ homepage = "https://helix-editor.com"
[dependencies] [dependencies]
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-event = { version = "0.6", path = "../helix-event" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
parking_lot = "0.12" parking_lot = "0.12"
arc-swap = { version = "1.6.0" } arc-swap = { version = "1.6.0" }
gix = { version = "0.44.1", default-features = false , optional = true } gix = { version = "0.48.0", default-features = false , optional = true }
imara-diff = "0.1.5" imara-diff = "0.1.5"
anyhow = "1" anyhow = "1"
@ -27,4 +28,4 @@ log = "0.4"
git = ["gix"] git = ["gix"]
[dev-dependencies] [dev-dependencies]
tempfile = "3.4" tempfile = "3.8"

@ -2,10 +2,10 @@ use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
use helix_core::Rope; use helix_core::Rope;
use helix_event::RenderLockGuard;
use imara_diff::Algorithm; use imara_diff::Algorithm;
use parking_lot::{Mutex, MutexGuard}; use parking_lot::{Mutex, MutexGuard};
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio::time::Instant; use tokio::time::Instant;
@ -14,11 +14,9 @@ use crate::diff::worker::DiffWorker;
mod line_cache; mod line_cache;
mod worker; mod worker;
type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
/// A rendering lock passed to the differ the prevents redraws from occurring /// A rendering lock passed to the differ the prevents redraws from occurring
struct RenderLock { struct RenderLock {
pub lock: OwnedRwLockReadGuard<()>, pub lock: RenderLockGuard,
pub timeout: Option<Instant>, pub timeout: Option<Instant>,
} }
@ -38,28 +36,22 @@ struct DiffInner {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DiffHandle { pub struct DiffHandle {
channel: UnboundedSender<Event>, channel: UnboundedSender<Event>,
render_lock: Arc<RwLock<()>>,
diff: Arc<Mutex<DiffInner>>, diff: Arc<Mutex<DiffInner>>,
inverted: bool, inverted: bool,
} }
impl DiffHandle { impl DiffHandle {
pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle { pub fn new(diff_base: Rope, doc: Rope) -> DiffHandle {
DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0 DiffHandle::new_with_handle(diff_base, doc).0
} }
fn new_with_handle( fn new_with_handle(diff_base: Rope, doc: Rope) -> (DiffHandle, JoinHandle<()>) {
diff_base: Rope,
doc: Rope,
redraw_handle: RedrawHandle,
) -> (DiffHandle, JoinHandle<()>) {
let (sender, receiver) = unbounded_channel(); let (sender, receiver) = unbounded_channel();
let diff: Arc<Mutex<DiffInner>> = Arc::default(); let diff: Arc<Mutex<DiffInner>> = Arc::default();
let worker = DiffWorker { let worker = DiffWorker {
channel: receiver, channel: receiver,
diff: diff.clone(), diff: diff.clone(),
new_hunks: Vec::default(), new_hunks: Vec::default(),
redraw_notify: redraw_handle.0,
diff_finished_notify: Arc::default(), diff_finished_notify: Arc::default(),
}; };
let handle = tokio::spawn(worker.run(diff_base, doc)); let handle = tokio::spawn(worker.run(diff_base, doc));
@ -67,7 +59,6 @@ impl DiffHandle {
channel: sender, channel: sender,
diff, diff,
inverted: false, inverted: false,
render_lock: redraw_handle.1,
}; };
(differ, handle) (differ, handle)
} }
@ -87,11 +78,7 @@ impl DiffHandle {
/// This function is only intended to be called from within the rendering loop /// This function is only intended to be called from within the rendering loop
/// if called from elsewhere it may fail to acquire the render lock and panic /// if called from elsewhere it may fail to acquire the render lock and panic
pub fn update_document(&self, doc: Rope, block: bool) -> bool { pub fn update_document(&self, doc: Rope, block: bool) -> bool {
// unwrap is ok here because the rendering lock is let lock = helix_event::lock_frame();
// only exclusively locked during redraw.
// This function is only intended to be called
// from the core rendering loop where no redraw can happen in parallel
let lock = self.render_lock.clone().try_read_owned().unwrap();
let timeout = if block { let timeout = if block {
None None
} else { } else {

@ -23,7 +23,6 @@ pub(super) struct DiffWorker {
pub channel: UnboundedReceiver<Event>, pub channel: UnboundedReceiver<Event>,
pub diff: Arc<Mutex<DiffInner>>, pub diff: Arc<Mutex<DiffInner>>,
pub new_hunks: Vec<Hunk>, pub new_hunks: Vec<Hunk>,
pub redraw_notify: Arc<Notify>,
pub diff_finished_notify: Arc<Notify>, pub diff_finished_notify: Arc<Notify>,
} }
@ -32,11 +31,7 @@ impl DiffWorker {
let mut accumulator = EventAccumulator::new(); let mut accumulator = EventAccumulator::new();
accumulator.handle_event(event).await; accumulator.handle_event(event).await;
accumulator accumulator
.accumulate_debounced_events( .accumulate_debounced_events(&mut self.channel, self.diff_finished_notify.clone())
&mut self.channel,
self.redraw_notify.clone(),
self.diff_finished_notify.clone(),
)
.await; .await;
(accumulator.doc, accumulator.diff_base) (accumulator.doc, accumulator.diff_base)
} }
@ -137,7 +132,6 @@ impl<'a> EventAccumulator {
async fn accumulate_debounced_events( async fn accumulate_debounced_events(
&mut self, &mut self,
channel: &mut UnboundedReceiver<Event>, channel: &mut UnboundedReceiver<Event>,
redraw_notify: Arc<Notify>,
diff_finished_notify: Arc<Notify>, diff_finished_notify: Arc<Notify>,
) { ) {
let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC); let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
@ -164,7 +158,7 @@ impl<'a> EventAccumulator {
None => { None => {
tokio::spawn(async move { tokio::spawn(async move {
diff_finished_notify.notified().await; diff_finished_notify.notified().await;
redraw_notify.notify_one(); helix_event::request_redraw();
}); });
} }
// diff is performed inside the rendering loop // diff is performed inside the rendering loop
@ -190,7 +184,7 @@ impl<'a> EventAccumulator {
// and wait until the diff occurs to trigger an async redraw // and wait until the diff occurs to trigger an async redraw
log::info!("Diff computation timed out, update of diffs might appear delayed"); log::info!("Diff computation timed out, update of diffs might appear delayed");
diff_finished_notify.notified().await; diff_finished_notify.notified().await;
redraw_notify.notify_one(); helix_event::request_redraw()
}); });
} }
// a blocking diff is performed inside the rendering loop // a blocking diff is performed inside the rendering loop

@ -5,11 +5,7 @@ use crate::diff::{DiffHandle, Hunk};
impl DiffHandle { impl DiffHandle {
fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) { fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
DiffHandle::new_with_handle( DiffHandle::new_with_handle(Rope::from_str(diff_base), Rope::from_str(doc))
Rope::from_str(diff_base),
Rope::from_str(doc),
Default::default(),
)
} }
async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> { async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
let diff = self.diff; let diff = self.diff;

@ -14,13 +14,14 @@ default = []
term = ["crossterm"] term = ["crossterm"]
[dependencies] [dependencies]
bitflags = "2.3" bitflags = "2.4"
anyhow = "1" anyhow = "1"
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-event = { version = "0.6", path = "../helix-event" }
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.26", optional = true } crossterm = { version = "0.27", optional = true }
helix-vcs = { version = "0.6", path = "../helix-vcs" } helix-vcs = { version = "0.6", path = "../helix-vcs" }
# Conversion traits # Conversion traits
@ -51,6 +52,7 @@ clipboard-win = { version = "4.5", features = ["std"] }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2" libc = "0.2"
rustix = { version = "0.38", features = ["fs"] }
[dev-dependencies] [dev-dependencies]
helix-tui = { path = "../helix-tui" } helix-tui = { path = "../helix-tui" }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save