Merge branch 'master' into help-command

pull/997/head
Omnikar 3 years ago
commit 063edb12ea
No known key found for this signature in database
GPG Key ID: 7DE6694CDA7885ED

@ -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') }}

16
.gitmodules vendored

@ -134,3 +134,19 @@
path = helix-syntax/languages/tree-sitter-cmake path = helix-syntax/languages/tree-sitter-cmake
url = https://github.com/uyha/tree-sitter-cmake url = https://github.com/uyha/tree-sitter-cmake
shallow = true shallow = true
[submodule "helix-syntax/languages/tree-sitter-glsl"]
path = helix-syntax/languages/tree-sitter-glsl
url = https://github.com/theHamsta/tree-sitter-glsl.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-perl"]
path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true
[submodule "helix-syntax/languages/tree-sitter-wgsl"]
path = helix-syntax/languages/tree-sitter-wgsl
url = https://github.com/szebniok/tree-sitter-wgsl
shallow = true
[submodule "helix-syntax/tree-sitter-llvm"]
path = helix-syntax/languages/tree-sitter-llvm
url = https://github.com/benwilliamgraham/tree-sitter-llvm
shallow = true

50
Cargo.lock generated

@ -13,15 +13,15 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.44" 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 = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.4.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63" checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -66,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.71" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]] [[package]]
name = "cfg-if" name = "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",
@ -566,9 +566,9 @@ checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"winapi", "winapi",
@ -897,9 +897,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.68" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1069,9 +1069,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.13.0" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee" checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -1089,9 +1089,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.5.0" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1120,9 +1120,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter" name = "tree-sitter"
version = "0.20.0" version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9" checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1"
dependencies = [ dependencies = [
"cc", "cc",
"regex", "regex",

@ -14,3 +14,6 @@ members = [
[profile.dev] [profile.dev]
split-debuginfo = "unpacked" split-debuginfo = "unpacked"
[profile.release]
lto = "thin"

@ -69,7 +69,7 @@ Contributors are very welcome! **No contribution is too small and all contributi
Some suggestions to get started: Some suggestions to get started:
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker. - 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! - 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: - 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!")`) * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)

@ -8,5 +8,7 @@
- [Keymap](./keymap.md) - [Keymap](./keymap.md)
- [Key Remapping](./remapping.md) - [Key Remapping](./remapping.md)
- [Hooks](./hooks.md) - [Hooks](./hooks.md)
- [Languages](./languages.md)
- [Guides](./guides/README.md) - [Guides](./guides/README.md)
- [Adding Languages](./guides/adding_languages.md)
- [Adding Textobject Queries](./guides/textobject.md) - [Adding Textobject Queries](./guides/textobject.md)

@ -24,6 +24,18 @@ To override global configuration parameters, create a `config.toml` file located
| `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` |
`[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default.
| Key | Description | Default |
|--|--|---------|
|`hidden` | Enables ignoring hidden files. | true
|`parents` | Enables reading ignore files from parent directories. | true
|`ignore` | Enables reading `.ignore` files. | true
|`git-ignore` | Enables reading `.gitignore` files. | true
|`git-global` | Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option. | true
|`git-exclude` | Enables reading `.git/info/exclude` files. | true
|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
## LSP ## LSP
To display all language server messages in the status line add the following to your `config.toml`: To display all language server messages in the status line add the following to your `config.toml`:

@ -0,0 +1,60 @@
# Adding languages
## Submodules
To add a new language, you should first add a tree-sitter submodule. To do this,
you can run the command
```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
```
For example, to add tree-sitter-ocaml you would run
```sh
git submodule add -f https://github.com/tree-sitter/tree-sitter-ocaml helix-syntax/languages/tree-sitter-ocaml
```
Make sure the submodule is shallow by doing
```sh
git config -f .gitmodules submodule.helix-syntax/languages/tree-sitter-<name>.shallow true
```
or you can manually add `shallow = true` to `.gitmodules`.
## languages.toml
Next, you need to add the language to the [`languages.toml`][languages.toml] found in the root of
the repository; this `languages.toml` file is included at compilation time, and
is distinct from the `language.toml` file in the user's [configuration
directory](../configuration.md).
These are the available keys and descriptions for the file.
| Key | Description |
| ---- | ----------- |
| name | The name of the language |
| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
| auto-format | Whether to autoformat this language when saving |
| comment-token | The token to use as a comment-token |
| indent | The indent to use. Has sub keys `tab-width` and `unit` |
| config | Language server configuration |
## Queries
For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries/<name>/`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries.
## Common Issues
- If you get errors when building after switching branches, you may have to remove or update tree-sitter submodules. You can update submodules by running
```sh
git submodule sync; git submodule update --init
```
- Make sure to not use the `--remote` flag. To remove submodules look inside the `.gitmodules` and remove directories that are not present inside of it.
- If a parser is segfaulting or you want to remove the parser, make sure to remove the submodule *and* the compiled parser in `runtime/grammar/<name>.so`
- The indents query is `indents.toml`, *not* `indents.scm`. See [this](https://github.com/helix-editor/helix/issues/114) issue for more information.
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml

@ -5,7 +5,7 @@ require an accompanying tree-sitter grammar and a `textobjects.scm` query file
to work properly. Tree-sitter allows us to query the source code syntax tree to work properly. Tree-sitter allows us to query the source code syntax tree
and capture specific parts of it. The queries are written in a lisp dialect. and capture specific parts of it. The queries are written in a lisp dialect.
More information on how to write queries can be found in the [official tree-sitter More information on how to write queries can be found in the [official tree-sitter
documentation](tree-sitter-queries). documentation][tree-sitter-queries].
Query files should be placed in `runtime/queries/{language}/textobjects.scm` Query files should be placed in `runtime/queries/{language}/textobjects.scm`
when contributing. Note that to test the query files locally you should put when contributing. Note that to test the query files locally you should put

@ -25,9 +25,7 @@ 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
## Build from source ## Build from source

@ -1,5 +1,8 @@
# Keymap # Keymap
- Mappings marked (**LSP**) require an active language server for the file.
- Mappings marked (**TS**) require a tree-sitter grammar for the filetype.
## Normal mode ## Normal mode
### Movement ### Movement
@ -58,24 +61,30 @@
| `.` | Repeat last change | N/A | | `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` | | `u` | Undo change | `undo` |
| `U` | Redo change | `redo` | | `U` | Redo change | `redo` |
| `Alt-u` | Move backward in history | `earlier` |
| `Alt-U` | Move forward in history | `later` |
| `y` | Yank selection | `yank` | | `y` | Yank selection | `yank` |
| `p` | Paste after selection | `paste_after` | | `p` | Paste after selection | `paste_after` |
| `P` | Paste before selection | `paste_before` | | `P` | Paste before selection | `paste_before` |
| `"` `<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 | `format_selections` | | `=` | Format selection (**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-x` | Decrement object (number) under cursor | `decrement` |
#### 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
@ -85,6 +94,8 @@
| `s` | Select all regex matches inside selections | `select_regex` | | `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into subselections on regex matches | `split_selection` | | `S` | Split selection into subselections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `&` | Align selection in columns | `align_selections` |
| `_` | Trim whitespace from the selection | `trim_selections` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` | | `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `,` | Keep only the primary selection | `keep_primary_selection` | | `,` | Keep only the primary selection | `keep_primary_selection` |
@ -98,9 +109,10 @@
| `%` | Select entire file | `select_all` | | `%` | Select entire file | `select_all` |
| `x` | Select current line, if already selected, extend to next line | `extend_line` | | `x` | Select current line, if already selected, extend to next line | `extend_line` |
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | | Expand selection to parent syntax node TODO: pick a key (**TS**) | `expand_selection` |
| `J` | Join lines inside selection | `join_selections` | | `J` | Join lines inside selection | `join_selections` |
| `K` | Keep selections matching the regex | `keep_selections` | | `K` | Keep selections matching the regex | `keep_selections` |
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
@ -133,8 +145,8 @@ over text and not actively editing it).
| `t` | Align the line to the top of the screen | `align_view_top` | | `t` | Align the line to the top of the screen | `align_view_top` |
| `b` | Align the line to the bottom of the screen | `align_view_bottom` | | `b` | Align the line to the bottom of the screen | `align_view_bottom` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` | | `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` | Scroll the view downwards | `scroll_down` | | `j` , `down` | Scroll the view downwards | `scroll_down` |
| `k` | Scroll the view upwards | `scroll_up` | | `k` , `up` | Scroll the view upwards | `scroll_up` |
| `f` | Move page down | `page_down` | | `f` | Move page down | `page_down` |
| `b` | Move page up | `page_up` | | `b` | Move page up | `page_up` |
| `d` | Move half page down | `half_page_down` | | `d` | Move half page down | `half_page_down` |
@ -144,25 +156,26 @@ over text and not actively editing it).
Jumps to various locations. Jumps to various locations.
> NOTE: Some of these features are only available with the LSP present.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `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 | `goto_definition` | | `d` | Go to definition (**LSP**) | `goto_definition` |
| `y` | Go to type definition | `goto_type_definition` | | `y` | Go to type definition (**LSP**) | `goto_type_definition` |
| `r` | Go to references | `goto_reference` | | `r` | Go to references (**LSP**) | `goto_reference` |
| `i` | Go to implementation | `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` |
#### Match mode #### Match mode
@ -172,7 +185,7 @@ and [textobject](./usage.md#textobject) usage.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `m` | Goto matching bracket | `match_brackets` | | `m` | Goto matching bracket (**TS**) | `match_brackets` |
| `s` `<char>` | Surround current selection with `<char>` | `surround_add` | | `s` `<char>` | Surround current selection with `<char>` | `surround_add` |
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` | | `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` |
| `d` `<char>` | Delete surround character `<char>` | `surround_delete` | | `d` `<char>` | Delete surround character `<char>` | `surround_delete` |
@ -191,22 +204,28 @@ 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` |
| `q`, `Ctrl-q` | Close current window | `wclose` | | `q`, `Ctrl-q` | Close current window | `wclose` |
| `o`, `Ctrl-o` | Only keep the current window, closing all the others | `wonly` |
#### Space mode #### Space mode
This layer is a kludge of mappings, mostly pickers. This layer is a kludge of mappings, mostly pickers.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `k` | Show documentation for the item under the cursor | `hover` |
| `f` | Open file picker | `file_picker` | | `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` | | `b` | Open buffer picker | `buffer_picker` |
| `s` | Open symbol picker (current document) | `symbol_picker` | | `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `a` | Apply code action | `code_action` | | `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
| `r` | Rename symbol (**LSP**) | `rename_symbol` |
| `a` | Apply code action (**LSP**) | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` | | `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A | | `w` | Enter [window mode](#window-mode) | N/A |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` | | `p` | Paste system clipboard after selections | `paste_clipboard_after` |
@ -216,7 +235,16 @@ This layer is a kludge of mappings, mostly pickers.
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` | | `/` | Global search in workspace folder | `global_search` |
> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file. > TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
##### Popup
Displays documentation for item under cursor.
| Key | Description |
| ---- | ----------- |
| `Ctrl-u` | Scroll up |
| `Ctrl-d` | Scroll down |
#### Unimpaired #### Unimpaired
@ -224,10 +252,10 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `[d` | Go to previous diagnostic | `goto_prev_diag` | | `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]d` | Go to next diagnostic | `goto_next_diag` | | `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[D` | Go to first diagnostic in document | `goto_first_diag` | | `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]D` | Go to last diagnostic in document | `goto_last_diag` | | `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` | | `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` | | `]space` | Add newline below | `add_newline_below` |
@ -237,7 +265,21 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` | | `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` | | `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-r` | Insert a register content | `insert_register` |
| `Ctrl-w` | Delete previous word | `delete_word_backward` | | `Ctrl-w` | Delete previous word | `delete_word_backward` |
| `Alt-d` | Delete next word | `delete_word_forward` |
| `Alt-b`, `Alt-Left` | Backward a word | `move_prev_word_end` |
| `Ctrl-b`, `Left` | Backward a char | `move_char_left` |
| `Alt-f`, `Alt-Right` | Forward a word | `move_next_word_start` |
| `Ctrl-f`, `Right` | Forward a char | `move_char_right` |
| `Ctrl-e`, `End` | move to line end | `goto_line_end_newline` |
| `Ctrl-a`, `Home` | move to line start | `goto_line_start` |
| `Ctrl-u` | delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | delete to end of line | `kill_to_line_end` |
| `backspace`, `Ctrl-h` | delete previous char | `delete_char_backward` |
| `delete`, `Ctrl-d` | delete previous char | `delete_char_forward` |
| `Ctrl-p`, `Up` | move to previous line | `move_line_up` |
| `Ctrl-n`, `Down` | move to next line | `move_line_down` |
## Select / extend mode ## Select / extend mode
@ -262,7 +304,9 @@ Keys to use within picker. Remapping currently not supported.
| `Escape`, `Ctrl-c` | Close picker | | `Escape`, `Ctrl-c` | Close picker |
# Prompt # Prompt
Keys to use within prompt, Remapping currently not supported. Keys to use within prompt, Remapping currently not supported.
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |
| `Escape`, `Ctrl-c` | Close prompt | | `Escape`, `Ctrl-c` | Close prompt |
@ -270,15 +314,18 @@ Keys to use within prompt, Remapping currently not supported.
| `Ctrl-b`, `Left` | Backward a char | | `Ctrl-b`, `Left` | Backward a char |
| `Alt-f`, `Alt-Right` | Forward a word | | `Alt-f`, `Alt-Right` | Forward a word |
| `Ctrl-f`, `Right` | Forward a char | | `Ctrl-f`, `Right` | Forward a char |
| `Ctrl-e`, `End` | move prompt end | | `Ctrl-e`, `End` | Move prompt end |
| `Ctrl-a`, `Home` | move prompt start | | `Ctrl-a`, `Home` | Move prompt start |
| `Ctrl-w` | delete previous word | | `Ctrl-w` | Delete previous word |
| `Ctrl-k` | delete to end of line | | `Alt-d` | Delete next word |
| `backspace` | delete previous char | | `Ctrl-u` | Delete to start of line |
| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later | | `Ctrl-k` | Delete to end of line |
| `Ctrl-p`, `Up` | select previous history | | `backspace`, `Ctrl-h` | Delete previous char |
| `Ctrl-n`, `Down` | select next history | | `delete`, `Ctrl-d` | Delete next char |
| `Tab` | slect next completion item | | `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `BackTab` | slect previous completion item | | `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected | | `Enter` | Open selected |

@ -0,0 +1,14 @@
# Languages
Language-specific settings and settings for particular language servers can be configured in a `languages.toml` file placed in your [configuration directory](./configuration.md). Helix actually uses two `languages.toml` files, the [first one](https://github.com/helix-editor/helix/blob/master/languages.toml) is in the main helix repository; it contains the default settings for each language and is included in the helix binary at compile time. Users who want to see the available settings and options can either reference the helix repo's `languages.toml` file, or consult the table in the [adding languages](./guides/adding_languages.md) section.
Changes made to the `languages.toml` file in a user's [configuration directory](./configuration.md) are merged with helix's defaults on start-up, such that a user's settings will take precedence over defaults in the event of a collision. For example, the default `languages.toml` sets rust's `auto-format` to `true`. If a user wants to disable auto-format, they can change the `languages.toml` in their [configuration directory](./configuration.md) to make the rust entry read like the example below; the new key/value pair `auto-format = false` will override the default when the two sets of settings are merged on start-up:
```
# in <config_dir>/helix/languages.toml
[[language]]
name = "rust"
auto-format = false
```

@ -15,6 +15,7 @@ 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
g = { a = "code_action" } # Maps `ga` to show possible code actions g = { a = "code_action" } # Maps `ga` to show possible code actions
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
[keys.insert] [keys.insert]
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode "A-x" = "normal_mode" # Maps Alt-X to enter normal mode
@ -38,6 +39,7 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
| Left | `"left"` | | Left | `"left"` |
| Right | `"right"` | | Right | `"right"` |
| Up | `"up"` | | Up | `"up"` |
| Down | `"down"` |
| Home | `"home"` | | Home | `"home"` |
| End | `"end"` | | End | `"end"` |
| Page | `"pageup"` | | Page | `"pageup"` |
@ -51,4 +53,5 @@ 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 in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) 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.

@ -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`

@ -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
@ -62,6 +64,7 @@ Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
| Key after `mi` or `ma` | Textobject selected | | Key after `mi` or `ma` | Textobject selected |
| --- | --- | | --- | --- |
| `w` | Word | | `w` | Word |
| `W` | WORD |
| `(`, `[`, `'`, etc | Specified surround pairs | | `(`, `[`, `'`, etc | Specified surround pairs |
| `f` | Function | | `f` | Function |
| `c` | Class | | `c` | Class |

@ -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 }

@ -63,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
let token = token.unwrap_or("//"); let token = token.unwrap_or("//");
let comment = Tendril::from(format!("{} ", token)); let comment = Tendril::from(format!("{} ", token));
let mut lines: Vec<usize> = Vec::new(); let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
let mut min_next_line = 0; let mut min_next_line = 0;
for selection in selection { for selection in selection {

@ -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,

@ -1,4 +1,4 @@
use crate::{ChangeSet, Rope, State, Transaction}; use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
@ -133,6 +133,32 @@ impl History {
Some(&self.revisions[last_child.get()].transaction) Some(&self.revisions[last_child.get()].transaction)
} }
// Get the position of last change
pub fn last_edit_pos(&self) -> Option<usize> {
if self.current == 0 {
return None;
}
let current_revision = &self.revisions[self.current];
let primary_selection = current_revision
.inversion
.selection()
.expect("inversion always contains a selection")
.primary();
let (_from, to, _fragment) = current_revision
.transaction
.changes_iter()
// find a change that matches the primary selection
.find(|(from, to, _fragment)| Range::new(*from, *to).overlaps(&primary_selection))
// or use the first change
.or_else(|| current_revision.transaction.changes_iter().next())
.unwrap();
let pos = current_revision
.transaction
.changes()
.map_pos(to, Assoc::After);
Some(pos)
}
fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize { fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
use std::collections::HashSet; use std::collections::HashSet;
let mut a_path_set = HashSet::new(); let mut a_path_set = HashSet::new();
@ -256,7 +282,7 @@ impl History {
} }
/// Whether to undo by a number of edits or a duration of time. /// Whether to undo by a number of edits or a duration of time.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone, Copy)]
pub enum UndoKind { pub enum UndoKind {
Steps(usize), Steps(usize),
TimePeriod(std::time::Duration), TimePeriod(std::time::Duration),

@ -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);
}

@ -0,0 +1,507 @@
use std::borrow::Cow;
use ropey::RopeSlice;
use super::Increment;
use crate::{
textobject::{textobject_word, TextObject},
Range, Tendril,
};
#[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> {
value: i64,
radix: u32,
range: Range,
text: RopeSlice<'a>,
}
impl<'a> NumberIncrementor<'a> {
/// Return information about number under rang if there is one.
pub fn from_range(text: RopeSlice, range: Range) -> Option<NumberIncrementor> {
// If the cursor is on the minus sign of a number we want to get the word textobject to the
// right of it.
let range = if range.to() < text.len_chars()
&& range.to() - range.from() <= 1
&& text.char(range.from()) == '-'
{
Range::new(range.from() + 1, range.to() + 1)
} else {
range
};
let range = textobject_word(text, range, TextObject::Inside, 1, false);
// If there is a minus sign to the left of the word object, we want to include it in the range.
let range = if range.from() > 0 && text.char(range.from() - 1) == '-' {
range.extend(range.from() - 1, range.from())
} else {
range
};
let word: String = text
.slice(range.from()..range.to())
.chars()
.filter(|&c| c != '_')
.collect();
let (radix, prefixed) = if word.starts_with("0x") {
(16, true)
} else if word.starts_with("0o") {
(8, true)
} else if word.starts_with("0b") {
(2, true)
} else {
(10, false)
};
let number = if prefixed { &word[2..] } else { &word };
let value = i128::from_str_radix(number, radix).ok()?;
if (value.is_positive() && value.leading_zeros() < 64)
|| (value.is_negative() && value.leading_ones() < 64)
{
return None;
}
let value = value as i64;
Some(NumberIncrementor {
range,
value,
radix,
text,
})
}
}
impl<'a> Increment for NumberIncrementor<'a> {
fn increment(&self, amount: i64) -> (Range, Tendril) {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount);
// Get separator indexes from right to left.
let separator_rtl_indexes: Vec<usize> = old_text
.chars()
.rev()
.enumerate()
.filter_map(|(i, c)| if c == '_' { Some(i) } else { None })
.collect();
let format_length = if self.radix == 10 {
match (self.value.is_negative(), new_value.is_negative()) {
(true, false) => old_length - 1,
(false, true) => old_length + 1,
_ => old_text.len(),
}
} else {
old_text.len() - 2
} - separator_rtl_indexes.len();
let mut new_text = match self.radix {
2 => format!("0b{:01$b}", new_value, format_length),
8 => format!("0o{:01$o}", new_value, format_length),
10 if old_text.starts_with('0') || old_text.starts_with("-0") => {
format!("{:01$}", new_value, format_length)
}
10 => format!("{}", new_value),
16 => {
let (lower_count, upper_count): (usize, usize) =
old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| {
(
lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0),
upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0),
)
});
if upper_count > lower_count {
format!("0x{:01$X}", new_value, format_length)
} else {
format!("0x{:01$x}", new_value, format_length)
}
}
_ => unimplemented!("radix not supported: {}", self.radix),
};
// Add separators from original number.
for &rtl_index in &separator_rtl_indexes {
if rtl_index < new_text.len() {
let new_index = new_text.len() - rtl_index;
new_text.insert(new_index, '_');
}
}
// Add in additional separators if necessary.
if new_text.len() > old_length && !separator_rtl_indexes.is_empty() {
let spacing = match separator_rtl_indexes.as_slice() {
[.., b, a] => a - b - 1,
_ => separator_rtl_indexes[0],
};
let prefix_length = if self.radix == 10 { 0 } else { 2 };
if let Some(mut index) = new_text.find('_') {
while index - prefix_length > spacing {
index -= spacing;
new_text.insert(index, '_');
}
}
}
(self.range, new_text.into())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Rope;
#[test]
fn test_decimal_at_point() {
let rope = Rope::from_str("Test text 12345 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 15),
value: 12345,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_uppercase_hexadecimal_at_point() {
let rope = Rope::from_str("Test text 0x123ABCDEF more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 21),
value: 0x123ABCDEF,
radix: 16,
text: rope.slice(..),
})
);
}
#[test]
fn test_lowercase_hexadecimal_at_point() {
let rope = Rope::from_str("Test text 0xfa3b4e more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 18),
value: 0xfa3b4e,
radix: 16,
text: rope.slice(..),
})
);
}
#[test]
fn test_octal_at_point() {
let rope = Rope::from_str("Test text 0o1074312 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 19),
value: 0o1074312,
radix: 8,
text: rope.slice(..),
})
);
}
#[test]
fn test_binary_at_point() {
let rope = Rope::from_str("Test text 0b10111010010101 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 26),
value: 0b10111010010101,
radix: 2,
text: rope.slice(..),
})
);
}
#[test]
fn test_negative_decimal_at_point() {
let rope = Rope::from_str("Test text -54321 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 16),
value: -54321,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_decimal_with_leading_zeroes_at_point() {
let rope = Rope::from_str("Test text 000045326 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 19),
value: 45326,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_negative_decimal_cursor_on_minus_sign() {
let rope = Rope::from_str("Test text -54321 more text.");
let range = Range::point(10);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 16),
value: -54321,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_under_range_start_of_rope() {
let rope = Rope::from_str("100");
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(0, 3),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_under_range_end_of_rope() {
let rope = Rope::from_str("100");
let range = Range::point(2);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(0, 3),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_surrounded_by_punctuation() {
let rope = Rope::from_str(",100;");
let range = Range::point(1);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(1, 4),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_not_a_number_point() {
let rope = Rope::from_str("Test text 45326 more text.");
let range = Range::point(6);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_too_large_at_point() {
let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text.");
let range = Range::point(12);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_cursor_one_right_of_number() {
let rope = Rope::from_str("100 ");
let range = Range::point(3);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_cursor_one_left_of_number() {
let rope = Rope::from_str(" 100");
let range = Range::point(0);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_increment_basic_decimal_numbers() {
let tests = [
("100", 1, "101"),
("100", -1, "99"),
("99", 1, "100"),
("100", 1000, "1100"),
("100", -1000, "-900"),
("-1", 1, "0"),
("-1", 2, "1"),
("1", -1, "0"),
("1", -2, "-1"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_basic_hexadedimal_numbers() {
let tests = [
("0x0100", 1, "0x0101"),
("0x0100", -1, "0x00ff"),
("0x0001", -1, "0x0000"),
("0x0000", -1, "0xffffffffffffffff"),
("0xffffffffffffffff", 1, "0x0000000000000000"),
("0xffffffffffffffff", 2, "0x0000000000000001"),
("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
("0xabcdef1234567890", 1, "0xabcdef1234567891"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_basic_octal_numbers() {
let tests = [
("0o0107", 1, "0o0110"),
("0o0110", -1, "0o0107"),
("0o0001", -1, "0o0000"),
("0o7777", 1, "0o10000"),
("0o1000", -1, "0o0777"),
("0o0107", 10, "0o0121"),
("0o0000", -1, "0o1777777777777777777777"),
("0o1777777777777777777777", 1, "0o0000000000000000000000"),
("0o1777777777777777777777", 2, "0o0000000000000000000001"),
("0o1777777777777777777777", -1, "0o1777777777777777777776"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_basic_binary_numbers() {
let tests = [
("0b00000100", 1, "0b00000101"),
("0b00000100", -1, "0b00000011"),
("0b00000100", 2, "0b00000110"),
("0b00000100", -2, "0b00000010"),
("0b00000001", -1, "0b00000000"),
("0b00111111", 10, "0b01001001"),
("0b11111111", 1, "0b100000000"),
("0b10000000", -1, "0b01111111"),
(
"0b0000",
-1,
"0b1111111111111111111111111111111111111111111111111111111111111111",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
1,
"0b0000000000000000000000000000000000000000000000000000000000000000",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
2,
"0b0000000000000000000000000000000000000000000000000000000000000001",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
-1,
"0b1111111111111111111111111111111111111111111111111111111111111110",
),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_with_separators() {
let tests = [
("999_999", 1, "1_000_000"),
("1_000_000", -1, "999_999"),
("-999_999", -1, "-1_000_000"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"),
("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"),
("0b01111111_11111111", 1, "0b10000000_00000000"),
("0b11111111_11111111", 1, "0b1_00000000_00000000"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
}

@ -450,6 +450,7 @@ where
language: vec![LanguageConfiguration { language: vec![LanguageConfiguration {
scope: "source.rust".to_string(), scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()], file_types: vec!["rs".to_string()],
shebangs: vec![],
language_id: "Rust".to_string(), language_id: "Rust".to_string(),
highlight_config: OnceCell::new(), highlight_config: OnceCell::new(),
config: None, config: None,

@ -5,6 +5,7 @@ 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;
@ -157,7 +158,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);

@ -1,48 +1,92 @@
use tree_sitter::Node;
use crate::{Rope, Syntax}; use crate::{Rope, Syntax};
const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')]; const PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
('[', ']'),
('<', '>'),
('\'', '\''),
('\"', '\"'),
];
// limit matching pairs to only ( ) { } [ ] < > // limit matching pairs to only ( ) { } [ ] < >
// Returns the position of the matching bracket under cursor.
//
// If the cursor is one the opening bracket, the position of
// the closing bracket is returned. If the cursor in the closing
// bracket, the position of the opening bracket is returned.
//
// If the cursor is not on a bracket, `None` is returned.
#[must_use]
pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
return None;
}
find_pair(syntax, doc, pos, false)
}
// Returns the position of the bracket that is closing the current scope.
//
// If the cursor is on an opening or closing bracket, the function
// behaves equivalent to [`find_matching_bracket`].
//
// If the cursor position is within a scope, the function searches
// for the surrounding scope that is surrounded by brackets and
// returns the position of the closing bracket for that scope.
//
// If no surrounding scope is found, the function returns `None`.
#[must_use] #[must_use]
pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> { pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
find_pair(syntax, doc, pos, true)
}
fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> {
let tree = syntax.tree(); let tree = syntax.tree();
let pos = doc.char_to_byte(pos);
let byte_pos = doc.char_to_byte(pos); let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
// most naive implementation: find the innermost syntax node, if we're at the edge of a node, loop {
// return the other edge. let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
let node = match tree if is_valid_pair(doc, start_char, end_char) {
.root_node() if end_byte == pos {
.named_descendant_for_byte_range(byte_pos, byte_pos) return Some(start_char);
{ }
Some(node) => node, // We return the end char if the cursor is either on the start char
None => return None, // or at some arbitrary position between start and end char.
}; return Some(end_char);
}
if node.is_error() { if traverse_parents {
node = node.parent()?;
} else {
return None; return None;
} }
}
}
fn is_valid_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, r)| *l == c || *r == c)
}
fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
}
fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
let len = doc.len_bytes(); let len = doc.len_bytes();
let start_byte = node.start_byte(); let start_byte = node.start_byte();
let end_byte = node.end_byte().saturating_sub(1); // it's end exclusive let end_byte = node.end_byte().saturating_sub(1);
if start_byte >= len || end_byte >= len { if start_byte >= len || end_byte >= len {
return None; return None;
} }
let start_char = doc.byte_to_char(start_byte); Some((start_byte, end_byte))
let end_char = doc.byte_to_char(end_byte);
if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
if start_byte == byte_pos {
return Some(end_char);
}
if end_byte == byte_pos {
return Some(start_char);
}
}
None
} }

@ -168,7 +168,7 @@ pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<u
where where
F: Fn(char) -> bool, F: Fn(char) -> bool,
{ {
let mut chars_starting_from_next = slice.chars_at(pos + 1); let mut chars_starting_from_next = slice.chars_at(pos);
let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate(); let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
backwards.find_map(|(i, c)| { backwards.find_map(|(i, c)| {
if !fun(c) { if !fun(c) {

@ -40,7 +40,6 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
/// needs to improve on. /// needs to improve on.
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81> /// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
pub fn get_normalized_path(path: &Path) -> PathBuf { pub fn get_normalized_path(path: &Path) -> PathBuf {
let path = expand_tilde(path);
let mut components = path.components().peekable(); let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next(); components.next();
@ -72,10 +71,11 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify /// This function is used instead of `std::fs::canonicalize` because we don't want to verify
/// here if the path exists, just normalize it's components. /// here if the path exists, just normalize it's components.
pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> { pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
let path = expand_tilde(path);
let path = if path.is_relative() { let path = if path.is_relative() {
std::env::current_dir().map(|current_dir| current_dir.join(path))? std::env::current_dir().map(|current_dir| current_dir.join(path))?
} else { } else {
path.to_path_buf() path
}; };
Ok(get_normalized_path(path.as_path())) Ok(get_normalized_path(path.as_path()))

@ -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,12 +31,16 @@ 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

@ -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,
} }
} }
@ -360,7 +360,7 @@ impl Selection {
self.normalize() self.normalize()
} }
/// Adds a new range to the selection and makes it the primary range. /// Removes a range from the selection.
pub fn remove(mut self, index: usize) -> Self { pub fn remove(mut self, index: usize) -> Self {
assert!( assert!(
self.ranges.len() > 1, self.ranges.len() > 1,
@ -528,14 +528,15 @@ impl<'a> IntoIterator for &'a Selection {
// TODO: checkSelection -> check if valid for doc length && sorted // TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_matches( pub fn keep_or_remove_matches(
text: RopeSlice, text: RopeSlice,
selection: &Selection, selection: &Selection,
regex: &crate::regex::Regex, regex: &crate::regex::Regex,
remove: bool,
) -> Option<Selection> { ) -> Option<Selection> {
let result: SmallVec<_> = selection let result: SmallVec<_> = selection
.iter() .iter()
.filter(|range| regex.is_match(&range.fragment(text))) .filter(|range| regex.is_match(&range.fragment(text)) ^ remove)
.copied() .copied()
.collect(); .collect();

@ -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]

@ -40,18 +40,21 @@ where
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Configuration { pub struct Configuration {
pub language: Vec<LanguageConfiguration>, pub language: Vec<LanguageConfiguration>,
} }
// largely based on tree-sitter/cli/src/loader.rs // largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[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,
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)]
pub shebangs: Vec<String>, // interpreter(s) associated with language
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>, pub comment_token: Option<String>,
@ -254,6 +257,7 @@ pub struct Loader {
// highlight_names ? // highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>, language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize> language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_shebang: HashMap<String, usize>,
} }
impl Loader { impl Loader {
@ -261,6 +265,7 @@ impl Loader {
let mut loader = Self { let mut loader = Self {
language_configs: Vec::new(), language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(), language_config_ids_by_file_type: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
}; };
for config in config.language { for config in config.language {
@ -273,6 +278,11 @@ impl Loader {
.language_config_ids_by_file_type .language_config_ids_by_file_type
.insert(file_type.clone(), language_id); .insert(file_type.clone(), language_id);
} }
for shebang in &config.shebangs {
loader
.language_config_ids_by_shebang
.insert(shebang.clone(), language_id);
}
loader.language_configs.push(Arc::new(config)); loader.language_configs.push(Arc::new(config));
} }
@ -298,6 +308,18 @@ impl Loader {
// TODO: content_regex handling conflict resolution // TODO: content_regex handling conflict resolution
} }
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0));
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
});
let configuration_id = SHEBANG_REGEX
.captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
}
pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> { pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
self.language_configs self.language_configs
.iter() .iter()

@ -10,7 +10,7 @@ use crate::surround;
use crate::syntax::LanguageConfiguration; use crate::syntax::LanguageConfiguration;
use crate::Range; use crate::Range;
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
use CharCategory::{Eol, Whitespace}; use CharCategory::{Eol, Whitespace};
let iter = match direction { let iter = match direction {
@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) ->
match categorize_char(ch) { match categorize_char(ch) {
Eol | Whitespace => return pos, Eol | Whitespace => return pos,
category => { category => {
if category != prev_category && pos != 0 && pos != slice.len_chars() { if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
return pos; return pos;
} else { } else {
match direction { match direction {
@ -70,13 +70,14 @@ pub fn textobject_word(
range: Range, range: Range,
textobject: TextObject, textobject: TextObject,
_count: usize, _count: usize,
long: bool,
) -> Range { ) -> Range {
let pos = range.cursor(slice); let pos = range.cursor(slice);
let word_start = find_word_boundary(slice, pos, Direction::Backward); let word_start = find_word_boundary(slice, pos, Direction::Backward, long);
let word_end = match slice.get_char(pos).map(categorize_char) { let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos, None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
_ => find_word_boundary(slice, pos + 1, Direction::Forward), _ => find_word_boundary(slice, pos + 1, Direction::Forward, long),
}; };
// Special case. // Special case.
@ -113,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)),
@ -169,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",
@ -268,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); // 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(),
@ -282,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.

@ -23,5 +23,5 @@ lsp-types = { version = "0.91", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.13", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tokio-stream = "0.1.8" tokio-stream = "0.1.8"

@ -257,6 +257,12 @@ impl Client {
content_format: Some(vec![lsp::MarkupKind::Markdown]), content_format: Some(vec![lsp::MarkupKind::Markdown]),
..Default::default() ..Default::default()
}), }),
rename: Some(lsp::RenameClientCapabilities {
dynamic_registration: Some(false),
prepare_support: Some(false),
prepare_support_default_behavior: None,
honors_change_annotations: Some(false),
}),
code_action: Some(lsp::CodeActionClientCapabilities { code_action: Some(lsp::CodeActionClientCapabilities {
code_action_literal_support: Some(lsp::CodeActionLiteralSupport { code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
code_action_kind: lsp::CodeActionKindLiteralSupport { code_action_kind: lsp::CodeActionKindLiteralSupport {
@ -773,4 +779,25 @@ impl Client {
self.call::<lsp::request::CodeActionRequest>(params) self.call::<lsp::request::CodeActionRequest>(params)
} }
pub async fn rename_symbol(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> anyhow::Result<lsp::WorkspaceEdit> {
let params = lsp::RenameParams {
text_document_position: lsp::TextDocumentPositionParams {
text_document,
position,
},
new_name,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
};
let response = self.request::<lsp::request::Rename>(params).await?;
Ok(response.unwrap_or_default())
}
} }

@ -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 88408ffc5e27abcffced7010fc77396ae3636d7e

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

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

@ -0,0 +1,12 @@
use std::process::Command;
fn main() {
let git_hash = Command::new("git")
.args(&["describe", "--dirty"])
.output()
.map(|x| String::from_utf8(x.stdout).ok())
.ok()
.flatten()
.unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
}

@ -7,7 +7,7 @@ use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui};
use log::{error, warn}; use log::{error, warn};
use std::{ use std::{
io::{stdout, Write}, io::{stdin, stdout, Write},
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -17,6 +17,7 @@ use anyhow::Error;
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal, execute, terminal,
tty::IsTty,
}; };
#[cfg(not(windows))] #[cfg(not(windows))]
use { use {
@ -60,14 +61,19 @@ impl Application {
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir())); std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
// load default and user config, and merge both // load default and user config, and merge both
let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml")) let builtin_err_msg =
.expect("Could not parse built-in languages.toml, something must be very wrong"); "Could not parse built-in languages.toml, something must be very wrong";
let user_lang_conf: Option<toml::Value> = std::fs::read(conf_dir.join("languages.toml")) let def_lang_conf: toml::Value =
toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg);
let def_syn_loader_conf: helix_core::syntax::Configuration =
def_lang_conf.clone().try_into().expect(builtin_err_msg);
let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
.ok() .ok()
.map(|raw| toml::from_slice(&raw).expect("Could not parse user languages.toml")); .map(|raw| toml::from_slice(&raw));
let lang_conf = match user_lang_conf { let lang_conf = match user_lang_conf {
Some(value) => merge_toml_values(def_lang_conf, value), Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
None => def_lang_conf, Some(err @ Err(_)) => err,
None => Ok(def_lang_conf),
}; };
let theme = if let Some(theme) = &config.theme { let theme = if let Some(theme) = &config.theme {
@ -83,8 +89,15 @@ impl Application {
}; };
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.try_into() .and_then(|conf| conf.try_into())
.expect("Could not parse merged (built-in + user) languages.toml"); .unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
eprintln!("Press <ENTER> to continue with default language config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
def_syn_loader_conf
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut editor = Editor::new( let mut editor = Editor::new(
@ -107,7 +120,7 @@ impl Application {
if first.is_dir() { if first.is_dir() {
std::env::set_current_dir(&first)?; std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(".".into()))); compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
} else { } else {
let nr_of_files = args.files.len(); let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?; editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@ -122,8 +135,17 @@ impl Application {
} }
editor.set_status(format!("Loaded {} files.", nr_of_files)); editor.set_status(format!("Loaded {} files.", nr_of_files));
} }
} else { } else if stdin().is_tty() {
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
} else if cfg!(target_os = "macos") {
// On Linux and Windows, we allow the output of a command to be piped into the new buffer.
// This doesn't currently work on macOS because of the following issue:
// https://github.com/crossterm-rs/crossterm/issues/500
anyhow::bail!("Piping into helix-term is currently not supported on macOS");
} else {
editor
.new_file_from_stdin(Action::VerticalSplit)
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
} }
editor.set_theme(theme); editor.set_theme(theme);
@ -243,17 +265,13 @@ 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
.compositor .compositor
.find(std::any::type_name::<ui::EditorView>()) .find::<ui::EditorView>()
.expect("expected at least one EditorView"); .expect("expected at least one EditorView");
let editor_view = editor_view
.as_any_mut()
.downcast_mut::<ui::EditorView>()
.unwrap();
if editor_view.completion.is_some() { if editor_view.completion.is_some() {
return; return;
@ -418,12 +436,8 @@ impl Application {
{ {
let editor_view = self let editor_view = self
.compositor .compositor
.find(std::any::type_name::<ui::EditorView>()) .find::<ui::EditorView>()
.expect("expected at least one EditorView"); .expect("expected at least one EditorView");
let editor_view = editor_view
.as_any_mut()
.downcast_mut::<ui::EditorView>()
.unwrap();
let lsp::ProgressParams { token, value } = params; let lsp::ProgressParams { token, value } = params;
let lsp::ProgressParamsValue::WorkDone(work) = value; let lsp::ProgressParamsValue::WorkDone(work) = value;
@ -537,12 +551,8 @@ impl Application {
let editor_view = self let editor_view = self
.compositor .compositor
.find(std::any::type_name::<ui::EditorView>()) .find::<ui::EditorView>()
.expect("expected at least one EditorView"); .expect("expected at least one EditorView");
let editor_view = editor_view
.as_any_mut()
.downcast_mut::<ui::EditorView>()
.unwrap();
let spinner = editor_view.spinners_mut().get_or_create(server_id); let spinner = editor_view.spinners_mut().get_or_create(server_id);
if spinner.is_stopped() { if spinner.is_stopped() {
spinner.start(); spinner.start();
@ -577,7 +587,7 @@ impl Application {
Ok(()) Ok(())
} }
pub async fn run(&mut self) -> Result<(), Error> { pub async fn run(&mut self) -> Result<i32, Error> {
self.claim_term().await?; self.claim_term().await?;
// Exit the alternate screen and disable raw mode before panicking // Exit the alternate screen and disable raw mode before panicking
@ -600,6 +610,6 @@ impl Application {
self.restore_term()?; self.restore_term()?;
Ok(()) Ok(self.editor.exit_code)
} }
} }

File diff suppressed because it is too large Load Diff

@ -177,11 +177,12 @@ impl Compositor {
.any(|component| component.type_name() == type_name) .any(|component| component.type_name() == type_name)
} }
pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { pub fn find<T: 'static>(&mut self) -> Option<&mut T> {
let type_name = std::any::type_name::<T>();
self.layers self.layers
.iter_mut() .iter_mut()
.find(|component| component.type_name() == type_name) .find(|component| component.type_name() == type_name)
.map(|component| component.as_mut()) .and_then(|component| component.as_any_mut().downcast_mut())
} }
} }

@ -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};
@ -25,6 +25,54 @@ macro_rules! key {
}; };
} }
#[macro_export]
macro_rules! shift {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
}
};
}
#[macro_export]
macro_rules! ctrl {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
}
};
}
#[macro_export]
macro_rules! alt {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
}
};
}
/// Macro for defining the root of a `Keymap` object. Example: /// Macro for defining the root of a `Keymap` object. Example:
/// ///
/// ``` /// ```
@ -44,7 +92,7 @@ macro_rules! key {
#[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
@ -53,6 +101,10 @@ macro_rules! keymap {
keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
}; };
(@trie [$($cmd:ident),* $(,)?]) => {
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
};
( (
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => { ) => {
@ -64,10 +116,11 @@ macro_rules! keymap {
$( $(
$( $(
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
_map.insert( let _duplicate = _map.insert(
_key, _key,
keymap!(@trie $value) keymap!(@trie $value)
); );
assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key); _order.push(_key);
)+ )+
)* )*
@ -147,6 +200,7 @@ impl KeyTrieNode {
cmd.doc() cmd.doc()
} }
KeyTrie::Node(n) => n.name(), KeyTrie::Node(n) => n.name(),
KeyTrie::Sequence(_) => "[Multiple commands]",
}; };
match body.iter().position(|(d, _)| d == &desc) { match body.iter().position(|(d, _)| d == &desc) {
Some(pos) => { Some(pos) => {
@ -206,7 +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<MappableCommand>),
Node(KeyTrieNode), Node(KeyTrieNode),
} }
@ -214,14 +269,14 @@ impl KeyTrie {
pub fn node(&self) -> Option<&KeyTrieNode> { pub fn node(&self) -> Option<&KeyTrieNode> {
match *self { match *self {
KeyTrie::Node(ref node) => Some(node), KeyTrie::Node(ref node) => Some(node),
KeyTrie::Leaf(_) => None, KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
} }
} }
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
match *self { match *self {
KeyTrie::Node(ref mut node) => Some(node), KeyTrie::Node(ref mut node) => Some(node),
KeyTrie::Leaf(_) => None, KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
} }
} }
@ -238,7 +293,7 @@ impl KeyTrie {
trie = match trie { trie = match trie {
KeyTrie::Node(map) => map.get(key), KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process // leaf encountered while keys left to process
KeyTrie::Leaf(_) => None, KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}? }?
} }
Some(trie) Some(trie)
@ -249,7 +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.
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
@ -329,8 +386,14 @@ 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)) => {
return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
)
} }
None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()), None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
Some(t) => t, Some(t) => t,
@ -345,9 +408,16 @@ 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();
return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
}
Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear(); self.state.clear();
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()); KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
)
} }
None => KeymapResult::new( None => KeymapResult::new(
KeymapResultKind::Cancelled(self.state.drain(..).collect()), KeymapResultKind::Cancelled(self.state.drain(..).collect()),
@ -442,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,
@ -450,11 +521,13 @@ 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,
}, },
":" => command_mode, ":" => command_mode,
@ -466,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,
@ -511,6 +584,8 @@ impl Default for Keymaps {
"u" => undo, "u" => undo,
"U" => redo, "U" => redo,
"A-u" => earlier,
"A-U" => later,
"y" => yank, "y" => yank,
// yank_all // yank_all
@ -523,7 +598,7 @@ impl Default for Keymaps {
"=" => format_selections, "=" => format_selections,
"J" => join_selections, "J" => join_selections,
"K" => keep_selections, "K" => keep_selections,
// TODO: and another method for inverse "A-K" => remove_selections,
"," => keep_primary_selection, "," => keep_primary_selection,
"A-," => remove_primary_selection, "A-," => remove_primary_selection,
@ -531,8 +606,8 @@ impl Default for Keymaps {
// "q" => record_macro, // "q" => record_macro,
// "Q" => replay_macro, // "Q" => replay_macro,
// & align selections "&" => align_selections,
// _ trim selections "_" => trim_selections,
"(" => rotate_selections_backward, "(" => rotate_selections_backward,
")" => rotate_selections_forward, ")" => rotate_selections_forward,
@ -549,7 +624,10 @@ 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-h" | "h" | "left" => jump_view_left, "C-h" | "h" | "left" => jump_view_left,
"C-j" | "j" | "down" => jump_view_down, "C-j" | "j" | "down" => jump_view_down,
"C-k" | "k" | "up" => jump_view_up, "C-k" | "k" | "up" => jump_view_up,
@ -569,13 +647,21 @@ impl Default for Keymaps {
"f" => file_picker, "f" => file_picker,
"b" => buffer_picker, "b" => buffer_picker,
"s" => symbol_picker, "s" => symbol_picker,
"S" => workspace_symbol_picker,
"a" => code_action, "a" => code_action,
"'" => last_picker, "'" => last_picker,
"w" => { "Window" "w" => { "Window"
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-h" | "h" => 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-h" | "h" | "left" => jump_view_left,
"C-j" | "j" | "down" => jump_view_down,
"C-k" | "k" | "up" => jump_view_up,
"C-l" | "l" | "right" => jump_view_right,
}, },
"y" => yank_joined_to_clipboard, "y" => yank_joined_to_clipboard,
"Y" => yank_main_selection_to_clipboard, "Y" => yank_main_selection_to_clipboard,
@ -584,30 +670,31 @@ impl Default for Keymaps {
"R" => replace_selections_with_clipboard, "R" => replace_selections_with_clipboard,
"/" => global_search, "/" => global_search,
"k" => hover, "k" => hover,
"r" => rename_symbol,
}, },
"z" => { "View" "z" => { "View"
"z" | "c" => align_view_center, "z" | "c" => align_view_center,
"t" => align_view_top, "t" => align_view_top,
"b" => align_view_bottom, "b" => align_view_bottom,
"m" => align_view_middle, "m" => align_view_middle,
"k" => scroll_up, "k" | "up" => scroll_up,
"j" => scroll_down, "j" | "down" => scroll_down,
"b" => page_up, "C-b" | "pageup" => page_up,
"f" => page_down, "C-f" | "pagedown" => page_down,
"u" => half_page_up, "C-u" => half_page_up,
"d" => half_page_down, "C-d" => half_page_down,
}, },
"Z" => { "View" sticky=true "Z" => { "View" sticky=true
"z" | "c" => align_view_center, "z" | "c" => align_view_center,
"t" => align_view_top, "t" => align_view_top,
"b" => align_view_bottom, "b" => align_view_bottom,
"m" => align_view_middle, "m" => align_view_middle,
"k" => scroll_up, "k" | "up" => scroll_up,
"j" => scroll_down, "j" | "down" => scroll_down,
"b" => page_up, "C-b" | "pageup" => page_up,
"f" => page_down, "C-f" | "pagedown" => page_down,
"u" => half_page_up, "C-u" => half_page_up,
"d" => half_page_down, "C-d" => half_page_down,
}, },
"\"" => select_register, "\"" => select_register,
@ -617,6 +704,9 @@ impl Default for Keymaps {
"A-!" => shell_append_output, "A-!" => shell_append_output,
"$" => shell_keep_pipe, "$" => shell_keep_pipe,
"C-z" => suspend, "C-z" => suspend,
"C-a" => increment,
"C-x" => decrement,
}); });
let mut select = normal.clone(); let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode" select.merge_nodes(keymap!({ "Select mode"
@ -650,21 +740,38 @@ impl Default for Keymaps {
"esc" => normal_mode, "esc" => normal_mode,
"backspace" => delete_char_backward, "backspace" => delete_char_backward,
"C-h" => delete_char_backward,
"del" => delete_char_forward, "del" => delete_char_forward,
"C-d" => delete_char_forward,
"ret" => insert_newline, "ret" => insert_newline,
"tab" => insert_tab, "tab" => insert_tab,
"C-w" => delete_word_backward, "C-w" => delete_word_backward,
"A-d" => delete_word_forward,
"left" => move_char_left, "left" => move_char_left,
"C-b" => move_char_left,
"down" => move_line_down, "down" => move_line_down,
"C-n" => move_line_down,
"up" => move_line_up, "up" => move_line_up,
"C-p" => move_line_up,
"right" => move_char_right, "right" => move_char_right,
"C-f" => move_char_right,
"A-b" => move_prev_word_end,
"A-left" => move_prev_word_end,
"A-f" => move_next_word_start,
"A-right" => move_next_word_start,
"pageup" => page_up, "pageup" => page_up,
"pagedown" => page_down, "pagedown" => page_down,
"home" => goto_line_start, "home" => goto_line_start,
"C-a" => goto_line_start,
"end" => goto_line_end_newline, "end" => goto_line_end_newline,
"C-e" => goto_line_end_newline,
"C-k" => kill_to_line_end,
"C-u" => kill_to_line_start,
"C-x" => completion, "C-x" => completion,
"C-r" => insert_register,
}); });
Keymaps(hashmap!( Keymaps(hashmap!(
Mode::Normal => Keymap::new(normal), Mode::Normal => Keymap::new(normal),
@ -686,6 +793,22 @@ pub fn merge_keys(mut config: Config) -> Config {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
#[should_panic]
fn duplicate_keys_should_panic() {
keymap!({ "Normal mode"
"i" => normal_mode,
"i" => goto_definition,
});
}
#[test]
fn check_duplicate_keys_in_default_keymap() {
// will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro
Keymaps::default();
}
#[test] #[test]
fn merge_partial_keys() { fn merge_partial_keys() {
let config = Config { let config = Config {
@ -710,36 +833,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"
); );
@ -773,11 +896,27 @@ 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
let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty()) assert!(!node.node().unwrap().order().is_empty())
} }
#[test]
fn aliased_modes_are_same_in_default_keymap() {
let keymaps = Keymaps::default();
let root = keymaps.get(&Mode::Normal).unwrap().root();
assert_eq!(
root.search(&[key!(' '), key!('w')]).unwrap(),
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
"Mismatch for window mode on `Space-w` and `Ctrl-w`"
);
assert_eq!(
root.search(&[key!('z')]).unwrap(),
root.search(&[key!('Z')]).unwrap(),
"Mismatch for view mode on `z` and `Z`"
);
}
} }

@ -16,11 +16,6 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
}; };
// Separate file config so we can include year, month and day in file logs // Separate file config so we can include year, month and day in file logs
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(logpath)?;
let file_config = fern::Dispatch::new() let file_config = fern::Dispatch::new()
.format(|out, message, record| { .format(|out, message, record| {
out.finish(format_args!( out.finish(format_args!(
@ -31,15 +26,20 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message message
)) ))
}) })
.chain(file); .chain(fern::log_file(logpath)?);
base_config.chain(file_config).apply()?; base_config.chain(file_config).apply()?;
Ok(()) Ok(())
} }
fn main() -> Result<()> {
let exit_code = main_impl()?;
std::process::exit(exit_code);
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main_impl() -> Result<i32> {
let cache_dir = helix_core::cache_dir(); let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() { if !cache_dir.exists() {
std::fs::create_dir_all(&cache_dir).ok(); std::fs::create_dir_all(&cache_dir).ok();
@ -66,7 +66,7 @@ FLAGS:
-V, --version Prints version information -V, --version Prints version information
", ",
env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"), env!("VERSION_AND_GIT_HASH"),
env!("CARGO_PKG_AUTHORS"), env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"), env!("CARGO_PKG_DESCRIPTION"),
logpath.display(), logpath.display(),
@ -81,7 +81,7 @@ FLAGS:
} }
if args.display_version { if args.display_version {
println!("helix {}", env!("CARGO_PKG_VERSION")); println!("helix {}", env!("VERSION_AND_GIT_HASH"));
std::process::exit(0); std::process::exit(0);
} }
@ -109,7 +109,8 @@ FLAGS:
// TODO: use the thread local executor to spawn the application task separately from the work pool // TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config).context("unable to create new application")?; let mut app = Application::new(args, config).context("unable to create new application")?;
app.run().await.unwrap();
Ok(()) let exit_code = app.run().await?;
Ok(exit_code)
} }

@ -16,8 +16,7 @@ use helix_core::{
LineEnding, Position, Range, Selection, LineEnding, Position, Range, Selection,
}; };
use helix_view::{ use helix_view::{
document::Mode, 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,
@ -305,17 +304,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,
@ -346,6 +344,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,
@ -377,7 +379,7 @@ impl EditorView {
use helix_core::match_brackets; use helix_core::match_brackets;
let pos = doc.selection(view.id).primary().cursor(text); let pos = doc.selection(view.id).primary().cursor(text);
let pos = match_brackets::find(syntax, doc.text(), pos) let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
if let Some(pos) = pos { if let Some(pos) = pos {
@ -412,22 +414,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)]
@ -437,52 +423,30 @@ 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) { // avoid lots of small allocations by reusing a text buffer for each line
surface.set_stringn( let mut text = String::with_capacity(8);
viewport.x,
viewport.y + i as u16,
"●",
1,
match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
}
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 { style,
linenr_select
} else {
linenr
},
); );
} }
text.clear();
}
offset += *width as u16;
}
} }
pub fn render_diagnostics( pub fn render_diagnostics(
@ -497,7 +461,7 @@ impl EditorView {
use tui::{ use tui::{
layout::Alignment, layout::Alignment,
text::Text, text::Text,
widgets::{Paragraph, Widget}, widgets::{Paragraph, Widget, Wrap},
}; };
let cursor = doc let cursor = doc
@ -529,8 +493,10 @@ impl EditorView {
lines.extend(text.lines); lines.extend(text.lines);
} }
let paragraph = Paragraph::new(lines).alignment(Alignment::Right); let paragraph = Paragraph::new(lines)
let width = 80.min(viewport.width); .alignment(Alignment::Right)
.wrap(Wrap { trim: true });
let width = 100.min(viewport.width);
let height = 15.min(viewport.height); let height = 15.min(viewport.height);
paragraph.render( paragraph.render(
Rect::new(viewport.right() - width, viewport.y + 1, width, height), Rect::new(viewport.right() - width, viewport.y + 1, width, height),
@ -580,8 +546,11 @@ impl EditorView {
} }
surface.set_string(viewport.x + 5, viewport.y, progress, base_style); surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
if let Some(path) = doc.relative_path() { let rel_path = doc.relative_path();
let path = path.to_string_lossy(); let path = rel_path
.as_ref()
.map(|p| p.to_string_lossy())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" }); let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
surface.set_stringn( surface.set_stringn(
@ -591,7 +560,6 @@ impl EditorView {
viewport.width.saturating_sub(6) as usize, viewport.width.saturating_sub(6) as usize,
base_style, base_style,
); );
}
//------------------------------- //-------------------------------
// Right side of the status line. // Right side of the status line.
@ -695,6 +663,11 @@ impl EditorView {
match &key_result.kind { match &key_result.kind {
KeymapResultKind::Matched(command) => command.execute(cxt), KeymapResultKind::Matched(command) => command.execute(cxt),
KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
KeymapResultKind::MatchedSequence(commands) => {
for command in commands {
command.execute(cxt);
}
}
KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result), KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
} }
None None
@ -736,7 +709,7 @@ impl EditorView {
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i)); std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
} }
// special handling for repeat operator // special handling for repeat operator
key!('.') => { key!('.') if self.keymaps.pending().is_empty() => {
// first execute whatever put us into insert mode // first execute whatever put us into insert mode
self.last_insert.0.execute(cxt); self.last_insert.0.execute(cxt);
// then replay the inputs // then replay the inputs
@ -902,7 +875,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)
} }
@ -920,7 +893,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);
} }
@ -934,7 +908,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);
} }
@ -949,7 +923,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,
@ -1158,12 +1132,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
}
}

@ -13,7 +13,7 @@ use helix_core::{
Rope, Rope,
}; };
use helix_view::{ use helix_view::{
graphics::{Color, Margin, Rect, Style}, graphics::{Margin, Rect},
Theme, Theme,
}; };
@ -55,15 +55,21 @@ fn parse<'a>(
fn to_span(text: pulldown_cmark::CowStr) -> Span { fn to_span(text: pulldown_cmark::CowStr) -> Span {
use std::ops::Deref; use std::ops::Deref;
Span::raw::<std::borrow::Cow<_>>(match text { Span::raw::<std::borrow::Cow<_>>(match text {
CowStr::Borrowed(s) => s.to_string().into(), // could retain borrow CowStr::Borrowed(s) => s.into(),
CowStr::Boxed(s) => s.to_string().into(), CowStr::Boxed(s) => s.to_string().into(),
CowStr::Inlined(s) => s.deref().to_owned().into(), CowStr::Inlined(s) => s.deref().to_owned().into(),
}) })
} }
let text_style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
let code_style = Style::default().fg(Color::Rgb(255, 255, 255)); // white
let heading_style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac // TODO: use better scopes for these, `markup.raw.block`, `markup.heading`
let code_style = theme
.map(|theme| theme.get("ui.text.focus"))
.unwrap_or_default(); // white
let heading_style = theme
.map(|theme| theme.get("ui.linenr.selected"))
.unwrap_or_default(); // lilac
for event in parser { for event in parser {
match event { match event {
@ -173,7 +179,9 @@ fn parse<'a>(
spans.push(Span::raw(" ")); spans.push(Span::raw(" "));
} }
Event::Rule => { Event::Rule => {
lines.push(Spans::from("---")); let mut span = Span::raw("---");
span.style = code_style;
lines.push(Spans::from(span));
lines.push(Spans::default()); lines.push(Spans::default());
} }
// TaskListMarker(bool) true if checked // TaskListMarker(bool) true if checked
@ -220,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;

@ -1,5 +1,8 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::{
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; compositor::{Component, Compositor, Context, EventResult},
ctrl, key, shift,
};
use crossterm::event::Event;
use tui::{buffer::Buffer as Surface, widgets::Table}; use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row}; pub use tui::widgets::{Cell, Row};
@ -192,63 +195,25 @@ impl<T: Item + 'static> Component for Menu<T> {
compositor.pop(); compositor.pop();
}))); })));
match event { match event.into() {
// esc or ctrl-c aborts the completion and closes the menu // esc or ctrl-c aborts the completion and closes the menu
KeyEvent { key!(Esc) | ctrl!('c') => {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => {
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
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)
KeyEvent { shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Up, ..
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => {
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);
} }
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
// arrow down/ctrl-n/tab advances completion choice (including updating the doc) // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Down,
..
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_down(); self.move_down();
(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);
} }
KeyEvent { key!(Enter) => {
code: KeyCode::Enter,
..
} => {
if let Some(selection) = self.selection() { if let Some(selection) = self.selection() {
(self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate); (self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate);
} }

@ -35,6 +35,7 @@ pub fn regex_prompt(
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let view_id = view.id; let view_id = view.id;
let snapshot = doc.selection(view_id).clone(); let snapshot = doc.selection(view_id).clone();
let offset_snapshot = view.offset;
Prompt::new( Prompt::new(
prompt, prompt,
@ -45,6 +46,7 @@ pub fn regex_prompt(
PromptEvent::Abort => { PromptEvent::Abort => {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, snapshot.clone()); doc.set_selection(view.id, snapshot.clone());
view.offset = offset_snapshot;
} }
PromptEvent::Validate => { PromptEvent::Validate => {
// TODO: push_jump to store selection just before jump // TODO: push_jump to store selection just before jump
@ -91,13 +93,22 @@ pub fn regex_prompt(
) )
} }
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> { pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder}; use ignore::{types::TypesBuilder, WalkBuilder};
use std::time; use std::time;
// We want to exclude files that the editor can't handle yet // We want to exclude files that the editor can't handle yet
let mut type_builder = TypesBuilder::new(); let mut type_builder = TypesBuilder::new();
let mut walk_builder = WalkBuilder::new(&root); let mut walk_builder = WalkBuilder::new(&root);
walk_builder
.hidden(config.file_picker.hidden)
.parents(config.file_picker.parents)
.ignore(config.file_picker.ignore)
.git_ignore(config.file_picker.git_ignore)
.git_global(config.file_picker.git_global)
.git_exclude(config.file_picker.git_exclude)
.max_depth(config.file_picker.max_depth);
let walk_builder = match type_builder.add( let walk_builder = match type_builder.add(
"compressed", "compressed",
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}", "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",

@ -1,8 +1,9 @@
use crate::{ use crate::{
compositor::{Component, Compositor, Context, EventResult}, compositor::{Component, Compositor, Context, EventResult},
ctrl, key, shift,
ui::EditorView, ui::EditorView,
}; };
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::Event;
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
widgets::{Block, BorderType, Borders}, widgets::{Block, BorderType, Borders},
@ -36,6 +37,7 @@ type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> { pub struct FilePicker<T> {
picker: Picker<T>, picker: Picker<T>,
pub truncate_start: bool,
/// Caches paths to documents /// Caches paths to documents
preview_cache: HashMap<PathBuf, CachedPreview>, preview_cache: HashMap<PathBuf, CachedPreview>,
read_buffer: Vec<u8>, read_buffer: Vec<u8>,
@ -44,7 +46,7 @@ pub struct FilePicker<T> {
} }
pub enum CachedPreview { pub enum CachedPreview {
Document(Document), Document(Box<Document>),
Binary, Binary,
LargeFile, LargeFile,
NotFound, NotFound,
@ -89,6 +91,7 @@ impl<T> FilePicker<T> {
) -> Self { ) -> Self {
Self { Self {
picker: Picker::new(false, options, format_fn, callback_fn), picker: Picker::new(false, options, format_fn, callback_fn),
truncate_start: true,
preview_cache: HashMap::new(), preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024), read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn), file_fn: Box::new(preview_fn),
@ -137,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)
} }
}, },
@ -171,6 +174,7 @@ impl<T: 'static> Component for FilePicker<T> {
}; };
let picker_area = area.with_width(picker_width); let picker_area = area.with_width(picker_width);
self.picker.truncate_start = self.truncate_start;
self.picker.render(picker_area, surface, cx); self.picker.render(picker_area, surface, cx);
if !render_preview { if !render_preview {
@ -276,6 +280,8 @@ pub struct Picker<T> {
prompt: Prompt, prompt: Prompt,
/// Whether to render in the middle of the area /// Whether to render in the middle of the area
render_centered: bool, render_centered: bool,
/// Wheather to truncate the start (default true)
pub truncate_start: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>, format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>, callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
@ -305,6 +311,7 @@ impl<T> Picker<T> {
cursor: 0, cursor: 0,
prompt, prompt,
render_centered, render_centered,
truncate_start: true,
format_fn: Box::new(format_fn), format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
}; };
@ -402,81 +409,35 @@ impl<T: 'static> Component for Picker<T> {
compositor.last_picker = compositor.pop(); compositor.last_picker = compositor.pop();
}))); })));
match key_event { match key_event.into() {
KeyEvent { shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
code: KeyCode::Up, ..
}
| KeyEvent {
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_up(); self.move_up();
} }
KeyEvent { key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
code: KeyCode::Down,
..
}
| KeyEvent {
code: KeyCode::Tab, ..
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_down(); self.move_down();
} }
KeyEvent { key!(Esc) | ctrl!('c') => {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => {
return close_fn; return close_fn;
} }
KeyEvent { key!(Enter) => {
code: KeyCode::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;
} }
KeyEvent { ctrl!('s') => {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
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;
} }
KeyEvent { ctrl!('v') => {
code: KeyCode::Char('v'),
modifiers: KeyModifiers::CONTROL,
} => {
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;
} }
KeyEvent { ctrl!(' ') => {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::CONTROL,
} => {
self.save_filter(); self.save_filter();
} }
_ => { _ => {
@ -566,7 +527,7 @@ impl<T: 'static> Component for Picker<T> {
text_style text_style
}, },
true, true,
true, self.truncate_start,
); );
} }
} }

@ -1,5 +1,8 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::{
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; compositor::{Component, Compositor, Context, EventResult},
ctrl, key,
};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use helix_core::Position; use helix_core::Position;
@ -95,27 +98,14 @@ impl<T: Component> Component for Popup<T> {
compositor.pop(); compositor.pop();
}))); })));
match key { match key.into() {
// esc or ctrl-c aborts the completion and closes the menu // esc or ctrl-c aborts the completion and closes the menu
KeyEvent { key!(Esc) | ctrl!('c') => close_fn,
code: KeyCode::Esc, .. ctrl!('d') => {
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => close_fn,
KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL,
} => {
self.scroll(self.size.1 as usize / 2, true); self.scroll(self.size.1 as usize / 2, true);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
KeyEvent { ctrl!('u') => {
code: KeyCode::Char('u'),
modifiers: KeyModifiers::CONTROL,
} => {
self.scroll(self.size.1 as usize / 2, false); self.scroll(self.size.1 as usize / 2, false);
EventResult::Consumed(None) EventResult::Consumed(None)
} }

@ -1,6 +1,8 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Compositor, Context, EventResult};
use crate::ui; use crate::{alt, ctrl, key, shift, ui};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::Event;
use helix_view::input::KeyEvent;
use helix_view::keyboard::{KeyCode, KeyModifiers};
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
@ -212,6 +214,14 @@ impl Prompt {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
} }
pub fn delete_char_forwards(&mut self) {
let pos = self.eval_movement(Movement::ForwardChar(1));
self.line.replace_range(self.cursor..pos, "");
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn delete_word_backwards(&mut self) { pub fn delete_word_backwards(&mut self) {
let pos = self.eval_movement(Movement::BackwardWord(1)); let pos = self.eval_movement(Movement::BackwardWord(1));
self.line.replace_range(pos..self.cursor, ""); self.line.replace_range(pos..self.cursor, "");
@ -221,6 +231,23 @@ impl Prompt {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
} }
pub fn delete_word_forwards(&mut self) {
let pos = self.eval_movement(Movement::ForwardWord(1));
self.line.replace_range(self.cursor..pos, "");
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn kill_to_start_of_line(&mut self) {
let pos = self.eval_movement(Movement::StartOfLine);
self.line.replace_range(pos..self.cursor, "");
self.cursor = pos;
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn kill_to_end_of_line(&mut self) { pub fn kill_to_end_of_line(&mut self) {
let pos = self.eval_movement(Movement::EndOfLine); let pos = self.eval_movement(Movement::EndOfLine);
self.line.replace_range(self.cursor..pos, ""); self.line.replace_range(self.cursor..pos, "");
@ -404,84 +431,30 @@ impl Component for Prompt {
compositor.pop(); compositor.pop();
}))); })));
match event { match event.into() {
KeyEvent { ctrl!('c') | key!(Esc) => {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Esc, ..
} => {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort); (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn; return close_fn;
} }
KeyEvent { alt!('b') | alt!(Left) => self.move_cursor(Movement::BackwardWord(1)),
code: KeyCode::Left, alt!('f') | alt!(Right) => self.move_cursor(Movement::ForwardWord(1)),
modifiers: KeyModifiers::ALT, ctrl!('b') | key!(Left) => self.move_cursor(Movement::BackwardChar(1)),
} ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
| KeyEvent { ctrl!('e') | key!(End) => self.move_end(),
code: KeyCode::Char('b'), ctrl!('a') | key!(Home) => self.move_start(),
modifiers: KeyModifiers::ALT, ctrl!('w') => self.delete_word_backwards(),
} => self.move_cursor(Movement::BackwardWord(1)), alt!('d') => self.delete_word_forwards(),
KeyEvent { ctrl!('k') => self.kill_to_end_of_line(),
code: KeyCode::Right, ctrl!('u') => self.kill_to_start_of_line(),
modifiers: KeyModifiers::ALT, ctrl!('h') | key!(Backspace) => {
}
| KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::ForwardWord(1)),
KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Right,
..
} => self.move_cursor(Movement::ForwardChar(1)),
KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Left,
..
} => self.move_cursor(Movement::BackwardChar(1)),
KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
} => self.move_end(),
KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
} => self.move_start(),
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL,
} => self.delete_word_backwards(),
KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => self.kill_to_end_of_line(),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE,
} => {
self.delete_char_backwards(); self.delete_char_backwards();
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
KeyEvent { ctrl!('d') | key!(Delete) => {
code: KeyCode::Char('s'), self.delete_char_forwards();
modifiers: KeyModifiers::CONTROL, (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} => { }
ctrl!('s') => {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..); let text = doc.text().slice(..);
@ -491,6 +464,7 @@ impl Component for Prompt {
doc.selection(view.id).primary(), doc.selection(view.id).primary(),
textobject::TextObject::Inside, textobject::TextObject::Inside,
1, 1,
false,
); );
let line = text.slice(range.from()..range.to()).to_string(); let line = text.slice(range.from()..range.to()).to_string();
if !line.is_empty() { if !line.is_empty() {
@ -498,10 +472,7 @@ impl Component for Prompt {
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { key!(Enter) => {
code: KeyCode::Enter,
..
} => {
if self.selection.is_some() && self.line.ends_with('/') { if self.selection.is_some() && self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
self.exit_selection(); self.exit_selection();
@ -516,50 +487,29 @@ impl Component for Prompt {
return close_fn; return close_fn;
} }
} }
KeyEvent { ctrl!('p') | key!(Up) => {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Up, ..
} => {
if let Some(register) = self.history_register { if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register); let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward); self.change_history(register.read(), CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { ctrl!('n') | key!(Down) => {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Down,
..
} => {
if let Some(register) = self.history_register { if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register); let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward); self.change_history(register.read(), CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { key!(Tab) => {
code: KeyCode::Tab, ..
} => {
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)
} }
KeyEvent { shift!(BackTab) => {
code: KeyCode::BackTab,
..
} => {
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)
} }
KeyEvent { ctrl!('q') => self.exit_selection(),
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
} => self.exit_selection(),
// any char event that's not combined with control or mapped to any other combo // any char event that's not combined with control or mapped to any other combo
KeyEvent { KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),

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

@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192;
const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4); const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode { pub enum Mode {
Normal, Normal,
@ -96,12 +98,13 @@ pub struct Document {
// It can be used as a cell where we will take it out to get some parts of the history and put // It can be used as a cell where we will take it out to get some parts of the history and put
// it back as it separated from the edits. We could split out the parts manually but that will // it back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome. // be more troublesome.
history: Cell<History>, pub history: Cell<History>,
pub savepoint: Option<Transaction>, pub savepoint: Option<Transaction>,
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>>,
@ -125,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()
@ -342,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,
} }
} }
@ -494,7 +499,9 @@ impl Document {
/// Detect the programming language based on the file type. /// Detect the programming language based on the file type.
pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) { pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
if let Some(path) = &self.path { if let Some(path) = &self.path {
let language_config = config_loader.language_config_for_file_name(path); let language_config = config_loader
.language_config_for_file_name(path)
.or_else(|| config_loader.language_config_for_shebang(self.text()));
self.set_language(theme, language_config); self.set_language(theme, language_config);
} }
} }
@ -635,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() {
@ -749,19 +759,35 @@ impl Document {
} }
/// Undo modifications to the [`Document`] according to `uk`. /// Undo modifications to the [`Document`] according to `uk`.
pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
let txns = self.history.get_mut().earlier(uk); let txns = self.history.get_mut().earlier(uk);
let mut success = false;
for txn in txns { for txn in txns {
self.apply_impl(&txn, view_id); if self.apply_impl(&txn, view_id) {
success = true;
}
}
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
} }
success
} }
/// Redo modifications to the [`Document`] according to `uk`. /// Redo modifications to the [`Document`] according to `uk`.
pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
let txns = self.history.get_mut().later(uk); let txns = self.history.get_mut().later(uk);
let mut success = false;
for txn in txns { for txn in txns {
self.apply_impl(&txn, view_id); if self.apply_impl(&txn, view_id) {
success = true;
}
} }
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
}
success
} }
/// Commit pending changes to history /// Commit pending changes to history

@ -1,5 +1,6 @@
use crate::{ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::SCRATCH_BUFFER_NAME,
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
theme::{self, Theme}, theme::{self, Theme},
tree::{self, Tree}, tree::{self, Tree},
@ -9,6 +10,8 @@ use crate::{
use futures_util::future; use futures_util::future;
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
io::stdin,
num::NonZeroUsize,
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
@ -16,12 +19,12 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep}; use tokio::time::{sleep, Duration, Instant, Sleep};
use anyhow::Error; use anyhow::{bail, Context, 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;
use helix_core::syntax; use helix_core::syntax;
use helix_core::Position; use helix_core::{Position, Selection};
use serde::Deserialize; use serde::Deserialize;
@ -33,6 +36,46 @@ where
Ok(Duration::from_millis(millis)) Ok(Duration::from_millis(millis))
} }
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct FilePickerConfig {
/// IgnoreOptions
/// Enables ignoring hidden files.
/// Whether to hide hidden files in file picker and global search results. Defaults to true.
pub hidden: bool,
/// Enables reading ignore files from parent directories. Defaults to true.
pub parents: bool,
/// Enables reading `.ignore` files.
/// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
pub ignore: bool,
/// Enables reading `.gitignore` files.
/// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
pub git_ignore: bool,
/// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
/// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
pub git_global: bool,
/// Enables reading `.git/info/exclude` files.
/// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
pub git_exclude: bool,
/// WalkBuilder options
/// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
pub max_depth: Option<usize>,
}
impl Default for FilePickerConfig {
fn default() -> Self {
Self {
hidden: true,
parents: true,
ignore: true,
git_ignore: true,
git_global: true,
git_exclude: true,
max_depth: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config { pub struct Config {
@ -60,9 +103,10 @@ pub struct Config {
pub completion_trigger_len: u8, pub completion_trigger_len: u8,
/// Whether to display infoboxes. Defaults to true. /// Whether to display infoboxes. Defaults to true.
pub auto_info: bool, pub auto_info: bool,
pub file_picker: FilePickerConfig,
} }
#[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
@ -91,6 +135,7 @@ impl Default for Config {
idle_timeout: Duration::from_millis(400), idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2, completion_trigger_len: 2,
auto_info: true, auto_info: true,
file_picker: FilePickerConfig::default(),
} }
} }
} }
@ -110,7 +155,7 @@ 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>,
@ -128,6 +173,8 @@ pub struct Editor {
pub idle_timer: Pin<Box<Sleep>>, pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>, pub last_motion: Option<Motion>,
pub exit_code: i32,
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@ -141,8 +188,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();
@ -152,20 +199,21 @@ 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(), 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,
idle_timer: Box::pin(sleep(config.idle_timeout)), idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None, last_motion: None,
config, config,
exit_code: 0,
} }
} }
@ -215,7 +263,6 @@ impl Editor {
} }
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> { pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
use anyhow::Context;
let theme = self let theme = self
.theme_loader .theme_loader
.load(theme.as_ref()) .load(theme.as_ref())
@ -224,6 +271,53 @@ impl Editor {
Ok(()) Ok(())
} }
/// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?;
doc.detect_language(Some(&self.theme), &self.syn_loader);
Self::launch_language_server(&mut self.language_servers, doc)
}
/// Launch a language server for a given document
fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
// try to find a language server based on the language name
let language_server = doc.language.as_ref().and_then(|language| {
ls.get(language)
.map_err(|e| {
log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}",
language.scope(),
e
)
})
.ok()
});
if let Some(language_server) = language_server {
// only spawn a new lang server if the servers aren't the same
if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
let language_id = doc
.language()
.and_then(|s| s.split('.').last()) // source.rust
.map(ToOwned::to_owned)
.unwrap_or_default();
// TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
doc.version(),
doc.text(),
language_id,
));
doc.set_language_server(Some(language_server));
}
}
Some(())
}
fn _refresh(&mut self) { fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() { for (view, _) in self.tree.views_mut() {
let doc = &self.documents[&view.doc]; let doc = &self.documents[&view.doc];
@ -231,9 +325,28 @@ impl Editor {
} }
} }
fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
let view = self.tree.get_mut(current_view);
view.doc = doc_id;
view.offset = Position::default();
let doc = self.documents.get_mut(&doc_id).unwrap();
// initialize selection for view
doc.selections
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
}
pub fn switch(&mut self, id: DocumentId, action: Action) { pub fn switch(&mut self, id: DocumentId, action: Action) {
use crate::tree::Layout; use crate::tree::Layout;
use helix_core::Selection;
if !self.documents.contains_key(&id) { if !self.documents.contains_key(&id) {
log::error!("cannot switch to document that does not exist (anymore)"); log::error!("cannot switch to document that does not exist (anymore)");
@ -256,7 +369,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`.
@ -265,40 +379,41 @@ 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]];
}
}
} }
view.doc = id;
view.offset = Position::default();
let (view, doc) = current!(self);
// initialize selection for view let view_id = view.id;
doc.selections self.replace_document_in_view(view_id, id);
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
return; return;
} }
Action::Load => { Action::Load => {
return; let view_id = view!(self).id;
}
Action::HorizontalSplit => {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal);
// initialize selection for view
let doc = self.documents.get_mut(&id).unwrap(); let doc = self.documents.get_mut(&id).unwrap();
if doc.selections().is_empty() {
doc.selections.insert(view_id, Selection::point(0)); doc.selections.insert(view_id, Selection::point(0));
} }
Action::VerticalSplit => { return;
}
Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id); let view = View::new(id);
let view_id = self.tree.split(view, Layout::Vertical); let view_id = self.tree.split(
view,
match action {
Action::HorizontalSplit => Layout::Horizontal,
Action::VerticalSplit => Layout::Vertical,
_ => unreachable!(),
},
);
// initialize selection for view // 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));
@ -308,73 +423,51 @@ impl Editor {
self._refresh(); self._refresh();
} }
pub fn new_file(&mut self, action: Action) -> 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;
let mut doc = Document::default(); // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
self.next_document_id =
DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
doc.id = id; doc.id = id;
self.documents.insert(id, doc); self.documents.insert(id, doc);
id
}
fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
let id = self.new_document(doc);
self.switch(id, action); self.switch(id, action);
id id
} }
pub fn new_file(&mut self, action: Action) -> DocumentId {
self.new_file_from_document(action, Document::default())
}
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
}
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 {
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)); self.new_document(doc)
}
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);
Ok(id) Ok(id)
} }
pub fn close(&mut self, id: ViewId, close_buffer: bool) { pub fn close(&mut self, id: ViewId) {
let view = self.tree.get(self.tree.focus); let view = self.tree.get(self.tree.focus);
// remove selection // remove selection
self.documents self.documents
@ -383,18 +476,66 @@ impl Editor {
.selections .selections
.remove(&id); .remove(&id);
if close_buffer { self.tree.remove(id);
// get around borrowck issues self._refresh();
let doc = &self.documents[&view.doc]; }
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
let doc = match self.documents.get(&doc_id) {
Some(doc) => doc,
None => bail!("document does not exist"),
};
if !force && doc.is_modified() {
bail!(
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
);
}
if let Some(language_server) = doc.language_server() { if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }
self.documents.remove(&view.doc);
let views_to_close = self
.tree
.views()
.filter_map(|(view, _focus)| {
if view.doc == doc_id {
Some(view.id)
} else {
None
}
})
.collect::<Vec<_>>();
for view_id in views_to_close {
self.close(view_id);
}
self.documents.remove(&doc_id);
// If the document we removed was visible in all views, we will have no more views. We don't
// want to close the editor just for a simple buffer close, so we need to create a new view
// containing either an existing document, or a brand new document.
if self.tree.views().next().is_none() {
let doc_id = self
.documents
.iter()
.map(|(&doc_id, _)| doc_id)
.next()
.unwrap_or_else(|| self.new_document(Document::default()));
let view = View::new(doc_id);
let view_id = self.tree.insert(view);
let doc = self.documents.get_mut(&doc_id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
} }
self.tree.remove(id);
self._refresh(); self._refresh();
Ok(())
} }
pub fn resize(&mut self, area: Rect) { pub fn resize(&mut self, area: Rect) {
@ -464,8 +605,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()

@ -25,7 +25,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,
@ -33,17 +33,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,95 @@
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 Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
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
}
}

@ -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;

@ -11,10 +11,19 @@
/// Returns `(&mut View, &mut Document)` /// Returns `(&mut View, &mut Document)`
#[macro_export] #[macro_export]
macro_rules! current { macro_rules! current {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
let view = $crate::view_mut!( $( $editor ).+ ); let view = $crate::view_mut!($editor);
let id = view.doc; let id = view.doc;
let doc = $( $editor ).+ .documents.get_mut(&id).unwrap(); let doc = $editor.documents.get_mut(&id).unwrap();
(view, doc)
}};
}
#[macro_export]
macro_rules! current_ref {
($editor:expr) => {{
let view = $editor.tree.get($editor.tree.focus);
let doc = &$editor.documents[&view.doc];
(view, doc) (view, doc)
}}; }};
} }
@ -23,8 +32,8 @@ macro_rules! current {
/// Returns `&mut Document` /// Returns `&mut Document`
#[macro_export] #[macro_export]
macro_rules! doc_mut { macro_rules! doc_mut {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$crate::current!( $( $editor ).+ ).1 $crate::current!($editor).1
}}; }};
} }
@ -32,8 +41,8 @@ macro_rules! doc_mut {
/// Returns `&mut View` /// Returns `&mut View`
#[macro_export] #[macro_export]
macro_rules! view_mut { macro_rules! view_mut {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus) $editor.tree.get_mut($editor.tree.focus)
}}; }};
} }
@ -41,23 +50,14 @@ macro_rules! view_mut {
/// Returns `&View` /// Returns `&View`
#[macro_export] #[macro_export]
macro_rules! view { macro_rules! view {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$( $editor ).+ .tree.get($( $editor ).+ .tree.focus) $editor.tree.get($editor.tree.focus)
}}; }};
} }
#[macro_export] #[macro_export]
macro_rules! doc { macro_rules! doc {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$crate::current_ref!( $( $editor ).+ ).1 $crate::current_ref!($editor).1
}};
}
#[macro_export]
macro_rules! current_ref {
( $( $editor:ident ).+ ) => {{
let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
let doc = &$( $editor ).+ .documents[&view.doc];
(view, doc)
}}; }};
} }

@ -78,8 +78,11 @@ impl Loader {
#[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 +91,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,21 +107,36 @@ 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_else(|| Style::default().fg(Color::Rgb(0, 0, 255))) .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))

@ -314,6 +314,9 @@ impl Tree {
pub fn recalculate(&mut self) { pub fn recalculate(&mut self) {
if self.is_empty() { if self.is_empty() {
// There are no more views, so the tree should focus itself again.
self.focus = self.root;
return; return;
} }

@ -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,
@ -54,8 +58,14 @@ impl JumpList {
None None
} }
} }
pub fn remove(&mut self, doc_id: &DocumentId) {
self.jumps.retain(|(other_id, _)| other_id != doc_id);
}
} }
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,
@ -65,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 {
@ -76,16 +91,31 @@ 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
} }
pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { //
pub fn offset_coords_to_in_view(
&self,
doc: &Document,
scrolloff: usize,
) -> Option<(usize, usize)> {
let cursor = doc let cursor = doc
.selection(self.id) .selection(self.id)
.primary() .primary()
@ -104,21 +134,41 @@ impl View {
let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
if line > last_line.saturating_sub(scrolloff) { let row = if line > last_line.saturating_sub(scrolloff) {
// scroll down // scroll down
self.offset.row += line - (last_line.saturating_sub(scrolloff)); self.offset.row + line - (last_line.saturating_sub(scrolloff))
} else if line < self.offset.row + scrolloff { } else if line < self.offset.row + scrolloff {
// scroll up // scroll up
self.offset.row = line.saturating_sub(scrolloff); line.saturating_sub(scrolloff)
} } else {
self.offset.row
};
if col > last_col.saturating_sub(scrolloff) { let col = if col > last_col.saturating_sub(scrolloff) {
// scroll right // scroll right
self.offset.col += col - (last_col.saturating_sub(scrolloff)); self.offset.col + col - (last_col.saturating_sub(scrolloff))
} else if col < self.offset.col + scrolloff { } else if col < self.offset.col + scrolloff {
// scroll left // scroll left
self.offset.col = col.saturating_sub(scrolloff); col.saturating_sub(scrolloff)
} else {
self.offset.col
};
if row == self.offset.row && col == self.offset.col {
None
} else {
Some((row, col))
}
} }
pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) {
self.offset.row = row;
self.offset.col = col;
}
}
pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool {
self.offset_coords_to_in_view(doc, scrolloff).is_none()
} }
/// Calculates the last visible line on screen /// Calculates the last visible line on screen
@ -247,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() {

@ -37,12 +37,25 @@ name = "elixir"
scope = "source.elixir" scope = "source.elixir"
injection-regex = "elixir" injection-regex = "elixir"
file-types = ["ex", "exs"] file-types = ["ex", "exs"]
shebangs = ["elixir"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
language-server = { command = "elixir-ls" } language-server = { command = "elixir-ls" }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[language]]
name = "mint"
scope = "source.mint"
injection-regex = "mint"
file-types = ["mint"]
shebangs = []
roots = []
comment-token = "//"
language-server = { command = "mint", args = ["ls"] }
indent = { tab-width = 2, unit = " " }
[[language]] [[language]]
name = "json" name = "json"
scope = "source.json" scope = "source.json"
@ -102,6 +115,7 @@ name = "javascript"
scope = "source.js" scope = "source.js"
injection-regex = "^(js|javascript)$" injection-regex = "^(js|javascript)$"
file-types = ["js", "mjs"] file-types = ["js", "mjs"]
shebangs = ["node"]
roots = [] roots = []
comment-token = "//" comment-token = "//"
# TODO: highlights-jsx, highlights-params # TODO: highlights-jsx, highlights-params
@ -113,6 +127,7 @@ name = "typescript"
scope = "source.ts" scope = "source.ts"
injection-regex = "^(ts|typescript)$" injection-regex = "^(ts|typescript)$"
file-types = ["ts"] file-types = ["ts"]
shebangs = []
roots = [] roots = []
# TODO: highlights-jsx, highlights-params # TODO: highlights-jsx, highlights-params
@ -153,6 +168,7 @@ name = "python"
scope = "source.python" scope = "source.python"
injection-regex = "python" injection-regex = "python"
file-types = ["py"] file-types = ["py"]
shebangs = ["python"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -165,6 +181,7 @@ name = "nix"
scope = "source.nix" scope = "source.nix"
injection-regex = "nix" injection-regex = "nix"
file-types = ["nix"] file-types = ["nix"]
shebangs = []
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -176,6 +193,7 @@ name = "ruby"
scope = "source.ruby" scope = "source.ruby"
injection-regex = "ruby" injection-regex = "ruby"
file-types = ["rb"] file-types = ["rb"]
shebangs = ["ruby"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -187,6 +205,7 @@ name = "bash"
scope = "source.bash" scope = "source.bash"
injection-regex = "bash" injection-regex = "bash"
file-types = ["sh", "bash"] file-types = ["sh", "bash"]
shebangs = ["sh", "bash", "dash"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -198,6 +217,7 @@ name = "php"
scope = "source.php" scope = "source.php"
injection-regex = "php" injection-regex = "php"
file-types = ["php"] file-types = ["php"]
shebangs = ["php"]
roots = [] roots = []
indent = { tab-width = 4, unit = " " } indent = { tab-width = 4, unit = " " }
@ -259,6 +279,7 @@ name = "ocaml"
scope = "source.ocaml" scope = "source.ocaml"
injection-regex = "ocaml" injection-regex = "ocaml"
file-types = ["ml"] file-types = ["ml"]
shebangs = []
roots = [] roots = []
comment-token = "(**)" comment-token = "(**)"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
@ -267,6 +288,7 @@ indent = { tab-width = 2, unit = " " }
name = "ocaml-interface" name = "ocaml-interface"
scope = "source.ocaml.interface" scope = "source.ocaml.interface"
file-types = ["mli"] file-types = ["mli"]
shebangs = []
roots = [] roots = []
comment-token = "(**)" comment-token = "(**)"
indent = { tab-width = 2, unit = " "} indent = { tab-width = 2, unit = " "}
@ -275,6 +297,7 @@ indent = { tab-width = 2, unit = " "}
name = "lua" name = "lua"
scope = "source.lua" scope = "source.lua"
file-types = ["lua"] file-types = ["lua"]
shebangs = ["lua"]
roots = [] roots = []
comment-token = "--" comment-token = "--"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
@ -332,6 +355,7 @@ name = "prolog"
scope = "source.prolog" scope = "source.prolog"
roots = [] roots = []
file-types = ["pl", "prolog"] file-types = ["pl", "prolog"]
shebangs = ["swipl"]
comment-token = "%" comment-token = "%"
language-server = { command = "swipl", args = [ language-server = { command = "swipl", args = [
@ -355,3 +379,45 @@ roots = []
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "cmake-language-server" } language-server = { command = "cmake-language-server" }
[[language]]
name = "glsl"
scope = "source.glsl"
file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
[[language]]
name = "perl"
scope = "source.perl"
file-types = ["pl", "pm"]
shebangs = ["perl"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
[[language]]
name = "racket"
scope = "source.rkt"
roots = []
file-types = ["rkt"]
shebangs = ["racket"]
comment-token = ";"
language-server = { command = "racket", args = ["-l", "racket-langserver"] }
[[language]]
name = "wgsl"
scope = "source.wgsl"
file-types = ["wgsl"]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
[[language]]
name = "llvm"
scope = "source.llvm"
roots = []
file-types = ["ll"]
comment-token = ";"
indent = { tab-width = 2, unit = " " }

@ -0,0 +1,37 @@
; inherits: c
[
"in"
"out"
"inout"
"uniform"
"shared"
"layout"
"attribute"
"varying"
"buffer"
"coherent"
"readonly"
"writeonly"
"precision"
"highp"
"mediump"
"lowp"
"centroid"
"sample"
"patch"
"smooth"
"flat"
"noperspective"
"invariant"
"precise"
] @keyword
"subroutine" @keyword.function
(extension_storage_class) @attribute
(
(identifier) @variable.builtin
(#match? @variable.builtin "^gl_")
)

@ -0,0 +1,19 @@
indent = [
"init_declarator",
"compound_statement",
"preproc_arg",
"field_declaration_list",
"case_statement",
"conditional_expression",
"enumerator_list",
"struct_specifier",
"compound_literal_expression"
]
outdent = [
"#define",
"#ifdef",
"#endif",
"{",
"}"
]

@ -0,0 +1,3 @@
(preproc_arg) @glsl
(comment) @comment

@ -0,0 +1,9 @@
indent = [
"object",
"array"
]
outdent = [
"]",
"}"
]

@ -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,181 @@
; Variables
(variable_declaration
.
(scope) @keyword)
[
(single_var_declaration)
(scalar_variable)
(array_variable)
(hash_variable)
(hash_variable)
] @variable
[
(package_name)
(special_scalar_variable)
(special_array_variable)
(special_hash_variable)
(special_literal)
(super)
] @constant
(
[
(package_name)
(super)
]
.
("::" @operator)
)
(comments) @comment
(pod_statement) @comment.block.documentation
[
(use_no_statement)
(use_no_feature_statement)
(use_no_if_statement)
(use_no_version)
(use_constant_statement)
(use_parent_statement)
] @keyword
(use_constant_statement
constant: (identifier) @constant)
[
"require"
] @keyword
(method_invocation
.
(identifier) @variable)
(method_invocation
(arrow_operator)
.
(identifier) @function)
(method_invocation
function_name: (identifier) @function)
(named_block_statement
function_name: (identifier) @function)
(call_expression
function_name: (identifier) @function)
(function_definition
name: (identifier) @function)
[
(function)
(map)
(grep)
(bless)
] @function
[
"return"
"sub"
"package"
"BEGIN"
"END"
] @keyword.function
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
(standard_input_to_variable) @punctuation.bracket
[
"=~"
"or"
"="
"=="
"+"
"-"
"."
"//"
"||"
(arrow_operator)
(hash_arrow_operator)
(array_dereference)
(hash_dereference)
(to_reference)
(type_glob)
(hash_access_variable)
(ternary_expression)
(ternary_expression_in_hash)
] @operator
[
(regex_option)
(regex_option_for_substitution)
(regex_option_for_transliteration)
] @variable.parameter
(type_glob
(identifier) @variable)
(
(scalar_variable)
.
("->" @operator))
[
(word_list_qw)
(command_qx_quoted)
(string_single_quoted)
(string_double_quoted)
(string_qq_quoted)
(bareword)
(transliteration_tr_or_y)
] @string
[
(regex_pattern_qr)
(patter_matcher_m)
(substitution_pattern_s)
] @string.regexp
(escape_sequence) @string.special
[
","
(semi_colon)
(start_delimiter)
(end_delimiter)
(ellipsis_statement)
] @punctuation.delimiter
[
(integer)
(floating_point)
(scientific_notation)
(hexadecimal)
] @constant.numeric
[
; (if_statement)
(unless_statement)
(if_simple_statement)
(unless_simple_statement)
] @keyword.control.conditional
[
"if"
"elsif"
"else"
] @keyword.control.conditional
(foreach_statement) @keyword.control.repeat
(foreach_statement
.
(scope) @keyword)
(function_attribute) @label
(function_signature) @type

@ -0,0 +1,8 @@
(function_definition
(identifier) (_) @function.inside) @function.around
(anonymous_function
(_) @function.inside) @function.around
(argument
(_) @parameter.inside)

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

@ -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,11 +10,11 @@
# Polar Night # Polar Night
# nord0 - background color # nord0 - background color
"ui.background" = { bg = "nord0" } "ui.background" = { bg = "nord0" }
"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" } "ui.statusline.inactive" = { fg = "nord8", bg = "nord1" }
# nord1 - status bars, panels, modals, autocompletion # nord1 - status bars, panels, modals, autocompletion
"ui.statusline" = { fg = "nord8", bg = "nord1" } "ui.statusline" = { fg = "nord4", bg = "#4c566a" }
"ui.popup" = { bg = "#232d38" } "ui.popup" = { bg = "#232d38" }
"ui.window" = { bg = "#232d38" } "ui.window" = { bg = "#232d38" }
"ui.help" = { bg = "#232d38", fg = "nord4" } "ui.help" = { bg = "#232d38", fg = "nord4" }
@ -25,7 +25,7 @@
# nord3 - comments, nord3 based lighter color # nord3 - comments, nord3 based lighter color
# relative: https://github.com/arcticicestudio/nord/issues/94 # relative: https://github.com/arcticicestudio/nord/issues/94
"comment" = "gray" "comment" = { fg = "gray", modifiers = ["italic"] }
"ui.linenr" = { fg = "gray" } "ui.linenr" = { fg = "gray" }
# Snow Storm # Snow Storm

@ -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"

@ -0,0 +1,97 @@
"attribute" = { fg = "violet" }
"keyword" = { fg = "green" }
"keyword.directive" = { fg = "orange" }
"namespace" = { fg = "violet" }
"operator" = { fg = "green" }
"special" = { fg = "orange" }
"variable.builtin" = { fg = "cyan", modifiers = ["bold"] }
"variable.function" = { fg = "blue" }
"type" = { fg = "yellow" }
"type.builtin" = { fg = "yellow", modifiers = ["bold"] }
"constructor" = { fg = "blue" }
"function" = { fg = "blue" }
"function.macro" = { fg = "magenta" }
"function.builtin" = { fg = "blue", modifiers = ["bold"] }
"function.special" = { fg = "magenta" }
"comment" = { fg = "base01" }
"string" = { fg = "cyan" }
"constant" = { fg = "cyan" }
"constant.builtin" = { fg = "cyan", modifiers = ["bold"] }
"constant.character.escape" = { fg = "red", modifiers = ["bold"] }
"label" = { fg = "green" }
"module" = { fg = "violet" }
"tag" = { fg = "magenta" }
# 背景
"ui.background" = { bg = "base03" }
# 行号栏
"ui.linenr" = { fg = "base0", bg = "base02" }
# 当前行号栏
"ui.linenr.selected" = { fg = "blue", modifiers = ["bold"] }
# 状态栏
"ui.statusline" = { fg = "base03", bg = "base0" }
# 非活动状态栏
"ui.statusline.inactive" = { fg = "base1", bg = "base01" }
# 补全窗口, preview窗口
"ui.popup" = { bg = "base02" }
# 影响 补全选中 cmd弹出信息选中
"ui.menu.selected" = { fg = "base02", bg = "base2"}
"ui.menu" = { fg = "base1" }
# ??
"ui.window" = { fg = "base3" }
# 命令行 补全的帮助信息
"ui.help" = { modifiers = ["reversed"] }
# 快捷键窗口
"ui.info" = { bg = "base1" }
# 快捷键字体
"ui.info.text" = {fg = "base02", modifiers = ["bold"]}
# 普通ui的字体样式
"ui.text" = { fg = "base1" }
# 影响 picker列表选中, 快捷键帮助窗口文本
"ui.text.focus" = { fg = "blue", modifiers = ["bold"]}
# file picker中 预览的当前选中项
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
"ui.cursor.primary" = {fg = "base03", bg = "base1"}
"ui.selection.primary" = { fg = "base03", bg = "base01" }
"ui.cursor.select" = {fg = "base02", bg = "green"}
"ui.selection" = { fg = "base02", bg = "yellow" }
# normal模式的光标
"ui.cursor" = {fg = "base03", bg = "green"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
"warning" = { fg = "orange", modifiers= ["bold", "underlined"] }
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { mdifiers = ["underlined"] }
[palette]
# 深色 越来越深
base03 = "#002b36"
base02 = "#073642"
base01 = "#586e75"
base00 = "#657b83"
base0 = "#839496"
base1 = "#93a1a1"
base2 = "#eee8d5"
base3 = "#fdf6e3"
# 浅色 越來越浅
yellow = "#b58900"
orange = "#cb4b16"
red = "#dc322f"
magenta = "#d33682"
violet = "#6c71c4"
blue = "#268bd2"
cyan = "#2aa198"
green = "#859900"

@ -0,0 +1,98 @@
"attribute" = { fg = "violet" }
"keyword" = { fg = "green" }
"keyword.directive" = { fg = "orange" }
"namespace" = { fg = "violet" }
"operator" = { fg = "green" }
"special" = { fg = "orange" }
"variable.builtin" = { fg = "cyan", modifiers = ["bold"] }
"variable.function" = { fg = "blue" }
"type" = { fg = "yellow" }
"type.builtin" = { fg = "yellow", modifiers = ["bold"] }
"constructor" = { fg = "blue" }
"function" = { fg = "blue" }
"function.macro" = { fg = "magenta" }
"function.builtin" = { fg = "blue", modifiers = ["bold"] }
"function.special" = { fg = "magenta" }
"comment" = { fg = "base01" }
"string" = { fg = "cyan" }
"constant" = { fg = "cyan" }
"constant.builtin" = { fg = "cyan", modifiers = ["bold"] }
"constant.character.escape" = { fg = "red", modifiers = ["bold"] }
"label" = { fg = "green" }
"module" = { fg = "violet" }
"tag" = { fg = "magenta" }
# 背景
"ui.background" = { bg = "base03" }
# 行号栏
"ui.linenr" = { fg = "base0", bg = "base02" }
# 当前行号栏
"ui.linenr.selected" = { fg = "blue", modifiers = ["bold"] }
# 状态栏
"ui.statusline" = { fg = "base03", bg = "base0" }
# 非活动状态栏
"ui.statusline.inactive" = { fg = "base1", bg = "base01" }
# 补全窗口, preview窗口
"ui.popup" = { bg = "base02" }
# 影响 补全选中 cmd弹出信息选中
"ui.menu.selected" = { fg = "base02", bg = "base2"}
"ui.menu" = { fg = "base1" }
# ??
"ui.window" = { fg = "base3" }
# 命令行 补全的帮助信息
"ui.help" = { modifiers = ["reversed"] }
# 快捷键窗口
"ui.info" = { bg = "base1" }
# 快捷键字体
"ui.info.text" = {fg = "base02", modifiers = ["bold"]}
# 普通ui的字体样式
"ui.text" = { fg = "base1" }
# 影响 picker列表选中, 快捷键帮助窗口文本
"ui.text.focus" = { fg = "blue", modifiers = ["bold"]}
# file picker中 预览的当前选中项
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
"ui.cursor.primary" = {fg = "base03", bg = "base1"}
"ui.selection.primary" = { fg = "base03", bg = "base01" }
"ui.cursor.select" = {fg = "base02", bg = "green"}
"ui.selection" = { fg = "base02", bg = "yellow" }
# normal模式的光标
"ui.cursor" = {fg = "base03", bg = "green"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
"warning" = { fg = "orange", modifiers= ["bold", "underlined"] }
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { mdifiers = ["underlined"] }
[palette]
red = '#dc322f'
green = '#859900'
yellow = '#b58900'
blue = '#268bd2'
magenta = '#d33682'
cyan = '#2aa198'
orange = '#cb4b16'
violet = '#6c71c4'
# 深色 越来越深
base0 = '#657b83'
base1 = '#586e75'
base2 = '#073642'
base3 = '#002b36'
## 浅色 越來越浅
base00 = '#839496'
base01 = '#93a1a1'
base02 = '#eee8d5'
base03 = '#fdf6e3'

@ -0,0 +1,65 @@
# Author : Koen Van der Auwera <atog@hey.com>
# Based on SpaceBones Light https://github.com/chipotle/spacebones
# https://github.com/chipotle/spacebones/blob/main/SpaceBones%20Light.bbColorScheme
"attribute" = "#b1951d"
"keyword" = { fg = "#3a81c3" }
"keyword.directive" = "#3a81c3"
"namespace" = "#b1951d"
"punctuation" = "#6c3163"
"punctuation.delimiter" = "#6c3163"
"operator" = "#ba2f59"
"special" = "#ba2f59"
"property" = "#7590db"
"variable.property" = "#7590db"
"variable" = "#715ab1"
"variable.builtin" = "#715ab1"
"variable.parameter" = "#7590db"
"type" = "#6c3163"
"type.builtin" = "#6c3163"
"constructor" = { fg = "#4e3163", modifiers = ["bold"] }
"function" = { fg = "#715ab1", modifiers = ["bold"] }
"function.macro" = "#b1951d"
"function.builtin" = "#b1951d"
"comment" = { fg = "#a49da5", modifiers = ["italic"] }
"constant" = { fg = "#6c3163" }
"constant.builtin" = { fg = "#6c3163", modifiers = ["bold"] }
"string" = "#2d9574"
"number" = "#6c3163"
"escape" = { fg = "fg2", modifiers = ["bold"] }
"label" = "#b1951d"
"module" = "#b1951d"
"warning" = { fg = "#da8b55" }
"error" = { fg = "#e0211d" }
"info" = { fg = "#b1951d" }
"hint" = { fg = "#d1dcdf" }
"ui.background" = { bg = "bg0" }
"ui.linenr" = { fg = "bg3" }
"ui.linenr.selected" = { fg = "#b1951d" }
"ui.statusline" = { fg = "fg1", bg = "bg2" }
"ui.statusline.inactive" = { fg = "fg4", bg = "bg1" }
"ui.popup" = { bg = "bg1" }
"ui.window" = { bg = "bg1" }
"ui.help" = { bg = "bg1", fg = "fg1" }
"ui.text" = { fg = "fg1" }
"ui.text.focus" = { fg = "fg1" }
"ui.selection" = { bg = "bg3", modifiers = ["reversed"] }
"ui.cursor.primary" = { modifiers = ["reversed"] }
"ui.cursor.match" = { modifiers = ["reversed"] }
"ui.menu" = { fg = "fg1", bg = "bg2" }
"ui.menu.selected" = { fg = "#655370", bg = "#d1dcdf", modifiers = ["bold"] }
"diagnostic" = { modifiers = ["underlined"] }
[palette]
bg0 = "#fbf8ef"
bg1 = "#efeae9"
bg2 = "#d1dcdf"
bg3 = "#b4c6cb"
fg1 = "#655370"
fg2 = "#5f3bc4"
fg3 = "#bdae93"
fg4 = "#a89984"

@ -23,8 +23,8 @@ comment = "sirocco"
constant = "white" constant = "white"
"constant.builtin" = "white" "constant.builtin" = "white"
string = "silver" string = "silver"
number = "chamois" "constant.numeric" = "chamois"
escape = "honey" "constant.character.escape" = "honey"
# used for lifetimes # used for lifetimes
label = "honey" label = "honey"

Loading…
Cancel
Save