Merge branch 'master' into cursor-shape-new

imgbot
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 ### Environment
- Platform: <!-- macOS / Windows / Linux --> - 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> <details><summary>~/.cache/helix/helix.log</summary>

@ -25,19 +25,19 @@ jobs:
override: true override: true
- name: Cache cargo registry - name: Cache cargo registry
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: target path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -64,19 +64,19 @@ jobs:
override: true override: true
- name: Cache cargo registry - name: Cache cargo registry
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: target path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -109,19 +109,19 @@ jobs:
components: rustfmt, clippy components: rustfmt, clippy
- name: Cache cargo registry - name: Cache cargo registry
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
with: with:
path: target path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -137,3 +137,51 @@ jobs:
with: with:
command: clippy command: clippy
args: -- -D warnings 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 fi
cp -r runtime dist cp -r runtime dist
- uses: actions/upload-artifact@v2.2.4 - uses: actions/upload-artifact@v2.3.0
with: with:
name: bins-${{ matrix.build }} name: bins-${{ matrix.build }}
path: dist path: dist

12
.gitmodules vendored

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

@ -6,6 +6,7 @@ members = [
"helix-tui", "helix-tui",
"helix-syntax", "helix-syntax",
"helix-lsp", "helix-lsp",
"xtask",
] ]
# Build helix-syntax in release mode to make the code path faster in development. # 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`. 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 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 config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
via the `HELIX_RUNTIME` environment variable. 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 Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir. that sets the variable to the install dir.
@ -65,21 +65,7 @@ brew install helix
# Contributing # Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.** Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
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.
# Getting help # 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) - [Installation](./install.md)
- [Usage](./usage.md) - [Usage](./usage.md)
- [Keymap](./keymap.md)
- [Commands](./commands.md)
- [Language Support](./lang-support.md)
- [Migrating from Vim](./from-vim.md) - [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md) - [Configuration](./configuration.md)
- [Themes](./themes.md) - [Themes](./themes.md)
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.md) - [Key Remapping](./remapping.md)
- [Hooks](./hooks.md) - [Hooks](./hooks.md)
- [Languages](./languages.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` | | `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` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` | | `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 ### `[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 ## 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 you can run the command
```sh ```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name> 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. Releases are available in the `community` repository.
Packages are also available on AUR: A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
- [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 ### Fedora Linux
You can install the COPR package for Helix via
```
sudo dnf copr enable varlad/helix
sudo dnf install helix
```
## Build from source ## Build from source

@ -34,6 +34,7 @@
| `Ctrl-d` | Move half page down | `half_page_down` | | `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | | `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | | `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` | | `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A | | `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A | | `m` | Enter [match mode](#match-mode) | N/A |
@ -69,20 +70,24 @@
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` | | `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` | | `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` | | `<` | Unindent selection | `unindent` |
| `=` | Format selection (**LSP**) | `format_selections` | | `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_selection` | | `d` | Delete selection | `delete_selection` |
| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` | | `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-a` | Increment object (number) under cursor | `increment` |
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | | `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 #### Shell
| Key | Description | Command | | Key | Description | Command |
| ------ | ----------- | ------- | | ------ | ----------- | ------- |
| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` | | <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` | | <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` | | `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | | `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
### Selection manipulation ### Selection manipulation
@ -158,17 +163,19 @@ Jumps to various locations.
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` | | `g` | Go to the start of the file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` | | `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` | | `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` | | `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` | | `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
| `t` | Go to the top of the screen | `goto_window_top` | | `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` | | `b` | Go to the bottom of the screen | `goto_window_bottom` |
| `d` | Go to definition (**LSP**) | `goto_definition` | | `d` | Go to definition (**LSP**) | `goto_definition` |
| `y` | Go to type definition (**LSP**) | `goto_type_definition` | | `y` | Go to type definition (**LSP**) | `goto_type_definition` |
| `r` | Go to references (**LSP**) | `goto_reference` | | `r` | Go to references (**LSP**) | `goto_reference` |
| `i` | Go to implementation (**LSP**) | `goto_implementation` | | `i` | Go to implementation (**LSP**) | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | | `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` | | `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` | | `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` | | `.` | 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` | | `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` | | `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` | | `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` | | `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` | | `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` | | `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-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line | | `Ctrl-k` | Delete to end of line |
| `backspace`, `Ctrl-h` | Delete previous char | | `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-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | Select previous history | | `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next 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 ```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select' # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal] [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 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 w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line "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 "A-x" = "normal_mode" # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert 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 Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows: `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"` | | Down | `"down"` |
| Home | `"home"` | | Home | `"home"` |
| End | `"end"` | | End | `"end"` |
| Page | `"pageup"` | | Page Up | `"pageup"` |
| Page | `"pagedown"` | | Page Down | `"pagedown"` |
| Tab | `"tab"` | | Tab | `"tab"` |
| Back | `"backtab"` |
| Delete | `"del"` | | Delete | `"del"` |
| Insert | `"ins"` | | Insert | `"ins"` |
| Null | `"null"` | | 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. 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 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` - `conditional` - `if`, `else`
- `repeat` - `for`, `while`, `loop` - `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export` - `import` - `import`, `export`
- (TODO: return?) - `return`
- `operator` - `or`, `in`
- `directive` - Preprocessor directives (`#if` in C) - `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func` - `function` - `fn`, `func`
- `operator` - `||`, `+=`, `>`, `or` - `operator` - `||`, `+=`, `>`
- `function` - `function`
- `builtin` - `builtin`
@ -161,6 +162,20 @@ We use a similar set of scopes as
- `namespace` - `namespace`
- `markup`
- `heading`
- `list`
- `unnumbered`
- `numbered`
- `bold`
- `italic`
- `underline`
- `link`
- `quote`
- `raw`
- `inline`
- `block`
#### Interface #### Interface
These scopes are used for theming the editor 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 search |
| `:` | Last executed command | | `:` | Last executed command |
| `"` | Last yanked text | | `"` | 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. > 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 ## 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": { "nodes": {
"devshell": { "devshell": {
"locked": { "locked": {
"lastModified": 1632436039, "lastModified": 1637575296,
"narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=", "narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6", "rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -17,11 +17,11 @@
}, },
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1623875721, "lastModified": 1637014545,
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -30,22 +30,6 @@
"type": "github" "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": { "nixCargoIntegration": {
"inputs": { "inputs": {
"devshell": "devshell", "devshell": "devshell",
@ -57,11 +41,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1634796585, "lastModified": 1638425401,
"narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=", "narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "a84a2137a396f303978f1d48341e0390b0e16a8b", "rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -72,11 +56,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1634782485, "lastModified": 1638376152,
"narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=", "narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be", "rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -88,22 +72,22 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1628186154, "lastModified": 1637453606,
"narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=", "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "06552b72346632b6943c8032e57e702ea12413bf", "rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration", "nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
@ -115,11 +99,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1634869268, "lastModified": 1638497756,
"narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=", "narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "c02c2d86354327317546501af001886fbb53d374", "rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -9,10 +9,6 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay"; inputs.rustOverlay.follows = "rust-overlay";
}; };
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
}; };
outputs = inputs@{ self, nixCargoIntegration, ... }: outputs = inputs@{ self, nixCargoIntegration, ... }:
@ -63,7 +59,7 @@
''; '';
}; };
shell = common: prev: { 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 ++ [ env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; } { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; } { name = "RUST_BACKTRACE"; value = "1"; }

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

@ -2,6 +2,7 @@
//! this module provides the functionality to insert the paired closing character. //! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction}; use crate::{Range, Rope, Selection, Tendril, Transaction};
use log::debug;
use smallvec::SmallVec; use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/ // 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: // insert hook:
// Fn(doc, selection, char) => Option<Transaction> // 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 // 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] #[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> { 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 { for &(open, close) in PAIRS {
if open == ch { if open == ch {
if open == close { if open == close {
return handle_same(doc, selection, open); return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
} else { } else {
return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE)); return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
} }
} }
if close == ch { if close == ch {
// && char_at pos == close // && char_at pos == close
return Some(handle_close(doc, selection, open, close)); return Some(handle_close(doc, &cursors, open, close));
} }
} }
None None
} }
// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close ' fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
// for example "&'a mut", or "fn<'a>" if pos == 0 {
fn next_char(doc: &Rope, pos: usize) -> Option<char> {
if pos >= doc.len_chars() {
return None; 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( fn handle_open(
doc: &Rope, doc: &Rope,
selection: &Selection, selection: &Selection,
@ -66,98 +73,362 @@ fn handle_open(
close: char, close: char,
close_before: &str, close_before: &str,
) -> Transaction { ) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len()); let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0; let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let pos = range.head; let start_head = start_range.head;
let next = next_char(doc, pos);
let head = pos + offs + open.len_utf8(); let next = doc.get_char(start_head);
// if selection, retain anchor, if cursor, move over let end_head = start_head + offs + open.len_utf8();
ranges.push(Range::new(
if range.is_empty() { let end_anchor = if start_range.is_empty() {
head end_head
} else { } else {
range.anchor + offs start_range.anchor + offs
}, };
head,
)); end_ranges.push(Range::new(end_anchor, end_head));
match next { match next {
Some(ch) if !close_before.contains(ch) => { Some(ch) if !close_before.contains(ch) => {
offs += 1; offs += open.len_utf8();
// TODO: else return (use default handler that inserts open) (start_head, start_head, Some(Tendril::from_char(open)))
(pos, pos, Some(Tendril::from_char(open)))
} }
// None | Some(ch) if close_before.contains(ch) => {} // None | Some(ch) if close_before.contains(ch) => {}
_ => { _ => {
// insert open & close // insert open & close
let mut pair = Tendril::with_capacity(2); let pair = Tendril::from_iter([open, close]);
pair.push_char(open); offs += open.len_utf8() + close.len_utf8();
pair.push_char(close); (start_head, start_head, Some(pair))
offs += 2;
(pos, pos, 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 { 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 mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let pos = range.head; let start_head = start_range.head;
let next = next_char(doc, pos); let next = doc.get_char(start_head);
let end_head = start_head + offs + close.len_utf8();
let head = pos + offs + close.len_utf8(); let end_anchor = if start_range.is_empty() {
// if selection, retain anchor, if cursor, move over end_head
ranges.push(Range::new(
if range.is_empty() {
head
} else { } else {
range.anchor + offs start_range.anchor + offs
}, };
head,
)); end_ranges.push(Range::new(end_anchor, end_head));
if next == Some(close) { if next == Some(close) {
// return transaction that moves past close // return transaction that moves past close
(pos, pos, None) // no-op (start_head, start_head, None) // no-op
} else { } else {
offs += close.len_utf8(); offs += close.len_utf8();
(start_head, start_head, Some(Tendril::from_char(close)))
}
});
// TODO: else return (use default handler that inserts close) transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
(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""") #[cfg(test)]
fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> { mod test {
// if not cursor but selection, wrap use super::*;
// let next = next char use smallvec::smallvec;
// if next == bracket { fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
// // if start of syntax node, insert token twice (new pair because node is complete) PAIRS.iter().filter(|(open, close)| open != close)
// // elseif colsedBracketAt }
// // is_triple == allow triple && next 3 is equal
// // cursor jump over fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
// } PAIRS.iter().filter(|(open, close)| open == close)
//} else if allow_triple && followed by triple { }
//}
//} else if next != word char && prev != bracket && prev != word char { fn test_hooks(
// // condition checks for cases like I' where you don't want I'' (or I'm) in_doc: &Rope,
// insert pair ("") in_sel: &Selection,
//} ch: char,
None 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. //! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`]. /// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Severity { pub enum Severity {
Error, Error,
Warning, 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) /// 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 struct Diagnostic {
pub range: Range, pub range: Range,
pub line: usize, 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 ropey::RopeSlice;
use super::Increment;
use crate::{ use crate::{
textobject::{textobject_word, TextObject}, textobject::{textobject_word, TextObject},
Range, Tendril, Range, Tendril,
@ -9,9 +11,9 @@ use crate::{
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> { pub struct NumberIncrementor<'a> {
pub range: Range, value: i64,
pub value: i64, radix: u32,
pub radix: u32, range: Range,
text: RopeSlice<'a>, text: RopeSlice<'a>,
} }
@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> {
text, text,
}) })
} }
}
/// Add `amount` to the number and return the formatted text. impl<'a> Increment for NumberIncrementor<'a> {
pub fn incremented_text(&self, amount: i64) -> Tendril { fn increment(&self, amount: i64) -> (Range, Tendril) {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into(); let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len(); let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount); 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!( assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range) NumberIncrementor::from_range(rope.slice(..), range)
.unwrap() .unwrap()
.incremented_text(amount), .increment(amount)
.1,
expected.into() expected.into()
); );
} }
@ -392,7 +396,8 @@ mod test {
assert_eq!( assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range) NumberIncrementor::from_range(rope.slice(..), range)
.unwrap() .unwrap()
.incremented_text(amount), .increment(amount)
.1,
expected.into() expected.into()
); );
} }
@ -419,7 +424,8 @@ mod test {
assert_eq!( assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range) NumberIncrementor::from_range(rope.slice(..), range)
.unwrap() .unwrap()
.incremented_text(amount), .increment(amount)
.1,
expected.into() expected.into()
); );
} }
@ -464,7 +470,8 @@ mod test {
assert_eq!( assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range) NumberIncrementor::from_range(rope.slice(..), range)
.unwrap() .unwrap()
.incremented_text(amount), .increment(amount)
.1,
expected.into() expected.into()
); );
} }
@ -491,7 +498,8 @@ mod test {
assert_eq!( assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range) NumberIncrementor::from_range(rope.slice(..), range)
.unwrap() .unwrap()
.incremented_text(amount), .increment(amount)
.1,
expected.into() expected.into()
); );
} }

@ -5,18 +5,19 @@ pub mod diagnostic;
pub mod diff; pub mod diff;
pub mod graphemes; pub mod graphemes;
pub mod history; pub mod history;
pub mod increment;
pub mod indent; pub mod indent;
pub mod line_ending; pub mod line_ending;
pub mod macros; pub mod macros;
pub mod match_brackets; pub mod match_brackets;
pub mod movement; pub mod movement;
pub mod numbers;
pub mod object; pub mod object;
pub mod path; pub mod path;
mod position; mod position;
pub mod register; pub mod register;
pub mod search; pub mod search;
pub mod selection; pub mod selection;
pub mod shellwords;
mod state; mod state;
pub mod surround; pub mod surround;
pub mod syntax; pub mod syntax;
@ -158,7 +159,7 @@ mod merge_toml_tests {
"; ";
let base: Value = toml::from_slice(include_bytes!("../../languages.toml")) 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 user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user); let merged = merge_toml_values(base, user);

@ -15,8 +15,12 @@ impl Register {
} }
pub fn new_with_values(name: char, values: Vec<String>) -> Self { pub fn new_with_values(name: char, values: Vec<String>) -> Self {
if name == '_' {
Self::new(name)
} else {
Self { name, values } Self { name, values }
} }
}
pub const fn name(&self) -> char { pub const fn name(&self) -> char {
self.name self.name
@ -27,13 +31,17 @@ impl Register {
} }
pub fn write(&mut self, values: Vec<String>) { pub fn write(&mut self, values: Vec<String>) {
if self.name != '_' {
self.values = values; self.values = values;
} }
}
pub fn push(&mut self, value: String) { pub fn push(&mut self, value: String) {
if self.name != '_' {
self.values.push(value); self.values.push(value);
} }
} }
}
/// Currently just wraps a `HashMap` of `Register`s /// Currently just wraps a `HashMap` of `Register`s
#[derive(Debug, Default)] #[derive(Debug, Default)]

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

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

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

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

@ -337,7 +337,10 @@ impl Registry {
}) })
.await; .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> // next up, notify<initialized>
_client _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" repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com" homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"] include = ["src/**/*", "README.md"]
default-run = "hx"
[package.metadata.nix] [package.metadata.nix]
build = true build = true

@ -76,17 +76,27 @@ impl Application {
None => Ok(def_lang_conf), None => Ok(def_lang_conf),
}; };
let theme = if let Some(theme) = &config.theme { let true_color = config.editor.true_color || crate::true_color();
match theme_loader.load(theme) { let theme = config
Ok(theme) => theme, .theme
Err(e) => { .as_ref()
.and_then(|theme| {
theme_loader
.load(theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, 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() theme_loader.default()
}
}
} else { } else {
theme_loader.default() theme_loader.base16_default()
}; }
});
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.and_then(|conf| conf.try_into()) .and_then(|conf| conf.try_into())
@ -265,7 +275,7 @@ impl Application {
use crate::commands::{insert::idle_completion, Context}; use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode; 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; return;
} }
let editor_view = self 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 crossterm::event::Event;
use tui::buffer::Buffer as Surface; 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(), // --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer. // .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. /// 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. /// 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)> { 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 None
} }
fn type_name(&self) -> &'static str { fn type_name(&self) -> &'static str {
std::any::type_name::<Self>() std::any::type_name::<Self>()
} }
fn id(&self) -> Option<&'static str> {
None
}
} }
use anyhow::Error; use anyhow::Error;
@ -126,12 +131,17 @@ impl Compositor {
} }
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { 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 // propagate events through the layers until we either find a layer that consumes it or we
// run out of layers (event bubbling) // run out of layers (event bubbling)
for layer in self.layers.iter_mut().rev() { for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event, cx) { match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => { EventResult::Consumed(Some(callback)) => {
callback(self); callback(self, cx);
return true; return true;
} }
EventResult::Consumed(None) => return true, EventResult::Consumed(None) => return true,
@ -184,6 +194,14 @@ impl Compositor {
.find(|component| component.type_name() == type_name) .find(|component| component.type_name() == type_name)
.and_then(|component| component.as_any_mut().downcast_mut()) .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 // 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 crate::config::Config;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent}; use helix_view::{document::Mode, info::Info, input::KeyEvent};
@ -92,7 +92,7 @@ macro_rules! alt {
#[macro_export] #[macro_export]
macro_rules! keymap { macro_rules! keymap {
(@trie $cmd:ident) => { (@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd) $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
}; };
(@trie (@trie
@ -120,7 +120,7 @@ macro_rules! keymap {
_key, _key,
keymap!(@trie $value) 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); _order.push(_key);
)+ )+
)* )*
@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum KeyTrie { pub enum KeyTrie {
Leaf(Command), Leaf(MappableCommand),
Sequence(Vec<Command>), Sequence(Vec<MappableCommand>),
Node(KeyTrieNode), Node(KeyTrieNode),
} }
@ -304,9 +304,9 @@ impl KeyTrie {
pub enum KeymapResultKind { pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke. /// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode), Pending(KeyTrieNode),
Matched(Command), Matched(MappableCommand),
/// Matched a sequence of commands to execute. /// Matched a sequence of commands to execute.
MatchedSequence(Vec<Command>), MatchedSequence(Vec<MappableCommand>),
/// Key was not found in the root keymap /// Key was not found in the root keymap
NotFound, NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto /// 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]) { let trie = match trie_node.search(&[*first]) {
Some(&KeyTrie::Leaf(cmd)) => { Some(KeyTrie::Leaf(ref cmd)) => {
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()) return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
} }
Some(&KeyTrie::Sequence(ref cmds)) => { Some(KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new( return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()), KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(), self.sticky(),
@ -408,9 +408,9 @@ impl Keymap {
} }
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky()) KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
} }
Some(&KeyTrie::Leaf(cmd)) => { Some(&KeyTrie::Leaf(ref cmd)) => {
self.state.clear(); 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)) => { Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear(); self.state.clear();
@ -512,6 +512,7 @@ impl Default for Keymaps {
"g" => { "Goto" "g" => { "Goto"
"g" => goto_file_start, "g" => goto_file_start,
"e" => goto_last_line, "e" => goto_last_line,
"f" => goto_file,
"h" => goto_line_start, "h" => goto_line_start,
"l" => goto_line_end, "l" => goto_line_end,
"s" => goto_first_nonwhitespace, "s" => goto_first_nonwhitespace,
@ -520,9 +521,10 @@ impl Default for Keymaps {
"r" => goto_reference, "r" => goto_reference,
"i" => goto_implementation, "i" => goto_implementation,
"t" => goto_window_top, "t" => goto_window_top,
"m" => goto_window_middle, "c" => goto_window_center,
"b" => goto_window_bottom, "b" => goto_window_bottom,
"a" => goto_last_accessed_file, "a" => goto_last_accessed_file,
"m" => goto_last_modified_file,
"n" => goto_next_buffer, "n" => goto_next_buffer,
"p" => goto_previous_buffer, "p" => goto_previous_buffer,
"." => goto_last_modification, "." => goto_last_modification,
@ -537,9 +539,9 @@ impl Default for Keymaps {
"O" => open_above, "O" => open_above,
"d" => delete_selection, "d" => delete_selection,
// TODO: also delete without yanking "A-d" => delete_selection_noyank,
"c" => change_selection, "c" => change_selection,
// TODO: also change delete without yanking "A-c" => change_selection_noyank,
"C" => copy_selection_on_next_line, "C" => copy_selection_on_next_line,
"A-C" => copy_selection_on_prev_line, "A-C" => copy_selection_on_prev_line,
@ -591,6 +593,9 @@ impl Default for Keymaps {
// paste_all // paste_all
"P" => paste_before, "P" => paste_before,
"q" => record_macro,
"Q" => play_macro,
">" => indent, ">" => indent,
"<" => unindent, "<" => unindent,
"=" => format_selections, "=" => format_selections,
@ -622,6 +627,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit, "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit, "C-v" | "v" => vsplit,
"f" => goto_file_hsplit,
"F" => goto_file_vsplit,
"C-q" | "q" => wclose, "C-q" | "q" => wclose,
"C-o" | "o" => wonly, "C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left, "C-h" | "h" | "left" => jump_view_left,
@ -637,7 +644,7 @@ impl Default for Keymaps {
"tab" => jump_forward, // tab == <C-i> "tab" => jump_forward, // tab == <C-i>
"C-o" => jump_backward, "C-o" => jump_backward,
// "C-s" => save_selection, "C-s" => save_selection,
"space" => { "Space" "space" => { "Space"
"f" => file_picker, "f" => file_picker,
@ -650,6 +657,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit, "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit, "C-v" | "v" => vsplit,
"f" => goto_file_hsplit,
"F" => goto_file_vsplit,
"C-q" | "q" => wclose, "C-q" | "q" => wclose,
"C-o" | "o" => wonly, "C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left, "C-h" | "h" | "left" => jump_view_left,
@ -827,36 +836,36 @@ mod tests {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!( assert_eq!(
keymap.get(key!('i')).kind, keymap.get(key!('i')).kind,
KeymapResultKind::Matched(Command::normal_mode), KeymapResultKind::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf" "Leaf should replace leaf"
); );
assert_eq!( assert_eq!(
keymap.get(key!('无')).kind, keymap.get(key!('无')).kind,
KeymapResultKind::Matched(Command::insert_mode), KeymapResultKind::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap" "New leaf should be present in merged keymap"
); );
// Assumes that z is a node in the default keymap // Assumes that z is a node in the default keymap
assert_eq!( assert_eq!(
keymap.get(key!('z')).kind, keymap.get(key!('z')).kind,
KeymapResultKind::Matched(Command::jump_backward), KeymapResultKind::Matched(MappableCommand::jump_backward),
"Leaf should replace node" "Leaf should replace node"
); );
// Assumes that `g` is a node in default keymap // Assumes that `g` is a node in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(), keymap.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" "Leaf should be present in merged subnode"
); );
// Assumes that `gg` is in default keymap // Assumes that `gg` is in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(), keymap.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" "Leaf should replace old leaf in merged subnode"
); );
// Assumes that `ge` is in default keymap // Assumes that `ge` is in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(), keymap.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" "Old leaves in subnode should be present in merged node"
); );
@ -890,7 +899,7 @@ mod tests {
.root() .root()
.search(&[key!(' '), key!('s'), key!('v')]) .search(&[key!(' '), key!('s'), key!('v')])
.unwrap(), .unwrap(),
&KeyTrie::Leaf(Command::vsplit), &KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode" "Leaf should be present in merged subnode"
); );
// Make sure an order was set during merge // Make sure an order was set during merge

@ -9,3 +9,14 @@ pub mod config;
pub mod job; pub mod job;
pub mod keymap; pub mod keymap;
pub mod ui; 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 { let mut completion = Self {
popup, popup,
start_offset, start_offset,
@ -328,8 +328,8 @@ impl Component for Completion {
let y = popup_y; let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
width = rel_width; width = rel_width.min(width);
height = rel_height; height = rel_height.min(height);
} }
Rect::new(x, y, width, height) Rect::new(x, y, width, height)
} else { } else {

@ -17,7 +17,6 @@ use helix_core::{
}; };
use helix_view::{ use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SCRATCH_BUFFER_NAME},
editor::LineNumber,
graphics::{CursorKind, Modifier, Rect, Style}, graphics::{CursorKind, Modifier, Rect, Style},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
@ -32,7 +31,7 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView { pub struct EditorView {
keymaps: Keymaps, keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, 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>, pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
autoinfo: Option<Info>, autoinfo: Option<Info>,
@ -49,7 +48,7 @@ impl EditorView {
Self { Self {
keymaps, keymaps,
on_next_key: None, on_next_key: None,
last_insert: (commands::Command::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
autoinfo: None, autoinfo: None,
@ -310,17 +309,16 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes}; 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) { for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < offset.col as u16 let out_of_bounds = visual_x < offset.col as u16
|| visual_x >= viewport.width + offset.col as u16; || visual_x >= viewport.width + offset.col as u16;
if LineEnding::from_rope_slice(&grapheme).is_some() { if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds { 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 // we still want to render an empty cell with the style
surface.set_string( surface.set_string(
viewport.x + visual_x - offset.col as u16, viewport.x + visual_x - offset.col as u16,
@ -351,6 +349,10 @@ impl EditorView {
}; };
if !out_of_bounds { 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 // if we're offscreen just keep going until we hit a new line
surface.set_string( surface.set_string(
viewport.x + visual_x - offset.col as u16, viewport.x + visual_x - offset.col as u16,
@ -417,22 +419,6 @@ impl EditorView {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let last_line = view.last_line(doc); 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: // it's used inside an iterator so the collect isn't needless:
// https://github.com/rust-lang/rust-clippy/issues/6164 // https://github.com/rust-lang/rust-clippy/issues/6164
#[allow(clippy::needless_collect)] #[allow(clippy::needless_collect)]
@ -442,52 +428,32 @@ impl EditorView {
.map(|range| range.cursor_line(text)) .map(|range| range.cursor_line(text))
.collect(); .collect();
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() { let mut offset = 0;
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) { let gutter_style = theme.get("ui.gutter");
surface.set_stringn(
viewport.x, // avoid lots of small allocations by reusing a text buffer for each line
viewport.y + i as u16, let mut text = String::with_capacity(8);
"●",
1,
match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
}
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); let selected = cursors.contains(&line);
let text = if line == last_line && !draw_last { if let Some(style) = gutter(line, selected, &mut text) {
" ~".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( surface.set_stringn(
viewport.x + 1, viewport.x + offset,
viewport.y + i as u16, viewport.y + i as u16,
text, &text,
5, *width,
if selected && is_focused { gutter_style.patch(style),
linenr_select
} else {
linenr
},
); );
} }
text.clear();
}
offset += *width as u16;
}
} }
pub fn render_diagnostics( pub fn render_diagnostics(
@ -916,7 +882,7 @@ impl EditorView {
return EventResult::Ignored; 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) EventResult::Consumed(None)
} }
@ -934,7 +900,8 @@ impl EditorView {
} }
if modifiers == crossterm::event::KeyModifiers::ALT { 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); return EventResult::Consumed(None);
} }
@ -948,7 +915,7 @@ impl EditorView {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap(); let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos)); doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id; 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); return EventResult::Consumed(None);
} }
@ -963,7 +930,7 @@ impl EditorView {
impl Component for EditorView { impl Component for EditorView {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let mut cxt = commands::Context { let mut cxt = commands::Context {
editor: &mut cx.editor, editor: cx.editor,
count: None, count: None,
register: None, register: None,
callback: None, callback: None,
@ -1140,13 +1107,31 @@ impl Component for EditorView {
disp.push_str(&s); 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( 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), area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..) disp.get(disp.len().saturating_sub(key_width as usize)..)
.unwrap_or(&disp), .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() { if let Some(completion) = self.completion.as_mut() {
@ -1172,12 +1157,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT) 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; return None;
} }
let contents = parse(&self.contents, None, &self.config_loader); let contents = parse(&self.contents, None, &self.config_loader);
// TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120); let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0; let mut text_width = 0;
let mut height = padding; let mut height = padding;
@ -240,11 +241,6 @@ impl Component for Markdown {
} else if content_width > text_width { } else if content_width > text_width {
text_width = content_width; text_width = content_width;
} }
if height >= viewport.1 {
height = viewport.1;
break;
}
} }
Some((text_width + padding, height)) Some((text_width + padding, height))

@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); compositor.pop();
}))); })));
@ -202,7 +202,7 @@ impl<T: Item + 'static> Component for Menu<T> {
return close_fn; return close_fn;
} }
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) // 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.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None); return EventResult::Consumed(None);

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

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

@ -15,16 +15,20 @@ pub struct Popup<T: Component> {
contents: T, contents: T,
position: Option<Position>, position: Option<Position>,
size: (u16, u16), size: (u16, u16),
child_size: (u16, u16),
scroll: usize, scroll: usize,
id: &'static str,
} }
impl<T: Component> Popup<T> { impl<T: Component> Popup<T> {
pub fn new(contents: T) -> Self { pub fn new(id: &'static str, contents: T) -> Self {
Self { Self {
contents, contents,
position: None, position: None,
size: (0, 0), size: (0, 0),
child_size: (0, 0),
scroll: 0, scroll: 0,
id,
} }
} }
@ -68,6 +72,9 @@ impl<T: Component> Popup<T> {
pub fn scroll(&mut self, offset: usize, direction: bool) { pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction { if direction {
self.scroll += offset; 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 { } else {
self.scroll = self.scroll.saturating_sub(offset); self.scroll = self.scroll.saturating_sub(offset);
} }
@ -93,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored, _ => 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 // remove the layer
compositor.pop(); 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. // 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 let (width, height) = self
.contents .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"); .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) Some(self.size)
} }
@ -143,4 +158,8 @@ impl<T: Component> Component for Popup<T> {
self.contents.render(area, surface, cx); 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, _ => 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 // remove the layer
compositor.pop(); compositor.pop();
}))); })));
@ -505,7 +505,7 @@ impl Component for Prompt {
self.change_completion_selection(CompletionDirection::Forward); self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update) (self.callback_fn)(cx, &self.line, PromptEvent::Update)
} }
shift!(BackTab) => { shift!(Tab) => {
self.change_completion_selection(CompletionDirection::Backward); self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update) (self.callback_fn)(cx, &self.line, PromptEvent::Update)
} }

@ -102,7 +102,7 @@ impl Default for Cell {
/// buf.get_mut(5, 0).set_char('x'); /// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x"); /// assert_eq!(buf.get(5, 0).symbol, "x");
/// ``` /// ```
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Default, Clone, PartialEq)]
pub struct Buffer { pub struct Buffer {
/// The area represented by this buffer /// The area represented by this buffer
pub area: Rect, pub area: Rect,
@ -111,15 +111,6 @@ pub struct Buffer {
pub content: Vec<Cell>, pub content: Vec<Cell>,
} }
impl Default for Buffer {
fn default() -> Buffer {
Buffer {
area: Default::default(),
content: Vec::new(),
}
}
}
impl Buffer { impl Buffer {
/// Returns a Buffer with all cells set to the default one /// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer { 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. /// 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>>); 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> { impl<'a> Spans<'a> {
/// Returns the width of the underlying string. /// 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)); /// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height()); /// assert_eq!(6, text.height());
/// ``` /// ```
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Default, Clone, PartialEq)]
pub struct Text<'a> { pub struct Text<'a> {
pub lines: Vec<Spans<'a>>, pub lines: Vec<Spans<'a>>,
} }
impl<'a> Default for Text<'a> {
fn default() -> Text<'a> {
Text { lines: Vec::new() }
}
}
impl<'a> Text<'a> { impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style. /// 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 struct TableState {
pub offset: usize, pub offset: usize,
pub selected: Option<usize>, pub selected: Option<usize>,
} }
impl Default for TableState {
fn default() -> TableState {
TableState {
offset: 0,
selected: None,
}
}
}
impl TableState { impl TableState {
pub fn selected(&self) -> Option<usize> { pub fn selected(&self) -> Option<usize> {
self.selected self.selected

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

@ -2,6 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SCRATCH_BUFFER_NAME},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
input::KeyEvent,
theme::{self, Theme}, theme::{self, Theme},
tree::{self, Tree}, tree::{self, Tree},
Document, DocumentId, View, ViewId, Document, DocumentId, View, ViewId,
@ -11,6 +12,7 @@ use futures_util::future;
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
io::stdin, io::stdin,
num::NonZeroUsize,
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
@ -18,7 +20,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep}; use tokio::time::{sleep, Duration, Instant, Sleep};
use anyhow::Error; use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers; pub use helix_core::register::Registers;
@ -105,6 +107,8 @@ pub struct Config {
pub file_picker: FilePickerConfig, pub file_picker: FilePickerConfig,
/// Shape for cursor in each mode /// Shape for cursor in each mode
pub cursor_shape: CursorShapeConfig, 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 // 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")] #[serde(rename_all = "kebab-case")]
pub enum LineNumber { pub enum LineNumber {
/// Show absolute line number /// Show absolute line number
@ -171,6 +175,7 @@ impl Default for Config {
auto_info: true, auto_info: true,
file_picker: FilePickerConfig::default(), file_picker: FilePickerConfig::default(),
cursor_shape: CursorShapeConfig::default(), cursor_shape: CursorShapeConfig::default(),
true_color: false,
} }
} }
} }
@ -190,11 +195,12 @@ impl std::fmt::Debug for Motion {
#[derive(Debug)] #[derive(Debug)]
pub struct Editor { pub struct Editor {
pub tree: Tree, pub tree: Tree,
pub next_document_id: usize, pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>, pub documents: BTreeMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>, pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>, pub selected_register: Option<char>,
pub registers: Registers, pub registers: Registers,
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme, pub theme: Theme,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>, pub clipboard_provider: Box<dyn ClipboardProvider>,
@ -223,8 +229,8 @@ pub enum Action {
impl Editor { impl Editor {
pub fn new( pub fn new(
mut area: Rect, mut area: Rect,
themes: Arc<theme::Loader>, theme_loader: Arc<theme::Loader>,
config_loader: Arc<syntax::Loader>, syn_loader: Arc<syntax::Loader>,
config: Config, config: Config,
) -> Self { ) -> Self {
let language_servers = helix_lsp::Registry::new(); let language_servers = helix_lsp::Registry::new();
@ -234,14 +240,15 @@ impl Editor {
Self { Self {
tree: Tree::new(area), tree: Tree::new(area),
next_document_id: 0, next_document_id: DocumentId::default(),
documents: BTreeMap::new(), documents: BTreeMap::new(),
count: None, count: None,
selected_register: None, selected_register: None,
theme: themes.default(), macro_recording: None,
theme: theme_loader.default(),
language_servers, language_servers,
syn_loader: config_loader, syn_loader,
theme_loader: themes, theme_loader,
registers: Registers::default(), registers: Registers::default(),
clipboard_provider: get_clipboard_provider(), clipboard_provider: get_clipboard_provider(),
status_msg: None, status_msg: None,
@ -297,14 +304,51 @@ impl Editor {
self._refresh(); self._refresh();
} }
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> { /// Refreshes the language server for a given document
use anyhow::Context; pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let theme = self let doc = self.documents.get_mut(&doc_id)?;
.theme_loader doc.detect_language(Some(&self.theme), &self.syn_loader);
.load(theme.as_ref()) Self::launch_language_server(&mut self.language_servers, doc)
.with_context(|| format!("failed setting theme `{}`", theme))?; }
self.set_theme(theme);
Ok(()) /// 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) { fn _refresh(&mut self) {
@ -358,7 +402,8 @@ impl Editor {
.tree .tree
.traverse() .traverse()
.any(|(_, v)| v.doc == doc.id && v.id != view.id); .any(|(_, v)| v.doc == doc.id && v.id != view.id);
let view = view_mut!(self);
let (view, doc) = current!(self);
if remove_empty_scratch { if remove_empty_scratch {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// borrow, invalidating direct access to `doc.id`. // borrow, invalidating direct access to `doc.id`.
@ -367,7 +412,16 @@ impl Editor {
} else { } else {
let jump = (view.doc, doc.selection(view.id).clone()); let jump = (view.doc, doc.selection(view.id).clone());
view.jumps.push(jump); view.jumps.push(jump);
// Set last accessed doc if it is a different document
if doc.id != id {
view.last_accessed_doc = Some(view.doc); 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; let view_id = view.id;
@ -377,23 +431,22 @@ impl Editor {
} }
Action::Load => { Action::Load => {
let view_id = view!(self).id; let view_id = view!(self).id;
if let Some(doc) = self.document_mut(id) { let doc = self.documents.get_mut(&id).unwrap();
if doc.selections().is_empty() { if doc.selections().is_empty() {
doc.selections.insert(view_id, Selection::point(0)); doc.selections.insert(view_id, Selection::point(0));
} }
}
return; return;
} }
Action::HorizontalSplit => { Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id); let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal); let view_id = self.tree.split(
// initialize selection for view view,
let doc = self.documents.get_mut(&id).unwrap(); match action {
doc.selections.insert(view_id, Selection::point(0)); Action::HorizontalSplit => Layout::Horizontal,
} Action::VerticalSplit => Layout::Vertical,
Action::VerticalSplit => { _ => unreachable!(),
let view = View::new(id); },
let view_id = self.tree.split(view, Layout::Vertical); );
// initialize selection for view // initialize selection for view
let doc = self.documents.get_mut(&id).unwrap(); let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0)); doc.selections.insert(view_id, Selection::point(0));
@ -403,16 +456,19 @@ impl Editor {
self._refresh(); self._refresh();
} }
fn new_document(&mut self, mut document: Document) -> DocumentId { /// Generate an id for a new document and register it.
let id = DocumentId(self.next_document_id); fn new_document(&mut self, mut doc: Document) -> DocumentId {
self.next_document_id += 1; let id = self.next_document_id;
document.id = id; // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
self.documents.insert(id, document); 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 id
} }
fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId { fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
let id = self.new_document(document); let id = self.new_document(doc);
self.switch(id, action); self.switch(id, action);
id id
} }
@ -428,54 +484,16 @@ impl Editor {
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> { pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?; let path = helix_core::path::get_canonicalized_path(&path)?;
let id = self.document_by_path(&path).map(|doc| doc.id);
let id = self
.documents()
.find(|doc| doc.path() == Some(&path))
.map(|doc| doc.id);
let id = if let Some(id) = id { let id = if let Some(id) = id {
id id
} else { } else {
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?; 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 _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
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 { self.new_document(doc)
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 id = DocumentId(self.next_document_id);
self.next_document_id += 1;
doc.id = id;
self.documents.insert(id, doc);
id
}; };
self.switch(id, action); self.switch(id, action);
@ -498,11 +516,11 @@ impl Editor {
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> { pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
let doc = match self.documents.get(&doc_id) { let doc = match self.documents.get(&doc_id) {
Some(doc) => doc, Some(doc) => doc,
None => anyhow::bail!("document does not exist"), None => bail!("document does not exist"),
}; };
if !force && doc.is_modified() { if !force && doc.is_modified() {
anyhow::bail!( bail!(
"buffer {:?} is modified", "buffer {:?} is modified",
doc.relative_path() doc.relative_path()
.map(|path| path.to_string_lossy().to_string()) .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 // 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 // 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. // 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 let doc_id = self
.documents .documents
.iter() .iter()
@ -620,8 +638,7 @@ impl Editor {
} }
pub fn cursor(&self) -> (Option<Position>, CursorKind) { pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let view = view!(self); let (view, doc) = current_ref!(self);
let doc = &self.documents[&view.doc];
let cursor = doc let cursor = doc
.selection(view.id) .selection(view.id)
.primary() .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 /// 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. /// 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 struct Rect {
pub x: u16, pub x: u16,
pub y: u16, pub y: u16,
@ -41,17 +41,6 @@ pub struct Rect {
pub height: u16, pub height: u16,
} }
impl Default for Rect {
fn default() -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Rect { impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16. /// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved. /// 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 PAGEUP: &str = "pageup";
pub(crate) const PAGEDOWN: &str = "pagedown"; pub(crate) const PAGEDOWN: &str = "pagedown";
pub(crate) const TAB: &str = "tab"; pub(crate) const TAB: &str = "tab";
pub(crate) const BACKTAB: &str = "backtab";
pub(crate) const DELETE: &str = "del"; pub(crate) const DELETE: &str = "del";
pub(crate) const INSERT: &str = "ins"; pub(crate) const INSERT: &str = "ins";
pub(crate) const NULL: &str = "null"; pub(crate) const NULL: &str = "null";
@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
KeyCode::PageUp => f.write_str(keys::PAGEUP)?, KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?, KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str(keys::TAB)?, KeyCode::Tab => f.write_str(keys::TAB)?,
KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str(keys::DELETE)?, KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str(keys::INSERT)?, KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str(keys::NULL)?, KeyCode::Null => f.write_str(keys::NULL)?,
@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::PageUp => keys::PAGEUP.len(), KeyCode::PageUp => keys::PAGEUP.len(),
KeyCode::PageDown => keys::PAGEDOWN.len(), KeyCode::PageDown => keys::PAGEDOWN.len(),
KeyCode::Tab => keys::TAB.len(), KeyCode::Tab => keys::TAB.len(),
KeyCode::BackTab => keys::BACKTAB.len(),
KeyCode::Delete => keys::DELETE.len(), KeyCode::Delete => keys::DELETE.len(),
KeyCode::Insert => keys::INSERT.len(), KeyCode::Insert => keys::INSERT.len(),
KeyCode::Null => keys::NULL.len(), KeyCode::Null => keys::NULL.len(),
@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
keys::PAGEUP => KeyCode::PageUp, keys::PAGEUP => KeyCode::PageUp,
keys::PAGEDOWN => KeyCode::PageDown, keys::PAGEDOWN => KeyCode::PageDown,
keys::TAB => KeyCode::Tab, keys::TAB => KeyCode::Tab,
keys::BACKTAB => KeyCode::BackTab,
keys::DELETE => KeyCode::Delete, keys::DELETE => KeyCode::Delete,
keys::INSERT => KeyCode::Insert, keys::INSERT => KeyCode::Insert,
keys::NULL => KeyCode::Null, keys::NULL => KeyCode::Null,
@ -220,15 +216,43 @@ impl<'de> Deserialize<'de> for KeyEvent {
#[cfg(feature = "term")] #[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent { impl From<crossterm::event::KeyEvent> for KeyEvent {
fn from( fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent, if code == crossterm::event::KeyCode::BackTab {
) -> KeyEvent { // special case for BackTab -> Shift-Tab
KeyEvent { let mut modifiers: KeyModifiers = modifiers.into();
modifiers.insert(KeyModifiers::SHIFT);
Self {
code: KeyCode::Tab,
modifiers,
}
} else {
Self {
code: code.into(), code: code.into(),
modifiers: modifiers.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(),
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {

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

@ -5,6 +5,7 @@ pub mod clipboard;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod graphics; pub mod graphics;
pub mod gutter;
pub mod info; pub mod info;
pub mod input; pub mod input;
pub mod keyboard; pub mod keyboard;
@ -12,8 +13,18 @@ pub mod theme;
pub mod tree; pub mod tree;
pub mod view; pub mod view;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] use std::num::NonZeroUsize;
pub struct DocumentId(usize);
// 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! { slotmap::new_key_type! {
pub struct ViewId; pub struct ViewId;

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

@ -1,6 +1,10 @@
use std::borrow::Cow; use std::borrow::Cow;
use crate::{graphics::Rect, Document, DocumentId, ViewId}; use crate::{
graphics::Rect,
gutter::{self, Gutter},
Document, DocumentId, ViewId,
};
use helix_core::{ use helix_core::{
graphemes::{grapheme_width, RopeGraphemes}, graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index, 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)] #[derive(Debug)]
pub struct View { pub struct View {
pub id: ViewId, pub id: ViewId,
@ -69,6 +75,11 @@ pub struct View {
pub jumps: JumpList, pub jumps: JumpList,
/// the last accessed file before the current one /// the last accessed file before the current one
pub last_accessed_doc: Option<DocumentId>, 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 { impl View {
@ -80,13 +91,23 @@ impl View {
area: Rect::default(), // will get calculated upon inserting into tree area: Rect::default(), // will get calculated upon inserting into tree
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
last_accessed_doc: None, last_accessed_doc: None,
last_modified_docs: [None, None],
} }
} }
pub fn gutters(&self) -> &[(Gutter, usize)] {
GUTTERS
}
pub fn inner_area(&self) -> Rect { pub fn inner_area(&self) -> Rect {
// TODO: not ideal // TODO: cache this
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter let offset = self
self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline .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 super::*;
use helix_core::Rope; use helix_core::Rope;
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
#[test] #[test]
fn test_text_pos_at_screen_coords() { fn test_text_pos_at_screen_coords() {

@ -11,6 +11,7 @@ indent = { tab-width = 4, unit = " " }
[language.config] [language.config]
cargo = { loadOutDirsFromCheck = true } cargo = { loadOutDirsFromCheck = true }
procMacro = { enable = false } procMacro = { enable = false }
diagnostics = { disabled = ["unresolved-proc-macro"] }
[[language]] [[language]]
name = "toml" name = "toml"
@ -249,7 +250,6 @@ language-server = { command = "julia", args = [
using Pkg; using Pkg;
import StaticLint; import StaticLint;
env_path = dirname(Pkg.Types.Context().env.project_file); env_path = dirname(Pkg.Types.Context().env.project_file);
server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
server.runlinter = true; server.runlinter = true;
run(server); run(server);
@ -396,3 +396,37 @@ shebangs = ["perl"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } 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 (constructor) @constructor
(pragma) @pragma (pragma) @pragma
(comment) @comment (comment) @comment
(signature name: (variable) @fun_type_name) (signature name: (variable) @type)
(function name: (variable) @function) (function name: (variable) @function)
(constraint class: (class_name (type)) @class) (constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class)) (class (class_head class: (class_name (type)) @class))

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

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

@ -12,7 +12,7 @@
((account) @variable.other.member) ((account) @variable.other.member)
((commodity) @text.literal) ((commodity) @text.literal)
"include" @include "include" @keyword.local.import
[ [
"account" "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 ["exception" "try"] @keyword.control.exception
["include" "open"] @include ["include" "open"] @keyword.control.import
["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat ["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
(attribute_name) @_attr (attribute_name) @_attr
(quoted_attribute_value (attribute_value) @markup.undeline.link)) (quoted_attribute_value (attribute_value) @markup.underline.link))
(#match? @_attr "^(href|src)$")) (#match? @_attr "^(href|src)$"))
(tag_name) @tag (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

@ -2,26 +2,27 @@
"ui.background" = { bg = "base00" } "ui.background" = { bg = "base00" }
"ui.menu" = "base01" "ui.menu" = "base01"
"ui.menu.selected" = { fg = "base04", bg = "base01" } "ui.menu.selected" = { fg = "base01", bg = "base04" }
"ui.linenr" = {fg = "base01" } "ui.linenr" = { fg = "base03", bg = "base01" }
"ui.popup" = { bg = "base01" } "ui.popup" = { bg = "base01" }
"ui.window" = { bg = "base01" } "ui.window" = { bg = "base01" }
"ui.liner.selected" = "base02" "ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
"ui.selection" = "base02" "ui.selection" = { bg = "base02" }
"comment" = "base03" "comment" = { fg = "base03", modifiers = ["italic"] }
"ui.statusline" = { fg = "base04", bg = "base01" } "ui.statusline" = { fg = "base04", bg = "base01" }
"ui.help" = { fg = "base04", bg = "base01" } "ui.help" = { fg = "base04", bg = "base01" }
"ui.cursor" = { fg = "base05", modifiers = ["reversed"] } "ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
"ui.text" = { fg = "base05" } "ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
"ui.text" = "base05"
"operator" = "base05" "operator" = "base05"
"ui.text.focus" = { fg = "base05" } "ui.text.focus" = "base05"
"variable" = "base08" "variable" = "base08"
"constant.numeric" = "base09" "constant.numeric" = "base09"
"constant" = "base09" "constant" = "base09"
"attributes" = "base09" "attributes" = "base09"
"type" = "base0A" "type" = "base0A"
"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] } "ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
"strings" = "base0B" "string" = "base0B"
"variable.other.member" = "base0B" "variable.other.member" = "base0B"
"constant.character.escape" = "base0C" "constant.character.escape" = "base0C"
"function" = "base0D" "function" = "base0D"
@ -30,15 +31,15 @@
"keyword" = "base0E" "keyword" = "base0E"
"label" = "base0E" "label" = "base0E"
"namespace" = "base0E" "namespace" = "base0E"
"ui.popup" = { bg = "base01" } "ui.help" = { fg = "base06", bg = "base01" }
"ui.window" = { bg = "base00" }
"ui.help" = { bg = "base01", fg = "base06" }
"info" = "base03" "diagnostic" = { modifiers = ["underlined"] }
"ui.gutter" = { bg = "base01" }
"info" = "base0D"
"hint" = "base03" "hint" = "base03"
"debug" = "base03" "debug" = "base03"
"diagnostic" = "base03" "warning" = "base09"
"error" = "base0E" "error" = "base08"
[palette] [palette]
base00 = "#181818" # Default Background 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" "ui.selection" = "highlight"
"comment" = "subtle" "comment" = "subtle"
"ui.statusline" = {fg = "foam", bg = "surface" } "ui.statusline" = {fg = "foam", bg = "surface" }
"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
"ui.help" = { fg = "foam", bg = "surface" } "ui.help" = { fg = "foam", bg = "surface" }
"ui.cursor" = { fg = "rose", modifiers = ["reversed"] } "ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
"ui.text" = { fg = "text" } "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"

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

@ -59,12 +59,12 @@
# 主光标/selectio # 主光标/selectio
"ui.cursor.primary" = { fg = "base03", bg = "base1" } "ui.cursor.primary" = { fg = "base03", bg = "base1" }
"ui.selection.primary" = { fg = "base03", bg = "base01" } "ui.cursor.select" = { fg = "base02", bg = "cyan" }
"ui.cursor.select" = {fg = "base02", bg = "green"} "ui.selection" = { bg = "base0175" }
"ui.selection" = { fg = "base02", bg = "yellow" } "ui.selection.primary" = { bg = "base015" }
# normal模式的光标 # normal模式的光标
"ui.cursor" = {fg = "base03", bg = "green"} "ui.cursor" = {fg = "base02", bg = "cyan"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"} "ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号 # 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]} "ui.cursor.match" = {modifiers = ["reversed"]}
@ -73,7 +73,7 @@
"error" = { fg = "red", modifiers= ["bold", "underlined"] } "error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] } "info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] } "hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { mdifiers = ["underlined"] } "diagnostic" = { modifiers = ["underlined"] }
[palette] [palette]
red = '#dc322f' red = '#dc322f'
@ -94,5 +94,7 @@ base3 = '#002b36'
## 浅色 越來越浅 ## 浅色 越來越浅
base00 = '#839496' base00 = '#839496'
base01 = '#93a1a1' base01 = '#93a1a1'
base015 = '#c5c8bd'
base0175 = '#dddbcc'
base02 = '#eee8d5' base02 = '#eee8d5'
base03 = '#fdf6e3' base03 = '#fdf6e3'

@ -28,6 +28,12 @@ string = "silver"
# used for lifetimes # used for lifetimes
label = "honey" 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 # TODO: diferentiate doc comment
# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors # 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