mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 00:13:28 +02:00
Compare commits
78 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ae5ecfdf66 | ||
|
d545e61644 | ||
|
df217f71c1 | ||
|
d008e86037 | ||
|
b9100fbd44 | ||
|
52d3c29244 | ||
|
17c9a8499e | ||
|
62e6232a32 | ||
|
d8b5d1181f | ||
|
b500a2a138 | ||
|
a3f01503e2 | ||
|
9640ed1425 | ||
|
9baf1ecc90 | ||
|
44cc0d8eb0 | ||
|
1953588873 | ||
|
45793d7c09 | ||
|
4b6aff8c66 | ||
|
4a40e935de | ||
|
716067ba05 | ||
|
c754df12b3 | ||
|
1bf5b103b0 | ||
|
1665bac1b6 | ||
|
278361a086 | ||
|
69fe46a122 | ||
|
86af55c379 | ||
|
0c2b99327a | ||
|
a8a5bcd13d | ||
|
098806ce2a | ||
|
c0d32707d0 | ||
|
d8df10f295 | ||
|
38073fd64c | ||
|
01760c3845 | ||
|
8590f6a912 | ||
|
69378382c3 | ||
|
1a774d61bb | ||
|
1b14e9a19a | ||
|
e46346c907 | ||
|
9887b1275a | ||
|
7cc13fefe9 | ||
|
1a3a924634 | ||
|
aebdef8257 | ||
|
6b3c9d8ed3 | ||
|
4dbc23ff1c | ||
|
b20e4a108c | ||
|
1bb9977faf | ||
|
29962a5bd9 | ||
|
7ef0e2cab6 | ||
|
35feb614b6 | ||
|
5e2ba28e0e | ||
|
83723957fe | ||
|
ae51065213 | ||
|
4e3a343602 | ||
|
81e02e1ba4 | ||
|
c349ceb61f | ||
|
2e4a338944 | ||
|
9c83a98469 | ||
|
1bffb34350 | ||
|
c978d811d9 | ||
|
48df05b16d | ||
|
b873fb9897 | ||
|
8f1eb7b2b0 | ||
|
82fdfdc38e | ||
|
ea6667070f | ||
|
960bc9f134 | ||
|
08f50310bd | ||
|
4bec87ad18 | ||
|
c65b4dea09 | ||
|
6fc0e0b5fb | ||
|
0201ef9205 | ||
|
037f45f24e | ||
|
9821beb5c4 | ||
|
3cee0bf200 | ||
|
4fd38f82a3 | ||
|
b5682f984b | ||
|
68affa3c59 | ||
|
d5de9183ef | ||
|
8d6fad4cac | ||
|
14830e75ff |
5
.envrc
5
.envrc
@@ -1,2 +1,5 @@
|
||||
watch_file shell.nix
|
||||
use flake
|
||||
watch_file flake.lock
|
||||
|
||||
# try to use flakes, if it fails use normal nix (ie. shell.nix)
|
||||
use flake || use nix
|
||||
|
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: C-bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Your issue may already be reported!
|
||||
Please search on the issue tracker before creating one. -->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
<!-- Ideally provide a key sequence and/or asciinema.org recording. -->
|
||||
|
||||
### Environment
|
||||
|
||||
- Platform: <!-- macOS / Windows / Linux -->
|
||||
- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
|
||||
|
||||
<details><summary>~/.cache/helix/helix.log</summary>
|
||||
|
||||
```
|
||||
please provide a copy of `~/.cache/helix/helix.log` here if possible, you may need to redact some of the lines
|
||||
```
|
||||
|
||||
</details>
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ target
|
||||
.direnv
|
||||
helix-term/rustfmt.toml
|
||||
helix-syntax/languages/
|
||||
result
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -86,3 +86,6 @@
|
||||
path = helix-syntax/languages/tree-sitter-elixir
|
||||
url = https://github.com/IceDragon200/tree-sitter-elixir
|
||||
shallow = true
|
||||
[submodule "helix-syntax/languages/tree-sitter-nix"]
|
||||
path = helix-syntax/languages/tree-sitter-nix
|
||||
url = https://github.com/cstrahan/tree-sitter-nix
|
||||
|
53
Cargo.lock
generated
53
Cargo.lock
generated
@@ -79,11 +79,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
|
||||
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"lazy_static",
|
||||
]
|
||||
@@ -238,12 +237,6 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.6"
|
||||
@@ -259,7 +252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-core"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"etcetera",
|
||||
"helix-syntax",
|
||||
@@ -272,35 +265,32 @@ dependencies = [
|
||||
"tendril",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"unicode-general-category",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "helix-lsp"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"glob",
|
||||
"helix-core",
|
||||
"jsonrpc-core",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"once_cell",
|
||||
"pathdiff",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "helix-syntax"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"serde",
|
||||
@@ -310,7 +300,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-term"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -336,7 +326,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-tui"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
@@ -348,7 +338,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-view"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossterm",
|
||||
@@ -429,7 +419,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "jsonrpc-core"
|
||||
version = "17.1.0"
|
||||
source = "git+https://github.com/paritytech/jsonrpc#609d7a6cc160742d035510fa89fb424ccf077660"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4467ab6dfa369b69e52bd0692e480c4d117410538526a57a304a0f2250fd95e"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -596,12 +587,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
@@ -803,9 +788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -986,10 +971,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.17"
|
||||
name = "unicode-general-category"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
|
||||
checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
15
README.md
15
README.md
@@ -41,14 +41,12 @@ cargo install --path helix-term
|
||||
|
||||
This will install the `hx` binary to `$HOME/.cargo/bin`.
|
||||
|
||||
Now copy the `runtime/` directory somewhere. Helix will by default look for the
|
||||
runtime inside the same folder as the executable, but that can be overriden via
|
||||
the `HELIX_RUNTIME` environment variable.
|
||||
Now copy the `runtime/` directory somewhere. Helix will by default look for the runtime
|
||||
inside the config directory or the same directory as executable, but that can be overriden
|
||||
via the `HELIX_RUNTIME` environment variable.
|
||||
|
||||
> NOTE: You should set this to <path to repository>/runtime in development (if
|
||||
> running via cargo).
|
||||
>
|
||||
> `export HELIX_RUNTIME=$PWD/runtime`
|
||||
> NOTE: running via cargo doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
|
||||
> detect the `runtime` directory in the project root.
|
||||
|
||||
If you want to embed the `runtime/` directory into the Helix binary you can build
|
||||
it with:
|
||||
@@ -78,6 +76,9 @@ Some suggestions to get started:
|
||||
|
||||
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/good%20first%20issue) label on the issue tracker.
|
||||
- Help with packaging on various distributions needed!
|
||||
- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
|
||||
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
|
||||
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
|
||||
- If your preferred language is missing, integrating a tree-sitter grammar for
|
||||
it and defining syntax highlight queries for it is straight forward and
|
||||
doesn't require much knowledge of the internals.
|
||||
|
@@ -19,7 +19,7 @@
|
||||
| F | find previous char |
|
||||
| Home | move to the start of the line |
|
||||
| End | move to the end of the line |
|
||||
| m | Jump to matching bracket |
|
||||
| m | Jump to matching bracket |
|
||||
| PageUp | Move page up |
|
||||
| PageDown | Move page down |
|
||||
| ctrl-u | Move half page up |
|
||||
@@ -38,13 +38,14 @@
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| r | replace (single character change) |
|
||||
| r | replace with a character |
|
||||
| R | replace with yanked text |
|
||||
| i | Insert before selection |
|
||||
| a | Insert after selection (append) |
|
||||
| I | Insert at the start of the line |
|
||||
| A | Insert at the end of the line |
|
||||
| o | Open new line below selection |
|
||||
| o | Open new line above selection |
|
||||
| I | Insert at the start of the line |
|
||||
| A | Insert at the end of the line |
|
||||
| o | Open new line below selection |
|
||||
| o | Open new line above selection |
|
||||
| u | Undo change |
|
||||
| U | Redo change |
|
||||
| y | Yank selection |
|
||||
@@ -53,26 +54,26 @@
|
||||
| > | Indent selection |
|
||||
| < | Unindent selection |
|
||||
| = | Format selection |
|
||||
| d | Delete selection |
|
||||
| c | Change selection (delete and enter insert mode) |
|
||||
| d | Delete selection |
|
||||
| c | Change selection (delete and enter insert mode) |
|
||||
|
||||
### Selection manipulation
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| s | Select all regex matches inside selections |
|
||||
| S | Split selection into subselections on regex matches |
|
||||
| alt-s | Split selection on newlines |
|
||||
| ; | Collapse selection onto a single cursor |
|
||||
| alt-; | Flip selection cursor and anchor |
|
||||
| % | Select entire file |
|
||||
| x | Select current line |
|
||||
| X | Extend to next line |
|
||||
| [ | Expand selection to parent syntax node TODO: pick a key |
|
||||
| s | Select all regex matches inside selections |
|
||||
| S | Split selection into subselections on regex matches |
|
||||
| alt-s | Split selection on newlines |
|
||||
| ; | Collapse selection onto a single cursor |
|
||||
| alt-; | Flip selection cursor and anchor |
|
||||
| % | Select entire file |
|
||||
| x | Select current line |
|
||||
| X | Extend to next line |
|
||||
| [ | Expand selection to parent syntax node TODO: pick a key |
|
||||
| J | join lines inside selection |
|
||||
| K | keep selections matching the regex TODO: overlapped by hover help |
|
||||
| space | keep only the primary selection TODO: overlapped by space mode |
|
||||
| ctrl-c | Comment/uncomment the selections |
|
||||
| ctrl-c | Comment/uncomment the selections |
|
||||
|
||||
### Search
|
||||
|
||||
@@ -81,10 +82,10 @@ in reverse, or searching via smartcase.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| / | Search for regex pattern |
|
||||
| n | Select next search match |
|
||||
| N | Add next search match to selection |
|
||||
| * | Use current selection as the search pattern |
|
||||
| / | Search for regex pattern |
|
||||
| n | Select next search match |
|
||||
| N | Add next search match to selection |
|
||||
| * | Use current selection as the search pattern |
|
||||
|
||||
### Diagnostics
|
||||
|
||||
@@ -132,6 +133,7 @@ Jumps to various locations.
|
||||
| e | Go to the end of the file |
|
||||
| h | Go to the start of the line |
|
||||
| l | Go to the end of the line |
|
||||
| s | Go to first non-whitespace character of the line |
|
||||
| t | Go to the top of the screen |
|
||||
| m | Go to the middle of the screen |
|
||||
| b | Go to the bottom of the screen |
|
||||
@@ -139,6 +141,7 @@ Jumps to various locations.
|
||||
| y | Go to type definition |
|
||||
| r | Go to references |
|
||||
| i | Go to implementation |
|
||||
| a | Go to the last accessed/alternate file |
|
||||
|
||||
## Object mode
|
||||
|
||||
@@ -163,5 +166,6 @@ This layer is a kludge of mappings I had under leader key in neovim.
|
||||
|-----|-----------|
|
||||
| f | Open file picker |
|
||||
| b | Open buffer picker |
|
||||
| s | Open symbol picker (current document)|
|
||||
| w | Enter window mode |
|
||||
| space | Keep primary selection TODO: it's here because space mode replaced it |
|
||||
|
124
flake.lock
generated
124
flake.lock
generated
@@ -1,73 +1,83 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"devshell": {
|
||||
"locked": {
|
||||
"lastModified": 1620759905,
|
||||
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=",
|
||||
"lastModified": 1622711433,
|
||||
"narHash": "sha256-rGjXz7FA7HImAT3TtoqwecByLO5yhVPSwPdaYPBFRQw=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8",
|
||||
"repo": "devshell",
|
||||
"rev": "1f4fb67b662b65fa7cfe696fc003fcc1e8f7cc36",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"flakeCompat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1614513358,
|
||||
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
|
||||
"lastModified": 1606424373,
|
||||
"narHash": "sha256-oq8d4//CJOrVj+EcOaSXvMebvuTkmBJuT5tzlfewUnQ=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "99f1c2157fba4bfe6211a321fd0ee43199025dbf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"helix": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1623545930,
|
||||
"narHash": "sha256-14ASoYbxXHU/qPGctiUymb4fMRCoih9c7YujjxqEkdU=",
|
||||
"ref": "master",
|
||||
"rev": "9640ed1425f2db904fb42cd0c54dc6fbc05ca292",
|
||||
"revCount": 821,
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/helix-editor/helix.git"
|
||||
},
|
||||
"original": {
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/helix-editor/helix.git"
|
||||
}
|
||||
},
|
||||
"nixCargoIntegration": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"devshell": "devshell",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rustOverlay": "rustOverlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1620316130,
|
||||
"narHash": "sha256-sU0VS5oJS1FsHsZsLELAXc7G2eIelVuucRw+q5B1x9k=",
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"rev": "a3f40fe42cc6d267ff7518fa3199e99ff1444ac4",
|
||||
"lastModified": 1623560601,
|
||||
"narHash": "sha256-H1Dq461b2m8v/FxmPphd8pOAx4pPja0UE/xvcMUYwwY=",
|
||||
"owner": "yusdacra",
|
||||
"repo": "nix-cargo-integration",
|
||||
"rev": "1238fd751e5d6eb030aee244da9fee6c3ad8b316",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"owner": "yusdacra",
|
||||
"repo": "nix-cargo-integration",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1622059058,
|
||||
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=",
|
||||
"path": "/nix/store/2gam4i1fa1v19k3n5rc9vgvqac1c2xj5-source",
|
||||
"rev": "84aa23742f6c72501f9cc209f29c438766f5352d",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1622194753,
|
||||
"narHash": "sha256-76qtvFp/vFEz46lz5iZMJ0mnsWQYmuGYlb0fHgKqqMg=",
|
||||
"lastModified": 1623324058,
|
||||
"narHash": "sha256-Jm9GUTXdjXz56gWDKy++EpFfjrBaxqXlLvTLfgEi8lo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "540dccb2aeaffa9dc69bfdc41c55abd7ccc6baa3",
|
||||
"rev": "432fc2d9a67f92e05438dff5fdc2b39d33f77997",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,40 +87,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1617325113,
|
||||
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-overlay": "rust-overlay"
|
||||
"flakeCompat": "flakeCompat",
|
||||
"helix": "helix",
|
||||
"nixCargoIntegration": "nixCargoIntegration",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"rustOverlay": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1622257069,
|
||||
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=",
|
||||
"lastModified": 1623550815,
|
||||
"narHash": "sha256-RumRrkE6OTJDndHV4qZNZv8kUGnzwRHZQSyzx29r6/g=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5",
|
||||
"rev": "9824f142cbd7bc3e2a92eefbb79addfff8704cd3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
69
flake.nix
69
flake.nix
@@ -3,31 +3,54 @@
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
naersk.url = "github:nmattia/naersk";
|
||||
nixCargoIntegration = {
|
||||
url = "github:yusdacra/nix-cargo-integration";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flakeCompat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
helix = {
|
||||
url = "https://github.com/helix-editor/helix.git";
|
||||
type = "git";
|
||||
flake = false;
|
||||
submodules = true;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ self, nixpkgs, naersk, rust-overlay, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlay ]; };
|
||||
rust = (pkgs.rustChannelOf {
|
||||
date = "2021-05-01";
|
||||
channel = "nightly";
|
||||
}).minimal; # cargo, rustc and rust-std
|
||||
naerskLib = naersk.lib."${system}".override {
|
||||
# naersk can't build with stable?!
|
||||
# inherit (pkgs.rust-bin.stable.latest) rustc cargo;
|
||||
rustc = rust;
|
||||
cargo = rust;
|
||||
outputs = inputs@{ nixCargoIntegration, helix, ... }:
|
||||
nixCargoIntegration.lib.makeOutputs {
|
||||
root = ./.;
|
||||
buildPlatform = "crate2nix";
|
||||
renameOutputs = { "helix-term" = "helix"; };
|
||||
# Set default app to hx (binary is from helix-term release build)
|
||||
# Set default package to helix-term release build
|
||||
defaultOutputs = { app = "hx"; package = "helix"; };
|
||||
overrides = {
|
||||
crateOverrides = common: _: {
|
||||
helix-term = prev: { buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ]; };
|
||||
# link runtime since helix-core expects it because of embed_runtime feature
|
||||
helix-core = _: { preConfigure = "ln -s ${common.root + "/runtime"} ../runtime"; };
|
||||
# link languages and theme toml files since helix-view expects them
|
||||
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
|
||||
helix-syntax = prev: {
|
||||
src = common.pkgs.runCommand prev.src.name { } ''
|
||||
mkdir -p $out
|
||||
ln -s ${prev.src}/* $out
|
||||
ln -sf ${helix}/helix-syntax/languages $out
|
||||
'';
|
||||
};
|
||||
};
|
||||
in rec {
|
||||
packages.helix = naerskLib.buildPackage {
|
||||
pname = "helix";
|
||||
root = ./.;
|
||||
shell = common: prev: {
|
||||
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb ]);
|
||||
env = prev.env ++ [
|
||||
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
|
||||
{ name = "RUST_BACKTRACE"; value = "1"; }
|
||||
{ name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
|
||||
];
|
||||
};
|
||||
defaultPackage = packages.helix;
|
||||
devShell = pkgs.callPackage ./shell.nix {};
|
||||
});
|
||||
build = _: prev: { rootFeatures = prev.rootFeatures ++ [ "embed_runtime" ]; };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-core"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -17,8 +17,9 @@ helix-syntax = { path = "../helix-syntax" }
|
||||
ropey = "1.2"
|
||||
smallvec = "1.4"
|
||||
tendril = "0.4.2"
|
||||
unicode-segmentation = "1.6"
|
||||
unicode-segmentation = "1.7.1"
|
||||
unicode-width = "0.1"
|
||||
unicode-general-category = "0.4.0"
|
||||
# slab = "0.4.2"
|
||||
tree-sitter = "0.19"
|
||||
once_cell = "1.4"
|
||||
|
@@ -67,7 +67,7 @@ fn handle_open(
|
||||
|
||||
let mut offs = 0;
|
||||
|
||||
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let pos = range.head;
|
||||
let next = next_char(doc, pos);
|
||||
|
||||
@@ -109,7 +109,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
|
||||
|
||||
let mut offs = 0;
|
||||
|
||||
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |range| {
|
||||
let pos = range.head;
|
||||
let next = next_char(doc, pos);
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
find_first_non_whitespace_char2, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
|
||||
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
|
||||
};
|
||||
use core::ops::Range;
|
||||
use std::borrow::Cow;
|
||||
@@ -14,7 +14,7 @@ fn find_line_comment(
|
||||
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
|
||||
for line in lines {
|
||||
let line_slice = text.line(line);
|
||||
if let Some(pos) = find_first_non_whitespace_char2(line_slice) {
|
||||
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
|
||||
let len = line_slice.len_chars();
|
||||
|
||||
if pos < min {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#[derive(Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
Warning,
|
||||
@@ -6,10 +6,13 @@ pub enum Severity {
|
||||
Hint,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Range {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Diagnostic {
|
||||
pub range: Range,
|
||||
pub line: usize,
|
||||
|
@@ -3,6 +3,8 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
|
||||
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[must_use]
|
||||
pub fn grapheme_width(g: &str) -> usize {
|
||||
if g.as_bytes()[0] <= 127 {
|
||||
@@ -156,6 +158,18 @@ pub struct RopeGraphemes<'a> {
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for RopeGraphemes<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RopeGraphemes<'a> {
|
||||
#[must_use]
|
||||
pub fn new(slice: RopeSlice) -> RopeGraphemes {
|
||||
|
@@ -1,19 +1,61 @@
|
||||
use crate::{ChangeSet, Rope, State, Transaction};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Undo-tree style history store.
|
||||
// Stores the history of changes to a buffer.
|
||||
//
|
||||
// Currently the history is represented as a vector of revisions. The vector
|
||||
// always has at least one element: the empty root revision. Each revision
|
||||
// with the exception of the root has a parent revision, a [Transaction]
|
||||
// that can be applied to its parent to transition from the parent to itself,
|
||||
// and an inversion of that transaction to transition from the parent to its
|
||||
// latest child.
|
||||
//
|
||||
// When using `u` to undo a change, an inverse of the stored transaction will
|
||||
// be applied which will transition the buffer to the parent state.
|
||||
//
|
||||
// Each revision with the exception of the last in the vector also has a
|
||||
// last child revision. When using `U` to redo a change, the last child transaction
|
||||
// will be applied to the current state of the buffer.
|
||||
//
|
||||
// The current revision is the one currently displayed in the buffer.
|
||||
//
|
||||
// Commiting a new revision to the history will update the last child of the
|
||||
// current revision, and push a new revision to the end of the vector.
|
||||
//
|
||||
// Revisions are commited with a timestamp. :earlier and :later can be used
|
||||
// to jump to the closest revision to a moment in time relative to the timestamp
|
||||
// of the current revision plus (:later) or minus (:earlier) the duration
|
||||
// given to the command. If a single integer is given, the editor will instead
|
||||
// jump the given number of revisions in the vector.
|
||||
//
|
||||
// Limitations:
|
||||
// * Changes in selections currently don't commit history changes. The selection
|
||||
// will only be updated to the state after a commited buffer change.
|
||||
// * The vector of history revisions is currently unbounded. This might
|
||||
// cause the memory consumption to grow significantly large during long
|
||||
// editing sessions.
|
||||
// * Because delete transactions currently don't store the text that they
|
||||
// delete, we also store an inversion of the transaction.
|
||||
#[derive(Debug)]
|
||||
pub struct History {
|
||||
revisions: Vec<Revision>,
|
||||
cursor: usize,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
// A single point in history. See [History] for more information.
|
||||
#[derive(Debug)]
|
||||
struct Revision {
|
||||
parent: usize,
|
||||
children: SmallVec<[(usize, Transaction); 1]>,
|
||||
/// The transaction to revert to previous state.
|
||||
revert: Transaction,
|
||||
// selection before, selection after?
|
||||
last_child: Option<NonZeroUsize>,
|
||||
transaction: Transaction,
|
||||
// We need an inversion for undos because delete transactions don't store
|
||||
// the deleted text.
|
||||
inversion: Transaction,
|
||||
timestamp: Instant,
|
||||
}
|
||||
|
||||
impl Default for History {
|
||||
@@ -22,72 +64,253 @@ impl Default for History {
|
||||
Self {
|
||||
revisions: vec![Revision {
|
||||
parent: 0,
|
||||
children: SmallVec::new(),
|
||||
revert: Transaction::from(ChangeSet::new(&Rope::new())),
|
||||
last_child: None,
|
||||
transaction: Transaction::from(ChangeSet::new(&Rope::new())),
|
||||
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
|
||||
timestamp: Instant::now(),
|
||||
}],
|
||||
cursor: 0,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) {
|
||||
// TODO: could store a single transaction, if deletes also stored the text they delete
|
||||
let revert = transaction
|
||||
self.commit_revision_at_timestamp(transaction, original, Instant::now());
|
||||
}
|
||||
|
||||
pub fn commit_revision_at_timestamp(
|
||||
&mut self,
|
||||
transaction: &Transaction,
|
||||
original: &State,
|
||||
timestamp: Instant,
|
||||
) {
|
||||
let inversion = transaction
|
||||
.invert(&original.doc)
|
||||
// Store the current cursor position
|
||||
.with_selection(original.selection.clone());
|
||||
|
||||
let new_cursor = self.revisions.len();
|
||||
let new_current = self.revisions.len();
|
||||
self.revisions[self.current].last_child = NonZeroUsize::new(new_current);
|
||||
self.revisions.push(Revision {
|
||||
parent: self.cursor,
|
||||
children: SmallVec::new(),
|
||||
revert,
|
||||
parent: self.current,
|
||||
last_child: None,
|
||||
transaction: transaction.clone(),
|
||||
inversion,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
// add a reference to the parent
|
||||
self.revisions
|
||||
.get_mut(self.cursor)
|
||||
.unwrap() // TODO: get_unchecked_mut
|
||||
.children
|
||||
.push((new_cursor, transaction.clone()));
|
||||
|
||||
self.cursor = new_cursor;
|
||||
self.current = new_current;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_revision(&self) -> usize {
|
||||
self.cursor
|
||||
self.current
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn at_root(&self) -> bool {
|
||||
self.cursor == 0
|
||||
self.current == 0
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) -> Option<&Transaction> {
|
||||
if self.at_root() {
|
||||
// We're at the root of undo, nothing to do.
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_revision = &self.revisions[self.cursor];
|
||||
|
||||
self.cursor = current_revision.parent;
|
||||
|
||||
Some(¤t_revision.revert)
|
||||
let current_revision = &self.revisions[self.current];
|
||||
self.current = current_revision.parent;
|
||||
Some(¤t_revision.inversion)
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<&Transaction> {
|
||||
let current_revision = &self.revisions[self.cursor];
|
||||
let current_revision = &self.revisions[self.current];
|
||||
let last_child = current_revision.last_child?;
|
||||
self.current = last_child.get();
|
||||
|
||||
// for now, simply pick the latest child (linear undo / redo)
|
||||
if let Some((index, transaction)) = current_revision.children.last() {
|
||||
self.cursor = *index;
|
||||
let last_child_revision = &self.revisions[last_child.get()];
|
||||
Some(&self.revisions[last_child.get()].transaction)
|
||||
}
|
||||
|
||||
return Some(&transaction);
|
||||
fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
|
||||
use std::collections::HashSet;
|
||||
let mut a_path_set = HashSet::new();
|
||||
let mut b_path_set = HashSet::new();
|
||||
loop {
|
||||
a_path_set.insert(a);
|
||||
b_path_set.insert(b);
|
||||
if a_path_set.contains(&b) {
|
||||
return b;
|
||||
}
|
||||
if b_path_set.contains(&a) {
|
||||
return a;
|
||||
}
|
||||
a = self.revisions[a].parent; // Relies on the parent of 0 being 0.
|
||||
b = self.revisions[b].parent; // Same as above.
|
||||
}
|
||||
}
|
||||
|
||||
// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
|
||||
// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
|
||||
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
|
||||
let mut path = Vec::new();
|
||||
while n != a {
|
||||
path.push(n);
|
||||
n = self.revisions[n].parent;
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
|
||||
let lca = self.lowest_common_ancestor(self.current, to);
|
||||
let up = self.path_up(self.current, lca);
|
||||
let down = self.path_up(to, lca);
|
||||
self.current = to;
|
||||
let up_txns = up.iter().map(|&n| self.revisions[n].inversion.clone());
|
||||
let down_txns = down
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&n| self.revisions[n].transaction.clone());
|
||||
up_txns.chain(down_txns).collect()
|
||||
}
|
||||
|
||||
fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
|
||||
self.jump_to(self.current.saturating_sub(delta))
|
||||
}
|
||||
|
||||
fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
|
||||
self.jump_to(
|
||||
self.current
|
||||
.saturating_add(delta)
|
||||
.min(self.revisions.len() - 1),
|
||||
)
|
||||
}
|
||||
|
||||
// Helper for a binary search case below.
|
||||
fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
|
||||
let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
|
||||
let dur_i = self.revisions[i].timestamp.duration_since(instant);
|
||||
use std::cmp::Ordering::*;
|
||||
match dur_im1.cmp(&dur_i) {
|
||||
Less => i - 1,
|
||||
Equal | Greater => i,
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
|
||||
let search_result = self
|
||||
.revisions
|
||||
.binary_search_by(|rev| rev.timestamp.cmp(&instant));
|
||||
let revision = match search_result {
|
||||
Ok(revision) => revision,
|
||||
Err(insert_point) => match insert_point {
|
||||
0 => 0,
|
||||
n if n == self.revisions.len() => n - 1,
|
||||
i => self.revision_closer_to_instant(i, instant),
|
||||
},
|
||||
};
|
||||
self.jump_to(revision)
|
||||
}
|
||||
|
||||
fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
match self.revisions[self.current].timestamp.checked_sub(duration) {
|
||||
Some(instant) => self.jump_instant(instant),
|
||||
None => self.jump_to(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
match self.revisions[self.current].timestamp.checked_add(duration) {
|
||||
Some(instant) => self.jump_instant(instant),
|
||||
None => self.jump_to(self.revisions.len() - 1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
use UndoKind::*;
|
||||
match uk {
|
||||
Steps(n) => self.jump_backward(n),
|
||||
TimePeriod(d) => self.jump_duration_backward(d),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
use UndoKind::*;
|
||||
match uk {
|
||||
Steps(n) => self.jump_forward(n),
|
||||
TimePeriod(d) => self.jump_duration_forward(d),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum UndoKind {
|
||||
Steps(usize),
|
||||
TimePeriod(std::time::Duration),
|
||||
}
|
||||
|
||||
// A subset of sytemd.time time span syntax units.
|
||||
const TIME_UNITS: &[(&[&str], &str, u64)] = &[
|
||||
(&["seconds", "second", "sec", "s"], "seconds", 1),
|
||||
(&["minutes", "minute", "min", "m"], "minutes", 60),
|
||||
(&["hours", "hour", "hr", "h"], "hours", 60 * 60),
|
||||
(&["days", "day", "d"], "days", 24 * 60 * 60),
|
||||
];
|
||||
|
||||
static DURATION_VALIDATION_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
|
||||
|
||||
static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
|
||||
|
||||
fn parse_human_duration(s: &str) -> Result<Duration, String> {
|
||||
if !DURATION_VALIDATION_REGEX.is_match(s) {
|
||||
return Err("duration should be composed \
|
||||
of positive integers followed by time units"
|
||||
.to_string());
|
||||
}
|
||||
|
||||
let mut specified = [false; TIME_UNITS.len()];
|
||||
let mut seconds = 0u64;
|
||||
for cap in NUMBER_UNIT_REGEX.captures_iter(s) {
|
||||
let (n, unit_str) = (&cap[1], &cap[2]);
|
||||
|
||||
let n: u64 = n.parse().map_err(|_| format!("integer too large: {}", n))?;
|
||||
|
||||
let time_unit = TIME_UNITS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, (forms, _, _))| forms.iter().any(|f| f == &unit_str));
|
||||
|
||||
if let Some((i, (_, unit, mul))) = time_unit {
|
||||
if specified[i] {
|
||||
return Err(format!("{} specified more than once", unit));
|
||||
}
|
||||
specified[i] = true;
|
||||
|
||||
let new_seconds = n.checked_mul(*mul).and_then(|s| seconds.checked_add(s));
|
||||
match new_seconds {
|
||||
Some(ns) => seconds = ns,
|
||||
None => return Err("duration too large".to_string()),
|
||||
}
|
||||
} else {
|
||||
return Err(format!("incorrect time unit: {}", unit_str));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Duration::from_secs(seconds))
|
||||
}
|
||||
|
||||
impl std::str::FromStr for UndoKind {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
Ok(Self::Steps(1usize))
|
||||
} else if let Ok(n) = s.parse::<usize>() {
|
||||
Ok(UndoKind::Steps(n))
|
||||
} else {
|
||||
Ok(Self::TimePeriod(parse_human_duration(s)?))
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,4 +366,191 @@ mod test {
|
||||
undo(&mut history, &mut state);
|
||||
assert_eq!("hello", state.doc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_earlier_later() {
|
||||
let mut history = History::default();
|
||||
let doc = Rope::from("a\n");
|
||||
let mut state = State::new(doc);
|
||||
|
||||
fn undo(history: &mut History, state: &mut State) {
|
||||
if let Some(transaction) = history.undo() {
|
||||
transaction.apply(&mut state.doc);
|
||||
}
|
||||
};
|
||||
|
||||
fn earlier(history: &mut History, state: &mut State, uk: UndoKind) {
|
||||
let txns = history.earlier(uk);
|
||||
for txn in txns {
|
||||
txn.apply(&mut state.doc);
|
||||
}
|
||||
};
|
||||
|
||||
fn later(history: &mut History, state: &mut State, uk: UndoKind) {
|
||||
let txns = history.later(uk);
|
||||
for txn in txns {
|
||||
txn.apply(&mut state.doc);
|
||||
}
|
||||
};
|
||||
|
||||
fn commit_change(
|
||||
history: &mut History,
|
||||
state: &mut State,
|
||||
change: crate::transaction::Change,
|
||||
instant: Instant,
|
||||
) {
|
||||
let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter());
|
||||
history.commit_revision_at_timestamp(&txn, &state, instant);
|
||||
txn.apply(&mut state.doc);
|
||||
};
|
||||
|
||||
let t0 = Instant::now();
|
||||
let t = |n| t0.checked_add(Duration::from_secs(n)).unwrap();
|
||||
|
||||
commit_change(&mut history, &mut state, (1, 1, Some(" b".into())), t(0));
|
||||
assert_eq!("a b\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (3, 3, Some(" c".into())), t(10));
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (5, 5, Some(" d".into())), t(20));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
undo(&mut history, &mut state);
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (5, 5, Some(" e".into())), t(30));
|
||||
assert_eq!("a b c e\n", state.doc);
|
||||
|
||||
undo(&mut history, &mut state);
|
||||
undo(&mut history, &mut state);
|
||||
assert_eq!("a b\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (1, 3, None), t(40));
|
||||
assert_eq!("a\n", state.doc);
|
||||
|
||||
commit_change(&mut history, &mut state, (1, 1, Some(" f".into())), t(50));
|
||||
assert_eq!("a f\n", state.doc);
|
||||
|
||||
use UndoKind::*;
|
||||
|
||||
earlier(&mut history, &mut state, Steps(3));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(20, 0)));
|
||||
assert_eq!("a\n", state.doc);
|
||||
|
||||
earlier(&mut history, &mut state, TimePeriod(Duration::new(19, 0)));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
earlier(
|
||||
&mut history,
|
||||
&mut state,
|
||||
TimePeriod(Duration::new(10000, 0)),
|
||||
);
|
||||
assert_eq!("a\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, Steps(50));
|
||||
assert_eq!("a f\n", state.doc);
|
||||
|
||||
earlier(&mut history, &mut state, Steps(4));
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(1, 0)));
|
||||
assert_eq!("a b c\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(5, 0)));
|
||||
assert_eq!("a b c d\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, TimePeriod(Duration::new(6, 0)));
|
||||
assert_eq!("a b c e\n", state.doc);
|
||||
|
||||
later(&mut history, &mut state, Steps(1));
|
||||
assert_eq!("a\n", state.doc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_undo_kind() {
|
||||
use UndoKind::*;
|
||||
|
||||
// Default is one step.
|
||||
assert_eq!("".parse(), Ok(Steps(1)));
|
||||
|
||||
// An integer means the number of steps.
|
||||
assert_eq!("1".parse(), Ok(Steps(1)));
|
||||
assert_eq!(" 16 ".parse(), Ok(Steps(16)));
|
||||
|
||||
// Duration has a strict format.
|
||||
let validation_err = Err("duration should be composed \
|
||||
of positive integers followed by time units"
|
||||
.to_string());
|
||||
assert_eq!(" 16 33".parse::<UndoKind>(), validation_err);
|
||||
assert_eq!(" seconds 22 ".parse::<UndoKind>(), validation_err);
|
||||
assert_eq!(" -4 m".parse::<UndoKind>(), validation_err);
|
||||
assert_eq!("5s 3".parse::<UndoKind>(), validation_err);
|
||||
|
||||
// Units are u64.
|
||||
assert_eq!(
|
||||
"18446744073709551616minutes".parse::<UndoKind>(),
|
||||
Err("integer too large: 18446744073709551616".to_string())
|
||||
);
|
||||
|
||||
// Units are validated.
|
||||
assert_eq!(
|
||||
"1 millenium".parse::<UndoKind>(),
|
||||
Err("incorrect time unit: millenium".to_string())
|
||||
);
|
||||
|
||||
// Units can't be specified twice.
|
||||
assert_eq!(
|
||||
"2 seconds 6s".parse::<UndoKind>(),
|
||||
Err("seconds specified more than once".to_string())
|
||||
);
|
||||
|
||||
// Various formats are correctly handled.
|
||||
assert_eq!(
|
||||
"4s".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(4)))
|
||||
);
|
||||
assert_eq!(
|
||||
"2m".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(120)))
|
||||
);
|
||||
assert_eq!(
|
||||
"5h".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(5 * 60 * 60)))
|
||||
);
|
||||
assert_eq!(
|
||||
"3d".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(3 * 24 * 60 * 60)))
|
||||
);
|
||||
assert_eq!(
|
||||
"1m30s".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(90)))
|
||||
);
|
||||
assert_eq!(
|
||||
"1m 20 seconds".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(80)))
|
||||
);
|
||||
assert_eq!(
|
||||
" 2 minute 1day".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(24 * 60 * 60 + 2 * 60)))
|
||||
);
|
||||
assert_eq!(
|
||||
"3 d 2hour 5 minutes 30sec".parse::<UndoKind>(),
|
||||
Ok(TimePeriod(Duration::from_secs(
|
||||
3 * 24 * 60 * 60 + 2 * 60 * 60 + 5 * 60 + 30
|
||||
)))
|
||||
);
|
||||
|
||||
// Sum overflow is handled.
|
||||
assert_eq!(
|
||||
"18446744073709551615minutes".parse::<UndoKind>(),
|
||||
Err("duration too large".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
"1 minute 18446744073709551615 seconds".parse::<UndoKind>(),
|
||||
Err("duration too large".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -105,11 +105,14 @@ fn suggested_indent_for_line(
|
||||
line_num: usize,
|
||||
tab_width: usize,
|
||||
) -> usize {
|
||||
let line = text.line(line_num);
|
||||
let current = indent_level_for_line(line, tab_width);
|
||||
|
||||
if let Some(start) = find_first_non_whitespace_char(text, line_num) {
|
||||
return suggested_indent_for_pos(Some(language_config), syntax, text, start, false);
|
||||
if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
|
||||
return suggested_indent_for_pos(
|
||||
Some(language_config),
|
||||
syntax,
|
||||
text,
|
||||
start + text.line_to_char(line_num),
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
// if the line is blank, indent should be zero
|
||||
@@ -260,6 +263,7 @@ where
|
||||
highlight_config: OnceCell::new(),
|
||||
//
|
||||
roots: vec![],
|
||||
auto_format: false,
|
||||
language_server: None,
|
||||
indent: Some(IndentationConfiguration {
|
||||
tab_width: 4,
|
||||
|
@@ -3,7 +3,7 @@ pub mod auto_pairs;
|
||||
pub mod comment;
|
||||
pub mod diagnostic;
|
||||
pub mod graphemes;
|
||||
mod history;
|
||||
pub mod history;
|
||||
pub mod indent;
|
||||
pub mod macros;
|
||||
pub mod match_brackets;
|
||||
@@ -16,46 +16,60 @@ pub mod selection;
|
||||
mod state;
|
||||
pub mod syntax;
|
||||
mod transaction;
|
||||
pub mod words;
|
||||
|
||||
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
|
||||
// find first non-whitespace char
|
||||
for (start, ch) in line.chars().enumerate() {
|
||||
// TODO: could use memchr with chunks?
|
||||
if ch != ' ' && ch != '\t' && ch != '\n' {
|
||||
return Some(start);
|
||||
}
|
||||
}
|
||||
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
|
||||
once_cell::sync::Lazy::new(runtime_dir);
|
||||
|
||||
None
|
||||
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
|
||||
line.chars().position(|ch| !ch.is_whitespace())
|
||||
}
|
||||
pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -> Option<usize> {
|
||||
let line = text.line(line_num);
|
||||
let mut start = text.line_to_char(line_num);
|
||||
|
||||
// find first non-whitespace char
|
||||
for ch in line.chars() {
|
||||
// TODO: could use memchr with chunks?
|
||||
if ch != ' ' && ch != '\t' && ch != '\n' {
|
||||
return Some(start);
|
||||
pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
|
||||
let current_dir = std::env::current_dir().expect("unable to determine current directory");
|
||||
|
||||
let root = match root {
|
||||
Some(root) => {
|
||||
let root = std::path::Path::new(root);
|
||||
if root.is_absolute() {
|
||||
root.to_path_buf()
|
||||
} else {
|
||||
current_dir.join(root)
|
||||
}
|
||||
}
|
||||
start += 1;
|
||||
}
|
||||
None => current_dir,
|
||||
};
|
||||
|
||||
for ancestor in root.ancestors() {
|
||||
// TODO: also use defined roots if git isn't found
|
||||
if ancestor.join(".git").is_dir() {
|
||||
return Some(ancestor.to_path_buf());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(embed_runtime))]
|
||||
pub fn runtime_dir() -> std::path::PathBuf {
|
||||
// runtime env var || dir where binary is located
|
||||
std::env::var("HELIX_RUNTIME")
|
||||
.map(|path| path.into())
|
||||
.unwrap_or_else(|_| {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
|
||||
.unwrap()
|
||||
})
|
||||
fn runtime_dir() -> std::path::PathBuf {
|
||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||
return dir.into();
|
||||
}
|
||||
|
||||
const RT_DIR: &str = "runtime";
|
||||
let conf_dir = config_dir().join(RT_DIR);
|
||||
if conf_dir.exists() {
|
||||
return conf_dir;
|
||||
}
|
||||
|
||||
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
|
||||
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
|
||||
return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
|
||||
}
|
||||
|
||||
// fallback to location of the executable being run
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn config_dir() -> std::path::PathBuf {
|
||||
@@ -89,7 +103,6 @@ pub use smallvec::SmallVec;
|
||||
pub use syntax::Syntax;
|
||||
|
||||
pub use diagnostic::Diagnostic;
|
||||
pub use history::History;
|
||||
pub use state::State;
|
||||
|
||||
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};
|
||||
|
@@ -1,45 +1,62 @@
|
||||
use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
|
||||
use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
|
||||
use std::iter::{self, from_fn, Peekable, SkipWhile};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
use ropey::iter::Chars;
|
||||
|
||||
use crate::{
|
||||
coords_at_pos,
|
||||
graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
|
||||
pos_at_coords, Position, Range, RopeSlice,
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Movement {
|
||||
Extend,
|
||||
Move,
|
||||
}
|
||||
|
||||
pub fn move_horizontally(
|
||||
text: RopeSlice,
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
dir: Direction,
|
||||
count: usize,
|
||||
extend: bool,
|
||||
behaviour: Movement,
|
||||
) -> Range {
|
||||
let pos = range.head;
|
||||
let line = text.char_to_line(pos);
|
||||
let line = slice.char_to_line(pos);
|
||||
// TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
|
||||
// we stop calculating past start/end of line.
|
||||
let pos = match dir {
|
||||
Direction::Backward => {
|
||||
let start = text.line_to_char(line);
|
||||
nth_prev_grapheme_boundary(text, pos, count).max(start)
|
||||
let start = slice.line_to_char(line);
|
||||
nth_prev_grapheme_boundary(slice, pos, count).max(start)
|
||||
}
|
||||
Direction::Forward => {
|
||||
// Line end is pos at the start of next line - 1
|
||||
let end = text.line_to_char(line + 1).saturating_sub(1);
|
||||
nth_next_grapheme_boundary(text, pos, count).min(end)
|
||||
let end = slice.line_to_char(line + 1).saturating_sub(1);
|
||||
nth_next_grapheme_boundary(slice, pos, count).min(end)
|
||||
}
|
||||
};
|
||||
Range::new(if extend { range.anchor } else { pos }, pos)
|
||||
let anchor = match behaviour {
|
||||
Movement::Extend => range.anchor,
|
||||
Movement::Move => pos,
|
||||
};
|
||||
Range::new(anchor, pos)
|
||||
}
|
||||
|
||||
pub fn move_vertically(
|
||||
text: RopeSlice,
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
dir: Direction,
|
||||
count: usize,
|
||||
extend: bool,
|
||||
behaviour: Movement,
|
||||
) -> Range {
|
||||
let Position { row, col } = coords_at_pos(text, range.head);
|
||||
let Position { row, col } = coords_at_pos(slice, range.head);
|
||||
|
||||
let horiz = range.horiz.unwrap_or(col as u32);
|
||||
|
||||
@@ -47,143 +64,83 @@ pub fn move_vertically(
|
||||
Direction::Backward => row.saturating_sub(count),
|
||||
Direction::Forward => std::cmp::min(
|
||||
row.saturating_add(count),
|
||||
text.len_lines().saturating_sub(2),
|
||||
slice.len_lines().saturating_sub(2),
|
||||
),
|
||||
};
|
||||
|
||||
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
|
||||
let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
|
||||
let new_line_len = slice.line(new_line).len_chars().saturating_sub(2);
|
||||
|
||||
let new_col = std::cmp::min(horiz as usize, new_line_len);
|
||||
|
||||
let pos = pos_at_coords(text, Position::new(new_line, new_col));
|
||||
let pos = pos_at_coords(slice, Position::new(new_line, new_col));
|
||||
|
||||
let mut range = Range::new(if extend { range.anchor } else { pos }, pos);
|
||||
let anchor = match behaviour {
|
||||
Movement::Extend => range.anchor,
|
||||
Movement::Move => pos,
|
||||
};
|
||||
|
||||
let mut range = Range::new(anchor, pos);
|
||||
range.horiz = Some(horiz);
|
||||
range
|
||||
}
|
||||
|
||||
pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
|
||||
let mut end = begin;
|
||||
|
||||
for _ in 0..count {
|
||||
if begin + 1 == slice.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut ch = slice.char(begin);
|
||||
let next = slice.char(begin + 1);
|
||||
|
||||
// if we're at the end of a word, or on whitespce right before new one
|
||||
if categorize(ch) != categorize(next) {
|
||||
begin += 1;
|
||||
}
|
||||
|
||||
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
|
||||
return None;
|
||||
};
|
||||
ch = slice.char(begin);
|
||||
|
||||
end = begin + 1;
|
||||
|
||||
if is_word(ch) {
|
||||
skip_over_next(slice, &mut end, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
|
||||
skip_over_next(slice, &mut end, is_horiz_blank);
|
||||
}
|
||||
|
||||
Some(Range::new(begin, end - 1))
|
||||
pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
|
||||
word_move(slice, range, count, WordMotionTarget::NextWordStart)
|
||||
}
|
||||
|
||||
pub fn move_prev_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
|
||||
let mut with_end = false;
|
||||
let mut end = begin;
|
||||
|
||||
for _ in 0..count {
|
||||
if begin == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ch = slice.char(begin);
|
||||
let prev = slice.char(begin - 1);
|
||||
|
||||
if categorize(ch) != categorize(prev) {
|
||||
begin -= 1;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_prev(slice, &mut begin, |ch| ch == '\n');
|
||||
|
||||
end = begin;
|
||||
|
||||
with_end = skip_over_prev(slice, &mut end, is_horiz_blank);
|
||||
|
||||
// refetch
|
||||
let ch = slice.char(end);
|
||||
|
||||
if is_word(ch) {
|
||||
with_end = skip_over_prev(slice, &mut end, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
with_end = skip_over_prev(slice, &mut end, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
}
|
||||
|
||||
Some(Range::new(begin, if with_end { end } else { end + 1 }))
|
||||
pub fn move_next_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
|
||||
word_move(slice, range, count, WordMotionTarget::NextWordEnd)
|
||||
}
|
||||
|
||||
pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
|
||||
let mut end = begin;
|
||||
pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
|
||||
word_move(slice, range, count, WordMotionTarget::PrevWordStart)
|
||||
}
|
||||
|
||||
for _ in 0..count {
|
||||
if begin + 2 >= slice.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ch = slice.char(begin);
|
||||
let next = slice.char(begin + 1);
|
||||
|
||||
if categorize(ch) != categorize(next) {
|
||||
begin += 1;
|
||||
}
|
||||
|
||||
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
|
||||
return None;
|
||||
};
|
||||
|
||||
end = begin;
|
||||
|
||||
skip_over_next(slice, &mut end, is_horiz_blank);
|
||||
|
||||
// refetch
|
||||
let ch = slice.char(end);
|
||||
|
||||
if is_word(ch) {
|
||||
skip_over_next(slice, &mut end, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
}
|
||||
|
||||
Some(Range::new(begin, end - 1))
|
||||
fn word_move(slice: RopeSlice, mut range: Range, count: usize, target: WordMotionTarget) -> Range {
|
||||
(0..count).fold(range, |range, _| {
|
||||
slice.chars_at(range.head).range_to_target(target, range)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- util ------------
|
||||
|
||||
// used for by-word movement
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_word(ch: char) -> bool {
|
||||
ch.is_alphanumeric() || ch == '_'
|
||||
}
|
||||
|
||||
pub(crate) fn is_horiz_blank(ch: char) -> bool {
|
||||
matches!(ch, ' ' | '\t')
|
||||
#[inline]
|
||||
pub(crate) fn is_end_of_line(ch: char) -> bool {
|
||||
ch == '\n'
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// Whitespace, but not end of line
|
||||
pub(crate) fn is_strict_whitespace(ch: char) -> bool {
|
||||
ch.is_whitespace() && !is_end_of_line(ch)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_punctuation(ch: char) -> bool {
|
||||
use unicode_general_category::{get_general_category, GeneralCategory};
|
||||
|
||||
matches!(
|
||||
get_general_category(ch),
|
||||
GeneralCategory::OtherPunctuation
|
||||
| GeneralCategory::OpenPunctuation
|
||||
| GeneralCategory::ClosePunctuation
|
||||
| GeneralCategory::InitialPunctuation
|
||||
| GeneralCategory::FinalPunctuation
|
||||
| GeneralCategory::ConnectorPunctuation
|
||||
| GeneralCategory::DashPunctuation
|
||||
| GeneralCategory::MathSymbol
|
||||
| GeneralCategory::CurrencySymbol
|
||||
| GeneralCategory::ModifierSymbol
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub(crate) enum Category {
|
||||
pub enum Category {
|
||||
Whitespace,
|
||||
Eol,
|
||||
Word,
|
||||
@@ -191,14 +148,15 @@ pub(crate) enum Category {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn categorize(ch: char) -> Category {
|
||||
if ch == '\n' {
|
||||
if is_end_of_line(ch) {
|
||||
Category::Eol
|
||||
} else if ch.is_ascii_whitespace() {
|
||||
} else if ch.is_whitespace() {
|
||||
Category::Whitespace
|
||||
} else if is_word(ch) {
|
||||
Category::Word
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
} else if is_punctuation(ch) {
|
||||
Category::Punctuation
|
||||
} else {
|
||||
Category::Unknown
|
||||
@@ -206,44 +164,160 @@ pub(crate) fn categorize(ch: char) -> Category {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns true if there are more characters left after the new position.
|
||||
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
|
||||
/// Returns first index that doesn't satisfy a given predicate when
|
||||
/// advancing the character index.
|
||||
///
|
||||
/// Returns none if all characters satisfy the predicate.
|
||||
pub fn skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
let mut chars = slice.chars_at(*pos);
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if !fun(ch) {
|
||||
break;
|
||||
}
|
||||
*pos += 1;
|
||||
}
|
||||
chars.next().is_some()
|
||||
let mut chars = slice.chars_at(pos).enumerate();
|
||||
chars.find_map(|(i, c)| if !fun(c) { Some(pos + i) } else { None })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns true if the final pos matches the predicate.
|
||||
pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
|
||||
/// Returns first index that doesn't satisfy a given predicate when
|
||||
/// retreating the character index, saturating if all elements satisfy
|
||||
/// the condition.
|
||||
pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
// need to +1 so that prev() includes current char
|
||||
let mut chars = slice.chars_at(*pos + 1);
|
||||
|
||||
while let Some(ch) = chars.prev() {
|
||||
if !fun(ch) {
|
||||
break;
|
||||
let mut chars_starting_from_next = slice.chars_at(pos + 1);
|
||||
let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
|
||||
backwards.find_map(|(i, c)| {
|
||||
if !fun(c) {
|
||||
Some(pos.saturating_sub(i))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Possible targets of a word motion
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum WordMotionTarget {
|
||||
NextWordStart,
|
||||
NextWordEnd,
|
||||
PrevWordStart,
|
||||
}
|
||||
|
||||
pub trait CharHelpers {
|
||||
fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range;
|
||||
}
|
||||
|
||||
enum WordMotionPhase {
|
||||
Start,
|
||||
SkipNewlines,
|
||||
ReachTarget,
|
||||
}
|
||||
|
||||
impl CharHelpers for Chars<'_> {
|
||||
fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
|
||||
let range = origin;
|
||||
// Characters are iterated forward or backwards depending on the motion direction.
|
||||
let characters: Box<dyn Iterator<Item = char>> = match target {
|
||||
WordMotionTarget::PrevWordStart => {
|
||||
self.next();
|
||||
Box::new(from_fn(|| self.prev()))
|
||||
}
|
||||
_ => Box::new(self),
|
||||
};
|
||||
|
||||
// Index advancement also depends on the direction.
|
||||
let advance: &dyn Fn(&mut usize) = match target {
|
||||
WordMotionTarget::PrevWordStart => &|u| *u = u.saturating_sub(1),
|
||||
_ => &|u| *u += 1,
|
||||
};
|
||||
|
||||
let mut characters = characters.peekable();
|
||||
let mut phase = WordMotionPhase::Start;
|
||||
let mut head = origin.head;
|
||||
let mut anchor: Option<usize> = None;
|
||||
let is_boundary = |a: char, b: Option<char>| categorize(a) != categorize(b.unwrap_or(a));
|
||||
while let Some(peek) = characters.peek().copied() {
|
||||
phase = match phase {
|
||||
WordMotionPhase::Start => {
|
||||
characters.next();
|
||||
if characters.peek().is_none() {
|
||||
break; // We're at the end, so there's nothing to do.
|
||||
}
|
||||
// Anchor may remain here if the head wasn't at a boundary
|
||||
if !is_boundary(peek, characters.peek().copied()) && !is_end_of_line(peek) {
|
||||
anchor = Some(head);
|
||||
}
|
||||
// First character is always skipped by the head
|
||||
advance(&mut head);
|
||||
WordMotionPhase::SkipNewlines
|
||||
}
|
||||
WordMotionPhase::SkipNewlines => {
|
||||
if is_end_of_line(peek) {
|
||||
characters.next();
|
||||
if characters.peek().is_some() {
|
||||
advance(&mut head);
|
||||
}
|
||||
WordMotionPhase::SkipNewlines
|
||||
} else {
|
||||
WordMotionPhase::ReachTarget
|
||||
}
|
||||
}
|
||||
WordMotionPhase::ReachTarget => {
|
||||
characters.next();
|
||||
anchor = anchor.or(Some(head));
|
||||
if reached_target(target, peek, characters.peek()) {
|
||||
break;
|
||||
} else {
|
||||
advance(&mut head);
|
||||
}
|
||||
WordMotionPhase::ReachTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
Range::new(anchor.unwrap_or(origin.anchor), head)
|
||||
}
|
||||
}
|
||||
|
||||
fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>) -> bool {
|
||||
let next_peek = match next_peek {
|
||||
Some(next_peek) => next_peek,
|
||||
None => return true,
|
||||
};
|
||||
|
||||
match target {
|
||||
WordMotionTarget::NextWordStart => {
|
||||
((categorize(peek) != categorize(*next_peek))
|
||||
&& (is_end_of_line(*next_peek) || !next_peek.is_whitespace()))
|
||||
}
|
||||
WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
|
||||
((categorize(peek) != categorize(*next_peek))
|
||||
&& (!peek.is_whitespace() || is_end_of_line(*next_peek)))
|
||||
}
|
||||
*pos = pos.saturating_sub(1);
|
||||
}
|
||||
fun(slice.char(*pos))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::array::{self, IntoIter};
|
||||
|
||||
use ropey::Rope;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SINGLE_LINE_SAMPLE: &str = "This is a simple alphabetic line";
|
||||
const MULTILINE_SAMPLE: &str = "\
|
||||
Multiline\n\
|
||||
text sample\n\
|
||||
which\n\
|
||||
is merely alphabetic\n\
|
||||
and whitespaced\n\
|
||||
";
|
||||
|
||||
const MULTIBYTE_CHARACTER_SAMPLE: &str = "\
|
||||
パーティーへ行かないか\n\
|
||||
The text above is Japanese\n\
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn test_vertical_move() {
|
||||
let text = Rope::from("abcd\nefg\nwrs");
|
||||
@@ -254,9 +328,477 @@ mod test {
|
||||
assert_eq!(
|
||||
coords_at_pos(
|
||||
slice,
|
||||
move_vertically(slice, range, Direction::Forward, 1, false).head
|
||||
move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
|
||||
),
|
||||
(1, 2).into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_moves_through_single_line_in_single_line_text() {
|
||||
let text = Rope::from(SINGLE_LINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
|
||||
let mut range = Range::point(position);
|
||||
|
||||
let moves_and_expected_coordinates = [
|
||||
((Direction::Forward, 1usize), (0, 1)),
|
||||
((Direction::Forward, 2usize), (0, 3)),
|
||||
((Direction::Forward, 0usize), (0, 3)),
|
||||
((Direction::Forward, 999usize), (0, 31)),
|
||||
((Direction::Forward, 999usize), (0, 31)),
|
||||
((Direction::Backward, 999usize), (0, 0)),
|
||||
];
|
||||
|
||||
for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
|
||||
range = move_horizontally(slice, range, direction, amount, Movement::Move);
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_moves_through_single_line_in_multiline_text() {
|
||||
let text = Rope::from(MULTILINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
|
||||
let mut range = Range::point(position);
|
||||
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
((Direction::Forward, 1usize), (0, 1)), // M_ltiline
|
||||
((Direction::Forward, 2usize), (0, 3)), // Mul_iline
|
||||
((Direction::Backward, 6usize), (0, 0)), // _ultiline
|
||||
((Direction::Backward, 999usize), (0, 0)), // _ultiline
|
||||
((Direction::Forward, 3usize), (0, 3)), // Mul_iline
|
||||
((Direction::Forward, 0usize), (0, 3)), // Mul_iline
|
||||
((Direction::Backward, 0usize), (0, 3)), // Mul_iline
|
||||
((Direction::Forward, 999usize), (0, 9)), // Multilin_
|
||||
((Direction::Forward, 999usize), (0, 9)), // Multilin_
|
||||
]);
|
||||
|
||||
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
|
||||
range = move_horizontally(slice, range, direction, amount, Movement::Move);
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_extending_moves_in_single_line_text() {
|
||||
let text = Rope::from(SINGLE_LINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
|
||||
let mut range = Range::point(position);
|
||||
let original_anchor = range.anchor;
|
||||
|
||||
let moves = IntoIter::new([
|
||||
(Direction::Forward, 1usize),
|
||||
(Direction::Forward, 5usize),
|
||||
(Direction::Backward, 3usize),
|
||||
]);
|
||||
|
||||
for (direction, amount) in moves {
|
||||
range = move_horizontally(slice, range, direction, amount, Movement::Extend);
|
||||
assert_eq!(range.anchor, original_anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_moves_in_single_column() {
|
||||
let text = Rope::from(MULTILINE_SAMPLE);
|
||||
let slice = dbg!(&text).slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
let mut range = Range::point(position);
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
((Direction::Forward, 1usize), (1, 0)),
|
||||
((Direction::Forward, 2usize), (3, 0)),
|
||||
((Direction::Backward, 999usize), (0, 0)),
|
||||
((Direction::Forward, 3usize), (3, 0)),
|
||||
((Direction::Forward, 0usize), (3, 0)),
|
||||
((Direction::Backward, 0usize), (3, 0)),
|
||||
((Direction::Forward, 5), (4, 0)),
|
||||
((Direction::Forward, 999usize), (4, 0)),
|
||||
]);
|
||||
|
||||
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
|
||||
range = move_vertically(slice, range, direction, amount, Movement::Move);
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_moves_jumping_column() {
|
||||
let text = Rope::from(MULTILINE_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
let mut range = Range::point(position);
|
||||
|
||||
enum Axis {
|
||||
H,
|
||||
V,
|
||||
}
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
// Places cursor at the end of line
|
||||
((Axis::H, Direction::Forward, 8usize), (0, 8)),
|
||||
// First descent preserves column as the target line is wider
|
||||
((Axis::V, Direction::Forward, 1usize), (1, 8)),
|
||||
// Second descent clamps column as the target line is shorter
|
||||
((Axis::V, Direction::Forward, 1usize), (2, 4)),
|
||||
// Third descent restores the original column
|
||||
((Axis::V, Direction::Forward, 1usize), (3, 8)),
|
||||
// Behaviour is preserved even through long jumps
|
||||
((Axis::V, Direction::Backward, 999usize), (0, 8)),
|
||||
((Axis::V, Direction::Forward, 999usize), (4, 8)),
|
||||
]);
|
||||
|
||||
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
|
||||
range = match axis {
|
||||
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
|
||||
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
|
||||
};
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multibyte_character_column_jumps() {
|
||||
let text = Rope::from(MULTIBYTE_CHARACTER_SAMPLE);
|
||||
let slice = text.slice(..);
|
||||
let position = pos_at_coords(slice, (0, 0).into());
|
||||
let mut range = Range::point(position);
|
||||
|
||||
// FIXME: The behaviour captured in this test diverges from both Kakoune and Vim. These
|
||||
// will attempt to preserve the horizontal position of the cursor, rather than
|
||||
// placing it at the same character index.
|
||||
enum Axis {
|
||||
H,
|
||||
V,
|
||||
}
|
||||
let moves_and_expected_coordinates = IntoIter::new([
|
||||
// Places cursor at the fourth kana
|
||||
((Axis::H, Direction::Forward, 4), (0, 4)),
|
||||
// Descent places cursor at the fourth character.
|
||||
((Axis::V, Direction::Forward, 1usize), (1, 4)),
|
||||
]);
|
||||
|
||||
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
|
||||
range = match axis {
|
||||
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
|
||||
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
|
||||
};
|
||||
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
|
||||
assert_eq!(range.head, range.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn nonsensical_ranges_panic_on_forward_movement_attempt_in_debug_mode() {
|
||||
move_next_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn nonsensical_ranges_panic_on_forward_to_end_movement_attempt_in_debug_mode() {
|
||||
move_next_word_end(Rope::from("Sample").slice(..), Range::point(99999999), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn nonsensical_ranges_panic_on_backwards_movement_attempt_in_debug_mode() {
|
||||
move_prev_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_behaviour_when_moving_to_start_of_next_words() {
|
||||
let tests = array::IntoIter::new([
|
||||
("Basic forward motion stops at the first space",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 5))]),
|
||||
(" Starting from a boundary advances the anchor",
|
||||
vec![(1, Range::new(0, 0), Range::new(1, 9))]),
|
||||
("Long whitespace gap is bridged by the head",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 10))]),
|
||||
("Previous anchor is irrelevant for forward motions",
|
||||
vec![(1, Range::new(12, 0), Range::new(0, 8))]),
|
||||
(" Starting from whitespace moves to last space in sequence",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 3))]),
|
||||
("Starting from mid-word leaves anchor at start position and moves head",
|
||||
vec![(1, Range::new(3, 3), Range::new(3, 8))]),
|
||||
("Identifiers_with_underscores are considered a single word",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 28))]),
|
||||
("Jumping\n into starting whitespace selects the spaces before 'into'",
|
||||
vec![(1, Range::new(0, 6), Range::new(8, 11))]),
|
||||
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 11)),
|
||||
(1, Range::new(0, 11), Range::new(12, 14)),
|
||||
(1, Range::new(12, 14), Range::new(15, 17))
|
||||
]),
|
||||
("... ... punctuation and spaces behave as expected",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 5)),
|
||||
(1, Range::new(0, 5), Range::new(6, 9)),
|
||||
]),
|
||||
(".._.._ punctuation is not joined by underscores into a single block",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 1))]),
|
||||
("Newlines\n\nare bridged seamlessly.",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 7)),
|
||||
(1, Range::new(0, 7), Range::new(10, 13)),
|
||||
]),
|
||||
("Jumping\n\n\n\n\n\n from newlines to whitespace selects whitespace.",
|
||||
vec![
|
||||
(1, Range::new(0, 8), Range::new(13, 15)),
|
||||
]),
|
||||
("A failed motion does not modify the range",
|
||||
vec![
|
||||
(3, Range::new(37, 41), Range::new(37, 41)),
|
||||
]),
|
||||
("oh oh oh two character words!",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 2)),
|
||||
(1, Range::new(0, 2), Range::new(3, 5)),
|
||||
(1, Range::new(0, 1), Range::new(2, 2)),
|
||||
]),
|
||||
("Multiple motions at once resolve correctly",
|
||||
vec![
|
||||
(3, Range::new(0, 0), Range::new(17, 19)),
|
||||
]),
|
||||
("Excessive motions are performed partially",
|
||||
vec![
|
||||
(999, Range::new(0, 0), Range::new(32, 40)),
|
||||
]),
|
||||
("", // Edge case of moving forward in empty string
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
("\n\n\n\n\n", // Edge case of moving forward in all newlines
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 4)),
|
||||
]),
|
||||
("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(1, 3)),
|
||||
(1, Range::new(1, 3), Range::new(5, 7)),
|
||||
]),
|
||||
("ヒーリクス multibyte characters behave as normal characters",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 5)),
|
||||
]),
|
||||
]);
|
||||
|
||||
for (sample, scenario) in tests {
|
||||
for (count, begin, expected_end) in scenario.into_iter() {
|
||||
let range = move_next_word_start(Rope::from(sample).slice(..), begin, count);
|
||||
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_behaviour_when_moving_to_start_of_previous_words() {
|
||||
let tests = array::IntoIter::new([
|
||||
("Basic backward motion from the middle of a word",
|
||||
vec![(1, Range::new(3, 3), Range::new(3, 0))]),
|
||||
("Starting from after boundary retreats the anchor",
|
||||
vec![(1, Range::new(0, 8), Range::new(7, 0))]),
|
||||
(" Jump to start of a word preceded by whitespace",
|
||||
vec![(1, Range::new(5, 5), Range::new(5, 4))]),
|
||||
(" Jump to start of line from start of word preceded by whitespace",
|
||||
vec![(1, Range::new(4, 4), Range::new(3, 0))]),
|
||||
("Previous anchor is irrelevant for backward motions",
|
||||
vec![(1, Range::new(12, 5), Range::new(5, 0))]),
|
||||
(" Starting from whitespace moves to first space in sequence",
|
||||
vec![(1, Range::new(0, 3), Range::new(3, 0))]),
|
||||
("Identifiers_with_underscores are considered a single word",
|
||||
vec![(1, Range::new(0, 20), Range::new(20, 0))]),
|
||||
("Jumping\n \nback through a newline selects whitespace",
|
||||
vec![(1, Range::new(0, 13), Range::new(11, 8))]),
|
||||
("Jumping to start of word from the end selects the word",
|
||||
vec![(1, Range::new(6, 6), Range::new(6, 0))]),
|
||||
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
|
||||
vec![
|
||||
(1, Range::new(30, 30), Range::new(30, 21)),
|
||||
(1, Range::new(30, 21), Range::new(20, 18)),
|
||||
(1, Range::new(20, 18), Range::new(17, 15))
|
||||
]),
|
||||
|
||||
("... ... punctuation and spaces behave as expected",
|
||||
vec![
|
||||
(1, Range::new(0, 10), Range::new(9, 6)),
|
||||
(1, Range::new(9, 6), Range::new(5, 0)),
|
||||
]),
|
||||
(".._.._ punctuation is not joined by underscores into a single block",
|
||||
vec![(1, Range::new(0, 5), Range::new(4, 3))]),
|
||||
("Newlines\n\nare bridged seamlessly.",
|
||||
vec![
|
||||
(1, Range::new(0, 10), Range::new(7, 0)),
|
||||
]),
|
||||
("Jumping \n\n\n\n\nback from within a newline group selects previous block",
|
||||
vec![
|
||||
(1, Range::new(0, 13), Range::new(10, 0)),
|
||||
]),
|
||||
("Failed motions do not modify the range",
|
||||
vec![
|
||||
(0, Range::new(3, 0), Range::new(3, 0)),
|
||||
]),
|
||||
("Multiple motions at once resolve correctly",
|
||||
vec![
|
||||
(3, Range::new(18, 18), Range::new(8, 0)),
|
||||
]),
|
||||
("Excessive motions are performed partially",
|
||||
vec![
|
||||
(999, Range::new(40, 40), Range::new(9, 0)),
|
||||
]),
|
||||
("", // Edge case of moving backwards in empty string
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
("\n\n\n\n\n", // Edge case of moving backwards in all newlines
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
(" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
|
||||
vec![
|
||||
(1, Range::new(0, 7), Range::new(6, 4)),
|
||||
(1, Range::new(6, 4), Range::new(2, 0)),
|
||||
]),
|
||||
("ヒーリクス multibyte characters behave as normal characters",
|
||||
vec![
|
||||
(1, Range::new(0, 5), Range::new(4, 0)),
|
||||
]),
|
||||
]);
|
||||
|
||||
for (sample, scenario) in tests {
|
||||
for (count, begin, expected_end) in scenario.into_iter() {
|
||||
let range = move_prev_word_start(Rope::from(sample).slice(..), begin, count);
|
||||
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_behaviour_when_moving_to_end_of_next_words() {
|
||||
let tests = array::IntoIter::new([
|
||||
("Basic forward motion from the start of a word to the end of it",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 4))]),
|
||||
("Basic forward motion from the end of a word to the end of the next",
|
||||
vec![(1, Range::new(0, 4), Range::new(5, 12))]),
|
||||
("Basic forward motion from the middle of a word to the end of it",
|
||||
vec![(1, Range::new(2, 2), Range::new(2, 4))]),
|
||||
(" Jumping to end of a word preceded by whitespace",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 10))]),
|
||||
(" Starting from a boundary advances the anchor",
|
||||
vec![(1, Range::new(0, 0), Range::new(1, 8))]),
|
||||
("Previous anchor is irrelevant for end of word motion",
|
||||
vec![(1, Range::new(12, 2), Range::new(2, 7))]),
|
||||
("Identifiers_with_underscores are considered a single word",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 27))]),
|
||||
("Jumping\n into starting whitespace selects up to the end of next word",
|
||||
vec![(1, Range::new(0, 6), Range::new(8, 15))]),
|
||||
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 11)),
|
||||
(1, Range::new(0, 11), Range::new(12, 14)),
|
||||
(1, Range::new(12, 14), Range::new(15, 17))
|
||||
]),
|
||||
("... ... punctuation and spaces behave as expected",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 2)),
|
||||
(1, Range::new(0, 2), Range::new(3, 8)),
|
||||
]),
|
||||
(".._.._ punctuation is not joined by underscores into a single block",
|
||||
vec![(1, Range::new(0, 0), Range::new(0, 1))]),
|
||||
("Newlines\n\nare bridged seamlessly.",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 7)),
|
||||
(1, Range::new(0, 7), Range::new(10, 12)),
|
||||
]),
|
||||
("Jumping\n\n\n\n\n\n from newlines to whitespace selects to end of next word.",
|
||||
vec![
|
||||
(1, Range::new(0, 8), Range::new(13, 19)),
|
||||
]),
|
||||
("A failed motion does not modify the range",
|
||||
vec![
|
||||
(3, Range::new(37, 41), Range::new(37, 41)),
|
||||
]),
|
||||
("Multiple motions at once resolve correctly",
|
||||
vec![
|
||||
(3, Range::new(0, 0), Range::new(16, 18)),
|
||||
]),
|
||||
("Excessive motions are performed partially",
|
||||
vec![
|
||||
(999, Range::new(0, 0), Range::new(31, 40)),
|
||||
]),
|
||||
("", // Edge case of moving forward in empty string
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 0)),
|
||||
]),
|
||||
("\n\n\n\n\n", // Edge case of moving forward in all newlines
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 4)),
|
||||
]),
|
||||
("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(1, 3)),
|
||||
(1, Range::new(1, 3), Range::new(5, 7)),
|
||||
]),
|
||||
("ヒーリクス multibyte characters behave as normal characters",
|
||||
vec![
|
||||
(1, Range::new(0, 0), Range::new(0, 4)),
|
||||
]),
|
||||
]);
|
||||
|
||||
for (sample, scenario) in tests {
|
||||
for (count, begin, expected_end) in scenario.into_iter() {
|
||||
let range = move_next_word_end(Rope::from(sample).slice(..), begin, count);
|
||||
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_categorize() {
|
||||
const WORD_TEST_CASE: &'static str =
|
||||
"_hello_world_あいうえおー12345678901234567890";
|
||||
const PUNCTUATION_TEST_CASE: &'static str =
|
||||
"!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
|
||||
const WHITESPACE_TEST_CASE: &'static str = " ";
|
||||
|
||||
assert_eq!(Category::Eol, categorize('\n'));
|
||||
|
||||
for ch in WHITESPACE_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
Category::Whitespace,
|
||||
categorize(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Whitespace`",
|
||||
ch,
|
||||
categorize(ch)
|
||||
);
|
||||
}
|
||||
|
||||
for ch in WORD_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
Category::Word,
|
||||
categorize(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Word`",
|
||||
ch,
|
||||
categorize(ch)
|
||||
);
|
||||
}
|
||||
|
||||
for ch in PUNCTUATION_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
Category::Punctuation,
|
||||
categorize(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Punctuation`",
|
||||
ch,
|
||||
categorize(ch)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,16 +6,15 @@ use std::{collections::HashMap, sync::RwLock};
|
||||
static REGISTRY: Lazy<RwLock<HashMap<char, Vec<String>>>> =
|
||||
Lazy::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
pub fn get(register: char) -> Option<Vec<String>> {
|
||||
/// Read register values.
|
||||
pub fn get(register_name: char) -> Option<Vec<String>> {
|
||||
let registry = REGISTRY.read().unwrap();
|
||||
|
||||
// TODO: no cloning
|
||||
registry.get(®ister).cloned()
|
||||
registry.get(®ister_name).cloned() // TODO: no cloning
|
||||
}
|
||||
|
||||
/// Read register values.
|
||||
// restoring: bool
|
||||
pub fn set(register: char, values: Vec<String>) {
|
||||
pub fn set(register_name: char, values: Vec<String>) {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
|
||||
registry.insert(register, values);
|
||||
registry.insert(register_name, values);
|
||||
}
|
||||
|
@@ -35,6 +35,10 @@ impl Range {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn point(head: usize) -> Self {
|
||||
Self::new(head, head)
|
||||
}
|
||||
|
||||
/// Start of the range.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use crate::{Rope, Selection};
|
||||
|
||||
/// A state represents the current editor state of a single buffer.
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub doc: Rope,
|
||||
pub selection: Selection,
|
||||
|
@@ -5,6 +5,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -12,13 +13,13 @@ use std::{
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Configuration {
|
||||
pub language: Vec<LanguageConfiguration>,
|
||||
}
|
||||
|
||||
// largely based on tree-sitter/cli/src/loader.rs
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageConfiguration {
|
||||
#[serde(rename = "name")]
|
||||
@@ -27,8 +28,8 @@ pub struct LanguageConfiguration {
|
||||
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
|
||||
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
|
||||
|
||||
// pub path: PathBuf,
|
||||
// root_path for tree-sitter (^)
|
||||
#[serde(default)]
|
||||
pub auto_format: bool,
|
||||
|
||||
// content_regex
|
||||
// injection_regex
|
||||
@@ -46,7 +47,7 @@ pub struct LanguageConfiguration {
|
||||
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageServerConfiguration {
|
||||
pub command: String,
|
||||
@@ -55,14 +56,14 @@ pub struct LanguageServerConfiguration {
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IndentationConfiguration {
|
||||
pub tab_width: usize,
|
||||
pub unit: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IndentQuery {
|
||||
#[serde(default)]
|
||||
@@ -75,8 +76,10 @@ pub struct IndentQuery {
|
||||
|
||||
#[cfg(not(feature = "embed_runtime"))]
|
||||
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||
let root = crate::runtime_dir();
|
||||
let path = root.join("queries").join(language).join(filename);
|
||||
let path = crate::RUNTIME_DIR
|
||||
.join("queries")
|
||||
.join(language)
|
||||
.join(filename);
|
||||
std::fs::read_to_string(&path)
|
||||
}
|
||||
|
||||
@@ -189,6 +192,7 @@ impl LanguageConfiguration {
|
||||
|
||||
pub static LOADER: OnceCell<Loader> = OnceCell::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Loader {
|
||||
// highlight_names ?
|
||||
language_configs: Vec<Arc<LanguageConfiguration>>,
|
||||
@@ -256,6 +260,12 @@ pub struct TsParser {
|
||||
cursors: Vec<QueryCursor>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for TsParser {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TsParser").finish()
|
||||
}
|
||||
}
|
||||
|
||||
// could also just use a pool, or a single instance?
|
||||
thread_local! {
|
||||
pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
|
||||
@@ -264,6 +274,7 @@ thread_local! {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Syntax {
|
||||
config: Arc<HighlightConfiguration>,
|
||||
|
||||
@@ -366,7 +377,11 @@ impl Syntax {
|
||||
// prevents them from being moved. But both of these values are really just
|
||||
// pointers, so it's actually ok to move them.
|
||||
|
||||
let mut cursor = QueryCursor::new(); // reuse a pool
|
||||
// reuse a cursor from the pool if possible
|
||||
let mut cursor = PARSER.with(|ts_parser| {
|
||||
let highlighter = &mut ts_parser.borrow_mut();
|
||||
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
|
||||
});
|
||||
let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
|
||||
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
|
||||
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
|
||||
@@ -440,6 +455,7 @@ impl Syntax {
|
||||
// buffer_range_for_scope_at_pos
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LanguageLayer {
|
||||
// mode
|
||||
// grammar
|
||||
@@ -748,6 +764,7 @@ pub enum HighlightEvent {
|
||||
/// Contains the data neeeded to higlight code written in a particular language.
|
||||
///
|
||||
/// This struct is immutable and can be shared between threads.
|
||||
#[derive(Debug)]
|
||||
pub struct HighlightConfiguration {
|
||||
pub language: Grammar,
|
||||
pub query: Query,
|
||||
@@ -778,6 +795,7 @@ struct LocalScope<'a> {
|
||||
local_defs: Vec<LocalDef<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HighlightIter<'a, 'tree: 'a, F>
|
||||
where
|
||||
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
|
||||
@@ -803,6 +821,12 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HighlightIterLayer").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightConfiguration {
|
||||
/// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting
|
||||
/// queries.
|
||||
|
@@ -15,7 +15,7 @@ pub enum Operation {
|
||||
Insert(Tendril),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Assoc {
|
||||
Before,
|
||||
After,
|
||||
@@ -758,7 +758,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn combine_with_utf8() {
|
||||
const TEST_CASE: &'static str = "Hello, これはヒレクスエディターです!";
|
||||
const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
|
||||
|
||||
let empty = Rope::from("");
|
||||
let mut a = ChangeSet::new(&empty);
|
||||
|
@@ -1,65 +0,0 @@
|
||||
use crate::movement::{categorize, is_horiz_blank, is_word, skip_over_prev};
|
||||
use ropey::RopeSlice;
|
||||
|
||||
#[must_use]
|
||||
pub fn nth_prev_word_boundary(slice: RopeSlice, mut char_idx: usize, count: usize) -> usize {
|
||||
let mut with_end = false;
|
||||
|
||||
for _ in 0..count {
|
||||
if char_idx == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// return if not skip while?
|
||||
skip_over_prev(slice, &mut char_idx, |ch| ch == '\n');
|
||||
|
||||
with_end = skip_over_prev(slice, &mut char_idx, is_horiz_blank);
|
||||
|
||||
// refetch
|
||||
let ch = slice.char(char_idx);
|
||||
|
||||
if is_word(ch) {
|
||||
with_end = skip_over_prev(slice, &mut char_idx, is_word);
|
||||
} else if ch.is_ascii_punctuation() {
|
||||
with_end = skip_over_prev(slice, &mut char_idx, |ch| ch.is_ascii_punctuation());
|
||||
}
|
||||
}
|
||||
|
||||
if with_end {
|
||||
char_idx
|
||||
} else {
|
||||
char_idx + 1
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_prev_word_boundary() {
|
||||
use ropey::Rope;
|
||||
let t = |x, y| {
|
||||
let text = Rope::from(x);
|
||||
let out = nth_prev_word_boundary(text.slice(..), text.len_chars() - 1, 1);
|
||||
assert_eq!(text.slice(..out), y, r#"from "{}""#, x);
|
||||
};
|
||||
t("abcd\nefg\nwrs", "abcd\nefg\n");
|
||||
t("abcd\nefg\n", "abcd\n");
|
||||
t("abcd\n", "");
|
||||
t("hello, world!", "hello, world");
|
||||
t("hello, world", "hello, ");
|
||||
t("hello, ", "hello");
|
||||
t("hello", "");
|
||||
t("こんにちは、世界!", "こんにちは、世界!"); // TODO: punctuation
|
||||
t("こんにちは、世界", "こんにちは、");
|
||||
t("こんにちは、", "こんにちは、"); // what?
|
||||
t("こんにちは", "");
|
||||
t("この世界。", "この世界。"); // what?
|
||||
t("この世界", "");
|
||||
t("お前はもう死んでいる", "");
|
||||
t("その300円です", ""); // TODO: should stop at 300
|
||||
t("唱k", ""); // TODO: should stop at 唱
|
||||
t("1 + 1 = 2", "1 + 1 = ");
|
||||
t("1 + 1 =", "1 + 1 ");
|
||||
t("1 + 1", "1 + ");
|
||||
t("1 + ", "1 ");
|
||||
t("1 ", "");
|
||||
t("1+1=2", "1+1=");
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-lsp"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -10,20 +10,14 @@ license = "MPL-2.0"
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
|
||||
once_cell = "1.4"
|
||||
|
||||
lsp-types = { version = "0.89", features = ["proposed"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1.5"
|
||||
futures-executor = { version = "0.3" }
|
||||
url = "2"
|
||||
pathdiff = "0.2"
|
||||
glob = "0.3"
|
||||
anyhow = "1"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
# jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
|
||||
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc", default-features = false } # don't pull in all of futures
|
||||
anyhow = "1.0"
|
||||
futures-executor = "0.3"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
|
||||
log = "0.4"
|
||||
lsp-types = { version = "0.89", features = ["proposed"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
log = "~0.4"
|
||||
tokio = { version = "1.6", features = ["full"] }
|
||||
tokio-stream = "0.1.6"
|
@@ -3,31 +3,24 @@ use crate::{
|
||||
Call, Error, OffsetEncoding, Result,
|
||||
};
|
||||
|
||||
use helix_core::{ChangeSet, Rope};
|
||||
|
||||
// use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use helix_core::{find_root, ChangeSet, Rope};
|
||||
use jsonrpc_core as jsonrpc;
|
||||
use lsp_types as lsp;
|
||||
use serde_json::Value;
|
||||
|
||||
use std::future::Future;
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use tokio::{
|
||||
io::{BufReader, BufWriter},
|
||||
// prelude::*,
|
||||
process::{Child, Command},
|
||||
sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
_process: Child,
|
||||
|
||||
outgoing: UnboundedSender<Payload>,
|
||||
// pub incoming: Receiver<Call>,
|
||||
pub request_counter: AtomicU64,
|
||||
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
request_counter: AtomicU64,
|
||||
capabilities: Option<lsp::ServerCapabilities>,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
@@ -43,40 +36,27 @@ impl Client {
|
||||
.kill_on_drop(true)
|
||||
.spawn();
|
||||
|
||||
// use std::io::ErrorKind;
|
||||
let mut process = match process {
|
||||
Ok(process) => process,
|
||||
Err(err) => match err.kind() {
|
||||
// ErrorKind::NotFound | ErrorKind::PermissionDenied => {
|
||||
// return Err(Error::Other(err.into()))
|
||||
// }
|
||||
_kind => return Err(Error::Other(err.into())),
|
||||
},
|
||||
};
|
||||
let mut process = process?;
|
||||
|
||||
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
|
||||
let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
|
||||
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
|
||||
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
|
||||
|
||||
let (incoming, outgoing) = Transport::start(reader, writer, stderr);
|
||||
let (server_rx, server_tx) = Transport::start(reader, writer, stderr);
|
||||
|
||||
let client = Self {
|
||||
_process: process,
|
||||
|
||||
outgoing,
|
||||
// incoming,
|
||||
server_tx,
|
||||
request_counter: AtomicU64::new(0),
|
||||
|
||||
capabilities: None,
|
||||
// diagnostics: HashMap::new(),
|
||||
offset_encoding: OffsetEncoding::Utf8,
|
||||
};
|
||||
|
||||
// TODO: async client.initialize()
|
||||
// maybe use an arc<atomic> flag
|
||||
|
||||
Ok((client, incoming))
|
||||
Ok((client, server_rx))
|
||||
}
|
||||
|
||||
fn next_request_id(&self) -> jsonrpc::Id {
|
||||
@@ -106,7 +86,7 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Execute a RPC request on the language server.
|
||||
pub async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
|
||||
async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
R::Result: core::fmt::Debug, // TODO: temporary
|
||||
@@ -118,17 +98,20 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Execute a RPC request on the language server.
|
||||
pub fn call<R: lsp::request::Request>(
|
||||
fn call<R: lsp::request::Request>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
let outgoing = self.outgoing.clone();
|
||||
let server_tx = self.server_tx.clone();
|
||||
let id = self.next_request_id();
|
||||
|
||||
async move {
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
let params = serde_json::to_value(params)?;
|
||||
|
||||
let request = jsonrpc::MethodCall {
|
||||
@@ -140,32 +123,29 @@ impl Client {
|
||||
|
||||
let (tx, mut rx) = channel::<Result<Value>>(1);
|
||||
|
||||
outgoing
|
||||
server_tx
|
||||
.send(Payload::Request {
|
||||
chan: tx,
|
||||
value: request,
|
||||
})
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
timeout(Duration::from_secs(2), rx.recv())
|
||||
.await
|
||||
.map_err(|_| Error::Timeout)? // return Timeout
|
||||
.unwrap() // TODO: None if channel closed
|
||||
.ok_or(Error::StreamClosed)?
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a RPC notification to the language server.
|
||||
pub fn notify<R: lsp::notification::Notification>(
|
||||
fn notify<R: lsp::notification::Notification>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
) -> impl Future<Output = Result<()>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
let outgoing = self.outgoing.clone();
|
||||
let server_tx = self.server_tx.clone();
|
||||
|
||||
async move {
|
||||
let params = serde_json::to_value(params)?;
|
||||
@@ -176,7 +156,7 @@ impl Client {
|
||||
params: Self::value_into_params(params),
|
||||
};
|
||||
|
||||
outgoing
|
||||
server_tx
|
||||
.send(Payload::Notification(notification))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
@@ -205,7 +185,7 @@ impl Client {
|
||||
}),
|
||||
};
|
||||
|
||||
self.outgoing
|
||||
self.server_tx
|
||||
.send(Payload::Response(output))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
@@ -216,15 +196,16 @@ impl Client {
|
||||
// General messages
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
pub async fn initialize(&mut self) -> Result<()> {
|
||||
pub(crate) async fn initialize(&mut self) -> Result<()> {
|
||||
// TODO: delay any requests that are triggered prior to initialize
|
||||
let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
|
||||
|
||||
#[allow(deprecated)]
|
||||
let params = lsp::InitializeParams {
|
||||
process_id: Some(std::process::id()),
|
||||
// root_path is obsolete, use root_uri
|
||||
root_path: None,
|
||||
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
|
||||
root_uri: None, // set to project root in the future
|
||||
root_uri: root,
|
||||
initialization_options: None,
|
||||
capabilities: lsp::ClientCapabilities {
|
||||
text_document: Some(lsp::TextDocumentClientCapabilities {
|
||||
@@ -247,6 +228,11 @@ impl Client {
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
window: Some(lsp::WindowClientCapabilities {
|
||||
// TODO: temporarily disabled until we implement handling for window/workDoneProgress/create
|
||||
// work_done_progress: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
trace: None,
|
||||
@@ -674,4 +660,17 @@ impl Client {
|
||||
|
||||
self.call::<lsp::request::References>(params)
|
||||
}
|
||||
|
||||
pub fn document_symbols(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
let params = lsp::DocumentSymbolParams {
|
||||
text_document,
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
|
||||
partial_result_params: lsp::PartialResultParams::default(),
|
||||
};
|
||||
|
||||
self.call::<lsp::request::DocumentSymbolRequest>(params)
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +1,27 @@
|
||||
mod client;
|
||||
mod transport;
|
||||
|
||||
pub use client::Client;
|
||||
pub use futures_executor::block_on;
|
||||
pub use jsonrpc::Call;
|
||||
pub use jsonrpc_core as jsonrpc;
|
||||
pub use lsp::{Position, Url};
|
||||
pub use lsp_types as lsp;
|
||||
|
||||
pub use client::Client;
|
||||
pub use lsp::{Position, Url};
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use helix_core::syntax::LanguageConfiguration;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
pub use futures_executor::block_on;
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
type LanguageId = String;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
@@ -27,8 +29,14 @@ pub enum Error {
|
||||
Rpc(#[from] jsonrpc::Error),
|
||||
#[error("failed to parse: {0}")]
|
||||
Parse(#[from] serde_json::Error),
|
||||
#[error("IO Error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error("request timed out")]
|
||||
Timeout,
|
||||
#[error("server closed the stream")]
|
||||
StreamClosed,
|
||||
#[error("LSP not defined")]
|
||||
LspNotDefined,
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -47,23 +55,54 @@ pub mod util {
|
||||
use super::*;
|
||||
use helix_core::{Range, Rope, Transaction};
|
||||
|
||||
/// Converts [`lsp::Position`] to a position in the document.
|
||||
///
|
||||
/// Returns `None` if position exceeds document length or an operation overflows.
|
||||
pub fn lsp_pos_to_pos(
|
||||
doc: &Rope,
|
||||
pos: lsp::Position,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) -> usize {
|
||||
) -> Option<usize> {
|
||||
let max_line = doc.lines().count().saturating_sub(1);
|
||||
let pos_line = pos.line as usize;
|
||||
let pos_line = if pos_line > max_line {
|
||||
return None;
|
||||
} else {
|
||||
pos_line
|
||||
};
|
||||
match offset_encoding {
|
||||
OffsetEncoding::Utf8 => {
|
||||
let line = doc.line_to_char(pos.line as usize);
|
||||
line + pos.character as usize
|
||||
let max_char = doc
|
||||
.line_to_char(max_line)
|
||||
.checked_add(doc.line(max_line).len_chars())?;
|
||||
let line = doc.line_to_char(pos_line);
|
||||
let pos = line.checked_add(pos.character as usize)?;
|
||||
if pos <= max_char {
|
||||
Some(pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OffsetEncoding::Utf16 => {
|
||||
let line = doc.line_to_char(pos.line as usize);
|
||||
let max_char = doc
|
||||
.line_to_char(max_line)
|
||||
.checked_add(doc.line(max_line).len_chars())?;
|
||||
let max_cu = doc.char_to_utf16_cu(max_char);
|
||||
let line = doc.line_to_char(pos_line);
|
||||
let line_start = doc.char_to_utf16_cu(line);
|
||||
doc.utf16_cu_to_char(line_start + pos.character as usize)
|
||||
let pos = line_start.checked_add(pos.character as usize)?;
|
||||
if pos <= max_cu {
|
||||
Some(doc.utf16_cu_to_char(pos))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts position in the document to [`lsp::Position`].
|
||||
///
|
||||
/// Panics when `pos` is out of `doc` bounds or operation overflows.
|
||||
pub fn pos_to_lsp_pos(
|
||||
doc: &Rope,
|
||||
pos: usize,
|
||||
@@ -87,6 +126,7 @@ pub mod util {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a range in the document to [`lsp::Range`].
|
||||
pub fn range_to_lsp_range(
|
||||
doc: &Rope,
|
||||
range: Range,
|
||||
@@ -98,6 +138,17 @@ pub mod util {
|
||||
lsp::Range::new(start, end)
|
||||
}
|
||||
|
||||
pub fn lsp_range_to_range(
|
||||
doc: &Rope,
|
||||
range: lsp::Range,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) -> Option<Range> {
|
||||
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
|
||||
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
|
||||
|
||||
Some(Range::new(start, end))
|
||||
}
|
||||
|
||||
pub fn generate_transaction_from_edits(
|
||||
doc: &Rope,
|
||||
edits: Vec<lsp::TextEdit>,
|
||||
@@ -113,14 +164,21 @@ pub mod util {
|
||||
None
|
||||
};
|
||||
|
||||
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding);
|
||||
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding);
|
||||
let start =
|
||||
if let Some(start) = lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
|
||||
start
|
||||
} else {
|
||||
return (0, 0, None);
|
||||
};
|
||||
let end = if let Some(end) = lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
|
||||
end
|
||||
} else {
|
||||
return (0, 0, None);
|
||||
};
|
||||
(start, end, replacement)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// apply_insert_replace_edit
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@@ -128,6 +186,7 @@ pub enum Notification {
|
||||
PublishDiagnostics(lsp::PublishDiagnosticsParams),
|
||||
ShowMessage(lsp::ShowMessageParams),
|
||||
LogMessage(lsp::LogMessageParams),
|
||||
ProgressMessage(lsp::ProgressParams),
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
@@ -145,17 +204,20 @@ impl Notification {
|
||||
}
|
||||
|
||||
lsp::notification::ShowMessage::METHOD => {
|
||||
let params: lsp::ShowMessageParams =
|
||||
params.parse().expect("Failed to parse ShowMessage params");
|
||||
let params: lsp::ShowMessageParams = params.parse().ok()?;
|
||||
|
||||
Self::ShowMessage(params)
|
||||
}
|
||||
lsp::notification::LogMessage::METHOD => {
|
||||
let params: lsp::LogMessageParams =
|
||||
params.parse().expect("Failed to parse ShowMessage params");
|
||||
let params: lsp::LogMessageParams = params.parse().ok()?;
|
||||
|
||||
Self::LogMessage(params)
|
||||
}
|
||||
lsp::notification::Progress::METHOD => {
|
||||
let params: lsp::ProgressParams = params.parse().ok()?;
|
||||
|
||||
Self::ProgressMessage(params)
|
||||
}
|
||||
_ => {
|
||||
log::error!("unhandled LSP notification: {}", method);
|
||||
return None;
|
||||
@@ -166,14 +228,9 @@ impl Notification {
|
||||
}
|
||||
}
|
||||
|
||||
pub use jsonrpc::Call;
|
||||
|
||||
type LanguageId = String;
|
||||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Registry {
|
||||
inner: HashMap<LanguageId, Option<Arc<Client>>>,
|
||||
inner: HashMap<LanguageId, Arc<Client>>,
|
||||
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<Call>>,
|
||||
}
|
||||
@@ -192,35 +249,29 @@ impl Registry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Option<Arc<Client>> {
|
||||
// TODO: propagate the error
|
||||
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
|
||||
if let Some(config) = &language_config.language_server {
|
||||
// avoid borrow issues
|
||||
let inner = &mut self.inner;
|
||||
let s_incoming = &mut self.incoming;
|
||||
|
||||
let language_server = inner
|
||||
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769
|
||||
.or_insert_with(|| {
|
||||
// TODO: lookup defaults for id (name, args)
|
||||
|
||||
match inner.entry(language_config.scope.clone()) {
|
||||
Entry::Occupied(language_server) => Ok(language_server.get().clone()),
|
||||
Entry::Vacant(entry) => {
|
||||
// initialize a new client
|
||||
let (mut client, incoming) =
|
||||
Client::start(&config.command, &config.args).ok()?;
|
||||
|
||||
let (mut client, incoming) = Client::start(&config.command, &config.args)?;
|
||||
// TODO: run this async without blocking
|
||||
futures_executor::block_on(client.initialize()).unwrap();
|
||||
|
||||
futures_executor::block_on(client.initialize())?;
|
||||
s_incoming.push(UnboundedReceiverStream::new(incoming));
|
||||
let client = Arc::new(client);
|
||||
|
||||
Some(Arc::new(client))
|
||||
})
|
||||
.clone();
|
||||
|
||||
return language_server;
|
||||
entry.insert(client.clone());
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(Error::LspNotDefined)
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,3 +300,34 @@ impl Registry {
|
||||
// there needs to be a way to process incoming lsp messages from all clients.
|
||||
// -> notifications need to be dispatched to wherever
|
||||
// -> requests need to generate a reply and travel back to the same lsp!
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{lsp, util::*, OffsetEncoding};
|
||||
use helix_core::Rope;
|
||||
|
||||
#[test]
|
||||
fn converts_lsp_pos_to_pos() {
|
||||
macro_rules! test_case {
|
||||
($doc:expr, ($x:expr, $y:expr) => $want:expr) => {
|
||||
let doc = Rope::from($doc);
|
||||
let pos = lsp::Position::new($x, $y);
|
||||
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf16));
|
||||
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf8))
|
||||
};
|
||||
}
|
||||
|
||||
test_case!("", (0, 0) => Some(0));
|
||||
test_case!("", (0, 1) => None);
|
||||
test_case!("", (1, 0) => None);
|
||||
test_case!("\n\n", (0, 0) => Some(0));
|
||||
test_case!("\n\n", (1, 0) => Some(1));
|
||||
test_case!("\n\n", (1, 1) => Some(2));
|
||||
test_case!("\n\n", (2, 0) => Some(2));
|
||||
test_case!("\n\n", (3, 0) => None);
|
||||
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
|
||||
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
|
||||
test_case!("test\n\n\n\ncase", (4, 5) => None);
|
||||
test_case!("", (u32::MAX, u32::MAX) => None);
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
|
||||
use log::{error, info};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
use crate::Result;
|
||||
use jsonrpc_core as jsonrpc;
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use tokio::{
|
||||
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||
process::{ChildStderr, ChildStdin, ChildStdout},
|
||||
@@ -26,47 +20,45 @@ pub enum Payload {
|
||||
Response(jsonrpc::Output),
|
||||
}
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
/// A type representing all possible values sent from the server to the client.
|
||||
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(untagged)]
|
||||
enum Message {
|
||||
enum ServerMessage {
|
||||
/// A regular JSON-RPC request output (single response).
|
||||
Output(jsonrpc::Output),
|
||||
/// A JSON-RPC request or notification.
|
||||
Call(jsonrpc::Call),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
incoming: UnboundedSender<jsonrpc::Call>,
|
||||
outgoing: UnboundedReceiver<Payload>,
|
||||
client_tx: UnboundedSender<jsonrpc::Call>,
|
||||
client_rx: UnboundedReceiver<Payload>,
|
||||
|
||||
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
|
||||
headers: HashMap<String, String>,
|
||||
|
||||
writer: BufWriter<ChildStdin>,
|
||||
reader: BufReader<ChildStdout>,
|
||||
stderr: BufReader<ChildStderr>,
|
||||
server_stdin: BufWriter<ChildStdin>,
|
||||
server_stdout: BufReader<ChildStdout>,
|
||||
server_stderr: BufReader<ChildStderr>,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn start(
|
||||
reader: BufReader<ChildStdout>,
|
||||
writer: BufWriter<ChildStdin>,
|
||||
stderr: BufReader<ChildStderr>,
|
||||
server_stdout: BufReader<ChildStdout>,
|
||||
server_stdin: BufWriter<ChildStdin>,
|
||||
server_stderr: BufReader<ChildStderr>,
|
||||
) -> (UnboundedReceiver<jsonrpc::Call>, UnboundedSender<Payload>) {
|
||||
let (incoming, rx) = unbounded_channel();
|
||||
let (tx, outgoing) = unbounded_channel();
|
||||
let (client_tx, rx) = unbounded_channel();
|
||||
let (tx, client_rx) = unbounded_channel();
|
||||
|
||||
let transport = Self {
|
||||
reader,
|
||||
writer,
|
||||
stderr,
|
||||
incoming,
|
||||
outgoing,
|
||||
server_stdout,
|
||||
server_stdin,
|
||||
server_stderr,
|
||||
client_tx,
|
||||
client_rx,
|
||||
pending_requests: HashMap::default(),
|
||||
headers: HashMap::default(),
|
||||
};
|
||||
|
||||
tokio::spawn(transport.duplex());
|
||||
@@ -74,105 +66,104 @@ impl Transport {
|
||||
(rx, tx)
|
||||
}
|
||||
|
||||
async fn recv(
|
||||
async fn recv_server_message(
|
||||
reader: &mut (impl AsyncBufRead + Unpin + Send),
|
||||
headers: &mut HashMap<String, String>,
|
||||
) -> core::result::Result<Message, std::io::Error> {
|
||||
// read headers
|
||||
buffer: &mut String,
|
||||
) -> Result<ServerMessage> {
|
||||
let mut content_length = None;
|
||||
loop {
|
||||
let mut header = String::new();
|
||||
// detect pipe closed if 0
|
||||
reader.read_line(&mut header).await?;
|
||||
let header = header.trim();
|
||||
buffer.truncate(0);
|
||||
reader.read_line(buffer).await?;
|
||||
let header = buffer.trim();
|
||||
|
||||
if header.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = header.split(": ").collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to parse header",
|
||||
));
|
||||
let mut parts = header.split(": ");
|
||||
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some("Content-Length"), Some(value), None) => {
|
||||
content_length = Some(value.parse().unwrap());
|
||||
}
|
||||
(Some(_), Some(_), None) => {}
|
||||
_ => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to parse header",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
headers.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
|
||||
// find content-length
|
||||
let content_length = headers.get("Content-Length").unwrap().parse().unwrap();
|
||||
let content_length = content_length.unwrap();
|
||||
|
||||
//TODO: reuse vector
|
||||
let mut content = vec![0; content_length];
|
||||
reader.read_exact(&mut content).await?;
|
||||
let msg = String::from_utf8(content).unwrap();
|
||||
|
||||
// read data
|
||||
|
||||
info!("<- {}", msg);
|
||||
|
||||
// try parsing as output (server response) or call (server request)
|
||||
let output: serde_json::Result<Message> = serde_json::from_str(&msg);
|
||||
let output: serde_json::Result<ServerMessage> = serde_json::from_str(&msg);
|
||||
|
||||
Ok(output?)
|
||||
}
|
||||
|
||||
async fn err(
|
||||
async fn recv_server_error(
|
||||
err: &mut (impl AsyncBufRead + Unpin + Send),
|
||||
) -> core::result::Result<(), std::io::Error> {
|
||||
let mut line = String::new();
|
||||
err.read_line(&mut line).await?;
|
||||
error!("err <- {}", line);
|
||||
buffer: &mut String,
|
||||
) -> Result<()> {
|
||||
buffer.truncate(0);
|
||||
err.read_line(buffer).await?;
|
||||
error!("err <- {}", buffer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_payload(&mut self, payload: Payload) -> io::Result<()> {
|
||||
match payload {
|
||||
async fn send_payload_to_server(&mut self, payload: Payload) -> Result<()> {
|
||||
//TODO: reuse string
|
||||
let json = match payload {
|
||||
Payload::Request { chan, value } => {
|
||||
self.pending_requests.insert(value.id.clone(), chan);
|
||||
|
||||
let json = serde_json::to_string(&value)?;
|
||||
self.send(json).await
|
||||
serde_json::to_string(&value)?
|
||||
}
|
||||
Payload::Notification(value) => {
|
||||
let json = serde_json::to_string(&value)?;
|
||||
self.send(json).await
|
||||
}
|
||||
Payload::Response(error) => {
|
||||
let json = serde_json::to_string(&error)?;
|
||||
self.send(json).await
|
||||
}
|
||||
}
|
||||
Payload::Notification(value) => serde_json::to_string(&value)?,
|
||||
Payload::Response(error) => serde_json::to_string(&error)?,
|
||||
};
|
||||
self.send_string_to_server(json).await
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, request: String) -> io::Result<()> {
|
||||
async fn send_string_to_server(&mut self, request: String) -> Result<()> {
|
||||
info!("-> {}", request);
|
||||
|
||||
// send the headers
|
||||
self.writer
|
||||
self.server_stdin
|
||||
.write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes())
|
||||
.await?;
|
||||
|
||||
// send the body
|
||||
self.writer.write_all(request.as_bytes()).await?;
|
||||
self.server_stdin.write_all(request.as_bytes()).await?;
|
||||
|
||||
self.writer.flush().await?;
|
||||
self.server_stdin.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
|
||||
async fn process_server_message(&mut self, msg: ServerMessage) -> Result<()> {
|
||||
match msg {
|
||||
Message::Output(output) => self.recv_response(output).await?,
|
||||
Message::Call(call) => {
|
||||
self.incoming.send(call).unwrap();
|
||||
ServerMessage::Output(output) => self.process_request_response(output).await?,
|
||||
ServerMessage::Call(call) => {
|
||||
self.client_tx.send(call).unwrap();
|
||||
// let notification = Notification::parse(&method, params);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_response(&mut self, output: jsonrpc::Output) -> io::Result<()> {
|
||||
async fn process_request_response(&mut self, output: jsonrpc::Output) -> Result<()> {
|
||||
let (id, result) = match output {
|
||||
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
|
||||
info!("<- {}", result);
|
||||
@@ -200,29 +191,33 @@ impl Transport {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn duplex(mut self) {
|
||||
async fn duplex(mut self) {
|
||||
let mut recv_buffer = String::new();
|
||||
let mut err_buffer = String::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
// client -> server
|
||||
msg = self.outgoing.recv() => {
|
||||
if msg.is_none() {
|
||||
break;
|
||||
msg = self.client_rx.recv() => {
|
||||
match msg {
|
||||
Some(msg) => {
|
||||
self.send_payload_to_server(msg).await.unwrap()
|
||||
},
|
||||
None => break
|
||||
}
|
||||
let msg = msg.unwrap();
|
||||
|
||||
self.send_payload(msg).await.unwrap();
|
||||
}
|
||||
// server <- client
|
||||
msg = Self::recv(&mut self.reader, &mut self.headers) => {
|
||||
if msg.is_err() {
|
||||
error!("err: <- {:?}", msg);
|
||||
break;
|
||||
// server -> client
|
||||
msg = Self::recv_server_message(&mut self.server_stdout, &mut recv_buffer) => {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
self.process_server_message(msg).await.unwrap();
|
||||
}
|
||||
Err(_) => {
|
||||
error!("err: <- {:?}", msg);
|
||||
break;
|
||||
},
|
||||
}
|
||||
let msg = msg.unwrap();
|
||||
|
||||
self.recv_msg(msg).await.unwrap();
|
||||
}
|
||||
_msg = Self::err(&mut self.stderr) => {}
|
||||
_msg = Self::recv_server_error(&mut self.server_stderr, &mut err_buffer) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-syntax"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
1
helix-syntax/languages/tree-sitter-nix
Submodule
1
helix-syntax/languages/tree-sitter-nix
Submodule
Submodule helix-syntax/languages/tree-sitter-nix added at 50f38ceab6
@@ -68,18 +68,19 @@ mk_langs!(
|
||||
// 2) tree-sitter function to call to get a Language
|
||||
(Agda, tree_sitter_agda),
|
||||
(Bash, tree_sitter_bash),
|
||||
(C, tree_sitter_c),
|
||||
(CSharp, tree_sitter_c_sharp),
|
||||
(Cpp, tree_sitter_cpp),
|
||||
(CSharp, tree_sitter_c_sharp),
|
||||
(Css, tree_sitter_css),
|
||||
(C, tree_sitter_c),
|
||||
(Elixir, tree_sitter_elixir),
|
||||
(Go, tree_sitter_go),
|
||||
// (Haskell, tree_sitter_haskell),
|
||||
(Html, tree_sitter_html),
|
||||
(Java, tree_sitter_java),
|
||||
(Javascript, tree_sitter_javascript),
|
||||
(Java, tree_sitter_java),
|
||||
(Json, tree_sitter_json),
|
||||
(Julia, tree_sitter_julia),
|
||||
(Nix, tree_sitter_nix),
|
||||
(Php, tree_sitter_php),
|
||||
(Python, tree_sitter_python),
|
||||
(Ruby, tree_sitter_ruby),
|
||||
|
@@ -1,12 +1,14 @@
|
||||
[package]
|
||||
name = "helix-term"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
description = "A post-modern text editor."
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[package.metadata.nix]
|
||||
build = true
|
||||
app = true
|
||||
|
||||
[features]
|
||||
embed_runtime = ["helix-core/embed_runtime"]
|
||||
|
@@ -1,6 +1,7 @@
|
||||
use helix_lsp::lsp;
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
|
||||
use crate::{compositor::Compositor, ui, Args};
|
||||
use crate::{args::Args, compositor::Compositor, ui};
|
||||
|
||||
use log::{error, info};
|
||||
|
||||
@@ -177,7 +178,7 @@ impl Application {
|
||||
let diagnostics = params
|
||||
.diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| {
|
||||
.filter_map(|diagnostic| {
|
||||
use helix_core::{
|
||||
diagnostic::{Range, Severity, Severity::*},
|
||||
Diagnostic,
|
||||
@@ -188,18 +189,29 @@ impl Application {
|
||||
let language_server = doc.language_server().unwrap();
|
||||
|
||||
// TODO: convert inside server
|
||||
let start = lsp_pos_to_pos(
|
||||
let start = if let Some(start) = lsp_pos_to_pos(
|
||||
text,
|
||||
diagnostic.range.start,
|
||||
language_server.offset_encoding(),
|
||||
);
|
||||
let end = lsp_pos_to_pos(
|
||||
) {
|
||||
start
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
|
||||
let end = if let Some(end) = lsp_pos_to_pos(
|
||||
text,
|
||||
diagnostic.range.end,
|
||||
language_server.offset_encoding(),
|
||||
);
|
||||
) {
|
||||
end
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
|
||||
Diagnostic {
|
||||
Some(Diagnostic {
|
||||
range: Range { start, end },
|
||||
line: diagnostic.range.start.line as usize,
|
||||
message: diagnostic.message,
|
||||
@@ -213,7 +225,7 @@ impl Application {
|
||||
),
|
||||
// code
|
||||
// source
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -228,6 +240,59 @@ impl Application {
|
||||
Notification::LogMessage(params) => {
|
||||
log::warn!("unhandled window/logMessage: {:?}", params);
|
||||
}
|
||||
Notification::ProgressMessage(params) => {
|
||||
let token = match params.token {
|
||||
lsp::NumberOrString::Number(n) => n.to_string(),
|
||||
lsp::NumberOrString::String(s) => s,
|
||||
};
|
||||
let msg = {
|
||||
let lsp::ProgressParamsValue::WorkDone(work) = params.value;
|
||||
let parts = match work {
|
||||
lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
|
||||
title,
|
||||
message,
|
||||
percentage,
|
||||
..
|
||||
}) => (Some(title), message, percentage.map(|n| n.to_string())),
|
||||
lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
|
||||
message,
|
||||
percentage,
|
||||
..
|
||||
}) => (None, message, percentage.map(|n| n.to_string())),
|
||||
lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd {
|
||||
message,
|
||||
}) => {
|
||||
if let Some(message) = message {
|
||||
(None, Some(message), None)
|
||||
} else {
|
||||
self.editor.clear_status();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
match parts {
|
||||
(Some(title), Some(message), Some(percentage)) => {
|
||||
format!("{}% {} - {}", percentage, title, message)
|
||||
}
|
||||
(Some(title), None, Some(percentage)) => {
|
||||
format!("{}% {}", percentage, title)
|
||||
}
|
||||
(Some(title), Some(message), None) => {
|
||||
format!("{} - {}", title, message)
|
||||
}
|
||||
(None, Some(message), Some(percentage)) => {
|
||||
format!("{}% {}", percentage, message)
|
||||
}
|
||||
(Some(title), None, None) => title,
|
||||
(None, Some(message), None) => message,
|
||||
(None, None, Some(percentage)) => format!("{}%", percentage),
|
||||
(None, None, None) => "".into(),
|
||||
}
|
||||
};
|
||||
let status = format!("[{}] {}", token, msg);
|
||||
self.editor.set_status(status);
|
||||
self.render();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
53
helix-term/src/args.rs
Normal file
53
helix-term/src/args.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use anyhow::{Error, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Args {
|
||||
pub display_help: bool,
|
||||
pub display_version: bool,
|
||||
pub verbosity: u64,
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn parse_args() -> Result<Args> {
|
||||
let mut args = Args::default();
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let mut iter = argv.iter();
|
||||
|
||||
iter.next(); // skip the program, we don't care about that
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
"--" => break, // stop parsing at this point treat the remaining as files
|
||||
"--version" => args.display_version = true,
|
||||
"--help" => args.display_help = true,
|
||||
arg if arg.starts_with("--") => {
|
||||
return Err(Error::msg(format!(
|
||||
"unexpected double dash argument: {}",
|
||||
arg
|
||||
)))
|
||||
}
|
||||
arg if arg.starts_with('-') => {
|
||||
let arg = arg.get(1..).unwrap().chars();
|
||||
for chr in arg {
|
||||
match chr {
|
||||
'v' => args.verbosity += 1,
|
||||
'V' => args.display_version = true,
|
||||
'h' => args.display_help = true,
|
||||
_ => return Err(Error::msg(format!("unexpected short arg {}", chr))),
|
||||
}
|
||||
}
|
||||
}
|
||||
arg => args.files.push(PathBuf::from(arg)),
|
||||
}
|
||||
}
|
||||
|
||||
// push the remaining args, if any to the files
|
||||
for filename in iter {
|
||||
args.files.push(PathBuf::from(filename));
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -182,10 +182,8 @@ pub trait AnyComponent {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use cursive_core::views::TextComponent;
|
||||
/// # use cursive_core::view::Component;
|
||||
/// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||
/// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||
/// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||
/// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||
/// ```
|
||||
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
|
||||
}
|
||||
|
@@ -11,7 +11,8 @@ use std::collections::HashMap;
|
||||
// W = next WORD
|
||||
// e = end of word
|
||||
// E = end of WORD
|
||||
// r =
|
||||
// r = replace
|
||||
// R = replace with yanked
|
||||
// t = 'till char
|
||||
// y = yank
|
||||
// u = undo
|
||||
@@ -156,6 +157,7 @@ pub fn default() -> Keymaps {
|
||||
// and matching set for select mode (extend)
|
||||
//
|
||||
key!('r') => commands::replace,
|
||||
key!('R') => commands::replace_with_yanked,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Home,
|
||||
@@ -278,12 +280,17 @@ pub fn default() -> Keymaps {
|
||||
|
||||
// z family for save/restore/combine from/to sels from register
|
||||
|
||||
ctrl!('i') => commands::jump_forward, // TODO: ctrl-i conflicts tab
|
||||
KeyEvent { // supposedly ctrl!('i') but did not work
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
} => commands::jump_forward,
|
||||
ctrl!('o') => commands::jump_backward,
|
||||
// ctrl!('s') => commands::save_selection,
|
||||
|
||||
key!(' ') => commands::space_mode,
|
||||
key!('z') => commands::view_mode,
|
||||
|
||||
key!('"') => commands::select_register,
|
||||
);
|
||||
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
|
||||
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
|
||||
|
8
helix-term/src/lib.rs
Normal file
8
helix-term/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#![allow(unused)]
|
||||
|
||||
pub mod application;
|
||||
pub mod args;
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod keymap;
|
||||
pub mod ui;
|
@@ -1,16 +1,9 @@
|
||||
#![allow(unused)]
|
||||
|
||||
mod application;
|
||||
mod commands;
|
||||
mod compositor;
|
||||
mod keymap;
|
||||
mod ui;
|
||||
|
||||
use application::Application;
|
||||
use helix_term::application::Application;
|
||||
use helix_term::args::Args;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Error, Result};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
let mut base_config = fern::Dispatch::new();
|
||||
@@ -45,58 +38,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Args {
|
||||
display_help: bool,
|
||||
display_version: bool,
|
||||
verbosity: u64,
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn parse_args(mut args: Args) -> Result<Args> {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let mut iter = argv.iter();
|
||||
|
||||
iter.next(); // skip the program, we don't care about that
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
"--" => break, // stop parsing at this point treat the remaining as files
|
||||
"--version" => args.display_version = true,
|
||||
"--help" => args.display_help = true,
|
||||
arg if arg.starts_with("--") => {
|
||||
return Err(Error::msg(format!(
|
||||
"unexpected double dash argument: {}",
|
||||
arg
|
||||
)))
|
||||
}
|
||||
arg if arg.starts_with('-') => {
|
||||
let arg = arg.get(1..).unwrap().chars();
|
||||
for chr in arg {
|
||||
match chr {
|
||||
'v' => args.verbosity += 1,
|
||||
'V' => args.display_version = true,
|
||||
'h' => args.display_help = true,
|
||||
_ => return Err(Error::msg(format!("unexpected short arg {}", chr))),
|
||||
}
|
||||
}
|
||||
}
|
||||
arg => args.files.push(PathBuf::from(arg)),
|
||||
}
|
||||
}
|
||||
|
||||
// push the remaining args, if any to the files
|
||||
for filename in iter {
|
||||
args.files.push(PathBuf::from(filename));
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cache_dir = helix_core::cache_dir();
|
||||
if !cache_dir.exists() {
|
||||
std::fs::create_dir(&cache_dir);
|
||||
std::fs::create_dir_all(&cache_dir).ok();
|
||||
}
|
||||
|
||||
let logpath = cache_dir.join("helix.log");
|
||||
@@ -125,14 +71,7 @@ FLAGS:
|
||||
logpath.display(),
|
||||
);
|
||||
|
||||
let mut args: Args = Args {
|
||||
display_help: false,
|
||||
display_version: false,
|
||||
verbosity: 0,
|
||||
files: [].to_vec(),
|
||||
};
|
||||
|
||||
args = parse_args(args).context("could not parse arguments")?;
|
||||
let args = Args::parse_args().context("could not parse arguments")?;
|
||||
|
||||
// Help has a higher priority and should be handled separately.
|
||||
if args.display_help {
|
||||
@@ -147,14 +86,14 @@ FLAGS:
|
||||
|
||||
let conf_dir = helix_core::config_dir();
|
||||
if !conf_dir.exists() {
|
||||
std::fs::create_dir(&conf_dir);
|
||||
std::fs::create_dir_all(&conf_dir).ok();
|
||||
}
|
||||
|
||||
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
||||
|
||||
// TODO: use the thread local executor to spawn the application task separately from the work pool
|
||||
let mut app = Application::new(args).context("unable to create new appliction")?;
|
||||
app.run().await;
|
||||
app.run().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -108,20 +108,6 @@ impl Completion {
|
||||
let item = item.unwrap();
|
||||
|
||||
use helix_lsp::{lsp, util};
|
||||
// determine what to insert: text_edit | insert_text | label
|
||||
let edit = if let Some(edit) = &item.text_edit {
|
||||
match edit {
|
||||
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
||||
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
||||
unimplemented!("completion: insert_and_replace {:?}", item)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item.insert_text.as_ref().unwrap_or(&item.label);
|
||||
unimplemented!();
|
||||
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
|
||||
// and we insert at position.
|
||||
};
|
||||
|
||||
// if more text was entered, remove it
|
||||
let cursor = doc.selection(view.id).cursor();
|
||||
@@ -134,11 +120,27 @@ impl Completion {
|
||||
}
|
||||
|
||||
use helix_lsp::OffsetEncoding;
|
||||
let transaction = util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
vec![edit],
|
||||
offset_encoding, // TODO: should probably transcode in Client
|
||||
);
|
||||
let transaction = if let Some(edit) = &item.text_edit {
|
||||
let edit = match edit {
|
||||
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
||||
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
||||
unimplemented!("completion: insert_and_replace {:?}", item)
|
||||
}
|
||||
};
|
||||
util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
vec![edit],
|
||||
offset_encoding, // TODO: should probably transcode in Client
|
||||
)
|
||||
} else {
|
||||
let text = item.insert_text.as_ref().unwrap_or(&item.label);
|
||||
let cursor = doc.selection(view.id).cursor();
|
||||
Transaction::change(
|
||||
doc.text(),
|
||||
vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
|
||||
)
|
||||
};
|
||||
|
||||
doc.apply(&transaction, view.id);
|
||||
|
||||
// TODO: merge edit with additional_text_edits
|
||||
|
@@ -34,6 +34,12 @@ pub struct EditorView {
|
||||
|
||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
|
||||
impl Default for EditorView {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -328,11 +334,13 @@ impl EditorView {
|
||||
if let Some(pos) = pos {
|
||||
let pos = view.screen_coords_at_pos(doc, text, pos);
|
||||
if let Some(pos) = pos {
|
||||
// this only prevents panic due to painting selection too far
|
||||
// TODO: prevent painting when scroll past x or in gutter
|
||||
// TODO: use a more correct width check
|
||||
if (pos.col as u16) < viewport.width {
|
||||
let style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
if (pos.col as u16) < viewport.width + view.first_col as u16
|
||||
&& pos.col >= view.first_col
|
||||
{
|
||||
let style = Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.add_modifier(Modifier::DIM);
|
||||
|
||||
surface
|
||||
.get_mut(
|
||||
viewport.x + pos.col as u16,
|
||||
@@ -355,7 +363,7 @@ impl EditorView {
|
||||
let info: Style = theme.get("info");
|
||||
let hint: Style = theme.get("hint");
|
||||
|
||||
for (i, line) in (view.first_line..=last_line).enumerate() {
|
||||
for (i, line) in (view.first_line..last_line).enumerate() {
|
||||
use helix_core::diagnostic::Severity;
|
||||
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
|
||||
surface.set_stringn(
|
||||
@@ -519,7 +527,8 @@ impl EditorView {
|
||||
// count handling
|
||||
key!(i @ '0'..='9') => {
|
||||
let i = i.to_digit(10).unwrap() as usize;
|
||||
cxt.editor.count = Some(cxt.editor.count.map_or(i, |c| c * 10 + i));
|
||||
cxt.editor.count =
|
||||
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
|
||||
}
|
||||
// special handling for repeat operator
|
||||
key!('.') => {
|
||||
@@ -532,11 +541,14 @@ impl EditorView {
|
||||
}
|
||||
_ => {
|
||||
// set the count
|
||||
cxt.count = cxt.editor.count.take().unwrap_or(1);
|
||||
cxt._count = cxt.editor.count.take();
|
||||
// TODO: edge case: 0j -> reset to 1
|
||||
// if this fails, count was Some(0)
|
||||
// debug_assert!(cxt.count != 0);
|
||||
|
||||
// set the register
|
||||
cxt.register = cxt.editor.register.take();
|
||||
|
||||
if let Some(command) = self.keymap[&mode].get(&event) {
|
||||
command(cxt);
|
||||
}
|
||||
@@ -575,11 +587,12 @@ impl Component for EditorView {
|
||||
let mode = doc.mode();
|
||||
|
||||
let mut cxt = commands::Context {
|
||||
register: helix_view::RegisterSelection::default(),
|
||||
editor: &mut cx.editor,
|
||||
count: 1,
|
||||
_count: None,
|
||||
callback: None,
|
||||
callbacks: cx.callbacks,
|
||||
on_next_key_callback: None,
|
||||
callbacks: cx.callbacks,
|
||||
};
|
||||
|
||||
if let Some(on_next_key) = self.on_next_key.take() {
|
||||
|
@@ -166,8 +166,8 @@ impl<T: Item + 'static> Component for Menu<T> {
|
||||
}
|
||||
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Up, ..
|
||||
|
@@ -85,10 +85,15 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
||||
Err(_err) => None,
|
||||
});
|
||||
|
||||
const MAX: usize = 2048;
|
||||
let files = if root.join(".git").is_dir() {
|
||||
files.collect()
|
||||
} else {
|
||||
const MAX: usize = 8192;
|
||||
files.take(MAX).collect()
|
||||
};
|
||||
|
||||
Picker::new(
|
||||
files.take(MAX).collect(),
|
||||
files,
|
||||
move |path: &PathBuf| {
|
||||
// format_fn
|
||||
path.strip_prefix(&root)
|
||||
|
@@ -151,7 +151,11 @@ impl<T: 'static> Component for Picker<T> {
|
||||
code: KeyCode::Up, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('k'),
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_up(),
|
||||
KeyEvent {
|
||||
@@ -159,11 +163,18 @@ impl<T: 'static> Component for Picker<T> {
|
||||
..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('j'),
|
||||
code: KeyCode::Tab, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_down(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
return close_fn;
|
||||
}
|
||||
@@ -177,7 +188,7 @@ impl<T: 'static> Component for Picker<T> {
|
||||
return close_fn;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('x'),
|
||||
code: KeyCode::Char('h'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
if let Some(option) = self.selection() {
|
||||
|
@@ -125,13 +125,13 @@ impl<T: Component> Component for Popup<T> {
|
||||
let mut rel_x = position.col as u16;
|
||||
let mut rel_y = position.row as u16;
|
||||
if viewport.width <= rel_x + width {
|
||||
rel_x -= ((rel_x + width) - viewport.width)
|
||||
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
|
||||
};
|
||||
|
||||
// TODO: be able to specify orientation preference. We want above for most popups, below
|
||||
// for menus/autocomplete.
|
||||
if height <= rel_y {
|
||||
rel_y -= height // position above point
|
||||
rel_y = rel_y.saturating_sub(height) // position above point
|
||||
} else {
|
||||
rel_y += 1 // position below point
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ pub struct Prompt {
|
||||
pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum PromptEvent {
|
||||
/// The prompt input has been updated.
|
||||
Update,
|
||||
@@ -126,8 +126,21 @@ impl Prompt {
|
||||
let selected_color = theme.get("ui.menu.selected");
|
||||
// completion
|
||||
|
||||
let max_col = std::cmp::max(1, area.width / BASE_WIDTH);
|
||||
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
|
||||
let max_len = self
|
||||
.completion
|
||||
.iter()
|
||||
.map(|(_, completion)| completion.len() as u16)
|
||||
.max()
|
||||
.unwrap_or(BASE_WIDTH)
|
||||
.max(BASE_WIDTH);
|
||||
|
||||
let cols = std::cmp::max(1, area.width / max_len);
|
||||
let col_width = (area.width - (cols)) / cols;
|
||||
|
||||
let height = ((self.completion.len() as u16 + cols - 1) / cols)
|
||||
.min(10) // at most 10 rows (or less)
|
||||
.min(area.height);
|
||||
|
||||
let completion_area = Rect::new(
|
||||
area.x,
|
||||
(area.height - height).saturating_sub(1),
|
||||
@@ -144,7 +157,13 @@ impl Prompt {
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
|
||||
for (i, (_range, completion)) in self.completion.iter().enumerate() {
|
||||
// TODO: paginate
|
||||
for (i, (_range, completion)) in self
|
||||
.completion
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(height as usize * cols as usize)
|
||||
{
|
||||
let color = if Some(i) == self.selection {
|
||||
// Style::default().bg(Color::Rgb(104, 60, 232))
|
||||
selected_color // TODO: just invert bg
|
||||
@@ -152,10 +171,10 @@ impl Prompt {
|
||||
text_color
|
||||
};
|
||||
surface.set_stringn(
|
||||
area.x + 1 + col * BASE_WIDTH,
|
||||
area.x + col * (1 + col_width),
|
||||
area.y + row,
|
||||
&completion,
|
||||
BASE_WIDTH as usize - 1,
|
||||
col_width.saturating_sub(1) as usize,
|
||||
color,
|
||||
);
|
||||
row += 1;
|
||||
@@ -163,9 +182,6 @@ impl Prompt {
|
||||
row = 0;
|
||||
col += 1;
|
||||
}
|
||||
if col > max_col {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-tui"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-view"
|
||||
version = "0.0.10"
|
||||
version = "0.2.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
@@ -5,15 +5,16 @@ use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use helix_core::{
|
||||
history::History,
|
||||
syntax::{LanguageConfiguration, LOADER},
|
||||
ChangeSet, Diagnostic, History, Rope, Selection, State, Syntax, Transaction,
|
||||
ChangeSet, Diagnostic, Rope, Selection, State, Syntax, Transaction,
|
||||
};
|
||||
|
||||
use crate::{DocumentId, ViewId};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Select,
|
||||
@@ -52,6 +53,29 @@ pub struct Document {
|
||||
language_server: Option<Arc<helix_lsp::Client>>,
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
impl fmt::Debug for Document {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Document")
|
||||
.field("id", &self.id)
|
||||
.field("text", &self.text)
|
||||
.field("selections", &self.selections)
|
||||
.field("path", &self.path)
|
||||
.field("mode", &self.mode)
|
||||
.field("restore_cursor", &self.restore_cursor)
|
||||
.field("syntax", &self.syntax)
|
||||
.field("language", &self.language)
|
||||
.field("changes", &self.changes)
|
||||
.field("old_state", &self.old_state)
|
||||
// .field("history", &self.history)
|
||||
.field("last_saved_revision", &self.last_saved_revision)
|
||||
.field("version", &self.version)
|
||||
.field("diagnostics", &self.diagnostics)
|
||||
// .field("language_server", &self.language_server)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Like std::mem::replace() except it allows the replacement value to be mapped from the
|
||||
/// original value.
|
||||
fn take_with<T, F>(mut_ref: &mut T, closure: F)
|
||||
@@ -147,7 +171,12 @@ impl Document {
|
||||
Rope::from("\n")
|
||||
} else {
|
||||
let file = File::open(&path).context(format!("unable to open {:?}", path))?;
|
||||
Rope::from_reader(BufReader::new(file))?
|
||||
let mut doc = Rope::from_reader(BufReader::new(file))?;
|
||||
// add missing newline at the end of file
|
||||
if doc.len_bytes() == 0 || doc.byte(doc.len_bytes() - 1) != b'\n' {
|
||||
doc.insert_char(doc.len_chars(), '\n');
|
||||
}
|
||||
doc
|
||||
};
|
||||
|
||||
let mut doc = Self::new(doc);
|
||||
@@ -359,7 +388,7 @@ impl Document {
|
||||
success
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, view_id: ViewId) -> bool {
|
||||
pub fn undo(&mut self, view_id: ViewId) {
|
||||
let mut history = self.history.take();
|
||||
let success = if let Some(transaction) = history.undo() {
|
||||
self._apply(&transaction, view_id)
|
||||
@@ -372,11 +401,9 @@ impl Document {
|
||||
// reset changeset to fix len
|
||||
self.changes = ChangeSet::new(self.text());
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, view_id: ViewId) -> bool {
|
||||
pub fn redo(&mut self, view_id: ViewId) {
|
||||
let mut history = self.history.take();
|
||||
let success = if let Some(transaction) = history.redo() {
|
||||
self._apply(&transaction, view_id)
|
||||
@@ -389,8 +416,20 @@ impl Document {
|
||||
// reset changeset to fix len
|
||||
self.changes = ChangeSet::new(self.text());
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
|
||||
let txns = self.history.get_mut().earlier(uk);
|
||||
for txn in txns {
|
||||
self._apply(&txn, view_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
|
||||
let txns = self.history.get_mut().later(uk);
|
||||
for txn in txns {
|
||||
self._apply(&txn, view_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_changes_to_history(&mut self, view_id: ViewId) {
|
||||
@@ -423,8 +462,7 @@ impl Document {
|
||||
let history = self.history.take();
|
||||
let current_revision = history.current_revision();
|
||||
self.history.set(history);
|
||||
self.path.is_some()
|
||||
&& (current_revision != self.last_saved_revision || !self.changes.is_empty())
|
||||
current_revision != self.last_saved_revision || !self.changes.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use crate::{theme::Theme, tree::Tree, Document, DocumentId, View, ViewId};
|
||||
use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -9,17 +9,19 @@ use anyhow::Error;
|
||||
|
||||
pub use helix_core::diagnostic::Severity;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Editor {
|
||||
pub tree: Tree,
|
||||
pub documents: SlotMap<DocumentId, Document>,
|
||||
pub count: Option<usize>,
|
||||
pub count: Option<std::num::NonZeroUsize>,
|
||||
pub register: RegisterSelection,
|
||||
pub theme: Theme,
|
||||
pub language_servers: helix_lsp::Registry,
|
||||
|
||||
pub status_msg: Option<(String, Severity)>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Action {
|
||||
Replace,
|
||||
HorizontalSplit,
|
||||
@@ -57,12 +59,17 @@ impl Editor {
|
||||
tree: Tree::new(area),
|
||||
documents: SlotMap::with_key(),
|
||||
count: None,
|
||||
register: RegisterSelection::default(),
|
||||
theme,
|
||||
language_servers,
|
||||
status_msg: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_status(&mut self) {
|
||||
self.status_msg = None;
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: String) {
|
||||
self.status_msg = Some((status, Severity::Info));
|
||||
}
|
||||
@@ -81,6 +88,12 @@ impl Editor {
|
||||
pub fn switch(&mut self, id: DocumentId, action: Action) {
|
||||
use crate::tree::Layout;
|
||||
use helix_core::Selection;
|
||||
|
||||
if !self.documents.contains_key(id) {
|
||||
log::error!("cannot switch to document that does not exist (anymore)");
|
||||
return;
|
||||
}
|
||||
|
||||
match action {
|
||||
Action::Replace => {
|
||||
let view = self.view();
|
||||
@@ -91,6 +104,7 @@ impl Editor {
|
||||
|
||||
let view = self.view_mut();
|
||||
view.jumps.push(jump);
|
||||
view.last_accessed_doc = Some(view.doc);
|
||||
view.doc = id;
|
||||
view.first_line = 0;
|
||||
|
||||
@@ -153,7 +167,7 @@ impl Editor {
|
||||
let language_server = doc
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|language| self.language_servers.get(language));
|
||||
.and_then(|language| self.language_servers.get(language).ok());
|
||||
|
||||
if let Some(language_server) = language_server {
|
||||
doc.set_language_server(Some(language_server.clone()));
|
||||
@@ -194,7 +208,7 @@ impl Editor {
|
||||
let language_server = doc
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|language| language_servers.get(language));
|
||||
.and_then(|language| language_servers.get(language).ok());
|
||||
if let Some(language_server) = language_server {
|
||||
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
pub mod register_selection;
|
||||
pub mod theme;
|
||||
pub mod tree;
|
||||
pub mod view;
|
||||
@@ -10,5 +11,6 @@ new_key_type! { pub struct ViewId; }
|
||||
|
||||
pub use document::Document;
|
||||
pub use editor::Editor;
|
||||
pub use register_selection::RegisterSelection;
|
||||
pub use theme::Theme;
|
||||
pub use view::View;
|
||||
|
48
helix-view/src/register_selection.rs
Normal file
48
helix-view/src/register_selection.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
/// Register selection and configuration
|
||||
///
|
||||
/// This is a kind a of specialized `Option<char>` for register selection.
|
||||
/// Point is to keep whether the register selection has been explicitely
|
||||
/// set or not while being convenient by knowing the default register name.
|
||||
#[derive(Debug)]
|
||||
pub struct RegisterSelection {
|
||||
selected: char,
|
||||
default_name: char,
|
||||
}
|
||||
|
||||
impl RegisterSelection {
|
||||
pub fn new(default_name: char) -> Self {
|
||||
Self {
|
||||
selected: default_name,
|
||||
default_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&mut self, name: char) {
|
||||
self.selected = name;
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> Self {
|
||||
Self {
|
||||
selected: std::mem::replace(&mut self.selected, self.default_name),
|
||||
default_name: self.default_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.selected == self.default_name
|
||||
}
|
||||
|
||||
pub fn name(&self) -> char {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegisterSelection {
|
||||
fn default() -> Self {
|
||||
let default_name = '"';
|
||||
Self {
|
||||
selected: default_name,
|
||||
default_name,
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ use tui::layout::Rect;
|
||||
|
||||
// the dimensions are recomputed on windo resize/tree change.
|
||||
//
|
||||
#[derive(Debug)]
|
||||
pub struct Tree {
|
||||
root: ViewId,
|
||||
// (container, index inside the container)
|
||||
@@ -17,11 +18,13 @@ pub struct Tree {
|
||||
stack: Vec<(ViewId, Rect)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Node {
|
||||
parent: ViewId,
|
||||
content: Content,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Content {
|
||||
View(Box<View>),
|
||||
Container(Box<Container>),
|
||||
@@ -45,13 +48,14 @@ impl Node {
|
||||
|
||||
// TODO: screen coord to container + container coordinate helpers
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
// could explore stacked/tabbed
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Container {
|
||||
layout: Layout,
|
||||
children: Vec<ViewId>,
|
||||
@@ -432,6 +436,7 @@ impl Tree {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Traverse<'a> {
|
||||
tree: &'a Tree,
|
||||
stack: Vec<ViewId>, // TODO: reuse the one we use on update
|
||||
|
@@ -12,6 +12,7 @@ pub const PADDING: usize = 5;
|
||||
|
||||
type Jump = (DocumentId, Selection);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JumpList {
|
||||
jumps: Vec<Jump>,
|
||||
current: usize,
|
||||
@@ -37,20 +38,28 @@ impl JumpList {
|
||||
pub fn forward(&mut self, count: usize) -> Option<&Jump> {
|
||||
if self.current + count < self.jumps.len() {
|
||||
self.current += count;
|
||||
return self.jumps.get(self.current);
|
||||
self.jumps.get(self.current)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn backward(&mut self, count: usize) -> Option<&Jump> {
|
||||
if self.current.checked_sub(count).is_some() {
|
||||
self.current -= count;
|
||||
return self.jumps.get(self.current);
|
||||
// Taking view and doc to prevent unnecessary cloning when jump is not required.
|
||||
pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) -> Option<&Jump> {
|
||||
if let Some(current) = self.current.checked_sub(count) {
|
||||
if self.current == self.jumps.len() {
|
||||
let jump = (doc.id(), doc.selection(view_id).clone());
|
||||
self.push(jump);
|
||||
}
|
||||
self.current = current;
|
||||
self.jumps.get(self.current)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct View {
|
||||
pub id: ViewId,
|
||||
pub doc: DocumentId,
|
||||
@@ -58,6 +67,8 @@ pub struct View {
|
||||
pub first_col: usize,
|
||||
pub area: Rect,
|
||||
pub jumps: JumpList,
|
||||
/// the last accessed file before the current one
|
||||
pub last_accessed_doc: Option<DocumentId>,
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -69,6 +80,7 @@ impl View {
|
||||
first_col: 0,
|
||||
area: Rect::default(), // will get calculated upon inserting into tree
|
||||
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
|
||||
last_accessed_doc: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +118,7 @@ impl View {
|
||||
/// Calculates the last visible line on screen
|
||||
#[inline]
|
||||
pub fn last_line(&self, doc: &Document) -> usize {
|
||||
let height = self.area.height.saturating_sub(2); // - 2 for statusline
|
||||
let height = self.area.height.saturating_sub(1); // - 1 for statusline
|
||||
std::cmp::min(
|
||||
self.first_line + height as usize,
|
||||
doc.text().len_lines() - 1,
|
||||
|
@@ -4,6 +4,7 @@ scope = "source.rust"
|
||||
injection-regex = "rust"
|
||||
file-types = ["rs"]
|
||||
roots = []
|
||||
auto-format = true
|
||||
|
||||
language-server = { command = "rust-analyzer" }
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
@@ -61,6 +62,7 @@ scope = "source.go"
|
||||
injection-regex = "go"
|
||||
file-types = ["go"]
|
||||
roots = ["Gopkg.toml", "go.mod"]
|
||||
auto-format = true
|
||||
|
||||
language-server = { command = "gopls" }
|
||||
# TODO: gopls needs utf-8 offsets?
|
||||
@@ -116,6 +118,15 @@ language-server = { command = "pyls" }
|
||||
# TODO: pyls needs utf-8 offsets
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "nix"
|
||||
scope = "source.nix"
|
||||
injection-regex = "nix"
|
||||
file-types = ["nix"]
|
||||
roots = []
|
||||
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "ruby"
|
||||
scope = "source.ruby"
|
||||
|
@@ -17,7 +17,8 @@
|
||||
(atom_content)
|
||||
(atom_end)] @tag
|
||||
|
||||
(comment) @comment
|
||||
[(comment)
|
||||
(unused_identifier)] @comment
|
||||
|
||||
(escape_sequence) @escape
|
||||
|
||||
@@ -50,8 +51,7 @@
|
||||
left: (identifier) @variable.parameter
|
||||
operator: _ @function
|
||||
right: (identifier) @variable.parameter)]
|
||||
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$")
|
||||
(#match? @variable.parameter "^[^_]"))
|
||||
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
|
||||
|
||||
(call (function_identifier) @keyword
|
||||
[(call
|
||||
@@ -73,8 +73,7 @@
|
||||
(_ (_ (identifier) @variable.parameter))
|
||||
(_ (_ (_ (identifier) @variable.parameter)))
|
||||
(_ (_ (_ (_ (identifier) @variable.parameter))))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
|
||||
(#match? @variable.parameter "^[^_]"))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])))
|
||||
|
||||
(unary_op
|
||||
operator: "@"
|
||||
@@ -134,13 +133,6 @@
|
||||
">>"
|
||||
] @punctuation.bracket
|
||||
|
||||
[(identifier) @function.special
|
||||
(#match? @function.special "^__.+__$")]
|
||||
|
||||
[(remote_identifier) @function.special
|
||||
(#match? @function.special "^__.+__$")]
|
||||
|
||||
[(identifier) @comment
|
||||
(#match? @comment "^_")]
|
||||
(special_identifier) @function.special
|
||||
|
||||
(ERROR) @warning
|
||||
|
87
runtime/queries/nix/highlights.scm
Normal file
87
runtime/queries/nix/highlights.scm
Normal file
@@ -0,0 +1,87 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"let"
|
||||
"inherit"
|
||||
"in"
|
||||
"rec"
|
||||
"with"
|
||||
"assert"
|
||||
] @keyword
|
||||
|
||||
((identifier) @variable.builtin
|
||||
(#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$")
|
||||
(#is-not? local))
|
||||
|
||||
((identifier) @function.builtin
|
||||
(#match? @function.builtin "^(__add|__addErrorContext|__all|__any|__appendContext|__attrNames|__attrValues|__bitAnd|__bitOr|__bitXor|__catAttrs|__compareVersions|__concatLists|__concatMap|__concatStringsSep|__deepSeq|__div|__elem|__elemAt|__fetchurl|__filter|__filterSource|__findFile|__foldl'|__fromJSON|__functionArgs|__genList|__genericClosure|__getAttr|__getContext|__getEnv|__hasAttr|__hasContext|__hashFile|__hashString|__head|__intersectAttrs|__isAttrs|__isBool|__isFloat|__isFunction|__isInt|__isList|__isPath|__isString|__langVersion|__length|__lessThan|__listToAttrs|__mapAttrs|__match|__mul|__parseDrvName|__partition|__path|__pathExists|__readDir|__readFile|__replaceStrings|__seq|__sort|__split|__splitVersion|__storePath|__stringLength|__sub|__substring|__tail|__toFile|__toJSON|__toPath|__toXML|__trace|__tryEval|__typeOf|__unsafeDiscardOutputDependency|__unsafeDiscardStringContext|__unsafeGetAttrPos|__valueSize|abort|baseNameOf|derivation|derivationStrict|dirOf|fetchGit|fetchMercurial|fetchTarball|fromTOML|import|isNull|map|placeholder|removeAttrs|scopedImport|throw|toString)$")
|
||||
(#is-not? local))
|
||||
|
||||
[
|
||||
(string)
|
||||
(indented_string)
|
||||
] @string
|
||||
|
||||
[
|
||||
(path)
|
||||
(hpath)
|
||||
(spath)
|
||||
] @string.special.path
|
||||
|
||||
(uri) @string.special.uri
|
||||
|
||||
[
|
||||
(integer)
|
||||
(float)
|
||||
] @number
|
||||
|
||||
(interpolation
|
||||
"${" @punctuation.special
|
||||
"}" @punctuation.special) @embedded
|
||||
|
||||
(escape_sequence) @escape
|
||||
|
||||
(function
|
||||
universal: (identifier) @variable.parameter
|
||||
)
|
||||
|
||||
(formal
|
||||
name: (identifier) @variable.parameter
|
||||
"?"? @punctuation.delimiter)
|
||||
|
||||
(app
|
||||
function: [
|
||||
(identifier) @function
|
||||
(select
|
||||
attrpath: (attrpath
|
||||
attr: (attr_identifier) @function .))])
|
||||
|
||||
|
||||
(unary
|
||||
operator: _ @operator)
|
||||
|
||||
(binary
|
||||
operator: _ @operator)
|
||||
|
||||
(attr_identifier) @property
|
||||
(inherit attrs: (attrs_inherited (identifier) @property) )
|
||||
|
||||
[
|
||||
";"
|
||||
"."
|
||||
","
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
(identifier) @variable
|
9
runtime/queries/nix/indents.toml
Normal file
9
runtime/queries/nix/indents.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
indent = [
|
||||
"if",
|
||||
"let",
|
||||
"function",
|
||||
"attrset",
|
||||
"list",
|
||||
"indented_string",
|
||||
"parenthesized"
|
||||
]
|
26
shell.nix
26
shell.nix
@@ -1,20 +1,6 @@
|
||||
{ stdenv, pkgs }:
|
||||
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
(rust-bin.stable.latest.default.override { extensions = ["rust-src"]; })
|
||||
lld_10
|
||||
lldb
|
||||
# pythonPackages.six
|
||||
stdenv.cc.cc.lib
|
||||
# pkg-config
|
||||
];
|
||||
RUSTFLAGS = "-C link-arg=-fuse-ld=lld -C target-cpu=native";
|
||||
RUST_BACKTRACE = "1";
|
||||
# https://github.com/rust-lang/rust/issues/55979
|
||||
LD_LIBRARY_PATH="${stdenv.cc.cc.lib}/lib64:$LD_LIBRARY_PATH";
|
||||
|
||||
shellHook = ''
|
||||
export HELIX_RUNTIME=$PWD/runtime
|
||||
'';
|
||||
}
|
||||
# Flake's devShell for non-flake-enabled nix instances
|
||||
let
|
||||
src = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.flakeCompat.locked;
|
||||
compat = fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/${src.rev}.tar.gz"; sha256 = src.narHash; };
|
||||
in
|
||||
(import compat { src = ./.; }).shellNix.default
|
||||
|
Reference in New Issue
Block a user