Merge branch 'master' into cursor-shape-new

pull/1154/head
Gokul Soumya 3 years ago
commit d4fb1d0633

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

@ -17,7 +17,7 @@ Please search on the issue tracker before creating one. -->
### Environment
- Platform: <!-- macOS / Windows / Linux -->
- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
- Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master -->
<details><summary>~/.cache/helix/helix.log</summary>

@ -25,19 +25,19 @@ jobs:
override: true
- name: Cache cargo registry
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -64,19 +64,19 @@ jobs:
override: true
- name: Cache cargo registry
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -109,19 +109,19 @@ jobs:
components: rustfmt, clippy
- name: Cache cargo registry
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -137,3 +137,51 @@ jobs:
with:
command: clippy
args: -- -D warnings
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
with:
submodules: true
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Cache cargo registry
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Generate docs
uses: actions-rs/cargo@v1
with:
command: xtask
args: docgen
- name: Check uncommitted documentation changes
run: |
git diff
git diff-files --quiet \
|| (echo "Run 'cargo xtask docgen', commit the changes and push again" \
&& exit 1)

@ -102,7 +102,7 @@ jobs:
fi
cp -r runtime dist
- uses: actions/upload-artifact@v2.2.4
- uses: actions/upload-artifact@v2.3.0
with:
name: bins-${{ matrix.build }}
path: dist

12
.gitmodules vendored

@ -142,3 +142,15 @@
path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true
[submodule "helix-syntax/languages/tree-sitter-wgsl"]
path = helix-syntax/languages/tree-sitter-wgsl
url = https://github.com/szebniok/tree-sitter-wgsl
shallow = true
[submodule "helix-syntax/tree-sitter-llvm"]
path = helix-syntax/languages/tree-sitter-llvm
url = https://github.com/benwilliamgraham/tree-sitter-llvm
shallow = true
[submodule "helix-syntax/languages/tree-sitter-markdown"]
path = helix-syntax/languages/tree-sitter-markdown
url = https://github.com/MDeiml/tree-sitter-markdown
shallow = true

55
Cargo.lock generated

@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.48"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
[[package]]
name = "arc-swap"
@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encoding_rs"
version = "0.8.29"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [
"cfg-if",
]
@ -258,15 +258,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
[[package]]
name = "futures-executor"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
dependencies = [
"futures-core",
"futures-task",
@ -275,17 +275,16 @@ dependencies = [
[[package]]
name = "futures-task"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
[[package]]
name = "futures-util"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
dependencies = [
"autocfg",
"futures-core",
"futures-task",
"pin-project-lite",
@ -370,6 +369,7 @@ name = "helix-core"
version = "0.5.0"
dependencies = [
"arc-swap",
"chrono",
"etcetera",
"helix-syntax",
"log",
@ -535,9 +535,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "0.4.8"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "jsonrpc-core"
@ -877,18 +877,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.130"
version = "1.0.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.130"
version = "1.0.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
dependencies = [
"proc-macro2",
"quote",
@ -897,9 +897,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.71"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
dependencies = [
"itoa",
"ryu",
@ -919,9 +919,9 @@ dependencies = [
[[package]]
name = "signal-hook"
version = "0.3.10"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922"
dependencies = [
"libc",
"signal-hook-registry",
@ -1259,3 +1259,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xtask"
version = "0.5.0"
dependencies = [
"helix-core",
"helix-term",
"toml",
]

@ -6,6 +6,7 @@ members = [
"helix-tui",
"helix-syntax",
"helix-lsp",
"xtask",
]
# Build helix-syntax in release mode to make the code path faster in development.

@ -44,8 +44,8 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`.
Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
via the `HELIX_RUNTIME` environment variable.
config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
This location can be overriden via the `HELIX_RUNTIME` environment variable.
Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir.
@ -65,21 +65,7 @@ brew install helix
# Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
Some suggestions to get started:
- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker.
- Help with packaging on various distributions needed!
- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and
doesn't require much knowledge of the internals.
We provide an [architecture.md](./docs/architecture.md) that should give you
a good overview of the internals.
Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
# Getting help

@ -0,0 +1,38 @@
# Author: NNB <nnbnh@protonmail.com>
"ui.menu" = "black"
"ui.menu.selected" = { modifiers = ["reversed"] }
"ui.linenr" = { fg = "gray", bg = "black" }
"ui.popup" = { modifiers = ["reversed"] }
"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
"ui.selection" = { fg = "black", bg = "blue" }
"ui.selection.primary" = { fg = "white", bg = "blue" }
"comment" = { fg = "gray" }
"ui.statusline" = { fg = "black", bg = "white" }
"ui.statusline.inactive" = { fg = "gray", bg = "white" }
"ui.help" = { modifiers = ["reversed"] }
"ui.cursor" = { modifiers = ["reversed"] }
"variable" = "red"
"constant.numeric" = "yellow"
"constant" = "yellow"
"attributes" = "yellow"
"type" = "yellow"
"ui.cursor.match" = { fg = "yellow", modifiers = ["underlined"] }
"string" = "green"
"variable.other.member" = "green"
"constant.character.escape" = "cyan"
"function" = "blue"
"constructor" = "blue"
"special" = "blue"
"keyword" = "magenta"
"label" = "magenta"
"namespace" = "magenta"
"ui.help" = { fg = "white", bg = "black" }
"diagnostic" = { modifiers = ["underlined"] }
"ui.gutter" = { bg = "black" }
"info" = "blue"
"hint" = "gray"
"debug" = "gray"
"warning" = "yellow"
"error" = "red"

@ -2,10 +2,12 @@
- [Installation](./install.md)
- [Usage](./usage.md)
- [Keymap](./keymap.md)
- [Commands](./commands.md)
- [Language Support](./lang-support.md)
- [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md)
- [Themes](./themes.md)
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)
- [Languages](./languages.md)

@ -0,0 +1,5 @@
# Commands
Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
{{#include ./generated/typable-cmd.md}}

@ -41,6 +41,7 @@ hidden = false
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
### `[editor.cursor-shape]` Section

@ -0,0 +1,42 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| bash | ✓ | | | `bash-language-server` |
| c | ✓ | | | `clangd` |
| c-sharp | ✓ | | | |
| cmake | ✓ | | | `cmake-language-server` |
| cpp | ✓ | | | `clangd` |
| css | ✓ | | | |
| elixir | ✓ | | | `elixir-ls` |
| glsl | ✓ | | ✓ | |
| go | ✓ | ✓ | ✓ | `gopls` |
| html | ✓ | | | |
| java | ✓ | | | |
| javascript | ✓ | | ✓ | |
| json | ✓ | | ✓ | |
| julia | ✓ | | | `julia` |
| latex | ✓ | | | |
| ledger | ✓ | | | |
| llvm | ✓ | | | |
| lua | ✓ | | ✓ | |
| markdown | ✓ | | | |
| mint | | | | `mint` |
| nix | ✓ | | ✓ | `rnix-lsp` |
| ocaml | ✓ | | ✓ | |
| ocaml-interface | ✓ | | | |
| perl | ✓ | ✓ | ✓ | |
| php | ✓ | | ✓ | |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | |
| python | ✓ | ✓ | ✓ | `pylsp` |
| racket | | | | `racket` |
| ruby | ✓ | | | `solargraph` |
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
| svelte | ✓ | | ✓ | `svelteserver` |
| toml | ✓ | | | |
| tsq | ✓ | | | |
| tsx | ✓ | | | `typescript-language-server` |
| typescript | ✓ | | ✓ | `typescript-language-server` |
| vue | ✓ | | | |
| wgsl | ✓ | | | |
| yaml | ✓ | | ✓ | |
| zig | ✓ | | ✓ | `zls` |

@ -0,0 +1,43 @@
| Name | Description |
| --- | --- |
| `:quit`, `:q` | Close the current view. |
| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
| `:open`, `:o` | Open a file from disk into the current view. |
| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
| `:new`, `:n` | Create a new scratch buffer. |
| `:format`, `:fmt` | Format the file using the LSP formatter. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. |
| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
| `: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 views to disk. |
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). |
| `:quit-all`, `:qa` | Close all views. |
| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:theme` | Change the editor theme. |
| `: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. |
| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. |
| `:clipboard-paste-after` | Paste system clipboard after selections. |
| `:clipboard-paste-before` | Paste system clipboard before selections. |
| `:clipboard-paste-replace` | Replace selections with content of system clipboard. |
| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. |
| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. |
| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. |
| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
| `:change-current-directory`, `:cd` | Change the current working directory. |
| `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
| `:reload` | Discard changes and reload from the source file. |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:vsplit`, `:vs` | Open the file in a vertical split. |
| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
| `:tutor` | Open the tutorial. |
| `:goto`, `:g` | Go to line number. |

@ -2,7 +2,7 @@
## Submodules
To add a new langauge, you should first add a tree-sitter submodule. To do this,
To add a new language, you should first add a tree-sitter submodule. To do this,
you can run the command
```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>

@ -25,9 +25,16 @@ shell for working on Helix.
Releases are available in the `community` repository.
Packages are also available on AUR:
- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
### Fedora Linux
You can install the COPR package for Helix via
```
sudo dnf copr enable varlad/helix
sudo dnf install helix
```
## Build from source

@ -34,6 +34,7 @@
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
@ -45,44 +46,48 @@
### Changes
| Key | Description | Command |
| ----- | ----------- | ------- |
| `r` | Replace with a character | `replace` |
| `R` | Replace with yanked text | `replace_with_yanked` |
| `~` | Switch case of the selected text | `switch_case` |
| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
| `i` | Insert before selection | `insert_mode` |
| `a` | Insert after selection (append) | `append_mode` |
| `I` | Insert at the start of the line | `prepend_to_line` |
| `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `Alt-u` | Move backward in history | `earlier` |
| `Alt-U` | Move forward in history | `later` |
| `y` | Yank selection | `yank` |
| `p` | Paste after selection | `paste_after` |
| `P` | Paste before selection | `paste_before` |
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` |
| `=` | Format selection (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_selection` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` |
| `Ctrl-a` | Increment object (number) under cursor | `increment` |
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `r` | Replace with a character | `replace` |
| `R` | Replace with yanked text | `replace_with_yanked` |
| `~` | Switch case of the selected text | `switch_case` |
| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
| `i` | Insert before selection | `insert_mode` |
| `a` | Insert after selection (append) | `append_mode` |
| `I` | Insert at the start of the line | `prepend_to_line` |
| `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `Alt-u` | Move backward in history | `earlier` |
| `Alt-U` | Move forward in history | `later` |
| `y` | Yank selection | `yank` |
| `p` | Paste after selection | `paste_after` |
| `P` | Paste before selection | `paste_before` |
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` |
| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_selection` |
| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` |
| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
| `Ctrl-a` | Increment object (number) under cursor | `increment` |
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
| `q` | Start/stop macro recording to the selected register | `record_macro` |
| `Q` | Play back a recorded macro from the selected register | `play_macro` |
#### Shell
| Key | Description | Command |
| ------ | ----------- | ------- |
| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
| Key | Description | Command |
| ------ | ----------- | ------- |
| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
### Selection manipulation
@ -158,17 +163,19 @@ Jumps to various locations.
| ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` |
| `f` | Go to files in the selection | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
| `t` | Go to the top of the screen | `goto_window_top` |
| `m` | Go to the middle of the screen | `goto_window_middle` |
| `c` | Go to the middle of the screen | `goto_window_center` |
| `b` | Go to the bottom of the screen | `goto_window_bottom` |
| `d` | Go to definition (**LSP**) | `goto_definition` |
| `y` | Go to type definition (**LSP**) | `goto_type_definition` |
| `r` | Go to references (**LSP**) | `goto_reference` |
| `i` | Go to implementation (**LSP**) | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
| `m` | Go to the last modified/alternate file | `goto_last_modified_file` |
| `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` |
@ -200,6 +207,8 @@ This layer is similar to vim keybindings as kakoune does not support window.
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
| `f` | Go to files in the selection in horizontal splits | `goto_file` |
| `F` | Go to files in the selection in vertical splits | `goto_file` |
| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
@ -315,7 +324,7 @@ Keys to use within prompt, Remapping currently not supported.
| `Ctrl-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line |
| `backspace`, `Ctrl-h` | Delete previous char |
| `delete`, `Ctrl-d` | Delete previous char |
| `delete`, `Ctrl-d` | Delete next char |
| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |

@ -0,0 +1,10 @@
# Language Support
For more information like arguments passed to default LSP server,
extensions assosciated with a filetype, custom LSP settings, filetype
specific indent settings, etc see the default
[`languages.toml`][languages.toml] file.
{{#include ./generated/lang-support.md}}
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml

@ -11,6 +11,8 @@ this:
```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal]
C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file)
C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
@ -21,6 +23,7 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert mode
```
> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command.
Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows:
@ -42,10 +45,9 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
| Down | `"down"` |
| Home | `"home"` |
| End | `"end"` |
| Page | `"pageup"` |
| Page | `"pagedown"` |
| Page Up | `"pageup"` |
| Page Down | `"pagedown"` |
| Tab | `"tab"` |
| Back | `"backtab"` |
| Delete | `"del"` |
| Insert | `"ins"` |
| Null | `"null"` |
@ -54,4 +56,4 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
Keys can be disabled by binding them to the `no_op` command.
Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro.
> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`.

@ -145,11 +145,12 @@ We use a similar set of scopes as
- `conditional` - `if`, `else`
- `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export`
- (TODO: return?)
- `return`
- `operator` - `or`, `in`
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
- `operator` - `||`, `+=`, `>`, `or`
- `operator` - `||`, `+=`, `>`
- `function`
- `builtin`
@ -161,6 +162,20 @@ We use a similar set of scopes as
- `namespace`
- `markup`
- `heading`
- `list`
- `unnumbered`
- `numbered`
- `bold`
- `italic`
- `underline`
- `link`
- `quote`
- `raw`
- `inline`
- `block`
#### Interface
These scopes are used for theming the editor interface.

@ -23,8 +23,10 @@ If there is a selected register before invoking a change or delete command, the
| `/` | Last search |
| `:` | Last executed command |
| `"` | Last yanked text |
| `_` | Black hole |
> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
> The black hole register works as a no-op register, meaning no data will be written to / read from it.
## Surround

@ -0,0 +1,37 @@
# Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
Some suggestions to get started:
- You can look at the [good first issue][good-first-issue] label on the issue tracker.
- Help with packaging on various distributions needed!
- To use print debugging to the [Helix log file][log-file], you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and
doesn't require much knowledge of the internals.
We provide an [architecture.md][architecture.md] that should give you
a good overview of the internals.
# Auto generated documentation
Some parts of [the book][docs] are autogenerated from the code itself,
like the list of `:commands` and supported languages. To generate these
files, run
```shell
cargo xtask docgen
```
inside the project. We use [xtask][xtask] as an ad-hoc task runner and
thus do not require any dependencies other than `cargo` (You don't have
to `cargo install` anything either).
[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy
[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file
[architecture.md]: ./architecture.md
[docs]: https://docs.helix-editor.com/
[xtask]: https://github.com/matklad/cargo-xtask

@ -2,11 +2,11 @@
"nodes": {
"devshell": {
"locked": {
"lastModified": 1632436039,
"narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
"lastModified": 1637575296,
"narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=",
"owner": "numtide",
"repo": "devshell",
"rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
"rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618",
"type": "github"
},
"original": {
@ -17,11 +17,11 @@
},
"flake-utils": {
"locked": {
"lastModified": 1623875721,
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
"lastModified": 1637014545,
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
@ -30,22 +30,6 @@
"type": "github"
}
},
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1627913399,
"narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"nixCargoIntegration": {
"inputs": {
"devshell": "devshell",
@ -57,11 +41,11 @@
]
},
"locked": {
"lastModified": 1634796585,
"narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
"lastModified": 1638425401,
"narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
"rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37",
"type": "github"
},
"original": {
@ -72,11 +56,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1634782485,
"narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
"lastModified": 1638376152,
"narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
"rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f",
"type": "github"
},
"original": {
@ -88,22 +72,22 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1628186154,
"narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
"lastModified": 1637453606,
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "06552b72346632b6943c8032e57e702ea12413bf",
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
@ -115,11 +99,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1634869268,
"narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
"lastModified": 1638497756,
"narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c02c2d86354327317546501af001886fbb53d374",
"rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a",
"type": "github"
},
"original": {

@ -9,10 +9,6 @@
inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
};
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
};
outputs = inputs@{ self, nixCargoIntegration, ... }:
@ -63,7 +59,7 @@
'';
};
shell = common: prev: {
packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }

@ -36,5 +36,7 @@ similar = "2.1"
etcetera = "0.3"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
[dev-dependencies]
quickcheck = { version = "1", default-features = false }

@ -2,6 +2,7 @@
//! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction};
use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
('`', '`'),
];
const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// [TODO] build this dynamically in language config. see #992
const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@ -25,40 +28,44 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
// TODO: delete implementation where it erases the whole bracket (|) -> |
// [TODO]
// * delete implementation where it erases the whole bracket (|) -> |
// * do not reduce to cursors; use whole selections, and surround with pair
// * change to multi character pairs to handle cases like placing the cursor in the
// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
debug!("autopairs hook selection: {:#?}", selection);
let cursors = selection.clone().cursors(doc.slice(..));
for &(open, close) in PAIRS {
if open == ch {
if open == close {
return handle_same(doc, selection, open);
return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
}
}
if close == ch {
// && char_at pos == close
return Some(handle_close(doc, selection, open, close));
return Some(handle_close(doc, &cursors, open, close));
}
}
None
}
// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
// for example "&'a mut", or "fn<'a>"
fn next_char(doc: &Rope, pos: usize) -> Option<char> {
if pos >= doc.len_chars() {
fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
if pos == 0 {
return None;
}
Some(doc.char(pos))
doc.get_char(pos - 1)
}
// TODO: selections should be extended if range, moved if point.
// TODO: if not cursor but selection, wrap on both sides of selection (surround)
fn handle_open(
doc: &Rope,
selection: &Selection,
@ -66,98 +73,362 @@ fn handle_open(
close: char,
close_before: &str,
) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len());
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let start_head = start_range.head;
let head = pos + offs + open.len_utf8();
// if selection, retain anchor, if cursor, move over
ranges.push(Range::new(
if range.is_empty() {
head
} else {
range.anchor + offs
},
head,
));
let next = doc.get_char(start_head);
let end_head = start_head + offs + open.len_utf8();
let end_anchor = if start_range.is_empty() {
end_head
} else {
start_range.anchor + offs
};
end_ranges.push(Range::new(end_anchor, end_head));
match next {
Some(ch) if !close_before.contains(ch) => {
offs += 1;
// TODO: else return (use default handler that inserts open)
(pos, pos, Some(Tendril::from_char(open)))
offs += open.len_utf8();
(start_head, start_head, Some(Tendril::from_char(open)))
}
// None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
let mut pair = Tendril::with_capacity(2);
pair.push_char(open);
pair.push_char(close);
offs += 2;
(pos, pos, Some(pair))
let pair = Tendril::from_iter([open, close]);
offs += open.len_utf8() + close.len_utf8();
(start_head, start_head, Some(pair))
}
}
});
transaction.with_selection(Selection::new(ranges, selection.primary_index()))
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
debug!("auto pair transaction: {:#?}", t);
t
}
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len());
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let start_head = start_range.head;
let next = doc.get_char(start_head);
let end_head = start_head + offs + close.len_utf8();
let head = pos + offs + close.len_utf8();
// if selection, retain anchor, if cursor, move over
ranges.push(Range::new(
if range.is_empty() {
head
} else {
range.anchor + offs
},
head,
));
let end_anchor = if start_range.is_empty() {
end_head
} else {
start_range.anchor + offs
};
end_ranges.push(Range::new(end_anchor, end_head));
if next == Some(close) {
// return transaction that moves past close
(pos, pos, None) // no-op
// return transaction that moves past close
(start_head, start_head, None) // no-op
} else {
offs += close.len_utf8();
(start_head, start_head, Some(Tendril::from_char(close)))
}
});
transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
}
// TODO: else return (use default handler that inserts close)
(pos, pos, Some(Tendril::from_char(close)))
/// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same(
doc: &Rope,
selection: &Selection,
token: char,
close_before: &str,
open_before: &str,
) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let start_head = start_range.head;
let end_head = start_head + offs + token.len_utf8();
// if selection, retain anchor, if cursor, move over
let end_anchor = if start_range.is_empty() {
end_head
} else {
start_range.anchor + offs
};
end_ranges.push(Range::new(end_anchor, end_head));
let next = doc.get_char(start_head);
let prev = prev_char(doc, start_head);
if next == Some(token) {
// return transaction that moves past close
(start_head, start_head, None) // no-op
} else {
let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
pair.push_char(token);
// for equal pairs, don't insert both open and close if either
// side has a non-pair char
if (next.is_none() || close_before.contains(next.unwrap()))
&& (prev.is_none() || open_before.contains(prev.unwrap()))
{
pair.push_char(token);
}
offs += pair.len();
(start_head, start_head, Some(pair))
}
});
transaction.with_selection(Selection::new(ranges, selection.primary_index()))
transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
}
// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
// if not cursor but selection, wrap
// let next = next char
// if next == bracket {
// // if start of syntax node, insert token twice (new pair because node is complete)
// // elseif colsedBracketAt
// // is_triple == allow triple && next 3 is equal
// // cursor jump over
// }
//} else if allow_triple && followed by triple {
//}
//} else if next != word char && prev != bracket && prev != word char {
// // condition checks for cases like I' where you don't want I'' (or I'm)
// insert pair ("")
//}
None
#[cfg(test)]
mod test {
use super::*;
use smallvec::smallvec;
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
PAIRS.iter().filter(|(open, close)| open != close)
}
fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
PAIRS.iter().filter(|(open, close)| open == close)
}
fn test_hooks(
in_doc: &Rope,
in_sel: &Selection,
ch: char,
expected_doc: &Rope,
expected_sel: &Selection,
) {
let trans = hook(&in_doc, &in_sel, ch).unwrap();
let mut actual_doc = in_doc.clone();
assert!(trans.apply(&mut actual_doc));
assert_eq!(expected_doc, &actual_doc);
assert_eq!(expected_sel, trans.selection().unwrap());
}
fn test_hooks_with_pairs<I, F, R>(
in_doc: &Rope,
in_sel: &Selection,
pairs: I,
get_expected_doc: F,
actual_sel: &Selection,
) where
I: IntoIterator<Item = &'static (char, char)>,
F: Fn(char, char) -> R,
R: Into<Rope>,
Rope: From<R>,
{
pairs.into_iter().for_each(|(open, close)| {
test_hooks(
in_doc,
in_sel,
*open,
&Rope::from(get_expected_doc(*open, *close)),
actual_sel,
)
});
}
// [] indicates range
/// [] -> insert ( -> ([])
#[test]
fn test_insert_blank() {
test_hooks_with_pairs(
&Rope::new(),
&Selection::single(1, 0),
PAIRS,
|open, close| format!("{}{}", open, close),
&Selection::single(1, 1),
);
}
/// [] ([])
/// [] -> insert -> ([])
/// [] ([])
#[test]
fn test_insert_blank_multi_cursor() {
test_hooks_with_pairs(
&Rope::from("\n\n\n"),
&Selection::new(
smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
0,
),
PAIRS,
|open, close| {
format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
)
},
&Selection::new(
smallvec!(Range::point(1), Range::point(4), Range::point(7),),
0,
),
);
}
// [TODO] broken until it works with selections
/// fo[o] -> append ( -> fo[o(])
#[ignore]
#[test]
fn test_append() {
test_hooks_with_pairs(
&Rope::from("foo"),
&Selection::single(2, 4),
PAIRS,
|open, close| format!("foo{}{}", open, close),
&Selection::single(2, 5),
);
}
/// ([]) -> insert ) -> ()[]
#[test]
fn test_insert_close_inside_pair() {
for (open, close) in PAIRS {
let doc = Rope::from(format!("{}{}", open, close));
test_hooks(
&doc,
&Selection::single(2, 1),
*close,
&doc,
&Selection::point(2),
);
}
}
/// ([]) ()[]
/// ([]) -> insert ) -> ()[]
/// ([]) ()[]
#[test]
fn test_insert_close_inside_pair_multi_cursor() {
let sel = Selection::new(
smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
0,
);
let expected_sel = Selection::new(
// smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
smallvec!(Range::point(2), Range::point(5), Range::point(8),),
0,
);
for (open, close) in PAIRS {
let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
));
test_hooks(&doc, &sel, *close, &doc, &expected_sel);
}
}
/// ([]) -> insert ( -> (([]))
#[test]
fn test_insert_open_inside_pair() {
let sel = Selection::single(2, 1);
let expected_sel = Selection::point(2);
for (open, close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", open, close));
let expected_doc = Rope::from(format!(
"{open}{open}{close}{close}",
open = open,
close = close
));
test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
}
}
/// ([]) -> insert " -> ("[]")
#[test]
fn test_insert_nested_open_inside_pair() {
let sel = Selection::single(2, 1);
let expected_sel = Selection::point(2);
for (outer_open, outer_close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
for (inner_open, inner_close) in matching_pairs() {
let expected_doc = Rope::from(format!(
"{}{}{}{}",
outer_open, inner_open, inner_close, outer_close
));
test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
}
}
}
/// []word -> insert ( -> ([]word
#[test]
fn test_insert_open_before_non_pair() {
test_hooks_with_pairs(
&Rope::from("word"),
&Selection::single(1, 0),
PAIRS,
|open, _| format!("{}word", open),
&Selection::point(1),
)
}
// [TODO] broken until it works with selections
/// [wor]d -> insert ( -> ([wor]d
#[test]
#[ignore]
fn test_insert_open_with_selection() {
test_hooks_with_pairs(
&Rope::from("word"),
&Selection::single(0, 4),
PAIRS,
|open, _| format!("{}word", open),
&Selection::single(1, 5),
)
}
/// we want pairs that are *not* the same char to be inserted after
/// a non-pair char, for cases like functions, but for pairs that are
/// the same char, we want to *not* insert a pair to handle cases like "I'm"
///
/// word[] -> insert ( -> word([])
/// word[] -> insert ' -> word'[]
#[test]
fn test_insert_open_after_non_pair() {
let doc = Rope::from("word");
let sel = Selection::single(5, 4);
let expected_sel = Selection::point(5);
test_hooks_with_pairs(
&doc,
&sel,
differing_pairs(),
|open, close| format!("word{}{}", open, close),
&expected_sel,
);
test_hooks_with_pairs(
&doc,
&sel,
matching_pairs(),
|open, _| format!("word{}", open),
&expected_sel,
);
}
}

@ -1,7 +1,7 @@
//! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Severity {
Error,
Warning,
@ -17,7 +17,7 @@ pub struct Range {
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub range: Range,
pub line: usize,

@ -0,0 +1,490 @@
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use once_cell::sync::Lazy;
use regex::Regex;
use ropey::RopeSlice;
use std::borrow::Cow;
use std::cmp;
use super::Increment;
use crate::{Range, Tendril};
#[derive(Debug, PartialEq, Eq)]
pub struct DateTimeIncrementor {
date_time: NaiveDateTime,
range: Range,
fmt: &'static str,
field: DateField,
}
impl DateTimeIncrementor {
pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> {
let range = if range.is_empty() {
if range.anchor < text.len_chars() {
// Treat empty range as a cursor range.
range.put_cursor(text, range.anchor + 1, true)
} else {
// The range is empty and at the end of the text.
return None;
}
} else {
range
};
FORMATS.iter().find_map(|format| {
let from = range.from().saturating_sub(format.max_len);
let to = (range.from() + format.max_len).min(text.len_chars());
let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
let text: Cow<str> = text.slice(from..to).into();
let captures = format.regex.captures(&text)?;
if captures.len() - 1 != format.fields.len() {
return None;
}
let date_time = captures.get(0)?;
let offset = range.from() - from_in_text;
let range = Range::new(date_time.start() + offset, date_time.end() + offset);
let field = captures
.iter()
.skip(1)
.enumerate()
.find_map(|(i, capture)| {
let capture = capture?;
let capture_range = capture.range();
if capture_range.contains(&from_in_text)
&& capture_range.contains(&(to_in_text - 1))
{
Some(format.fields[i])
} else {
None
}
})?;
let has_date = format.fields.iter().any(|f| f.unit.is_date());
let has_time = format.fields.iter().any(|f| f.unit.is_time());
let date_time = &text[date_time.start()..date_time.end()];
let date_time = match (has_date, has_time) {
(true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?,
(true, false) => {
let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
date.and_hms(0, 0, 0)
}
(false, true) => {
let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
NaiveDate::from_ymd(0, 1, 1).and_time(time)
}
(false, false) => return None,
};
Some(DateTimeIncrementor {
date_time,
range,
fmt: format.fmt,
field,
})
})
}
}
impl Increment for DateTimeIncrementor {
fn increment(&self, amount: i64) -> (Range, Tendril) {
let date_time = match self.field.unit {
DateUnit::Years => add_years(self.date_time, amount),
DateUnit::Months => add_months(self.date_time, amount),
DateUnit::Days => add_duration(self.date_time, Duration::days(amount)),
DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)),
DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)),
DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)),
DateUnit::AmPm => toggle_am_pm(self.date_time),
}
.unwrap_or(self.date_time);
(self.range, date_time.format(self.fmt).to_string().into())
}
}
static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| {
vec![
Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23
Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23
Format::new("%Y-%m-%d %H:%M"), // 2021-11-24 07:12
Format::new("%Y/%m/%d %H:%M"), // 2021/11/24 07:12
Format::new("%Y-%m-%d"), // 2021-11-24
Format::new("%Y/%m/%d"), // 2021/11/24
Format::new("%a %b %d %Y"), // Wed Nov 24 2021
Format::new("%d-%b-%Y"), // 24-Nov-2021
Format::new("%Y %b %d"), // 2021 Nov 24
Format::new("%b %d, %Y"), // Nov 24, 2021
Format::new("%-I:%M:%S %P"), // 7:21:53 am
Format::new("%-I:%M %P"), // 7:21 am
Format::new("%-I:%M:%S %p"), // 7:21:53 AM
Format::new("%-I:%M %p"), // 7:21 AM
Format::new("%H:%M:%S"), // 23:24:23
Format::new("%H:%M"), // 23:24
]
});
#[derive(Debug)]
struct Format {
fmt: &'static str,
fields: Vec<DateField>,
regex: Regex,
max_len: usize,
}
impl Format {
fn new(fmt: &'static str) -> Self {
let mut remaining = fmt;
let mut fields = Vec::new();
let mut regex = String::new();
let mut max_len = 0;
while let Some(i) = remaining.find('%') {
let after = &remaining[i + 1..];
let mut chars = after.chars();
let c = chars.next().unwrap();
let spec_len = if c == '-' {
1 + chars.next().unwrap().len_utf8()
} else {
c.len_utf8()
};
let specifier = &after[..spec_len];
let field = DateField::from_specifier(specifier).unwrap();
fields.push(field);
max_len += field.max_len + remaining[..i].len();
regex += &remaining[..i];
regex += &format!("({})", field.regex);
remaining = &after[spec_len..];
}
let regex = Regex::new(&regex).unwrap();
Self {
fmt,
fields,
regex,
max_len,
}
}
}
impl PartialEq for Format {
fn eq(&self, other: &Self) -> bool {
self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len
}
}
impl Eq for Format {}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct DateField {
regex: &'static str,
unit: DateUnit,
max_len: usize,
}
impl DateField {
fn from_specifier(specifier: &str) -> Option<Self> {
match specifier {
"Y" => Some(DateField {
regex: r"\d{4}",
unit: DateUnit::Years,
max_len: 5,
}),
"y" => Some(DateField {
regex: r"\d\d",
unit: DateUnit::Years,
max_len: 2,
}),
"m" => Some(DateField {
regex: r"[0-1]\d",
unit: DateUnit::Months,
max_len: 2,
}),
"d" => Some(DateField {
regex: r"[0-3]\d",
unit: DateUnit::Days,
max_len: 2,
}),
"-d" => Some(DateField {
regex: r"[1-3]?\d",
unit: DateUnit::Days,
max_len: 2,
}),
"a" => Some(DateField {
regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
unit: DateUnit::Days,
max_len: 3,
}),
"A" => Some(DateField {
regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
unit: DateUnit::Days,
max_len: 9,
}),
"b" | "h" => Some(DateField {
regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
unit: DateUnit::Months,
max_len: 3,
}),
"B" => Some(DateField {
regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
unit: DateUnit::Months,
max_len: 9,
}),
"H" => Some(DateField {
regex: r"[0-2]\d",
unit: DateUnit::Hours,
max_len: 2,
}),
"M" => Some(DateField {
regex: r"[0-5]\d",
unit: DateUnit::Minutes,
max_len: 2,
}),
"S" => Some(DateField {
regex: r"[0-5]\d",
unit: DateUnit::Seconds,
max_len: 2,
}),
"I" => Some(DateField {
regex: r"[0-1]\d",
unit: DateUnit::Hours,
max_len: 2,
}),
"-I" => Some(DateField {
regex: r"1?\d",
unit: DateUnit::Hours,
max_len: 2,
}),
"P" => Some(DateField {
regex: r"am|pm",
unit: DateUnit::AmPm,
max_len: 2,
}),
"p" => Some(DateField {
regex: r"AM|PM",
unit: DateUnit::AmPm,
max_len: 2,
}),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum DateUnit {
Years,
Months,
Days,
Hours,
Minutes,
Seconds,
AmPm,
}
impl DateUnit {
fn is_date(self) -> bool {
matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days)
}
fn is_time(self) -> bool {
matches!(
self,
DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds
)
}
}
fn ndays_in_month(year: i32, month: u32) -> u32 {
// The first day of the next month...
let (y, m) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
let d = NaiveDate::from_ymd(y, m, 1);
// ...is preceded by the last day of the original month.
d.pred().day()
}
fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
let month = (date_time.month0() as i64).checked_add(amount)?;
let year = date_time.year() + i32::try_from(month / 12).ok()?;
let year = if month.is_negative() { year - 1 } else { year };
// Normalize month
let month = month % 12;
let month = if month.is_negative() {
month + 12
} else {
month
} as u32
+ 1;
let day = cmp::min(date_time.day(), ndays_in_month(year, month));
Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
}
fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?;
let ndays = ndays_in_month(year, date_time.month());
if date_time.day() > ndays {
let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
Some(d.succ().and_time(date_time.time()))
} else {
date_time.with_year(year)
}
}
fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> {
date_time.checked_add_signed(duration)
}
fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> {
if date_time.hour() < 12 {
add_duration(date_time, Duration::hours(12))
} else {
add_duration(date_time, Duration::hours(-12))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Rope;
#[test]
fn test_increment_date_times() {
let tests = [
// (original, cursor, amount, expected)
("2020-02-28", 0, 1, "2021-02-28"),
("2020-02-29", 0, 1, "2021-03-01"),
("2020-01-31", 5, 1, "2020-02-29"),
("2020-01-20", 5, 1, "2020-02-20"),
("2021-01-01", 5, -1, "2020-12-01"),
("2021-01-31", 5, -2, "2020-11-30"),
("2020-02-28", 8, 1, "2020-02-29"),
("2021-02-28", 8, 1, "2021-03-01"),
("2021-02-28", 0, -1, "2020-02-28"),
("2021-03-01", 0, -1, "2020-03-01"),
("2020-02-29", 5, -1, "2020-01-29"),
("2020-02-20", 5, -1, "2020-01-20"),
("2020-02-29", 8, -1, "2020-02-28"),
("2021-03-01", 8, -1, "2021-02-28"),
("1980/12/21", 8, 100, "1981/03/31"),
("1980/12/21", 8, -100, "1980/09/12"),
("1980/12/21", 8, 1000, "1983/09/17"),
("1980/12/21", 8, -1000, "1978/03/27"),
("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"),
("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"),
("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"),
("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"),
("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"),
("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"),
("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"),
("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"),
("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"),
("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"),
("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"),
("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"),
("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"),
("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"),
("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"),
("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"),
("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"),
("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"),
("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"),
("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"),
("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"),
("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"),
("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"),
("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"),
("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"),
("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"),
("24-Nov-2021", 0, 1, "25-Nov-2021"),
("24-Nov-2021", 3, 1, "24-Dec-2021"),
("24-Nov-2021", 7, 1, "24-Nov-2022"),
("2021 Nov 24", 0, 1, "2022 Nov 24"),
("2021 Nov 24", 5, 1, "2021 Dec 24"),
("2021 Nov 24", 9, 1, "2021 Nov 25"),
("Nov 24, 2021", 0, 1, "Dec 24, 2021"),
("Nov 24, 2021", 4, 1, "Nov 25, 2021"),
("Nov 24, 2021", 8, 1, "Nov 24, 2022"),
("7:21:53 am", 0, 1, "8:21:53 am"),
("7:21:53 am", 3, 1, "7:22:53 am"),
("7:21:53 am", 5, 1, "7:21:54 am"),
("7:21:53 am", 8, 1, "7:21:53 pm"),
("7:21:53 AM", 0, 1, "8:21:53 AM"),
("7:21:53 AM", 3, 1, "7:22:53 AM"),
("7:21:53 AM", 5, 1, "7:21:54 AM"),
("7:21:53 AM", 8, 1, "7:21:53 PM"),
("7:21 am", 0, 1, "8:21 am"),
("7:21 am", 3, 1, "7:22 am"),
("7:21 am", 5, 1, "7:21 pm"),
("7:21 AM", 0, 1, "8:21 AM"),
("7:21 AM", 3, 1, "7:22 AM"),
("7:21 AM", 5, 1, "7:21 PM"),
("23:24:23", 1, 1, "00:24:23"),
("23:24:23", 3, 1, "23:25:23"),
("23:24:23", 6, 1, "23:24:24"),
("23:24", 1, 1, "00:24"),
("23:24", 3, 1, "23:25"),
];
for (original, cursor, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::new(cursor, cursor + 1);
assert_eq!(
DateTimeIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_invalid_date_times() {
let tests = [
"0000-00-00",
"1980-2-21",
"1980-12-1",
"12345",
"2020-02-30",
"1999-12-32",
"19-12-32",
"1-2-3",
"0000/00/00",
"1980/2/21",
"1980/12/1",
"12345",
"2020/02/30",
"1999/12/32",
"19/12/32",
"1/2/3",
"123:456:789",
"11:61",
"2021-55-12 08:12:54",
];
for invalid in tests {
let rope = Rope::from_str(invalid);
let range = Range::new(0, 1);
assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None)
}
}
}

@ -0,0 +1,8 @@
pub mod date_time;
pub mod number;
use crate::{Range, Tendril};
pub trait Increment {
fn increment(&self, amount: i64) -> (Range, Tendril);
}

@ -2,6 +2,8 @@ use std::borrow::Cow;
use ropey::RopeSlice;
use super::Increment;
use crate::{
textobject::{textobject_word, TextObject},
Range, Tendril,
@ -9,9 +11,9 @@ use crate::{
#[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> {
pub range: Range,
pub value: i64,
pub radix: u32,
value: i64,
radix: u32,
range: Range,
text: RopeSlice<'a>,
}
@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> {
text,
})
}
}
/// Add `amount` to the number and return the formatted text.
pub fn incremented_text(&self, amount: i64) -> Tendril {
impl<'a> Increment for NumberIncrementor<'a> {
fn increment(&self, amount: i64) -> (Range, Tendril) {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount);
@ -144,7 +147,7 @@ impl<'a> NumberIncrementor<'a> {
}
}
new_text.into()
(self.range, new_text.into())
}
}
@ -366,7 +369,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
.increment(amount)
.1,
expected.into()
);
}
@ -392,7 +396,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
.increment(amount)
.1,
expected.into()
);
}
@ -419,7 +424,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
.increment(amount)
.1,
expected.into()
);
}
@ -464,7 +470,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
.increment(amount)
.1,
expected.into()
);
}
@ -491,7 +498,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
.increment(amount)
.1,
expected.into()
);
}

@ -5,18 +5,19 @@ pub mod diagnostic;
pub mod diff;
pub mod graphemes;
pub mod history;
pub mod increment;
pub mod indent;
pub mod line_ending;
pub mod macros;
pub mod match_brackets;
pub mod movement;
pub mod numbers;
pub mod object;
pub mod path;
mod position;
pub mod register;
pub mod search;
pub mod selection;
pub mod shellwords;
mod state;
pub mod surround;
pub mod syntax;
@ -158,7 +159,7 @@ mod merge_toml_tests {
";
let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
.expect("Couldn't parse built-in langauges config");
.expect("Couldn't parse built-in languages config");
let user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user);

@ -15,7 +15,11 @@ impl Register {
}
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
Self { name, values }
if name == '_' {
Self::new(name)
} else {
Self { name, values }
}
}
pub const fn name(&self) -> char {
@ -27,11 +31,15 @@ impl Register {
}
pub fn write(&mut self, values: Vec<String>) {
self.values = values;
if self.name != '_' {
self.values = values;
}
}
pub fn push(&mut self, value: String) {
self.values.push(value);
if self.name != '_' {
self.values.push(value);
}
}
}

@ -308,10 +308,10 @@ impl Range {
}
impl From<(usize, usize)> for Range {
fn from(tuple: (usize, usize)) -> Self {
fn from((anchor, head): (usize, usize)) -> Self {
Self {
anchor: tuple.0,
head: tuple.1,
anchor,
head,
horiz: None,
}
}

@ -0,0 +1,164 @@
use std::borrow::Cow;
/// Get the vec of escaped / quoted / doublequoted filenames from the input str
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
enum State {
Normal,
NormalEscaped,
Quoted,
QuoteEscaped,
Dquoted,
DquoteEscaped,
}
use State::*;
let mut state = Normal;
let mut args: Vec<Cow<str>> = Vec::new();
let mut escaped = String::with_capacity(input.len());
let mut start = 0;
let mut end = 0;
for (i, c) in input.char_indices() {
state = match state {
Normal => match c {
'\\' => {
escaped.push_str(&input[start..i]);
start = i + 1;
NormalEscaped
}
'"' => {
end = i;
Dquoted
}
'\'' => {
end = i;
Quoted
}
c if c.is_ascii_whitespace() => {
end = i;
Normal
}
_ => Normal,
},
NormalEscaped => Normal,
Quoted => match c {
'\\' => {
escaped.push_str(&input[start..i]);
start = i + 1;
QuoteEscaped
}
'\'' => {
end = i;
Normal
}
_ => Quoted,
},
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' => {
escaped.push_str(&input[start..i]);
start = i + 1;
DquoteEscaped
}
'"' => {
end = i;
Normal
}
_ => Dquoted,
},
DquoteEscaped => Dquoted,
};
if i >= input.len() - 1 && end == 0 {
end = i + 1;
}
if end > 0 {
let esc_trim = escaped.trim();
let inp = &input[start..end];
if !(esc_trim.is_empty() && inp.trim().is_empty()) {
if esc_trim.is_empty() {
args.push(inp.into());
} else {
args.push([escaped, inp.into()].concat().into());
escaped = "".to_string();
}
}
start = i + 1;
end = 0;
}
}
args
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
let result = shellwords(input);
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó"),
Cow::from("wörds"),
Cow::from(r#"three "with escaping\"#),
];
// TODO test is_owned and is_borrowed, once they get stabilized.
assert_eq!(expected, result);
}
#[test]
fn test_quoted() {
let quoted =
r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
let result = shellwords(quoted);
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó wörds"),
Cow::from(r#"three' "with escaping\"#),
Cow::from("quote incomplete"),
];
assert_eq!(expected, result);
}
#[test]
fn test_dquoted() {
let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
let result = shellwords(dquoted);
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó wörds"),
Cow::from(r#"three' "with escaping\"#),
Cow::from("dquote incomplete"),
];
assert_eq!(expected, result);
}
#[test]
fn test_mixed() {
let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
let result = shellwords(dquoted);
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó wörds"),
Cow::from("three' \"with escaping\\"),
Cow::from("no space before"),
Cow::from("and after"),
Cow::from("$#%^@"),
Cow::from("%^&(%^"),
Cow::from(")(*&^%"),
Cow::from(r#"a\\b"#),
//last ' just changes to quoted but since we dont have anything after it, it should be ignored
];
assert_eq!(expected, result);
}
}

@ -1,4 +1,4 @@
use crate::{search, Selection};
use crate::{search, Range, Selection};
use ropey::RopeSlice;
pub const PAIRS: &[(char, char)] = &[
@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
pub fn find_nth_pairs_pos(
text: RopeSlice,
ch: char,
pos: usize,
range: Range,
n: usize,
) -> Option<(usize, usize)> {
let (open, close) = get_pair(ch);
if text.len_chars() < 2 || pos >= text.len_chars() {
if text.len_chars() < 2 || range.to() >= text.len_chars() {
return None;
}
let (open, close) = get_pair(ch);
let pos = range.cursor(text);
if open == close {
if Some(open) == text.get_char(pos) {
// Special case: cursor is directly on a matching char.
match pos {
0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
_ if (pos + 1) == text.len_chars() => {
Some((search::find_nth_prev(text, open, pos, n)?, pos))
}
// We return no match because there's no way to know which
// side of the char we should be searching on.
_ => None,
}
} else {
Some((
search::find_nth_prev(text, open, pos, n)?,
search::find_nth_next(text, close, pos, n)?,
))
// Cursor is directly on match char. We return no match
// because there's no way to know which side of the char
// we should be searching on.
return None;
}
Some((
search::find_nth_prev(text, open, pos, n)?,
search::find_nth_next(text, close, pos, n)?,
))
} else {
Some((
find_nth_open_pair(text, open, close, pos, n)?,
@ -160,8 +154,8 @@ pub fn get_surround_pos(
) -> Option<Vec<usize>> {
let mut change_pos = Vec::new();
for range in selection {
let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
for &range in selection {
let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return None;
}
@ -178,67 +172,91 @@ mod test {
use ropey::Rope;
use smallvec::SmallVec;
#[test]
fn test_find_nth_pairs_pos() {
let doc = Rope::from("some (text) here");
fn check_find_nth_pair_pos(
text: &str,
cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
) {
let doc = Rope::from(text);
let slice = doc.slice(..);
// cursor on [t]ext
assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
// cursor on so[m]e
assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
// cursor on bracket itself
assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
for (cursor_pos, ch, n, expected_range) in cases {
let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n);
assert_eq!(
range, expected_range,
"Expected {:?}, got {:?}",
expected_range, range
);
}
}
#[test]
fn test_find_nth_pairs_pos_skip() {
let doc = Rope::from("(so (many (good) text) here)");
let slice = doc.slice(..);
fn test_find_nth_pairs_pos() {
check_find_nth_pair_pos(
"some (text) here",
vec![
// cursor on [t]ext
(6, '(', 1, Some((5, 10))),
(6, ')', 1, Some((5, 10))),
// cursor on so[m]e
(2, '(', 1, None),
// cursor on bracket itself
(5, '(', 1, Some((5, 10))),
(10, '(', 1, Some((5, 10))),
],
);
}
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
#[test]
fn test_find_nth_pairs_pos_skip() {
check_find_nth_pair_pos(
"(so (many (good) text) here)",
vec![
// cursor on go[o]d
(13, '(', 1, Some((10, 15))),
(13, '(', 2, Some((4, 21))),
(13, '(', 3, Some((0, 27))),
],
);
}
#[test]
fn test_find_nth_pairs_pos_same() {
let doc = Rope::from("'so 'many 'good' text' here'");
let slice = doc.slice(..);
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
// cursor on the quotes
assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
// this is the best we can do since opening and closing pairs are same
assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
check_find_nth_pair_pos(
"'so 'many 'good' text' here'",
vec![
// cursor on go[o]d
(13, '\'', 1, Some((10, 15))),
(13, '\'', 2, Some((4, 21))),
(13, '\'', 3, Some((0, 27))),
// cursor on the quotes
(10, '\'', 1, None),
],
)
}
#[test]
fn test_find_nth_pairs_pos_step() {
let doc = Rope::from("((so)((many) good (text))(here))");
let slice = doc.slice(..);
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
check_find_nth_pair_pos(
"((so)((many) good (text))(here))",
vec![
// cursor on go[o]d
(15, '(', 1, Some((5, 24))),
(15, '(', 2, Some((0, 31))),
],
)
}
#[test]
fn test_find_nth_pairs_pos_mixed() {
let doc = Rope::from("(so [many {good} text] here)");
let slice = doc.slice(..);
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
check_find_nth_pair_pos(
"(so [many {good} text] here)",
vec![
// cursor on go[o]d
(13, '{', 1, Some((10, 15))),
(13, '[', 1, Some((4, 21))),
(13, '(', 1, Some((0, 27))),
],
)
}
#[test]

@ -50,7 +50,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
pub language_id: String,
pub language_id: String, // c-sharp, rust
pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
#[serde(default)]
@ -310,8 +310,9 @@ impl Loader {
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0));
static SHEBANG_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
});
let configuration_id = SHEBANG_REGEX
.captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));

@ -114,7 +114,7 @@ pub fn textobject_surround(
ch: char,
count: usize,
) -> Range {
surround::find_nth_pairs_pos(slice, ch, range.head, count)
surround::find_nth_pairs_pos(slice, ch, range, count)
.map(|(anchor, head)| match textobject {
TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
@ -170,7 +170,7 @@ mod test {
#[test]
fn test_textobject_word() {
// (text, [(cursor position, textobject, final range), ...])
// (text, [(char position, textobject, final range), ...])
let tests = &[
(
"cursor at beginning of doc",
@ -269,7 +269,9 @@ mod test {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range) = case;
let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
// cursor is a single width selection
let range = Range::new(pos, pos + 1);
let result = textobject_word(slice, range, objtype, 1, false);
assert_eq!(
result,
expected_range.into(),
@ -283,7 +285,7 @@ mod test {
#[test]
fn test_textobject_surround() {
// (text, [(cursor position, textobject, final range, count), ...])
// (text, [(cursor position, textobject, final range, surround char, count), ...])
let tests = &[
(
"simple (single) surround pairs",

@ -22,7 +22,7 @@ pub enum Assoc {
}
// ChangeSpec = Change | ChangeSet | Vec<Change>
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ChangeSet {
pub(crate) changes: Vec<Operation>,
/// The required document length. Will refuse to apply changes unless it matches.
@ -30,16 +30,6 @@ pub struct ChangeSet {
len_after: usize,
}
impl Default for ChangeSet {
fn default() -> Self {
Self {
changes: Vec::new(),
len: 0,
len_after: 0,
}
}
}
impl ChangeSet {
pub fn with_capacity(capacity: usize) -> Self {
Self {
@ -330,7 +320,7 @@ impl ChangeSet {
/// `true` when the set is empty.
#[inline]
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
}
/// Map a position through the changes.
@ -419,7 +409,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,

@ -337,7 +337,10 @@ impl Registry {
})
.await;
value.expect("failed to initialize capabilities");
if let Err(e) = value {
log::error!("failed to initialize language server: {}", e);
return;
}
// next up, notify<initialized>
_client

@ -0,0 +1 @@
Subproject commit ad8c32917a16dfbb387d1da567bf0c3fb6fffde2

@ -0,0 +1 @@
Subproject commit f00ff52251edbd58f4d39c9c3204383253032c11

@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
default-run = "hx"
[package.metadata.nix]
build = true

@ -76,17 +76,27 @@ impl Application {
None => Ok(def_lang_conf),
};
let theme = if let Some(theme) = &config.theme {
match theme_loader.load(theme) {
Ok(theme) => theme,
Err(e) => {
log::warn!("failed to load theme `{}` - {}", theme, e);
let true_color = config.editor.true_color || crate::true_color();
let theme = config
.theme
.as_ref()
.and_then(|theme| {
theme_loader
.load(theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
e
})
.ok()
.filter(|theme| (true_color || theme.is_16_color()))
})
.unwrap_or_else(|| {
if true_color {
theme_loader.default()
} else {
theme_loader.base16_default()
}
}
} else {
theme_loader.default()
};
});
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.and_then(|conf| conf.try_into())
@ -265,7 +275,7 @@ impl Application {
use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode;
if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
return;
}
let editor_view = self

File diff suppressed because it is too large Load Diff

@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
// --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer.
@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent {
/// May be used by the parent component to compute the child area.
/// viewport is the maximum allowed area, and the child should stay within those bounds.
///
/// The returned size might be larger than the viewport if the child is too big to fit.
/// In this case the parent can use the values to calculate scroll.
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
// TODO: for scrolling, the scroll wrapper should place a size + offset on the Context
// that way render can use it
None
}
fn type_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
fn id(&self) -> Option<&'static str> {
None
}
}
use anyhow::Error;
@ -126,12 +131,17 @@ impl Compositor {
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
// If it is a key event and a macro is being recorded, push the key event to the recording.
if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
keys.push(key.into());
}
// propagate events through the layers until we either find a layer that consumes it or we
// run out of layers (event bubbling)
for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => {
callback(self);
callback(self, cx);
return true;
}
EventResult::Consumed(None) => return true,
@ -184,6 +194,14 @@ impl Compositor {
.find(|component| component.type_name() == type_name)
.and_then(|component| component.as_any_mut().downcast_mut())
}
pub fn find_id<T: 'static>(&mut self, id: &'static str) -> Option<&mut T> {
let type_name = std::any::type_name::<T>();
self.layers
.iter_mut()
.find(|component| component.type_name() == type_name && component.id() == Some(id))
.and_then(|component| component.as_any_mut().downcast_mut())
}
}
// View casting, taken straight from Cursive

@ -1,4 +1,4 @@
pub use crate::commands::Command;
pub use crate::commands::MappableCommand;
use crate::config::Config;
use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent};
@ -92,7 +92,7 @@ macro_rules! alt {
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
};
(@trie
@ -120,7 +120,7 @@ macro_rules! keymap {
_key,
keymap!(@trie $value)
);
debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum KeyTrie {
Leaf(Command),
Sequence(Vec<Command>),
Leaf(MappableCommand),
Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
@ -304,9 +304,9 @@ impl KeyTrie {
pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
Matched(Command),
Matched(MappableCommand),
/// Matched a sequence of commands to execute.
MatchedSequence(Vec<Command>),
MatchedSequence(Vec<MappableCommand>),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
@ -386,10 +386,10 @@ impl Keymap {
};
let trie = match trie_node.search(&[*first]) {
Some(&KeyTrie::Leaf(cmd)) => {
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
Some(KeyTrie::Leaf(ref cmd)) => {
return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
}
Some(&KeyTrie::Sequence(ref cmds)) => {
Some(KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
@ -408,9 +408,9 @@ impl Keymap {
}
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
}
Some(&KeyTrie::Leaf(cmd)) => {
Some(&KeyTrie::Leaf(ref cmd)) => {
self.state.clear();
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
}
Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear();
@ -512,6 +512,7 @@ impl Default for Keymaps {
"g" => { "Goto"
"g" => goto_file_start,
"e" => goto_last_line,
"f" => goto_file,
"h" => goto_line_start,
"l" => goto_line_end,
"s" => goto_first_nonwhitespace,
@ -520,9 +521,10 @@ impl Default for Keymaps {
"r" => goto_reference,
"i" => goto_implementation,
"t" => goto_window_top,
"m" => goto_window_middle,
"c" => goto_window_center,
"b" => goto_window_bottom,
"a" => goto_last_accessed_file,
"m" => goto_last_modified_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
"." => goto_last_modification,
@ -537,9 +539,9 @@ impl Default for Keymaps {
"O" => open_above,
"d" => delete_selection,
// TODO: also delete without yanking
"A-d" => delete_selection_noyank,
"c" => change_selection,
// TODO: also change delete without yanking
"A-c" => change_selection_noyank,
"C" => copy_selection_on_next_line,
"A-C" => copy_selection_on_prev_line,
@ -591,6 +593,9 @@ impl Default for Keymaps {
// paste_all
"P" => paste_before,
"q" => record_macro,
"Q" => play_macro,
">" => indent,
"<" => unindent,
"=" => format_selections,
@ -622,6 +627,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"f" => goto_file_hsplit,
"F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
@ -637,7 +644,7 @@ impl Default for Keymaps {
"tab" => jump_forward, // tab == <C-i>
"C-o" => jump_backward,
// "C-s" => save_selection,
"C-s" => save_selection,
"space" => { "Space"
"f" => file_picker,
@ -650,6 +657,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"f" => goto_file_hsplit,
"F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
@ -827,36 +836,36 @@ mod tests {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
keymap.get(key!('i')).kind,
KeymapResultKind::Matched(Command::normal_mode),
KeymapResultKind::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(key!('无')).kind,
KeymapResultKind::Matched(Command::insert_mode),
KeymapResultKind::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(key!('z')).kind,
KeymapResultKind::Matched(Command::jump_backward),
KeymapResultKind::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(Command::goto_line_end),
&KeyTrie::Leaf(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(Command::delete_char_forward),
&KeyTrie::Leaf(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(Command::goto_last_line),
&KeyTrie::Leaf(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
@ -890,7 +899,7 @@ mod tests {
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
&KeyTrie::Leaf(Command::vsplit),
&KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge

@ -9,3 +9,14 @@ pub mod config;
pub mod job;
pub mod keymap;
pub mod ui;
#[cfg(not(windows))]
fn true_color() -> bool {
std::env::var("COLORTERM")
.map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
.unwrap_or(false)
}
#[cfg(windows)]
fn true_color() -> bool {
true
}

@ -168,7 +168,7 @@ impl Completion {
}
};
});
let popup = Popup::new(menu);
let popup = Popup::new("completion", menu);
let mut completion = Self {
popup,
start_offset,
@ -328,8 +328,8 @@ impl Component for Completion {
let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
width = rel_width;
height = rel_height;
width = rel_width.min(width);
height = rel_height.min(height);
}
Rect::new(x, y, width, height)
} else {

@ -17,7 +17,6 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
editor::LineNumber,
graphics::{CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent,
@ -32,7 +31,7 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView {
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::Command, Vec<KeyEvent>),
last_insert: (commands::MappableCommand, Vec<KeyEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
autoinfo: Option<Info>,
@ -49,7 +48,7 @@ impl EditorView {
Self {
keymaps,
on_next_key: None,
last_insert: (commands::Command::normal_mode, Vec::new()),
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
autoinfo: None,
@ -310,17 +309,16 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
let style = spans.iter().fold(text_style, |acc, span| {
let style = theme.get(theme.scopes()[span.0].as_str());
acc.patch(style)
});
for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < offset.col as u16
|| visual_x >= viewport.width + offset.col as u16;
if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds {
let style = spans.iter().fold(text_style, |acc, span| {
acc.patch(theme.highlight(span.0))
});
// we still want to render an empty cell with the style
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@ -351,6 +349,10 @@ impl EditorView {
};
if !out_of_bounds {
let style = spans.iter().fold(text_style, |acc, span| {
acc.patch(theme.highlight(span.0))
});
// if we're offscreen just keep going until we hit a new line
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@ -417,22 +419,6 @@ impl EditorView {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
let linenr = theme.get("ui.linenr");
let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
let warning = theme.get("warning");
let error = theme.get("error");
let info = theme.get("info");
let hint = theme.get("hint");
// Whether to draw the line number for the last line of the
// document or not. We only draw it if it's not an empty line.
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
let current_line = doc
.text()
.char_to_line(doc.selection(view.id).primary().cursor(text));
// it's used inside an iterator so the collect isn't needless:
// https://github.com/rust-lang/rust-clippy/issues/6164
#[allow(clippy::needless_collect)]
@ -442,51 +428,31 @@ impl EditorView {
.map(|range| range.cursor_line(text))
.collect();
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
surface.set_stringn(
viewport.x,
viewport.y + i as u16,
"●",
1,
match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
}
let mut offset = 0;
let selected = cursors.contains(&line);
let gutter_style = theme.get("ui.gutter");
let text = if line == last_line && !draw_last {
" ~".into()
} else {
let line = match config.line_number {
LineNumber::Absolute => line + 1,
LineNumber::Relative => {
if current_line == line {
line + 1
} else {
abs_diff(current_line, line)
}
}
};
format!("{:>5}", line)
};
surface.set_stringn(
viewport.x + 1,
viewport.y + i as u16,
text,
5,
if selected && is_focused {
linenr_select
} else {
linenr
},
);
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8);
for (constructor, width) in view.gutters() {
let gutter = constructor(doc, view, theme, config, is_focused, *width);
text.reserve(*width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
let selected = cursors.contains(&line);
if let Some(style) = gutter(line, selected, &mut text) {
surface.set_stringn(
viewport.x + offset,
viewport.y + i as u16,
&text,
*width,
gutter_style.patch(style),
);
}
text.clear();
}
offset += *width as u16;
}
}
@ -916,7 +882,7 @@ impl EditorView {
return EventResult::Ignored;
}
commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
EventResult::Consumed(None)
}
@ -934,7 +900,8 @@ impl EditorView {
}
if modifiers == crossterm::event::KeyModifiers::ALT {
commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
commands::MappableCommand::replace_selections_with_primary_clipboard
.execute(cxt);
return EventResult::Consumed(None);
}
@ -948,7 +915,7 @@ impl EditorView {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id;
commands::Command::paste_primary_clipboard_before.execute(cxt);
commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
return EventResult::Consumed(None);
}
@ -963,7 +930,7 @@ impl EditorView {
impl Component for EditorView {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let mut cxt = commands::Context {
editor: &mut cx.editor,
editor: cx.editor,
count: None,
register: None,
callback: None,
@ -1140,13 +1107,31 @@ impl Component for EditorView {
disp.push_str(&s);
}
}
let style = cx.editor.theme.get("ui.text");
let macro_width = if cx.editor.macro_recording.is_some() {
3
} else {
0
};
surface.set_string(
area.x + area.width.saturating_sub(key_width),
area.x + area.width.saturating_sub(key_width + macro_width),
area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..)
.unwrap_or(&disp),
cx.editor.theme.get("ui.text"),
style,
);
if let Some((reg, _)) = cx.editor.macro_recording {
let disp = format!("[{}]", reg);
let style = style
.fg(helix_view::graphics::Color::Yellow)
.add_modifier(Modifier::BOLD);
surface.set_string(
area.x + area.width.saturating_sub(3),
area.y + area.height.saturating_sub(1),
&disp,
style,
);
}
}
if let Some(completion) = self.completion.as_mut() {
@ -1172,12 +1157,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT)
}
}
#[inline]
const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {
a - b
} else {
b - a
}
}

@ -228,6 +228,7 @@ impl Component for Markdown {
return None;
}
let contents = parse(&self.contents, None, &self.config_loader);
// TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
let mut height = padding;
@ -240,11 +241,6 @@ impl Component for Markdown {
} else if content_width > text_width {
text_width = content_width;
}
if height >= viewport.1 {
height = viewport.1;
break;
}
}
Some((text_width + padding, height))

@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored,
};
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@ -202,7 +202,7 @@ impl<T: Item + 'static> Component for Menu<T> {
return close_fn;
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None);

@ -186,6 +186,7 @@ pub mod completers {
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
names.push("base16_default".into());
let mut names: Vec<_> = names
.into_iter()

@ -46,7 +46,7 @@ pub struct FilePicker<T> {
}
pub enum CachedPreview {
Document(Document),
Document(Box<Document>),
Binary,
LargeFile,
NotFound,
@ -140,7 +140,7 @@ impl<T> FilePicker<T> {
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, Some(&editor.theme), None)
.map(CachedPreview::Document)
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
@ -404,13 +404,13 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored,
};
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.last_picker = compositor.pop();
})));
match key_event.into() {
shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
}
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
@ -421,19 +421,19 @@ impl<T: 'static> Component for Picker<T> {
}
key!(Enter) => {
if let Some(option) = self.selection() {
(self.callback_fn)(&mut cx.editor, option, Action::Replace);
(self.callback_fn)(cx.editor, option, Action::Replace);
}
return close_fn;
}
ctrl!('s') => {
if let Some(option) = self.selection() {
(self.callback_fn)(&mut cx.editor, option, Action::HorizontalSplit);
(self.callback_fn)(cx.editor, option, Action::HorizontalSplit);
}
return close_fn;
}
ctrl!('v') => {
if let Some(option) = self.selection() {
(self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit);
(self.callback_fn)(cx.editor, option, Action::VerticalSplit);
}
return close_fn;
}

@ -15,16 +15,20 @@ pub struct Popup<T: Component> {
contents: T,
position: Option<Position>,
size: (u16, u16),
child_size: (u16, u16),
scroll: usize,
id: &'static str,
}
impl<T: Component> Popup<T> {
pub fn new(contents: T) -> Self {
pub fn new(id: &'static str, contents: T) -> Self {
Self {
contents,
position: None,
size: (0, 0),
child_size: (0, 0),
scroll: 0,
id,
}
}
@ -68,6 +72,9 @@ impl<T: Component> Popup<T> {
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
let max_offset = self.child_size.1.saturating_sub(self.size.1);
self.scroll = (self.scroll + offset).min(max_offset as usize);
} else {
self.scroll = self.scroll.saturating_sub(offset);
}
@ -93,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored,
};
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@ -115,13 +122,21 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
}
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let max_width = 120.min(viewport.0);
let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
let (width, height) = self
.contents
.required_size((120, 26)) // max width, max height
.required_size((max_width, max_height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
self.size = (width, height);
self.child_size = (width, height);
self.size = (width.min(max_width), height.min(max_height));
// re-clamp scroll offset
let max_offset = self.child_size.1.saturating_sub(self.size.1);
self.scroll = self.scroll.min(max_offset as usize);
Some(self.size)
}
@ -143,4 +158,8 @@ impl<T: Component> Component for Popup<T> {
self.contents.render(area, surface, cx);
}
fn id(&self) -> Option<&'static str> {
Some(self.id)
}
}

@ -426,7 +426,7 @@ impl Component for Prompt {
_ => return EventResult::Ignored,
};
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@ -505,7 +505,7 @@ impl Component for Prompt {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
shift!(BackTab) => {
shift!(Tab) => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}

@ -102,7 +102,7 @@ impl Default for Cell {
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@ -111,15 +111,6 @@ pub struct Buffer {
pub content: Vec<Cell>,
}
impl Default for Buffer {
fn default() -> Buffer {
Buffer {
area: Default::default(),
content: Vec::new(),
}
}
}
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {

@ -195,15 +195,9 @@ impl<'a> From<&'a str> for Span<'a> {
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Default for Spans<'a> {
fn default() -> Spans<'a> {
Spans(Vec::new())
}
}
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
@ -280,17 +274,11 @@ impl<'a> From<Spans<'a>> for String {
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
}
impl<'a> Default for Text<'a> {
fn default() -> Text<'a> {
Text { lines: Vec::new() }
}
}
impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style.
///

@ -363,21 +363,12 @@ impl<'a> Table<'a> {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct TableState {
pub offset: usize,
pub selected: Option<usize>,
}
impl Default for TableState {
fn default() -> TableState {
TableState {
offset: 0,
selected: None,
}
}
}
impl TableState {
pub fn selected(&self) -> Option<usize> {
self.selected

@ -104,6 +104,7 @@ pub struct Document {
last_saved_revision: usize,
version: i32, // should be usize?
pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
@ -127,6 +128,7 @@ impl fmt::Debug for Document {
// .field("history", &self.history)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
.field("modified_since_accessed", &self.modified_since_accessed)
.field("diagnostics", &self.diagnostics)
// .field("language_server", &self.language_server)
.finish()
@ -344,6 +346,7 @@ impl Document {
history: Cell::new(History::default()),
savepoint: None,
last_saved_revision: 0,
modified_since_accessed: false,
language_server: None,
}
}
@ -639,6 +642,9 @@ impl Document {
selection.clone().ensure_invariants(self.text.slice(..)),
);
}
// set modified since accessed
self.modified_since_accessed = true;
}
if !transaction.changes().is_empty() {

@ -2,6 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::{CursorKind, Rect},
input::KeyEvent,
theme::{self, Theme},
tree::{self, Tree},
Document, DocumentId, View, ViewId,
@ -11,6 +12,7 @@ use futures_util::future;
use std::{
collections::{BTreeMap, HashMap},
io::stdin,
num::NonZeroUsize,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@ -18,7 +20,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
use anyhow::Error;
use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@ -105,6 +107,8 @@ pub struct Config {
pub file_picker: FilePickerConfig,
/// Shape for cursor in each mode
pub cursor_shape: CursorShapeConfig,
/// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
pub true_color: bool,
}
// Cursor shape is read and used on every rendered frame and so needs
@ -141,7 +145,7 @@ impl Default for CursorShapeConfig {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
@ -171,6 +175,7 @@ impl Default for Config {
auto_info: true,
file_picker: FilePickerConfig::default(),
cursor_shape: CursorShapeConfig::default(),
true_color: false,
}
}
}
@ -190,11 +195,12 @@ impl std::fmt::Debug for Motion {
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
pub next_document_id: usize,
pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>,
@ -223,8 +229,8 @@ pub enum Action {
impl Editor {
pub fn new(
mut area: Rect,
themes: Arc<theme::Loader>,
config_loader: Arc<syntax::Loader>,
theme_loader: Arc<theme::Loader>,
syn_loader: Arc<syntax::Loader>,
config: Config,
) -> Self {
let language_servers = helix_lsp::Registry::new();
@ -234,14 +240,15 @@ impl Editor {
Self {
tree: Tree::new(area),
next_document_id: 0,
next_document_id: DocumentId::default(),
documents: BTreeMap::new(),
count: None,
selected_register: None,
theme: themes.default(),
macro_recording: None,
theme: theme_loader.default(),
language_servers,
syn_loader: config_loader,
theme_loader: themes,
syn_loader,
theme_loader,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
@ -297,14 +304,51 @@ impl Editor {
self._refresh();
}
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
use anyhow::Context;
let theme = self
.theme_loader
.load(theme.as_ref())
.with_context(|| format!("failed setting theme `{}`", theme))?;
self.set_theme(theme);
Ok(())
/// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?;
doc.detect_language(Some(&self.theme), &self.syn_loader);
Self::launch_language_server(&mut self.language_servers, doc)
}
/// Launch a language server for a given document
fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
// try to find a language server based on the language name
let language_server = doc.language.as_ref().and_then(|language| {
ls.get(language)
.map_err(|e| {
log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}",
language.scope(),
e
)
})
.ok()
});
if let Some(language_server) = language_server {
// only spawn a new lang server if the servers aren't the same
if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
let language_id = doc
.language()
.and_then(|s| s.split('.').last()) // source.rust
.map(ToOwned::to_owned)
.unwrap_or_default();
// TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
doc.version(),
doc.text(),
language_id,
));
doc.set_language_server(Some(language_server));
}
}
Some(())
}
fn _refresh(&mut self) {
@ -358,7 +402,8 @@ impl Editor {
.tree
.traverse()
.any(|(_, v)| v.doc == doc.id && v.id != view.id);
let view = view_mut!(self);
let (view, doc) = current!(self);
if remove_empty_scratch {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// borrow, invalidating direct access to `doc.id`.
@ -367,7 +412,16 @@ impl Editor {
} else {
let jump = (view.doc, doc.selection(view.id).clone());
view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc);
// Set last accessed doc if it is a different document
if doc.id != id {
view.last_accessed_doc = Some(view.doc);
// Set last modified doc if modified and last modified doc is different
if std::mem::take(&mut doc.modified_since_accessed)
&& view.last_modified_docs[0] != Some(id)
{
view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
}
}
}
let view_id = view.id;
@ -377,23 +431,22 @@ impl Editor {
}
Action::Load => {
let view_id = view!(self).id;
if let Some(doc) = self.document_mut(id) {
if doc.selections().is_empty() {
doc.selections.insert(view_id, Selection::point(0));
}
let doc = self.documents.get_mut(&id).unwrap();
if doc.selections().is_empty() {
doc.selections.insert(view_id, Selection::point(0));
}
return;
}
Action::HorizontalSplit => {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal);
// initialize selection for view
let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
}
Action::VerticalSplit => {
Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Vertical);
let view_id = self.tree.split(
view,
match action {
Action::HorizontalSplit => Layout::Horizontal,
Action::VerticalSplit => Layout::Vertical,
_ => unreachable!(),
},
);
// initialize selection for view
let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
@ -403,16 +456,19 @@ impl Editor {
self._refresh();
}
fn new_document(&mut self, mut document: Document) -> DocumentId {
let id = DocumentId(self.next_document_id);
self.next_document_id += 1;
document.id = id;
self.documents.insert(id, document);
/// Generate an id for a new document and register it.
fn new_document(&mut self, mut doc: Document) -> DocumentId {
let id = self.next_document_id;
// Safety: adding 1 from 1 is fine, probably impossible to reach usize max
self.next_document_id =
DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
doc.id = id;
self.documents.insert(id, doc);
id
}
fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
let id = self.new_document(document);
fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
let id = self.new_document(doc);
self.switch(id, action);
id
}
@ -428,54 +484,16 @@ impl Editor {
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?;
let id = self
.documents()
.find(|doc| doc.path() == Some(&path))
.map(|doc| doc.id);
let id = self.document_by_path(&path).map(|doc| doc.id);
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
// try to find a language server based on the language name
let language_server = doc.language.as_ref().and_then(|language| {
self.language_servers
.get(language)
.map_err(|e| {
log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}",
language.scope(),
e
)
})
.ok()
});
if let Some(language_server) = language_server {
let language_id = doc
.language()
.and_then(|s| s.split('.').last()) // source.rust
.map(ToOwned::to_owned)
.unwrap_or_default();
// TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
doc.version(),
doc.text(),
language_id,
));
doc.set_language_server(Some(language_server));
}
let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
let id = DocumentId(self.next_document_id);
self.next_document_id += 1;
doc.id = id;
self.documents.insert(id, doc);
id
self.new_document(doc)
};
self.switch(id, action);
@ -498,11 +516,11 @@ impl Editor {
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
let doc = match self.documents.get(&doc_id) {
Some(doc) => doc,
None => anyhow::bail!("document does not exist"),
None => bail!("document does not exist"),
};
if !force && doc.is_modified() {
anyhow::bail!(
bail!(
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
@ -535,7 +553,7 @@ impl Editor {
// If the document we removed was visible in all views, we will have no more views. We don't
// want to close the editor just for a simple buffer close, so we need to create a new view
// containing either an existing document, or a brand new document.
if self.tree.views().peekable().peek().is_none() {
if self.tree.views().next().is_none() {
let doc_id = self
.documents
.iter()
@ -620,8 +638,7 @@ impl Editor {
}
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let view = view!(self);
let doc = &self.documents[&view.doc];
let (view, doc) = current_ref!(self);
let cursor = doc
.selection(view.id)
.primary()

@ -33,7 +33,7 @@ pub struct Margin {
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
@ -41,17 +41,6 @@ pub struct Rect {
pub height: u16,
}
impl Default for Rect {
fn default() -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.

@ -0,0 +1,96 @@
use std::fmt::Write;
use crate::{editor::Config, graphics::Style, Document, Theme, View};
pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
pub type Gutter =
for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
pub fn diagnostic<'doc>(
doc: &'doc Document,
_view: &View,
theme: &Theme,
_config: &Config,
_is_focused: bool,
_width: usize,
) -> GutterFn<'doc> {
let warning = theme.get("warning");
let error = theme.get("error");
let info = theme.get("info");
let hint = theme.get("hint");
let diagnostics = doc.diagnostics();
Box::new(move |line: usize, _selected: bool, out: &mut String| {
use helix_core::diagnostic::Severity;
if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
let diagnostic = &diagnostics[index];
write!(out, "●").unwrap();
return Some(match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
});
}
None
})
}
pub fn line_number<'doc>(
doc: &'doc Document,
view: &View,
theme: &Theme,
config: &Config,
is_focused: bool,
width: usize,
) -> GutterFn<'doc> {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
// Whether to draw the line number for the last line of the
// document or not. We only draw it if it's not an empty line.
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
let linenr = theme.get("ui.linenr");
let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
let current_line = doc
.text()
.char_to_line(doc.selection(view.id).primary().cursor(text));
let config = config.line_number;
Box::new(move |line: usize, selected: bool, out: &mut String| {
if line == last_line && !draw_last {
write!(out, "{:>1$}", '~', width).unwrap();
Some(linenr)
} else {
use crate::editor::LineNumber;
let line = match config {
LineNumber::Absolute => line + 1,
LineNumber::Relative => {
if current_line == line {
line + 1
} else {
abs_diff(current_line, line)
}
}
};
let style = if selected && is_focused {
linenr_select
} else {
linenr
};
write!(out, "{:>1$}", line, width).unwrap();
Some(style)
}
})
}
#[inline(always)]
const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {
a - b
} else {
b - a
}
}

@ -36,7 +36,6 @@ pub(crate) mod keys {
pub(crate) const PAGEUP: &str = "pageup";
pub(crate) const PAGEDOWN: &str = "pagedown";
pub(crate) const TAB: &str = "tab";
pub(crate) const BACKTAB: &str = "backtab";
pub(crate) const DELETE: &str = "del";
pub(crate) const INSERT: &str = "ins";
pub(crate) const NULL: &str = "null";
@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str(keys::TAB)?,
KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str(keys::NULL)?,
@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::PageUp => keys::PAGEUP.len(),
KeyCode::PageDown => keys::PAGEDOWN.len(),
KeyCode::Tab => keys::TAB.len(),
KeyCode::BackTab => keys::BACKTAB.len(),
KeyCode::Delete => keys::DELETE.len(),
KeyCode::Insert => keys::INSERT.len(),
KeyCode::Null => keys::NULL.len(),
@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
keys::PAGEUP => KeyCode::PageUp,
keys::PAGEDOWN => KeyCode::PageDown,
keys::TAB => KeyCode::Tab,
keys::BACKTAB => KeyCode::BackTab,
keys::DELETE => KeyCode::Delete,
keys::INSERT => KeyCode::Insert,
keys::NULL => KeyCode::Null,
@ -220,12 +216,40 @@ impl<'de> Deserialize<'de> for KeyEvent {
#[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent {
fn from(
crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
) -> KeyEvent {
KeyEvent {
code: code.into(),
modifiers: modifiers.into(),
fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
if code == crossterm::event::KeyCode::BackTab {
// special case for BackTab -> Shift-Tab
let mut modifiers: KeyModifiers = modifiers.into();
modifiers.insert(KeyModifiers::SHIFT);
Self {
code: KeyCode::Tab,
modifiers,
}
} else {
Self {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
}
#[cfg(feature = "term")]
impl From<KeyEvent> for crossterm::event::KeyEvent {
fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
// special case for Shift-Tab -> BackTab
let mut modifiers = modifiers;
modifiers.remove(KeyModifiers::SHIFT);
crossterm::event::KeyEvent {
code: crossterm::event::KeyCode::BackTab,
modifiers: modifiers.into(),
}
} else {
crossterm::event::KeyEvent {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
}

@ -79,8 +79,6 @@ pub enum KeyCode {
PageDown,
/// Tab key.
Tab,
/// Shift + Tab key.
BackTab,
/// Delete key.
Delete,
/// Insert key.
@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::PageUp => CKeyCode::PageUp,
KeyCode::PageDown => CKeyCode::PageDown,
KeyCode::Tab => CKeyCode::Tab,
KeyCode::BackTab => CKeyCode::BackTab,
KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert,
KeyCode::F(f_number) => CKeyCode::F(f_number),
@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode {
CKeyCode::PageUp => KeyCode::PageUp,
CKeyCode::PageDown => KeyCode::PageDown,
CKeyCode::Tab => KeyCode::Tab,
CKeyCode::BackTab => KeyCode::BackTab,
CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
CKeyCode::Delete => KeyCode::Delete,
CKeyCode::Insert => KeyCode::Insert,
CKeyCode::F(f_number) => KeyCode::F(f_number),

@ -5,6 +5,7 @@ pub mod clipboard;
pub mod document;
pub mod editor;
pub mod graphics;
pub mod gutter;
pub mod info;
pub mod input;
pub mod keyboard;
@ -12,8 +13,18 @@ pub mod theme;
pub mod tree;
pub mod view;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
pub struct DocumentId(usize);
use std::num::NonZeroUsize;
// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct DocumentId(NonZeroUsize);
impl Default for DocumentId {
fn default() -> DocumentId {
// Safety: 1 is non-zero
DocumentId(unsafe { NonZeroUsize::new_unchecked(1) })
}
}
slotmap::new_key_type! {
pub struct ViewId;

@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
});
pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
toml::from_slice(include_bytes!("../../base16_theme.toml"))
.expect("Failed to parse base 16 default theme")
});
#[derive(Clone, Debug)]
pub struct Loader {
@ -35,6 +39,9 @@ impl Loader {
if name == "default" {
return Ok(self.default());
}
if name == "base16_default" {
return Ok(self.base16_default());
}
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
@ -74,12 +81,20 @@ impl Loader {
pub fn default(&self) -> Theme {
DEFAULT_THEME.clone()
}
/// Returns the alternative 16-color default theme
pub fn base16_default(&self) -> Theme {
BASE16_DEFAULT_THEME.clone()
}
}
#[derive(Clone, Debug)]
pub struct Theme {
scopes: Vec<String>,
// UI styles are stored in a HashMap
styles: HashMap<String, Style>,
// tree-sitter highlight styles are stored in a Vec to optimize lookups
scopes: Vec<String>,
highlights: Vec<Style>,
}
impl<'de> Deserialize<'de> for Theme {
@ -88,6 +103,8 @@ impl<'de> Deserialize<'de> for Theme {
D: Deserializer<'de>,
{
let mut styles = HashMap::new();
let mut scopes = Vec::new();
let mut highlights = Vec::new();
if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
// TODO: alert user of parsing failures in editor
@ -102,24 +119,38 @@ impl<'de> Deserialize<'de> for Theme {
.unwrap_or_default();
styles.reserve(colors.len());
scopes.reserve(colors.len());
highlights.reserve(colors.len());
for (name, style_value) in colors {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
}
styles.insert(name, style);
// these are used both as UI and as highlights
styles.insert(name.clone(), style);
scopes.push(name);
highlights.push(style);
}
}
let scopes = styles.keys().map(ToString::to_string).collect();
Ok(Self { scopes, styles })
Ok(Self {
scopes,
styles,
highlights,
})
}
}
impl Theme {
#[inline]
pub fn highlight(&self, index: usize) -> Style {
self.highlights[index]
}
pub fn get(&self, scope: &str) -> Style {
self.try_get(scope)
.unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
self.try_get(scope).unwrap_or_default()
}
pub fn try_get(&self, scope: &str) -> Option<Style> {
@ -134,6 +165,14 @@ impl Theme {
pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
self.scopes().iter().position(|s| s == scope)
}
pub fn is_16_color(&self) -> bool {
self.styles.iter().all(|(_, style)| {
[style.fg, style.bg]
.into_iter()
.all(|color| !matches!(color, Some(Color::Rgb(..))))
})
}
}
struct ThemePalette {

@ -1,6 +1,10 @@
use std::borrow::Cow;
use crate::{graphics::Rect, Document, DocumentId, ViewId};
use crate::{
graphics::Rect,
gutter::{self, Gutter},
Document, DocumentId, ViewId,
};
use helix_core::{
graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index,
@ -60,6 +64,8 @@ impl JumpList {
}
}
const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
#[derive(Debug)]
pub struct View {
pub id: ViewId,
@ -69,6 +75,11 @@ pub struct View {
pub jumps: JumpList,
/// the last accessed file before the current one
pub last_accessed_doc: Option<DocumentId>,
/// the last modified files before the current one
/// ordered from most frequent to least frequent
// uses two docs because we want to be able to swap between the
// two last modified docs which we need to manually keep track of
pub last_modified_docs: [Option<DocumentId>; 2],
}
impl View {
@ -80,13 +91,23 @@ impl View {
area: Rect::default(), // will get calculated upon inserting into tree
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
last_accessed_doc: None,
last_modified_docs: [None, None],
}
}
pub fn gutters(&self) -> &[(Gutter, usize)] {
GUTTERS
}
pub fn inner_area(&self) -> Rect {
// TODO: not ideal
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
// TODO: cache this
let offset = self
.gutters()
.iter()
.map(|(_, width)| *width as u16)
.sum::<u16>()
+ 1; // +1 for some space between gutters and line
self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
}
//
@ -276,6 +297,7 @@ mod tests {
use super::*;
use helix_core::Rope;
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
#[test]
fn test_text_pos_at_screen_coords() {

@ -11,6 +11,7 @@ indent = { tab-width = 4, unit = " " }
[language.config]
cargo = { loadOutDirsFromCheck = true }
procMacro = { enable = false }
diagnostics = { disabled = ["unresolved-proc-macro"] }
[[language]]
name = "toml"
@ -249,7 +250,6 @@ language-server = { command = "julia", args = [
using Pkg;
import StaticLint;
env_path = dirname(Pkg.Types.Context().env.project_file);
server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
server.runlinter = true;
run(server);
@ -396,3 +396,37 @@ shebangs = ["perl"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
[[language]]
name = "racket"
scope = "source.rkt"
roots = []
file-types = ["rkt"]
shebangs = ["racket"]
comment-token = ";"
language-server = { command = "racket", args = ["-l", "racket-langserver"] }
[[language]]
name = "wgsl"
scope = "source.wgsl"
file-types = ["wgsl"]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
[[language]]
name = "llvm"
scope = "source.llvm"
roots = []
file-types = ["ll"]
comment-token = ";"
indent = { tab-width = 2, unit = " " }
[[language]]
name = "markdown"
scope = "source.md"
injection-regex = "md|markdown"
file-types = ["md"]
roots = []
indent = { tab-width = 2, unit = " " }

@ -8,7 +8,7 @@
(constructor) @constructor
(pragma) @pragma
(comment) @comment
(signature name: (variable) @fun_type_name)
(signature name: (variable) @type)
(function name: (variable) @function)
(constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class))

@ -2,24 +2,24 @@
(import_statement
(identifier) @definition.import)
(variable_declaration
(identifier) @definition.var)
(identifier) @local.definition)
(variable_declaration
(tuple_expression
(identifier) @definition.var))
(identifier) @local.definition))
(for_binding
(identifier) @definition.var)
(identifier) @local.definition)
(for_binding
(tuple_expression
(identifier) @definition.var))
(identifier) @local.definition))
(assignment_expression
(tuple_expression
(identifier) @definition.var))
(identifier) @local.definition))
(assignment_expression
(bare_tuple_expression
(identifier) @definition.var))
(identifier) @local.definition))
(assignment_expression
(identifier) @definition.var)
(identifier) @local.definition)
(type_parameter_list
(identifier) @definition.type)
@ -43,11 +43,11 @@
(identifier) @definition.parameter)
(function_definition
name: (identifier) @definition.function) @scope
name: (identifier) @definition.function) @local.scope
(macro_definition
name: (identifier) @definition.macro) @scope
name: (identifier) @definition.macro) @local.scope
(identifier) @reference
(identifier) @local.reference
[
(try_statement)
@ -56,4 +56,4 @@
(let_statement)
(compound_expression)
(for_statement)
] @scope
] @local.scope

@ -278,7 +278,7 @@
"\\includeinkscape"
"\\usepgflibrary"
"\\usetikzlibrary"
] @include
] @keyword.control.import
[
"\\part"
@ -318,60 +318,60 @@
["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
(chapter
text: (brace_group) @text.title)
text: (brace_group) @markup.heading)
(part
text: (brace_group) @text.title)
text: (brace_group) @markup.heading)
(section
text: (brace_group) @text.title)
text: (brace_group) @markup.heading)
(subsection
text: (brace_group) @text.title)
text: (brace_group) @markup.heading)
(subsubsection
text: (brace_group) @text.title)
text: (brace_group) @markup.heading)
(paragraph
text: (brace_group) @text.title)
text: (brace_group) @markup.heading)
(subparagraph
text: (brace_group) @text.title)
text: (brace_group) @markup.heading)
((environment
(begin
name: (word) @_frame)
(brace_group
child: (text) @text.title))
child: (text) @markup.heading))
(#eq? @_frame "frame"))
((generic_command
name:(generic_command_name) @_name
arg: (brace_group
(text) @text.title))
(text) @markup.heading))
(#eq? @_name "\\frametitle"))
;; Formatting
((generic_command
name:(generic_command_name) @_name
arg: (_) @text.emphasis)
arg: (_) @markup.italic)
(#eq? @_name "\\emph"))
((generic_command
name:(generic_command_name) @_name
arg: (_) @text.emphasis)
arg: (_) @markup.italic)
(#match? @_name "^(\\\\textit|\\\\mathit)$"))
((generic_command
name:(generic_command_name) @_name
arg: (_) @text.strong)
arg: (_) @markup.bold)
(#match? @_name "^(\\\\textbf|\\\\mathbf)$"))
((generic_command
name:(generic_command_name) @_name
.
arg: (_) @text.uri)
arg: (_) @markup.underline.link)
(#match? @_name "^(\\\\url|\\\\href)$"))
(ERROR) @error

@ -12,7 +12,7 @@
((account) @variable.other.member)
((commodity) @text.literal)
"include" @include
"include" @keyword.local.import
[
"account"

@ -0,0 +1,14 @@
(type) @type
(statement) @keyword.operator
(number) @constant.numeric.integer
(comment) @comment
(string) @string
(label) @label
(keyword) @keyword
"ret" @keyword.control.return
(boolean) @constant.builtin.boolean
(float) @constant.numeric.float
(constant) @constant
(identifier) @variable
(symbol) @punctuation.delimiter
(bracket) @punctuation.bracket

@ -0,0 +1,35 @@
[
(atx_heading)
(setext_heading)
] @markup.heading
(code_fence_content) @none
[
(indented_code_block)
(fenced_code_block)
] @markup.raw.block
(code_span) @markup.raw.inline
(emphasis) @markup.italic
(strong_emphasis) @markup.bold
(link_destination) @markup.underline.link
; (link_label) @markup.label ; TODO: rename
[
(list_marker_plus)
(list_marker_minus)
(list_marker_star)
(list_marker_dot)
(list_marker_parenthesis)
] @punctuation.special
[
(backslash_escape)
(hard_line_break)
] @string.character.escape

@ -0,0 +1,8 @@
(fenced_code_block
(info_string) @injection.language
(code_fence_content) @injection.content)
((html_block) @injection.content
(#set! injection.language "html"))
((html_tag) @injection.content
(#set! injection.language "html"))

@ -90,7 +90,7 @@
["exception" "try"] @keyword.control.exception
["include" "open"] @include
["include" "open"] @keyword.control.import
["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat

@ -0,0 +1,17 @@
indent = [
"function",
"identifier",
"method_invocation",
"if_statement",
"unless_statement",
"if_simple_statement",
"unless_simple_statement",
"variable_declaration",
"block",
"list_item",
"word_list_qw"
]
outdent = [
"}"
]

@ -25,7 +25,7 @@
((attribute
(attribute_name) @_attr
(quoted_attribute_value (attribute_value) @markup.undeline.link))
(quoted_attribute_value (attribute_value) @markup.underline.link))
(#match? @_attr "^(href|src)$"))
(tag_name) @tag

@ -0,0 +1,102 @@
(const_literal) @constant.numeric
(type_declaration) @type
(function_declaration
(identifier) @function)
(struct_declaration
(identifier) @type)
(type_constructor_or_function_call_expression
(type_declaration) @function)
(parameter
(variable_identifier_declaration (identifier) @variable.parameter))
[
"struct"
"bitcast"
; "block"
"discard"
"enable"
"fallthrough"
"fn"
"let"
"private"
"read"
"read_write"
"return"
"storage"
"type"
"uniform"
"var"
"workgroup"
"write"
(texel_format)
] @keyword ; TODO reserved keywords
[
(true)
(false)
] @constant.builtin.boolean
[ "," "." ":" ";" ] @punctuation.delimiter
;; brackets
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
[
"loop"
"for"
"break"
"continue"
"continuing"
] @keyword.control.repeat
[
"if"
"else"
"elseif"
"switch"
"case"
"default"
] @keyword.control.conditional
[
"&"
"&&"
"/"
"!"
"="
"=="
"!="
">"
">="
">>"
"<"
"<="
"<<"
"%"
"-"
"+"
"|"
"||"
"*"
"~"
"^"
] @operator
(attribute
(identifier) @variable.other.member)
(comment) @comment
(ERROR) @error

@ -1,27 +1,28 @@
# Author: RayGervais<raygervais@hotmail.ca>
# Author: RayGervais <raygervais@hotmail.ca>
"ui.background" = { bg = "base00" }
"ui.menu" = "base01"
"ui.menu.selected" = { fg = "base04", bg = "base01" }
"ui.linenr" = {fg = "base01" }
"ui.menu.selected" = { fg = "base01", bg = "base04" }
"ui.linenr" = { fg = "base03", bg = "base01" }
"ui.popup" = { bg = "base01" }
"ui.window" = { bg = "base01" }
"ui.liner.selected" = "base02"
"ui.selection" = "base02"
"comment" = "base03"
"ui.statusline" = {fg = "base04", bg = "base01" }
"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
"ui.selection" = { bg = "base02" }
"comment" = { fg = "base03", modifiers = ["italic"] }
"ui.statusline" = { fg = "base04", bg = "base01" }
"ui.help" = { fg = "base04", bg = "base01" }
"ui.cursor" = { fg = "base05", modifiers = ["reversed"] }
"ui.text" = { fg = "base05" }
"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
"ui.text" = "base05"
"operator" = "base05"
"ui.text.focus" = { fg = "base05" }
"ui.text.focus" = "base05"
"variable" = "base08"
"constant.numeric" = "base09"
"constant" = "base09"
"attributes" = "base09"
"type" = "base0A"
"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
"strings" = "base0B"
"string" = "base0B"
"variable.other.member" = "base0B"
"constant.character.escape" = "base0C"
"function" = "base0D"
@ -30,15 +31,15 @@
"keyword" = "base0E"
"label" = "base0E"
"namespace" = "base0E"
"ui.popup" = { bg = "base01" }
"ui.window" = { bg = "base00" }
"ui.help" = { bg = "base01", fg = "base06" }
"ui.help" = { fg = "base06", bg = "base01" }
"info" = "base03"
"diagnostic" = { modifiers = ["underlined"] }
"ui.gutter" = { bg = "base01" }
"info" = "base0D"
"hint" = "base03"
"debug" = "base03"
"diagnostic" = "base03"
"error" = "base0E"
"warning" = "base09"
"error" = "base08"
[palette]
base00 = "#181818" # Default Background

@ -0,0 +1,60 @@
# Author: NNB <nnbnh@protonmail.com>
"ui.background" = { bg = "base00" }
"ui.menu" = "base01"
"ui.menu.selected" = { fg = "base01", bg = "base04" }
"ui.linenr" = { fg = "base03", bg = "base01" }
"ui.popup" = { bg = "base01" }
"ui.window" = { bg = "base01" }
"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
"ui.selection" = { bg = "base02" }
"comment" = { fg = "base03", modifiers = ["italic"] }
"ui.statusline" = { fg = "base04", bg = "base01" }
"ui.help" = { fg = "base04", bg = "base01" }
"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
"ui.text" = "base05"
"operator" = "base05"
"ui.text.focus" = "base05"
"variable" = "base08"
"constant.numeric" = "base09"
"constant" = "base09"
"attributes" = "base09"
"type" = "base0A"
"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
"string" = "base0B"
"variable.other.member" = "base0B"
"constant.character.escape" = "base0C"
"function" = "base0D"
"constructor" = "base0D"
"special" = "base0D"
"keyword" = "base0E"
"label" = "base0E"
"namespace" = "base0E"
"ui.help" = { fg = "base06", bg = "base01" }
"diagnostic" = { modifiers = ["underlined"] }
"ui.gutter" = { bg = "base01" }
"info" = "base0D"
"hint" = "base03"
"debug" = "base03"
"warning" = "base09"
"error" = "base08"
[palette]
base00 = "#f8f8f8" # Default Background
base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks)
base02 = "#d8d8d8" # Selection Background
base03 = "#b8b8b8" # Comments, Invisibles, Line Highlighting
base04 = "#585858" # Dark Foreground (Used for status bars)
base05 = "#383838" # Default Foreground, Caret, Delimiters, Operators
base06 = "#282828" # Light Foreground (Not often used)
base07 = "#181818" # Light Background (Not often used)
base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>

@ -0,0 +1,39 @@
# Author: NNB <nnbnh@protonmail.com>
"ui.menu" = "black"
"ui.menu.selected" = { modifiers = ["reversed"] }
"ui.linenr" = { fg = "light-gray", bg = "black" }
"ui.popup" = { bg = "black" }
"ui.window" = { bg = "black" }
"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
"ui.selection" = { fg = "gray", modifiers = ["reversed"] }
"comment" = { fg = "light-gray", modifiers = ["italic"] }
"ui.statusline" = { fg = "white", bg = "black" }
"ui.statusline.inactive" = { fg = "gray", bg = "black" }
"ui.help" = { fg = "white", bg = "black" }
"ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] }
"ui.cursor.primary" = { modifiers = ["reversed"] }
"variable" = "light-red"
"constant.numeric" = "yellow"
"constant" = "yellow"
"attributes" = "yellow"
"type" = "light-yellow"
"ui.cursor.match" = { fg = "light-yellow", modifiers = ["underlined"] }
"string" = "light-green"
"variable.other.member" = "light-green"
"constant.character.escape" = "light-cyan"
"function" = "light-blue"
"constructor" = "light-blue"
"special" = "light-blue"
"keyword" = "light-magenta"
"label" = "light-magenta"
"namespace" = "light-magenta"
"ui.help" = { fg = "white", bg = "black" }
"diagnostic" = { modifiers = ["underlined"] }
"ui.gutter" = { bg = "black" }
"info" = "light-blue"
"hint" = "gray"
"debug" = "gray"
"warning" = "yellow"
"error" = "light-red"

@ -0,0 +1,102 @@
# Author : WindSoilder<WindSoilder@outlook.com>
# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
# Credit goes to the original creator: https://monokai.pro
"ui.linenr.selected" = { bg = "base3" }
"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
"ui.menu.selected" = { fg = "base2", bg = "yellow" }
"info" = "base8"
"hint" = "base8"
# background color
"ui.background" = { bg = "base2" }
"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
# status bars, panels, modals, autocompletion
"ui.statusline" = { bg = "base4" }
"ui.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" }
"ui.help" = { bg = "base3" }
# active line, highlighting
"ui.selection" = { bg = "base4" }
"ui.cursor.match" = { bg = "base4" }
# comments, nord3 based lighter color
"comment" = { fg = "base5", modifiers = ["italic"] }
"ui.linenr" = { fg = "base5" }
# cursor, variables, constants, attributes, fields
"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
"attribute" = "blue"
"variable" = "base8"
"constant" = "orange"
"variable.builtin" = "red"
"constant.builtin" = "red"
"namespace" = "base8"
# base text, punctuation
"ui.text" = { fg = "base8" }
"punctuation" = "base6"
# classes, types, primiatives
"type" = "green"
"type.builtin" = { fg = "red"}
"label" = "base8"
# declaration, methods, routines
"constructor" = "blue"
"function" = "green"
"function.macro" = { fg = "blue" }
"function.builtin" = { fg = "cyan" }
# operator, tags, units, punctuations
"operator" = "red"
"variable.other.member" = "base8"
# keywords, special
"keyword" = { fg = "red" }
"keyword.directive" = "blue"
"variable.parameter" = "#f59762"
# error
"error" = "red"
# annotations, decorators
"special" = "#f59762"
"module" = "#f59762"
# warnings, escape characters, regex
"warning" = "orange"
"constant.character.escape" = { fg = "base8" }
# strings
"string" = "yellow"
# integer, floating point
"constant.numeric" = "purple"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[palette]
# primary colors
"red" = "#ff6188"
"orange" = "#fc9867"
"yellow" = "#ffd866"
"green" = "#a9dc76"
"blue" = "#78dce8"
"purple" = "#ab9df2"
# base colors, sorted from darkest to lightest
"base0" = "#19181a"
"base1" = "#221f22"
"base2" = "#2d2a2e"
"base3" = "#403e41"
"base4" = "#5b595c"
"base5" = "#727072"
"base6" = "#939293"
"base7" = "#c1c0c0"
"base8" = "#fcfcfa"
# variants (for when transparency isn't supported)
"base8x0c" = "#363337" # using base2 as bg

@ -0,0 +1,102 @@
# Author : WindSoilder<WindSoilder@outlook.com>
# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
# Credit goes to the original creator: https://monokai.pro
"ui.linenr.selected" = { bg = "base3" }
"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
"ui.menu.selected" = { fg = "base2", bg = "yellow" }
"info" = "base8"
"hint" = "base8"
# background color
"ui.background" = { bg = "base2" }
"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
# status bars, panels, modals, autocompletion
"ui.statusline" = { bg = "base4" }
"ui.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" }
"ui.help" = { bg = "base3" }
# active line, highlighting
"ui.selection" = { bg = "base4" }
"ui.cursor.match" = { bg = "base4" }
# comments, nord3 based lighter color
"comment" = { fg = "base5", modifiers = ["italic"] }
"ui.linenr" = { fg = "base5" }
# cursor, variables, constants, attributes, fields
"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
"attribute" = "blue"
"variable" = "base8"
"constant" = "orange"
"variable.builtin" = "red"
"constant.builtin" = "red"
"namespace" = "base8"
# base text, punctuation
"ui.text" = { fg = "base8" }
"punctuation" = "base6"
# classes, types, primiatives
"type" = "green"
"type.builtin" = { fg = "red"}
"label" = "base8"
# declaration, methods, routines
"constructor" = "blue"
"function" = "green"
"function.macro" = { fg = "blue" }
"function.builtin" = { fg = "cyan" }
# operator, tags, units, punctuations
"operator" = "red"
"variable.other.member" = "base8"
# keywords, special
"keyword" = { fg = "red" }
"keyword.directive" = "blue"
"variable.parameter" = "#f59762"
# error
"error" = "red"
# annotations, decorators
"special" = "#f59762"
"module" = "#f59762"
# warnings, escape characters, regex
"warning" = "orange"
"constant.character.escape" = { fg = "base8" }
# strings
"string" = "yellow"
# integer, floating point
"constant.numeric" = "purple"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[palette]
# primary colors
"red" = "#ff6d7e"
"orange" = "#ffb270"
"yellow" = "#ffed72"
"green" = "#a2e57b"
"blue" = "#7cd5f1"
"purple" = "#baa0f8"
# base colors
"base0" = "#161b1e"
"base1" = "#1d2528"
"base2" = "#273136"
"base3" = "#3a4449"
"base4" = "#545f62"
"base5" = "#6b7678"
"base6" = "#798384"
"base7" = "#b8c4c3"
"base8" = "#f2fffc"
# variants
"base8x0c" = "#303a3e"

@ -0,0 +1,102 @@
# Author : WindSoilder<WindSoilder@outlook.com>
# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
# Credit goes to the original creator: https://monokai.pro
"ui.linenr.selected" = { bg = "base3" }
"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
"ui.menu.selected" = { fg = "base2", bg = "yellow" }
"info" = "base8"
"hint" = "base8"
# background color
"ui.background" = { bg = "base2" }
"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
# status bars, panels, modals, autocompletion
"ui.statusline" = { bg = "base4" }
"ui.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" }
"ui.help" = { bg = "base3" }
# active line, highlighting
"ui.selection" = { bg = "base4" }
"ui.cursor.match" = { bg = "base4" }
# comments, nord3 based lighter color
"comment" = { fg = "base5", modifiers = ["italic"] }
"ui.linenr" = { fg = "base5" }
# cursor, variables, constants, attributes, fields
"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
"attribute" = "blue"
"variable" = "base8"
"constant" = "orange"
"variable.builtin" = "red"
"constant.builtin" = "red"
"namespace" = "base8"
# base text, punctuation
"ui.text" = { fg = "base8" }
"punctuation" = "base6"
# classes, types, primiatives
"type" = "green"
"type.builtin" = { fg = "red"}
"label" = "base8"
# declaration, methods, routines
"constructor" = "blue"
"function" = "green"
"function.macro" = { fg = "blue" }
"function.builtin" = { fg = "cyan" }
# operator, tags, units, punctuations
"operator" = "red"
"variable.other.member" = "base8"
# keywords, special
"keyword" = { fg = "red" }
"keyword.directive" = "blue"
"variable.parameter" = "#f59762"
# error
"error" = "red"
# annotations, decorators
"special" = "#f59762"
"module" = "#f59762"
# warnings, escape characters, regex
"warning" = "orange"
"constant.character.escape" = { fg = "base8" }
# strings
"string" = "yellow"
# integer, floating point
"constant.numeric" = "purple"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[palette]
# primary colors
"red" = "#ff657a"
"orange" = "#ff9b5e"
"yellow" = "#ffd76d"
"green" = "#bad761"
"blue" = "#9cd1bb"
"purple" = "#c39ac9"
# base colors
"base0" = "#161821"
"base1" = "#1e1f2b"
"base2" = "#282a3a"
"base3" = "#3a3d4b"
"base4" = "#535763"
"base5" = "#696d77"
"base6" = "#767b81"
"base7" = "#b2b9bd"
"base8" = "#eaf2f1"
# variants
"base8x0c" = "#303342"

@ -0,0 +1,102 @@
# Author : WindSoilder<WindSoilder@outlook.com>
# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
# Credit goes to the original creator: https://monokai.pro
"ui.linenr.selected" = { bg = "base3" }
"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
"ui.menu.selected" = { fg = "base2", bg = "yellow" }
"info" = "base8"
"hint" = "base8"
# background color
"ui.background" = { bg = "base2" }
"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
# status bars, panels, modals, autocompletion
"ui.statusline" = { bg = "base4" }
"ui.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" }
"ui.help" = { bg = "base3" }
# active line, highlighting
"ui.selection" = { bg = "base4" }
"ui.cursor.match" = { bg = "base4" }
# comments, nord3 based lighter color
"comment" = { fg = "base5", modifiers = ["italic"] }
"ui.linenr" = { fg = "base5" }
# cursor, variables, constants, attributes, fields
"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
"attribute" = "blue"
"variable" = "base8"
"constant" = "orange"
"variable.builtin" = "red"
"constant.builtin" = "red"
"namespace" = "base8"
# base text, punctuation
"ui.text" = { fg = "base8" }
"punctuation" = "base6"
# classes, types, primiatives
"type" = "green"
"type.builtin" = { fg = "red"}
"label" = "base8"
# declaration, methods, routines
"constructor" = "blue"
"function" = "green"
"function.macro" = { fg = "blue" }
"function.builtin" = { fg = "cyan" }
# operator, tags, units, punctuations
"operator" = "red"
"variable.other.member" = "base8"
# keywords, special
"keyword" = { fg = "red" }
"keyword.directive" = "blue"
"variable.parameter" = "#f59762"
# error
"error" = "red"
# annotations, decorators
"special" = "#f59762"
"module" = "#f59762"
# warnings, escape characters, regex
"warning" = "orange"
"constant.character.escape" = { fg = "base8" }
# strings
"string" = "yellow"
# integer, floating point
"constant.numeric" = "purple"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[palette]
# primary colors
"red" = "#fd6883"
"orange" = "#f38d70"
"yellow" = "#f9cc6c"
"green" = "#adda78"
"blue" = "#85dacc"
"purple" = "#a8a9eb"
# base colors
"base0" = "#191515"
"base1" = "#211c1c"
"base2" = "#2c2525"
"base3" = "#403838"
"base4" = "#5b5353"
"base5" = "#72696a"
"base6" = "#8c8384"
"base7" = "#c3b7b8"
"base8" = "#fff1f3"
# variants
"base8x0c" = "#352e2e"

@ -0,0 +1,102 @@
# Author : WindSoilder<WindSoilder@outlook.com>
# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
# Credit goes to the original creator: https://monokai.pro
"ui.linenr.selected" = { bg = "base3" }
"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
"ui.menu.selected" = { fg = "base2", bg = "yellow" }
"info" = "base8"
"hint" = "base8"
# background color
"ui.background" = { bg = "base2" }
"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
# status bars, panels, modals, autocompletion
"ui.statusline" = { bg = "base4" }
"ui.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" }
"ui.help" = { bg = "base3" }
# active line, highlighting
"ui.selection" = { bg = "base4" }
"ui.cursor.match" = { bg = "base4" }
# comments, nord3 based lighter color
"comment" = { fg = "base5", modifiers = ["italic"] }
"ui.linenr" = { fg = "base5" }
# cursor, variables, constants, attributes, fields
"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
"attribute" = "blue"
"variable" = "base8"
"constant" = "orange"
"variable.builtin" = "red"
"constant.builtin" = "red"
"namespace" = "base8"
# base text, punctuation
"ui.text" = { fg = "base8" }
"punctuation" = "base6"
# classes, types, primiatives
"type" = "green"
"type.builtin" = { fg = "red"}
"label" = "base8"
# declaration, methods, routines
"constructor" = "blue"
"function" = "green"
"function.macro" = { fg = "blue" }
"function.builtin" = { fg = "cyan" }
# operator, tags, units, punctuations
"operator" = "red"
"variable.other.member" = "base8"
# keywords, special
"keyword" = { fg = "red" }
"keyword.directive" = "blue"
"variable.parameter" = "#f59762"
# error
"error" = "red"
# annotations, decorators
"special" = "#f59762"
"module" = "#f59762"
# warnings, escape characters, regex
"warning" = "orange"
"constant.character.escape" = { fg = "base8" }
# strings
"string" = "yellow"
# integer, floating point
"constant.numeric" = "purple"
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
[palette]
# primary colors
"red" = "#fc618d"
"orange" = "#fd9353"
"yellow" = "#fce566"
"green" = "#7bd88f"
"blue" = "#5ad4e6"
"purple" = "#948ae3"
# base colors
"base0" = "#131313"
"base1" = "#191919"
"base2" = "#222222"
"base3" = "#363537"
"base4" = "#525053"
"base5" = "#69676c"
"base6" = "#8b888f"
"base7" = "#bab6c0"
"base8" = "#f7f1ff"
# variants
"base8x0c" = "#2b2b2b"

@ -10,6 +10,7 @@
"ui.selection" = "highlight"
"comment" = "subtle"
"ui.statusline" = {fg = "foam", bg = "surface" }
"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
"ui.help" = { fg = "foam", bg = "surface" }
"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
"ui.text" = { fg = "text" }

@ -0,0 +1,63 @@
# Author: ChrisHa<chunghha@users.noreply.github.com>
# Author: RayGervais<raygervais@hotmail.ca>
"ui.background" = { bg = "base" }
"ui.menu" = "surface"
"ui.menu.selected" = { fg = "iris", bg = "surface" }
"ui.linenr" = {fg = "subtle" }
"ui.popup" = { bg = "overlay" }
"ui.window" = { bg = "overlay" }
"ui.liner.selected" = "highlightOverlay"
"ui.selection" = "highlight"
"comment" = "subtle"
"ui.statusline" = {fg = "foam", bg = "surface" }
"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
"ui.help" = { fg = "foam", bg = "surface" }
"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
"ui.text" = { fg = "text" }
"operator" = "rose"
"ui.text.focus" = { fg = "base05" }
"variable" = "text"
"number" = "iris"
"constant" = "gold"
"attributes" = "gold"
"type" = "foam"
"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] }
"string" = "gold"
"property" = "foam"
"escape" = "subtle"
"function" = "rose"
"function.builtin" = "rose"
"function.method" = "foam"
"constructor" = "gold"
"special" = "gold"
"keyword" = "pine"
"label" = "iris"
"namespace" = "pine"
"ui.popup" = { bg = "overlay" }
"ui.window" = { bg = "base" }
"ui.help" = { bg = "overlay", fg = "foam" }
"text" = "text"
"info" = "gold"
"hint" = "gold"
"debug" = "rose"
"diagnostic" = "rose"
"error" = "love"
[palette]
base = "#faf4ed"
surface = "#fffaf3"
overlay = "#f2e9de"
inactive = "#9893a5"
subtle = "#6e6a86"
text = "#575279"
love = "#b4637a"
gold = "#ea9d34"
rose = "#d7827e"
pine = "#286983"
foam = "#56949f"
iris = "#907aa9"
highlight = "#eee9e6"
highlightInactive = "#f2ede9"
highlightOverlay = "#e4dfde"

@ -58,13 +58,13 @@
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
"ui.cursor.primary" = {fg = "base03", bg = "base1"}
"ui.selection.primary" = { fg = "base03", bg = "base01" }
"ui.cursor.select" = {fg = "base02", bg = "green"}
"ui.selection" = { fg = "base02", bg = "yellow" }
"ui.cursor.primary" = { fg = "base03", bg = "base1" }
"ui.cursor.select" = { fg = "base02", bg = "cyan" }
"ui.selection" = { bg = "base0175" }
"ui.selection.primary" = { bg = "base015" }
# normal模式的光标
"ui.cursor" = {fg = "base03", bg = "green"}
"ui.cursor" = {fg = "base02", bg = "cyan"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
@ -73,18 +73,20 @@
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { mdifiers = ["underlined"] }
"diagnostic" = { modifiers = ["underlined"] }
[palette]
# 深色 越来越深
base03 = "#002b36"
base02 = "#073642"
base01 = "#586e75"
base00 = "#657b83"
base0 = "#839496"
base1 = "#93a1a1"
base2 = "#eee8d5"
base3 = "#fdf6e3"
base03 = "#002b36"
base02 = "#073642"
base0175 = "#16404b"
base015 = "#2c4f59"
base01 = "#586e75"
base00 = "#657b83"
base0 = "#839496"
base1 = "#93a1a1"
base2 = "#eee8d5"
base3 = "#fdf6e3"
# 浅色 越來越浅
yellow = "#b58900"

@ -58,13 +58,13 @@
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
"ui.cursor.primary" = {fg = "base03", bg = "base1"}
"ui.selection.primary" = { fg = "base03", bg = "base01" }
"ui.cursor.select" = {fg = "base02", bg = "green"}
"ui.selection" = { fg = "base02", bg = "yellow" }
"ui.cursor.primary" = { fg = "base03", bg = "base1" }
"ui.cursor.select" = { fg = "base02", bg = "cyan" }
"ui.selection" = { bg = "base0175" }
"ui.selection.primary" = { bg = "base015" }
# normal模式的光标
"ui.cursor" = {fg = "base03", bg = "green"}
"ui.cursor" = {fg = "base02", bg = "cyan"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
@ -73,26 +73,28 @@
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { mdifiers = ["underlined"] }
"diagnostic" = { modifiers = ["underlined"] }
[palette]
red = '#dc322f'
green = '#859900'
yellow = '#b58900'
blue = '#268bd2'
magenta = '#d33682'
cyan = '#2aa198'
orange = '#cb4b16'
violet = '#6c71c4'
red = '#dc322f'
green = '#859900'
yellow = '#b58900'
blue = '#268bd2'
magenta = '#d33682'
cyan = '#2aa198'
orange = '#cb4b16'
violet = '#6c71c4'
# 深色 越来越深
base0 = '#657b83'
base1 = '#586e75'
base2 = '#073642'
base3 = '#002b36'
base0 = '#657b83'
base1 = '#586e75'
base2 = '#073642'
base3 = '#002b36'
## 浅色 越來越浅
base00 = '#839496'
base01 = '#93a1a1'
base02 = '#eee8d5'
base03 = '#fdf6e3'
base00 = '#839496'
base01 = '#93a1a1'
base015 = '#c5c8bd'
base0175 = '#dddbcc'
base02 = '#eee8d5'
base03 = '#fdf6e3'

@ -28,6 +28,12 @@ string = "silver"
# used for lifetimes
label = "honey"
"markup.heading" = "lilac"
"markup.bold" = { modifiers = ["bold"] }
"markup.italic" = { modifiers = ["italic"] }
"markup.underline.link" = { fg = "silver", modifiers = ["underlined"] }
"markup.raw" = "almond"
# TODO: diferentiate doc comment
# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors

@ -0,0 +1,11 @@
[package]
name = "xtask"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-term = { version = "0.5", path = "../helix-term" }
helix-core = { version = "0.5", path = "../helix-core" }
toml = "0.5"

@ -0,0 +1,277 @@
use std::{env, error::Error};
type DynError = Box<dyn Error>;
pub mod helpers {
use std::{
fmt::Display,
path::{Path, PathBuf},
};
use crate::path;
use helix_core::syntax::Configuration as LangConfig;
#[derive(Copy, Clone)]
pub enum TsFeature {
Highlight,
TextObjects,
AutoIndent,
}
impl TsFeature {
pub fn all() -> &'static [Self] {
&[Self::Highlight, Self::TextObjects, Self::AutoIndent]
}
pub fn runtime_filename(&self) -> &'static str {
match *self {
Self::Highlight => "highlights.scm",
Self::TextObjects => "textobjects.scm",
Self::AutoIndent => "indents.toml",
}
}
}
impl Display for TsFeature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match *self {
Self::Highlight => "Syntax Highlighting",
Self::TextObjects => "Treesitter Textobjects",
Self::AutoIndent => "Auto Indent",
}
)
}
}
/// Get the list of languages that support a particular tree-sitter
/// based feature.
pub fn ts_lang_support(feat: TsFeature) -> Vec<String> {
let queries_dir = path::ts_queries();
find_files(&queries_dir, feat.runtime_filename())
.iter()
.map(|f| {
// .../helix/runtime/queries/python/highlights.scm
let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm
let lang = tail.components().next().unwrap(); // python
lang.as_os_str().to_string_lossy().to_string()
})
.collect()
}
/// Get the list of languages that have any form of tree-sitter
/// queries defined in the runtime directory.
pub fn langs_with_ts_queries() -> Vec<String> {
std::fs::read_dir(path::ts_queries())
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
entry
.file_type()
.ok()?
.is_dir()
.then(|| entry.file_name().to_string_lossy().to_string())
})
.collect()
}
// naive implementation, but suffices for our needs
pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> {
std::fs::read_dir(dir)
.unwrap()
.filter_map(|entry| {
let path = entry.ok()?.path();
if path.is_dir() {
Some(find_files(&path, filename))
} else {
(path.file_name()?.to_string_lossy() == filename).then(|| vec![path])
}
})
.flatten()
.collect()
}
pub fn lang_config() -> LangConfig {
let bytes = std::fs::read(path::lang_config()).unwrap();
toml::from_slice(&bytes).unwrap()
}
}
pub mod md_gen {
use crate::DynError;
use crate::helpers;
use crate::path;
use std::fs;
use helix_term::commands::cmd::TYPABLE_COMMAND_LIST;
pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
fn md_table_heading(cols: &[String]) -> String {
let mut header = String::new();
header += &md_table_row(cols);
header += &md_table_row(&vec!["---".to_string(); cols.len()]);
header
}
fn md_table_row(cols: &[String]) -> String {
"| ".to_owned() + &cols.join(" | ") + " |\n"
}
fn md_mono(s: &str) -> String {
format!("`{}`", s)
}
pub fn typable_commands() -> Result<String, DynError> {
let mut md = String::new();
md.push_str(&md_table_heading(&[
"Name".to_owned(),
"Description".to_owned(),
]));
let cmdify = |s: &str| format!("`:{}`", s);
for cmd in TYPABLE_COMMAND_LIST {
let names = std::iter::once(&cmd.name)
.chain(cmd.aliases.iter())
.map(|a| cmdify(a))
.collect::<Vec<_>>()
.join(", ");
md.push_str(&md_table_row(&[names.to_owned(), cmd.doc.to_owned()]));
}
Ok(md)
}
pub fn lang_features() -> Result<String, DynError> {
let mut md = String::new();
let ts_features = helpers::TsFeature::all();
let mut cols = vec!["Language".to_owned()];
cols.append(
&mut ts_features
.iter()
.map(|t| t.to_string())
.collect::<Vec<_>>(),
);
cols.push("Default LSP".to_owned());
md.push_str(&md_table_heading(&cols));
let config = helpers::lang_config();
let mut langs = config
.language
.iter()
.map(|l| l.language_id.clone())
.collect::<Vec<_>>();
langs.sort_unstable();
let mut ts_features_to_langs = Vec::new();
for &feat in ts_features {
ts_features_to_langs.push((feat, helpers::ts_lang_support(feat)));
}
let mut row = Vec::new();
for lang in langs {
let lc = config
.language
.iter()
.find(|l| l.language_id == lang)
.unwrap(); // lang comes from config
row.push(lc.language_id.clone());
for (_feat, support_list) in &ts_features_to_langs {
row.push(
if support_list.contains(&lang) {
"✓"
} else {
""
}
.to_owned(),
);
}
row.push(
lc.language_server
.as_ref()
.map(|s| s.command.clone())
.map(|c| md_mono(&c))
.unwrap_or_default(),
);
md.push_str(&md_table_row(&row));
row.clear();
}
Ok(md)
}
pub fn write(filename: &str, data: &str) {
let error = format!("Could not write to {}", filename);
let path = path::book_gen().join(filename);
fs::write(path, data).expect(&error);
}
}
pub mod path {
use std::path::{Path, PathBuf};
pub fn project_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf()
}
pub fn book_gen() -> PathBuf {
project_root().join("book/src/generated/")
}
pub fn ts_queries() -> PathBuf {
project_root().join("runtime/queries")
}
pub fn lang_config() -> PathBuf {
project_root().join("languages.toml")
}
}
pub mod tasks {
use crate::md_gen;
use crate::DynError;
pub fn docgen() -> Result<(), DynError> {
use md_gen::*;
write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?);
write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?);
Ok(())
}
pub fn print_help() {
println!(
"
Usage: Run with `cargo xtask <task>`, eg. `cargo xtask docgen`.
Tasks:
docgen: Generate files to be included in the mdbook output.
"
);
}
}
fn main() -> Result<(), DynError> {
let task = env::args().nth(1);
match task {
None => tasks::print_help(),
Some(t) => match t.as_str() {
"docgen" => tasks::docgen()?,
invalid => return Err(format!("Invalid task name: {}", invalid).into()),
},
};
Ok(())
}
Loading…
Cancel
Save