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
- name: Install nix
uses: cachix/install-nix-action@v21
uses: cachix/install-nix-action@v22
- name: Authenticate with Cachix
uses: cachix/cachix-action@v12

@ -160,7 +160,7 @@ jobs:
- name: Build AppImage
shell: bash
if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux'
if: matrix.build == 'x86_64-linux'
run: |
# Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo add-apt-repository universe
@ -263,7 +263,7 @@ jobs:
mv bins-$platform/hx$exe $pkgname
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/
fi

@ -1,5 +1,2 @@
# 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
# 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
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
- Movement is now fully tested and matches Kakoune implementation
- 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-tui",
"helix-lsp",
"helix-event",
"helix-dap",
"helix-loader",
"helix-vcs",
@ -32,3 +33,7 @@ inherits = "test"
package.helix-core.opt-level = 2
package.helix-tui.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
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"
multilingual = false
src = "src"
edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
[output.html]
cname = "docs.helix-editor.com"
default-theme = "colibri"
preferred-dark-theme = "colibri"
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-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` |
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
| `auto-info` | Whether to display info boxes | `true` |
@ -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` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
### `[editor.statusline]` Section
@ -87,9 +89,9 @@ The `[editor.statusline]` key takes the following sub-keys:
| Key | Description | Default |
| --- | --- | --- |
| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name"]` |
| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
| `center` | A list of elements aligned to the middle of the statusline | `[]` |
| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "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 | `"│"` |
| `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"` |
@ -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-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) |
| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
@ -117,6 +120,7 @@ The following statusline elements can be configured:
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |
| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
| `version-control` | The current branch name or detached commit hash of the opened workspace |
| `register` | The current selected register |
### `[editor.lsp]` Section
@ -131,8 +135,8 @@ The following statusline elements can be configured:
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix.
Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances, please report any bugs you see so we can fix them!
[^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
@ -344,3 +348,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it
```
### `[editor.smart-tab]` Section
| 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 | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | | ✓ | `bash-language-server` |
| bash | ✓ | | ✓ | `bash-language-server` |
| bass | ✓ | | | `bass` |
| beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
@ -41,9 +41,11 @@
| erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | |
| forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | ✓ | `fortls` |
| fsharp | ✓ | | | `fsautocomplete` |
| gdscript | ✓ | ✓ | ✓ | |
| gemini | ✓ | | | |
| git-attributes | ✓ | | | |
| git-commit | ✓ | ✓ | | |
| git-config | ✓ | | | |
@ -59,6 +61,7 @@
| graphql | ✓ | | | |
| hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | |
| hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` |
| hosts | ✓ | | | |
@ -67,8 +70,9 @@
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
| java | ✓ | ✓ | | `jdtls` |
| java | ✓ | ✓ | | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | |
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` |
@ -88,7 +92,7 @@
| markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman` |
| markdown.inline | ✓ | | | |
| matlab | ✓ | | | |
| matlab | ✓ | | | |
| mermaid | ✓ | | | |
| meson | ✓ | | ✓ | |
| mint | | | | `mint` |
@ -98,6 +102,7 @@
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | | | `nil` |
| nu | ✓ | | | |
| nunjucks | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | ✓ | `ols` |
@ -107,13 +112,14 @@
| pascal | ✓ | ✓ | | `pasls` |
| passwd | ✓ | | | |
| pem | ✓ | | | |
| perl | ✓ | | | `perlnavigator` |
| perl | ✓ | | | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` |
| po | ✓ | ✓ | | |
| pod | ✓ | | | |
| ponylang | ✓ | ✓ | ✓ | |
| prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | |
| protobuf | ✓ | | ✓ | `bufls`, `pb` |
| prql | ✓ | | | |
| purescript | ✓ | | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` |
@ -140,29 +146,35 @@
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
| svelte | ✓ | | | `svelteserver` |
| strace | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| task | ✓ | | | |
| tfvars | ✓ | | ✓ | `terraform-ls` |
| todotxt | ✓ | | | |
| toml | ✓ | | | `taplo` |
| tsq | ✓ | | | |
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| twig | ✓ | | | |
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| ungrammar | ✓ | | | |
| unison | ✓ | | | |
| uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v` |
| v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | |
| vue | ✓ | | | `vls` |
| vue | ✓ | | | `vue-language-server` |
| wast | ✓ | | | |
| wat | ✓ | | | |
| webc | ✓ | | | |
| wgsl | ✓ | | | `wgsl_analyzer` |
| wit | ✓ | | ✓ | |
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | |
| xml | ✓ | | ✓ | |
| 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 forcefully. Accepts an optional path (:wq! some/path.txt) |
| `: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 forcefully (ignoring unsaved changes). |
| `: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!` | 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). |
| `: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-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. |
@ -46,8 +48,8 @@
| `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
| `:character-info`, `:char` | Get info about the character under the primary cursor. |
| `:reload` | Discard changes and reload from the source file. |
| `:reload-all` | Discard changes and reload all documents from the source files. |
| `:reload`, `:rl` | Discard changes and reload from the source file. |
| `:reload-all`, `:rla` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the language servers used by the current doc |

@ -1,76 +1,299 @@
# Adding indent queries
Helix uses tree-sitter to correctly indent new lines. This requires
a tree-sitter grammar and an `indent.scm` query file placed in
`runtime/queries/{language}/indents.scm`. The indentation for a line
is calculated by traversing the syntax tree from the lowest node at the
beginning of the new line. Each of these nodes contributes to the total
indent when it is captured by the query (in what way depends on the name
of the capture).
Helix uses tree-sitter to correctly indent new lines. This requires a tree-
sitter grammar and an `indent.scm` query file placed in `runtime/queries/
{language}/indents.scm`. The indentation for a line is calculated by traversing
the syntax tree from the lowest node at the beginning of the new line (see
[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 of
the capture.
Note that it matters where these added indents begin. For example,
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
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):
When Helix is inserting a new line through `o`, `O`, or `<ret>`, to determine
the indent level for the new line, the query in `indents.scm` is run on the
document. The starting position of the query is the end of the line above where
a new line will be inserted.
- `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 `o`, the inserted line is the line below the cursor, so that starting
position of the query is the end of the current line.
- `tail`:
This scope applies to everything except for the first line of the
captured node.
```rust
fn need_hero(some_hero: Hero, life: Life) -> {
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
in most situations. When a different scope is required, this can be
changed by using a `#set!` declaration anywhere in the pattern:
```scm
(assignment_expression
right: (_) @indent
(#set! "scope" "all"))
For `O`, the newly inserted line is the *current* line, so the starting position
of the query is the end of the line above the cursor.
```rust
fn need_hero(some_hero: Hero, life: Life) -> { // ←─╮
matches!(some_hero, Hero { // ←╮ ↑ │
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`):
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.
### Capture types
- `@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`):
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 the range of this node to the end of the line and to lines that
are indented more than the line that this node starts on. This is useful
for languages like Python, where for the purpose of indentation some nodes
(like functions or classes) should also contain indented lines that follow them.
Extend the range of this node to the end of the line and to lines that are
indented more than the line that this node starts on. This is useful for
languages like Python, where for the purpose of indentation some nodes (like
functions or classes) should also contain indented lines that follow them.
- `@extend.prevent-once`:
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
extension of the next `@extend` capture. If multiple ancestors are captured,
only the extension of the innermost one is prevented. All other ancestors are unaffected
(regardless of whether the innermost ancestor would actually have been extended).
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 extension of the next `@extend` capture. If multiple ancestors are
captured, only the extension of the innermost one is prevented. All other
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
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,
similar to how `#set!` declarations work:
```scm
(some_kind
(child_kind) @indent
(#predicate? arg1 arg2 ...)
)
```
The number of arguments depends on the predicate that's used.
Each argument is either a capture (`@name`) or a string (`"some string"`).
The following predicates are supported by tree-sitter:
@ -91,3 +314,47 @@ argument (a string).
- `#same-line?`/`#not-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)
- [Ubuntu](#ubuntu)
- [Fedora/RHEL](#fedorarhel)
- [Arch Linux community](#arch-linux-community)
- [Arch Linux extra](#arch-linux-extra)
- [NixOS](#nixos)
- [Flatpak](#flatpak)
- [Snap](#snap)
- [AppImage](#appimage)
- [macOS](#macos)
- [Homebrew Core](#homebrew-core)
@ -70,9 +71,9 @@ sudo dnf copr enable varlad/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
sudo pacman -S helix
@ -104,6 +105,16 @@ flatpak install flathub com.helix_editor.Helix
flatpak run com.helix_editor.Helix
```
### Snap
Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with:
```sh
snap install --classic helix
```
This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`.
### AppImage
Install Helix using the Linux [AppImage](https://appimage.org/) format.
@ -159,9 +170,13 @@ pacman -S mingw-w64-ucrt-x86_64-helix
Requirements:
Clone the Helix GitHub repository into a directory of your choice. The
examples in this documentation assume installation into either `~/src/` on
Linux and macOS, or `%userprofile%\src\` on Windows.
- The [Rust toolchain](https://www.rust-lang.org/tools/install)
- The [Git version control system](https://git-scm.com/)
- A c++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang
- 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:
@ -171,19 +186,19 @@ RUSTFLAGS="-C target-feature=-crt-static"
1. Clone the repository:
```sh
git clone https://github.com/helix-editor/helix
cd helix
```
```sh
git clone https://github.com/helix-editor/helix
cd helix
```
2. Compile from source:
```sh
cargo install --path helix-term --locked
```
```sh
cargo install --path helix-term --locked
```
This command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder.
This command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder.
> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
> grammars with `hx --grammar fetch` and compile them with
@ -195,13 +210,15 @@ grammars in the local `runtime` folder.
#### 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
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
ln -Ts $PWD/runtime ~/.config/helix/runtime

@ -25,6 +25,8 @@
## Normal mode
Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`.
### Movement
> 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 |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `y` | Yank selections to clipboard | `yank_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
@ -339,6 +341,8 @@ These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim
## Insert mode
Accessed by typing `i` in [normal mode](#normal-mode).
Insert mode bindings are minimal by default. Helix is designed to
be a modal editor, and this is reflected in the user experience and internal
mechanics. Changes to the text are only saved for undos when
@ -392,9 +396,11 @@ end = "no_op"
## Select / extend mode
Accessed by typing `v` in [normal mode](#normal-mode).
Select mode echoes Normal mode, but changes any movements to extend
selections rather than replace them. Goto motions are also changed to
extend, so that `vgl` for example extends the selection to the end of
extend, so that `vgl`, for example, extends the selection to the end of
the line.
Search is also affected. By default, `n` and `N` will remove the current
@ -416,6 +422,7 @@ Keys to use within picker. Remapping currently not supported.
| `Home` | Go to first entry |
| `End` | Go to last entry |
| `Enter` | Open selected |
| `Alt-Enter` | Open selected in the background without closing the picker |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Ctrl-t` | Toggle preview |

@ -7,24 +7,24 @@ in `languages.toml` files.
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).
It provides the default configurations for languages and language servers.
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:
```toml
# in <config_dir>/helix/languages.toml
```toml
# in <config_dir>/helix/languages.toml
[language-server.mylang-lsp]
command = "mylang-lsp"
[language-server.mylang-lsp]
command = "mylang-lsp"
[[language]]
name = "rust"
auto-format = false
```
[[language]]
name = "rust"
auto-format = false
```
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
@ -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
[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
[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`,
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`,
so everything else should be handled by the `typescript-language-server` (which is configured by default)
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).
The language configuration for typescript could look like this:
```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.
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.
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 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).
The list of supported features is:

@ -70,6 +70,7 @@ over it and is merged into the default palette.
| Color Name |
| --- |
| `default` |
| `black` |
| `red` |
| `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).
- `"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 |
| --- | --- |
| `/` | Last search |
| `:` | Last executed command |
| `"` | 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
[key map](keymap.md#space-mode) for more details.
### Special registers
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

@ -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 -->
{{> head}}
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
@ -53,18 +52,19 @@
{{#if mathjax_support}}
<!-- 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}}
</head>
<body>
<div id="body-container">
<!-- Provide site root to javascript -->
<script type="text/javascript">
<script>
var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
@ -80,7 +80,7 @@
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
@ -92,12 +92,14 @@
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
<script>
var html = document.querySelector('html');
var sidebar = 'hidden';
var sidebar = null;
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
@ -110,12 +112,34 @@
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</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 class="page">
{{> header}}
<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">
<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>
@ -124,12 +148,12 @@
<i class="fa fa-paint-brush"></i>
</button>
<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="rust">{{ theme_option "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="navy">{{ theme_option "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="colibri">{{ theme_option "Colibri" }}</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">Rust</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">Navy</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">Colibri</button></li>
</ul>
{{#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">
@ -151,13 +175,19 @@
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a>
{{/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>
{{#if search_enabled}}
<div id="search-wrapper" class="hidden">
<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>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
@ -168,7 +198,7 @@
{{/if}}
<!-- 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').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
@ -216,10 +246,12 @@
</div>
{{#if livereload}}
{{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript">
var socket = new WebSocket("{{{livereload}}}");
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
@ -235,7 +267,7 @@
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script type="text/javascript">
<script>
var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
@ -253,43 +285,43 @@
{{/if}}
{{#if playground_line_numbers}}
<script type="text/javascript">
<script>
window.playground_line_numbers = true;
</script>
{{/if}}
{{#if playground_copyable}}
<script type="text/javascript">
<script>
window.playground_copyable = true;
</script>
{{/if}}
{{#if playground_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}ace.js"></script>
<script src="{{ path_to_root }}editor.js"></script>
<script src="{{ path_to_root }}mode-rust.js"></script>
<script src="{{ path_to_root }}theme-dawn.js"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
{{/if}}
{{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}searcher.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"></script>
<script src="{{ path_to_root }}searcher.js"></script>
{{/if}}
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}clipboard.min.js"></script>
<script src="{{ path_to_root }}highlight.js"></script>
<script src="{{ path_to_root }}book.js"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
<script src="{{ ../path_to_root }}{{this}}"></script>
{{/each}}
{{#if is_print}}
{{#if mathjax_support}}
<script type="text/javascript">
<script>
window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100);
@ -297,7 +329,7 @@
});
</script>
{{else}}
<script type="text/javascript">
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
@ -305,5 +337,6 @@
{{/if}}
{{/if}}
</div>
</body>
</html>

@ -1,7 +1,7 @@
## Checklist
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.
* 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
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
```

@ -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.
* **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.
* **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.

@ -1,110 +1,29 @@
{
"nodes": {
"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": {
"all-cabal-json": [
"nci"
],
"crane": "crane",
"devshell": [
"nci"
],
"drv-parts": "drv-parts",
"flake-compat": "flake-compat",
"flake-parts": [
"nci",
"parts"
],
"flake-utils-pre-commit": [
"nci"
],
"ghc-utils": [
"nci"
],
"gomod2nix": [
"nci"
],
"mach-nix": [
"nci"
],
"nix-pypi-fetcher": [
"nci"
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nci",
"nixpkgs"
],
"nixpkgsV1": "nixpkgsV1",
"poetry2nix": [
"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"
"rust-overlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1680698112,
"narHash": "sha256-FgnobN/DvCjEsc0UAZEAdPLkL4IZi2ZMnu2K2bUaElc=",
"owner": "davhau",
"repo": "drv-parts",
"rev": "e8c2ec1157dc1edb002989669a0dbd935f430201",
"lastModified": 1688772518,
"narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e",
"type": "github"
},
"original": {
"owner": "davhau",
"repo": "drv-parts",
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
@ -129,11 +48,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
@ -142,55 +61,13 @@
"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": {
"locked": {
"lastModified": 1683408522,
"narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=",
"lastModified": 1690272529,
"narHash": "sha256-MakzcKXEdv/I4qJUtq/k/eG+rVmyOZLnYNC2w1mB59Y=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7",
"rev": "ef99fa5c5ed624460217c31ac4271cfb5cb2502c",
"type": "github"
},
"original": {
@ -200,99 +77,29 @@
"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": {
"inputs": {
"nci": "nci",
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"parts": "parts_2",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1683771545,
"narHash": "sha256-we0GYcKTo2jRQGmUGrzQ9VH0OYAUsJMCsK8UkF+vZUA=",
"lastModified": 1690424156,
"narHash": "sha256-Bpml+L280tHTQpwpC5/BJbU4HSvEzMvW8IZ4gAXimhE=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c57e210faf68e5d5386f18f1b17ad8365d25e4ed",
"rev": "f335a0213504c7e6481c359dc1009be9cf34432c",
"type": "github"
},
"original": {

@ -3,19 +3,35 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
nci = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
};
crane = {
url = "github:ipetkov/crane";
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 = {
self,
nixpkgs,
crane,
flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
mkRootPath = rel:
builtins.path {
path = "${toString ./.}/${rel}";
@ -27,7 +43,6 @@
".ignore"
".github"
".gitignore"
"logo.svg"
"logo_dark.svg"
"logo_light.svg"
"rust-toolchain.toml"
@ -35,7 +50,6 @@
"runtime"
"screenshot.png"
"book"
"contrib"
"docs"
"README.md"
"CHANGELOG.md"
@ -46,7 +60,7 @@
"flake.lock"
];
ignorePaths = path: type: let
inherit (inp.nixpkgs) lib;
inherit (nixpkgs) lib;
# split the nix store path into its components
components = lib.splitString "/" path;
# drop off the `/nix/hash-source` section from the path
@ -62,22 +76,6 @@
# 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" {} ''
@ -122,38 +120,64 @@
if stdenv.isLinux
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
else "$RUSTFLAGS";
in {
nci.projects."helix-project".relPath = "";
nci.crates."helix-term" = {
overrides = {
add-meta.override = _: {meta.mainProgram = "hx";};
add-inputs.overrideAttrs = prev: {
buildInputs = (prev.buildInputs or []) ++ [stdenv.cc.cc.lib];
};
disable-grammar-builds = {
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;
};
disable-tests = {checkPhase = ":";};
set-stdenv.override = _: {inherit stdenv;};
set-filtered-src.override = _: {src = filteredSource;};
};
};
packages.helix-unwrapped = config.nci.outputs."helix-term".packages.release;
packages.helix-unwrapped-dev = config.nci.outputs."helix-term".packages.dev;
packages.helix = makeOverridableHelix config.packages.helix-unwrapped {};
packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {};
packages.default = config.packages.helix;
checks = {
# Build the crate itself
inherit (self.packages.${system}) helix;
clippy = craneLib.cargoClippy (commonArgs
// {
inherit cargoArtifacts;
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
});
overlayAttrs = {
inherit (config.packages) helix;
fmt = craneLib.cargoFmt commonArgs;
doc = craneLib.cargoDoc (commonArgs
// {
inherit cargoArtifacts;
});
test = craneLib.cargoTest (commonArgs
// {
inherit cargoArtifacts;
});
};
devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: {
nativeBuildInputs =
(old.nativeBuildInputs or [])
++ (with pkgs; [lld_13 cargo-flamegraph rust-analyzer])
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);
@ -162,7 +186,11 @@
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" }
ropey = { version = "1.6.0", default-features = false, features = ["simd"] }
smallvec = "1.10"
smallvec = "1.11"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
unicode-width = "0.1"
unicode-general-category = "0.6"
# slab = "0.4.2"
slotmap = "1.0"
tree-sitter = "0.20"
tree-sitter.workspace = true
once_cell = "1.18"
arc-swap = "1"
regex = "1"
bitflags = "2.3"
bitflags = "2.4"
ahash = "0.8.3"
hashbrown = { version = "0.14.0", features = ["raw"] }
dunce = "1.0"
@ -48,6 +48,9 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
etcetera = "0.8"
textwrap = "0.16.0"
nucleo.workspace = true
parking_lot = "0.12"
[dev-dependencies]
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;
GraphemeStr {
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,
}
}

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

@ -1,13 +1,13 @@
use std::collections::HashMap;
use std::{borrow::Cow, collections::HashMap};
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
graphemes::tab_width_at,
graphemes::{grapheme_width, tab_width_at},
syntax::{LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node,
Rope, RopeSlice,
Rope, RopeGraphemes, RopeSlice,
};
/// Enum representing indentation style.
@ -19,6 +19,10 @@ pub enum IndentStyle {
Spaces(u8),
}
// 16 spaces
const INDENTS: &str = " ";
const MAX_INDENT: u8 = 16;
impl IndentStyle {
/// Creates an `IndentStyle` from an indentation string.
///
@ -27,10 +31,10 @@ impl IndentStyle {
#[inline]
pub fn from_str(indent: &str) -> Self {
// 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(' ') {
IndentStyle::Spaces(indent.len() as u8)
IndentStyle::Spaces(indent.len().clamp(1, MAX_INDENT as usize) as u8)
} else {
IndentStyle::Tabs
}
@ -40,20 +44,13 @@ impl IndentStyle {
pub fn as_str(&self) -> &'static str {
match *self {
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) => {
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
// subsequent lines, ignoring lines that are all whitespace.
//
// Index 0 is for tabs, the rest are 1-8 spaces.
let histogram: [usize; 9] = {
let mut histogram = [0; 9];
// Index 0 is for tabs, the rest are 1-MAX_INDENT spaces.
let histogram: [usize; MAX_INDENT as usize + 1] = {
let mut histogram = [0; MAX_INDENT as usize + 1];
let mut prev_line_is_tabs = false;
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;
} else {
let amount = leading_count - prev_line_leading_count;
if amount <= 8 {
if amount <= MAX_INDENT as usize {
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:
/// - Successively add indent captures to get the (added) indent from a single 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 {
/// 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_always: 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 {
/// Add some other [Indentation] to this.
/// The added indent should be the total added indent from one line
fn add_line(&mut self, added: &Indentation) {
if added.indent > 0 && added.outdent == 0 {
self.indent += 1;
} else if added.outdent > 0 && added.indent == 0 {
self.outdent += 1;
}
/// The added indent should be the total added indent from one line.
/// Indent should always be added starting from the bottom (or equivalently, the innermost tree-sitter node).
fn add_line(&mut self, added: Indentation) {
// Align overrides the indent from outer scopes.
if self.align.is_some() {
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.
/// All the captures that are added in this way should be on the same line.
fn add_capture(&mut self, added: IndentCaptureType) {
match added {
IndentCaptureType::Indent => {
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 => {
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 {
let indent_level = if self.indent >= self.outdent {
self.indent - self.outdent
}
fn into_string(self, indent_style: &IndentStyle) -> String {
let indent = self.indent_always + self.indent;
let outdent = self.outdent_always + self.outdent;
let indent_level = if indent >= outdent {
indent - outdent
} else {
log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent);
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
#[derive(Debug)]
struct IndentCapture {
capture_type: IndentCaptureType,
scope: IndentScope,
}
#[derive(Clone, Copy)]
#[derive(Debug, Clone, PartialEq)]
enum IndentCaptureType {
Indent,
IndentAlways,
Outdent,
OutdentAlways,
/// Alignment given as a string of whitespace
Align(String),
}
impl IndentCaptureType {
fn default_scope(&self) -> IndentScope {
match self {
IndentCaptureType::Indent => IndentScope::Tail,
IndentCaptureType::Outdent => IndentScope::All,
IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
IndentCaptureType::Outdent | IndentCaptureType::OutdentAlways => IndentScope::All,
IndentCaptureType::Align(_) => IndentScope::All,
}
}
}
/// This defines which part of a node an [IndentCapture] applies to.
/// Each [IndentCaptureType] has a default scope, but the scope can be changed
/// with `#set!` property declarations.
#[derive(Clone, Copy)]
#[derive(Debug, Clone, Copy)]
enum IndentScope {
/// The indent applies to the whole node
All,
@ -311,6 +357,7 @@ enum IndentScope {
/// 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.
#[derive(Debug)]
enum ExtendCapture {
Extend,
PreventOnce,
@ -319,24 +366,41 @@ enum ExtendCapture {
/// The result of running a tree-sitter indent query. This stores for
/// each node (identified by its ID) the relevant captures (already filtered
/// by predicates).
#[derive(Debug)]
struct IndentQueryResult {
indent_captures: HashMap<usize, Vec<IndentCapture>>,
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(
query: &Query,
syntax: &Syntax,
cursor: &mut QueryCursor,
text: RopeSlice,
range: std::ops::Range<usize>,
// Position of the (optional) newly inserted line break.
// Given as (line, byte_pos)
new_line_break: Option<(usize, usize)>,
new_line_byte_pos: Option<usize>,
) -> IndentQueryResult {
let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
cursor.set_byte_range(range);
// Iterate over all captures from the query
for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
// Skip matches where not all custom predicates are fulfilled
@ -363,21 +427,13 @@ fn query_indents(
Some(QueryPredicateArg::Capture(capt1)),
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 n2 = m.nodes_for_capture_index(*capt2).next();
match (n1, 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?")
}
_ => 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!(
"Invalid indent query: Unknown predicate (\"{}\")",
@ -398,11 +471,28 @@ fn query_indents(
}) {
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 {
let capture_name = query.capture_names()[capture.index as usize].as_str();
let capture_type = match capture_name {
"indent" => IndentCaptureType::Indent,
"indent.always" => IndentCaptureType::IndentAlways,
"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_captures
.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
.entry(capture.node.id())
// Most entries only need to contain a single IndentCapture
.entry(node_id)
.or_insert_with(|| Vec::with_capacity(1))
.push(indent_capture);
.push(capture);
}
}
IndentQueryResult {
let result = IndentQueryResult {
indent_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.
@ -581,12 +706,14 @@ pub fn treesitter_indent_for_pos(
new_line: bool,
) -> Option<String> {
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
// computation. It may change if some predeceding node is extended
let mut node = syntax
.tree()
.root_node()
.descendant_for_byte_range(byte_pos, byte_pos)?;
let (query_result, deepest_preceding) = {
// The query range should intersect with all nodes directly preceding
// 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,
text,
query_range,
new_line.then_some((line, byte_pos)),
new_line_byte_pos,
);
ts_parser.cursors.push(cursor);
(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;
// 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
let mut indent_for_line = Indentation::default();
let mut indent_for_line_below = Indentation::default();
loop {
// This can safely be unwrapped because `first_in_line` contains
// one entry for each ancestor of the node (which is what we iterate over)
let is_first = *first_in_line.last().unwrap();
// 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 {
match definition.scope {
IndentScope::All => {
@ -668,28 +799,22 @@ pub fn treesitter_indent_for_pos(
}
if let Some(parent) = node.parent() {
let mut node_line = node.start_position().row;
let mut parent_line = parent.start_position().row;
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;
}
};
let node_line = get_node_start_line(node, new_line_byte_pos);
let parent_line = get_node_start_line(parent, new_line_byte_pos);
if node_line != parent_line {
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);
if node_line < line + (new_line as usize) {
result.add_line(indent_for_line_below);
}
if node_line == parent_line + 1 {
indent_for_line_below = indent_for_line;
} else {
result.add_line(&indent_for_line);
result.add_line(indent_for_line);
indent_for_line_below = Indentation::default();
}
indent_for_line = Indentation::default();
}
@ -701,13 +826,13 @@ pub fn treesitter_indent_for_pos(
if (node.start_position().row < line)
|| (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;
}
}
Some(result.as_string(indent_style))
Some(result.into_string(indent_style))
}
/// Returns the indentation for a new line.
@ -797,4 +922,138 @@ mod test {
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 diff;
pub mod doc_formatter;
pub mod fuzzy;
pub mod graphemes;
pub mod history;
pub mod increment;
@ -18,7 +19,6 @@ pub mod movement;
pub mod object;
pub mod path;
mod position;
pub mod register;
pub mod search;
pub mod selection;
pub mod shellwords;
@ -41,7 +41,9 @@ pub use helix_loader::find_workspace;
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
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 tendril::StrTendril as Tendril;
@ -66,5 +68,5 @@ pub use syntax::Syntax;
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};

@ -1,9 +1,9 @@
use crate::{Rope, RopeSlice};
#[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"))]
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::LF;
pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::LF;
/// Represents one of the valid Unicode line endings.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]

@ -1,8 +1,13 @@
use std::iter;
use ropey::RopeSlice;
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 MATCH_LIMIT: usize = 16;
// Limit matching pairs to only ( ) { } [ ] < > ' ' " "
const PAIRS: &[(char, char)] = &[
@ -24,7 +29,7 @@ const PAIRS: &[(char, char)] = &[
///
/// If no matching bracket is found, `None` is returned.
#[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)) {
return None;
}
@ -42,17 +47,23 @@ pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<
//
// If no surrounding scope is found, the function returns `None`.
#[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)
}
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 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 {
if node.is_named() {
let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
@ -60,17 +71,60 @@ fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) ->
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 traverse_parents {
node = node.parent()?;
} else {
return None;
// check if we are *on* the pair (special cased so we don't look
// at the current node twice and to jump to the start on that case)
if let Some(open) = as_close_pair(doc, &node) {
if let Some(pair_start) = find_pair_end(doc, node.prev_sibling(), open, Backward) {
return Some(pair_start);
}
}
if !traverse_parents {
// check if we are *on* the opening pair (special cased here as
// an opptimization since we only care about bracket on the cursor
// here)
if let Some(close) = as_open_pair(doc, &node) {
if let Some(pair_end) = find_pair_end(doc, node.next_sibling(), close, Forward) {
return Some(pair_end);
}
}
if 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.
@ -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.
#[must_use]
pub fn find_matching_bracket_current_line_plaintext(
doc: &Rope,
cursor_pos: usize,
) -> Option<usize> {
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
// Don't do anything when the cursor is not on top of a bracket.
let bracket = doc.char(cursor_pos);
if !is_valid_bracket(bracket) {
@ -144,11 +195,11 @@ fn is_forward_bracket(c: char) -> bool {
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)))
}
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 start_byte = node.start_byte();
@ -161,6 +212,55 @@ fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
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)]
mod tests {
use super::*;
@ -168,11 +268,11 @@ mod tests {
#[test]
fn test_find_matching_bracket_current_line_plaintext() {
let assert = |input: &str, pos, expected| {
let input = &Rope::from(input);
let actual = find_matching_bracket_current_line_plaintext(input, pos);
let input = RopeSlice::from(input);
let actual = find_matching_bracket_plaintext(input, pos);
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");
};

@ -1,4 +1,4 @@
use std::iter;
use std::{cmp::Reverse, iter};
use ropey::iter::Chars;
use tree_sitter::{Node, QueryCursor};
@ -16,7 +16,7 @@ use crate::{
syntax::LanguageConfiguration,
text_annotations::TextAnnotations,
textobject::TextObject,
visual_offset_from_block, Range, RopeSlice,
visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
};
#[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)
}
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 {
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)
}
pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevWordEnd)
pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd)
}
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::PrevLongWordStart
| WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd
);
// Special-case early-out.
@ -377,6 +382,7 @@ pub enum WordMotionTarget {
NextLongWordStart,
NextLongWordEnd,
PrevLongWordStart,
PrevLongWordEnd,
}
pub trait CharHelpers {
@ -393,6 +399,7 @@ impl CharHelpers for Chars<'_> {
WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd
);
// 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)
&& (!prev_ch.is_whitespace() || char_is_line_ending(next_ch))
}
WordMotionTarget::NextLongWordStart => {
WordMotionTarget::NextLongWordStart | WordMotionTarget::PrevLongWordEnd => {
is_long_word_boundary(prev_ch, next_ch)
&& (char_is_line_ending(next_ch) || !next_ch.is_whitespace())
}
@ -520,10 +527,10 @@ pub fn goto_treesitter_object(
let node = match dir {
Direction::Forward => nodes
.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
.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();
@ -549,6 +556,85 @@ pub fn goto_treesitter_object(
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)]
mod test {
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]
fn test_behaviour_when_moving_to_prev_paragraph_single() {
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
/// 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 = if path.is_relative() {
std::env::current_dir().map(|current_dir| current_dir.join(path))?
helix_loader::current_working_dir().join(path)
} else {
path
};
Ok(get_normalized_path(path.as_path()))
get_normalized_path(path.as_path())
}
pub fn get_relative_path(path: &Path) -> PathBuf {
let path = PathBuf::from(path);
let path = if path.is_absolute() {
let cwdir = std::env::current_dir()
.map(|path| get_normalized_path(&path))
.expect("couldn't determine current directory");
let cwdir = get_normalized_path(&helix_loader::current_working_dir());
get_normalized_path(&path)
.strip_prefix(cwdir)
.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 {
let cwd = std::env::current_dir().unwrap_or_default();
let cwd = helix_loader::current_working_dir();
let path = path
.as_ref()
.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()
}
/// Map a range through a set of changes. Returns a new range representing the same position
/// after the changes are applied.
pub fn map(self, changes: &ChangeSet) -> Self {
/// Map a range through a set of changes. Returns a new range representing
/// the same position after the changes are applied. Note that this
/// 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;
let (anchor, head) = match self.anchor.cmp(&self.head) {
Ordering::Equal => (
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,
if changes.is_empty() {
return self;
}
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`.
@ -451,17 +452,36 @@ impl Selection {
/// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document.
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() {
return self;
}
Self::new(
self.ranges
.into_iter()
.map(|range| range.map(changes))
.collect(),
self.primary_index,
)
let positions_to_map = self.ranges.iter_mut().flat_map(|range| {
use std::cmp::Ordering;
range.old_visual_position = None;
match range.anchor.cmp(&range.head) {
Ordering::Equal => [
(&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] {
@ -497,6 +517,9 @@ impl Selection {
/// Normalizes a `Selection`.
fn normalize(mut self) -> Self {
if self.len() < 2 {
return self;
}
let mut primary = self.ranges[self.primary_index];
self.ranges.sort_unstable_by_key(Range::from);
@ -561,17 +584,12 @@ impl Selection {
assert!(!ranges.is_empty());
debug_assert!(primary_index < ranges.len());
let mut selection = Self {
let selection = Self {
ranges,
primary_index,
};
if selection.ranges.len() > 1 {
// TODO: only normalize if needed (any ranges out of order)
selection = selection.normalize();
}
selection
selection.normalize()
}
/// Takes a closure and maps each `Range` over the closure.
@ -612,11 +630,19 @@ impl Selection {
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))
}
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))
}

@ -4,7 +4,7 @@ use crate::{
diagnostic::Severity,
regex::Regex,
transaction::{ChangeSet, Operation},
Rope, RopeSlice, Tendril,
RopeSlice, Tendril,
};
use ahash::RandomState;
@ -48,6 +48,21 @@ where
.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>
where
D: serde::Deserializer<'de>,
@ -424,6 +439,7 @@ pub struct DebuggerQuirks {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentationConfiguration {
#[serde(deserialize_with = "deserialize_tab_width")]
pub tab_width: usize,
pub unit: String,
}
@ -802,7 +818,10 @@ impl Loader {
// 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));
static SHEBANG_REGEX: Lazy<Regex> =
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 {
pub fn new(
source: &Rope,
source: RopeSlice,
config: Arc<HighlightConfiguration>,
loader: Arc<Loader>,
) -> Option<Self> {
@ -951,8 +970,8 @@ impl Syntax {
pub fn update(
&mut self,
old_source: &Rope,
source: &Rope,
old_source: RopeSlice,
source: RopeSlice,
changeset: &ChangeSet,
) -> Result<(), Error> {
let mut queue = VecDeque::new();
@ -1119,12 +1138,38 @@ impl Syntax {
layer.tree().root_node(),
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 last_injection_end = 0;
for mat in matches {
let (injection_capture, content_node, included_children) = layer
.config
.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
// in the stream of captures.
mat.remove();
@ -1139,53 +1184,27 @@ impl Syntax {
intersect_ranges(&layer.ranges, &[content_node], included_children);
if !ranges.is_empty() {
if content_node.start_byte() < last_injection_end {
continue;
}
last_injection_end = content_node.end_byte();
injections.push((config, ranges));
}
}
}
}
// Process combined injections.
if let Some(combined_injections_query) = &layer.config.combined_injections_query {
let mut injections_by_pattern_index =
vec![
(None, Vec::new(), IncludedChildren::default());
combined_injections_query.pattern_count()
];
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
{
for (lang_name, content_nodes, included_children) in combined_injections {
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,
);
let ranges =
intersect_ranges(&layer.ranges, &content_nodes, included_children);
if !ranges.is_empty() {
injections.push((config, ranges));
}
}
}
}
}
let depth = layer.depth + 1;
// TODO: can't inline this since matches borrows self.layers
@ -1371,7 +1390,7 @@ impl LanguageLayer {
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
.set_included_ranges(&self.ranges)
.map_err(|_| Error::InvalidRanges)?;
@ -1386,7 +1405,7 @@ impl LanguageLayer {
&mut |byte, _| {
if byte <= source.len_bytes() {
let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
chunk[byte - start_byte..].as_bytes()
&chunk.as_bytes()[byte - start_byte..]
} else {
// out of range
&[]
@ -1402,7 +1421,7 @@ impl LanguageLayer {
}
pub(crate) fn generate_edits(
old_text: &Rope,
old_text: RopeSlice,
changeset: &ChangeSet,
) -> Vec<tree_sitter::InputEdit> {
use Operation::*;
@ -1418,7 +1437,7 @@ pub(crate) fn generate_edits(
// 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 line = text.char_to_line(pos);
let line_start_byte = text.line_to_byte(line);
@ -1544,7 +1563,7 @@ pub struct HighlightConfiguration {
pub language: Grammar,
pub query: Query,
injections_query: Query,
combined_injections_query: Option<Query>,
combined_injections_patterns: Vec<usize>,
highlights_pattern_index: usize,
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>,
@ -1595,7 +1614,7 @@ impl<'a> Iterator for ChunksBytes<'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>;
fn text(&mut self, node: Node) -> Self::I {
@ -1609,7 +1628,7 @@ impl<'a> TextProvider<'a> for RopeProvider<'a> {
struct HighlightIterLayer<'a> {
_tree: Option<Tree>,
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,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
@ -1660,26 +1679,15 @@ impl HighlightConfiguration {
}
}
let mut injections_query = Query::new(language, injection_query)?;
// Construct a separate query just for dealing with the 'combined injections'.
// Disable the combined injection patterns in the main query.
let mut combined_injections_query = Query::new(language, injection_query)?;
let mut has_combined_queries = false;
for pattern_index in 0..injections_query.pattern_count() {
let settings = injections_query.property_settings(pattern_index);
if settings.iter().any(|s| &*s.key == "injection.combined") {
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
};
let injections_query = Query::new(language, injection_query)?;
let combined_injections_patterns = (0..injections_query.pattern_count())
.filter(|&i| {
injections_query
.property_settings(i)
.iter()
.any(|s| &*s.key == "injection.combined")
})
.collect();
// Find all of the highlighting patterns that are disabled for nodes that
// have been identified as local variables.
@ -1728,7 +1736,7 @@ impl HighlightConfiguration {
language,
query,
injections_query,
combined_injections_query,
combined_injections_patterns,
highlights_pattern_index,
highlight_indices,
non_local_variable_patterns,
@ -2524,7 +2532,7 @@ mod test {
let mut cursor = QueryCursor::new();
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 mut test = |capture, range| {
@ -2598,7 +2606,7 @@ mod test {
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 root = tree.root_node();
assert_eq!(root.kind(), "source_file");
@ -2625,7 +2633,7 @@ mod test {
&doc,
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);
assert_eq!(
@ -2654,7 +2662,7 @@ mod test {
let mut doc = Rope::from("fn test() {}");
let transaction =
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);
assert_eq!(doc, "fn test(a: u32) {}");
@ -2688,7 +2696,7 @@ mod test {
let language = get_language(language_name).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()

@ -1,7 +1,8 @@
use ropey::RopeSlice;
use smallvec::SmallVec;
use crate::{Range, Rope, Selection, Tendril};
use std::borrow::Cow;
use std::{borrow::Cow, iter::once};
/// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>);
@ -42,7 +43,7 @@ impl ChangeSet {
}
#[must_use]
pub fn new(doc: &Rope) -> Self {
pub fn new(doc: RopeSlice) -> Self {
let len = doc.len_chars();
Self {
changes: Vec::new(),
@ -326,20 +327,75 @@ impl ChangeSet {
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
/// 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, pos: usize, assoc: Assoc) -> usize {
/// This is equivalent to updating each position with `map_pos`:
///
/// ``` no-compile
/// for (pos, assoc) in positions {
/// *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::*;
let mut positions = positions.peekable();
let mut old_pos = 0;
let mut new_pos = 0;
let mut iter = self.changes.iter().enumerate().peekable();
let mut iter = self.changes.iter().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 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 {
Delete(i) | Retain(i) => *i,
Insert(_) => 0,
@ -348,46 +404,51 @@ impl ChangeSet {
match change {
Retain(_) => {
if old_end > pos {
return new_pos + (pos - old_pos);
}
map!(
|pos, _| (old_end > pos).then_some(new_pos + (pos - old_pos)),
i
);
new_pos += len;
}
Delete(_) => {
// in range
if old_end > pos {
return new_pos;
}
map!(|pos, _| (old_end > pos).then_some(new_pos), i);
}
Insert(s) => {
let ins = s.chars().count();
// a subsequent delete means a replace, consume it
if let Some(Delete(len)) = iter.peek() {
if let Some((_, Delete(len))) = iter.peek() {
iter.next();
old_end = old_pos + len;
// in range of replaced text
if old_end > pos {
map!(
|pos, assoc| (old_end > pos).then(|| {
// at point or tracking before
if pos == old_pos || assoc == Assoc::Before {
return new_pos;
new_pos
} else {
// place to end of insert
return new_pos + ins;
}
new_pos + ins
}
}),
i
);
} else {
// at insert point
if old_pos == pos {
map!(
|pos, assoc| (old_pos == pos).then(|| {
// return position before inserted text
if assoc == Assoc::Before {
return new_pos;
new_pos
} else {
// after text
return new_pos + ins;
}
new_pos + ins
}
}),
i
);
}
new_pos += ins;
@ -395,14 +456,20 @@ impl ChangeSet {
}
old_pos = old_end;
}
let out_of_bounds: Vec<_> = positions.collect();
if pos > old_pos {
panic!(
"Position {} is out of range for changeset len {}!",
pos, old_pos
)
panic!("Positions {out_of_bounds:?} are out of range for changeset len {old_pos}!",)
}
new_pos
/// Map a position through the changes.
///
/// `assoc` indicates which side to associate the position with. `Before` will keep the
/// 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 {
@ -422,7 +489,7 @@ impl Transaction {
/// Create a new, empty transaction.
pub fn new(doc: &Rope) -> Self {
Self {
changes: ChangeSet::new(doc),
changes: ChangeSet::new(doc.slice(..)),
selection: None,
}
}
@ -803,6 +870,20 @@ mod test {
};
assert_eq!(cs.map_pos(2, Assoc::Before), 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]
@ -869,9 +950,9 @@ mod test {
#[test]
fn combine_with_empty() {
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());
let changes = a.compose(b);
@ -885,9 +966,9 @@ mod test {
const TEST_CASE: &str = "Hello, これはヘリックスエディターです!";
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());
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]]
name = "rust"
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::{
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::Loader,
syntax::{Configuration, Loader},
Syntax,
};
use std::path::PathBuf;
use ropey::Rope;
use std::{ops::Range, path::PathBuf, process::Command};
#[test]
fn test_treesitter_indent_rust() {
test_treesitter_indent("rust.rs", "source.rust");
standard_treesitter_test("rust.rs", "source.rust");
}
#[test]
fn test_treesitter_indent_rust_2() {
test_treesitter_indent("indent.rs", "source.rust");
// TODO Use commands.rs as indentation test.
// Currently this fails because we can't align the parameters of a closure yet
// test_treesitter_indent("commands.rs", "source.rust");
fn test_treesitter_indent_cpp() {
standard_treesitter_test("cpp.cpp", "source.cpp");
}
#[test]
fn test_treesitter_indent_rust_helix() {
// We pin a specific git revision to prevent unrelated changes from causing the indent tests to fail.
// Ideally, someone updates this once in a while and fixes any errors that occur.
let rev = "af382768cdaf89ff547dbd8f644a1bddd90e7c8f";
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]
@ -50,20 +152,41 @@ fn test_indent_level_for_line_with_spaces_and_tabs() {
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"));
test_dir.push("tests/data/indent");
test_dir
}
fn indent_test_path(name: &str) -> PathBuf {
let mut path = indent_tests_dir();
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()
}
let mut test_file = test_dir.clone();
test_file.push(file_name);
let test_file = std::fs::File::open(test_file).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();
test_treesitter_indent(file_name, doc, lang_scope, Vec::new())
}
let mut config_file = test_dir;
config_file.push("languages.toml");
let config = std::fs::read_to_string(config_file).unwrap();
let config = toml::from_str(&config).unwrap();
let loader = Loader::new(config);
/// Test that all the lines in the given file are indented as expected.
/// ignored_lines is a list of (1-indexed) line ranges that are excluded from this test.
fn test_treesitter_indent(
test_name: &str,
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
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());
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 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 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() {
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) {
let tab_and_indent_width: usize = 4;
let tab_width: usize = 4;
let suggested_indent = treesitter_indent_for_pos(
indent_query,
&syntax,
&IndentStyle::Spaces(tab_and_indent_width as u8),
tab_and_indent_width,
tab_and_indent_width,
&indent_style,
tab_width,
indent_style.indent_width(tab_width),
text,
i,
text.line_to_char(i) + pos,
@ -94,7 +221,8 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
.unwrap();
assert!(
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,
line.slice(..line.len_chars()-1),
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"] }
toml = "0.7"
etcetera = "0.8"
tree-sitter = "0.20"
tree-sitter.workspace = true
once_cell = "1.18"
log = "0.4"
which = "4.4"
# TODO: these two should be on !wasm32 only
# cloning/compiling tree-sitter grammars
cc = { version = "1" }
threadpool = { version = "1.0" }
tempfile = "3.5.0"
tempfile = "3.8.0"
dunce = "1.0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
libloading = "0.8"

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

@ -85,7 +85,16 @@ pub fn get_language(name: &str) -> Result<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<()> {
ensure_git_is_available()?;
// We do not need to fetch local grammars.
let mut grammars = get_grammar_configs()?;
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<()> {
ensure_git_is_available()?;
let grammars = get_grammar_configs()?;
println!("Building {} grammars", grammars.len());
let results = run_parallel(grammars, move |grammar| {

@ -3,29 +3,56 @@ pub mod grammar;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::{Path, PathBuf};
use std::sync::RwLock;
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>> =
once_cell::sync::Lazy::new(prioritize_runtime_dirs);
static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(|| {
let config_dir = config_dir();
static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
// Get the current working directory.
// 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();
}
config_dir.join("config.toml")
});
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
}
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();
}
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
///
/// The priority is:
@ -122,10 +149,11 @@ pub fn cache_dir() -> PathBuf {
}
pub fn config_file() -> PathBuf {
CONFIG_FILE
.get()
.map(|path| path.to_path_buf())
.unwrap_or_else(|| config_dir().join("config.toml"))
CONFIG_FILE.get().map(|path| path.to_path_buf()).unwrap()
}
pub fn log_file() -> PathBuf {
LOG_FILE.get().map(|path| path.to_path_buf()).unwrap()
}
pub fn workspace_config_file() -> PathBuf {
@ -136,7 +164,7 @@ pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml")
}
pub fn log_file() -> PathBuf {
pub fn default_log_file() -> PathBuf {
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).
/// Otherwise (workspace, false) is returned
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() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return (ancestor.to_owned(), false);
@ -227,13 +255,37 @@ pub fn find_workspace() -> (PathBuf, bool) {
(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)]
mod merge_toml_tests {
use std::str;
use super::merge_toml_values;
use super::{current_working_dir, merge_toml_values, set_current_working_dir};
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]
fn language_toml_map_merges() {
const USER: &str = r#"

@ -19,12 +19,13 @@ helix-parsec = { version = "0.6", path = "../helix-parsec" }
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
globset = "0.4.13"
log = "0.4"
lsp-types = { version = "0.94" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "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"
which = "4.4"
parking_lot = "0.12.1"

@ -7,8 +7,9 @@ use crate::{
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
WorkspaceFoldersChangeEvent,
};
use lsp_types as lsp;
use parking_lot::Mutex;
@ -543,6 +544,10 @@ impl Client {
normalizes_line_endings: Some(false),
change_annotation_support: None,
}),
did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities {
dynamic_registration: Some(true),
relative_pattern_support: Some(false),
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@ -609,6 +614,12 @@ impl Client {
.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()
}),
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
@ -954,6 +965,24 @@ impl Client {
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(
&self,
text_document: lsp::TextDocumentIdentifier,
@ -1428,4 +1457,13 @@ impl Client {
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;
pub mod file_event;
pub mod jsonrpc;
pub mod snippet;
mod transport;
@ -377,7 +378,7 @@ pub mod util {
.expect("transaction must be valid for primary selection");
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,
selection,
|range| {
@ -420,6 +421,11 @@ pub mod util {
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_primary_idx = 0;
let primary_range = selection.primary();
@ -428,9 +434,8 @@ pub mod util {
mapped_primary_idx = mapped_selection.len()
}
let range = range.map(changes);
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
let Some(tabstops) = tabstops else{
let Some(tabstops) = tabstops else {
// no tabstop normal mapping
mapped_selection.push(range);
continue;
@ -543,6 +548,7 @@ pub enum MethodCall {
WorkspaceFolders,
WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
}
impl MethodCall {
@ -566,6 +572,10 @@ impl MethodCall {
let params: lsp::RegistrationParams = params.parse()?;
Self::RegisterCapability(params)
}
lsp::request::UnregisterCapability::METHOD => {
let params: lsp::UnregistrationParams = params.parse()?;
Self::UnregisterCapability(params)
}
_ => {
return Err(Error::Unhandled);
}
@ -625,6 +635,7 @@ pub struct Registry {
syn_loader: Arc<helix_core::syntax::Loader>,
counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
pub file_event_handler: file_event::Handler,
}
impl Registry {
@ -634,6 +645,7 @@ impl Registry {
syn_loader,
counter: 0,
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) {
self.file_event_handler.remove_client(id);
self.inner.retain(|_, language_servers| {
language_servers.retain(|ls| id != ls.id());
!language_servers.is_empty()
@ -711,6 +724,7 @@ impl Registry {
.unwrap();
for old_client in old_clients {
self.file_event_handler.remove_client(old_client.id());
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
@ -727,6 +741,7 @@ impl Registry {
pub fn stop(&mut self, name: &str) {
if let Some(clients) = self.inner.remove(name) {
for client in clients {
self.file_event_handler.remove_client(client.id());
tokio::spawn(async move {
let _ = client.force_shutdown().await;
});
@ -927,7 +942,7 @@ pub fn find_lsp_workspace(
let mut file = if file.is_absolute() {
file.to_path_buf()
} 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)
};
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
loop {
@ -391,7 +396,10 @@ impl Transport {
}
msg = client_rx.recv() => {
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
if let Payload::Notification(_) = msg {
continue;

@ -24,6 +24,7 @@ path = "src/main.rs"
[dependencies]
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-lsp = { version = "0.6", path = "../helix-lsp" }
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"] }
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"
tokio-stream = "0.1"
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"
# File picker
fuzzy-matcher = "0.3"
nucleo.workspace = true
ignore = "0.4"
# markdown doc rendering
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
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]
helix-loader = { version = "0.6", path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.10"
indoc = "2.0.1"
tempfile = "3.4.0"
smallvec = "1.11"
indoc = "2.0.3"
tempfile = "3.8.0"

@ -5,7 +5,11 @@ use helix_core::{
path::get_relative_path,
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::{
align_view,
document::DocumentSavedEventResult,
@ -29,13 +33,9 @@ use crate::{
};
use log::{debug, error, warn};
use std::{
collections::btree_map::Entry,
io::{stdin, stdout},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
#[cfg(not(feature = "integration"))]
use std::io::stdout;
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
use anyhow::{Context, Error};
@ -45,8 +45,6 @@ use {signal_hook::consts::signal, signal_hook_tokio::Signals};
#[cfg(windows)]
type Signals = futures_util::stream::Empty<()>;
const LSP_DEADLINE: Duration = Duration::from_millis(16);
#[cfg(not(feature = "integration"))]
use tui::backend::CrosstermBackend;
@ -76,7 +74,6 @@ pub struct Application {
signals: Signals,
jobs: Jobs,
lsp_progress: LspProgressMap,
last_render: Instant,
}
#[cfg(feature = "integration")]
@ -163,11 +160,11 @@ impl Application {
let path = helix_loader::runtime_file(Path::new("tutor"));
editor.open(&path, Action::VerticalSplit)?;
// 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() {
let first = &args.files[0].0; // we know it's not empty
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);
let picker = ui::file_picker(".".into(), &config.load().editor);
compositor.push(Box::new(overlaid(picker)));
@ -215,11 +212,6 @@ impl Application {
}
} else if stdin().is_tty() || cfg!(feature = "integration") {
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 {
editor
.new_file_from_stdin(Action::VerticalSplit)
@ -253,7 +245,6 @@ impl Application {
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
last_render: Instant::now(),
};
Ok(app)
@ -266,16 +257,8 @@ impl Application {
scroll: None,
};
// Acquire mutable access to the redraw_handle lock
// to ensure that there are no tasks running that want to block rendering
drop(cx.editor.redraw_handle.1.write().await);
helix_event::start_frame();
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
.terminal
@ -297,10 +280,9 @@ impl Application {
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{
self.render().await;
self.last_render = Instant::now();
loop {
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
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{
loop {
if self.editor.should_close() {
@ -564,16 +546,7 @@ impl Application {
let bytes = doc_save_event.text.len_bytes();
if doc.path() != Some(&doc_save_event.path) {
if let Err(err) = 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;
}
doc.set_path(Some(&doc_save_event.path));
let loader = self.editor.syn_loader.clone();
@ -609,12 +582,7 @@ impl Application {
EditorEvent::LanguageServerMessage((id, call)) => {
self.handle_language_server_message(call, id).await;
// limit render calls for fast language server messages
let last = self.editor.language_servers.incoming.is_empty();
if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render().await;
self.last_render = Instant::now();
}
helix_event::request_redraw();
}
EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await;
@ -622,6 +590,9 @@ impl Application {
self.render().await;
}
}
EditorEvent::Redraw => {
self.render().await;
}
EditorEvent::IdleTimer => {
self.editor.clear_idle_timer();
self.handle_idle_timeout().await;
@ -636,10 +607,7 @@ impl Application {
false
}
pub async fn handle_terminal_events(
&mut self,
event: Result<CrosstermEvent, crossterm::ErrorKind>,
) {
pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
@ -746,7 +714,12 @@ impl Application {
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| {
if let Some(version) = params.version {
if version != doc.version() {
@ -1042,9 +1015,12 @@ impl Application {
Ok(serde_json::Value::Null)
}
Ok(MethodCall::ApplyWorkspaceEdit(params)) => {
let language_server = language_server!();
if language_server.is_initialized() {
let offset_encoding = language_server.offset_encoding();
let res = apply_workspace_edit(
&mut self.editor,
helix_lsp::OffsetEncoding::Utf8,
offset_encoding,
&params.edit,
);
@ -1056,6 +1032,14 @@ impl Application {
.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(json!(&*language_server!().workspace_folders().await))
@ -1080,17 +1064,65 @@ impl Application {
.collect();
Ok(json!(result))
}
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");
Ok(MethodCall::RegisterCapability(params)) => {
if let Some(client) = self
.editor
.language_servers
.iter_clients()
.find(|client| client.id() == server_id)
{
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 any capabilities. We should send a MethodNotFound JSONRPC error in this
// 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(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));
@ -1116,7 +1148,7 @@ impl Application {
pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{
self.claim_term().await?;

File diff suppressed because it is too large Load Diff

@ -2,7 +2,7 @@ use super::{Context, Editor};
use crate::{
compositor::{self, Compositor},
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 helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
@ -73,11 +73,10 @@ fn thread_picker(
let debugger = debugger!(editor);
let thread_states = debugger.thread_states.clone();
let picker = FilePicker::new(
threads,
thread_states,
move |cx, thread, _action| callback_fn(cx.editor, thread),
move |editor, thread| {
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
callback_fn(cx.editor, thread)
})
.with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frame = frames.get(0)?;
let path = frame.source.as_ref()?.path.clone()?;
@ -86,8 +85,7 @@ fn thread_picker(
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
));
Some((path.into(), pos))
},
);
});
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();
@ -341,8 +339,12 @@ fn debug_parameter_prompt(
.to_owned();
let completer = match field_type {
"filename" => ui::completers::filename,
"directory" => ui::completers::directory,
"filename" => |editor: &Editor, input: &str| {
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,
};
@ -728,10 +730,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let frames = debugger.stack_frames[&thread_id].clone();
let picker = FilePicker::new(
frames,
(),
move |cx, frame, _action| {
let picker = Picker::new(frames, (), move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find
let pos = debugger.stack_frames[&thread_id]
@ -745,8 +744,8 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
if let Some(frame) = &frame {
jump_to_stack_frame(cx.editor, frame);
}
},
move |_editor, frame| {
})
.with_preview(move |_editor, frame| {
frame
.source
.as_ref()
@ -760,7 +759,6 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
)),
)
})
},
);
});
cx.push_layer(Box::new(picker))
}

@ -32,8 +32,8 @@ use crate::{
compositor::{self, Compositor},
job::Callback,
ui::{
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
Popup, PromptEvent,
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
PromptEvent,
},
};
@ -196,7 +196,6 @@ fn location_to_file_location(location: &lsp::Location) -> FileLocation {
(path.into(), line)
}
// TODO: share with symbol picker(symbol.location)
fn jump_to_location(
editor: &mut Editor,
location: &lsp::Location,
@ -214,15 +213,16 @@ fn jump_to_location(
return;
}
};
match editor.open(&path, action) {
Ok(_) => (),
let doc = match editor.open(&path, action) {
Ok(id) => doc_mut!(editor, &id),
Err(err) => {
let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
editor.set_error(err);
return;
}
}
let (view, doc) = current!(editor);
};
let view = view_mut!(editor);
// TODO: convert inside server
let new_range =
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
// (for example start of the function).
doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
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 {
// TODO: drop current_path comparison and instead use workspace: bool flag?
FilePicker::new(
symbols,
current_path.clone(),
move |cx, item, action| {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
if current_path.as_ref() != Some(&item.symbol.location.uri) {
let uri = &item.symbol.location.uri;
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)),
)
Picker::new(symbols, current_path, move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
})
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
.truncate_start(false)
}
@ -291,9 +264,9 @@ enum DiagnosticsFormat {
fn diag_picker(
cx: &Context,
diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
current_path: Option<lsp::Url>,
_current_path: Option<lsp::Url>,
format: DiagnosticsFormat,
) -> FilePicker<PickerDiagnostic> {
) -> Picker<PickerDiagnostic> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs
@ -319,7 +292,7 @@ fn diag_picker(
error: cx.editor.theme.get("error"),
};
FilePicker::new(
Picker::new(
flat_diag,
(styles, format),
move |cx,
@ -329,28 +302,18 @@ fn diag_picker(
offset_encoding,
},
action| {
if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
} else {
let path = url.to_file_path().unwrap();
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);
}
jump_to_location(
cx.editor,
&lsp::Location::new(url.clone(), diag.range),
*offset_encoding,
action,
)
},
move |_editor, PickerDiagnostic { url, diag, .. }| {
)
.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)
}
@ -443,6 +406,15 @@ pub fn symbol_picker(cx: &mut Context) {
pub fn workspace_symbol_picker(cx: &mut Context) {
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 doc = doc!(editor);
@ -726,7 +698,8 @@ pub fn code_action(cx: &mut Context) {
// always present here
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");
return;
};
@ -739,7 +712,25 @@ pub fn code_action(cx: &mut Context) {
}
lsp::CodeActionOrCommand::CodeAction(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);
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
@ -1029,7 +1020,7 @@ fn goto_impl(
locations: Vec<lsp::Location>,
offset_encoding: OffsetEncoding,
) {
let cwdir = std::env::current_dir().unwrap_or_default();
let cwdir = helix_loader::current_working_dir();
match locations.as_slice() {
[location] => {
@ -1039,14 +1030,10 @@ fn goto_impl(
editor.set_error("No definition found.");
}
_locations => {
let picker = FilePicker::new(
locations,
cwdir,
move |cx, location, action| {
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
},
move |_editor, location| Some(location_to_file_location(location)),
);
})
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
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
// automatically on backspace, trigger characters, etc.
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;
};
@ -1455,7 +1443,8 @@ pub fn rename_symbol(cx: &mut Context) {
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
.find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
else {
cx.editor.set_error("No configured language server supports symbol renaming");
cx.editor
.set_error("No configured language server supports symbol renaming");
return;
};

@ -5,9 +5,11 @@ use crate::job::Job;
use super::*;
use helix_core::fuzzy::fuzzy_match;
use helix_core::{encoding, shellwords::Shellwords};
use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::editor::{Action, CloseError, ConfigEvent};
use serde_json::Value;
use ui::completers::{self, Completer};
#[derive(Clone)]
@ -671,7 +673,7 @@ pub fn write_all_impl(
}
if doc.path().is_none() {
if write_scratch {
errors.push("cannot write a buffer without a filename\n");
errors.push("cannot write a buffer without a filename");
}
return None;
}
@ -738,6 +740,18 @@ fn write_all(
write_all_impl(cx, false, true)
}
fn force_write_all(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_all_impl(cx, true, true)
}
fn write_all_quit(
cx: &mut compositor::Context,
_args: &[Cow<str>],
@ -891,7 +905,27 @@ fn yank_main_selection_to_clipboard(
return Ok(());
}
yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
yank_primary_selection_impl(cx.editor, '*');
Ok(())
}
fn yank_joined(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
ensure!(args.len() <= 1, ":yank-join takes at most 1 argument");
let doc = doc!(cx.editor);
let default_sep = Cow::Borrowed(doc.line_ending.as_str());
let separator = args.first().unwrap_or(&default_sep);
let register = cx.editor.selected_register.unwrap_or('"');
yank_joined_impl(cx.editor, separator, register);
Ok(())
}
fn yank_joined_to_clipboard(
@ -906,7 +940,8 @@ fn yank_joined_to_clipboard(
let doc = doc!(cx.editor);
let default_sep = Cow::Borrowed(doc.line_ending.as_str());
let separator = args.first().unwrap_or(&default_sep);
yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
yank_joined_impl(cx.editor, separator, '*');
Ok(())
}
fn yank_main_selection_to_primary_clipboard(
@ -918,7 +953,8 @@ fn yank_main_selection_to_primary_clipboard(
return Ok(());
}
yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
yank_primary_selection_impl(cx.editor, '+');
Ok(())
}
fn yank_joined_to_primary_clipboard(
@ -933,7 +969,8 @@ fn yank_joined_to_primary_clipboard(
let doc = doc!(cx.editor);
let default_sep = Cow::Borrowed(doc.line_ending.as_str());
let separator = args.first().unwrap_or(&default_sep);
yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
yank_joined_impl(cx.editor, separator, '+');
Ok(())
}
fn paste_clipboard_after(
@ -945,7 +982,8 @@ fn paste_clipboard_after(
return Ok(());
}
paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
paste(cx.editor, '*', Paste::After, 1);
Ok(())
}
fn paste_clipboard_before(
@ -957,7 +995,8 @@ fn paste_clipboard_before(
return Ok(());
}
paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Clipboard, 1)
paste(cx.editor, '*', Paste::Before, 1);
Ok(())
}
fn paste_primary_clipboard_after(
@ -969,7 +1008,8 @@ fn paste_primary_clipboard_after(
return Ok(());
}
paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
paste(cx.editor, '+', Paste::After, 1);
Ok(())
}
fn paste_primary_clipboard_before(
@ -981,30 +1021,8 @@ fn paste_primary_clipboard_before(
return Ok(());
}
paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Selection, 1)
}
fn replace_selections_with_clipboard_impl(
cx: &mut compositor::Context,
clipboard_type: ClipboardType,
) -> anyhow::Result<()> {
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
match cx.editor.clipboard_provider.get_contents(clipboard_type) {
Ok(contents) => {
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), Some(contents.as_str().into()))
});
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
paste(cx.editor, '+', Paste::Before, 1);
Ok(())
}
Err(e) => Err(e.context("Couldn't get system clipboard contents")),
}
}
fn replace_selections_with_clipboard(
@ -1016,7 +1034,8 @@ fn replace_selections_with_clipboard(
return Ok(());
}
replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
replace_with_yanked_impl(cx.editor, '*', 1);
Ok(())
}
fn replace_selections_with_primary_clipboard(
@ -1028,7 +1047,8 @@ fn replace_selections_with_primary_clipboard(
return Ok(());
}
replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
replace_with_yanked_impl(cx.editor, '+', 1);
Ok(())
}
fn show_clipboard_provider(
@ -1041,7 +1061,7 @@ fn show_clipboard_provider(
}
cx.editor
.set_status(cx.editor.clipboard_provider.name().to_string());
.set_status(cx.editor.registers.clipboard_provider_name().to_string());
Ok(())
}
@ -1061,14 +1081,11 @@ fn change_current_directory(
.as_ref(),
);
if let Err(e) = std::env::set_current_dir(dir) {
bail!("Couldn't change the current working directory: {}", e);
}
helix_loader::set_current_working_dir(dir)?;
let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
cx.editor.set_status(format!(
"Current working directory is now {}",
cwd.display()
helix_loader::current_working_dir().display()
));
Ok(())
}
@ -1082,9 +1099,14 @@ fn show_current_directory(
return Ok(());
}
let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
cx.editor
.set_status(format!("Current working directory is {}", cwd.display()));
let cwd = helix_loader::current_working_dir();
let message = format!("Current working directory is {}", cwd.display());
if cwd.exists() {
cx.editor.set_status(message);
} else {
cx.editor.set_error(format!("{} (deleted)", message));
}
Ok(())
}
@ -1244,12 +1266,17 @@ fn reload(
}
let scrolloff = cx.editor.config().scrolloff;
let redraw_handle = cx.editor.redraw_handle.clone();
let (view, doc) = current!(cx.editor);
doc.reload(view, &cx.editor.diff_providers, redraw_handle)
.map(|_| {
doc.reload(view, &cx.editor.diff_providers).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})
})?;
if let Some(path) = doc.path() {
cx.editor
.language_servers
.file_event_handler
.file_changed(path.clone());
}
Ok(())
}
fn reload_all(
@ -1288,8 +1315,13 @@ fn reload_all(
// Ensure that the view is synced with the document's history.
view.sync_changes(doc);
let redraw_handle = cx.editor.redraw_handle.clone();
doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
doc.reload(view, &cx.editor.diff_providers)?;
if let Some(path) = doc.path() {
cx.editor
.language_servers
.file_event_handler
.file_changed(path.clone());
}
for view_id in view_ids {
let view = view_mut!(cx.editor, view_id);
@ -1338,9 +1370,8 @@ fn lsp_workspace_command(
.map(|options| (ls.id(), options))
})
else {
cx.editor.set_status(
"No active language servers for this document support workspace commands",
);
cx.editor
.set_status("No active language servers for this document support workspace commands");
return Ok(());
};
@ -1646,7 +1677,7 @@ fn tutor(
let path = helix_loader::runtime_file(Path::new("tutor"));
cx.editor.open(&path, Action::Replace)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(cx.editor).set_path(None)?;
doc_mut!(cx.editor).set_path(None);
Ok(())
}
@ -1764,7 +1795,7 @@ fn set_option(
*value = if value.is_string() {
// JSON strings require quotes, so we can't .parse() directly
serde_json::Value::String(arg.to_string())
Value::String(arg.to_string())
} else {
arg.parse().map_err(field_error)?
};
@ -1800,29 +1831,21 @@ fn toggle_option(
let pointer = format!("/{}", key.replace('.', "/"));
let value = config.pointer_mut(&pointer).ok_or_else(key_error)?;
*value = match value.as_bool() {
Some(value) => {
*value = match value {
Value::Bool(ref value) => {
ensure!(
args.len() == 1,
"Bad arguments. For boolean configurations use: `:toggle key`"
);
serde_json::Value::Bool(!value)
Value::Bool(!value)
}
None => {
Value::String(ref value) => {
ensure!(
args.len() > 2,
"Bad arguments. For non-boolean configurations use: `:toggle key val1 val2 ...`",
"Bad arguments. For string configurations use: `:toggle key val1 val2 ...`",
);
ensure!(
value.is_string(),
"Bad configuration. Cannot cycle non-string configurations"
);
let value = value
.as_str()
.expect("programming error: should have been ensured before");
serde_json::Value::String(
Value::String(
args[1..]
.iter()
.skip_while(|e| *e != value)
@ -1831,11 +1854,29 @@ fn toggle_option(
.to_string(),
)
}
Value::Number(ref value) => {
ensure!(
args.len() > 2,
"Bad arguments. For number configurations use: `:toggle key val1 val2 ...`",
);
Value::Number(
args[1..]
.iter()
.skip_while(|&e| value.to_string() != *e.to_string())
.nth(1)
.unwrap_or_else(|| &args[1])
.parse()?,
)
}
Value::Null | Value::Object(_) | Value::Array(_) => {
anyhow::bail!("Configuration {key} does not support toggle yet")
}
};
let status = format!("'{key}' is now set to {value}");
let config = serde_json::from_value(config)
.map_err(|_| anyhow::anyhow!("Could not parse field: `{:?}`", &args))?;
.map_err(|err| anyhow::anyhow!("Cannot parse `{:?}`, {}", &args, err))?;
cx.editor
.config_events
@ -2244,13 +2285,12 @@ fn clear_register(
format!("Invalid register {}", args[0])
);
let register = args[0].chars().next().unwrap_or_default();
match cx.editor.registers.remove(register) {
Some(_) => cx
.editor
.set_status(format!("Register {} cleared", register)),
None => cx
.editor
.set_error(format!("Register {} not found", register)),
if cx.editor.registers.remove(register) {
cx.editor
.set_status(format!("Register {} cleared", register));
} else {
cx.editor
.set_error(format!("Register {} not found", register));
}
Ok(())
}
@ -2366,9 +2406,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
aliases: &["n"],
doc: "Create a new scratch buffer.",
fun: new_file,
// TODO: This seems to complete with a filename, but doesn't use that filename to
// set the path of the newly created buffer.
signature: CommandSignature::positional(&[completers::filename]),
signature: CommandSignature::none(),
},
TypableCommand {
name: "format",
@ -2429,6 +2467,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: write_all,
signature: CommandSignature::none(),
},
TypableCommand {
name: "write-all!",
aliases: &["wa!"],
doc: "Forcefully write changes from all buffers to disk creating necessary subdirectories.",
fun: force_write_all,
signature: CommandSignature::none(),
},
TypableCommand {
name: "write-quit-all",
aliases: &["wqa", "xa"],
@ -2478,6 +2523,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: theme,
signature: CommandSignature::positional(&[completers::theme]),
},
TypableCommand {
name: "yank-join",
aliases: &[],
doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.",
fun: yank_joined,
signature: CommandSignature::none(),
},
TypableCommand {
name: "clipboard-yank",
aliases: &[],
@ -2585,14 +2637,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
},
TypableCommand {
name: "reload",
aliases: &[],
aliases: &["rl"],
doc: "Discard changes and reload from the source file.",
fun: reload,
signature: CommandSignature::none(),
},
TypableCommand {
name: "reload-all",
aliases: &[],
aliases: &["rla"],
doc: "Discard changes and reload all documents from the source files.",
fun: reload_all,
signature: CommandSignature::none(),
@ -2829,7 +2881,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: clear_register,
signature: CommandSignature::none(),
},
];
];
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
Lazy::new(|| {
@ -2848,25 +2900,15 @@ pub(super) fn command_mode(cx: &mut Context) {
":".into(),
Some(':'),
|editor: &Editor, input: &str| {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
let shellwords = Shellwords::from(input);
let words = shellwords.words();
if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
// If the command has not been finished yet, complete commands.
let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
.iter()
.filter_map(|command| {
FUZZY_MATCHER
.fuzzy_match(command.name, input)
.map(|score| (command.name, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
matches
fuzzy_match(
input,
TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
false,
)
.into_iter()
.map(|(name, _)| (0.., name.into()))
.collect()

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

@ -18,7 +18,7 @@ use std::{
pub use default::default;
use macros::key;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct KeyTrieNode {
/// A label for keys coming under this node, like "Goto mode"
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
/// corresponding keyevent in self, except when both other and self have
/// subnodes for same key. In that case the merge is recursive.
@ -77,49 +73,40 @@ impl KeyTrieNode {
}
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() {
let desc = match trie {
KeyTrie::Leaf(cmd) => {
KeyTrie::MappableCommand(cmd) => {
if cmd.name() == "no_op" {
continue;
}
cmd.doc()
}
KeyTrie::Node(n) => n.name(),
KeyTrie::Node(n) => &n.name,
KeyTrie::Sequence(_) => "[Multiple commands]",
};
match body.iter().position(|(d, _)| d == &desc) {
match body.iter().position(|(_, d)| d == &desc) {
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
.iter()
.position(|&k| k == *keys.iter().next().unwrap())
.unwrap()
});
let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body
let body: Vec<_> = body
.into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.map(|(events, desc)| {
let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
(events.join(", "), desc)
})
.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 {
fn default() -> Self {
Self::new("", HashMap::new(), Vec::new())
Info::new(&self.name, &body)
}
}
@ -145,7 +132,7 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq)]
pub enum KeyTrie {
Leaf(MappableCommand),
MappableCommand(MappableCommand),
Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
@ -174,7 +161,7 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
{
command
.parse::<MappableCommand>()
.map(KeyTrie::Leaf)
.map(KeyTrie::MappableCommand)
.map_err(E::custom)
}
@ -208,17 +195,43 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
}
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> {
match *self {
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> {
match *self {
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 {
KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
}?
}
Some(trie)
@ -256,75 +269,11 @@ pub enum KeymapResult {
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.
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 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
/// sticky node if one is in use.
state: Vec<KeyEvent>,
@ -333,7 +282,7 @@ pub struct 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 {
map,
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()
}
@ -373,11 +322,11 @@ impl Keymaps {
let first = self.state.get(0).unwrap_or(&key);
let trie_node = match self.sticky {
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]) {
Some(KeyTrie::Leaf(ref cmd)) => {
Some(KeyTrie::MappableCommand(ref cmd)) => {
return KeymapResult::Matched(cmd.clone());
}
Some(KeyTrie::Sequence(ref cmds)) => {
@ -396,7 +345,7 @@ impl Keymaps {
}
KeymapResult::Pending(map.clone())
}
Some(KeyTrie::Leaf(cmd)) => {
Some(KeyTrie::MappableCommand(cmd)) => {
self.state.clear();
KeymapResult::Matched(cmd.clone())
}
@ -416,9 +365,13 @@ impl Default for Keymaps {
}
/// 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 {
keys.merge(delta.remove(mode).unwrap_or_default())
keys.merge_nodes(
delta
.remove(mode)
.unwrap_or_else(|| KeyTrie::Node(KeyTrieNode::default())),
)
}
}
@ -447,8 +400,7 @@ mod tests {
#[test]
fn merge_partial_keys() {
let keymap = hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
Mode::Normal => keymap!({ "Normal mode"
"i" => normal_mode,
"无" => insert_mode,
"z" => jump_backward,
@ -457,7 +409,6 @@ mod tests {
"g" => delete_char_forward,
},
})
)
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
@ -484,32 +435,45 @@ mod tests {
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_line_end),
keymap.search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::delete_char_forward),
keymap.search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_last_line),
keymap.search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0);
assert!(
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]
fn order_should_be_set() {
let keymap = hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
Mode::Normal => keymap!({ "Normal mode"
"space" => { ""
"s" => { ""
"v" => vsplit,
@ -517,7 +481,6 @@ mod tests {
},
},
})
)
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
@ -525,22 +488,19 @@ mod tests {
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works
assert_eq!(
keymap
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
&KeyTrie::Leaf(MappableCommand::vsplit),
keymap.search(&[key!(' '), key!('s'), key!('v')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty())
let node = keymap.search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order.as_slice().is_empty())
}
#[test]
fn aliased_modes_are_same_in_default_keymap() {
let keymaps = Keymaps::default().map();
let root = keymaps.get(&Mode::Normal).unwrap().root();
let root = keymaps.get(&Mode::Normal).unwrap();
assert_eq!(
root.search(&[key!(' '), key!('w')]).unwrap(),
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
@ -563,7 +523,7 @@ mod tests {
},
"j" | "k" => move_line_down,
});
let keymap = Keymap::new(normal_mode);
let keymap = normal_mode;
let mut reverse_map = keymap.reverse_map();
// sort keybindings in order to have consistent tests
@ -611,7 +571,7 @@ mod tests {
modifiers: KeyModifiers::NONE,
};
let expectation = Keymap::new(KeyTrie::Node(KeyTrieNode::new(
let expectation = KeyTrie::Node(KeyTrieNode::new(
"",
hashmap! {
key => KeyTrie::Sequence(vec!{
@ -628,7 +588,7 @@ mod tests {
})
},
vec![key],
)));
));
assert_eq!(toml::from_str(keys), Ok(expectation));
}

@ -1,10 +1,10 @@
use std::collections::HashMap;
use super::macros::keymap;
use super::{Keymap, Mode};
use super::{KeyTrie, Mode};
use helix_core::hashmap;
pub fn default() -> HashMap<Mode, Keymap> {
pub fn default() -> HashMap<Mode, KeyTrie> {
let normal = keymap!({ "Normal mode"
"h" | "left" => move_char_left,
"j" | "down" => move_visual_line_down,
@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"A-i" | "A-down" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,
"A-e" => move_parent_node_end,
"A-b" => move_parent_node_start,
"%" => select_all,
"x" => extend_line_below,
@ -267,7 +269,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-v" | "v" => vsplit_new,
},
},
"y" => yank_joined_to_clipboard,
"y" => yank_to_clipboard,
"Y" => yank_main_selection_to_clipboard,
"p" => paste_clipboard_after,
"P" => paste_clipboard_before,
@ -338,6 +340,9 @@ pub fn default() -> HashMap<Mode, Keymap> {
"B" => extend_prev_long_word_start,
"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_prev,
@ -370,7 +375,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-h" | "backspace" | "S-backspace" => delete_char_backward,
"C-d" | "del" => delete_char_forward,
"C-j" | "ret" => insert_newline,
"tab" => insert_tab,
"tab" => smart_tab,
"S-tab" => insert_tab,
"up" => move_visual_line_up,
"down" => move_visual_line_down,
@ -382,8 +388,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"end" => goto_line_end_newline,
});
hashmap!(
Mode::Normal => Keymap::new(normal),
Mode::Select => Keymap::new(select),
Mode::Insert => Keymap::new(insert),
Mode::Normal => normal,
Mode::Select => select,
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_term::keymap;
/// # use helix_term::keymap::Keymap;
/// let normal_mode = keymap!({ "Normal mode"
/// "i" => insert_mode,
/// "g" => { "Goto"
@ -76,12 +75,12 @@ macro_rules! alt {
/// },
/// "j" | "down" => move_line_down,
/// });
/// let keymap = Keymap::new(normal_mode);
/// let keymap = normal_mode;
/// ```
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
$crate::keymap::KeyTrie::MappableCommand($crate::commands::MappableCommand::$cmd)
};
(@trie

@ -4,9 +4,8 @@ use helix_loader::VERSION_AND_GIT_HASH;
use helix_term::application::Application;
use helix_term::args::Args;
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();
base_config = match verbosity {
@ -27,7 +26,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message
))
})
.chain(fern::log_file(logpath)?);
.chain(fern::log_file(helix_loader::log_file())?);
base_config.chain(file_config).apply()?;
@ -41,12 +40,6 @@ fn main() -> Result<()> {
#[tokio::main]
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!(
"\
{} {}
@ -68,7 +61,7 @@ FLAGS:
-g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml
-c, --config <file> Specifies a file to use for configuration
-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: {})
-V, --version Prints version information
--vsplit Splits all given files vertically into different windows
@ -78,11 +71,14 @@ FLAGS:
VERSION_AND_GIT_HASH,
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
helix_loader::default_log_file().display(),
);
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.
if args.display_help {
print!("{}", help);
@ -116,15 +112,7 @@ FLAGS:
return Ok(0);
}
let logpath = args.log_file.as_ref().cloned().unwrap_or(logpath);
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());
setup_logging(args.verbosity).context("failed to initialize logging")?;
let config = match Config::load_default() {
Ok(config) => config,

@ -110,6 +110,7 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Self {
let preview_completion_insert = editor.config().preview_completion_insert;
let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server)
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());
};
@ -230,7 +233,7 @@ impl Completion {
match event {
PromptEvent::Abort => {}
PromptEvent::Update => {
PromptEvent::Update if preview_completion_insert => {
// Update creates "ghost" transactions which are not sent to the
// 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
@ -263,6 +266,7 @@ impl Completion {
);
doc.apply_temporary(&transaction, view.id);
}
PromptEvent::Update => {}
PromptEvent::Validate => {
if let Some(CompleteAction::Selected { savepoint }) =
editor.last_completion.take()
@ -290,6 +294,8 @@ impl Completion {
};
// if more text was entered, remove it
doc.restore(view, &savepoint, true);
// save an undo checkpoint before the completion
doc.append_changes_to_history(view);
let transaction = item_to_transaction(
doc,
view.id,
@ -409,10 +415,18 @@ impl Completion {
_ => 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.
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(
future,

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

@ -10,14 +10,14 @@ use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use helix_core::{
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
Rope,
RopeSlice,
};
use helix_view::{
graphics::{Margin, Rect, Style},
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
.lines()
.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>(
text: String,
text: &str,
language: &str,
theme: Option<&Theme>,
config_loader: Arc<syntax::Loader>,
@ -45,13 +45,13 @@ pub fn highlighted_code_block<'a>(
None => return styled_multiline_text(text, code_style),
};
let rope = Rope::from(text.as_ref());
let ropeslice = RopeSlice::from(text);
let syntax = config_loader
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(),
))
.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 {
Some(s) => s,
@ -59,7 +59,7 @@ pub fn highlighted_code_block<'a>(
};
let highlight_iter = syntax
.highlight_iter(rope.slice(..), None, None)
.highlight_iter(ropeslice, None, None)
.map(|e| e.unwrap());
let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
if let Some(spans) = additional_highlight_spans {
@ -267,7 +267,7 @@ impl Markdown {
CodeBlockKind::Indented => "",
};
let tui_text = highlighted_code_block(
text.to_string(),
&text,
language,
theme,
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::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
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};
pub use tui::widgets::{Cell, Row};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::{graphics::Rect, Editor};
use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
use tui::layout::Constraint;
pub trait Item {
pub trait Item: Sync + Send + 'static {
/// Additional editor state that is used for label calculation.
type Data;
type Data: Sync + Send + 'static;
fn format(&self, data: &Self::Data) -> Row;
@ -51,9 +51,8 @@ pub struct Menu<T: Item> {
cursor: Option<usize>,
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
matches: Vec<(u32, u32)>,
widths: Vec<Constraint>,
@ -75,11 +74,10 @@ impl<T: Item> Menu<T> {
editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let matches = (0..options.len()).map(|i| (i, 0)).collect();
let matches = (0..options.len() as u32).map(|i| (i, 0)).collect();
Self {
options,
editor_data,
matcher: Box::new(Matcher::default().ignore_case()),
matches,
cursor: None,
widths: Vec::new(),
@ -94,20 +92,19 @@ impl<T: Item> Menu<T> {
pub fn score(&mut self, pattern: &str) {
// reuse the matches allocation
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
let pattern = Pattern::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy);
let mut buf = Vec::new();
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
let text = option.filter_text(&self.editor_data);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
// Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority
self.matches.sort_by_key(|(_, score)| -score);
pattern
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
.map(|score| (i as u32, score))
});
self.matches.extend(matches);
self.matches
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
// reset cursor position
self.cursor = None;
@ -201,7 +198,7 @@ impl<T: Item> Menu<T> {
self.cursor.and_then(|cursor| {
self.matches
.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.matches
.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();
}));
// 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 {
// esc or ctrl-c aborts the completion and closes the menu
key!(Esc) | ctrl!('c') => {
@ -317,7 +329,7 @@ impl<T: Item + 'static> Component for Menu<T> {
.iter()
.map(|(index, _score)| {
// (index, self.options.get(*index).unwrap()) // get_unchecked
&self.options[*index] // get_unchecked
&self.options[*index as usize] // get_unchecked
})
.collect();

@ -1,7 +1,6 @@
mod completion;
mod document;
pub(crate) mod editor;
mod fuzzy_match;
mod info;
pub mod lsp;
mod markdown;
@ -21,7 +20,7 @@ pub use completion::{Completion, CompletionItem};
pub use editor::EditorView;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
pub use picker::{DynamicPicker, FileLocation, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
@ -64,7 +63,7 @@ pub fn regex_prompt(
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
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 doc_id = view.doc;
@ -111,7 +110,7 @@ pub fn regex_prompt(
view.jumps.push((doc_id, snapshot.clone()));
}
fun(cx.editor, regex, event);
fun(cx, regex, event);
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
@ -142,23 +141,21 @@ pub fn regex_prompt(
};
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
prompt.recalculate_completion(cx.editor);
// 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 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_global(config.file_picker.git_global)
.git_exclude(config.file_picker.git_exclude)
.sort_by_file_name(|name1, name2| name1.cmp(name2))
.max_depth(config.file_picker.max_depth)
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
@ -192,35 +190,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
.build()
.expect("failed to build 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 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() {
Some(entry.into_path())
} else {
None
if !entry.file_type()?.is_file() {
return 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));
FilePicker::new(
files,
root,
move |cx, path: &PathBuf, action| {
let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
@ -229,22 +208,28 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
};
cx.editor.set_error(err);
}
},
|_editor, path| Some((path.clone().into(), None)),
)
})
.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;
}
}
});
picker
}
pub mod completers {
use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_core::fuzzy::fuzzy_match;
use helix_core::syntax::LanguageServerFeature;
use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme;
use helix_view::{editor::Config, Editor};
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::cmp::Reverse;
pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
@ -253,31 +238,16 @@ pub mod completers {
}
pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
let mut names: Vec<_> = editor
.documents
.values()
.map(|doc| {
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 names = editor.documents.values().map(|doc| {
doc.relative_path()
.map(|p| p.display().to_string().into())
.unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME))
});
let mut matches: Vec<_> = names
fuzzy_match(input, names, true)
.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
.map(|(name, _)| ((0..), name))
.collect()
}
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@ -290,26 +260,10 @@ pub mod completers {
names.sort();
names.dedup();
let mut names: Vec<_> = names
.into_iter()
.map(|name| ((0..), Cow::from(name)))
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
fuzzy_match(input, names, false)
.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
.map(|(name, _)| ((0..), name.into()))
.collect()
}
/// Recursive function to get all keys from this value and add them to vec
@ -336,22 +290,22 @@ pub mod completers {
keys
});
let matcher = Matcher::default();
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
fuzzy_match(input, &*KEYS, false)
.into_iter()
.map(|(name, _)| ((0..), name.into()))
.collect()
}
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());
if is_dir {
@ -363,8 +317,6 @@ pub mod completers {
}
pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
let text: String = "text".into();
let language_ids = editor
@ -373,27 +325,13 @@ pub mod completers {
.map(|config| &config.language_id)
.chain(std::iter::once(&text));
let mut matches: Vec<_> = language_ids
.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
fuzzy_match(input, language_ids, false)
.into_iter()
.map(|(language, _score)| ((0..), language.clone().into()))
.map(|(name, _)| ((0..), name.to_owned().into()))
.collect()
}
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
let Some(options) = doc!(editor)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
@ -401,28 +339,22 @@ pub mod completers {
return vec![];
};
let mut matches: Vec<_> = options
.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
fuzzy_match(input, &options.commands, false)
.into_iter()
.map(|(command, _score)| ((0..), command.clone().into()))
.map(|(name, _)| ((0..), name.to_owned().into()))
.collect()
}
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());
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.
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
F: Fn(&ignore::DirEntry) -> FileMatch,
{
@ -476,7 +413,7 @@ pub mod completers {
match path.parent() {
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
// 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 mut files: Vec<_> = WalkBuilder::new(&dir)
let files = WalkBuilder::new(&dir)
.hidden(false)
.follow_links(false) // We're scanning over depth 1
.git_ignore(git_ignore)
.max_depth(Some(1))
.build()
.filter_map(|file| {
@ -516,43 +454,25 @@ pub mod completers {
path.push("");
}
let path = path.to_str()?.to_owned();
Some((end.clone(), Cow::from(path)))
let path = path.into_os_string().into_string().ok()?;
Some(Cow::from(path))
})
}) // TODO: unwrap or skip
.filter(|(_, path)| !path.is_empty()) // TODO
.collect();
.filter(|path| !path.is_empty());
// if empty, return a list of dirs and files in current dir
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()))..;
matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
(Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
});
files = matches
fuzzy_match(&file_name, files, true)
.into_iter()
.map(|(file, _)| (range.clone(), file))
.collect();
.map(|(name, _)| (range.clone(), name))
.collect()
// TODO: complete to longest common match
} else {
let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
}
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::{alt, ctrl, key, shift, ui};
use helix_core::syntax;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::sync::Arc;
use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Widget};
@ -32,6 +34,7 @@ pub struct Prompt {
callback_fn: CallbackFn,
pub doc_fn: DocFn,
next_char_handler: Option<PromptCharHandler>,
language: Option<(&'static str, Arc<syntax::Loader>)>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
@ -83,6 +86,7 @@ impl Prompt {
callback_fn: Box::new(callback_fn),
doc_fn: Box::new(|_| None),
next_char_handler: None,
language: None,
}
}
@ -94,6 +98,11 @@ impl Prompt {
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 {
&self.line
}
@ -297,8 +306,8 @@ impl Prompt {
direction: CompletionDirection,
) {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
let values = match cx.editor.registers.read(register) {
Some(values) if !values.is_empty() => values,
let mut values = match cx.editor.registers.read(register, cx.editor) {
Some(values) if values.len() > 0 => values.rev(),
_ => return,
};
@ -306,13 +315,16 @@ impl Prompt {
let index = match direction {
CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.history_pos.unwrap_or(values.len()).saturating_sub(1)
}
CompletionDirection::Backward => self
.history_pos
.unwrap_or_else(|| values.len())
.saturating_sub(1),
}
.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);
@ -356,6 +368,7 @@ impl Prompt {
let completion_color = theme.get("ui.menu");
let selected_color = theme.get("ui.menu.selected");
let suggestion_color = theme.get("ui.text.inactive");
let background = theme.get("ui.background");
// completion
let max_len = self
@ -451,33 +464,32 @@ impl Prompt {
}
let line = area.height - 1;
surface.clear_with(area.clip_top(line), background);
// render buffer text
surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
let (input, is_suggestion): (Cow<str>, bool) = if self.line.is_empty() {
// latest value in the register list
match self
let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
if self.line.is_empty() {
// Show the most recently entered value as a suggestion.
if let Some(suggestion) = self
.history_register
.and_then(|reg| cx.editor.registers.last(reg))
.map(|entry| entry.into())
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
{
Some(value) => (value, true),
None => (Cow::from(""), false),
}
} else {
(self.line.as_str().into(), false)
};
surface.set_string(
area.x + self.prompt.len() as u16,
area.y + line,
&input,
if is_suggestion {
suggestion_color
surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
}
} 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 {
prompt_color
},
);
surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color);
}
}
}
@ -558,25 +570,29 @@ impl Component for Prompt {
} else {
let last_item = self
.history_register
.and_then(|reg| cx.editor.registers.last(reg).cloned())
.map(|entry| entry.into())
.unwrap_or_else(|| Cow::from(""));
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
.map(|entry| entry.to_string())
.unwrap_or_else(|| String::from(""));
// handle executing with last command in history if nothing entered
let input: Cow<str> = if self.line.is_empty() {
last_item
let input = if self.line.is_empty() {
&last_item
} else {
if last_item != self.line {
// store in history
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;
}
@ -608,25 +624,16 @@ impl Component for Prompt {
self.completion = cx
.editor
.registers
.inner()
.iter()
.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())
})
.iter_preview()
.map(|(ch, preview)| (0.., format!("{} {}", ch, &preview).into()))
.collect();
self.next_char_handler = Some(Box::new(|prompt, c, context| {
prompt.insert_str(
context
&context
.editor
.registers
.read(c)
.and_then(|r| r.first())
.map_or("", |r| r.as_str()),
.first(c, context.editor)
.unwrap_or_default(),
context.editor,
);
}));

@ -145,6 +145,7 @@ where
helix_view::editor::StatusLineElement::FileModificationIndicator => {
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::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type,
@ -160,6 +161,7 @@ where
helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer,
helix_view::editor::StatusLineElement::VersionControl => render_version_control,
helix_view::editor::StatusLineElement::Register => render_register,
}
}
@ -441,6 +443,19 @@ where
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)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
@ -489,3 +504,12 @@ where
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_pairs;
mod commands;
mod languages;
mod movement;
mod prompt;
mod splits;

@ -2,7 +2,7 @@ use helix_core::{auto_pairs::DEFAULT_PAIRS, hashmap};
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)> {
DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)

@ -2,6 +2,7 @@ use helix_term::application::Application;
use super::*;
mod movement;
mod write;
#[tokio::test(flavor = "multi_thread")]
@ -426,3 +427,56 @@ async fn test_delete_char_forward() -> anyhow::Result<()> {
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
/// appropriate line ending to the end of a 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
// 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"]
[dependencies]
bitflags = "2.3"
bitflags = "2.4"
cassowary = "0.3"
unicode-segmentation = "1.10"
crossterm = { version = "0.26", optional = true }
crossterm = { version = "0.27", optional = true }
termini = "1.0"
serde = { version = "1", "optional" = true, features = ["derive"]}
once_cell = "1.18"

@ -201,7 +201,7 @@ where
for (x, y, cell) in content {
// 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) {
map_error(queue!(self.buffer, MoveTo(x, y)))?;
queue!(self.buffer, MoveTo(x, y))?;
}
last_pos = Some((x, y));
if cell.modifier != modifier {
@ -214,12 +214,12 @@ where
}
if cell.fg != fg {
let color = CColor::from(cell.fg);
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
queue!(self.buffer, SetForegroundColor(color))?;
fg = cell.fg;
}
if cell.bg != bg {
let color = CColor::from(cell.bg);
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
queue!(self.buffer, SetBackgroundColor(color))?;
bg = cell.bg;
}
@ -227,7 +227,7 @@ where
if self.capabilities.has_extended_underlines {
if cell.underline_color != 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;
}
} else {
@ -239,24 +239,24 @@ where
if new_underline_style != 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;
}
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
queue!(self.buffer, Print(&cell.symbol))?;
}
map_error(queue!(
queue!(
self.buffer,
SetUnderlineColor(CColor::Reset),
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
))
)
}
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<()> {
@ -266,7 +266,7 @@ where
CursorKind::Underline => SetCursorStyle::SteadyUnderScore,
CursorKind::Hidden => unreachable!(),
};
map_error(execute!(self.buffer, Show, shape))
execute!(self.buffer, Show, shape)
}
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<()> {
map_error(execute!(self.buffer, MoveTo(x, y)))
execute!(self.buffer, MoveTo(x, y))
}
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> {
@ -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)]
struct ModifierDiff {
pub from: Modifier,
@ -312,48 +308,48 @@ impl ModifierDiff {
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
queue!(w, SetAttribute(CAttribute::NoReverse))?;
}
if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
if self.to.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
queue!(w, SetAttribute(CAttribute::Dim))?;
}
}
if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
queue!(w, SetAttribute(CAttribute::NoItalic))?;
}
if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
}
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) {
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
queue!(w, SetAttribute(CAttribute::NoBlink))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
queue!(w, SetAttribute(CAttribute::Reverse))?;
}
if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
queue!(w, SetAttribute(CAttribute::Bold))?;
}
if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
queue!(w, SetAttribute(CAttribute::Italic))?;
}
if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
queue!(w, SetAttribute(CAttribute::Dim))?;
}
if added.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
}
if added.contains(Modifier::SLOW_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
}
if added.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
}
Ok(())
@ -407,7 +403,7 @@ impl Command for SetUnderlineColor {
}
#[cfg(windows)]
fn execute_winapi(&self) -> crossterm::Result<()> {
fn execute_winapi(&self) -> io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"SetUnderlineColor not supported by winapi.",

@ -65,20 +65,6 @@ where
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>
where
B: Backend,

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

@ -12,12 +12,13 @@ homepage = "https://helix-editor.com"
[dependencies]
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"] }
parking_lot = "0.12"
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"
anyhow = "1"
@ -27,4 +28,4 @@ log = "0.4"
git = ["gix"]
[dev-dependencies]
tempfile = "3.4"
tempfile = "3.8"

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

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

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

@ -14,13 +14,14 @@ default = []
term = ["crossterm"]
[dependencies]
bitflags = "2.3"
bitflags = "2.4"
anyhow = "1"
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-lsp = { version = "0.6", path = "../helix-lsp" }
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" }
# Conversion traits
@ -51,6 +52,7 @@ clipboard-win = { version = "4.5", features = ["std"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
rustix = { version = "0.38", features = ["fs"] }
[dev-dependencies]
helix-tui = { path = "../helix-tui" }

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

Loading…
Cancel
Save