mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 00:13:28 +02:00
Compare commits
199 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
44566ea812 | ||
|
cad14c6b46 | ||
|
ed1a745442 | ||
|
a6cadddef4 | ||
|
2dba228c76 | ||
|
eb6fb63e74 | ||
|
d534d6470f | ||
|
e8d2f3612f | ||
|
c688288881 | ||
|
f2d8ce3415 | ||
|
503ca112ae | ||
|
8e277ad8ba | ||
|
4418e17547 | ||
|
74cc4b4a49 | ||
|
c2b937481f | ||
|
18beda38ac | ||
|
10548bf0e3 | ||
|
15ae2e7ef1 | ||
|
7ae21b98ce | ||
|
629df6124d | ||
|
8935e7a879 | ||
|
394629ab73 | ||
|
fb8e7dc25b | ||
|
2924522aea | ||
|
14f61fb6ac | ||
|
9cbf564d08 | ||
|
7f6265ecf3 | ||
|
0f55e67576 | ||
|
7366fe81e0 | ||
|
4ad7b61c69 | ||
|
655c1aeb73 | ||
|
ea8cd4765d | ||
|
39dc09e6c4 | ||
|
3606d8bd24 | ||
|
c534fdefdc | ||
|
d70be55f70 | ||
|
ac1e98d088 | ||
|
f09ccbc891 | ||
|
ed6528b9a6 | ||
|
6564257a7b | ||
|
7896eefd73 | ||
|
fd98e743e8 | ||
|
9706f1121d | ||
|
2ff9b362fb | ||
|
848cc1b438 | ||
|
481c4ba044 | ||
|
0cbaa998ce | ||
|
38bf9c2576 | ||
|
9c53461429 | ||
|
7511110d82 | ||
|
fd1ae35051 | ||
|
16883e7543 | ||
|
b56174d738 | ||
|
866b32b5d7 | ||
|
39d59216e4 | ||
|
20f33ead67 | ||
|
e0fd08d6df | ||
|
753ed4cbc5 | ||
|
892c80771a | ||
|
b00e9fc227 | ||
|
b79b5e66f2 | ||
|
86271bac18 | ||
|
4754b2e5ae | ||
|
13648d28b9 | ||
|
2f321b9335 | ||
|
6dddd5cd1d | ||
|
a70de6e980 | ||
|
c704970fd7 | ||
|
05bf9edebd | ||
|
f2954fa153 | ||
|
a18d50b777 | ||
|
7c4fa18764 | ||
|
d33355650f | ||
|
e436c30ed7 | ||
|
23d6188535 | ||
|
07e28802f6 | ||
|
714002048c | ||
|
9fd17d4ff5 | ||
|
994ff4b269 | ||
|
ee80fa8ea9 | ||
|
aca9d73fe4 | ||
|
cc357d5096 | ||
|
b2804b14b1 | ||
|
618ad55dc1 | ||
|
d39a764399 | ||
|
3d3149e0d5 | ||
|
e686c3e462 | ||
|
4efd6713c5 | ||
|
985625763a | ||
|
eaf259f8aa | ||
|
f41688d960 | ||
|
ffb54b4eac | ||
|
f50261c944 | ||
|
a2b8cfca34 | ||
|
d59c9f3baf | ||
|
82018af609 | ||
|
fc39a6c40d | ||
|
0882712b45 | ||
|
980e602352 | ||
|
5d22e3c4e5 | ||
|
34ebe82654 | ||
|
e9a3245aae | ||
|
9275021497 | ||
|
59c59deb46 | ||
|
29f77b9c5f | ||
|
4b7276ddd6 | ||
|
4f108ab1b2 | ||
|
8634e04a31 | ||
|
701eb0dd68 | ||
|
2d629a880c | ||
|
28d9673a8e | ||
|
6825e19509 | ||
|
42e13bd542 | ||
|
b1a41c4cc8 | ||
|
a2db161d5a | ||
|
ce97a2f05f | ||
|
f424a61054 | ||
|
3b534e17f4 | ||
|
cd0ecded1f | ||
|
10f9f72232 | ||
|
11f20af25f | ||
|
1e80fbb602 | ||
|
cdd9347457 | ||
|
97323dc2f9 | ||
|
ecb884db98 | ||
|
2cbec2b047 | ||
|
ca806d4f85 | ||
|
1c25852021 | ||
|
c5a2fd5da3 | ||
|
dd0af78079 | ||
|
c2aad859b1 | ||
|
03d1ca7b0a | ||
|
db5bdf4f2d | ||
|
b48054f3ee | ||
|
1c1474c3b8 | ||
|
b0522239e7 | ||
|
0151826233 | ||
|
1bb3b778ad | ||
|
b1cb98283d | ||
|
a3cb79ebaa | ||
|
bbefc1db63 | ||
|
d095ec15d4 | ||
|
612511dc98 | ||
|
e1109a5a01 | ||
|
38cb934d8f | ||
|
80b4a69053 | ||
|
a6d39585d8 | ||
|
52fb90b81e | ||
|
41b07486ad | ||
|
42142cf680 | ||
|
8664d70e73 | ||
|
f65db9397a | ||
|
14db2cc68b | ||
|
8bccd6df30 | ||
|
f7e00cf720 | ||
|
47d2e3aefa | ||
|
20d6b202d5 | ||
|
9c3eadb2e4 | ||
|
7cf0fa05a4 | ||
|
a4f5a0134e | ||
|
a9a718c3ca | ||
|
e4849f41be | ||
|
9c419fe05c | ||
|
5eb6918392 | ||
|
17f69a03e0 | ||
|
3756c21bae | ||
|
a364d6c383 | ||
|
d1c8a74771 | ||
|
33a35b7589 | ||
|
124514aa70 | ||
|
6bdf609caa | ||
|
6fb2d2679d | ||
|
eb77de6a51 | ||
|
05ed3e8fb8 | ||
|
002f1ad397 | ||
|
7c2fb92c91 | ||
|
d415a666fe | ||
|
ecb39da3e0 | ||
|
4faf1d3bf4 | ||
|
0a5580aa21 | ||
|
358ea6a37c | ||
|
8648e483f7 | ||
|
5ca043c17a | ||
|
2329512122 | ||
|
1bda454149 | ||
|
e819121f6e | ||
|
f33aaba53f | ||
|
9cfa163370 | ||
|
6b8c6ed535 | ||
|
e4b3a666d2 | ||
|
43e3173231 | ||
|
8c1edd22af | ||
|
9b352ceefd | ||
|
f882ea92b6 | ||
|
98485524c8 | ||
|
f47da891db | ||
|
5d23667a26 | ||
|
b6e363ef0e | ||
|
ca02024199 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
helix-view/tests/* linguist-generated
|
4
.github/ISSUE_TEMPLATE/blank_issue.md
vendored
Normal file
4
.github/ISSUE_TEMPLATE/blank_issue.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
name: Blank Issue
|
||||
about: Create a blank issue.
|
||||
---
|
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature or improvement
|
||||
title: ''
|
||||
labels: C-enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!-- Your feature may already be reported!
|
||||
Please search on the issue tracker before creating one. -->
|
||||
|
||||
#### Describe your feature request
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --locked
|
||||
args: --release --locked
|
||||
|
||||
- name: Build release binary
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
cp "target/${{ matrix.target }}/release/hx" "dist/"
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v2.2.3
|
||||
- uses: actions/upload-artifact@v2.2.4
|
||||
with:
|
||||
name: bins-${{ matrix.build }}
|
||||
path: dist
|
||||
|
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -89,3 +89,8 @@
|
||||
[submodule "helix-syntax/languages/tree-sitter-nix"]
|
||||
path = helix-syntax/languages/tree-sitter-nix
|
||||
url = https://github.com/cstrahan/tree-sitter-nix
|
||||
shallow = true
|
||||
[submodule "helix-syntax/languages/tree-sitter-latex"]
|
||||
path = helix-syntax/languages/tree-sitter-latex
|
||||
url = https://github.com/latex-lsp/tree-sitter-latex
|
||||
shallow = true
|
||||
|
87
CHANGELOG.md
Normal file
87
CHANGELOG.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 0.3.0
|
||||
|
||||
Another big release.
|
||||
|
||||
Highlights:
|
||||
|
||||
- Indentation is now automatically detected from file heuristics. ([#245](https://github.com/helix-editor/helix/pull/245))
|
||||
- Support for other line endings (CRLF). Significantly improved Windows support. ([#224](https://github.com/helix-editor/helix/pull/224))
|
||||
- Encodings other than UTF-8 are now supported! ([#228](https://github.com/helix-editor/helix/pull/228))
|
||||
- Key bindings can now be configured via a `config.toml` file ([#268](https://github.com/helix-editor/helix/pull/268))
|
||||
- Theme can now be configured and changed at runtime ([please feel free to contribute more themes!](https://github.com/helix-editor/helix/tree/master/runtime/themes)) ([#267](https://github.com/helix-editor/helix/pull/267))
|
||||
- System clipboard yank/paste is now supported! ([#310](https://github.com/helix-editor/helix/pull/310))
|
||||
- Surround commands were implemented ([#320](https://github.com/helix-editor/helix/pull/320))
|
||||
|
||||
Features:
|
||||
|
||||
- File picker can now be repeatedly filtered ([#232](https://github.com/helix-editor/helix/pull/232))
|
||||
- LSP progress is now received and rendered as a spinner ([#234](https://github.com/helix-editor/helix/pull/234))
|
||||
- Current line number can now be themed ([#260](https://github.com/helix-editor/helix/pull/260))
|
||||
- Arrow keys & home/end now work in insert mode ([#305](https://github.com/helix-editor/helix/pull/305))
|
||||
- Cursors and selections can now be themed ([#325](https://github.com/helix-editor/helix/pull/325))
|
||||
- Language servers are now gracefully shut down before `hx` exits ([#287](https://github.com/helix-editor/helix/pull/287))
|
||||
- `:show-directory`/`:change-directory` ([#335](https://github.com/helix-editor/helix/pull/335))
|
||||
- File picker is now sorted by access time (before filtering) ([#336](https://github.com/helix-editor/helix/pull/336))
|
||||
- Code is being migrated from helix-term to helix-view (prerequisite for
|
||||
alternative frontends) ([#366](https://github.com/helix-editor/helix/pull/366))
|
||||
- `x` and `X` merged
|
||||
([f41688d9](https://github.com/helix-editor/helix/commit/f41688d960ef89c29c4a51c872b8406fb8f81a85))
|
||||
|
||||
Fixes:
|
||||
|
||||
- The IME popup is now correctly positioned ([#273](https://github.com/helix-editor/helix/pull/273))
|
||||
- A bunch of bugs regarding `o`/`O` behavior ([#281](https://github.com/helix-editor/helix/pull/281))
|
||||
- `~` expansion now works in file completion ([#284](https://github.com/helix-editor/helix/pull/284))
|
||||
- Several UI related overflow crashes ([#318](https://github.com/helix-editor/helix/pull/318))
|
||||
- Fix a test failure occuring only on `test --release` ([4f108ab1](https://github.com/helix-editor/helix/commit/4f108ab1b2197809506bd7305ad903a3525eabfa))
|
||||
- Prompts now support unicode input ([#295](https://github.com/helix-editor/helix/pull/295))
|
||||
- Completion documentation no longer overlaps the popup ([#322](https://github.com/helix-editor/helix/pull/322))
|
||||
- Fix a crash when trying to select `^` ([9c534614](https://github.com/helix-editor/helix/commit/9c53461429a3e72e3b1fb87d7ca490e168d7dee2))
|
||||
- Prompt completions are now paginated ([39dc09e6](https://github.com/helix-editor/helix/commit/39dc09e6c4172299bc79de4c1c52288d3f624bd7))
|
||||
- Goto did not work on Windows ([503ca112](https://github.com/helix-editor/helix/commit/503ca112ae57ebdf3ea323baf8940346204b46d2))
|
||||
|
||||
# 0.2.1
|
||||
|
||||
Includes a fix where wq/wqa could exit before file saving completed.
|
||||
|
||||
# 0.2.0
|
||||
|
||||
Enough has changed to bump the version. We're skipping 0.1.x because
|
||||
previously the CLI would always report version as 0.1.0, and we'd like
|
||||
to distinguish it in bug reports..
|
||||
|
||||
- The `runtime/` directory is now properly detected on binary releases and
|
||||
on cargo run. `~/.config/helix/runtime` can also be used.
|
||||
- Registers can now be selected via " (for example `"ay`)
|
||||
- Support for Nix files was added
|
||||
- Movement is now fully tested and matches kakoune implementation
|
||||
- A per-file LSP symbol picker was added to space+s
|
||||
- Selection can be replaced with yanked text via R
|
||||
|
||||
- `1g` now correctly goes to line 1
|
||||
- `ctrl-i` now correctly jumps backwards in history
|
||||
- A small memory leak was fixed, where we tried to reuse tree-sitter
|
||||
query cursors, but always allocated a new one
|
||||
- Auto-formatting is now only on for certain languages
|
||||
- The root directory is now provided in LSP initialization, fixing
|
||||
certain language servers (typescript)
|
||||
- LSP failing to start no longer panics
|
||||
- Elixir language queries were fixed
|
||||
|
||||
# 0.0.10
|
||||
|
||||
Keymaps:
|
||||
- Add mappings to jump to diagnostics
|
||||
- Add gt/gm/gb mappings to jump to top/middle/bottom of screen
|
||||
- ^ and $ are now gh, gl
|
||||
|
||||
- The runtime/ can now optionally be embedded in the binary
|
||||
- Haskell syntax added
|
||||
- Window mode (ctrl-w) added
|
||||
- Show matching bracket (vim's matchbrackets)
|
||||
- Themes now support style modifiers
|
||||
- First user contributed theme
|
||||
- Create a document if it doesn't exist yet on save
|
||||
- Detect language on a new file on save
|
||||
|
||||
- Panic fixes, lots of them
|
152
Cargo.lock
generated
152
Cargo.lock
generated
@@ -13,9 +13,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.40"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
|
||||
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -59,12 +65,29 @@ dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chardetng"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81a81b0d8f8ee23417182818b4f06312c5f535c2b04eef1773f7c24bbdf8c500"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
@@ -83,32 +106,32 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
|
||||
checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"futures-core",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9"
|
||||
checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
@@ -119,7 +142,7 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
@@ -134,13 +157,28 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "016b04fd1e94fb833d432634245c9bb61cf1c7409668a0e7d4c3ab00c5172dec"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"dirs-next",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -232,16 +270,16 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
|
||||
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
@@ -252,8 +290,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-core"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"etcetera",
|
||||
"helix-syntax",
|
||||
"once_cell",
|
||||
@@ -272,7 +311,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-lsp"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-executor",
|
||||
@@ -290,7 +329,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-syntax"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"serde",
|
||||
@@ -300,7 +339,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-term"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -326,11 +365,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-tui"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"crossterm",
|
||||
"helix-core",
|
||||
"helix-view",
|
||||
"serde",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
@@ -338,10 +379,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-view"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"chardetng",
|
||||
"crossterm",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"helix-core",
|
||||
"helix-lsp",
|
||||
"helix-tui",
|
||||
@@ -352,6 +397,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"toml",
|
||||
"url",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -376,9 +422,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.17"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
|
||||
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"globset",
|
||||
@@ -398,7 +444,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -437,9 +483,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.95"
|
||||
version = "0.2.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
|
||||
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -456,14 +502,14 @@ version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.89.1"
|
||||
version = "0.89.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f"
|
||||
checksum = "852e0dedfd52cc32325598b2631e0eba31b7b708959676a9f837042f276b09a2"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"serde",
|
||||
@@ -492,9 +538,9 @@ checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.7.11"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956"
|
||||
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -558,9 +604,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.7.2"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -579,7 +625,7 @@ version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
@@ -636,9 +682,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
|
||||
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -672,9 +718,9 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "ropey"
|
||||
version = "1.2.0"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f3ef16589fdbb3e8fbce3dca944c08e61f39c7f16064b21a257d68ea911a83"
|
||||
checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
@@ -777,13 +823,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.17"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
|
||||
checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook-registry",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -818,9 +874,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.72"
|
||||
version = "1.0.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -893,9 +949,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.6.1"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
|
||||
checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -1045,6 +1101,16 @@ version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
|
||||
dependencies = [
|
||||
"either",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
@@ -55,10 +55,7 @@ it with:
|
||||
cargo install --path helix-term --features "embed_runtime"
|
||||
```
|
||||
|
||||
## Arch Linux
|
||||
There are two packages available from AUR:
|
||||
- `helix-bin`: contains prebuilt binary from GitHub releases
|
||||
- `helix-git`: builds the master branch of this repository
|
||||
[](https://repology.org/project/helix/versions)
|
||||
|
||||
## MacOS
|
||||
Helix can be installed on MacOS through homebrew via:
|
||||
@@ -74,7 +71,7 @@ Contributors are very welcome! **No contribution is too small and all contributi
|
||||
|
||||
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.
|
||||
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) 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!")`)
|
||||
|
19
TODO.md
19
TODO.md
@@ -1,8 +1,3 @@
|
||||
- Refactor tree-sitter-highlight to work like the atom one, recomputing partial tree updates.
|
||||
|
||||
------
|
||||
|
||||
as you type completion!
|
||||
|
||||
- tree sitter:
|
||||
- lua
|
||||
@@ -15,20 +10,16 @@ as you type completion!
|
||||
- clojure
|
||||
- erlang
|
||||
|
||||
as you type completion!
|
||||
- [ ] use signature_help_provider and completion_provider trigger characters in
|
||||
a hook to trigger signature help text / autocompletion
|
||||
- [ ] document.on_type provider triggers
|
||||
- [ ] completion isIncomplete support
|
||||
|
||||
- [ ] scroll wheel support
|
||||
- [ ] matching bracket highlight
|
||||
|
||||
1
|
||||
- [ ] respect view fullscreen flag
|
||||
- [ ] Implement marks (superset of Selection/Range)
|
||||
|
||||
- [ ] nixos packaging
|
||||
|
||||
- [ ] = for auto indent line/selection
|
||||
- [ ] :x for closing buffers
|
||||
|
||||
@@ -37,25 +28,19 @@ as you type completion!
|
||||
- [] jump to alt buffer
|
||||
|
||||
- [ ] lsp: signature help
|
||||
- [x] lsp: hover
|
||||
- [ ] lsp: document symbols (nested/vec)
|
||||
- [ ] lsp: code actions
|
||||
- [ ] lsp: formatting
|
||||
- [x] lsp: goto
|
||||
|
||||
- [ ] search: smart case by default: insensitive unless upper detected
|
||||
|
||||
- [ ] move Compositor into tui
|
||||
|
||||
2
|
||||
- [ ] surround bindings (select + surround ( wraps selection in parens )
|
||||
- [ ] macro recording
|
||||
- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
|
||||
- [x] bracket pairs
|
||||
- [x] comment block (gcc)
|
||||
- [ ] selection align
|
||||
- [ ] store some state between restarts: file positions, prompt history
|
||||
- [ ] highlight matched characters in completion
|
||||
- [ ] highlight matched characters in picker
|
||||
|
||||
3
|
||||
- [ ] diff mode with highlighting?
|
||||
|
@@ -3,5 +3,7 @@
|
||||
- [Installation](./install.md)
|
||||
- [Usage](./usage.md)
|
||||
- [Configuration](./configuration.md)
|
||||
- [Themes](./themes.md)
|
||||
- [Keymap](./keymap.md)
|
||||
- [Key Remapping](./remapping.md)
|
||||
- [Hooks](./hooks.md)
|
||||
|
@@ -1,87 +1,11 @@
|
||||
# Configuration
|
||||
|
||||
## Theme
|
||||
To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
|
||||
|
||||
Use a custom theme by placing a theme.toml in your config directory (i.e ~/.config/helix/theme.toml). The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/contrib/themes).
|
||||
|
||||
Styles in theme.toml are specified of in the form:
|
||||
## LSP
|
||||
|
||||
To display all language server messages in the status line add the following to your `config.toml`:
|
||||
```toml
|
||||
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
|
||||
[lsp]
|
||||
display-messages = true
|
||||
```
|
||||
|
||||
where `name` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
|
||||
|
||||
To specify only the foreground color:
|
||||
|
||||
```toml
|
||||
key = "#ffffff"
|
||||
```
|
||||
|
||||
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
|
||||
|
||||
```toml
|
||||
"key.key" = "#ffffff"
|
||||
```
|
||||
|
||||
Possible modifiers:
|
||||
|
||||
| modifier |
|
||||
| --- |
|
||||
| bold |
|
||||
| dim |
|
||||
| italic |
|
||||
| underlined |
|
||||
| slow\_blink |
|
||||
| rapid\_blink |
|
||||
| reversed |
|
||||
| hidden |
|
||||
| crossed\_out |
|
||||
|
||||
Possible keys:
|
||||
|
||||
| key | notes |
|
||||
| --- | --- |
|
||||
| attribute | |
|
||||
| keyword | |
|
||||
| keyword.directive | preprocessor directives (\#if in C) |
|
||||
| namespace | |
|
||||
| punctuation | |
|
||||
| punctuation.delimiter | |
|
||||
| operator | |
|
||||
| special | |
|
||||
| property | |
|
||||
| variable | |
|
||||
| variable.parameter | |
|
||||
| type | |
|
||||
| type.builtin | |
|
||||
| constructor | |
|
||||
| function | |
|
||||
| function.macro | |
|
||||
| function.builtin | |
|
||||
| comment | |
|
||||
| variable.builtin | |
|
||||
| constant | |
|
||||
| constant.builtin | |
|
||||
| string | |
|
||||
| number | |
|
||||
| escape | escaped characters |
|
||||
| label | used for lifetimes |
|
||||
| module | |
|
||||
| ui.background | |
|
||||
| ui.linenr | |
|
||||
| ui.statusline | |
|
||||
| ui.popup | |
|
||||
| ui.window | |
|
||||
| ui.help | |
|
||||
| ui.text | |
|
||||
| ui.text.focus | |
|
||||
| ui.menu.selected | |
|
||||
| warning | LSP warning |
|
||||
| error | LSP error |
|
||||
| info | LSP info |
|
||||
| hint | LSP hint |
|
||||
|
||||
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
|
||||
|
||||
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.
|
||||
|
@@ -2,11 +2,16 @@
|
||||
|
||||
We provide pre-built binaries on the [GitHub Releases page](https://github.com/helix-editor/helix/releases).
|
||||
|
||||
[](https://repology.org/project/helix/versions)
|
||||
|
||||
## OSX
|
||||
|
||||
TODO: brew tap
|
||||
A Homebrew tap is available:
|
||||
|
||||
Please use a pre-built binary release for the time being.
|
||||
```
|
||||
brew tap helix-editor/helix
|
||||
brew install helix
|
||||
```
|
||||
|
||||
## Linux
|
||||
|
||||
|
@@ -4,100 +4,101 @@
|
||||
|
||||
### Movement
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| h, Left | move left |
|
||||
| j, Down | move down |
|
||||
| k, Up | move up |
|
||||
| l, Right | move right |
|
||||
| w | move next word start |
|
||||
| b | move previous word start |
|
||||
| e | move next word end |
|
||||
| t | find 'till next char |
|
||||
| f | find next char |
|
||||
| T | find 'till previous char |
|
||||
| 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 |
|
||||
| PageUp | Move page up |
|
||||
| PageDown | Move page down |
|
||||
| ctrl-u | Move half page up |
|
||||
| ctrl-d | Move half page down |
|
||||
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab |
|
||||
| ctrl-o | Jump backward on the jumplist |
|
||||
| v | Enter select (extend) mode |
|
||||
| g | Enter goto mode |
|
||||
| : | Enter command mode |
|
||||
| z | Enter view mode |
|
||||
| ctrl-w | Enter window mode (maybe will be remove for spc w w later) |
|
||||
| space | Enter space mode |
|
||||
| K | Show documentation for the item under the cursor |
|
||||
> NOTE: `f`, `F`, `t` and `T` are not confined to the current line.
|
||||
|
||||
| Key | Description |
|
||||
| ----- | ----------- |
|
||||
| `h`, `Left` | Move left |
|
||||
| `j`, `Down` | Move down |
|
||||
| `k`, `Up` | Move up |
|
||||
| `l`, `Right` | Move right |
|
||||
| `w` | Move next word start |
|
||||
| `b` | Move previous word start |
|
||||
| `e` | Move next word end |
|
||||
| `t` | Find 'till next char |
|
||||
| `f` | Find next char |
|
||||
| `T` | Find 'till previous char |
|
||||
| `F` | Find previous char |
|
||||
| `Home` | Move to the start of the line |
|
||||
| `End` | Move to the end of the line |
|
||||
| `PageUp` | Move page up |
|
||||
| `PageDown` | Move page down |
|
||||
| `Ctrl-u` | Move half page up |
|
||||
| `Ctrl-d` | Move half page down |
|
||||
| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab |
|
||||
| `Ctrl-o` | Jump backward on the jumplist |
|
||||
| `v` | Enter [select (extend) mode](#select--extend-mode) |
|
||||
| `g` | Enter [goto mode](#goto-mode) |
|
||||
| `m` | Enter [match mode](#match-mode)
|
||||
| `:` | Enter command mode |
|
||||
| `z` | Enter [view mode](#view-mode) |
|
||||
| `Ctrl-w` | Enter [window mode](#window-mode) (maybe will be remove for spc w w later) |
|
||||
| `Space` | Enter [space mode](#space-mode) |
|
||||
| `K` | Show documentation for the item under the cursor |
|
||||
|
||||
### Changes
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| 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 |
|
||||
| u | Undo change |
|
||||
| U | Redo change |
|
||||
| y | Yank selection |
|
||||
| p | Paste after selection |
|
||||
| P | Paste before selection |
|
||||
| > | Indent selection |
|
||||
| < | Unindent selection |
|
||||
| = | Format selection |
|
||||
| d | Delete selection |
|
||||
| c | Change selection (delete and enter insert mode) |
|
||||
| Key | Description |
|
||||
| ----- | ----------- |
|
||||
| `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 |
|
||||
| `u` | Undo change |
|
||||
| `U` | Redo change |
|
||||
| `y` | Yank selection |
|
||||
| `p` | Paste after selection |
|
||||
| `P` | Paste before selection |
|
||||
| `>` | Indent selection |
|
||||
| `<` | Unindent selection |
|
||||
| `=` | Format selection |
|
||||
| `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 |
|
||||
| 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 |
|
||||
| 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, if already selected, 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 |
|
||||
|
||||
### Search
|
||||
|
||||
> TODO: The search implementation isn't ideal yet -- we don't support searching
|
||||
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 |
|
||||
| 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 |
|
||||
|
||||
### Diagnostics
|
||||
|
||||
> NOTE: `[` and `]` will likely contain more pair mappings in the style of
|
||||
> [vim-unimpaired](https://github.com/tpope/vim-unimpaired)
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| [d | Go to previous diagnostic |
|
||||
| ]d | Go to next diagnostic |
|
||||
| [D | Go to first diagnostic in document |
|
||||
| ]D | Go to last diagnostic in document |
|
||||
| Key | Description |
|
||||
| ----- | ----------- |
|
||||
| `[d` | Go to previous diagnostic |
|
||||
| `]d` | Go to next diagnostic |
|
||||
| `[D` | Go to first diagnostic in document |
|
||||
| `]D` | Go to last diagnostic in document |
|
||||
|
||||
## Select / extend mode
|
||||
|
||||
@@ -112,14 +113,14 @@ commands to extend the existing selection instead of replacing it.
|
||||
View mode is intended for scrolling and manipulating the view without changing
|
||||
the selection.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| z , c | Vertically center the line |
|
||||
| t | Align the line to the top of the screen |
|
||||
| b | Align the line to the bottom of the screen |
|
||||
| m | Align the line to the middle of the screen (horizontally) |
|
||||
| j | Scroll the view downwards |
|
||||
| k | Scroll the view upwards |
|
||||
| Key | Description |
|
||||
| ----- | ----------- |
|
||||
| `z` , `c` | Vertically center the line |
|
||||
| `t` | Align the line to the top of the screen |
|
||||
| `b` | Align the line to the bottom of the screen |
|
||||
| `m` | Align the line to the middle of the screen (horizontally) |
|
||||
| `j` | Scroll the view downwards |
|
||||
| `k` | Scroll the view upwards |
|
||||
|
||||
## Goto mode
|
||||
|
||||
@@ -127,21 +128,33 @@ Jumps to various locations.
|
||||
|
||||
> NOTE: Some of these features are only available with the LSP present.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| g | Go to the start of the file |
|
||||
| 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 |
|
||||
| d | Go to definition |
|
||||
| y | Go to type definition |
|
||||
| r | Go to references |
|
||||
| i | Go to implementation |
|
||||
| a | Go to the last accessed/alternate file |
|
||||
| Key | Description |
|
||||
| ----- | ----------- |
|
||||
| `g` | Go to the start of the file |
|
||||
| `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 |
|
||||
| `d` | Go to definition |
|
||||
| `y` | Go to type definition |
|
||||
| `r` | Go to references |
|
||||
| `i` | Go to implementation |
|
||||
| `a` | Go to the last accessed/alternate file |
|
||||
|
||||
## Match mode
|
||||
|
||||
Enter this mode using `m` from normal mode. See the relavant section
|
||||
in [Usage](./usage.md#surround) for an explanation about surround usage.
|
||||
|
||||
| Key | Description |
|
||||
| ----- | ----------- |
|
||||
| `m` | Goto matching bracket |
|
||||
| `s` `<char>` | Surround current selection with `<char>` |
|
||||
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` |
|
||||
| `d` `<char>` | Delete surround character `<char>` |
|
||||
|
||||
## Object mode
|
||||
|
||||
@@ -151,21 +164,40 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
|
||||
|
||||
This layer is similar to vim keybindings as kakoune does not support window.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| w, ctrl-w | Switch to next window |
|
||||
| v, ctrl-v | Vertical right split |
|
||||
| h, ctrl-h | Horizontal bottom split |
|
||||
| q, ctrl-q | Close current window |
|
||||
| Key | Description |
|
||||
| ----- | ------------- |
|
||||
| `w`, `Ctrl-w` | Switch to next window |
|
||||
| `v`, `Ctrl-v` | Vertical right split |
|
||||
| `h`, `Ctrl-h` | Horizontal bottom split |
|
||||
| `q`, `Ctrl-q` | Close current window |
|
||||
|
||||
## Space mode
|
||||
|
||||
This layer is a kludge of mappings I had under leader key in neovim.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-----------|
|
||||
| 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 |
|
||||
| Key | Description |
|
||||
| ----- | ----------- |
|
||||
| `f` | Open file picker |
|
||||
| `b` | Open buffer picker |
|
||||
| `s` | Open symbol picker (current document) |
|
||||
| `w` | Enter [window mode](#window-mode) |
|
||||
| `space` | Keep primary selection TODO: it's here because space mode replaced it |
|
||||
| `p` | paste system clipboard after selections |
|
||||
| `P` | paste system clipboard before selections |
|
||||
| `y` | join and yank selections to clipboard |
|
||||
| `Y` | yank main selection to clipboard |
|
||||
| `R` | replace selections by clipboard contents |
|
||||
|
||||
# Picker
|
||||
|
||||
Keys to use within picker.
|
||||
|
||||
| Key | Description |
|
||||
| ----- | ------------- |
|
||||
| `Up`, `Ctrl-p` | Previous entry |
|
||||
| `Down`, `Ctrl-n` | Next entry |
|
||||
| `Ctrl-space` | Filter options |
|
||||
| `Enter` | Open selected |
|
||||
| `Ctrl-h` | Open horizontally |
|
||||
| `Ctrl-v` | Open vertically |
|
||||
| `Escape`, `Ctrl-c` | Close picker |
|
||||
|
50
book/src/remapping.md
Normal file
50
book/src/remapping.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Key Remapping
|
||||
|
||||
One-way key remapping is temporarily supported via a simple TOML configuration
|
||||
file. (More powerful solutions such as rebinding via commands will be
|
||||
available in the feature).
|
||||
|
||||
To remap keys, write a `config.toml` file in your `helix` configuration
|
||||
directory (default `~/.config/helix` in Linux systems) with a structure like
|
||||
this:
|
||||
|
||||
```toml
|
||||
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
|
||||
[keys.normal]
|
||||
a = "move_char_left" # Maps the 'a' key to the move_char_left command
|
||||
w = "move_line_up" # Maps the 'w' key move_line_up
|
||||
C-S-esc = "extend_line" # Maps Control-Shift-Escape to extend_line
|
||||
|
||||
[keys.insert]
|
||||
A-x = "normal_mode" # Maps Alt-X to enter normal mode
|
||||
```
|
||||
|
||||
Control, Shift and Alt modifiers are encoded respectively with the prefixes
|
||||
`C-`, `S-` and `A-`. Special keys are encoded as follows:
|
||||
|
||||
| Key name | Representation |
|
||||
| --- | --- |
|
||||
| Backspace | `"backspace"` |
|
||||
| Space | `"space"` |
|
||||
| Return/Enter | `"ret"` |
|
||||
| < | `"lt"` |
|
||||
| \> | `"gt"` |
|
||||
| \+ | `"plus"` |
|
||||
| \- | `"minus"` |
|
||||
| ; | `"semicolon"` |
|
||||
| % | `"percent"` |
|
||||
| Left | `"left"` |
|
||||
| Right | `"right"` |
|
||||
| Up | `"up"` |
|
||||
| Home | `"home"` |
|
||||
| End | `"end"` |
|
||||
| Page | `"pageup"` |
|
||||
| Page | `"pagedown"` |
|
||||
| Tab | `"tab"` |
|
||||
| Back | `"backtab"` |
|
||||
| Delete | `"del"` |
|
||||
| Insert | `"ins"` |
|
||||
| Null | `"null"` |
|
||||
| Escape | `"esc"` |
|
||||
|
||||
Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
|
101
book/src/themes.md
Normal file
101
book/src/themes.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Themes
|
||||
|
||||
First you'll need to place selected themes in your `themes` directory (i.e `~/.config/helix/themes`), the directory might have to be created beforehand.
|
||||
|
||||
To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
|
||||
|
||||
The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
|
||||
|
||||
## Creating a theme
|
||||
|
||||
First create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`).
|
||||
|
||||
Each line in the theme file is specified as below:
|
||||
|
||||
```toml
|
||||
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
|
||||
```
|
||||
|
||||
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
|
||||
|
||||
To specify only the foreground color:
|
||||
|
||||
```toml
|
||||
key = "#ffffff"
|
||||
```
|
||||
|
||||
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
|
||||
|
||||
```toml
|
||||
"key.key" = "#ffffff"
|
||||
```
|
||||
|
||||
Possible modifiers:
|
||||
|
||||
| Modifier |
|
||||
| --- |
|
||||
| `bold` |
|
||||
| `dim` |
|
||||
| `italic` |
|
||||
| `underlined` |
|
||||
| `slow\_blink` |
|
||||
| `rapid\_blink` |
|
||||
| `reversed` |
|
||||
| `hidden` |
|
||||
| `crossed\_out` |
|
||||
|
||||
Possible keys:
|
||||
|
||||
| Key | Notes |
|
||||
| --- | --- |
|
||||
| `attribute` | |
|
||||
| `keyword` | |
|
||||
| `keyword.directive` | Preprocessor directives (\#if in C) |
|
||||
| `namespace` | |
|
||||
| `punctuation` | |
|
||||
| `punctuation.delimiter` | |
|
||||
| `operator` | |
|
||||
| `special` | |
|
||||
| `property` | |
|
||||
| `variable` | |
|
||||
| `variable.parameter` | |
|
||||
| `type` | |
|
||||
| `type.builtin` | |
|
||||
| `constructor` | |
|
||||
| `function` | |
|
||||
| `function.macro` | |
|
||||
| `function.builtin` | |
|
||||
| `comment` | |
|
||||
| `variable.builtin` | |
|
||||
| `constant` | |
|
||||
| `constant.builtin` | |
|
||||
| `string` | |
|
||||
| `number` | |
|
||||
| `escape` | Escaped characters |
|
||||
| `label` | For lifetimes |
|
||||
| `module` | |
|
||||
| `ui.background` | |
|
||||
| `ui.cursor` | |
|
||||
| `ui.cursor.insert` | |
|
||||
| `ui.cursor.select` | |
|
||||
| `ui.cursor.match` | Matching bracket etc. |
|
||||
| `ui.cursor.primary` | Cursor with primary selection |
|
||||
| `ui.linenr` | |
|
||||
| `ui.statusline` | |
|
||||
| `ui.statusline.inactive` | |
|
||||
| `ui.popup` | |
|
||||
| `ui.window` | |
|
||||
| `ui.help` | |
|
||||
| `ui.text` | |
|
||||
| `ui.text.focus` | |
|
||||
| `ui.menu.selected` | |
|
||||
| `ui.selection` | For selections in the editing area |
|
||||
| `ui.selection.primary` | |
|
||||
| `warning` | LSP warning |
|
||||
| `error` | LSP error |
|
||||
| `info` | LSP info |
|
||||
| `hint` | LSP hint |
|
||||
|
||||
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
|
||||
|
||||
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.
|
@@ -1 +1,26 @@
|
||||
# Usage
|
||||
|
||||
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
|
||||
|
||||
## Surround
|
||||
|
||||
Functionality similar to [vim-surround](https://github.com/tpope/vim-surround) is built into
|
||||
helix. The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich):
|
||||
|
||||

|
||||
|
||||
- `ms` - Add surround characters
|
||||
- `mr` - Replace surround characters
|
||||
- `md` - Delete surround characters
|
||||
|
||||
`ms` acts on a selection, so select the text first and use `ms<char>`. `mr` and `md` work
|
||||
on the closest pairs found and selections are not required; use counts to act in outer pairs.
|
||||
|
||||
It can also act on multiple seletions (yay!). For example, to change every occurance of `(use)` to `[use]`:
|
||||
|
||||
- `%` to select the whole file
|
||||
- `s` to split the selections on a search term
|
||||
- Input `use` and hit Enter
|
||||
- `mr([` to replace the parens with square brackets
|
||||
|
||||
Multiple characters are currently not supported, but planned.
|
||||
|
1
contrib/themes
Symbolic link
1
contrib/themes
Symbolic link
@@ -0,0 +1 @@
|
||||
../runtime/themes
|
37
flake.lock
generated
37
flake.lock
generated
@@ -31,24 +31,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"devshell": "devshell",
|
||||
@@ -58,11 +40,11 @@
|
||||
"rustOverlay": "rustOverlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1623560601,
|
||||
"narHash": "sha256-H1Dq461b2m8v/FxmPphd8pOAx4pPja0UE/xvcMUYwwY=",
|
||||
"lastModified": 1624244973,
|
||||
"narHash": "sha256-h+b4CwPjyibgwMYAeBaT5qBnxI0fsmGf66k23FqEH5Y=",
|
||||
"owner": "yusdacra",
|
||||
"repo": "nix-cargo-integration",
|
||||
"rev": "1238fd751e5d6eb030aee244da9fee6c3ad8b316",
|
||||
"rev": "00f5df6d8e7eeeac2764b7fa2c57e2e81f5d47cd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -73,11 +55,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1623324058,
|
||||
"narHash": "sha256-Jm9GUTXdjXz56gWDKy++EpFfjrBaxqXlLvTLfgEi8lo=",
|
||||
"lastModified": 1624024598,
|
||||
"narHash": "sha256-X++38oH5MKEmPW4/2WdMaHQvwJzO8pJfbnzMD7DbG1E=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "432fc2d9a67f92e05438dff5fdc2b39d33f77997",
|
||||
"rev": "33d42ad7cf2769ce6364ed4e52afa8e9d1439d58",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -90,7 +72,6 @@
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flakeCompat": "flakeCompat",
|
||||
"helix": "helix",
|
||||
"nixCargoIntegration": "nixCargoIntegration",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
@@ -98,11 +79,11 @@
|
||||
"rustOverlay": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1623550815,
|
||||
"narHash": "sha256-RumRrkE6OTJDndHV4qZNZv8kUGnzwRHZQSyzx29r6/g=",
|
||||
"lastModified": 1624242197,
|
||||
"narHash": "sha256-J0+j4DYFaE0O0marb4QN/S1bUhpGwAjQ4O04kIYKcb8=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "9824f142cbd7bc3e2a92eefbb79addfff8704cd3",
|
||||
"rev": "df5d330f34b64194d64dcbafb91e82e01a89a229",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
37
flake.nix
37
flake.nix
@@ -11,15 +11,9 @@
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
helix = {
|
||||
url = "https://github.com/helix-editor/helix.git";
|
||||
type = "git";
|
||||
flake = false;
|
||||
submodules = true;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ nixCargoIntegration, helix, ... }:
|
||||
outputs = inputs@{ self, nixCargoIntegration, ... }:
|
||||
nixCargoIntegration.lib.makeOutputs {
|
||||
root = ./.;
|
||||
buildPlatform = "crate2nix";
|
||||
@@ -29,18 +23,31 @@
|
||||
defaultOutputs = { app = "hx"; package = "helix"; };
|
||||
overrides = {
|
||||
crateOverrides = common: _: {
|
||||
helix-term = prev: { buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ]; };
|
||||
helix-term = prev: {
|
||||
# link languages and theme toml files since helix-term expects them (for tests)
|
||||
preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
|
||||
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
|
||||
'';
|
||||
};
|
||||
helix-syntax = prev:
|
||||
let
|
||||
helix = common.pkgs.fetchgit {
|
||||
url = "https://github.com/helix-editor/helix.git";
|
||||
rev = "9fd17d4ff5b81211317da1a28d2b30442a512ffc";
|
||||
fetchSubmodules = true;
|
||||
sha256 = "sha256-y652sn/tCc1XoKr3YxDZv6bS2Cmr6+9K/wzzNAMFZJw=";
|
||||
};
|
||||
in
|
||||
{
|
||||
src = common.pkgs.runCommand prev.src.name { } ''
|
||||
mkdir -p $out
|
||||
ln -s ${prev.src}/* $out
|
||||
ln -sf ${helix}/helix-syntax/languages $out
|
||||
'';
|
||||
};
|
||||
};
|
||||
shell = common: prev: {
|
||||
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb ]);
|
||||
|
@@ -1,28 +1,31 @@
|
||||
[package]
|
||||
name = "helix-core"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
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
|
||||
|
||||
description = "Helix editor core editing primitives"
|
||||
categories = ["editor"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
homepage = "https://helix-editor.com"
|
||||
include = ["src/**/*", "README.md"]
|
||||
|
||||
[features]
|
||||
embed_runtime = ["rust-embed"]
|
||||
|
||||
[dependencies]
|
||||
helix-syntax = { path = "../helix-syntax" }
|
||||
helix-syntax = { version = "0.3", path = "../helix-syntax" }
|
||||
|
||||
ropey = "1.2"
|
||||
ropey = "1.3"
|
||||
smallvec = "1.4"
|
||||
tendril = "0.4.2"
|
||||
unicode-segmentation = "1.7.1"
|
||||
unicode-segmentation = "1.7"
|
||||
unicode-width = "0.1"
|
||||
unicode-general-category = "0.4.0"
|
||||
unicode-general-category = "0.4"
|
||||
# slab = "0.4.2"
|
||||
tree-sitter = "0.19"
|
||||
once_cell = "1.4"
|
||||
once_cell = "1.8"
|
||||
arc-swap = "1"
|
||||
regex = "1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@@ -12,7 +12,7 @@ pub const PAIRS: &[(char, char)] = &[
|
||||
('`', '`'),
|
||||
];
|
||||
|
||||
const CLOSE_BEFORE: &str = ")]}'\":;> \n"; // includes space and newline
|
||||
const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
|
||||
|
||||
// insert hook:
|
||||
// Fn(doc, selection, char) => Option<Transaction>
|
||||
|
133
helix-core/src/chars.rs
Normal file
133
helix-core/src/chars.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use crate::LineEnding;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum CharCategory {
|
||||
Whitespace,
|
||||
Eol,
|
||||
Word,
|
||||
Punctuation,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn categorize_char(ch: char) -> CharCategory {
|
||||
if char_is_line_ending(ch) {
|
||||
CharCategory::Eol
|
||||
} else if ch.is_whitespace() {
|
||||
CharCategory::Whitespace
|
||||
} else if char_is_word(ch) {
|
||||
CharCategory::Word
|
||||
} else if char_is_punctuation(ch) {
|
||||
CharCategory::Punctuation
|
||||
} else {
|
||||
CharCategory::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether a character is a line ending.
|
||||
#[inline]
|
||||
pub fn char_is_line_ending(ch: char) -> bool {
|
||||
LineEnding::from_char(ch).is_some()
|
||||
}
|
||||
|
||||
/// Determine whether a character qualifies as (non-line-break)
|
||||
/// whitespace.
|
||||
#[inline]
|
||||
pub fn char_is_whitespace(ch: char) -> bool {
|
||||
// TODO: this is a naive binary categorization of whitespace
|
||||
// characters. For display, word wrapping, etc. we'll need a better
|
||||
// categorization based on e.g. breaking vs non-breaking spaces
|
||||
// and whether they're zero-width or not.
|
||||
match ch {
|
||||
//'\u{1680}' | // Ogham Space Mark (here for completeness, but usually displayed as a dash, not as whitespace)
|
||||
'\u{0009}' | // Character Tabulation
|
||||
'\u{0020}' | // Space
|
||||
'\u{00A0}' | // No-break Space
|
||||
'\u{180E}' | // Mongolian Vowel Separator
|
||||
'\u{202F}' | // Narrow No-break Space
|
||||
'\u{205F}' | // Medium Mathematical Space
|
||||
'\u{3000}' | // Ideographic Space
|
||||
'\u{FEFF}' // Zero Width No-break Space
|
||||
=> true,
|
||||
|
||||
// En Quad, Em Quad, En Space, Em Space, Three-per-em Space,
|
||||
// Four-per-em Space, Six-per-em Space, Figure Space,
|
||||
// Punctuation Space, Thin Space, Hair Space, Zero Width Space.
|
||||
ch if ('\u{2000}' ..= '\u{200B}').contains(&ch) => true,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn char_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
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn char_is_word(ch: char) -> bool {
|
||||
ch.is_alphanumeric() || ch == '_'
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_categorize() {
|
||||
const EOL_TEST_CASE: &'static str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
|
||||
const WORD_TEST_CASE: &'static str =
|
||||
"_hello_world_あいうえおー12345678901234567890";
|
||||
const PUNCTUATION_TEST_CASE: &'static str =
|
||||
"!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
|
||||
const WHITESPACE_TEST_CASE: &'static str = " ";
|
||||
|
||||
for ch in EOL_TEST_CASE.chars() {
|
||||
assert_eq!(CharCategory::Eol, categorize_char(ch));
|
||||
}
|
||||
|
||||
for ch in WHITESPACE_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
CharCategory::Whitespace,
|
||||
categorize_char(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Whitespace`",
|
||||
ch,
|
||||
categorize_char(ch)
|
||||
);
|
||||
}
|
||||
|
||||
for ch in WORD_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
CharCategory::Word,
|
||||
categorize_char(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Word`",
|
||||
ch,
|
||||
categorize_char(ch)
|
||||
);
|
||||
}
|
||||
|
||||
for ch in PUNCTUATION_TEST_CASE.chars() {
|
||||
assert_eq!(
|
||||
CharCategory::Punctuation,
|
||||
categorize_char(ch),
|
||||
"Testing '{}', but got `{:?}` instead of `Category::Punctuation`",
|
||||
ch,
|
||||
categorize_char(ch)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -51,7 +51,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection) -> Transaction {
|
||||
let lines = start..end + 1;
|
||||
let (commented, skipped, min) = find_line_comment(token, text, lines.clone());
|
||||
|
||||
changes.reserve(end - start - skipped.len());
|
||||
changes.reserve((end - start).saturating_sub(skipped.len()));
|
||||
|
||||
for line in lines {
|
||||
if skipped.contains(&line) {
|
||||
|
@@ -121,6 +121,16 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
|
||||
nth_next_grapheme_boundary(slice, char_idx, 1)
|
||||
}
|
||||
|
||||
/// Returns the passed char index if it's already a grapheme boundary,
|
||||
/// or the next grapheme boundary char index if not.
|
||||
pub fn ensure_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
|
||||
if char_idx == 0 {
|
||||
0
|
||||
} else {
|
||||
next_grapheme_boundary(slice, char_idx - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the given char position is a grapheme boundary.
|
||||
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
|
||||
// Bounds check
|
||||
@@ -207,6 +217,10 @@ impl<'a> Iterator for RopeGraphemes<'a> {
|
||||
self.cur_chunk_start += self.cur_chunk.len();
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
@@ -254,26 +254,23 @@ where
|
||||
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
let loader = Loader::new(
|
||||
Configuration {
|
||||
language: vec![LanguageConfiguration {
|
||||
scope: "source.rust".to_string(),
|
||||
file_types: vec!["rs".to_string()],
|
||||
language_id: Lang::Rust,
|
||||
highlight_config: OnceCell::new(),
|
||||
//
|
||||
roots: vec![],
|
||||
auto_format: false,
|
||||
language_server: None,
|
||||
indent: Some(IndentationConfiguration {
|
||||
tab_width: 4,
|
||||
unit: String::from(" "),
|
||||
}),
|
||||
indent_query: OnceCell::new(),
|
||||
}],
|
||||
},
|
||||
Vec::new(),
|
||||
);
|
||||
let loader = Loader::new(Configuration {
|
||||
language: vec![LanguageConfiguration {
|
||||
scope: "source.rust".to_string(),
|
||||
file_types: vec!["rs".to_string()],
|
||||
language_id: Lang::Rust,
|
||||
highlight_config: OnceCell::new(),
|
||||
//
|
||||
roots: vec![],
|
||||
auto_format: false,
|
||||
language_server: None,
|
||||
indent: Some(IndentationConfiguration {
|
||||
tab_width: 4,
|
||||
unit: String::from(" "),
|
||||
}),
|
||||
indent_query: OnceCell::new(),
|
||||
}],
|
||||
});
|
||||
|
||||
// set runtime path so we can find the queries
|
||||
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
@@ -1,10 +1,12 @@
|
||||
#![allow(unused)]
|
||||
pub mod auto_pairs;
|
||||
pub mod chars;
|
||||
pub mod comment;
|
||||
pub mod diagnostic;
|
||||
pub mod graphemes;
|
||||
pub mod history;
|
||||
pub mod indent;
|
||||
pub mod line_ending;
|
||||
pub mod macros;
|
||||
pub mod match_brackets;
|
||||
pub mod movement;
|
||||
@@ -14,9 +16,16 @@ pub mod register;
|
||||
pub mod search;
|
||||
pub mod selection;
|
||||
mod state;
|
||||
pub mod surround;
|
||||
pub mod syntax;
|
||||
mod transaction;
|
||||
|
||||
pub mod unicode {
|
||||
pub use unicode_general_category as category;
|
||||
pub use unicode_segmentation as segmentation;
|
||||
pub use unicode_width as width;
|
||||
}
|
||||
|
||||
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
|
||||
once_cell::sync::Lazy::new(runtime_dir);
|
||||
|
||||
@@ -49,7 +58,7 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
|
||||
}
|
||||
|
||||
#[cfg(not(embed_runtime))]
|
||||
fn runtime_dir() -> std::path::PathBuf {
|
||||
pub fn runtime_dir() -> std::path::PathBuf {
|
||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||
return dir.into();
|
||||
}
|
||||
@@ -88,15 +97,18 @@ pub fn cache_dir() -> std::path::PathBuf {
|
||||
path
|
||||
}
|
||||
|
||||
pub use etcetera::home_dir;
|
||||
|
||||
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
||||
|
||||
pub use ropey::{Rope, RopeSlice};
|
||||
pub use ropey::{Rope, RopeBuilder, RopeSlice};
|
||||
|
||||
pub use tendril::StrTendril as Tendril;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use {regex, tree_sitter};
|
||||
|
||||
pub use graphemes::RopeGraphemes;
|
||||
pub use position::{coords_at_pos, pos_at_coords, Position};
|
||||
pub use selection::{Range, Selection};
|
||||
pub use smallvec::SmallVec;
|
||||
@@ -105,4 +117,5 @@ pub use syntax::Syntax;
|
||||
pub use diagnostic::Diagnostic;
|
||||
pub use state::State;
|
||||
|
||||
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING};
|
||||
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};
|
||||
|
258
helix-core/src/line_ending.rs
Normal file
258
helix-core/src/line_ending.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use crate::{Rope, RopeGraphemes, RopeSlice};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::Crlf;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::LF;
|
||||
|
||||
/// Represents one of the valid Unicode line endings.
|
||||
#[derive(PartialEq, Copy, Clone, Debug)]
|
||||
pub enum LineEnding {
|
||||
Crlf, // CarriageReturn followed by LineFeed
|
||||
LF, // U+000A -- LineFeed
|
||||
VT, // U+000B -- VerticalTab
|
||||
FF, // U+000C -- FormFeed
|
||||
CR, // U+000D -- CarriageReturn
|
||||
Nel, // U+0085 -- NextLine
|
||||
LS, // U+2028 -- Line Separator
|
||||
PS, // U+2029 -- ParagraphSeparator
|
||||
}
|
||||
|
||||
impl LineEnding {
|
||||
#[inline]
|
||||
pub fn len_chars(&self) -> usize {
|
||||
match self {
|
||||
Self::Crlf => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Crlf => "\u{000D}\u{000A}",
|
||||
Self::LF => "\u{000A}",
|
||||
Self::VT => "\u{000B}",
|
||||
Self::FF => "\u{000C}",
|
||||
Self::CR => "\u{000D}",
|
||||
Self::Nel => "\u{0085}",
|
||||
Self::LS => "\u{2028}",
|
||||
Self::PS => "\u{2029}",
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_char(ch: char) -> Option<LineEnding> {
|
||||
match ch {
|
||||
'\u{000A}' => Some(LineEnding::LF),
|
||||
'\u{000B}' => Some(LineEnding::VT),
|
||||
'\u{000C}' => Some(LineEnding::FF),
|
||||
'\u{000D}' => Some(LineEnding::CR),
|
||||
'\u{0085}' => Some(LineEnding::Nel),
|
||||
'\u{2028}' => Some(LineEnding::LS),
|
||||
'\u{2029}' => Some(LineEnding::PS),
|
||||
// Not a line ending
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Normally we'd want to implement the FromStr trait, but in this case
|
||||
// that would force us into a different return type than from_char or
|
||||
// or from_rope_slice, which would be weird.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
#[inline]
|
||||
pub fn from_str(g: &str) -> Option<LineEnding> {
|
||||
match g {
|
||||
"\u{000D}\u{000A}" => Some(LineEnding::Crlf),
|
||||
"\u{000A}" => Some(LineEnding::LF),
|
||||
"\u{000B}" => Some(LineEnding::VT),
|
||||
"\u{000C}" => Some(LineEnding::FF),
|
||||
"\u{000D}" => Some(LineEnding::CR),
|
||||
"\u{0085}" => Some(LineEnding::Nel),
|
||||
"\u{2028}" => Some(LineEnding::LS),
|
||||
"\u{2029}" => Some(LineEnding::PS),
|
||||
// Not a line ending
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_rope_slice(g: &RopeSlice) -> Option<LineEnding> {
|
||||
if let Some(text) = g.as_str() {
|
||||
LineEnding::from_str(text)
|
||||
} else {
|
||||
// Non-contiguous, so it can't be a line ending.
|
||||
// Specifically, Ropey guarantees that CRLF is always
|
||||
// contiguous. And the remaining line endings are all
|
||||
// single `char`s, and therefore trivially contiguous.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn str_is_line_ending(s: &str) -> bool {
|
||||
LineEnding::from_str(s).is_some()
|
||||
}
|
||||
|
||||
/// Attempts to detect what line ending the passed document uses.
|
||||
pub fn auto_detect_line_ending(doc: &Rope) -> Option<LineEnding> {
|
||||
// Return first matched line ending. Not all possible line endings
|
||||
// are being matched, as they might be special-use only
|
||||
for line in doc.lines().take(100) {
|
||||
match get_line_ending(&line) {
|
||||
None | Some(LineEnding::VT) | Some(LineEnding::FF) | Some(LineEnding::PS) => {}
|
||||
ending => return ending,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the passed line's line ending, if any.
|
||||
pub fn get_line_ending(line: &RopeSlice) -> Option<LineEnding> {
|
||||
// Last character as str.
|
||||
let g1 = line
|
||||
.slice(line.len_chars().saturating_sub(1)..)
|
||||
.as_str()
|
||||
.unwrap();
|
||||
|
||||
// Last two characters as str, or empty str if they're not contiguous.
|
||||
// It's fine to punt on the non-contiguous case, because Ropey guarantees
|
||||
// that CRLF is always contiguous.
|
||||
let g2 = line
|
||||
.slice(line.len_chars().saturating_sub(2)..)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
|
||||
// First check the two-character case for CRLF, then check the single-character case.
|
||||
LineEnding::from_str(g2).or_else(|| LineEnding::from_str(g1))
|
||||
}
|
||||
|
||||
/// Returns the passed line's line ending, if any.
|
||||
pub fn get_line_ending_of_str(line: &str) -> Option<LineEnding> {
|
||||
if line.ends_with("\u{000D}\u{000A}") {
|
||||
Some(LineEnding::Crlf)
|
||||
} else if line.ends_with('\u{000A}') {
|
||||
Some(LineEnding::LF)
|
||||
} else if line.ends_with('\u{000B}') {
|
||||
Some(LineEnding::VT)
|
||||
} else if line.ends_with('\u{000C}') {
|
||||
Some(LineEnding::FF)
|
||||
} else if line.ends_with('\u{000D}') {
|
||||
Some(LineEnding::CR)
|
||||
} else if line.ends_with('\u{0085}') {
|
||||
Some(LineEnding::Nel)
|
||||
} else if line.ends_with('\u{2028}') {
|
||||
Some(LineEnding::LS)
|
||||
} else if line.ends_with('\u{2029}') {
|
||||
Some(LineEnding::PS)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the char index of the end of the given line, not including its line ending.
|
||||
pub fn line_end_char_index(slice: &RopeSlice, line: usize) -> usize {
|
||||
slice.line_to_char(line + 1)
|
||||
- get_line_ending(&slice.line(line))
|
||||
.map(|le| le.len_chars())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the char index of the end of the given RopeSlice, not including
|
||||
/// any final line ending.
|
||||
pub fn rope_end_without_line_ending(slice: &RopeSlice) -> usize {
|
||||
slice.len_chars() - get_line_ending(slice).map(|le| le.len_chars()).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod line_ending_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn line_ending_autodetect() {
|
||||
assert_eq!(
|
||||
auto_detect_line_ending(&Rope::from_str("\n")),
|
||||
Some(LineEnding::LF)
|
||||
);
|
||||
assert_eq!(
|
||||
auto_detect_line_ending(&Rope::from_str("\r\n")),
|
||||
Some(LineEnding::Crlf)
|
||||
);
|
||||
assert_eq!(auto_detect_line_ending(&Rope::from_str("hello")), None);
|
||||
assert_eq!(auto_detect_line_ending(&Rope::from_str("")), None);
|
||||
assert_eq!(
|
||||
auto_detect_line_ending(&Rope::from_str("hello\nhelix\r\n")),
|
||||
Some(LineEnding::LF)
|
||||
);
|
||||
assert_eq!(
|
||||
auto_detect_line_ending(&Rope::from_str("a formfeed\u{000C}")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
auto_detect_line_ending(&Rope::from_str("\n\u{000A}\n \u{000A}")),
|
||||
Some(LineEnding::LF)
|
||||
);
|
||||
assert_eq!(
|
||||
auto_detect_line_ending(&Rope::from_str(
|
||||
"a formfeed\u{000C} with a\u{000C} linefeed\u{000A}"
|
||||
)),
|
||||
Some(LineEnding::LF)
|
||||
);
|
||||
assert_eq!(auto_detect_line_ending(&Rope::from_str("a formfeed\u{000C} with a\u{000C} carriage return linefeed\u{000D}\u{000A} and a linefeed\u{000A}")), Some(LineEnding::Crlf));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_to_line_ending() {
|
||||
assert_eq!(LineEnding::from_str("\r"), Some(LineEnding::CR));
|
||||
assert_eq!(LineEnding::from_str("\n"), Some(LineEnding::LF));
|
||||
assert_eq!(LineEnding::from_str("\r\n"), Some(LineEnding::Crlf));
|
||||
assert_eq!(LineEnding::from_str("hello\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_slice_to_line_ending() {
|
||||
let r = Rope::from_str("hello\r\n");
|
||||
assert_eq!(
|
||||
LineEnding::from_rope_slice(&r.slice(5..6)),
|
||||
Some(LineEnding::CR)
|
||||
);
|
||||
assert_eq!(
|
||||
LineEnding::from_rope_slice(&r.slice(6..7)),
|
||||
Some(LineEnding::LF)
|
||||
);
|
||||
assert_eq!(
|
||||
LineEnding::from_rope_slice(&r.slice(5..7)),
|
||||
Some(LineEnding::Crlf)
|
||||
);
|
||||
assert_eq!(LineEnding::from_rope_slice(&r.slice(..)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_line_ending_rope_slice() {
|
||||
let r = Rope::from_str("Hello\rworld\nhow\r\nare you?");
|
||||
assert_eq!(get_line_ending(&r.slice(..6)), Some(LineEnding::CR));
|
||||
assert_eq!(get_line_ending(&r.slice(..12)), Some(LineEnding::LF));
|
||||
assert_eq!(get_line_ending(&r.slice(..17)), Some(LineEnding::Crlf));
|
||||
assert_eq!(get_line_ending(&r.slice(..)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_line_ending_str() {
|
||||
let text = "Hello\rworld\nhow\r\nare you?";
|
||||
assert_eq!(get_line_ending_of_str(&text[..6]), Some(LineEnding::CR));
|
||||
assert_eq!(get_line_ending_of_str(&text[..12]), Some(LineEnding::LF));
|
||||
assert_eq!(get_line_ending_of_str(&text[..17]), Some(LineEnding::Crlf));
|
||||
assert_eq!(get_line_ending_of_str(&text[..]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_end_char_index_rope_slice() {
|
||||
let r = Rope::from_str("Hello\rworld\nhow\r\nare you?");
|
||||
let s = &r.slice(..);
|
||||
assert_eq!(line_end_char_index(s, 0), 5);
|
||||
assert_eq!(line_end_char_index(s, 1), 11);
|
||||
assert_eq!(line_end_char_index(s, 2), 15);
|
||||
assert_eq!(line_end_char_index(s, 3), 25);
|
||||
}
|
||||
}
|
@@ -3,8 +3,13 @@ use std::iter::{self, from_fn, Peekable, SkipWhile};
|
||||
use ropey::iter::Chars;
|
||||
|
||||
use crate::{
|
||||
chars::{
|
||||
categorize_char, char_is_line_ending, char_is_punctuation, char_is_whitespace,
|
||||
char_is_word, CharCategory,
|
||||
},
|
||||
coords_at_pos,
|
||||
graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
|
||||
line_ending::{get_line_ending, line_end_char_index},
|
||||
pos_at_coords, Position, Range, RopeSlice,
|
||||
};
|
||||
|
||||
@@ -37,9 +42,8 @@ pub fn move_horizontally(
|
||||
nth_prev_grapheme_boundary(slice, pos, count).max(start)
|
||||
}
|
||||
Direction::Forward => {
|
||||
// Line end is pos at the start of next line - 1
|
||||
let end = slice.line_to_char(line + 1).saturating_sub(1);
|
||||
nth_next_grapheme_boundary(slice, pos, count).min(end)
|
||||
let end_char_idx = line_end_char_index(&slice, line);
|
||||
nth_next_grapheme_boundary(slice, pos, count).min(end_char_idx)
|
||||
}
|
||||
};
|
||||
let anchor = match behaviour {
|
||||
@@ -68,8 +72,11 @@ pub fn move_vertically(
|
||||
),
|
||||
};
|
||||
|
||||
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
|
||||
let new_line_len = slice.line(new_line).len_chars().saturating_sub(2);
|
||||
// Length of the line sans line-ending.
|
||||
let new_line_len = {
|
||||
let line = slice.line(new_line);
|
||||
line.len_chars() - get_line_ending(&line).map(|le| le.len_chars()).unwrap_or(0)
|
||||
};
|
||||
|
||||
let new_col = std::cmp::min(horiz as usize, new_line_len);
|
||||
|
||||
@@ -104,64 +111,6 @@ fn word_move(slice: RopeSlice, mut range: Range, count: usize, target: WordMotio
|
||||
}
|
||||
|
||||
// ---- util ------------
|
||||
#[inline]
|
||||
pub(crate) fn is_word(ch: char) -> bool {
|
||||
ch.is_alphanumeric() || ch == '_'
|
||||
}
|
||||
|
||||
#[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 enum Category {
|
||||
Whitespace,
|
||||
Eol,
|
||||
Word,
|
||||
Punctuation,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn categorize(ch: char) -> Category {
|
||||
if is_end_of_line(ch) {
|
||||
Category::Eol
|
||||
} else if ch.is_whitespace() {
|
||||
Category::Whitespace
|
||||
} else if is_word(ch) {
|
||||
Category::Word
|
||||
} else if is_punctuation(ch) {
|
||||
Category::Punctuation
|
||||
} else {
|
||||
Category::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns first index that doesn't satisfy a given predicate when
|
||||
@@ -235,7 +184,8 @@ impl CharHelpers for Chars<'_> {
|
||||
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));
|
||||
let is_boundary =
|
||||
|a: char, b: Option<char>| categorize_char(a) != categorize_char(b.unwrap_or(a));
|
||||
while let Some(peek) = characters.peek().copied() {
|
||||
phase = match phase {
|
||||
WordMotionPhase::Start => {
|
||||
@@ -244,7 +194,8 @@ impl CharHelpers for Chars<'_> {
|
||||
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) {
|
||||
if !is_boundary(peek, characters.peek().copied()) && !char_is_line_ending(peek)
|
||||
{
|
||||
anchor = Some(head);
|
||||
}
|
||||
// First character is always skipped by the head
|
||||
@@ -252,7 +203,7 @@ impl CharHelpers for Chars<'_> {
|
||||
WordMotionPhase::SkipNewlines
|
||||
}
|
||||
WordMotionPhase::SkipNewlines => {
|
||||
if is_end_of_line(peek) {
|
||||
if char_is_line_ending(peek) {
|
||||
characters.next();
|
||||
if characters.peek().is_some() {
|
||||
advance(&mut head);
|
||||
@@ -286,12 +237,12 @@ fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>
|
||||
|
||||
match target {
|
||||
WordMotionTarget::NextWordStart => {
|
||||
((categorize(peek) != categorize(*next_peek))
|
||||
&& (is_end_of_line(*next_peek) || !next_peek.is_whitespace()))
|
||||
((categorize_char(peek) != categorize_char(*next_peek))
|
||||
&& (char_is_line_ending(*next_peek) || !next_peek.is_whitespace()))
|
||||
}
|
||||
WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
|
||||
((categorize(peek) != categorize(*next_peek))
|
||||
&& (!peek.is_whitespace() || is_end_of_line(*next_peek)))
|
||||
((categorize_char(peek) != categorize_char(*next_peek))
|
||||
&& (!peek.is_whitespace() || char_is_line_ending(*next_peek)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,7 +281,7 @@ mod test {
|
||||
slice,
|
||||
move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
|
||||
),
|
||||
(1, 2).into()
|
||||
(1, 3).into()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -343,12 +294,12 @@ mod test {
|
||||
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)),
|
||||
((Direction::Forward, 1usize), (0, 1)), // T|his is a simple alphabetic line
|
||||
((Direction::Forward, 2usize), (0, 3)), // Thi|s is a simple alphabetic line
|
||||
((Direction::Forward, 0usize), (0, 3)), // Thi|s is a simple alphabetic line
|
||||
((Direction::Forward, 999usize), (0, 32)), // This is a simple alphabetic line|
|
||||
((Direction::Forward, 999usize), (0, 32)), // This is a simple alphabetic line|
|
||||
((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line
|
||||
];
|
||||
|
||||
for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
|
||||
@@ -366,15 +317,15 @@ mod test {
|
||||
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_
|
||||
((Direction::Forward, 1usize), (0, 1)), // M|ultiline\n
|
||||
((Direction::Forward, 2usize), (0, 3)), // Mul|tiline\n
|
||||
((Direction::Backward, 6usize), (0, 0)), // |Multiline\n
|
||||
((Direction::Backward, 999usize), (0, 0)), // |Multiline\n
|
||||
((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\n
|
||||
((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\n
|
||||
((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\n
|
||||
((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
|
||||
((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
|
||||
]);
|
||||
|
||||
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
|
||||
@@ -446,7 +397,7 @@ mod test {
|
||||
// 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)),
|
||||
((Axis::V, Direction::Forward, 1usize), (2, 5)),
|
||||
// Third descent restores the original column
|
||||
((Axis::V, Direction::Forward, 1usize), (3, 8)),
|
||||
// Behaviour is preserved even through long jumps
|
||||
@@ -760,45 +711,4 @@ mod test {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
chars::char_is_line_ending,
|
||||
graphemes::{nth_next_grapheme_boundary, RopeGraphemes},
|
||||
Rope, RopeSlice,
|
||||
};
|
||||
@@ -23,8 +24,9 @@ impl Position {
|
||||
pub fn traverse(self, text: &crate::Tendril) -> Self {
|
||||
let Self { mut row, mut col } = self;
|
||||
// TODO: there should be a better way here
|
||||
for ch in text.chars() {
|
||||
if ch == '\n' {
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
|
||||
row += 1;
|
||||
col = 0;
|
||||
} else {
|
||||
|
@@ -1,20 +1,63 @@
|
||||
use crate::Tendril;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, sync::RwLock};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: could be an instance on Editor
|
||||
static REGISTRY: Lazy<RwLock<HashMap<char, Vec<String>>>> =
|
||||
Lazy::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
/// Read register values.
|
||||
pub fn get(register_name: char) -> Option<Vec<String>> {
|
||||
let registry = REGISTRY.read().unwrap();
|
||||
registry.get(®ister_name).cloned() // TODO: no cloning
|
||||
#[derive(Debug)]
|
||||
pub struct Register {
|
||||
name: char,
|
||||
values: Vec<String>,
|
||||
}
|
||||
|
||||
/// Read register values.
|
||||
// restoring: bool
|
||||
pub fn set(register_name: char, values: Vec<String>) {
|
||||
let mut registry = REGISTRY.write().unwrap();
|
||||
registry.insert(register_name, values);
|
||||
impl Register {
|
||||
pub fn new(name: char) -> Self {
|
||||
Self {
|
||||
name,
|
||||
values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
|
||||
Self { name, values }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> char {
|
||||
self.name
|
||||
}
|
||||
|
||||
pub fn read(&self) -> &Vec<String> {
|
||||
&self.values
|
||||
}
|
||||
|
||||
pub fn write(&mut self, values: Vec<String>) {
|
||||
self.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
/// Currently just wraps a `HashMap` of `Register`s
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Registers {
|
||||
inner: HashMap<char, Register>,
|
||||
}
|
||||
|
||||
impl Registers {
|
||||
pub fn get(&self, name: char) -> Option<&Register> {
|
||||
self.inner.get(&name)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, name: char) -> Option<&mut Register> {
|
||||
self.inner.get_mut(&name)
|
||||
}
|
||||
|
||||
pub fn get_or_insert(&mut self, name: char) -> &mut Register {
|
||||
self.inner
|
||||
.entry(name)
|
||||
.or_insert_with(|| Register::new(name))
|
||||
}
|
||||
|
||||
pub fn write(&mut self, name: char, values: Vec<String>) {
|
||||
self.inner
|
||||
.insert(name, Register::new_with_values(name, values));
|
||||
}
|
||||
|
||||
pub fn read(&self, name: char) -> Option<&Vec<String>> {
|
||||
self.get(name).map(|reg| reg.read())
|
||||
}
|
||||
}
|
||||
|
@@ -352,7 +352,7 @@ pub fn select_on_matches(
|
||||
|
||||
let start = text.byte_to_char(start_byte + mat.start());
|
||||
let end = text.byte_to_char(start_byte + mat.end());
|
||||
result.push(Range::new(start, end - 1));
|
||||
result.push(Range::new(start, end.saturating_sub(1)));
|
||||
}
|
||||
}
|
||||
|
||||
|
266
helix-core/src/surround.rs
Normal file
266
helix-core/src/surround.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use crate::{search, Selection};
|
||||
use ropey::RopeSlice;
|
||||
|
||||
pub const PAIRS: &[(char, char)] = &[
|
||||
('(', ')'),
|
||||
('[', ']'),
|
||||
('{', '}'),
|
||||
('<', '>'),
|
||||
('«', '»'),
|
||||
('「', '」'),
|
||||
('(', ')'),
|
||||
];
|
||||
|
||||
/// Given any char in [PAIRS], return the open and closing chars. If not found in
|
||||
/// [PAIRS] return (ch, ch).
|
||||
///
|
||||
/// ```
|
||||
/// use helix_core::surround::get_pair;
|
||||
///
|
||||
/// assert_eq!(get_pair('['), ('[', ']'));
|
||||
/// assert_eq!(get_pair('}'), ('{', '}'));
|
||||
/// assert_eq!(get_pair('"'), ('"', '"'));
|
||||
/// ```
|
||||
pub fn get_pair(ch: char) -> (char, char) {
|
||||
PAIRS
|
||||
.iter()
|
||||
.find(|(open, close)| *open == ch || *close == ch)
|
||||
.copied()
|
||||
.unwrap_or((ch, ch))
|
||||
}
|
||||
|
||||
/// Find the position of surround pairs of `ch` which can be either a closing
|
||||
/// or opening pair. `n` will skip n - 1 pairs (eg. n=2 will discard (only)
|
||||
/// the first pair found and keep looking)
|
||||
pub fn find_nth_pairs_pos(
|
||||
text: RopeSlice,
|
||||
ch: char,
|
||||
pos: usize,
|
||||
n: usize,
|
||||
) -> Option<(usize, usize)> {
|
||||
let (open, close) = get_pair(ch);
|
||||
|
||||
let (open_pos, close_pos) = if open == close {
|
||||
// find_nth* do not consider current character; +1/-1 to include them
|
||||
(
|
||||
search::find_nth_prev(text, open, pos + 1, n, true)?,
|
||||
search::find_nth_next(text, close, pos - 1, n, true)?,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
find_nth_open_pair(text, open, close, pos, n)?,
|
||||
find_nth_close_pair(text, open, close, pos, n)?,
|
||||
)
|
||||
};
|
||||
|
||||
Some((open_pos, close_pos))
|
||||
}
|
||||
|
||||
fn find_nth_open_pair(
|
||||
text: RopeSlice,
|
||||
open: char,
|
||||
close: char,
|
||||
mut pos: usize,
|
||||
n: usize,
|
||||
) -> Option<usize> {
|
||||
let mut chars = text.chars_at(pos + 1);
|
||||
|
||||
// Adjusts pos for the first iteration, and handles the case of the
|
||||
// cursor being *on* the close character which will get falsely stepped over
|
||||
// if not skipped here
|
||||
if chars.prev()? == open {
|
||||
return Some(pos);
|
||||
}
|
||||
|
||||
for _ in 0..n {
|
||||
let mut step_over: usize = 0;
|
||||
|
||||
loop {
|
||||
let c = chars.prev()?;
|
||||
pos = pos.saturating_sub(1);
|
||||
|
||||
// ignore other surround pairs that are enclosed *within* our search scope
|
||||
if c == close {
|
||||
step_over += 1;
|
||||
} else if c == open {
|
||||
if step_over == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
step_over = step_over.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(pos)
|
||||
}
|
||||
|
||||
fn find_nth_close_pair(
|
||||
text: RopeSlice,
|
||||
open: char,
|
||||
close: char,
|
||||
mut pos: usize,
|
||||
n: usize,
|
||||
) -> Option<usize> {
|
||||
if pos >= text.len_chars() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut chars = text.chars_at(pos);
|
||||
|
||||
if chars.next()? == close {
|
||||
return Some(pos);
|
||||
}
|
||||
|
||||
for _ in 0..n {
|
||||
let mut step_over: usize = 0;
|
||||
|
||||
loop {
|
||||
let c = chars.next()?;
|
||||
pos += 1;
|
||||
|
||||
if c == open {
|
||||
step_over += 1;
|
||||
} else if c == close {
|
||||
if step_over == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
step_over = step_over.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(pos)
|
||||
}
|
||||
|
||||
/// Find position of surround characters around every cursor. Returns None
|
||||
/// if any positions overlap. Note that the positions are in a flat Vec.
|
||||
/// Use get_surround_pos().chunks(2) to get matching pairs of surround positions.
|
||||
/// `ch` can be either closing or opening pair.
|
||||
pub fn get_surround_pos(
|
||||
text: RopeSlice,
|
||||
selection: &Selection,
|
||||
ch: char,
|
||||
skip: usize,
|
||||
) -> Option<Vec<usize>> {
|
||||
let mut change_pos = Vec::new();
|
||||
|
||||
for range in selection {
|
||||
let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
|
||||
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
|
||||
return None;
|
||||
}
|
||||
change_pos.extend_from_slice(&[open_pos, close_pos]);
|
||||
}
|
||||
Some(change_pos)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Range;
|
||||
|
||||
use ropey::Rope;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[test]
|
||||
fn test_find_nth_pairs_pos() {
|
||||
let doc = Rope::from("some (text) here");
|
||||
let slice = doc.slice(..);
|
||||
|
||||
// cursor on [t]ext
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
|
||||
// cursor on so[m]e
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
|
||||
// cursor on bracket itself
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_nth_pairs_pos_skip() {
|
||||
let doc = Rope::from("(so (many (good) text) here)");
|
||||
let slice = doc.slice(..);
|
||||
|
||||
// cursor on go[o]d
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_nth_pairs_pos_same() {
|
||||
let doc = Rope::from("'so 'many 'good' text' here'");
|
||||
let slice = doc.slice(..);
|
||||
|
||||
// cursor on go[o]d
|
||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_nth_pairs_pos_step() {
|
||||
let doc = Rope::from("((so)((many) good (text))(here))");
|
||||
let slice = doc.slice(..);
|
||||
|
||||
// cursor on go[o]d
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_nth_pairs_pos_mixed() {
|
||||
let doc = Rope::from("(so [many {good} text] here)");
|
||||
let slice = doc.slice(..);
|
||||
|
||||
// cursor on go[o]d
|
||||
assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
|
||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_surround_pos() {
|
||||
let doc = Rope::from("(some) (chars)\n(newline)");
|
||||
let slice = doc.slice(..);
|
||||
let selection = Selection::new(
|
||||
SmallVec::from_slice(&[Range::point(2), Range::point(9), Range::point(20)]),
|
||||
0,
|
||||
);
|
||||
|
||||
// cursor on s[o]me, c[h]ars, newl[i]ne
|
||||
assert_eq!(
|
||||
get_surround_pos(slice, &selection, '(', 1)
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
&[0, 5, 7, 13, 15, 23]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_surround_pos_bail() {
|
||||
let doc = Rope::from("[some]\n(chars)xx\n(newline)");
|
||||
let slice = doc.slice(..);
|
||||
|
||||
let selection =
|
||||
Selection::new(SmallVec::from_slice(&[Range::point(2), Range::point(9)]), 0);
|
||||
|
||||
// cursor on s[o]me, c[h]ars
|
||||
assert_eq!(
|
||||
get_surround_pos(slice, &selection, '(', 1),
|
||||
None // different surround chars
|
||||
);
|
||||
|
||||
let selection = Selection::new(
|
||||
SmallVec::from_slice(&[Range::point(14), Range::point(24)]),
|
||||
0,
|
||||
);
|
||||
// cursor on [x]x, newli[n]e
|
||||
assert_eq!(
|
||||
get_surround_pos(slice, &selection, '(', 1),
|
||||
None // overlapping surround chars
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
use crate::{regex::Regex, Change, Rope, RopeSlice, Transaction};
|
||||
use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
|
||||
pub use helix_syntax::{get_language, get_language_name, Lang};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
@@ -143,44 +145,56 @@ fn read_query(language: &str, filename: &str) -> String {
|
||||
}
|
||||
|
||||
impl LanguageConfiguration {
|
||||
fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
|
||||
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
||||
|
||||
let highlights_query = read_query(&language, "highlights.scm");
|
||||
// always highlight syntax errors
|
||||
// highlights_query += "\n(ERROR) @error";
|
||||
|
||||
let injections_query = read_query(&language, "injections.scm");
|
||||
|
||||
let locals_query = "";
|
||||
|
||||
if highlights_query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let language = get_language(self.language_id);
|
||||
let mut config = HighlightConfiguration::new(
|
||||
language,
|
||||
&highlights_query,
|
||||
&injections_query,
|
||||
locals_query,
|
||||
)
|
||||
.unwrap(); // TODO: no unwrap
|
||||
config.configure(scopes);
|
||||
Some(Arc::new(config))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reconfigure(&self, scopes: &[String]) {
|
||||
if let Some(Some(config)) = self.highlight_config.get() {
|
||||
config.configure(scopes);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
|
||||
self.highlight_config
|
||||
.get_or_init(|| {
|
||||
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
||||
|
||||
let highlights_query = read_query(&language, "highlights.scm");
|
||||
// always highlight syntax errors
|
||||
// highlights_query += "\n(ERROR) @error";
|
||||
|
||||
let injections_query = read_query(&language, "injections.scm");
|
||||
|
||||
let locals_query = "";
|
||||
|
||||
if highlights_query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let language = get_language(self.language_id);
|
||||
let mut config = HighlightConfiguration::new(
|
||||
language,
|
||||
&highlights_query,
|
||||
&injections_query,
|
||||
locals_query,
|
||||
)
|
||||
.unwrap(); // TODO: no unwrap
|
||||
config.configure(scopes);
|
||||
Some(Arc::new(config))
|
||||
}
|
||||
})
|
||||
.get_or_init(|| self.initialize_highlight(scopes))
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn is_highlight_initialized(&self) -> bool {
|
||||
self.highlight_config.get().is_some()
|
||||
}
|
||||
|
||||
pub fn indent_query(&self) -> Option<&IndentQuery> {
|
||||
self.indent_query
|
||||
.get_or_init(|| {
|
||||
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
||||
|
||||
let toml = load_runtime_file(&language, "indents.toml").ok()?;
|
||||
toml::from_slice(&toml.as_bytes()).ok()
|
||||
toml::from_slice(toml.as_bytes()).ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
@@ -190,22 +204,18 @@ impl LanguageConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
pub static LOADER: OnceCell<Loader> = OnceCell::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Loader {
|
||||
// highlight_names ?
|
||||
language_configs: Vec<Arc<LanguageConfiguration>>,
|
||||
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
|
||||
scopes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
|
||||
pub fn new(config: Configuration) -> Self {
|
||||
let mut loader = Self {
|
||||
language_configs: Vec::new(),
|
||||
language_config_ids_by_file_type: HashMap::new(),
|
||||
scopes,
|
||||
};
|
||||
|
||||
for config in config.language {
|
||||
@@ -225,10 +235,6 @@ impl Loader {
|
||||
loader
|
||||
}
|
||||
|
||||
pub fn scopes(&self) -> &[String] {
|
||||
&self.scopes
|
||||
}
|
||||
|
||||
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
|
||||
// Find all the language configurations that match this file name
|
||||
// or a suffix of the file name.
|
||||
@@ -253,6 +259,10 @@ impl Loader {
|
||||
.find(|config| config.scope == scope)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
|
||||
self.language_configs.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TsParser {
|
||||
@@ -579,9 +589,10 @@ impl LanguageLayer {
|
||||
mut column,
|
||||
} = point;
|
||||
|
||||
// TODO: there should be a better way here
|
||||
for ch in text.bytes() {
|
||||
if ch == b'\n' {
|
||||
// TODO: there should be a better way here.
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
|
||||
row += 1;
|
||||
column = 0;
|
||||
} else {
|
||||
@@ -771,7 +782,7 @@ pub struct HighlightConfiguration {
|
||||
combined_injections_query: Option<Query>,
|
||||
locals_pattern_index: usize,
|
||||
highlights_pattern_index: usize,
|
||||
highlight_indices: Vec<Option<Highlight>>,
|
||||
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
|
||||
non_local_variable_patterns: Vec<bool>,
|
||||
injection_content_capture_index: Option<u32>,
|
||||
injection_language_capture_index: Option<u32>,
|
||||
@@ -923,7 +934,7 @@ impl HighlightConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
let highlight_indices = vec![None; query.capture_names().len()];
|
||||
let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
|
||||
Ok(Self {
|
||||
language,
|
||||
query,
|
||||
@@ -956,17 +967,20 @@ impl HighlightConfiguration {
|
||||
///
|
||||
/// When highlighting, results are returned as `Highlight` values, which contain the index
|
||||
/// of the matched highlight this list of highlight names.
|
||||
pub fn configure(&mut self, recognized_names: &[String]) {
|
||||
pub fn configure(&self, recognized_names: &[String]) {
|
||||
let mut capture_parts = Vec::new();
|
||||
self.highlight_indices.clear();
|
||||
self.highlight_indices
|
||||
.extend(self.query.capture_names().iter().map(move |capture_name| {
|
||||
let indices: Vec<_> = self
|
||||
.query
|
||||
.capture_names()
|
||||
.iter()
|
||||
.map(move |capture_name| {
|
||||
capture_parts.clear();
|
||||
capture_parts.extend(capture_name.split('.'));
|
||||
|
||||
let mut best_index = None;
|
||||
let mut best_match_len = 0;
|
||||
for (i, recognized_name) in recognized_names.iter().enumerate() {
|
||||
let recognized_name = recognized_name;
|
||||
let mut len = 0;
|
||||
let mut matches = true;
|
||||
for part in recognized_name.split('.') {
|
||||
@@ -982,7 +996,10 @@ impl HighlightConfiguration {
|
||||
}
|
||||
}
|
||||
best_index.map(Highlight)
|
||||
}));
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.highlight_indices.store(Arc::new(indices));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1561,7 +1578,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let current_highlight = layer.config.highlight_indices[capture.index as usize];
|
||||
let current_highlight = layer.config.highlight_indices.load()[capture.index as usize];
|
||||
|
||||
// If this node represents a local definition, then store the current
|
||||
// highlight value on the local scope entry representing this node.
|
||||
|
@@ -1,14 +1,18 @@
|
||||
[package]
|
||||
name = "helix-lsp"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
description = "LSP client implementation for Helix project"
|
||||
categories = ["editor"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
homepage = "https://helix-editor.com"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-core = { version = "0.3", path = "../helix-core" }
|
||||
|
||||
anyhow = "1.0"
|
||||
futures-executor = "0.3"
|
||||
@@ -19,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.6", features = ["full"] }
|
||||
tokio = { version = "1.7", features = ["full"] }
|
||||
tokio-stream = "0.1.6"
|
@@ -3,7 +3,7 @@ use crate::{
|
||||
Call, Error, OffsetEncoding, Result,
|
||||
};
|
||||
|
||||
use helix_core::{find_root, ChangeSet, Rope};
|
||||
use helix_core::{chars::char_is_line_ending, find_root, ChangeSet, Rope};
|
||||
use jsonrpc_core as jsonrpc;
|
||||
use lsp_types as lsp;
|
||||
use serde_json::Value;
|
||||
@@ -18,6 +18,7 @@ use tokio::{
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
id: usize,
|
||||
_process: Child,
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
request_counter: AtomicU64,
|
||||
@@ -26,7 +27,11 @@ pub struct Client {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn start(cmd: &str, args: &[String]) -> Result<(Self, UnboundedReceiver<Call>)> {
|
||||
pub fn start(
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
id: usize,
|
||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> {
|
||||
let process = Command::new(cmd)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
@@ -43,9 +48,10 @@ impl Client {
|
||||
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 (server_rx, server_tx) = Transport::start(reader, writer, stderr);
|
||||
let (server_rx, server_tx) = Transport::start(reader, writer, stderr, id);
|
||||
|
||||
let client = Self {
|
||||
id,
|
||||
_process: process,
|
||||
server_tx,
|
||||
request_counter: AtomicU64::new(0),
|
||||
@@ -59,6 +65,10 @@ impl Client {
|
||||
Ok((client, server_rx))
|
||||
}
|
||||
|
||||
pub fn id(&self) -> usize {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn next_request_id(&self) -> jsonrpc::Id {
|
||||
let id = self.request_counter.fetch_add(1, Ordering::Relaxed);
|
||||
jsonrpc::Id::Num(id)
|
||||
@@ -165,31 +175,35 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Reply to a language server RPC call.
|
||||
pub async fn reply(
|
||||
pub fn reply(
|
||||
&self,
|
||||
id: jsonrpc::Id,
|
||||
result: core::result::Result<Value, jsonrpc::Error>,
|
||||
) -> Result<()> {
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
use jsonrpc::{Failure, Output, Success, Version};
|
||||
|
||||
let output = match result {
|
||||
Ok(result) => Output::Success(Success {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
result,
|
||||
}),
|
||||
Err(error) => Output::Failure(Failure {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
error,
|
||||
}),
|
||||
};
|
||||
let server_tx = self.server_tx.clone();
|
||||
|
||||
self.server_tx
|
||||
.send(Payload::Response(output))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
async move {
|
||||
let output = match result {
|
||||
Ok(result) => Output::Success(Success {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
result,
|
||||
}),
|
||||
Err(error) => Output::Failure(Failure {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
error,
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
server_tx
|
||||
.send(Payload::Response(output))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
@@ -229,8 +243,7 @@ impl Client {
|
||||
..Default::default()
|
||||
}),
|
||||
window: Some(lsp::WindowClientCapabilities {
|
||||
// TODO: temporarily disabled until we implement handling for window/workDoneProgress/create
|
||||
// work_done_progress: Some(true),
|
||||
work_done_progress: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -259,6 +272,21 @@ impl Client {
|
||||
self.notify::<lsp::notification::Exit>(())
|
||||
}
|
||||
|
||||
/// Tries to shut down the language server but returns
|
||||
/// early if server responds with an error.
|
||||
pub async fn shutdown_and_exit(&self) -> Result<()> {
|
||||
self.shutdown().await?;
|
||||
self.exit().await
|
||||
}
|
||||
|
||||
/// Forcefully shuts down the language server ignoring any errors.
|
||||
pub async fn force_shutdown(&self) -> Result<()> {
|
||||
if let Err(e) = self.shutdown().await {
|
||||
log::warn!("language server failed to terminate gracefully - {}", e);
|
||||
}
|
||||
self.exit().await
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// Text document
|
||||
// -------------------------------------------------------------------------------------------
|
||||
@@ -309,8 +337,9 @@ impl Client {
|
||||
mut character,
|
||||
} = pos;
|
||||
|
||||
for ch in text.chars() {
|
||||
if ch == '\n' {
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
|
||||
line += 1;
|
||||
character = 0;
|
||||
} else {
|
||||
@@ -465,6 +494,7 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
// ) -> Result<Vec<lsp::CompletionItem>> {
|
||||
let params = lsp::CompletionParams {
|
||||
@@ -473,9 +503,7 @@ impl Client {
|
||||
position,
|
||||
},
|
||||
// TODO: support these tokens by async receiving and updating the choice list
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: None,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
partial_result_params: lsp::PartialResultParams {
|
||||
partial_result_token: None,
|
||||
},
|
||||
@@ -490,15 +518,14 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
let params = lsp::SignatureHelpParams {
|
||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||
text_document,
|
||||
position,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: None,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
context: None,
|
||||
// lsp::SignatureHelpContext
|
||||
};
|
||||
@@ -510,15 +537,14 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
let params = lsp::HoverParams {
|
||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||
text_document,
|
||||
position,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: None,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
// lsp::SignatureHelpContext
|
||||
};
|
||||
|
||||
@@ -531,6 +557,7 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
options: lsp::FormattingOptions,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> anyhow::Result<Vec<lsp::TextEdit>> {
|
||||
let capabilities = self.capabilities.as_ref().unwrap();
|
||||
|
||||
@@ -545,9 +572,7 @@ impl Client {
|
||||
let params = lsp::DocumentFormattingParams {
|
||||
text_document,
|
||||
options,
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: None,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
};
|
||||
|
||||
let response = self.request::<lsp::request::Formatting>(params).await?;
|
||||
@@ -560,6 +585,7 @@ impl Client {
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
range: lsp::Range,
|
||||
options: lsp::FormattingOptions,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> anyhow::Result<Vec<lsp::TextEdit>> {
|
||||
let capabilities = self.capabilities.as_ref().unwrap();
|
||||
|
||||
@@ -575,9 +601,7 @@ impl Client {
|
||||
text_document,
|
||||
range,
|
||||
options,
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: None,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
};
|
||||
|
||||
let response = self
|
||||
@@ -596,15 +620,14 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
let params = lsp::GotoDefinitionParams {
|
||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||
text_document,
|
||||
position,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: None,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
partial_result_params: lsp::PartialResultParams {
|
||||
partial_result_token: None,
|
||||
},
|
||||
@@ -617,30 +640,42 @@ impl Client {
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
self.goto_request::<lsp::request::GotoDefinition>(text_document, position)
|
||||
self.goto_request::<lsp::request::GotoDefinition>(text_document, position, work_done_token)
|
||||
}
|
||||
|
||||
pub fn goto_type_definition(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
self.goto_request::<lsp::request::GotoTypeDefinition>(text_document, position)
|
||||
self.goto_request::<lsp::request::GotoTypeDefinition>(
|
||||
text_document,
|
||||
position,
|
||||
work_done_token,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn goto_implementation(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
self.goto_request::<lsp::request::GotoImplementation>(text_document, position)
|
||||
self.goto_request::<lsp::request::GotoImplementation>(
|
||||
text_document,
|
||||
position,
|
||||
work_done_token,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn goto_reference(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> impl Future<Output = Result<Value>> {
|
||||
let params = lsp::ReferenceParams {
|
||||
text_document_position: lsp::TextDocumentPositionParams {
|
||||
@@ -650,9 +685,7 @@ impl Client {
|
||||
context: lsp::ReferenceContext {
|
||||
include_declaration: true,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams {
|
||||
work_done_token: None,
|
||||
},
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
partial_result_params: lsp::PartialResultParams {
|
||||
partial_result_token: None,
|
||||
},
|
||||
|
@@ -13,7 +13,10 @@ use helix_core::syntax::LanguageConfiguration;
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::Arc,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -181,6 +184,30 @@ pub mod util {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum MethodCall {
|
||||
WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams),
|
||||
}
|
||||
|
||||
impl MethodCall {
|
||||
pub fn parse(method: &str, params: jsonrpc::Params) -> Option<MethodCall> {
|
||||
use lsp::request::Request;
|
||||
let request = match method {
|
||||
lsp::request::WorkDoneProgressCreate::METHOD => {
|
||||
let params: lsp::WorkDoneProgressCreateParams = params
|
||||
.parse()
|
||||
.expect("Failed to parse WorkDoneCreate params");
|
||||
Self::WorkDoneProgressCreate(params)
|
||||
}
|
||||
_ => {
|
||||
log::warn!("unhandled lsp request: {}", method);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Notification {
|
||||
PublishDiagnostics(lsp::PublishDiagnosticsParams),
|
||||
@@ -230,9 +257,10 @@ impl Notification {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Registry {
|
||||
inner: HashMap<LanguageId, Arc<Client>>,
|
||||
inner: HashMap<LanguageId, (usize, Arc<Client>)>,
|
||||
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<Call>>,
|
||||
counter: AtomicUsize,
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
|
||||
}
|
||||
|
||||
impl Default for Registry {
|
||||
@@ -245,10 +273,18 @@ impl Registry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: HashMap::new(),
|
||||
counter: AtomicUsize::new(0),
|
||||
incoming: SelectAll::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_by_id(&mut self, id: usize) -> Option<&Client> {
|
||||
self.inner
|
||||
.values()
|
||||
.find(|(client_id, _)| client_id == &id)
|
||||
.map(|(_, client)| client.as_ref())
|
||||
}
|
||||
|
||||
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
|
||||
if let Some(config) = &language_config.language_server {
|
||||
// avoid borrow issues
|
||||
@@ -256,16 +292,17 @@ impl Registry {
|
||||
let s_incoming = &mut self.incoming;
|
||||
|
||||
match inner.entry(language_config.scope.clone()) {
|
||||
Entry::Occupied(language_server) => Ok(language_server.get().clone()),
|
||||
Entry::Occupied(entry) => Ok(entry.get().1.clone()),
|
||||
Entry::Vacant(entry) => {
|
||||
// initialize a new client
|
||||
let (mut client, incoming) = Client::start(&config.command, &config.args)?;
|
||||
let id = self.counter.fetch_add(1, Ordering::Relaxed);
|
||||
let (mut client, incoming) = Client::start(&config.command, &config.args, id)?;
|
||||
// TODO: run this async without blocking
|
||||
futures_executor::block_on(client.initialize())?;
|
||||
s_incoming.push(UnboundedReceiverStream::new(incoming));
|
||||
let client = Arc::new(client);
|
||||
|
||||
entry.insert(client.clone());
|
||||
entry.insert((id, client.clone()));
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
@@ -273,6 +310,88 @@ impl Registry {
|
||||
Err(Error::LspNotDefined)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
|
||||
self.inner.values().map(|(_, client)| client)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProgressStatus {
|
||||
Created,
|
||||
Started(lsp::WorkDoneProgress),
|
||||
}
|
||||
|
||||
impl ProgressStatus {
|
||||
pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
|
||||
match &self {
|
||||
ProgressStatus::Created => None,
|
||||
ProgressStatus::Started(progress) => Some(progress),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
/// Acts as a container for progress reported by language servers. Each server
|
||||
/// has a unique id assigned at creation through [`Registry`]. This id is then used
|
||||
/// to store the progress in this map.
|
||||
pub struct LspProgressMap(HashMap<usize, HashMap<lsp::ProgressToken, ProgressStatus>>);
|
||||
|
||||
impl LspProgressMap {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Returns a map of all tokens coresponding to the lanaguage server with `id`.
|
||||
pub fn progress_map(&self, id: usize) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
|
||||
self.0.get(&id)
|
||||
}
|
||||
|
||||
pub fn is_progressing(&self, id: usize) -> bool {
|
||||
self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns last progress status for a given server with `id` and `token`.
|
||||
pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> {
|
||||
self.0.get(&id).and_then(|values| values.get(token))
|
||||
}
|
||||
|
||||
/// Checks if progress `token` for server with `id` is created.
|
||||
pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool {
|
||||
self.0
|
||||
.get(&id)
|
||||
.map(|values| values.get(token).is_some())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
|
||||
self.0
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.insert(token, ProgressStatus::Created);
|
||||
}
|
||||
|
||||
/// Ends the progress by removing the `token` from server with `id`, if removed returns the value.
|
||||
pub fn end_progress(
|
||||
&mut self,
|
||||
id: usize,
|
||||
token: &lsp::ProgressToken,
|
||||
) -> Option<ProgressStatus> {
|
||||
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
|
||||
}
|
||||
|
||||
/// Updates the progess of `token` for server with `id` to `status`, returns the value replaced or `None`.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
id: usize,
|
||||
token: lsp::ProgressToken,
|
||||
status: lsp::WorkDoneProgress,
|
||||
) -> Option<ProgressStatus> {
|
||||
self.0
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.insert(token, ProgressStatus::Started(status))
|
||||
}
|
||||
}
|
||||
|
||||
// REGISTRY = HashMap<LanguageId, Lazy/OnceCell<Arc<RwLock<Client>>>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use jsonrpc_core as jsonrpc;
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -33,7 +34,8 @@ enum ServerMessage {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
client_tx: UnboundedSender<jsonrpc::Call>,
|
||||
id: usize,
|
||||
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
|
||||
client_rx: UnboundedReceiver<Payload>,
|
||||
|
||||
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
|
||||
@@ -48,11 +50,16 @@ impl Transport {
|
||||
server_stdout: BufReader<ChildStdout>,
|
||||
server_stdin: BufWriter<ChildStdin>,
|
||||
server_stderr: BufReader<ChildStderr>,
|
||||
) -> (UnboundedReceiver<jsonrpc::Call>, UnboundedSender<Payload>) {
|
||||
id: usize,
|
||||
) -> (
|
||||
UnboundedReceiver<(usize, jsonrpc::Call)>,
|
||||
UnboundedSender<Payload>,
|
||||
) {
|
||||
let (client_tx, rx) = unbounded_channel();
|
||||
let (tx, client_rx) = unbounded_channel();
|
||||
|
||||
let transport = Self {
|
||||
id,
|
||||
server_stdout,
|
||||
server_stdin,
|
||||
server_stderr,
|
||||
@@ -84,7 +91,7 @@ impl Transport {
|
||||
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some("Content-Length"), Some(value), None) => {
|
||||
content_length = Some(value.parse().unwrap());
|
||||
content_length = Some(value.parse().context("invalid content length")?);
|
||||
}
|
||||
(Some(_), Some(_), None) => {}
|
||||
_ => {
|
||||
@@ -97,12 +104,12 @@ impl Transport {
|
||||
}
|
||||
}
|
||||
|
||||
let content_length = content_length.unwrap();
|
||||
let content_length = content_length.context("missing content length")?;
|
||||
|
||||
//TODO: reuse vector
|
||||
let mut content = vec![0; content_length];
|
||||
reader.read_exact(&mut content).await?;
|
||||
let msg = String::from_utf8(content).unwrap();
|
||||
let msg = String::from_utf8(content).context("invalid utf8 from server")?;
|
||||
|
||||
info!("<- {}", msg);
|
||||
|
||||
@@ -156,7 +163,9 @@ impl Transport {
|
||||
match msg {
|
||||
ServerMessage::Output(output) => self.process_request_response(output).await?,
|
||||
ServerMessage::Call(call) => {
|
||||
self.client_tx.send(call).unwrap();
|
||||
self.client_tx
|
||||
.send((self.id, call))
|
||||
.context("failed to send a message to server")?;
|
||||
// let notification = Notification::parse(&method, params);
|
||||
}
|
||||
};
|
||||
|
@@ -1,11 +1,14 @@
|
||||
[package]
|
||||
name = "helix-syntax"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
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
|
||||
description = "Tree-sitter grammars support"
|
||||
categories = ["editor"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
homepage = "https://helix-editor.com"
|
||||
include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
|
||||
|
||||
[dependencies]
|
||||
tree-sitter = "0.19"
|
||||
|
1
helix-syntax/languages/tree-sitter-latex
Submodule
1
helix-syntax/languages/tree-sitter-latex
Submodule
Submodule helix-syntax/languages/tree-sitter-latex added at 7f720661de
@@ -80,6 +80,7 @@ mk_langs!(
|
||||
(Java, tree_sitter_java),
|
||||
(Json, tree_sitter_json),
|
||||
(Julia, tree_sitter_julia),
|
||||
(Latex, tree_sitter_latex),
|
||||
(Nix, tree_sitter_nix),
|
||||
(Php, tree_sitter_php),
|
||||
(Python, tree_sitter_python),
|
||||
|
@@ -1,10 +1,14 @@
|
||||
[package]
|
||||
name = "helix-term"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "A post-modern text editor."
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
categories = ["editor", "command-line-utilities"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
homepage = "https://helix-editor.com"
|
||||
include = ["src/**/*", "README.md"]
|
||||
|
||||
[package.metadata.nix]
|
||||
build = true
|
||||
@@ -18,17 +22,17 @@ name = "hx"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-view = { path = "../helix-view", features = ["term"]}
|
||||
helix-lsp = { path = "../helix-lsp"}
|
||||
helix-core = { version = "0.3", path = "../helix-core" }
|
||||
helix-view = { version = "0.3", path = "../helix-view" }
|
||||
helix-lsp = { version = "0.3", path = "../helix-lsp" }
|
||||
|
||||
anyhow = "1"
|
||||
once_cell = "1.4"
|
||||
once_cell = "1.8"
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
num_cpus = "1"
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
|
||||
crossterm = { version = "0.19", features = ["event-stream"] }
|
||||
crossterm = { version = "0.20", features = ["event-stream"] }
|
||||
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
|
||||
|
@@ -1,7 +0,0 @@
|
||||
|
||||
window -> buffer -> text
|
||||
\-> contains "view", a viewport into the buffer
|
||||
|
||||
view
|
||||
\-> selections etc
|
||||
-> cursor
|
@@ -1,29 +1,35 @@
|
||||
use helix_lsp::lsp;
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
use helix_core::syntax;
|
||||
use helix_lsp::{lsp, LspProgressMap};
|
||||
use helix_view::{document::Mode, graphics::Rect, theme, Document, Editor, Theme, View};
|
||||
|
||||
use crate::{args::Args, compositor::Compositor, ui};
|
||||
use crate::{
|
||||
args::Args,
|
||||
compositor::Compositor,
|
||||
config::Config,
|
||||
keymap::Keymaps,
|
||||
ui::{self, Spinner},
|
||||
};
|
||||
|
||||
use log::{error, info};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
io::{self, stdout, Stdout, Write},
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Error;
|
||||
use anyhow::{Context, Error};
|
||||
|
||||
use crossterm::{
|
||||
event::{Event, EventStream},
|
||||
execute, terminal,
|
||||
};
|
||||
|
||||
use tui::layout::Rect;
|
||||
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use std::pin::Pin;
|
||||
use futures_util::{future, stream::FuturesUnordered};
|
||||
|
||||
type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
pub type LspCallback =
|
||||
@@ -36,17 +42,52 @@ pub struct Application {
|
||||
compositor: Compositor,
|
||||
editor: Editor,
|
||||
|
||||
// TODO should be separate to take only part of the config
|
||||
config: Config,
|
||||
|
||||
theme_loader: Arc<theme::Loader>,
|
||||
syn_loader: Arc<syntax::Loader>,
|
||||
|
||||
callbacks: LspCallbacks,
|
||||
lsp_progress: LspProgressMap,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn new(mut args: Args) -> Result<Self, Error> {
|
||||
pub fn new(mut args: Args, mut config: Config) -> Result<Self, Error> {
|
||||
use helix_view::editor::Action;
|
||||
let mut compositor = Compositor::new()?;
|
||||
let size = compositor.size();
|
||||
let mut editor = Editor::new(size);
|
||||
|
||||
compositor.push(Box::new(ui::EditorView::new()));
|
||||
let conf_dir = helix_core::config_dir();
|
||||
|
||||
let theme_loader =
|
||||
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
|
||||
|
||||
// load $HOME/.config/helix/languages.toml, fallback to default config
|
||||
let lang_conf = std::fs::read(conf_dir.join("languages.toml"));
|
||||
let lang_conf = lang_conf
|
||||
.as_deref()
|
||||
.unwrap_or(include_bytes!("../../languages.toml"));
|
||||
|
||||
let theme = if let Some(theme) = &config.theme {
|
||||
match theme_loader.load(theme) {
|
||||
Ok(theme) => theme,
|
||||
Err(e) => {
|
||||
log::warn!("failed to load theme `{}` - {}", theme, e);
|
||||
theme_loader.default()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
theme_loader.default()
|
||||
};
|
||||
|
||||
let syn_loader_conf = toml::from_slice(lang_conf).expect("Could not parse languages.toml");
|
||||
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
|
||||
|
||||
let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone());
|
||||
|
||||
let mut editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
|
||||
compositor.push(editor_view);
|
||||
|
||||
if !args.files.is_empty() {
|
||||
let first = &args.files[0]; // we know it's not empty
|
||||
@@ -68,11 +109,19 @@ impl Application {
|
||||
editor.new_file(Action::VerticalSplit);
|
||||
}
|
||||
|
||||
editor.set_theme(theme);
|
||||
|
||||
let mut app = Self {
|
||||
compositor,
|
||||
editor,
|
||||
|
||||
config,
|
||||
|
||||
theme_loader,
|
||||
syn_loader,
|
||||
|
||||
callbacks: FuturesUnordered::new(),
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
};
|
||||
|
||||
Ok(app)
|
||||
@@ -102,14 +151,26 @@ impl Application {
|
||||
break;
|
||||
}
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use futures_util::{FutureExt, StreamExt};
|
||||
|
||||
tokio::select! {
|
||||
event = reader.next() => {
|
||||
self.handle_terminal_events(event)
|
||||
}
|
||||
Some(call) = self.editor.language_servers.incoming.next() => {
|
||||
self.handle_language_server_message(call).await
|
||||
Some((id, call)) = self.editor.language_servers.incoming.next() => {
|
||||
self.handle_language_server_message(call, id).await;
|
||||
|
||||
// eagerly process any other available notifications/calls
|
||||
let now = std::time::Instant::now();
|
||||
let deadline = std::time::Duration::from_millis(10);
|
||||
while let Some(Some((id, call))) = self.editor.language_servers.incoming.next().now_or_never() {
|
||||
self.handle_language_server_message(call, id).await;
|
||||
|
||||
if now.elapsed() > deadline { // use a deadline so we don't block too long
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.render();
|
||||
}
|
||||
Some(callback) = &mut self.callbacks.next() => {
|
||||
self.handle_language_server_callback(callback)
|
||||
@@ -152,8 +213,21 @@ impl Application {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_language_server_message(&mut self, call: helix_lsp::Call) {
|
||||
use helix_lsp::{Call, Notification};
|
||||
pub async fn handle_language_server_message(
|
||||
&mut self,
|
||||
call: helix_lsp::Call,
|
||||
server_id: usize,
|
||||
) {
|
||||
use helix_lsp::{Call, MethodCall, Notification};
|
||||
let editor_view = self
|
||||
.compositor
|
||||
.find(std::any::type_name::<ui::EditorView>())
|
||||
.expect("expected at least one EditorView");
|
||||
let editor_view = editor_view
|
||||
.as_any_mut()
|
||||
.downcast_mut::<ui::EditorView>()
|
||||
.unwrap();
|
||||
|
||||
match call {
|
||||
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
|
||||
let notification = match Notification::parse(&method, params) {
|
||||
@@ -161,7 +235,6 @@ impl Application {
|
||||
None => return,
|
||||
};
|
||||
|
||||
// TODO: parse should return Result/Option
|
||||
match notification {
|
||||
Notification::PublishDiagnostics(params) => {
|
||||
let path = Some(params.uri.to_file_path().unwrap());
|
||||
@@ -230,8 +303,6 @@ impl Application {
|
||||
.collect();
|
||||
|
||||
doc.set_diagnostics(diagnostics);
|
||||
// TODO: we want to process all the events in queue, then render. publishDiagnostic tends to send a whole bunch of events
|
||||
self.render();
|
||||
}
|
||||
}
|
||||
Notification::ShowMessage(params) => {
|
||||
@@ -241,64 +312,143 @@ impl Application {
|
||||
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;
|
||||
let lsp::ProgressParams { token, value } = params;
|
||||
|
||||
let lsp::ProgressParamsValue::WorkDone(work) = value;
|
||||
let parts = match &work {
|
||||
lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
|
||||
title,
|
||||
message,
|
||||
percentage,
|
||||
..
|
||||
}) => (Some(title), message, percentage),
|
||||
lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
|
||||
message,
|
||||
percentage,
|
||||
..
|
||||
}) => (None, message, percentage),
|
||||
lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message }) => {
|
||||
if message.is_some() {
|
||||
(None, message, &None)
|
||||
} else {
|
||||
self.lsp_progress.end_progress(server_id, &token);
|
||||
if !self.lsp_progress.is_progressing(server_id) {
|
||||
editor_view.spinners_mut().get_or_create(server_id).stop();
|
||||
}
|
||||
self.editor.clear_status();
|
||||
|
||||
// we want to render to clear any leftover spinners or messages
|
||||
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();
|
||||
|
||||
let token_d: &dyn std::fmt::Display = match &token {
|
||||
lsp::NumberOrString::Number(n) => n,
|
||||
lsp::NumberOrString::String(s) => s,
|
||||
};
|
||||
|
||||
let status = match parts {
|
||||
(Some(title), Some(message), Some(percentage)) => {
|
||||
format!("[{}] {}% {} - {}", token_d, percentage, title, message)
|
||||
}
|
||||
(Some(title), None, Some(percentage)) => {
|
||||
format!("[{}] {}% {}", token_d, percentage, title)
|
||||
}
|
||||
(Some(title), Some(message), None) => {
|
||||
format!("[{}] {} - {}", token_d, title, message)
|
||||
}
|
||||
(None, Some(message), Some(percentage)) => {
|
||||
format!("[{}] {}% {}", token_d, percentage, message)
|
||||
}
|
||||
(Some(title), None, None) => {
|
||||
format!("[{}] {}", token_d, title)
|
||||
}
|
||||
(None, Some(message), None) => {
|
||||
format!("[{}] {}", token_d, message)
|
||||
}
|
||||
(None, None, Some(percentage)) => {
|
||||
format!("[{}] {}%", token_d, percentage)
|
||||
}
|
||||
(None, None, None) => format!("[{}]", token_d),
|
||||
};
|
||||
|
||||
if let lsp::WorkDoneProgress::End(_) = work {
|
||||
self.lsp_progress.end_progress(server_id, &token);
|
||||
if !self.lsp_progress.is_progressing(server_id) {
|
||||
editor_view.spinners_mut().get_or_create(server_id).stop();
|
||||
}
|
||||
} else {
|
||||
self.lsp_progress.update(server_id, token, work);
|
||||
}
|
||||
|
||||
if self.config.lsp.display_messages {
|
||||
self.editor.set_status(status);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
Call::MethodCall(call) => {
|
||||
error!("Method not found {}", call.method);
|
||||
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
|
||||
method,
|
||||
params,
|
||||
jsonrpc,
|
||||
id,
|
||||
}) => {
|
||||
let call = match MethodCall::parse(&method, params) {
|
||||
Some(call) => call,
|
||||
None => {
|
||||
error!("Method not found {}", method);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match call {
|
||||
MethodCall::WorkDoneProgressCreate(params) => {
|
||||
self.lsp_progress.create(server_id, params.token);
|
||||
|
||||
let spinner = editor_view.spinners_mut().get_or_create(server_id);
|
||||
if spinner.is_stopped() {
|
||||
spinner.start();
|
||||
}
|
||||
|
||||
let doc = self.editor.documents().find(|doc| {
|
||||
doc.language_server()
|
||||
.map(|server| server.id() == server_id)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
match doc {
|
||||
Some(doc) => {
|
||||
// it's ok to unwrap, we check for the language server before
|
||||
let server = doc.language_server().unwrap();
|
||||
tokio::spawn(server.reply(id, Ok(serde_json::Value::Null)));
|
||||
}
|
||||
None => {
|
||||
if let Some(server) =
|
||||
self.editor.language_servers.get_by_id(server_id)
|
||||
{
|
||||
log::warn!(
|
||||
"missing document with language server id `{}`",
|
||||
server_id
|
||||
);
|
||||
tokio::spawn(server.reply(
|
||||
id,
|
||||
Err(helix_lsp::jsonrpc::Error {
|
||||
code: helix_lsp::jsonrpc::ErrorCode::InternalError,
|
||||
message: "document missing".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
));
|
||||
} else {
|
||||
log::warn!(
|
||||
"can't find language server with id `{}`",
|
||||
server_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// self.language_server.reply(
|
||||
// call.id,
|
||||
// // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
|
||||
@@ -330,6 +480,8 @@ impl Application {
|
||||
|
||||
self.event_loop().await;
|
||||
|
||||
self.editor.close_language_servers(None).await;
|
||||
|
||||
// reset cursor shape
|
||||
write!(stdout, "\x1B[2 q");
|
||||
|
||||
|
@@ -17,7 +17,7 @@ impl Args {
|
||||
|
||||
iter.next(); // skip the program, we don't care about that
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
for arg in &mut iter {
|
||||
match arg.as_str() {
|
||||
"--" => break, // stop parsing at this point treat the remaining as files
|
||||
"--version" => args.display_version = true,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
||||
// Each component declares it's own size constraints and gets fitted based on it's parent.
|
||||
// Q: how does this work with popups?
|
||||
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
|
||||
use helix_core::Position;
|
||||
use helix_lsp::LspProgressMap;
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
|
||||
use crossterm::event::Event;
|
||||
use helix_core::Position;
|
||||
use tui::{buffer::Buffer as Surface, layout::Rect};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
|
||||
|
||||
@@ -47,8 +49,9 @@ pub trait Component: Any + AnyComponent {
|
||||
/// Render the component onto the provided surface.
|
||||
fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
|
||||
|
||||
fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> {
|
||||
None
|
||||
/// Get cursor position and cursor kind.
|
||||
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
|
||||
(None, CursorKind::Hidden)
|
||||
}
|
||||
|
||||
/// May be used by the parent component to compute the child area.
|
||||
@@ -137,20 +140,19 @@ impl Compositor {
|
||||
layer.render(area, surface, cx)
|
||||
}
|
||||
|
||||
let pos = self
|
||||
.cursor_position(area, cx.editor)
|
||||
.map(|pos| (pos.col as u16, pos.row as u16));
|
||||
let (pos, kind) = self.cursor(area, cx.editor);
|
||||
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
|
||||
|
||||
self.terminal.draw(pos);
|
||||
self.terminal.draw(pos, kind);
|
||||
}
|
||||
|
||||
pub fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
|
||||
pub fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
|
||||
for layer in self.layers.iter().rev() {
|
||||
if let Some(pos) = layer.cursor_position(area, editor) {
|
||||
return Some(pos);
|
||||
if let (Some(pos), kind) = layer.cursor(area, editor) {
|
||||
return (Some(pos), kind);
|
||||
}
|
||||
}
|
||||
None
|
||||
(None, CursorKind::Hidden)
|
||||
}
|
||||
|
||||
pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> {
|
||||
@@ -178,13 +180,13 @@ pub trait AnyComponent {
|
||||
/// Returns a boxed any from a boxed self.
|
||||
///
|
||||
/// Can be used before `Box::downcast()`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||
/// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||
/// ```
|
||||
//
|
||||
// # Examples
|
||||
//
|
||||
// ```rust
|
||||
// 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>;
|
||||
}
|
||||
|
||||
|
65
helix-term/src/config.rs
Normal file
65
helix-term/src/config.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use anyhow::{Error, Result};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::commands::Command;
|
||||
use crate::keymap::Keymaps;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
|
||||
pub struct Config {
|
||||
pub theme: Option<String>,
|
||||
#[serde(default)]
|
||||
pub lsp: LspConfig,
|
||||
#[serde(default)]
|
||||
pub keys: Keymaps,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LspConfig {
|
||||
pub display_messages: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_keymaps_config_file() {
|
||||
use helix_core::hashmap;
|
||||
use helix_view::{
|
||||
document::Mode,
|
||||
input::KeyEvent,
|
||||
keyboard::{KeyCode, KeyModifiers},
|
||||
};
|
||||
|
||||
let sample_keymaps = r#"
|
||||
[keys.insert]
|
||||
y = "move_line_down"
|
||||
S-C-a = "delete_selection"
|
||||
|
||||
[keys.normal]
|
||||
A-F12 = "move_next_word_end"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
toml::from_str::<Config>(sample_keymaps).unwrap(),
|
||||
Config {
|
||||
keys: Keymaps(hashmap! {
|
||||
Mode::Insert => hashmap! {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('y'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
} => Command::move_line_down,
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
|
||||
} => Command::delete_selection,
|
||||
},
|
||||
Mode::Normal => hashmap! {
|
||||
KeyEvent {
|
||||
code: KeyCode::F(12),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
} => Command::move_next_word_end,
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
@@ -1,7 +1,20 @@
|
||||
use crate::commands::{self, Command};
|
||||
use crate::commands;
|
||||
pub use crate::commands::Command;
|
||||
use crate::config::Config;
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use helix_core::hashmap;
|
||||
use helix_view::document::Mode;
|
||||
use std::collections::HashMap;
|
||||
use helix_view::{
|
||||
document::Mode,
|
||||
input::KeyEvent,
|
||||
keyboard::{KeyCode, KeyModifiers},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
ops::{Deref, DerefMut},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
// Kakoune-inspired:
|
||||
// mode = {
|
||||
@@ -92,14 +105,14 @@ use std::collections::HashMap;
|
||||
// D] = last diagnostic
|
||||
// }
|
||||
|
||||
// #[cfg(feature = "term")]
|
||||
pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
pub type Keymap = HashMap<KeyEvent, Command>;
|
||||
pub type Keymaps = HashMap<Mode, Keymap>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! key {
|
||||
($key:ident) => {
|
||||
KeyEvent {
|
||||
code: KeyCode::$key,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}
|
||||
};
|
||||
($($ch:tt)*) => {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char($($ch)*),
|
||||
@@ -126,254 +139,262 @@ macro_rules! alt {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn default() -> Keymaps {
|
||||
let normal = hashmap!(
|
||||
key!('h') => commands::move_char_left as Command,
|
||||
key!('j') => commands::move_line_down,
|
||||
key!('k') => commands::move_line_up,
|
||||
key!('l') => commands::move_char_right,
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_char_left,
|
||||
KeyEvent {
|
||||
code: KeyCode::Down,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_down,
|
||||
KeyEvent {
|
||||
code: KeyCode::Up,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_up,
|
||||
KeyEvent {
|
||||
code: KeyCode::Right,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_char_right,
|
||||
impl Deref for Keymaps {
|
||||
type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
|
||||
|
||||
key!('t') => commands::find_till_char,
|
||||
key!('f') => commands::find_next_char,
|
||||
key!('T') => commands::till_prev_char,
|
||||
key!('F') => commands::find_prev_char,
|
||||
// and matching set for select mode (extend)
|
||||
//
|
||||
key!('r') => commands::replace,
|
||||
key!('R') => commands::replace_with_yanked,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Home,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_start,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::End,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::move_line_end,
|
||||
|
||||
key!('w') => commands::move_next_word_start,
|
||||
key!('b') => commands::move_prev_word_start,
|
||||
key!('e') => commands::move_next_word_end,
|
||||
|
||||
key!('v') => commands::select_mode,
|
||||
key!('g') => commands::goto_mode,
|
||||
key!(':') => commands::command_mode,
|
||||
|
||||
key!('i') => commands::insert_mode,
|
||||
key!('I') => commands::prepend_to_line,
|
||||
key!('a') => commands::append_mode,
|
||||
key!('A') => commands::append_to_line,
|
||||
key!('o') => commands::open_below,
|
||||
key!('O') => commands::open_above,
|
||||
// [<space> ]<space> equivalents too (add blank new line, no edit)
|
||||
|
||||
|
||||
key!('d') => commands::delete_selection,
|
||||
// TODO: also delete without yanking
|
||||
key!('c') => commands::change_selection,
|
||||
// TODO: also change delete without yanking
|
||||
|
||||
// key!('r') => commands::replace_with_char,
|
||||
|
||||
key!('s') => commands::select_regex,
|
||||
alt!('s') => commands::split_selection_on_newline,
|
||||
key!('S') => commands::split_selection,
|
||||
key!(';') => commands::collapse_selection,
|
||||
alt!(';') => commands::flip_selections,
|
||||
key!('%') => commands::select_all,
|
||||
key!('x') => commands::select_line,
|
||||
key!('X') => commands::extend_line,
|
||||
// or select mode X?
|
||||
// extend_to_whole_line, crop_to_whole_line
|
||||
|
||||
|
||||
key!('m') => commands::match_brackets,
|
||||
// TODO: refactor into
|
||||
// key!('m') => commands::select_to_matching,
|
||||
// key!('M') => commands::back_select_to_matching,
|
||||
// select mode extend equivalents
|
||||
|
||||
// key!('.') => commands::repeat_insert,
|
||||
// repeat_select
|
||||
|
||||
// TODO: figure out what key to use
|
||||
// key!('[') => commands::expand_selection, ??
|
||||
key!('[') => commands::left_bracket_mode,
|
||||
key!(']') => commands::right_bracket_mode,
|
||||
|
||||
key!('/') => commands::search,
|
||||
// ? for search_reverse
|
||||
key!('n') => commands::search_next,
|
||||
key!('N') => commands::extend_search_next,
|
||||
// N for search_prev
|
||||
key!('*') => commands::search_selection,
|
||||
|
||||
key!('u') => commands::undo,
|
||||
key!('U') => commands::redo,
|
||||
|
||||
key!('y') => commands::yank,
|
||||
// yank_all
|
||||
key!('p') => commands::paste_after,
|
||||
// paste_all
|
||||
key!('P') => commands::paste_before,
|
||||
|
||||
key!('>') => commands::indent,
|
||||
key!('<') => commands::unindent,
|
||||
key!('=') => commands::format_selections,
|
||||
key!('J') => commands::join_selections,
|
||||
// TODO: conflicts hover/doc
|
||||
key!('K') => commands::keep_selections,
|
||||
// TODO: and another method for inverse
|
||||
|
||||
// TODO: clashes with space mode
|
||||
key!(' ') => commands::keep_primary_selection,
|
||||
|
||||
// key!('q') => commands::record_macro,
|
||||
// key!('Q') => commands::replay_macro,
|
||||
|
||||
// ~ / apostrophe => change case
|
||||
// & align selections
|
||||
// _ trim selections
|
||||
|
||||
// C / altC = copy (repeat) selections on prev/next lines
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::normal_mode,
|
||||
KeyEvent {
|
||||
code: KeyCode::PageUp,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::page_up,
|
||||
ctrl!('b') => commands::page_up,
|
||||
KeyEvent {
|
||||
code: KeyCode::PageDown,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::page_down,
|
||||
ctrl!('f') => commands::page_down,
|
||||
ctrl!('u') => commands::half_page_up,
|
||||
ctrl!('d') => commands::half_page_down,
|
||||
|
||||
ctrl!('w') => commands::window_mode,
|
||||
|
||||
// move under <space>c
|
||||
ctrl!('c') => commands::toggle_comments,
|
||||
key!('K') => commands::hover,
|
||||
|
||||
// z family for save/restore/combine from/to sels from register
|
||||
|
||||
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
|
||||
// because some selection operations can now be done from normal mode, some from select mode.
|
||||
let mut select = normal.clone();
|
||||
select.extend(
|
||||
hashmap!(
|
||||
key!('h') => commands::extend_char_left as Command,
|
||||
key!('j') => commands::extend_line_down,
|
||||
key!('k') => commands::extend_line_up,
|
||||
key!('l') => commands::extend_char_right,
|
||||
|
||||
KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_char_left,
|
||||
KeyEvent {
|
||||
code: KeyCode::Down,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_down,
|
||||
KeyEvent {
|
||||
code: KeyCode::Up,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_up,
|
||||
KeyEvent {
|
||||
code: KeyCode::Right,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_char_right,
|
||||
|
||||
key!('w') => commands::extend_next_word_start,
|
||||
key!('b') => commands::extend_prev_word_start,
|
||||
key!('e') => commands::extend_next_word_end,
|
||||
|
||||
key!('t') => commands::extend_till_char,
|
||||
key!('f') => commands::extend_next_char,
|
||||
|
||||
key!('T') => commands::extend_till_prev_char,
|
||||
key!('F') => commands::extend_prev_char,
|
||||
KeyEvent {
|
||||
code: KeyCode::Home,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_start,
|
||||
KeyEvent {
|
||||
code: KeyCode::End,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::extend_line_end,
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::exit_select_mode,
|
||||
)
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
hashmap!(
|
||||
// as long as you cast the first item, rust is able to infer the other cases
|
||||
// TODO: select could be normal mode with some bindings merged over
|
||||
Mode::Normal => normal,
|
||||
Mode::Select => select,
|
||||
Mode::Insert => hashmap!(
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::normal_mode as Command,
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::delete_char_backward,
|
||||
KeyEvent {
|
||||
code: KeyCode::Delete,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::delete_char_forward,
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::insert_newline,
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::NONE
|
||||
} => commands::insert::insert_tab,
|
||||
|
||||
ctrl!('x') => commands::completion,
|
||||
ctrl!('w') => commands::insert::delete_word_backward,
|
||||
),
|
||||
)
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Keymaps {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Keymaps {
|
||||
fn default() -> Keymaps {
|
||||
let normal = hashmap!(
|
||||
key!('h') => Command::move_char_left,
|
||||
key!('j') => Command::move_line_down,
|
||||
key!('k') => Command::move_line_up,
|
||||
key!('l') => Command::move_char_right,
|
||||
|
||||
key!(Left) => Command::move_char_left,
|
||||
key!(Down) => Command::move_line_down,
|
||||
key!(Up) => Command::move_line_up,
|
||||
key!(Right) => Command::move_char_right,
|
||||
|
||||
key!('t') => Command::find_till_char,
|
||||
key!('f') => Command::find_next_char,
|
||||
key!('T') => Command::till_prev_char,
|
||||
key!('F') => Command::find_prev_char,
|
||||
// and matching set for select mode (extend)
|
||||
//
|
||||
key!('r') => Command::replace,
|
||||
key!('R') => Command::replace_with_yanked,
|
||||
|
||||
key!(Home) => Command::move_line_start,
|
||||
key!(End) => Command::move_line_end,
|
||||
|
||||
key!('w') => Command::move_next_word_start,
|
||||
key!('b') => Command::move_prev_word_start,
|
||||
key!('e') => Command::move_next_word_end,
|
||||
|
||||
key!('v') => Command::select_mode,
|
||||
key!('g') => Command::goto_mode,
|
||||
key!(':') => Command::command_mode,
|
||||
|
||||
key!('i') => Command::insert_mode,
|
||||
key!('I') => Command::prepend_to_line,
|
||||
key!('a') => Command::append_mode,
|
||||
key!('A') => Command::append_to_line,
|
||||
key!('o') => Command::open_below,
|
||||
key!('O') => Command::open_above,
|
||||
// [<space> ]<space> equivalents too (add blank new line, no edit)
|
||||
|
||||
|
||||
key!('d') => Command::delete_selection,
|
||||
// TODO: also delete without yanking
|
||||
key!('c') => Command::change_selection,
|
||||
// TODO: also change delete without yanking
|
||||
|
||||
// key!('r') => Command::replace_with_char,
|
||||
|
||||
key!('s') => Command::select_regex,
|
||||
alt!('s') => Command::split_selection_on_newline,
|
||||
key!('S') => Command::split_selection,
|
||||
key!(';') => Command::collapse_selection,
|
||||
alt!(';') => Command::flip_selections,
|
||||
key!('%') => Command::select_all,
|
||||
key!('x') => Command::extend_line,
|
||||
// extend_to_whole_line, crop_to_whole_line
|
||||
|
||||
|
||||
key!('m') => Command::match_mode,
|
||||
key!('[') => Command::left_bracket_mode,
|
||||
key!(']') => Command::right_bracket_mode,
|
||||
|
||||
key!('/') => Command::search,
|
||||
// ? for search_reverse
|
||||
key!('n') => Command::search_next,
|
||||
key!('N') => Command::extend_search_next,
|
||||
// N for search_prev
|
||||
key!('*') => Command::search_selection,
|
||||
|
||||
key!('u') => Command::undo,
|
||||
key!('U') => Command::redo,
|
||||
|
||||
key!('y') => Command::yank,
|
||||
// yank_all
|
||||
key!('p') => Command::paste_after,
|
||||
// paste_all
|
||||
key!('P') => Command::paste_before,
|
||||
|
||||
key!('>') => Command::indent,
|
||||
key!('<') => Command::unindent,
|
||||
key!('=') => Command::format_selections,
|
||||
key!('J') => Command::join_selections,
|
||||
// TODO: conflicts hover/doc
|
||||
key!('K') => Command::keep_selections,
|
||||
// TODO: and another method for inverse
|
||||
|
||||
// TODO: clashes with space mode
|
||||
key!(' ') => Command::keep_primary_selection,
|
||||
|
||||
// key!('q') => Command::record_macro,
|
||||
// key!('Q') => Command::replay_macro,
|
||||
|
||||
// ~ / apostrophe => change case
|
||||
// & align selections
|
||||
// _ trim selections
|
||||
|
||||
// C / altC = copy (repeat) selections on prev/next lines
|
||||
|
||||
key!(Esc) => Command::normal_mode,
|
||||
key!(PageUp) => Command::page_up,
|
||||
key!(PageDown) => Command::page_down,
|
||||
ctrl!('b') => Command::page_up,
|
||||
ctrl!('f') => Command::page_down,
|
||||
ctrl!('u') => Command::half_page_up,
|
||||
ctrl!('d') => Command::half_page_down,
|
||||
|
||||
ctrl!('w') => Command::window_mode,
|
||||
|
||||
// move under <space>c
|
||||
ctrl!('c') => Command::toggle_comments,
|
||||
key!('K') => Command::hover,
|
||||
|
||||
// z family for save/restore/combine from/to sels from register
|
||||
|
||||
// supposedly ctrl!('i') but did not work
|
||||
key!(Tab) => Command::jump_forward,
|
||||
ctrl!('o') => Command::jump_backward,
|
||||
// ctrl!('s') => Command::save_selection,
|
||||
|
||||
key!(' ') => Command::space_mode,
|
||||
key!('z') => Command::view_mode,
|
||||
|
||||
key!('"') => Command::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
|
||||
// because some selection operations can now be done from normal mode, some from select mode.
|
||||
let mut select = normal.clone();
|
||||
select.extend(
|
||||
hashmap!(
|
||||
key!('h') => Command::extend_char_left,
|
||||
key!('j') => Command::extend_line_down,
|
||||
key!('k') => Command::extend_line_up,
|
||||
key!('l') => Command::extend_char_right,
|
||||
|
||||
key!(Left) => Command::extend_char_left,
|
||||
key!(Down) => Command::extend_line_down,
|
||||
key!(Up) => Command::extend_line_up,
|
||||
key!(Right) => Command::extend_char_right,
|
||||
|
||||
key!('w') => Command::extend_next_word_start,
|
||||
key!('b') => Command::extend_prev_word_start,
|
||||
key!('e') => Command::extend_next_word_end,
|
||||
|
||||
key!('t') => Command::extend_till_char,
|
||||
key!('f') => Command::extend_next_char,
|
||||
|
||||
key!('T') => Command::extend_till_prev_char,
|
||||
key!('F') => Command::extend_prev_char,
|
||||
key!(Home) => Command::extend_line_start,
|
||||
key!(End) => Command::extend_line_end,
|
||||
key!(Esc) => Command::exit_select_mode,
|
||||
)
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
Keymaps(hashmap!(
|
||||
// as long as you cast the first item, rust is able to infer the other cases
|
||||
// TODO: select could be normal mode with some bindings merged over
|
||||
Mode::Normal => normal,
|
||||
Mode::Select => select,
|
||||
Mode::Insert => hashmap!(
|
||||
key!(Esc) => Command::normal_mode as Command,
|
||||
key!(Backspace) => Command::delete_char_backward,
|
||||
key!(Delete) => Command::delete_char_forward,
|
||||
key!(Enter) => Command::insert_newline,
|
||||
key!(Tab) => Command::insert_tab,
|
||||
key!(Left) => Command::move_char_left,
|
||||
key!(Down) => Command::move_line_down,
|
||||
key!(Up) => Command::move_line_up,
|
||||
key!(Right) => Command::move_char_right,
|
||||
key!(PageUp) => Command::page_up,
|
||||
key!(PageDown) => Command::page_down,
|
||||
key!(Home) => Command::move_line_start,
|
||||
key!(End) => Command::move_line_end,
|
||||
ctrl!('x') => Command::completion,
|
||||
ctrl!('w') => Command::delete_word_backward,
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge default config keys with user overwritten keys for custom
|
||||
/// user config.
|
||||
pub fn merge_keys(mut config: Config) -> Config {
|
||||
let mut delta = std::mem::take(&mut config.keys);
|
||||
for (mode, keys) in &mut *config.keys {
|
||||
keys.extend(delta.remove(mode).unwrap_or_default());
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_partial_keys() {
|
||||
let config = Config {
|
||||
keys: Keymaps(hashmap! {
|
||||
Mode::Normal => hashmap! {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('i'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
} => Command::normal_mode,
|
||||
KeyEvent { // key that does not exist
|
||||
code: KeyCode::Char('无'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
} => Command::insert_mode,
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let merged_config = merge_keys(config.clone());
|
||||
assert_ne!(config, merged_config);
|
||||
assert_eq!(
|
||||
*merged_config
|
||||
.keys
|
||||
.0
|
||||
.get(&Mode::Normal)
|
||||
.unwrap()
|
||||
.get(&KeyEvent {
|
||||
code: KeyCode::Char('i'),
|
||||
modifiers: KeyModifiers::NONE
|
||||
})
|
||||
.unwrap(),
|
||||
Command::normal_mode
|
||||
);
|
||||
assert_eq!(
|
||||
*merged_config
|
||||
.keys
|
||||
.0
|
||||
.get(&Mode::Normal)
|
||||
.unwrap()
|
||||
.get(&KeyEvent {
|
||||
code: KeyCode::Char('无'),
|
||||
modifiers: KeyModifiers::NONE
|
||||
})
|
||||
.unwrap(),
|
||||
Command::insert_mode
|
||||
);
|
||||
assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
|
||||
assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
|
||||
}
|
||||
|
@@ -1,8 +1,12 @@
|
||||
#![allow(unused)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate helix_view;
|
||||
|
||||
pub mod application;
|
||||
pub mod args;
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod config;
|
||||
pub mod keymap;
|
||||
pub mod ui;
|
||||
|
@@ -1,18 +1,13 @@
|
||||
use anyhow::{Context, Error, Result};
|
||||
use helix_term::application::Application;
|
||||
use helix_term::args::Args;
|
||||
|
||||
use helix_term::config::Config;
|
||||
use helix_term::keymap::merge_keys;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
let mut base_config = fern::Dispatch::new();
|
||||
|
||||
// Let's say we depend on something which whose "info" level messages are too
|
||||
// verbose to include in end-user output. If we don't need them,
|
||||
// let's not include them.
|
||||
// .level_for("overly-verbose-target", log::LevelFilter::Warn)
|
||||
|
||||
base_config = match verbosity {
|
||||
0 => base_config.level(log::LevelFilter::Warn),
|
||||
1 => base_config.level(log::LevelFilter::Info),
|
||||
@@ -89,10 +84,16 @@ FLAGS:
|
||||
std::fs::create_dir_all(&conf_dir).ok();
|
||||
}
|
||||
|
||||
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
|
||||
Ok(config) => merge_keys(toml::from_str(&config)?),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
|
||||
Err(err) => return Err(Error::new(err)),
|
||||
};
|
||||
|
||||
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")?;
|
||||
let mut app = Application::new(args, config).context("unable to create new application")?;
|
||||
app.run().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
|
@@ -1,15 +1,14 @@
|
||||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use helix_core::{Position, Transaction};
|
||||
use helix_view::Editor;
|
||||
use helix_view::{
|
||||
graphics::{Color, Rect, Style},
|
||||
Editor,
|
||||
};
|
||||
|
||||
use crate::commands;
|
||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
@@ -68,7 +67,7 @@ impl menu::Item for CompletionItem {
|
||||
|
||||
/// Wraps a Menu.
|
||||
pub struct Completion {
|
||||
popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
|
||||
popup: Popup<Menu<CompletionItem>>,
|
||||
trigger_offset: usize,
|
||||
// TODO: maintain a completioncontext with trigger kind & trigger char
|
||||
}
|
||||
@@ -82,27 +81,9 @@ impl Completion {
|
||||
// let items: Vec<CompletionItem> = Vec::new();
|
||||
let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| {
|
||||
match event {
|
||||
PromptEvent::Abort => {
|
||||
// revert state
|
||||
// let id = editor.view().doc;
|
||||
// let doc = &mut editor.documents[id];
|
||||
// doc.state = snapshot.clone();
|
||||
}
|
||||
PromptEvent::Abort => {}
|
||||
PromptEvent::Validate => {
|
||||
let (view, doc) = editor.current();
|
||||
|
||||
// revert state to what it was before the last update
|
||||
// doc.state = snapshot.clone();
|
||||
|
||||
// extract as fn(doc, item):
|
||||
|
||||
// TODO: need to apply without composing state...
|
||||
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
|
||||
// the final state?
|
||||
// -> on update simply update the snapshot, then on accept redo the call,
|
||||
// finally updating doc.changes + notifying lsp.
|
||||
//
|
||||
// or we could simply use doc.undo + apply when changing between options
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
// always present here
|
||||
let item = item.unwrap();
|
||||
@@ -143,7 +124,6 @@ impl Completion {
|
||||
|
||||
doc.apply(&transaction, view.id);
|
||||
|
||||
// TODO: merge edit with additional_text_edits
|
||||
if let Some(additional_edits) = &item.additional_text_edits {
|
||||
// gopls uses this to add extra imports
|
||||
if !additional_edits.is_empty() {
|
||||
@@ -169,7 +149,7 @@ impl Completion {
|
||||
pub fn update(&mut self, cx: &mut commands::Context) {
|
||||
// recompute menu based on matches
|
||||
let menu = self.popup.contents_mut();
|
||||
let (view, doc) = cx.editor.current();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
// cx.hooks()
|
||||
// cx.add_hook(enum type, ||)
|
||||
@@ -226,18 +206,21 @@ impl Component for Completion {
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
self.popup.render(area, surface, cx);
|
||||
|
||||
// TODO: if we have a selection, render a markdown popup on top/below with info
|
||||
// if we have a selection, render a markdown popup on top/below with info
|
||||
if let Some(option) = self.popup.contents().selection() {
|
||||
// need to render:
|
||||
// option.detail
|
||||
// ---
|
||||
// option.documentation
|
||||
|
||||
let (view, doc) = cx.editor.current();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let language = doc
|
||||
.language()
|
||||
.and_then(|scope| scope.strip_prefix("source."))
|
||||
.unwrap_or("");
|
||||
let cursor_pos = doc.selection(view.id).cursor();
|
||||
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
|
||||
- view.first_line) as u16;
|
||||
|
||||
let doc = match &option.documentation {
|
||||
Some(lsp::Documentation::String(contents))
|
||||
@@ -246,42 +229,60 @@ impl Component for Completion {
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: convert to wrapped text
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
))
|
||||
Markdown::new(
|
||||
format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
),
|
||||
cx.editor.syn_loader.clone(),
|
||||
)
|
||||
}
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: set language based on doc scope
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
))
|
||||
Markdown::new(
|
||||
format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
),
|
||||
cx.editor.syn_loader.clone(),
|
||||
)
|
||||
}
|
||||
None if option.detail.is_some() => {
|
||||
// TODO: copied from above
|
||||
|
||||
// TODO: set language based on doc scope
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
))
|
||||
Markdown::new(
|
||||
format!(
|
||||
"```{}\n{}\n```",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
),
|
||||
cx.editor.syn_loader.clone(),
|
||||
)
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
let half = area.height / 2;
|
||||
let height = 15.min(half);
|
||||
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
|
||||
let area = Rect::new(0, area.height - height - 2, area.width, height);
|
||||
// we want to make sure the cursor is visible (not hidden behind the documentation)
|
||||
let y = if cursor_pos + view.area.y
|
||||
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
|
||||
{
|
||||
0
|
||||
} else {
|
||||
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
|
||||
area.height.saturating_sub(height).saturating_sub(2)
|
||||
};
|
||||
|
||||
let area = Rect::new(0, y, area.width, height);
|
||||
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.popup");
|
||||
|
@@ -3,53 +3,63 @@ use crate::{
|
||||
compositor::{Component, Compositor, Context, EventResult},
|
||||
key,
|
||||
keymap::{self, Keymaps},
|
||||
ui::Completion,
|
||||
ui::{Completion, ProgressSpinners},
|
||||
};
|
||||
|
||||
use helix_core::{
|
||||
coords_at_pos,
|
||||
graphemes::ensure_grapheme_boundary,
|
||||
syntax::{self, HighlightEvent},
|
||||
Position, Range,
|
||||
LineEnding, Position, Range,
|
||||
};
|
||||
use helix_lsp::LspProgressMap;
|
||||
use helix_view::{
|
||||
document::Mode,
|
||||
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
||||
input::KeyEvent,
|
||||
keyboard::{KeyCode, KeyModifiers},
|
||||
Document, Editor, Theme, View,
|
||||
};
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
|
||||
};
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
event::{read, Event, EventStream},
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, buffer::Buffer as Surface};
|
||||
|
||||
pub struct EditorView {
|
||||
keymap: Keymaps,
|
||||
keymaps: Keymaps,
|
||||
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
|
||||
last_insert: (commands::Command, Vec<KeyEvent>),
|
||||
completion: Option<Completion>,
|
||||
spinners: ProgressSpinners,
|
||||
}
|
||||
|
||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
|
||||
impl Default for EditorView {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Self::new(Keymaps::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(keymaps: Keymaps) -> Self {
|
||||
Self {
|
||||
keymap: keymap::default(),
|
||||
keymaps,
|
||||
on_next_key: None,
|
||||
last_insert: (commands::normal_mode, Vec::new()),
|
||||
last_insert: (commands::Command::normal_mode, Vec::new()),
|
||||
completion: None,
|
||||
spinners: ProgressSpinners::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
|
||||
&mut self.spinners
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_view(
|
||||
&self,
|
||||
doc: &Document,
|
||||
@@ -114,8 +124,6 @@ impl EditorView {
|
||||
};
|
||||
|
||||
// TODO: range doesn't actually restrict source, just highlight range
|
||||
// TODO: cache highlight results
|
||||
// TODO: only recalculate when state.doc is actually modified
|
||||
let highlights: Vec<_> = match doc.syntax() {
|
||||
Some(syntax) => {
|
||||
syntax
|
||||
@@ -128,13 +136,13 @@ impl EditorView {
|
||||
})],
|
||||
};
|
||||
let mut spans = Vec::new();
|
||||
let mut visual_x = 0;
|
||||
let mut visual_x = 0u16;
|
||||
let mut line = 0u16;
|
||||
let tab_width = doc.tab_width();
|
||||
|
||||
'outer: for event in highlights {
|
||||
match event.unwrap() {
|
||||
HighlightEvent::HighlightStart(span) => {
|
||||
HighlightEvent::HighlightStart(mut span) => {
|
||||
spans.push(span);
|
||||
}
|
||||
HighlightEvent::HighlightEnd => {
|
||||
@@ -144,8 +152,8 @@ impl EditorView {
|
||||
// TODO: filter out spans out of viewport for now..
|
||||
|
||||
// TODO: do these before iterating
|
||||
let start = text.byte_to_char(start);
|
||||
let end = text.byte_to_char(end);
|
||||
let start = ensure_grapheme_boundary(text, text.byte_to_char(start));
|
||||
let end = ensure_grapheme_boundary(text, text.byte_to_char(end));
|
||||
|
||||
let text = text.slice(start..end);
|
||||
|
||||
@@ -175,7 +183,7 @@ impl EditorView {
|
||||
|
||||
// iterate over range char by char
|
||||
for grapheme in RopeGraphemes::new(text) {
|
||||
if grapheme == "\n" {
|
||||
if LineEnding::from_rope_slice(&grapheme).is_some() {
|
||||
visual_x = 0;
|
||||
line += 1;
|
||||
|
||||
@@ -184,7 +192,7 @@ impl EditorView {
|
||||
break 'outer;
|
||||
}
|
||||
} else if grapheme == "\t" {
|
||||
visual_x += (tab_width as u16);
|
||||
visual_x = visual_x.saturating_add(tab_width as u16);
|
||||
} else {
|
||||
let out_of_bounds = visual_x < view.first_col as u16
|
||||
|| visual_x >= viewport.width + view.first_col as u16;
|
||||
@@ -196,7 +204,7 @@ impl EditorView {
|
||||
|
||||
if out_of_bounds {
|
||||
// if we're offscreen just keep going until we hit a new line
|
||||
visual_x += width;
|
||||
visual_x = visual_x.saturating_add(width);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -222,13 +230,51 @@ impl EditorView {
|
||||
visual_x += width;
|
||||
}
|
||||
|
||||
char_index += 1;
|
||||
char_index += grapheme.chars().count();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// render selections
|
||||
// render gutters
|
||||
|
||||
let linenr: Style = theme.get("ui.linenr");
|
||||
let warning: Style = theme.get("warning");
|
||||
let error: Style = theme.get("error");
|
||||
let info: Style = theme.get("info");
|
||||
let hint: Style = theme.get("hint");
|
||||
|
||||
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(
|
||||
viewport.x - OFFSET,
|
||||
viewport.y + i as u16,
|
||||
"●",
|
||||
1,
|
||||
match diagnostic.severity {
|
||||
Some(Severity::Error) => error,
|
||||
Some(Severity::Warning) | None => warning,
|
||||
Some(Severity::Info) => info,
|
||||
Some(Severity::Hint) => hint,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// line numbers having selections are rendered differently
|
||||
surface.set_stringn(
|
||||
viewport.x + 1 - OFFSET,
|
||||
viewport.y + i as u16,
|
||||
format!("{:>5}", line + 1),
|
||||
5,
|
||||
linenr,
|
||||
);
|
||||
}
|
||||
|
||||
// render selections and selected linenr(s)
|
||||
let linenr_select: Style = theme
|
||||
.try_get("ui.linenr.selected")
|
||||
.unwrap_or_else(|| theme.get("ui.linenr"));
|
||||
|
||||
if is_focused {
|
||||
let screen = {
|
||||
@@ -236,22 +282,53 @@ impl EditorView {
|
||||
let end = text.line_to_char(last_line + 1);
|
||||
Range::new(start, end)
|
||||
};
|
||||
let cursor_style = Style::default()
|
||||
// .bg(Color::Rgb(255, 255, 255))
|
||||
.add_modifier(Modifier::REVERSED);
|
||||
|
||||
// let selection_style = Style::default().bg(Color::Rgb(94, 0, 128));
|
||||
let selection_style = Style::default().bg(Color::Rgb(84, 0, 153));
|
||||
let mode = doc.mode();
|
||||
let base_cursor_style = theme
|
||||
.try_get("ui.cursor")
|
||||
.unwrap_or_else(|| Style::default().add_modifier(Modifier::REVERSED));
|
||||
let cursor_style = match mode {
|
||||
Mode::Insert => theme.try_get("ui.cursor.insert"),
|
||||
Mode::Select => theme.try_get("ui.cursor.select"),
|
||||
Mode::Normal => Some(base_cursor_style),
|
||||
}
|
||||
.unwrap_or(base_cursor_style);
|
||||
let primary_cursor_style = theme
|
||||
.try_get("ui.cursor.primary")
|
||||
.map(|style| {
|
||||
if mode != Mode::Normal {
|
||||
// we want to make sure that the insert and select highlights
|
||||
// also affect the primary cursor if set
|
||||
style.patch(cursor_style)
|
||||
} else {
|
||||
style
|
||||
}
|
||||
})
|
||||
.unwrap_or(cursor_style);
|
||||
|
||||
for selection in doc
|
||||
.selection(view.id)
|
||||
let selection_style = theme.get("ui.selection");
|
||||
let primary_selection_style = theme
|
||||
.try_get("ui.selection.primary")
|
||||
.unwrap_or(selection_style);
|
||||
|
||||
let selection = doc.selection(view.id);
|
||||
let primary_idx = selection.primary_index();
|
||||
|
||||
for (i, selection) in selection
|
||||
.iter()
|
||||
.filter(|range| range.overlaps(&screen))
|
||||
.enumerate()
|
||||
.filter(|(_, range)| range.overlaps(&screen))
|
||||
{
|
||||
// TODO: render also if only one of the ranges is in viewport
|
||||
let mut start = view.screen_coords_at_pos(doc, text, selection.anchor);
|
||||
let mut end = view.screen_coords_at_pos(doc, text, selection.head);
|
||||
|
||||
let (cursor_style, selection_style) = if i == primary_idx {
|
||||
(primary_cursor_style, primary_selection_style)
|
||||
} else {
|
||||
(cursor_style, selection_style)
|
||||
};
|
||||
|
||||
let head = end;
|
||||
|
||||
if selection.head < selection.anchor {
|
||||
@@ -326,6 +403,13 @@ impl EditorView {
|
||||
),
|
||||
cursor_style,
|
||||
);
|
||||
surface.set_stringn(
|
||||
viewport.x + 1 - OFFSET,
|
||||
viewport.y + head.row as u16,
|
||||
format!("{:>5}", view.first_line + head.row + 1),
|
||||
5,
|
||||
linenr_select,
|
||||
);
|
||||
// TODO: set cursor position for IME
|
||||
if let Some(syntax) = doc.syntax() {
|
||||
use helix_core::match_brackets;
|
||||
@@ -337,9 +421,12 @@ impl EditorView {
|
||||
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);
|
||||
let style =
|
||||
theme.try_get("ui.cursor.match").unwrap_or_else(|| {
|
||||
Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.add_modifier(Modifier::DIM)
|
||||
});
|
||||
|
||||
surface
|
||||
.get_mut(
|
||||
@@ -354,40 +441,6 @@ impl EditorView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// render gutters
|
||||
|
||||
let style: Style = theme.get("ui.linenr");
|
||||
let warning: Style = theme.get("warning");
|
||||
let error: Style = theme.get("error");
|
||||
let info: Style = theme.get("info");
|
||||
let hint: Style = theme.get("hint");
|
||||
|
||||
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(
|
||||
viewport.x - OFFSET,
|
||||
viewport.y + i as u16,
|
||||
"●",
|
||||
1,
|
||||
match diagnostic.severity {
|
||||
Some(Severity::Error) => error,
|
||||
Some(Severity::Warning) | None => warning,
|
||||
Some(Severity::Info) => info,
|
||||
Some(Severity::Hint) => hint,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
surface.set_stringn(
|
||||
viewport.x + 1 - OFFSET,
|
||||
viewport.y + i as u16,
|
||||
format!("{:>5}", line + 1),
|
||||
5,
|
||||
style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_diagnostics(
|
||||
@@ -447,6 +500,7 @@ impl EditorView {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_statusline(
|
||||
&self,
|
||||
doc: &Document,
|
||||
@@ -456,63 +510,91 @@ impl EditorView {
|
||||
theme: &Theme,
|
||||
is_focused: bool,
|
||||
) {
|
||||
//-------------------------------
|
||||
// Left side of the status line.
|
||||
//-------------------------------
|
||||
|
||||
let mode = match doc.mode() {
|
||||
Mode::Insert => "INS",
|
||||
Mode::Select => "SEL",
|
||||
Mode::Normal => "NOR",
|
||||
};
|
||||
let text_color = if is_focused {
|
||||
theme.get("ui.text.focus")
|
||||
let progress = doc
|
||||
.language_server()
|
||||
.and_then(|srv| {
|
||||
self.spinners
|
||||
.get(srv.id())
|
||||
.and_then(|spinner| spinner.frame())
|
||||
})
|
||||
.unwrap_or("");
|
||||
|
||||
let style = if is_focused {
|
||||
theme.get("ui.statusline")
|
||||
} else {
|
||||
theme.get("ui.text")
|
||||
theme.get("ui.statusline.inactive")
|
||||
};
|
||||
// statusline
|
||||
surface.set_style(
|
||||
Rect::new(viewport.x, viewport.y, viewport.width, 1),
|
||||
theme.get("ui.statusline"),
|
||||
);
|
||||
surface.set_style(Rect::new(viewport.x, viewport.y, viewport.width, 1), style);
|
||||
if is_focused {
|
||||
surface.set_string(viewport.x + 1, viewport.y, mode, text_color);
|
||||
surface.set_string(viewport.x + 1, viewport.y, mode, style);
|
||||
}
|
||||
surface.set_string(viewport.x + 5, viewport.y, progress, style);
|
||||
|
||||
if let Some(path) = doc.relative_path() {
|
||||
let path = path.to_string_lossy();
|
||||
|
||||
let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
|
||||
surface.set_stringn(
|
||||
viewport.x + 6,
|
||||
viewport.x + 8,
|
||||
viewport.y,
|
||||
title,
|
||||
viewport.width.saturating_sub(6) as usize,
|
||||
text_color,
|
||||
style,
|
||||
);
|
||||
}
|
||||
|
||||
surface.set_stringn(
|
||||
viewport.x + viewport.width.saturating_sub(15),
|
||||
viewport.y,
|
||||
format!("{}", doc.diagnostics().len()),
|
||||
4,
|
||||
text_color,
|
||||
//-------------------------------
|
||||
// Right side of the status line.
|
||||
//-------------------------------
|
||||
|
||||
// Compute the individual info strings.
|
||||
let diag_count = format!("{}", doc.diagnostics().len());
|
||||
// let indent_info = match doc.indent_style {
|
||||
// IndentStyle::Tabs => "tabs",
|
||||
// IndentStyle::Spaces(1) => "spaces:1",
|
||||
// IndentStyle::Spaces(2) => "spaces:2",
|
||||
// IndentStyle::Spaces(3) => "spaces:3",
|
||||
// IndentStyle::Spaces(4) => "spaces:4",
|
||||
// IndentStyle::Spaces(5) => "spaces:5",
|
||||
// IndentStyle::Spaces(6) => "spaces:6",
|
||||
// IndentStyle::Spaces(7) => "spaces:7",
|
||||
// IndentStyle::Spaces(8) => "spaces:8",
|
||||
// _ => "indent:ERROR",
|
||||
// };
|
||||
let position_info = {
|
||||
let pos = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
|
||||
format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
|
||||
};
|
||||
|
||||
// Render them to the status line together.
|
||||
let right_side_text = format!(
|
||||
"{} {} ",
|
||||
&diag_count[..diag_count.len().min(4)],
|
||||
// indent_info,
|
||||
position_info
|
||||
);
|
||||
|
||||
// render line:col
|
||||
let pos = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
|
||||
|
||||
let text = format!("{}:{}", pos.row + 1, pos.col + 1); // convert to 1-indexing
|
||||
let len = text.len();
|
||||
|
||||
let text_len = right_side_text.len() as u16;
|
||||
surface.set_string(
|
||||
viewport.x + viewport.width.saturating_sub(len as u16 + 1),
|
||||
viewport.x + viewport.width.saturating_sub(text_len),
|
||||
viewport.y,
|
||||
text,
|
||||
text_color,
|
||||
right_side_text,
|
||||
style,
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) {
|
||||
if let Some(command) = self.keymap[&Mode::Insert].get(&event) {
|
||||
command(cx);
|
||||
if let Some(command) = self.keymaps[&Mode::Insert].get(&event) {
|
||||
command.execute(cx);
|
||||
} else if let KeyEvent {
|
||||
code: KeyCode::Char(ch),
|
||||
..
|
||||
@@ -533,7 +615,7 @@ impl EditorView {
|
||||
// special handling for repeat operator
|
||||
key!('.') => {
|
||||
// first execute whatever put us into insert mode
|
||||
(self.last_insert.0)(cxt);
|
||||
self.last_insert.0.execute(cxt);
|
||||
// then replay the inputs
|
||||
for key in &self.last_insert.1 {
|
||||
self.insert_mode(cxt, *key)
|
||||
@@ -541,16 +623,16 @@ impl EditorView {
|
||||
}
|
||||
_ => {
|
||||
// set the count
|
||||
cxt._count = cxt.editor.count.take();
|
||||
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();
|
||||
cxt.selected_register = cxt.editor.selected_register.take();
|
||||
|
||||
if let Some(command) = self.keymap[&mode].get(&event) {
|
||||
command(cxt);
|
||||
if let Some(command) = self.keymaps[&mode].get(&event) {
|
||||
command.execute(cxt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,18 +660,19 @@ impl Component for EditorView {
|
||||
cx.editor.resize(Rect::new(0, 0, width, height - 1));
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(mut key) => {
|
||||
Event::Key(key) => {
|
||||
let mut key = KeyEvent::from(key);
|
||||
canonicalize_key(&mut key);
|
||||
// clear status
|
||||
cx.editor.status_msg = None;
|
||||
|
||||
let (view, doc) = cx.editor.current();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let mode = doc.mode();
|
||||
|
||||
let mut cxt = commands::Context {
|
||||
register: helix_view::RegisterSelection::default(),
|
||||
selected_register: helix_view::RegisterSelection::default(),
|
||||
editor: &mut cx.editor,
|
||||
_count: None,
|
||||
count: None,
|
||||
callback: None,
|
||||
on_next_key_callback: None,
|
||||
callbacks: cx.callbacks,
|
||||
@@ -652,7 +735,7 @@ impl Component for EditorView {
|
||||
return EventResult::Ignored;
|
||||
}
|
||||
|
||||
let (view, doc) = cx.editor.current();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
view.ensure_cursor_in_view(doc);
|
||||
|
||||
// mode transitions
|
||||
@@ -664,7 +747,7 @@ impl Component for EditorView {
|
||||
// how we entered insert mode is important, and we should track that so
|
||||
// we can repeat the side effect.
|
||||
|
||||
self.last_insert.0 = self.keymap[&mode][&key];
|
||||
self.last_insert.0 = self.keymaps[&mode][&key];
|
||||
self.last_insert.1.clear();
|
||||
}
|
||||
(Mode::Insert, Mode::Normal) => {
|
||||
@@ -716,15 +799,12 @@ impl Component for EditorView {
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
|
||||
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
|
||||
// match view.doc.mode() {
|
||||
// Mode::Insert => write!(stdout, "\x1B[6 q"),
|
||||
// mode => write!(stdout, "\x1B[2 q"),
|
||||
// };
|
||||
// return editor.cursor_position()
|
||||
|
||||
// It's easier to just not render the cursor and use selection rendering instead.
|
||||
None
|
||||
editor.cursor()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,31 +1,38 @@
|
||||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::Text,
|
||||
use tui::{buffer::Buffer as Surface, text::Text};
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use helix_core::{syntax, Position};
|
||||
use helix_view::{
|
||||
graphics::{Color, Rect, Style},
|
||||
Editor, Theme,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use helix_core::Position;
|
||||
use helix_view::{Editor, Theme};
|
||||
|
||||
pub struct Markdown {
|
||||
contents: String,
|
||||
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
}
|
||||
|
||||
// TODO: pre-render and self reference via Pin
|
||||
// better yet, just use Tendril + subtendril for references
|
||||
|
||||
impl Markdown {
|
||||
pub fn new(contents: String) -> Self {
|
||||
Self { contents }
|
||||
pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
|
||||
Self {
|
||||
contents,
|
||||
config_loader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
fn parse<'a>(
|
||||
contents: &'a str,
|
||||
theme: Option<&Theme>,
|
||||
loader: &syntax::Loader,
|
||||
) -> tui::text::Text<'a> {
|
||||
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||
use tui::text::{Span, Spans, Text};
|
||||
|
||||
@@ -79,9 +86,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
use helix_core::Rope;
|
||||
|
||||
let rope = Rope::from(text.as_ref());
|
||||
let syntax = syntax::LOADER
|
||||
.get()
|
||||
.unwrap()
|
||||
let syntax = loader
|
||||
.language_config_for_scope(&format!("source.{}", language))
|
||||
.and_then(|config| config.highlight_config(theme.scopes()))
|
||||
.map(|config| Syntax::new(&rope, config));
|
||||
@@ -101,15 +106,15 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
}
|
||||
HighlightEvent::Source { start, end } => {
|
||||
let style = match highlights.first() {
|
||||
Some(span) => {
|
||||
theme.get(theme.scopes()[span.0].as_str())
|
||||
}
|
||||
Some(span) => theme.get(&theme.scopes()[span.0]),
|
||||
None => text_style,
|
||||
};
|
||||
|
||||
// TODO: replace tabs with indentation
|
||||
|
||||
let mut slice = &text[start..end];
|
||||
// TODO: do we need to handle all unicode line endings
|
||||
// here, or is just '\n' okay?
|
||||
while let Some(end) = slice.find('\n') {
|
||||
// emit span up to newline
|
||||
let text = &slice[..end];
|
||||
@@ -157,7 +162,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
||||
}
|
||||
}
|
||||
Event::Code(text) | Event::Html(text) => {
|
||||
log::warn!("code {:?}", text);
|
||||
let mut span = to_span(text);
|
||||
span.style = code_style;
|
||||
spans.push(span);
|
||||
@@ -196,7 +200,7 @@ impl Component for Markdown {
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
use tui::widgets::{Paragraph, Widget, Wrap};
|
||||
|
||||
let text = parse(&self.contents, Some(&cx.editor.theme));
|
||||
let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
|
||||
|
||||
let par = Paragraph::new(text)
|
||||
.wrap(Wrap { trim: false })
|
||||
@@ -207,7 +211,7 @@ impl Component for Markdown {
|
||||
}
|
||||
|
||||
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||
let contents = parse(&self.contents, None);
|
||||
let contents = parse(&self.contents, None, &self.config_loader);
|
||||
let padding = 2;
|
||||
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
|
||||
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
|
||||
|
@@ -1,11 +1,6 @@
|
||||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::Table,
|
||||
};
|
||||
use tui::{buffer::Buffer as Surface, widgets::Table};
|
||||
|
||||
pub use tui::widgets::{Cell, Row};
|
||||
|
||||
@@ -15,7 +10,10 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
|
||||
use helix_core::Position;
|
||||
use helix_view::Editor;
|
||||
use helix_view::{
|
||||
graphics::{Color, Rect, Style},
|
||||
Editor,
|
||||
};
|
||||
|
||||
pub trait Item {
|
||||
// TODO: sort_text
|
||||
@@ -82,7 +80,7 @@ impl<T: Item> Menu<T> {
|
||||
let text = option.filter_text();
|
||||
// TODO: using fuzzy_indices could give us the char idx for match highlighting
|
||||
matcher
|
||||
.fuzzy_match(&text, pattern)
|
||||
.fuzzy_match(text, pattern)
|
||||
.map(|score| (index, score))
|
||||
}),
|
||||
);
|
||||
|
@@ -5,6 +5,7 @@ mod menu;
|
||||
mod picker;
|
||||
mod popup;
|
||||
mod prompt;
|
||||
mod spinner;
|
||||
mod text;
|
||||
|
||||
pub use completion::Completion;
|
||||
@@ -14,32 +15,34 @@ pub use menu::Menu;
|
||||
pub use picker::Picker;
|
||||
pub use popup::Popup;
|
||||
pub use prompt::{Prompt, PromptEvent};
|
||||
pub use spinner::{ProgressSpinners, Spinner};
|
||||
pub use text::Text;
|
||||
|
||||
pub use tui::layout::Rect;
|
||||
pub use tui::style::{Color, Modifier, Style};
|
||||
|
||||
use helix_core::regex::Regex;
|
||||
use helix_view::{Document, Editor, View};
|
||||
use helix_core::register::Registers;
|
||||
use helix_view::{
|
||||
graphics::{Color, Modifier, Rect, Style},
|
||||
Document, Editor, View,
|
||||
};
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn regex_prompt(
|
||||
cx: &mut crate::commands::Context,
|
||||
prompt: String,
|
||||
fun: impl Fn(&mut View, &mut Document, Regex) + 'static,
|
||||
fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static,
|
||||
) -> Prompt {
|
||||
let view_id = cx.view().id;
|
||||
let snapshot = cx.doc().selection(view_id).clone();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let view_id = view.id;
|
||||
let snapshot = doc.selection(view_id).clone();
|
||||
|
||||
Prompt::new(
|
||||
prompt,
|
||||
|input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
|
||||
move |editor: &mut Editor, input: &str, event: PromptEvent| {
|
||||
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
|
||||
match event {
|
||||
PromptEvent::Abort => {
|
||||
// TODO: also revert text
|
||||
let (view, doc) = editor.current();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
doc.set_selection(view.id, snapshot.clone());
|
||||
}
|
||||
PromptEvent::Validate => {
|
||||
@@ -53,13 +56,13 @@ pub fn regex_prompt(
|
||||
|
||||
match Regex::new(input) {
|
||||
Ok(regex) => {
|
||||
let (view, doc) = editor.current();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let registers = &mut cx.editor.registers;
|
||||
|
||||
// revert state to what it was before the last update
|
||||
// TODO: also revert text
|
||||
doc.set_selection(view.id, snapshot.clone());
|
||||
|
||||
fun(view, doc, regex);
|
||||
fun(view, doc, registers, regex);
|
||||
|
||||
view.ensure_cursor_in_view(doc);
|
||||
}
|
||||
@@ -73,11 +76,22 @@ pub fn regex_prompt(
|
||||
|
||||
pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
||||
use ignore::Walk;
|
||||
use std::time;
|
||||
let files = Walk::new(root.clone()).filter_map(|entry| match entry {
|
||||
Ok(entry) => {
|
||||
// filter dirs, but we might need special handling for symlinks!
|
||||
if !entry.file_type().map_or(false, |entry| entry.is_dir()) {
|
||||
Some(entry.into_path())
|
||||
let time = if let Ok(metadata) = entry.metadata() {
|
||||
metadata
|
||||
.accessed()
|
||||
.or_else(|_| metadata.modified())
|
||||
.or_else(|_| metadata.created())
|
||||
.unwrap_or(time::UNIX_EPOCH)
|
||||
} else {
|
||||
time::UNIX_EPOCH
|
||||
};
|
||||
|
||||
Some((entry.into_path(), time))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -85,13 +99,17 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
||||
Err(_err) => None,
|
||||
});
|
||||
|
||||
let files = if root.join(".git").is_dir() {
|
||||
let mut files: Vec<_> = if root.join(".git").is_dir() {
|
||||
files.collect()
|
||||
} else {
|
||||
const MAX: usize = 8192;
|
||||
files.take(MAX).collect()
|
||||
};
|
||||
|
||||
files.sort_by_key(|file| file.1);
|
||||
|
||||
let files = files.into_iter().map(|(path, _)| path).collect();
|
||||
|
||||
Picker::new(
|
||||
files,
|
||||
move |path: &PathBuf| {
|
||||
@@ -112,21 +130,91 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
||||
|
||||
pub mod completers {
|
||||
use crate::ui::prompt::Completion;
|
||||
use std::borrow::Cow;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use helix_view::theme;
|
||||
use std::cmp::Reverse;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
pub type Completer = fn(&str) -> Vec<Completion>;
|
||||
|
||||
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
|
||||
pub fn theme(input: &str) -> Vec<Completion> {
|
||||
let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
|
||||
names.extend(theme::Loader::read_names(
|
||||
&helix_core::config_dir().join("themes"),
|
||||
));
|
||||
names.push("default".into());
|
||||
|
||||
let mut names: Vec<_> = names
|
||||
.into_iter()
|
||||
.map(|name| ((0..), Cow::from(name)))
|
||||
.collect();
|
||||
|
||||
let matcher = Matcher::default();
|
||||
|
||||
let mut matches: Vec<_> = names
|
||||
.into_iter()
|
||||
.filter_map(|(range, name)| {
|
||||
matcher.fuzzy_match(&name, input).map(|score| (name, score))
|
||||
})
|
||||
.collect();
|
||||
|
||||
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
|
||||
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
|
||||
|
||||
names
|
||||
}
|
||||
|
||||
pub fn filename(input: &str) -> Vec<Completion> {
|
||||
filename_impl(input, |entry| {
|
||||
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
|
||||
|
||||
if is_dir {
|
||||
FileMatch::AcceptIncomplete
|
||||
} else {
|
||||
FileMatch::Accept
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn directory(input: &str) -> Vec<Completion> {
|
||||
filename_impl(input, |entry| {
|
||||
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
|
||||
|
||||
if is_dir {
|
||||
FileMatch::Accept
|
||||
} else {
|
||||
FileMatch::Reject
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum FileMatch {
|
||||
/// Entry should be ignored
|
||||
Reject,
|
||||
/// Entry is usable but can't be the end (for instance if the entry is a directory and we
|
||||
/// try to match a file)
|
||||
AcceptIncomplete,
|
||||
/// Entry is usable and can be the end of the match
|
||||
Accept,
|
||||
}
|
||||
|
||||
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
|
||||
fn filename_impl<F>(input: &str, filter_fn: F) -> Vec<Completion>
|
||||
where
|
||||
F: Fn(&ignore::DirEntry) -> FileMatch,
|
||||
{
|
||||
// Rust's filename handling is really annoying.
|
||||
|
||||
use ignore::WalkBuilder;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
let path = Path::new(input);
|
||||
let is_tilde = input.starts_with('~') && input.len() == 1;
|
||||
let path = helix_view::document::expand_tilde(Path::new(input));
|
||||
|
||||
let (dir, file_name) = if input.ends_with('/') {
|
||||
(path.into(), None)
|
||||
(path, None)
|
||||
} else {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
@@ -147,17 +235,33 @@ pub mod completers {
|
||||
.max_depth(Some(1))
|
||||
.build()
|
||||
.filter_map(|file| {
|
||||
file.ok().map(|entry| {
|
||||
file.ok().and_then(|entry| {
|
||||
let fmatch = filter_fn(&entry);
|
||||
|
||||
if fmatch == FileMatch::Reject {
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
|
||||
|
||||
let path = entry.path();
|
||||
let mut path = path.strip_prefix(&dir).unwrap_or(path).to_path_buf();
|
||||
let mut path = if is_tilde {
|
||||
// if it's a single tilde an absolute path is displayed so that when `TAB` is pressed on
|
||||
// one of the directories the tilde will be replaced with a valid path not with a relative
|
||||
// home directory name.
|
||||
// ~ -> <TAB> -> /home/user
|
||||
// ~/ -> <TAB> -> ~/first_entry
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
path.strip_prefix(&dir).unwrap_or(path).to_path_buf()
|
||||
};
|
||||
|
||||
if is_dir {
|
||||
if fmatch == FileMatch::AcceptIncomplete {
|
||||
path.push("");
|
||||
}
|
||||
|
||||
let path = path.to_str().unwrap().to_owned();
|
||||
(end.clone(), Cow::from(path))
|
||||
Some((end.clone(), Cow::from(path)))
|
||||
})
|
||||
}) // TODO: unwrap or skip
|
||||
.filter(|(_, path)| !path.is_empty()) // TODO
|
||||
@@ -165,10 +269,6 @@ pub mod completers {
|
||||
|
||||
// if empty, return a list of dirs and files in current dir
|
||||
if let Some(file_name) = file_name {
|
||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use std::cmp::Reverse;
|
||||
|
||||
let matcher = Matcher::default();
|
||||
|
||||
// inefficient, but we need to calculate the scores, filter out None, then sort.
|
||||
@@ -181,7 +281,7 @@ pub mod completers {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let range = ((input.len() - file_name.len())..);
|
||||
let range = ((input.len().saturating_sub(file_name.len()))..);
|
||||
|
||||
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
|
||||
files = matches
|
||||
|
@@ -2,8 +2,6 @@ use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::{Block, BorderType, Borders},
|
||||
};
|
||||
|
||||
@@ -14,8 +12,11 @@ use std::borrow::Cow;
|
||||
|
||||
use crate::ui::{Prompt, PromptEvent};
|
||||
use helix_core::Position;
|
||||
use helix_view::editor::Action;
|
||||
use helix_view::Editor;
|
||||
use helix_view::{
|
||||
editor::Action,
|
||||
graphics::{Color, CursorKind, Rect, Style},
|
||||
Editor,
|
||||
};
|
||||
|
||||
pub struct Picker<T> {
|
||||
options: Vec<T>,
|
||||
@@ -23,6 +24,8 @@ pub struct Picker<T> {
|
||||
matcher: Box<Matcher>,
|
||||
/// (index, score)
|
||||
matches: Vec<(usize, i64)>,
|
||||
/// Filter over original options.
|
||||
filters: Vec<usize>, // could be optimized into bit but not worth it now
|
||||
|
||||
cursor: usize,
|
||||
// pattern: String,
|
||||
@@ -41,7 +44,7 @@ impl<T> Picker<T> {
|
||||
let prompt = Prompt::new(
|
||||
"".to_string(),
|
||||
|pattern: &str| Vec::new(),
|
||||
|editor: &mut Editor, pattern: &str, event: PromptEvent| {
|
||||
|editor: &mut Context, pattern: &str, event: PromptEvent| {
|
||||
//
|
||||
},
|
||||
);
|
||||
@@ -50,6 +53,7 @@ impl<T> Picker<T> {
|
||||
options,
|
||||
matcher: Box::new(Matcher::default()),
|
||||
matches: Vec::new(),
|
||||
filters: Vec::new(),
|
||||
cursor: 0,
|
||||
prompt,
|
||||
format_fn: Box::new(format_fn),
|
||||
@@ -68,6 +72,7 @@ impl<T> Picker<T> {
|
||||
ref mut options,
|
||||
ref mut matcher,
|
||||
ref mut matches,
|
||||
ref filters,
|
||||
ref format_fn,
|
||||
..
|
||||
} = *self;
|
||||
@@ -81,6 +86,10 @@ impl<T> Picker<T> {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, option)| {
|
||||
// filter options first before matching
|
||||
if !filters.is_empty() {
|
||||
filters.binary_search(&index).ok()?;
|
||||
}
|
||||
// TODO: maybe using format_fn isn't the best idea here
|
||||
let text = (format_fn)(option);
|
||||
// TODO: using fuzzy_indices could give us the char idx for match highlighting
|
||||
@@ -114,6 +123,14 @@ impl<T> Picker<T> {
|
||||
.get(self.cursor)
|
||||
.map(|(index, _score)| &self.options[*index])
|
||||
}
|
||||
|
||||
pub fn save_filter(&mut self) {
|
||||
self.filters.clear();
|
||||
self.filters
|
||||
.extend(self.matches.iter().map(|(index, _)| *index));
|
||||
self.filters.sort_unstable(); // used for binary search later
|
||||
self.prompt.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// process:
|
||||
@@ -205,6 +222,12 @@ impl<T: 'static> Component for Picker<T> {
|
||||
}
|
||||
return close_fn;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char(' '),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
self.save_filter();
|
||||
}
|
||||
_ => {
|
||||
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
|
||||
// TODO: recalculate only if pattern changed
|
||||
@@ -233,8 +256,6 @@ impl<T: 'static> Component for Picker<T> {
|
||||
let inner = block.inner(area);
|
||||
|
||||
block.render(area, surface);
|
||||
// TODO: abstract into a clear(area) fn
|
||||
// surface.set_style(inner, Style::default().bg(Color::Rgb(150, 50, 0)));
|
||||
|
||||
// -- Render the input bar:
|
||||
|
||||
@@ -268,21 +289,22 @@ impl<T: 'static> Component for Picker<T> {
|
||||
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
|
||||
}
|
||||
|
||||
surface.set_stringn(
|
||||
surface.set_string_truncated(
|
||||
inner.x + 3,
|
||||
inner.y + 2 + i as u16,
|
||||
(self.format_fn)(option),
|
||||
inner.width as usize - 1,
|
||||
(inner.width as usize).saturating_sub(3), // account for the " > "
|
||||
if i == (self.cursor - offset) {
|
||||
selected
|
||||
} else {
|
||||
style
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
|
||||
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
|
||||
// TODO: this is mostly duplicate code
|
||||
let area = inner_rect(area);
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
@@ -292,6 +314,6 @@ impl<T: 'static> Component for Picker<T> {
|
||||
// prompt area
|
||||
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
|
||||
|
||||
self.prompt.cursor_position(area, editor)
|
||||
self.prompt.cursor(area, editor)
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,14 @@
|
||||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use helix_core::Position;
|
||||
use helix_view::Editor;
|
||||
use helix_view::{
|
||||
graphics::{Color, Rect, Style},
|
||||
Editor,
|
||||
};
|
||||
|
||||
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
|
||||
// a width/height hint. maybe Popup(Box<Component>)
|
||||
@@ -116,7 +115,7 @@ impl<T: Component> Component for Popup<T> {
|
||||
|
||||
let position = self
|
||||
.position
|
||||
.or_else(|| cx.editor.cursor_position())
|
||||
.or_else(|| cx.editor.cursor().0)
|
||||
.unwrap_or_default();
|
||||
|
||||
let (width, height) = self.size;
|
||||
|
@@ -1,9 +1,18 @@
|
||||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crate::ui;
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use helix_core::Position;
|
||||
use helix_view::{Editor, Theme};
|
||||
use std::{borrow::Cow, ops::RangeFrom};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
use helix_core::{
|
||||
unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
|
||||
unicode::width::UnicodeWidthStr,
|
||||
Position,
|
||||
};
|
||||
use helix_view::{
|
||||
graphics::{Color, CursorKind, Margin, Modifier, Rect, Style},
|
||||
Editor, Theme,
|
||||
};
|
||||
|
||||
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
|
||||
|
||||
@@ -14,7 +23,7 @@ pub struct Prompt {
|
||||
completion: Vec<Completion>,
|
||||
selection: Option<usize>,
|
||||
completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>,
|
||||
callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>,
|
||||
callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>,
|
||||
pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
|
||||
}
|
||||
|
||||
@@ -33,11 +42,22 @@ pub enum CompletionDirection {
|
||||
Backward,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Movement {
|
||||
BackwardChar(usize),
|
||||
BackwardWord(usize),
|
||||
ForwardChar(usize),
|
||||
ForwardWord(usize),
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
pub fn new(
|
||||
prompt: String,
|
||||
mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static,
|
||||
callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static,
|
||||
callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
prompt,
|
||||
@@ -51,21 +71,120 @@ impl Prompt {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the cursor position after applying movement
|
||||
/// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
|
||||
fn eval_movement(&self, movement: Movement) -> usize {
|
||||
match movement {
|
||||
Movement::BackwardChar(rep) => {
|
||||
let mut position = self.cursor;
|
||||
for _ in 0..rep {
|
||||
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
|
||||
if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
|
||||
position = pos;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
position
|
||||
}
|
||||
Movement::BackwardWord(rep) => {
|
||||
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
|
||||
if char_indices.is_empty() {
|
||||
return self.cursor;
|
||||
}
|
||||
let mut char_position = char_indices
|
||||
.iter()
|
||||
.position(|(idx, _)| *idx == self.cursor)
|
||||
.unwrap_or(char_indices.len() - 1);
|
||||
|
||||
for _ in 0..rep {
|
||||
if char_position == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut found = None;
|
||||
for prev in (0..char_position - 1).rev() {
|
||||
if char_indices[prev].1.is_whitespace() {
|
||||
found = Some(prev + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
char_position = found.unwrap_or(0);
|
||||
}
|
||||
char_indices[char_position].0
|
||||
}
|
||||
Movement::ForwardWord(rep) => {
|
||||
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
|
||||
if char_indices.is_empty() {
|
||||
return self.cursor;
|
||||
}
|
||||
let mut char_position = char_indices
|
||||
.iter()
|
||||
.position(|(idx, _)| *idx == self.cursor)
|
||||
.unwrap_or_else(|| char_indices.len());
|
||||
|
||||
for _ in 0..rep {
|
||||
// Skip any non-whitespace characters
|
||||
while char_position < char_indices.len()
|
||||
&& !char_indices[char_position].1.is_whitespace()
|
||||
{
|
||||
char_position += 1;
|
||||
}
|
||||
|
||||
// Skip any whitespace characters
|
||||
while char_position < char_indices.len()
|
||||
&& char_indices[char_position].1.is_whitespace()
|
||||
{
|
||||
char_position += 1;
|
||||
}
|
||||
|
||||
// We are now on the start of the next word
|
||||
}
|
||||
char_indices
|
||||
.get(char_position)
|
||||
.map(|(i, _)| *i)
|
||||
.unwrap_or_else(|| self.line.len())
|
||||
}
|
||||
Movement::ForwardChar(rep) => {
|
||||
let mut position = self.cursor;
|
||||
for _ in 0..rep {
|
||||
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
|
||||
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
|
||||
position = pos;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
position
|
||||
}
|
||||
Movement::StartOfLine => 0,
|
||||
Movement::EndOfLine => {
|
||||
let mut cursor =
|
||||
GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
|
||||
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
|
||||
pos
|
||||
} else {
|
||||
self.cursor
|
||||
}
|
||||
}
|
||||
Movement::None => self.cursor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
self.line.insert(self.cursor, c);
|
||||
self.cursor += 1;
|
||||
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
|
||||
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
|
||||
self.cursor = pos;
|
||||
}
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
self.exit_selection();
|
||||
}
|
||||
|
||||
pub fn move_char_left(&mut self) {
|
||||
self.cursor = self.cursor.saturating_sub(1)
|
||||
}
|
||||
|
||||
pub fn move_char_right(&mut self) {
|
||||
if self.cursor < self.line.len() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
pub fn move_cursor(&mut self, movement: Movement) {
|
||||
let pos = self.eval_movement(movement);
|
||||
self.cursor = pos
|
||||
}
|
||||
|
||||
pub fn move_start(&mut self) {
|
||||
@@ -77,11 +196,35 @@ impl Prompt {
|
||||
}
|
||||
|
||||
pub fn delete_char_backwards(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.line.remove(self.cursor - 1);
|
||||
self.cursor -= 1;
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
}
|
||||
let pos = self.eval_movement(Movement::BackwardChar(1));
|
||||
self.line.replace_range(pos..self.cursor, "");
|
||||
self.cursor = pos;
|
||||
|
||||
self.exit_selection();
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
}
|
||||
|
||||
pub fn delete_word_backwards(&mut self) {
|
||||
let pos = self.eval_movement(Movement::BackwardWord(1));
|
||||
self.line.replace_range(pos..self.cursor, "");
|
||||
self.cursor = pos;
|
||||
|
||||
self.exit_selection();
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
}
|
||||
|
||||
pub fn kill_to_end_of_line(&mut self) {
|
||||
let pos = self.eval_movement(Movement::EndOfLine);
|
||||
self.line.replace_range(self.cursor..pos, "");
|
||||
|
||||
self.exit_selection();
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.line.clear();
|
||||
self.cursor = 0;
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
self.exit_selection();
|
||||
}
|
||||
|
||||
@@ -111,12 +254,6 @@ impl Prompt {
|
||||
}
|
||||
}
|
||||
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
const BASE_WIDTH: u16 = 30;
|
||||
|
||||
impl Prompt {
|
||||
@@ -139,7 +276,7 @@ impl Prompt {
|
||||
|
||||
let height = ((self.completion.len() as u16 + cols - 1) / cols)
|
||||
.min(10) // at most 10 rows (or less)
|
||||
.min(area.height);
|
||||
.min(area.height.saturating_sub(1));
|
||||
|
||||
let completion_area = Rect::new(
|
||||
area.x,
|
||||
@@ -152,17 +289,21 @@ impl Prompt {
|
||||
let area = completion_area;
|
||||
let background = theme.get("ui.statusline");
|
||||
|
||||
let items = height as usize * cols as usize;
|
||||
|
||||
let offset = self
|
||||
.selection
|
||||
.map(|selection| selection / items * items)
|
||||
.unwrap_or_default();
|
||||
|
||||
surface.clear_with(area, background);
|
||||
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
|
||||
// TODO: paginate
|
||||
for (i, (_range, completion)) in self
|
||||
.completion
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(height as usize * cols as usize)
|
||||
for (i, (_range, completion)) in
|
||||
self.completion.iter().enumerate().skip(offset).take(items)
|
||||
{
|
||||
let color = if Some(i) == self.selection {
|
||||
// Style::default().bg(Color::Rgb(104, 60, 232))
|
||||
@@ -191,7 +332,7 @@ impl Prompt {
|
||||
let viewport = area;
|
||||
let area = viewport.intersection(Rect::new(
|
||||
completion_area.x,
|
||||
completion_area.y - 3,
|
||||
completion_area.y.saturating_sub(3),
|
||||
BASE_WIDTH * 3,
|
||||
3,
|
||||
));
|
||||
@@ -199,7 +340,6 @@ impl Prompt {
|
||||
let background = theme.get("ui.help");
|
||||
surface.clear_with(area, background);
|
||||
|
||||
use tui::layout::Margin;
|
||||
text.render(
|
||||
area.inner(&Margin {
|
||||
vertical: 1,
|
||||
@@ -246,36 +386,80 @@ impl Component for Prompt {
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
} => {
|
||||
self.insert_char(c);
|
||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
} => {
|
||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
|
||||
return close_fn;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('f'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Right,
|
||||
..
|
||||
} => self.move_char_right(),
|
||||
} => self.move_cursor(Movement::ForwardChar(1)),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('b'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
..
|
||||
} => self.move_char_left(),
|
||||
} => self.move_cursor(Movement::BackwardChar(1)),
|
||||
KeyEvent {
|
||||
code: KeyCode::End,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('e'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_end(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Home,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_start(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
modifiers: KeyModifiers::ALT,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('b'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
} => self.move_cursor(Movement::BackwardWord(1)),
|
||||
KeyEvent {
|
||||
code: KeyCode::Right,
|
||||
modifiers: KeyModifiers::ALT,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('f'),
|
||||
modifiers: KeyModifiers::ALT,
|
||||
} => self.move_cursor(Movement::ForwardWord(1)),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.delete_word_backwards(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('k'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.kill_to_end_of_line(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
} => {
|
||||
self.delete_char_backwards();
|
||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
@@ -285,7 +469,7 @@ impl Component for Prompt {
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
self.exit_selection();
|
||||
} else {
|
||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Validate);
|
||||
return close_fn;
|
||||
}
|
||||
}
|
||||
@@ -310,11 +494,16 @@ impl Component for Prompt {
|
||||
self.render_prompt(area, surface, cx)
|
||||
}
|
||||
|
||||
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
|
||||
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
|
||||
let line = area.height as usize - 1;
|
||||
Some(Position::new(
|
||||
area.y as usize + line,
|
||||
area.x as usize + self.prompt.len() + self.cursor,
|
||||
))
|
||||
(
|
||||
Some(Position::new(
|
||||
area.y as usize + line,
|
||||
area.x as usize
|
||||
+ self.prompt.len()
|
||||
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
|
||||
)),
|
||||
CursorKind::Block,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
75
helix-term/src/ui/spinner.rs
Normal file
75
helix-term/src/ui/spinner.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::{collections::HashMap, time::SystemTime};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ProgressSpinners {
|
||||
inner: HashMap<usize, Spinner>,
|
||||
}
|
||||
|
||||
impl ProgressSpinners {
|
||||
pub fn get(&self, id: usize) -> Option<&Spinner> {
|
||||
self.inner.get(&id)
|
||||
}
|
||||
|
||||
pub fn get_or_create(&mut self, id: usize) -> &mut Spinner {
|
||||
self.inner.entry(id).or_insert_with(Spinner::default)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Spinner {
|
||||
fn default() -> Self {
|
||||
Self::dots(80)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Spinner {
|
||||
frames: Vec<&'static str>,
|
||||
count: usize,
|
||||
start: Option<SystemTime>,
|
||||
interval: u64,
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
/// Creates a new spinner with `frames` and `interval`.
|
||||
/// Expects the frames count and interval to be greater than 0.
|
||||
pub fn new(frames: Vec<&'static str>, interval: u64) -> Self {
|
||||
let count = frames.len();
|
||||
assert!(count > 0);
|
||||
assert!(interval > 0);
|
||||
|
||||
Self {
|
||||
frames,
|
||||
count,
|
||||
interval,
|
||||
start: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dots(interval: u64) -> Self {
|
||||
Self::new(vec!["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], interval)
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.start = Some(SystemTime::now());
|
||||
}
|
||||
|
||||
pub fn frame(&self) -> Option<&str> {
|
||||
let idx = (self
|
||||
.start
|
||||
.map(|time| SystemTime::now().duration_since(time))?
|
||||
.ok()?
|
||||
.as_millis()
|
||||
/ self.interval as u128) as usize
|
||||
% self.count;
|
||||
|
||||
self.frames.get(idx).copied()
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.start = None;
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
self.start.is_none()
|
||||
}
|
||||
}
|
@@ -1,15 +1,14 @@
|
||||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use helix_core::Position;
|
||||
use helix_view::Editor;
|
||||
use helix_view::{
|
||||
graphics::{Color, Rect, Style},
|
||||
Editor,
|
||||
};
|
||||
|
||||
pub struct Text {
|
||||
contents: String,
|
||||
|
@@ -1,12 +1,16 @@
|
||||
[package]
|
||||
name = "helix-tui"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
categories = ["editor"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
homepage = "https://helix-editor.com"
|
||||
include = ["src/**/*", "README.md"]
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
@@ -16,5 +20,7 @@ bitflags = "1.0"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
crossterm = { version = "0.19", optional = true }
|
||||
crossterm = { version = "0.20", optional = true }
|
||||
serde = { version = "1", "optional" = true, features = ["derive"]}
|
||||
helix-view = { version = "0.3", path = "../helix-view", features = ["term"] }
|
||||
helix-core = { version = "0.3", path = "../helix-core" }
|
||||
|
@@ -1,11 +1,6 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
use crate::{backend::Backend, buffer::Cell};
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
cursor::{CursorShape, Hide, MoveTo, SetCursorShape, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
@@ -13,6 +8,7 @@ use crossterm::{
|
||||
},
|
||||
terminal::{self, Clear, ClearType},
|
||||
};
|
||||
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
@@ -93,8 +89,14 @@ where
|
||||
map_error(execute!(self.buffer, Hide))
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Show))
|
||||
fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
|
||||
let shape = match kind {
|
||||
CursorKind::Block => CursorShape::Block,
|
||||
CursorKind::Bar => CursorShape::Line,
|
||||
CursorKind::Underline => CursorShape::UnderScore,
|
||||
CursorKind::Hidden => unreachable!(),
|
||||
};
|
||||
map_error(execute!(self.buffer, Show, SetCursorShape(shape)))
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
@@ -126,32 +128,6 @@ fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::Grey,
|
||||
Color::DarkGray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
|
@@ -1,7 +1,8 @@
|
||||
use std::io;
|
||||
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
@@ -16,7 +17,7 @@ pub trait Backend {
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn show_cursor(&mut self, kind: CursorKind) -> Result<(), io::Error>;
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
};
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use std::{fmt::Write, io};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -122,7 +122,7 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn show_cursor(&mut self, _kind: CursorKind) -> Result<(), io::Error> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -1,12 +1,10 @@
|
||||
use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
};
|
||||
use crate::text::{Span, Spans};
|
||||
use std::cmp::min;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use helix_view::graphics::{Color, Modifier, Rect, Style};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Cell {
|
||||
@@ -89,8 +87,7 @@ impl Default for Cell {
|
||||
///
|
||||
/// ```
|
||||
/// use helix_tui::buffer::{Buffer, Cell};
|
||||
/// use helix_tui::layout::Rect;
|
||||
/// use helix_tui::style::{Color, Style, Modifier};
|
||||
/// use helix_view::graphics::{Rect, Color, Style, Modifier};
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
@@ -193,7 +190,7 @@ impl Buffer {
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_tui::layout::Rect;
|
||||
/// # use helix_view::graphics::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
@@ -203,16 +200,6 @@ impl Buffer {
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_tui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
@@ -235,7 +222,7 @@ impl Buffer {
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_tui::layout::Rect;
|
||||
/// # use helix_view::graphics::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
@@ -245,15 +232,6 @@ impl Buffer {
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_tui::layout::Rect;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
@@ -285,11 +263,30 @@ impl Buffer {
|
||||
width: usize,
|
||||
style: Style,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.set_string_truncated(x, y, string, width, style, false)
|
||||
}
|
||||
|
||||
/// Print at most the first `width` characters of a string if enough space is available
|
||||
/// until the end of the line. If `markend` is true appends a `…` at the end of
|
||||
/// truncated lines.
|
||||
pub fn set_string_truncated<S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
y: u16,
|
||||
string: S,
|
||||
width: usize,
|
||||
style: Style,
|
||||
ellipsis: bool,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut index = self.index_of(x, y);
|
||||
let mut x_offset = x as usize;
|
||||
let width = if ellipsis { width - 1 } else { width };
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
@@ -312,6 +309,9 @@ impl Buffer {
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
|
||||
self.content[index].set_symbol("…");
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
@@ -510,6 +510,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
#[cfg(debug_assertions)]
|
||||
fn pos_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
@@ -520,6 +521,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
#[cfg(debug_assertions)]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
@@ -1,11 +1,12 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cassowary::strength::{REQUIRED, WEAK};
|
||||
use cassowary::WeightedRelation::*;
|
||||
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
|
||||
|
||||
use helix_view::graphics::{Margin, Rect};
|
||||
|
||||
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
TopLeft,
|
||||
@@ -45,12 +46,6 @@ impl Constraint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Margin {
|
||||
pub vertical: u16,
|
||||
pub horizontal: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
@@ -119,7 +114,8 @@ impl Layout {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use helix_tui::layout::{Rect, Constraint, Direction, Layout};
|
||||
/// # use helix_tui::layout::{Constraint, Direction, Layout};
|
||||
/// # use helix_view::graphics::Rect;
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
@@ -348,117 +344,6 @@ impl Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16.
|
||||
/// If clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
let max_area = u16::max_value();
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(self) -> u16 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
pub fn left(self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
pub fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
pub fn top(self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
|
||||
Rect::default()
|
||||
} else {
|
||||
Rect {
|
||||
x: self.x + margin.horizontal,
|
||||
y: self.y + margin.vertical,
|
||||
width: self.width - 2 * margin.horizontal,
|
||||
height: self.height - 2 * margin.vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.x + self.width, other.x + other.width);
|
||||
let y2 = max(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(self, other: Rect) -> Rect {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.x + self.width, other.x + other.width);
|
||||
let y2 = min(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects(self, other: Rect) -> bool {
|
||||
self.x < other.x + other.width
|
||||
&& self.x + self.width > other.x
|
||||
&& self.y < other.y + other.height
|
||||
&& self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -487,48 +372,4 @@ mod tests {
|
||||
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
|
||||
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
}
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@
|
||||
//! implement your own.
|
||||
//!
|
||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
|
||||
//! to customize them. The widget is then rendered using the `Frame::render_widget` which take
|
||||
//! your widget instance an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
@@ -125,7 +125,6 @@
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
pub mod terminal;
|
||||
pub mod text;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::{backend::Backend, buffer::Buffer, layout::Rect};
|
||||
use crate::{backend::Backend, buffer::Buffer};
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -57,7 +58,7 @@ where
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
if let Err(err) = self.show_cursor(CursorKind::Block) {
|
||||
eprintln!("Failed to show the cursor: {}", err);
|
||||
}
|
||||
}
|
||||
@@ -147,7 +148,11 @@ where
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw(&mut self, cursor_position: Option<(u16, u16)>) -> io::Result<()> {
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
cursor_kind: CursorKind,
|
||||
) -> io::Result<()> {
|
||||
// // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// // and the terminal (if growing), which may OOB.
|
||||
// self.autoresize()?;
|
||||
@@ -162,12 +167,13 @@ where
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some((x, y)) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
if let Some((x, y)) = cursor_position {
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
|
||||
match cursor_kind {
|
||||
CursorKind::Hidden => self.hide_cursor()?,
|
||||
kind => self.show_cursor(kind)?,
|
||||
}
|
||||
|
||||
// Swap buffers
|
||||
@@ -185,8 +191,8 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
pub fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
|
||||
self.backend.show_cursor(kind)?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@
|
||||
//! ```rust
|
||||
//! # use helix_tui::widgets::Block;
|
||||
//! # use helix_tui::text::{Span, Spans};
|
||||
//! # use helix_tui::style::{Color, Style};
|
||||
//! # use helix_view::graphics::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
@@ -46,7 +46,8 @@
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
//! ```
|
||||
use crate::style::Style;
|
||||
use helix_core::line_ending::str_is_line_ending;
|
||||
use helix_view::graphics::Style;
|
||||
use std::borrow::Cow;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -91,7 +92,7 @@ impl<'a> Span<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::Span;
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Span::styled("My text", style);
|
||||
/// Span::styled(String::from("My text"), style);
|
||||
@@ -120,7 +121,7 @@ impl<'a> Span<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::{Span, StyledGrapheme};
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let style = Style::default().fg(Color::Yellow);
|
||||
/// let span = Span::styled("Text", style);
|
||||
@@ -177,7 +178,7 @@ impl<'a> Span<'a> {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
.filter(|s| !str_is_line_ending(s.symbol))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +211,7 @@ impl<'a> Spans<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::{Span, Spans};
|
||||
/// # use helix_tui::style::{Color, Style};
|
||||
/// # use helix_view::graphics::{Color, Style};
|
||||
/// let spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
@@ -264,7 +265,7 @@ impl<'a> From<Spans<'a>> for String {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::Text;
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
@@ -318,7 +319,7 @@ impl<'a> Text<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::Text;
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
@@ -368,7 +369,7 @@ impl<'a> Text<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::Text;
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
|
@@ -1,11 +1,10 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols::line,
|
||||
text::{Span, Spans},
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
use helix_view::graphics::{Rect, Style};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum BorderType {
|
||||
@@ -33,7 +32,7 @@ impl BorderType {
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::widgets::{Block, BorderType, Borders};
|
||||
/// # use helix_tui::style::{Style, Color};
|
||||
/// # use helix_view::graphics::{Style, Color};
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
@@ -212,7 +211,6 @@ impl<'a> Widget for Block<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_borders() {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//! `widgets` is a collection of types that implement [`Widget`].
|
||||
//!
|
||||
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
||||
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
@@ -20,9 +20,11 @@ pub use self::block::{Block, BorderType};
|
||||
pub use self::paragraph::{Paragraph, Wrap};
|
||||
pub use self::table::{Cell, Row, Table, TableState};
|
||||
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
use crate::buffer::Buffer;
|
||||
use bitflags::bitflags;
|
||||
|
||||
use helix_view::graphics::Rect;
|
||||
|
||||
bitflags! {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
pub struct Borders: u32 {
|
||||
|
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Style,
|
||||
layout::Alignment,
|
||||
text::{StyledGrapheme, Text},
|
||||
widgets::{
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper},
|
||||
Block, Widget,
|
||||
},
|
||||
};
|
||||
use helix_view::graphics::{Rect, Style};
|
||||
use std::iter;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -26,8 +26,8 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
|
||||
/// ```
|
||||
/// # use helix_tui::text::{Text, Spans, Span};
|
||||
/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
/// # use helix_tui::style::{Style, Color, Modifier};
|
||||
/// # use helix_tui::layout::{Alignment};
|
||||
/// # use helix_view::graphics::{Style, Color, Modifier};
|
||||
/// let text = vec![
|
||||
/// Spans::from(vec![
|
||||
/// Span::raw("First"),
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::text::StyledGrapheme;
|
||||
use helix_core::line_ending::str_is_line_ending;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -62,13 +63,13 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width
|
||||
// Skip leading whitespace when trim is enabled.
|
||||
|| self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
|
||||
|| self.trim && symbol_whitespace && !str_is_line_ending(symbol) && current_line_width == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
if str_is_line_ending(symbol) {
|
||||
if prev_whitespace {
|
||||
current_line_width = width_to_last_word_end;
|
||||
self.current_line.truncate(symbols_to_last_word_end);
|
||||
@@ -170,7 +171,7 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
if str_is_line_ending(symbol) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -199,7 +200,7 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
||||
|
||||
if skip_rest {
|
||||
for StyledGrapheme { symbol, .. } in &mut self.symbols {
|
||||
if symbol == "\n" {
|
||||
if str_is_line_ending(symbol) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
layout::Constraint,
|
||||
text::Text,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
@@ -10,10 +9,8 @@ use cassowary::{
|
||||
WeightedRelation::*,
|
||||
{Expression, Solver},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
iter::{self, Iterator},
|
||||
};
|
||||
use helix_view::graphics::{Rect, Style};
|
||||
use std::collections::HashMap;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
@@ -21,8 +18,8 @@ use unicode_width::UnicodeWidthStr;
|
||||
/// It can be created from anything that can be converted to a [`Text`].
|
||||
/// ```rust
|
||||
/// # use helix_tui::widgets::Cell;
|
||||
/// # use helix_tui::style::{Style, Modifier};
|
||||
/// # use helix_tui::text::{Span, Spans, Text};
|
||||
/// # use helix_view::graphics::{Style, Modifier};
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// Cell::from(Span::from("span"));
|
||||
@@ -74,7 +71,7 @@ where
|
||||
/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
|
||||
/// ```rust
|
||||
/// # use helix_tui::widgets::{Row, Cell};
|
||||
/// # use helix_tui::style::{Style, Color};
|
||||
/// # use helix_view::graphics::{Style, Color};
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
@@ -137,7 +134,7 @@ impl<'a> Row<'a> {
|
||||
/// ```rust
|
||||
/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
|
||||
/// # use helix_tui::layout::Constraint;
|
||||
/// # use helix_tui::style::{Style, Color, Modifier};
|
||||
/// # use helix_view::graphics::{Style, Color, Modifier};
|
||||
/// # use helix_tui::text::{Text, Spans, Span};
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
@@ -415,9 +412,7 @@ impl<'a> Table<'a> {
|
||||
let has_selection = state.selected.is_some();
|
||||
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = iter::repeat(" ")
|
||||
.take(highlight_symbol.width())
|
||||
.collect::<String>();
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
let mut current_height = 0;
|
||||
let mut rows_height = table_area.height;
|
||||
|
||||
|
@@ -1,31 +1,42 @@
|
||||
[package]
|
||||
name = "helix-view"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
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
|
||||
description = "UI abstractions for use in backends"
|
||||
categories = ["editor"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
homepage = "https://helix-editor.com"
|
||||
|
||||
[features]
|
||||
term = ["tui", "crossterm"]
|
||||
default = ["term"]
|
||||
default = []
|
||||
term = ["crossterm"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
anyhow = "1"
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-lsp = { path = "../helix-lsp"}
|
||||
helix-core = { version = "0.3", path = "../helix-core" }
|
||||
helix-lsp = { version = "0.3", path = "../helix-lsp"}
|
||||
crossterm = { version = "0.20", optional = true }
|
||||
|
||||
# Conversion traits
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
|
||||
crossterm = { version = "0.19", features = ["event-stream"], optional = true }
|
||||
once_cell = "1.4"
|
||||
once_cell = "1.8"
|
||||
url = "2"
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
|
||||
slotmap = "1"
|
||||
|
||||
encoding_rs = "0.8"
|
||||
chardetng = "0.1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.5"
|
||||
log = "~0.4"
|
||||
|
||||
which = "4.1"
|
||||
|
||||
[dev-dependencies]
|
||||
helix-tui = { path = "../helix-tui" }
|
||||
|
193
helix-view/src/clipboard.rs
Normal file
193
helix-view/src/clipboard.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
|
||||
|
||||
use anyhow::Result;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub trait ClipboardProvider: std::fmt::Debug {
|
||||
fn name(&self) -> Cow<str>;
|
||||
fn get_contents(&self) -> Result<String>;
|
||||
fn set_contents(&self, contents: String) -> Result<()>;
|
||||
}
|
||||
|
||||
macro_rules! command_provider {
|
||||
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
|
||||
Box::new(provider::CommandProvider {
|
||||
get_cmd: provider::CommandConfig {
|
||||
prg: $get_prg,
|
||||
args: &[ $( $get_arg ),* ],
|
||||
},
|
||||
set_cmd: provider::CommandConfig {
|
||||
prg: $set_prg,
|
||||
args: &[ $( $set_arg ),* ],
|
||||
},
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
// TODO: support for user-defined provider, probably when we have plugin support by setting a
|
||||
// variable?
|
||||
|
||||
if exists("pbcopy") && exists("pbpaste") {
|
||||
command_provider! {
|
||||
paste => "pbpaste";
|
||||
copy => "pbcopy";
|
||||
}
|
||||
} else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
|
||||
command_provider! {
|
||||
paste => "wl-paste", "--no-newline";
|
||||
copy => "wl-copy", "--type", "text/plain";
|
||||
}
|
||||
} else if env_var_is_set("DISPLAY") && exists("xclip") {
|
||||
command_provider! {
|
||||
paste => "xclip", "-o", "-selection", "clipboard";
|
||||
copy => "xclip", "-i", "-selection", "clipboard";
|
||||
}
|
||||
} else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
|
||||
{
|
||||
// FIXME: check performance of is_exit_success
|
||||
command_provider! {
|
||||
paste => "xsel", "-o", "-b";
|
||||
copy => "xsel", "--nodetach", "-i", "-b";
|
||||
}
|
||||
} else if exists("lemonade") {
|
||||
command_provider! {
|
||||
paste => "lemonade", "paste";
|
||||
copy => "lemonade", "copy";
|
||||
}
|
||||
} else if exists("doitclient") {
|
||||
command_provider! {
|
||||
paste => "doitclient", "wclip", "-r";
|
||||
copy => "doitclient", "wclip";
|
||||
}
|
||||
} else if exists("win32yank.exe") {
|
||||
// FIXME: does it work within WSL?
|
||||
command_provider! {
|
||||
paste => "win32yank.exe", "-o", "--lf";
|
||||
copy => "win32yank.exe", "-i", "--crlf";
|
||||
}
|
||||
} else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
|
||||
command_provider! {
|
||||
paste => "termux-clipboard-get";
|
||||
copy => "termux-clipboard-set";
|
||||
}
|
||||
} else if env_var_is_set("TMUX") && exists("tmux") {
|
||||
command_provider! {
|
||||
paste => "tmux", "save-buffer", "-";
|
||||
copy => "tmux", "load-buffer", "-";
|
||||
}
|
||||
} else {
|
||||
Box::new(provider::NopProvider)
|
||||
}
|
||||
}
|
||||
|
||||
fn exists(executable_name: &str) -> bool {
|
||||
which::which(executable_name).is_ok()
|
||||
}
|
||||
|
||||
fn env_var_is_set(env_var_name: &str) -> bool {
|
||||
std::env::var_os(env_var_name).is_some()
|
||||
}
|
||||
|
||||
fn is_exit_success(program: &str, args: &[&str]) -> bool {
|
||||
std::process::Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
|
||||
.is_some()
|
||||
}
|
||||
|
||||
mod provider {
|
||||
use super::ClipboardProvider;
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NopProvider;
|
||||
|
||||
impl ClipboardProvider for NopProvider {
|
||||
fn name(&self) -> Cow<str> {
|
||||
Cow::Borrowed("none")
|
||||
}
|
||||
|
||||
fn get_contents(&self) -> Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
fn set_contents(&self, _: String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandConfig {
|
||||
pub prg: &'static str,
|
||||
pub args: &'static [&'static str],
|
||||
}
|
||||
|
||||
impl CommandConfig {
|
||||
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
|
||||
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
|
||||
|
||||
let mut child = Command::new(self.prg)
|
||||
.args(self.args)
|
||||
.stdin(stdin)
|
||||
.stdout(stdout)
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
if let Some(input) = input {
|
||||
let mut stdin = child.stdin.take().context("stdin is missing")?;
|
||||
stdin
|
||||
.write_all(input.as_bytes())
|
||||
.context("couldn't write in stdin")?;
|
||||
}
|
||||
|
||||
// TODO: add timer?
|
||||
let output = child.wait_with_output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("clipboard provider {} failed", self.prg);
|
||||
}
|
||||
|
||||
if pipe_output {
|
||||
Ok(Some(String::from_utf8(output.stdout)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandProvider {
|
||||
pub get_cmd: CommandConfig,
|
||||
pub set_cmd: CommandConfig,
|
||||
}
|
||||
|
||||
impl ClipboardProvider for CommandProvider {
|
||||
fn name(&self) -> Cow<str> {
|
||||
if self.get_cmd.prg != self.set_cmd.prg {
|
||||
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
|
||||
} else {
|
||||
Cow::Borrowed(self.get_cmd.prg)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_contents(&self) -> Result<String> {
|
||||
let output = self
|
||||
.get_cmd
|
||||
.execute(None, true)?
|
||||
.context("output is missing")?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn set_contents(&self, value: String) -> Result<()> {
|
||||
self.set_cmd.execute(Some(&value), false).map(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,18 +1,25 @@
|
||||
use anyhow::{Context, Error};
|
||||
use anyhow::{anyhow, Context, Error};
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use helix_core::{
|
||||
chars::{char_is_line_ending, char_is_whitespace},
|
||||
history::History,
|
||||
syntax::{LanguageConfiguration, LOADER},
|
||||
ChangeSet, Diagnostic, Rope, Selection, State, Syntax, Transaction,
|
||||
line_ending::auto_detect_line_ending,
|
||||
syntax::{self, LanguageConfiguration},
|
||||
ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
|
||||
DEFAULT_LINE_ENDING,
|
||||
};
|
||||
|
||||
use crate::{DocumentId, ViewId};
|
||||
use crate::{DocumentId, Theme, ViewId};
|
||||
|
||||
use std::collections::HashMap;
|
||||
const BUF_SIZE: usize = 8192;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Mode {
|
||||
@@ -21,6 +28,46 @@ pub enum Mode {
|
||||
Insert,
|
||||
}
|
||||
|
||||
impl Display for Mode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Mode::Normal => f.write_str("normal"),
|
||||
Mode::Select => f.write_str("select"),
|
||||
Mode::Insert => f.write_str("insert"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"normal" => Ok(Mode::Normal),
|
||||
"select" => Ok(Mode::Select),
|
||||
"insert" => Ok(Mode::Insert),
|
||||
_ => Err(anyhow!("Invalid mode '{}'", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toml deserializer doesn't seem to recognize string as enum
|
||||
impl<'de> Deserialize<'de> for Mode {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum IndentStyle {
|
||||
Tabs,
|
||||
Spaces(u8),
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
// rope + selection
|
||||
pub(crate) id: DocumentId,
|
||||
@@ -28,11 +75,18 @@ pub struct Document {
|
||||
pub(crate) selections: HashMap<ViewId, Selection>,
|
||||
|
||||
path: Option<PathBuf>,
|
||||
encoding: &'static encoding_rs::Encoding,
|
||||
|
||||
/// Current editing mode.
|
||||
pub mode: Mode,
|
||||
pub restore_cursor: bool,
|
||||
|
||||
/// Current indent style.
|
||||
pub indent_style: IndentStyle,
|
||||
|
||||
/// The document's default line ending.
|
||||
pub line_ending: LineEnding,
|
||||
|
||||
syntax: Option<Syntax>,
|
||||
// /// Corresponding language scope name. Usually `source.<lang>`.
|
||||
pub(crate) language: Option<Arc<LanguageConfiguration>>,
|
||||
@@ -61,6 +115,7 @@ impl fmt::Debug for Document {
|
||||
.field("text", &self.text)
|
||||
.field("selections", &self.selections)
|
||||
.field("path", &self.path)
|
||||
.field("encoding", &self.encoding)
|
||||
.field("mode", &self.mode)
|
||||
.field("restore_cursor", &self.restore_cursor)
|
||||
.field("syntax", &self.syntax)
|
||||
@@ -76,6 +131,181 @@ impl fmt::Debug for Document {
|
||||
}
|
||||
}
|
||||
|
||||
// The documentation and implementation of this function should be up-to-date with
|
||||
// its sibling function, `to_writer()`.
|
||||
//
|
||||
/// Decodes a stream of bytes into UTF-8, returning a `Rope` and the
|
||||
/// encoding it was decoded as. The optional `encoding` parameter can
|
||||
/// be used to override encoding auto-detection.
|
||||
pub fn from_reader<R: std::io::Read + ?Sized>(
|
||||
reader: &mut R,
|
||||
encoding: Option<&'static encoding_rs::Encoding>,
|
||||
) -> Result<(Rope, &'static encoding_rs::Encoding), Error> {
|
||||
// These two buffers are 8192 bytes in size each and are used as
|
||||
// intermediaries during the decoding process. Text read into `buf`
|
||||
// from `reader` is decoded into `buf_out` as UTF-8. Once either
|
||||
// `buf_out` is full or the end of the reader was reached, the
|
||||
// contents are appended to `builder`.
|
||||
let mut buf = [0u8; BUF_SIZE];
|
||||
let mut buf_out = [0u8; BUF_SIZE];
|
||||
let mut builder = RopeBuilder::new();
|
||||
|
||||
// By default, the encoding of the text is auto-detected via the
|
||||
// `chardetng` crate which requires sample data from the reader.
|
||||
// As a manual override to this auto-detection is possible, the
|
||||
// same data is read into `buf` to ensure symmetry in the upcoming
|
||||
// loop.
|
||||
let (encoding, mut decoder, mut slice, mut is_empty) = {
|
||||
let read = reader.read(&mut buf)?;
|
||||
let is_empty = read == 0;
|
||||
let encoding = encoding.unwrap_or_else(|| {
|
||||
let mut encoding_detector = chardetng::EncodingDetector::new();
|
||||
encoding_detector.feed(&buf, is_empty);
|
||||
encoding_detector.guess(None, true)
|
||||
});
|
||||
let decoder = encoding.new_decoder();
|
||||
|
||||
// If the amount of bytes read from the reader is less than
|
||||
// `buf.len()`, it is undesirable to read the bytes afterwards.
|
||||
let slice = &buf[..read];
|
||||
(encoding, decoder, slice, is_empty)
|
||||
};
|
||||
|
||||
// `RopeBuilder::append()` expects a `&str`, so this is the "real"
|
||||
// output buffer. When decoding, the number of bytes in the output
|
||||
// buffer will often exceed the number of bytes in the input buffer.
|
||||
// The `result` returned by `decode_to_str()` will state whether or
|
||||
// not that happened. The contents of `buf_str` is appended to
|
||||
// `builder` and it is reused for the next iteration of the decoding
|
||||
// loop.
|
||||
//
|
||||
// As it is possible to read less than the buffer's maximum from `read()`
|
||||
// even when the end of the reader has yet to be reached, the end of
|
||||
// the reader is determined only when a `read()` call returns `0`.
|
||||
//
|
||||
// SAFETY: `buf_out` is a zero-initialized array, thus it will always
|
||||
// contain valid UTF-8.
|
||||
let buf_str = unsafe { std::str::from_utf8_unchecked_mut(&mut buf_out[..]) };
|
||||
let mut total_written = 0usize;
|
||||
loop {
|
||||
let mut total_read = 0usize;
|
||||
|
||||
// An inner loop is necessary as it is possible that the input buffer
|
||||
// may not be completely decoded on the first `decode_to_str()` call
|
||||
// which would happen in cases where the output buffer is filled to
|
||||
// capacity.
|
||||
loop {
|
||||
let (result, read, written, ..) = decoder.decode_to_str(
|
||||
&slice[total_read..],
|
||||
&mut buf_str[total_written..],
|
||||
is_empty,
|
||||
);
|
||||
|
||||
// These variables act as the read and write cursors of `buf` and `buf_str` respectively.
|
||||
// They are necessary in case the output buffer fills before decoding of the entire input
|
||||
// loop is complete. Otherwise, the loop would endlessly iterate over the same `buf` and
|
||||
// the data inside the output buffer would be overwritten.
|
||||
total_read += read;
|
||||
total_written += written;
|
||||
match result {
|
||||
encoding_rs::CoderResult::InputEmpty => {
|
||||
debug_assert_eq!(slice.len(), total_read);
|
||||
break;
|
||||
}
|
||||
encoding_rs::CoderResult::OutputFull => {
|
||||
debug_assert!(slice.len() > total_read);
|
||||
builder.append(&buf_str[..total_written]);
|
||||
total_written = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Once the end of the stream is reached, the output buffer is
|
||||
// flushed and the loop terminates.
|
||||
if is_empty {
|
||||
debug_assert_eq!(reader.read(&mut buf)?, 0);
|
||||
builder.append(&buf_str[..total_written]);
|
||||
break;
|
||||
}
|
||||
|
||||
// Once the previous input has been processed and decoded, the next set of
|
||||
// data is fetched from the reader. The end of the reader is determined to
|
||||
// be when exactly `0` bytes were read from the reader, as per the invariants
|
||||
// of the `Read` trait.
|
||||
let read = reader.read(&mut buf)?;
|
||||
slice = &buf[..read];
|
||||
is_empty = read == 0;
|
||||
}
|
||||
let rope = builder.finish();
|
||||
Ok((rope, encoding))
|
||||
}
|
||||
|
||||
// The documentation and implementation of this function should be up-to-date with
|
||||
// its sibling function, `from_reader()`.
|
||||
//
|
||||
/// Encodes the text inside `rope` into the given `encoding` and writes the
|
||||
/// encoded output into `writer.` As a `Rope` can only contain valid UTF-8,
|
||||
/// replacement characters may appear in the encoded text.
|
||||
pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
|
||||
writer: &'a mut W,
|
||||
encoding: &'static encoding_rs::Encoding,
|
||||
rope: &'a Rope,
|
||||
) -> Result<(), Error> {
|
||||
// Text inside a `Rope` is stored as non-contiguous blocks of data called
|
||||
// chunks. The absolute size of each chunk is unknown, thus it is impossible
|
||||
// to predict the end of the chunk iterator ahead of time. Instead, it is
|
||||
// determined by filtering the iterator to remove all empty chunks and then
|
||||
// appending an empty chunk to it. This is valuable for detecting when all
|
||||
// chunks in the `Rope` have been iterated over in the subsequent loop.
|
||||
let iter = rope
|
||||
.chunks()
|
||||
.filter(|c| !c.is_empty())
|
||||
.chain(std::iter::once(""));
|
||||
let mut buf = [0u8; BUF_SIZE];
|
||||
let mut encoder = encoding.new_encoder();
|
||||
let mut total_written = 0usize;
|
||||
for chunk in iter {
|
||||
let is_empty = chunk.is_empty();
|
||||
let mut total_read = 0usize;
|
||||
|
||||
// An inner loop is necessary as it is possible that the input buffer
|
||||
// may not be completely encoded on the first `encode_from_utf8()` call
|
||||
// which would happen in cases where the output buffer is filled to
|
||||
// capacity.
|
||||
loop {
|
||||
let (result, read, written, ..) =
|
||||
encoder.encode_from_utf8(&chunk[total_read..], &mut buf[total_written..], is_empty);
|
||||
|
||||
// These variables act as the read and write cursors of `chunk` and `buf` respectively.
|
||||
// They are necessary in case the output buffer fills before encoding of the entire input
|
||||
// loop is complete. Otherwise, the loop would endlessly iterate over the same `chunk` and
|
||||
// the data inside the output buffer would be overwritten.
|
||||
total_read += read;
|
||||
total_written += written;
|
||||
match result {
|
||||
encoding_rs::CoderResult::InputEmpty => {
|
||||
debug_assert_eq!(chunk.len(), total_read);
|
||||
debug_assert!(buf.len() >= total_written);
|
||||
break;
|
||||
}
|
||||
encoding_rs::CoderResult::OutputFull => {
|
||||
debug_assert!(chunk.len() > total_read);
|
||||
writer.write_all(&buf[..total_written]).await?;
|
||||
total_written = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once the end of the iterator is reached, the output buffer is
|
||||
// flushed and the outer loop terminates.
|
||||
if is_empty {
|
||||
writer.write_all(&buf[..total_written]).await?;
|
||||
writer.flush().await?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -92,6 +322,36 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands tilde `~` into users home directory if avilable, otherwise returns the path
|
||||
/// unchanged. The tilde will only be expanded when present as the first component of the path
|
||||
/// and only slash follows it.
|
||||
pub fn expand_tilde(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
if let Some(Component::Normal(c)) = components.peek() {
|
||||
if c == &"~" {
|
||||
if let Ok(home) = helix_core::home_dir() {
|
||||
// it's ok to unwrap, the path starts with `~`
|
||||
return home.join(path.strip_prefix("~").unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Replaces users home directory from `path` with tilde `~` if the directory
|
||||
/// is available, otherwise returns the path unchanged.
|
||||
pub fn fold_home_dir(path: &Path) -> PathBuf {
|
||||
if let Ok(home) = helix_core::home_dir() {
|
||||
if path.starts_with(&home) {
|
||||
// it's ok to unwrap, the path starts with home dir
|
||||
return PathBuf::from("~").join(path.strip_prefix(&home).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Normalize a path, removing things like `.` and `..`.
|
||||
///
|
||||
/// CAUTION: This does not resolve symlinks (unlike
|
||||
@@ -100,8 +360,9 @@ where
|
||||
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
|
||||
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
|
||||
/// needs to improve on.
|
||||
/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81
|
||||
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
let path = expand_tilde(path);
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
components.next();
|
||||
@@ -128,27 +389,35 @@ pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
ret
|
||||
}
|
||||
|
||||
// Returns the canonical, absolute form of a path with all intermediate components normalized.
|
||||
//
|
||||
// This function is used instead of `std::fs::canonicalize` because we don't want to verify
|
||||
// here if the path exists, just normalize it's components.
|
||||
/// Returns the canonical, absolute form of a path with all intermediate components normalized.
|
||||
///
|
||||
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
|
||||
/// here if the path exists, just normalize it's components.
|
||||
pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
|
||||
std::env::current_dir().map(|current_dir| normalize_path(¤t_dir.join(path)))
|
||||
let normalized = normalize_path(path);
|
||||
if normalized.is_absolute() {
|
||||
Ok(normalized)
|
||||
} else {
|
||||
std::env::current_dir().map(|current_dir| current_dir.join(normalized))
|
||||
}
|
||||
}
|
||||
|
||||
use helix_lsp::lsp;
|
||||
use url::Url;
|
||||
|
||||
impl Document {
|
||||
pub fn new(text: Rope) -> Self {
|
||||
pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self {
|
||||
let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
|
||||
let changes = ChangeSet::new(&text);
|
||||
let old_state = None;
|
||||
|
||||
Self {
|
||||
id: DocumentId::default(),
|
||||
path: None,
|
||||
encoding,
|
||||
text,
|
||||
selections: HashMap::default(),
|
||||
indent_style: IndentStyle::Spaces(4),
|
||||
mode: Mode::Normal,
|
||||
restore_cursor: false,
|
||||
syntax: None,
|
||||
@@ -160,28 +429,45 @@ impl Document {
|
||||
history: Cell::new(History::default()),
|
||||
last_saved_revision: 0,
|
||||
language_server: None,
|
||||
line_ending: DEFAULT_LINE_ENDING,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: async fn?
|
||||
pub fn load(path: PathBuf) -> Result<Self, Error> {
|
||||
use std::{fs::File, io::BufReader};
|
||||
/// Create a new document from `path`. Encoding is auto-detected, but it can be manually
|
||||
/// overwritten with the `encoding` parameter.
|
||||
pub fn open(
|
||||
path: PathBuf,
|
||||
encoding: Option<&'static encoding_rs::Encoding>,
|
||||
theme: Option<&Theme>,
|
||||
config_loader: Option<&syntax::Loader>,
|
||||
) -> Result<Self, Error> {
|
||||
if !path.exists() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
|
||||
let doc = if !path.exists() {
|
||||
Rope::from("\n")
|
||||
} else {
|
||||
let file = File::open(&path).context(format!("unable to open {:?}", path))?;
|
||||
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 file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
|
||||
let (mut rope, encoding) = from_reader(&mut file, encoding)?;
|
||||
|
||||
// search for line endings
|
||||
let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING);
|
||||
|
||||
// add missing newline at the end of file
|
||||
if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
|
||||
rope.insert(rope.len_chars(), line_ending.as_str());
|
||||
}
|
||||
|
||||
let mut doc = Self::from(rope, Some(encoding));
|
||||
|
||||
let mut doc = Self::new(doc);
|
||||
// set the path and try detecting the language
|
||||
doc.set_path(&path)?;
|
||||
if let Some(loader) = config_loader {
|
||||
doc.detect_language(theme, loader);
|
||||
}
|
||||
|
||||
// Detect indentation style and set line ending.
|
||||
doc.detect_indent_style();
|
||||
doc.line_ending = line_ending;
|
||||
|
||||
Ok(doc)
|
||||
}
|
||||
@@ -190,10 +476,11 @@ impl Document {
|
||||
pub fn format(&mut self, view_id: ViewId) {
|
||||
if let Some(language_server) = self.language_server() {
|
||||
// TODO: await, no blocking
|
||||
let transaction = helix_lsp::block_on(
|
||||
language_server
|
||||
.text_document_formatting(self.identifier(), lsp::FormattingOptions::default()),
|
||||
)
|
||||
let transaction = helix_lsp::block_on(language_server.text_document_formatting(
|
||||
self.identifier(),
|
||||
lsp::FormattingOptions::default(),
|
||||
None,
|
||||
))
|
||||
.map(|edits| {
|
||||
helix_lsp::util::generate_transaction_from_edits(
|
||||
self.text(),
|
||||
@@ -211,6 +498,8 @@ impl Document {
|
||||
|
||||
// TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
|
||||
// or is that handled by the OS/async layer
|
||||
/// The `Document`'s text is encoded according to its encoding and written to the file located
|
||||
/// at its `path()`.
|
||||
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
|
||||
// we clone and move text + path into the future so that we asynchronously save the current
|
||||
// state without blocking any further edits.
|
||||
@@ -228,8 +517,11 @@ impl Document {
|
||||
self.last_saved_revision = history.current_revision();
|
||||
self.history.set(history);
|
||||
|
||||
let encoding = self.encoding;
|
||||
|
||||
// We encode the file according to the `Document`'s encoding.
|
||||
async move {
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
use tokio::fs::File;
|
||||
if let Some(parent) = path.parent() {
|
||||
// TODO: display a prompt asking the user if the directories should be created
|
||||
if !parent.exists() {
|
||||
@@ -238,13 +530,9 @@ impl Document {
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut file = File::create(path).await?;
|
||||
|
||||
// write all the rope chunks to file
|
||||
for chunk in text.chunks() {
|
||||
file.write_all(chunk.as_bytes()).await?;
|
||||
}
|
||||
// TODO: flush?
|
||||
let mut file = File::create(path).await?;
|
||||
to_writer(&mut file, encoding, &text).await?;
|
||||
|
||||
if let Some(language_server) = language_server {
|
||||
language_server
|
||||
@@ -256,12 +544,136 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_language(&mut self) {
|
||||
if let Some(path) = self.path() {
|
||||
let loader = LOADER.get().unwrap();
|
||||
let language_config = loader.language_config_for_file_name(path);
|
||||
let scopes = loader.scopes();
|
||||
self.set_language(language_config, scopes);
|
||||
pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
|
||||
if let Some(path) = &self.path {
|
||||
let language_config = config_loader.language_config_for_file_name(path);
|
||||
self.set_language(theme, language_config);
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_indent_style(&mut self) {
|
||||
// Build a histogram of the indentation *increases* between
|
||||
// subsequent lines, ignoring lines that are all whitespace.
|
||||
//
|
||||
// Index 0 is for tabs, the rest are 1-8 spaces.
|
||||
let histogram: [usize; 9] = {
|
||||
let mut histogram = [0; 9];
|
||||
let mut prev_line_is_tabs = false;
|
||||
let mut prev_line_leading_count = 0usize;
|
||||
|
||||
// Loop through the lines, checking for and recording indentation
|
||||
// increases as we go.
|
||||
'outer: for line in self.text.lines().take(1000) {
|
||||
let mut c_iter = line.chars();
|
||||
|
||||
// Is first character a tab or space?
|
||||
let is_tabs = match c_iter.next() {
|
||||
Some('\t') => true,
|
||||
Some(' ') => false,
|
||||
|
||||
// Ignore blank lines.
|
||||
Some(c) if char_is_line_ending(c) => continue,
|
||||
|
||||
_ => {
|
||||
prev_line_is_tabs = false;
|
||||
prev_line_leading_count = 0;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Count the line's total leading tab/space characters.
|
||||
let mut leading_count = 1;
|
||||
let mut count_is_done = false;
|
||||
for c in c_iter {
|
||||
match c {
|
||||
'\t' if is_tabs && !count_is_done => leading_count += 1,
|
||||
' ' if !is_tabs && !count_is_done => leading_count += 1,
|
||||
|
||||
// We stop counting if we hit whitespace that doesn't
|
||||
// qualify as indent or doesn't match the leading
|
||||
// whitespace, but we don't exit the loop yet because
|
||||
// we still want to determine if the line is blank.
|
||||
c if char_is_whitespace(c) => count_is_done = true,
|
||||
|
||||
// Ignore blank lines.
|
||||
c if char_is_line_ending(c) => continue 'outer,
|
||||
|
||||
_ => break,
|
||||
}
|
||||
|
||||
// Bound the worst-case execution time for weird text files.
|
||||
if leading_count > 256 {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
// If there was an increase in indentation over the previous
|
||||
// line, update the histogram with that increase.
|
||||
if (prev_line_is_tabs == is_tabs || prev_line_leading_count == 0)
|
||||
&& prev_line_leading_count < leading_count
|
||||
{
|
||||
if is_tabs {
|
||||
histogram[0] += 1;
|
||||
} else {
|
||||
let amount = leading_count - prev_line_leading_count;
|
||||
if amount <= 8 {
|
||||
histogram[amount] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store this line's leading whitespace info for use with
|
||||
// the next line.
|
||||
prev_line_is_tabs = is_tabs;
|
||||
prev_line_leading_count = leading_count;
|
||||
}
|
||||
|
||||
// Give more weight to tabs, because their presence is a very
|
||||
// strong indicator.
|
||||
histogram[0] *= 2;
|
||||
|
||||
histogram
|
||||
};
|
||||
|
||||
// Find the most frequent indent, its frequency, and the frequency of
|
||||
// the next-most frequent indent.
|
||||
let indent = histogram
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|kv| kv.1)
|
||||
.unwrap()
|
||||
.0;
|
||||
let indent_freq = histogram[indent];
|
||||
let indent_freq_2 = *histogram
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|kv| kv.0 != indent)
|
||||
.map(|kv| kv.1)
|
||||
.max()
|
||||
.unwrap();
|
||||
|
||||
// Use the auto-detected result if we're confident enough in its
|
||||
// accuracy, based on some heuristics. Otherwise fall back to
|
||||
// the language-based setting.
|
||||
if indent_freq >= 1 && (indent_freq_2 as f64 / indent_freq as f64) < 0.66 {
|
||||
// Use the auto-detected setting.
|
||||
self.indent_style = match indent {
|
||||
0 => IndentStyle::Tabs,
|
||||
_ => IndentStyle::Spaces(indent as u8),
|
||||
};
|
||||
} else {
|
||||
// Fall back to language-based setting.
|
||||
let indent = self
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|config| config.indent.as_ref())
|
||||
.map_or(" ", |config| config.unit.as_str()); // fallback to 2 spaces
|
||||
|
||||
self.indent_style = if indent.starts_with(' ') {
|
||||
IndentStyle::Spaces(indent.len() as u8)
|
||||
} else {
|
||||
IndentStyle::Tabs
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,18 +684,16 @@ impl Document {
|
||||
// and error out when document is saved
|
||||
self.path = Some(path);
|
||||
|
||||
// try detecting the language based on filepath
|
||||
self.detect_language();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_language(
|
||||
&mut self,
|
||||
theme: Option<&Theme>,
|
||||
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
|
||||
scopes: &[String],
|
||||
) {
|
||||
if let Some(language_config) = language_config {
|
||||
let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
|
||||
if let Some(highlight_config) = language_config.highlight_config(scopes) {
|
||||
let syntax = Syntax::new(&self.text, highlight_config);
|
||||
self.syntax = Some(syntax);
|
||||
@@ -297,12 +707,15 @@ impl Document {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_language2(&mut self, scope: &str) {
|
||||
let loader = LOADER.get().unwrap();
|
||||
let language_config = loader.language_config_for_scope(scope);
|
||||
let scopes = loader.scopes();
|
||||
pub fn set_language2(
|
||||
&mut self,
|
||||
scope: &str,
|
||||
theme: Option<&Theme>,
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
) {
|
||||
let language_config = config_loader.language_config_for_scope(scope);
|
||||
|
||||
self.set_language(language_config, scopes);
|
||||
self.set_language(theme, language_config);
|
||||
}
|
||||
|
||||
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
|
||||
@@ -314,7 +727,7 @@ impl Document {
|
||||
self.selections.insert(view_id, selection);
|
||||
}
|
||||
|
||||
fn _apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
||||
fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
||||
let old_doc = self.text().clone();
|
||||
|
||||
let success = transaction.changes().apply(&mut self.text);
|
||||
@@ -377,7 +790,7 @@ impl Document {
|
||||
});
|
||||
}
|
||||
|
||||
let success = self._apply(transaction, view_id);
|
||||
let success = self.apply_impl(transaction, view_id);
|
||||
|
||||
if !transaction.changes().is_empty() {
|
||||
// Compose this transaction with the previous one
|
||||
@@ -391,7 +804,7 @@ impl Document {
|
||||
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)
|
||||
self.apply_impl(transaction, view_id)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -406,7 +819,7 @@ impl Document {
|
||||
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)
|
||||
self.apply_impl(transaction, view_id)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -421,17 +834,18 @@ impl Document {
|
||||
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);
|
||||
self.apply_impl(&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);
|
||||
self.apply_impl(&txn, view_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit pending changes to history
|
||||
pub fn append_changes_to_history(&mut self, view_id: ViewId) {
|
||||
if self.changes.is_empty() {
|
||||
return;
|
||||
@@ -452,12 +866,10 @@ impl Document {
|
||||
self.history.set(history);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn id(&self) -> DocumentId {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_modified(&self) -> bool {
|
||||
let history = self.history.take();
|
||||
let current_revision = history.current_revision();
|
||||
@@ -465,12 +877,10 @@ impl Document {
|
||||
current_revision != self.last_saved_revision || !self.changes.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Corresponding language scope name. Usually `source.<lang>`.
|
||||
pub fn language(&self) -> Option<&str> {
|
||||
self.language
|
||||
@@ -478,21 +888,21 @@ impl Document {
|
||||
.map(|language| language.scope.as_str())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn language_config(&self) -> Option<&LanguageConfiguration> {
|
||||
self.language.as_deref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Current document version, incremented at each change.
|
||||
pub fn version(&self) -> i32 {
|
||||
self.version
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
|
||||
self.language_server.as_deref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Tree-sitter AST tree
|
||||
pub fn syntax(&self) -> Option<&Syntax> {
|
||||
self.syntax.as_ref()
|
||||
@@ -507,13 +917,25 @@ impl Document {
|
||||
}
|
||||
|
||||
/// Returns a string containing a single level of indentation.
|
||||
pub fn indent_unit(&self) -> &str {
|
||||
self.language
|
||||
.as_ref()
|
||||
.and_then(|config| config.indent.as_ref())
|
||||
.map_or(" ", |config| config.unit.as_str()) // fallback to 2 spaces
|
||||
///
|
||||
/// TODO: we might not need this function anymore, since the information
|
||||
/// is conveniently available in `Document::indent_style` now.
|
||||
pub fn indent_unit(&self) -> &'static str {
|
||||
match self.indent_style {
|
||||
IndentStyle::Tabs => "\t",
|
||||
IndentStyle::Spaces(1) => " ",
|
||||
IndentStyle::Spaces(2) => " ",
|
||||
IndentStyle::Spaces(3) => " ",
|
||||
IndentStyle::Spaces(4) => " ",
|
||||
IndentStyle::Spaces(5) => " ",
|
||||
IndentStyle::Spaces(6) => " ",
|
||||
IndentStyle::Spaces(7) => " ",
|
||||
IndentStyle::Spaces(8) => " ",
|
||||
|
||||
// " ".repeat(TAB_WIDTH)
|
||||
// Unsupported indentation style. This should never happen,
|
||||
// but just in case fall back to two spaces.
|
||||
_ => " ",
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -526,20 +948,29 @@ impl Document {
|
||||
self.path().map(|path| Url::from_file_path(path).unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn text(&self) -> &Rope {
|
||||
&self.text
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn selection(&self, view_id: ViewId) -> &Selection {
|
||||
&self.selections[&view_id]
|
||||
}
|
||||
|
||||
pub fn relative_path(&self) -> Option<&Path> {
|
||||
pub fn relative_path(&self) -> Option<PathBuf> {
|
||||
let cwdir = std::env::current_dir().expect("couldn't determine current directory");
|
||||
|
||||
self.path
|
||||
.as_ref()
|
||||
.map(|path| path.strip_prefix(cwdir).unwrap_or(path))
|
||||
self.path.as_ref().map(|path| {
|
||||
let path = fold_home_dir(path);
|
||||
if path.is_relative() {
|
||||
path
|
||||
} else {
|
||||
path.strip_prefix(cwdir)
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or(path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
|
||||
@@ -550,6 +981,7 @@ impl Document {
|
||||
|
||||
// -- LSP methods
|
||||
|
||||
#[inline]
|
||||
pub fn identifier(&self) -> lsp::TextDocumentIdentifier {
|
||||
lsp::TextDocumentIdentifier::new(self.url().unwrap())
|
||||
}
|
||||
@@ -558,6 +990,7 @@ impl Document {
|
||||
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn diagnostics(&self) -> &[Diagnostic] {
|
||||
&self.diagnostics
|
||||
}
|
||||
@@ -567,6 +1000,13 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Document {
|
||||
fn default() -> Self {
|
||||
let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
|
||||
Self::from(text, None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
@@ -575,7 +1015,7 @@ mod test {
|
||||
fn changeset_to_changes() {
|
||||
use helix_lsp::{lsp, Client, OffsetEncoding};
|
||||
let text = Rope::from("hello");
|
||||
let mut doc = Document::new(text);
|
||||
let mut doc = Document::from(text, None);
|
||||
let view = ViewId::default();
|
||||
doc.set_selection(view, Selection::single(5, 5));
|
||||
|
||||
@@ -684,4 +1124,94 @@ mod test {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_ending() {
|
||||
if cfg!(windows) {
|
||||
assert_eq!(Document::default().text().to_string(), "\r\n");
|
||||
} else {
|
||||
assert_eq!(Document::default().text().to_string(), "\n");
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test_decode {
|
||||
($label:expr, $label_override:expr) => {
|
||||
let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
|
||||
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
|
||||
let path = base_path.join(format!("{}_in.txt", $label));
|
||||
let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
|
||||
assert!(path.exists());
|
||||
assert!(ref_path.exists());
|
||||
|
||||
let mut file = std::fs::File::open(path).unwrap();
|
||||
let text = from_reader(&mut file, Some(encoding))
|
||||
.unwrap()
|
||||
.0
|
||||
.to_string();
|
||||
let expectation = std::fs::read_to_string(ref_path).unwrap();
|
||||
assert_eq!(text[..], expectation[..]);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_encode {
|
||||
($label:expr, $label_override:expr) => {
|
||||
let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
|
||||
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
|
||||
let path = base_path.join(format!("{}_out.txt", $label));
|
||||
let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
|
||||
assert!(path.exists());
|
||||
assert!(ref_path.exists());
|
||||
|
||||
let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap();
|
||||
|
||||
let expectation = std::fs::read(ref_path).unwrap();
|
||||
assert_eq!(buf, expectation);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_decode_fn {
|
||||
($name:ident, $label:expr, $label_override:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
test_decode!($label, $label_override);
|
||||
}
|
||||
};
|
||||
($name:ident, $label:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
test_decode!($label, $label);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_encode_fn {
|
||||
($name:ident, $label:expr, $label_override:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
test_encode!($label, $label_override);
|
||||
}
|
||||
};
|
||||
($name:ident, $label:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
test_encode!($label, $label);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_decode_fn!(test_big5_decode, "big5");
|
||||
test_encode_fn!(test_big5_encode, "big5");
|
||||
test_decode_fn!(test_euc_kr_decode, "euc_kr", "EUC-KR");
|
||||
test_encode_fn!(test_euc_kr_encode, "euc_kr", "EUC-KR");
|
||||
test_decode_fn!(test_gb18030_decode, "gb18030");
|
||||
test_encode_fn!(test_gb18030_encode, "gb18030");
|
||||
test_decode_fn!(test_iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP");
|
||||
test_encode_fn!(test_iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP");
|
||||
test_decode_fn!(test_jis0208_decode, "jis0208", "EUC-JP");
|
||||
test_encode_fn!(test_jis0208_encode, "jis0208", "EUC-JP");
|
||||
test_decode_fn!(test_jis0212_decode, "jis0212", "EUC-JP");
|
||||
test_decode_fn!(test_shift_jis_decode, "shift_jis");
|
||||
test_encode_fn!(test_shift_jis_encode, "shift_jis");
|
||||
}
|
||||
|
@@ -1,22 +1,36 @@
|
||||
use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId};
|
||||
use tui::layout::Rect;
|
||||
use crate::{
|
||||
clipboard::{get_clipboard_provider, ClipboardProvider},
|
||||
graphics::{CursorKind, Rect},
|
||||
theme::{self, Theme},
|
||||
tree::Tree,
|
||||
Document, DocumentId, RegisterSelection, View, ViewId,
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use futures_util::future;
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
pub use helix_core::diagnostic::Severity;
|
||||
pub use helix_core::register::Registers;
|
||||
use helix_core::syntax;
|
||||
use helix_core::Position;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Editor {
|
||||
pub tree: Tree,
|
||||
pub documents: SlotMap<DocumentId, Document>,
|
||||
pub count: Option<std::num::NonZeroUsize>,
|
||||
pub register: RegisterSelection,
|
||||
pub selected_register: RegisterSelection,
|
||||
pub registers: Registers,
|
||||
pub theme: Theme,
|
||||
pub language_servers: helix_lsp::Registry,
|
||||
pub clipboard_provider: Box<dyn ClipboardProvider>,
|
||||
|
||||
pub syn_loader: Arc<syntax::Loader>,
|
||||
pub theme_loader: Arc<theme::Loader>,
|
||||
|
||||
pub status_msg: Option<(String, Severity)>,
|
||||
}
|
||||
@@ -29,27 +43,11 @@ pub enum Action {
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new(mut area: tui::layout::Rect) -> Self {
|
||||
use helix_core::config_dir;
|
||||
let config = std::fs::read(config_dir().join("theme.toml"));
|
||||
// load $HOME/.config/helix/theme.toml, fallback to default config
|
||||
let toml = config
|
||||
.as_deref()
|
||||
.unwrap_or(include_bytes!("../../theme.toml"));
|
||||
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
|
||||
|
||||
// initialize language registry
|
||||
use helix_core::syntax::{Loader, LOADER};
|
||||
|
||||
// load $HOME/.config/helix/languages.toml, fallback to default config
|
||||
let config = std::fs::read(helix_core::config_dir().join("languages.toml"));
|
||||
let toml = config
|
||||
.as_deref()
|
||||
.unwrap_or(include_bytes!("../../languages.toml"));
|
||||
|
||||
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
|
||||
LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec()));
|
||||
|
||||
pub fn new(
|
||||
mut area: Rect,
|
||||
themes: Arc<theme::Loader>,
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
) -> Self {
|
||||
let language_servers = helix_lsp::Registry::new();
|
||||
|
||||
// HAXX: offset the render area height by 1 to account for prompt/commandline
|
||||
@@ -59,9 +57,13 @@ impl Editor {
|
||||
tree: Tree::new(area),
|
||||
documents: SlotMap::with_key(),
|
||||
count: None,
|
||||
register: RegisterSelection::default(),
|
||||
theme,
|
||||
selected_register: RegisterSelection::default(),
|
||||
theme: themes.default(),
|
||||
language_servers,
|
||||
syn_loader: config_loader,
|
||||
theme_loader: themes,
|
||||
registers: Registers::default(),
|
||||
clipboard_provider: get_clipboard_provider(),
|
||||
status_msg: None,
|
||||
}
|
||||
}
|
||||
@@ -78,6 +80,32 @@ impl Editor {
|
||||
self.status_msg = Some((error, Severity::Error));
|
||||
}
|
||||
|
||||
pub fn set_theme(&mut self, theme: Theme) {
|
||||
let scopes = theme.scopes();
|
||||
for config in self
|
||||
.syn_loader
|
||||
.language_configs_iter()
|
||||
.filter(|cfg| cfg.is_highlight_initialized())
|
||||
{
|
||||
config.reconfigure(scopes);
|
||||
}
|
||||
|
||||
self.theme = theme;
|
||||
self._refresh();
|
||||
}
|
||||
|
||||
pub fn set_theme_from_name(&mut self, theme: &str) {
|
||||
let theme = match self.theme_loader.load(theme.as_ref()) {
|
||||
Ok(theme) => theme,
|
||||
Err(e) => {
|
||||
log::warn!("failed setting theme `{}` - {}", theme, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.set_theme(theme);
|
||||
}
|
||||
|
||||
fn _refresh(&mut self) {
|
||||
for (view, _) in self.tree.views_mut() {
|
||||
let doc = &self.documents[view.doc];
|
||||
@@ -96,19 +124,19 @@ impl Editor {
|
||||
|
||||
match action {
|
||||
Action::Replace => {
|
||||
let view = self.view();
|
||||
let view = view!(self);
|
||||
let jump = (
|
||||
view.doc,
|
||||
self.documents[view.doc].selection(view.id).clone(),
|
||||
);
|
||||
|
||||
let view = self.view_mut();
|
||||
let view = view_mut!(self);
|
||||
view.jumps.push(jump);
|
||||
view.last_accessed_doc = Some(view.doc);
|
||||
view.doc = id;
|
||||
view.first_line = 0;
|
||||
|
||||
let (view, doc) = self.current();
|
||||
let (view, doc) = current!(self);
|
||||
|
||||
// initialize selection for view
|
||||
let selection = doc
|
||||
@@ -142,8 +170,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn new_file(&mut self, action: Action) -> DocumentId {
|
||||
use helix_core::Rope;
|
||||
let doc = Document::new(Rope::from("\n"));
|
||||
let doc = Document::default();
|
||||
let id = self.documents.insert(doc);
|
||||
self.documents[id].id = id;
|
||||
self.switch(id, action);
|
||||
@@ -161,7 +188,7 @@ impl Editor {
|
||||
let id = if let Some(id) = id {
|
||||
id
|
||||
} else {
|
||||
let mut doc = Document::load(path)?;
|
||||
let mut doc = Document::open(path, None, Some(&self.theme), Some(&self.syn_loader))?;
|
||||
|
||||
// try to find a language server based on the language name
|
||||
let language_server = doc
|
||||
@@ -233,20 +260,6 @@ impl Editor {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
pub fn current(&mut self) -> (&mut View, &mut Document) {
|
||||
let view = self.tree.get_mut(self.tree.focus);
|
||||
let doc = &mut self.documents[view.doc];
|
||||
(view, doc)
|
||||
}
|
||||
|
||||
pub fn view(&self) -> &View {
|
||||
self.tree.get(self.tree.focus)
|
||||
}
|
||||
|
||||
pub fn view_mut(&mut self) -> &mut View {
|
||||
self.tree.get_mut(self.tree.focus)
|
||||
}
|
||||
|
||||
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
|
||||
let view = self.tree.get_mut(id);
|
||||
let doc = &self.documents[view.doc];
|
||||
@@ -261,21 +274,44 @@ impl Editor {
|
||||
self.documents.iter().map(|(_id, doc)| doc)
|
||||
}
|
||||
|
||||
pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
|
||||
self.documents.iter_mut().map(|(_id, doc)| doc)
|
||||
}
|
||||
|
||||
// pub fn current_document(&self) -> Document {
|
||||
// let id = self.view().doc;
|
||||
// let doc = &mut editor.documents[id];
|
||||
// }
|
||||
|
||||
pub fn cursor_position(&self) -> Option<helix_core::Position> {
|
||||
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
let view = self.view();
|
||||
let view = view!(self);
|
||||
let doc = &self.documents[view.doc];
|
||||
let cursor = doc.selection(view.id).cursor();
|
||||
if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
|
||||
pos.col += view.area.x as usize + OFFSET as usize;
|
||||
pos.row += view.area.y as usize;
|
||||
return Some(pos);
|
||||
(Some(pos), CursorKind::Hidden)
|
||||
} else {
|
||||
(None, CursorKind::Hidden)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Closes language servers with timeout. The default timeout is 500 ms, use
|
||||
/// `timeout` parameter to override this.
|
||||
pub async fn close_language_servers(
|
||||
&self,
|
||||
timeout: Option<u64>,
|
||||
) -> Result<(), tokio::time::error::Elapsed> {
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(timeout.unwrap_or(500)),
|
||||
future::join_all(
|
||||
self.language_servers
|
||||
.iter_clients()
|
||||
.map(|client| client.force_shutdown()),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,135 @@
|
||||
//! `style` contains the primitives used to control how your user interface will look.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use std::cmp::{max, min};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// UNSTABLE
|
||||
pub enum CursorKind {
|
||||
/// █
|
||||
Block,
|
||||
/// |
|
||||
Bar,
|
||||
/// _
|
||||
Underline,
|
||||
/// Hidden cursor, can set cursor position with this to let IME have correct cursor position.
|
||||
Hidden,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Margin {
|
||||
pub vertical: u16,
|
||||
pub horizontal: u16,
|
||||
}
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16.
|
||||
/// If clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
let max_area = u16::max_value();
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(self) -> u16 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
pub fn left(self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
pub fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
pub fn top(self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
|
||||
Rect::default()
|
||||
} else {
|
||||
Rect {
|
||||
x: self.x + margin.horizontal,
|
||||
y: self.y + margin.vertical,
|
||||
width: self.width - 2 * margin.horizontal,
|
||||
height: self.height - 2 * margin.vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.x + self.width, other.x + other.width);
|
||||
let y2 = max(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(self, other: Rect) -> Rect {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.x + self.width, other.x + other.width);
|
||||
let y2 = min(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects(self, other: Rect) -> bool {
|
||||
self.x < other.x + other.width
|
||||
&& self.x + self.width > other.x
|
||||
&& self.y < other.y + other.height
|
||||
&& self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
@@ -26,6 +155,35 @@ pub enum Color {
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<Color> for crossterm::style::Color {
|
||||
fn from(color: Color) -> Self {
|
||||
use crossterm::style::Color as CColor;
|
||||
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::Grey,
|
||||
Color::DarkGray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
///
|
||||
@@ -34,7 +192,7 @@ bitflags! {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::style::Modifier;
|
||||
/// # use helix_view::graphics::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
@@ -55,7 +213,7 @@ bitflags! {
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
@@ -67,9 +225,8 @@ bitflags! {
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_tui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red),
|
||||
@@ -94,9 +251,8 @@ bitflags! {
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_tui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
@@ -151,7 +307,7 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::style::{Color, Style};
|
||||
/// # use helix_view::graphics::{Color, Style};
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
@@ -166,7 +322,7 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::style::{Color, Style};
|
||||
/// # use helix_view::graphics::{Color, Style};
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
@@ -183,7 +339,7 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -203,7 +359,7 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -221,7 +377,7 @@ impl Style {
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use helix_tui::style::{Color, Modifier, Style};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
@@ -246,6 +402,50 @@ impl Style {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
}
|
||||
|
||||
fn styles() -> Vec<Style> {
|
||||
vec![
|
||||
Style::default(),
|
231
helix-view/src/input.rs
Normal file
231
helix-view/src/input.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
//! Input event handling, currently backed by crossterm.
|
||||
use anyhow::{anyhow, Error};
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
use std::fmt;
|
||||
|
||||
use crate::keyboard::{KeyCode, KeyModifiers};
|
||||
|
||||
/// Represents a key event.
|
||||
// We use a newtype here because we want to customize Deserialize and Display.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
|
||||
pub struct KeyEvent {
|
||||
pub code: KeyCode,
|
||||
pub modifiers: KeyModifiers,
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"{}{}{}",
|
||||
if self.modifiers.contains(KeyModifiers::SHIFT) {
|
||||
"S-"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if self.modifiers.contains(KeyModifiers::ALT) {
|
||||
"A-"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if self.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
"C-"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
))?;
|
||||
match self.code {
|
||||
KeyCode::Backspace => f.write_str("backspace")?,
|
||||
KeyCode::Enter => f.write_str("ret")?,
|
||||
KeyCode::Left => f.write_str("left")?,
|
||||
KeyCode::Right => f.write_str("right")?,
|
||||
KeyCode::Up => f.write_str("up")?,
|
||||
KeyCode::Down => f.write_str("down")?,
|
||||
KeyCode::Home => f.write_str("home")?,
|
||||
KeyCode::End => f.write_str("end")?,
|
||||
KeyCode::PageUp => f.write_str("pageup")?,
|
||||
KeyCode::PageDown => f.write_str("pagedown")?,
|
||||
KeyCode::Tab => f.write_str("tab")?,
|
||||
KeyCode::BackTab => f.write_str("backtab")?,
|
||||
KeyCode::Delete => f.write_str("del")?,
|
||||
KeyCode::Insert => f.write_str("ins")?,
|
||||
KeyCode::Null => f.write_str("null")?,
|
||||
KeyCode::Esc => f.write_str("esc")?,
|
||||
KeyCode::Char('<') => f.write_str("lt")?,
|
||||
KeyCode::Char('>') => f.write_str("gt")?,
|
||||
KeyCode::Char('+') => f.write_str("plus")?,
|
||||
KeyCode::Char('-') => f.write_str("minus")?,
|
||||
KeyCode::Char(';') => f.write_str("semicolon")?,
|
||||
KeyCode::Char('%') => f.write_str("percent")?,
|
||||
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
|
||||
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for KeyEvent {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut tokens: Vec<_> = s.split('-').collect();
|
||||
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
|
||||
"backspace" => KeyCode::Backspace,
|
||||
"space" => KeyCode::Char(' '),
|
||||
"ret" => KeyCode::Enter,
|
||||
"lt" => KeyCode::Char('<'),
|
||||
"gt" => KeyCode::Char('>'),
|
||||
"plus" => KeyCode::Char('+'),
|
||||
"minus" => KeyCode::Char('-'),
|
||||
"semicolon" => KeyCode::Char(';'),
|
||||
"percent" => KeyCode::Char('%'),
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"up" => KeyCode::Down,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pageup" => KeyCode::PageUp,
|
||||
"pagedown" => KeyCode::PageDown,
|
||||
"tab" => KeyCode::Tab,
|
||||
"backtab" => KeyCode::BackTab,
|
||||
"del" => KeyCode::Delete,
|
||||
"ins" => KeyCode::Insert,
|
||||
"null" => KeyCode::Null,
|
||||
"esc" => KeyCode::Esc,
|
||||
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
|
||||
function if function.len() > 1 && function.starts_with('F') => {
|
||||
let function: String = function.chars().skip(1).collect();
|
||||
let function = str::parse::<u8>(&function)?;
|
||||
(function > 0 && function < 13)
|
||||
.then(|| KeyCode::F(function))
|
||||
.ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
|
||||
}
|
||||
invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
|
||||
};
|
||||
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
for token in tokens {
|
||||
let flag = match token {
|
||||
"S" => KeyModifiers::SHIFT,
|
||||
"A" => KeyModifiers::ALT,
|
||||
"C" => KeyModifiers::CONTROL,
|
||||
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
|
||||
};
|
||||
|
||||
if modifiers.contains(flag) {
|
||||
return Err(anyhow!("Repeated key modifier '{}-'", token));
|
||||
}
|
||||
modifiers.insert(flag);
|
||||
}
|
||||
|
||||
Ok(KeyEvent { code, modifiers })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyEvent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::KeyEvent> for KeyEvent {
|
||||
fn from(
|
||||
crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
|
||||
) -> KeyEvent {
|
||||
KeyEvent {
|
||||
code: code.into(),
|
||||
modifiers: modifiers.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parsing_unmodified_keys() {
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("backspace").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::NONE
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("left").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
modifiers: KeyModifiers::NONE
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>(",").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char(','),
|
||||
modifiers: KeyModifiers::NONE
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("w").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::NONE
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("F12").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::F(12),
|
||||
modifiers: KeyModifiers::NONE
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_modified_keys() {
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("S-minus").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('-'),
|
||||
modifiers: KeyModifiers::SHIFT
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("C-A-S-F12").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::F(12),
|
||||
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("S-C-2").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('2'),
|
||||
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_nonsensical_keys_fails() {
|
||||
assert!(str::parse::<KeyEvent>("F13").is_err());
|
||||
assert!(str::parse::<KeyEvent>("F0").is_err());
|
||||
assert!(str::parse::<KeyEvent>("aaa").is_err());
|
||||
assert!(str::parse::<KeyEvent>("S-S-a").is_err());
|
||||
assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err());
|
||||
assert!(str::parse::<KeyEvent>("FU").is_err());
|
||||
assert!(str::parse::<KeyEvent>("123").is_err());
|
||||
assert!(str::parse::<KeyEvent>("S--").is_err());
|
||||
}
|
||||
}
|
156
helix-view/src/keyboard.rs
Normal file
156
helix-view/src/keyboard.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
/// Represents key modifiers (shift, control, alt).
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct KeyModifiers: u8 {
|
||||
const SHIFT = 0b0000_0001;
|
||||
const CONTROL = 0b0000_0010;
|
||||
const ALT = 0b0000_0100;
|
||||
const NONE = 0b0000_0000;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<KeyModifiers> for crossterm::event::KeyModifiers {
|
||||
fn from(key_modifiers: KeyModifiers) -> Self {
|
||||
use crossterm::event::KeyModifiers as CKeyModifiers;
|
||||
|
||||
let mut result = CKeyModifiers::NONE;
|
||||
|
||||
if key_modifiers.contains(KeyModifiers::SHIFT) {
|
||||
result.insert(CKeyModifiers::SHIFT);
|
||||
}
|
||||
if key_modifiers.contains(KeyModifiers::CONTROL) {
|
||||
result.insert(CKeyModifiers::CONTROL);
|
||||
}
|
||||
if key_modifiers.contains(KeyModifiers::ALT) {
|
||||
result.insert(CKeyModifiers::ALT);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::KeyModifiers> for KeyModifiers {
|
||||
fn from(val: crossterm::event::KeyModifiers) -> Self {
|
||||
use crossterm::event::KeyModifiers as CKeyModifiers;
|
||||
|
||||
let mut result = KeyModifiers::NONE;
|
||||
|
||||
if val.contains(CKeyModifiers::SHIFT) {
|
||||
result.insert(KeyModifiers::SHIFT);
|
||||
}
|
||||
if val.contains(CKeyModifiers::CONTROL) {
|
||||
result.insert(KeyModifiers::CONTROL);
|
||||
}
|
||||
if val.contains(CKeyModifiers::ALT) {
|
||||
result.insert(KeyModifiers::ALT);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a key.
|
||||
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum KeyCode {
|
||||
/// Backspace key.
|
||||
Backspace,
|
||||
/// Enter key.
|
||||
Enter,
|
||||
/// Left arrow key.
|
||||
Left,
|
||||
/// Right arrow key.
|
||||
Right,
|
||||
/// Up arrow key.
|
||||
Up,
|
||||
/// Down arrow key.
|
||||
Down,
|
||||
/// Home key.
|
||||
Home,
|
||||
/// End key.
|
||||
End,
|
||||
/// Page up key.
|
||||
PageUp,
|
||||
/// Page dow key.
|
||||
PageDown,
|
||||
/// Tab key.
|
||||
Tab,
|
||||
/// Shift + Tab key.
|
||||
BackTab,
|
||||
/// Delete key.
|
||||
Delete,
|
||||
/// Insert key.
|
||||
Insert,
|
||||
/// F key.
|
||||
///
|
||||
/// `KeyCode::F(1)` represents F1 key, etc.
|
||||
F(u8),
|
||||
/// A character.
|
||||
///
|
||||
/// `KeyCode::Char('c')` represents `c` character, etc.
|
||||
Char(char),
|
||||
/// Null.
|
||||
Null,
|
||||
/// Escape key.
|
||||
Esc,
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<KeyCode> for crossterm::event::KeyCode {
|
||||
fn from(key_code: KeyCode) -> Self {
|
||||
use crossterm::event::KeyCode as CKeyCode;
|
||||
|
||||
match key_code {
|
||||
KeyCode::Backspace => CKeyCode::Backspace,
|
||||
KeyCode::Enter => CKeyCode::Enter,
|
||||
KeyCode::Left => CKeyCode::Left,
|
||||
KeyCode::Right => CKeyCode::Right,
|
||||
KeyCode::Up => CKeyCode::Up,
|
||||
KeyCode::Down => CKeyCode::Down,
|
||||
KeyCode::Home => CKeyCode::Home,
|
||||
KeyCode::End => CKeyCode::End,
|
||||
KeyCode::PageUp => CKeyCode::PageUp,
|
||||
KeyCode::PageDown => CKeyCode::PageDown,
|
||||
KeyCode::Tab => CKeyCode::Tab,
|
||||
KeyCode::BackTab => CKeyCode::BackTab,
|
||||
KeyCode::Delete => CKeyCode::Delete,
|
||||
KeyCode::Insert => CKeyCode::Insert,
|
||||
KeyCode::F(f_number) => CKeyCode::F(f_number),
|
||||
KeyCode::Char(character) => CKeyCode::Char(character),
|
||||
KeyCode::Null => CKeyCode::Null,
|
||||
KeyCode::Esc => CKeyCode::Esc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl From<crossterm::event::KeyCode> for KeyCode {
|
||||
fn from(val: crossterm::event::KeyCode) -> Self {
|
||||
use crossterm::event::KeyCode as CKeyCode;
|
||||
|
||||
match val {
|
||||
CKeyCode::Backspace => KeyCode::Backspace,
|
||||
CKeyCode::Enter => KeyCode::Enter,
|
||||
CKeyCode::Left => KeyCode::Left,
|
||||
CKeyCode::Right => KeyCode::Right,
|
||||
CKeyCode::Up => KeyCode::Up,
|
||||
CKeyCode::Down => KeyCode::Down,
|
||||
CKeyCode::Home => KeyCode::Home,
|
||||
CKeyCode::End => KeyCode::End,
|
||||
CKeyCode::PageUp => KeyCode::PageUp,
|
||||
CKeyCode::PageDown => KeyCode::PageDown,
|
||||
CKeyCode::Tab => KeyCode::Tab,
|
||||
CKeyCode::BackTab => KeyCode::BackTab,
|
||||
CKeyCode::Delete => KeyCode::Delete,
|
||||
CKeyCode::Insert => KeyCode::Insert,
|
||||
CKeyCode::F(f_number) => KeyCode::F(f_number),
|
||||
CKeyCode::Char(character) => KeyCode::Char(character),
|
||||
CKeyCode::Null => KeyCode::Null,
|
||||
CKeyCode::Esc => KeyCode::Esc,
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +1,21 @@
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
|
||||
pub mod clipboard;
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
pub mod graphics;
|
||||
pub mod input;
|
||||
pub mod keyboard;
|
||||
pub mod register_selection;
|
||||
pub mod theme;
|
||||
pub mod tree;
|
||||
pub mod view;
|
||||
|
||||
use slotmap::new_key_type;
|
||||
new_key_type! { pub struct DocumentId; }
|
||||
new_key_type! { pub struct ViewId; }
|
||||
slotmap::new_key_type! {
|
||||
pub struct DocumentId;
|
||||
pub struct ViewId;
|
||||
}
|
||||
|
||||
pub use document::Document;
|
||||
pub use editor::Editor;
|
||||
|
29
helix-view/src/macros.rs
Normal file
29
helix-view/src/macros.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
#[macro_export]
|
||||
macro_rules! current {
|
||||
( $( $editor:ident ).+ ) => {{
|
||||
let view = $crate::view_mut!( $( $editor ).+ );
|
||||
let doc = &mut $( $editor ).+ .documents[view.doc];
|
||||
(view, doc)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! doc_mut {
|
||||
( $( $editor:ident ).+ ) => {{
|
||||
$crate::current!( $( $editor ).+ ).1
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! view_mut {
|
||||
( $( $editor:ident ).+ ) => {{
|
||||
$( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! view {
|
||||
( $( $editor:ident ).+ ) => {{
|
||||
$( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
|
||||
}};
|
||||
}
|
@@ -1,92 +1,95 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use log::warn;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use toml::Value;
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
pub use tui::style::{Color, Modifier, Style};
|
||||
|
||||
// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||
// pub struct Color {
|
||||
// pub r: u8,
|
||||
// pub g: u8,
|
||||
// pub b: u8,
|
||||
// }
|
||||
|
||||
// impl Color {
|
||||
// pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||
// Self { r, g, b }
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(feature = "term")]
|
||||
// impl Into<tui::style::Color> for Color {
|
||||
// fn into(self) -> tui::style::Color {
|
||||
// tui::style::Color::Rgb(self.r, self.g, self.b)
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl std::str::FromStr for Color {
|
||||
// type Err = ();
|
||||
|
||||
// /// Tries to parse a string (`'#FFFFFF'` or `'FFFFFF'`) into RGB.
|
||||
// fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
// let input = input.trim();
|
||||
// let input = match (input.chars().next(), input.len()) {
|
||||
// (Some('#'), 7) => &input[1..],
|
||||
// (_, 6) => input,
|
||||
// _ => return Err(()),
|
||||
// };
|
||||
|
||||
// u32::from_str_radix(&input, 16)
|
||||
// .map(|s| Color {
|
||||
// r: ((s >> 16) & 0xFF) as u8,
|
||||
// g: ((s >> 8) & 0xFF) as u8,
|
||||
// b: (s & 0xFF) as u8,
|
||||
// })
|
||||
// .map_err(|_| ())
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||
// pub struct Style {
|
||||
// pub fg: Option<Color>,
|
||||
// pub bg: Option<Color>,
|
||||
// // TODO: modifiers (bold, underline, italic, etc)
|
||||
// }
|
||||
|
||||
// impl Style {
|
||||
// pub fn fg(mut self, fg: Color) -> Self {
|
||||
// self.fg = Some(fg);
|
||||
// self
|
||||
// }
|
||||
|
||||
// pub fn bg(mut self, bg: Color) -> Self {
|
||||
// self.bg = Some(bg);
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(feature = "term")]
|
||||
// impl Into<tui::style::Style> for Style {
|
||||
// fn into(self) -> tui::style::Style {
|
||||
// let style = tui::style::Style::default();
|
||||
|
||||
// if let Some(fg) = self.fg {
|
||||
// style.fg(fg.into());
|
||||
// }
|
||||
|
||||
// if let Some(bg) = self.bg {
|
||||
// style.bg(bg.into());
|
||||
// }
|
||||
|
||||
// style
|
||||
// }
|
||||
// }
|
||||
pub use crate::graphics::{Color, Modifier, Style};
|
||||
|
||||
/// Color theme for syntax highlighting.
|
||||
#[derive(Debug)]
|
||||
|
||||
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
|
||||
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Loader {
|
||||
user_dir: PathBuf,
|
||||
default_dir: PathBuf,
|
||||
}
|
||||
impl Loader {
|
||||
/// Creates a new loader that can load themes from two directories.
|
||||
pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
|
||||
Self {
|
||||
user_dir: user_dir.as_ref().join("themes"),
|
||||
default_dir: default_dir.as_ref().join("themes"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a theme first looking in the `user_dir` then in `default_dir`
|
||||
pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> {
|
||||
if name == "default" {
|
||||
return Ok(self.default());
|
||||
}
|
||||
let filename = format!("{}.toml", name);
|
||||
|
||||
let user_path = self.user_dir.join(&filename);
|
||||
let path = if user_path.exists() {
|
||||
user_path
|
||||
} else {
|
||||
self.default_dir.join(filename)
|
||||
};
|
||||
|
||||
let data = std::fs::read(&path)?;
|
||||
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
|
||||
}
|
||||
|
||||
pub fn read_names(path: &Path) -> Vec<String> {
|
||||
std::fs::read_dir(path)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|entry| {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext != "toml" {
|
||||
return None;
|
||||
}
|
||||
return Some(
|
||||
entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.trim_end_matches(".toml")
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Lists all theme names available in default and user directory
|
||||
pub fn names(&self) -> Vec<String> {
|
||||
let mut names = Self::read_names(&self.user_dir);
|
||||
names.extend(Self::read_names(&self.default_dir));
|
||||
names
|
||||
}
|
||||
|
||||
/// Returns the default theme
|
||||
pub fn default(&self) -> Theme {
|
||||
DEFAULT_THEME.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Theme {
|
||||
scopes: Vec<String>,
|
||||
styles: HashMap<String, Style>,
|
||||
@@ -200,12 +203,14 @@ fn parse_modifier(value: &Value) -> Option<Modifier> {
|
||||
|
||||
impl Theme {
|
||||
pub fn get(&self, scope: &str) -> Style {
|
||||
self.styles
|
||||
.get(scope)
|
||||
.copied()
|
||||
self.try_get(scope)
|
||||
.unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
|
||||
}
|
||||
|
||||
pub fn try_get(&self, scope: &str) -> Option<Style> {
|
||||
self.styles.get(scope).copied()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn scopes(&self) -> &[String] {
|
||||
&self.scopes
|
||||
|
@@ -1,6 +1,5 @@
|
||||
use crate::{View, ViewId};
|
||||
use crate::{graphics::Rect, View, ViewId};
|
||||
use slotmap::HopSlotMap;
|
||||
use tui::layout::Rect;
|
||||
|
||||
// the dimensions are recomputed on windo resize/tree change.
|
||||
//
|
||||
@@ -434,6 +433,10 @@ impl Tree {
|
||||
self.focus = key;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(&self) -> Rect {
|
||||
self.area
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@@ -1,12 +1,11 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{Document, DocumentId, ViewId};
|
||||
use crate::{graphics::Rect, Document, DocumentId, ViewId};
|
||||
use helix_core::{
|
||||
coords_at_pos,
|
||||
graphemes::{grapheme_width, RopeGraphemes},
|
||||
Position, RopeSlice, Selection,
|
||||
};
|
||||
use tui::layout::Rect;
|
||||
|
||||
pub const PADDING: usize = 5;
|
||||
|
||||
|
19787
helix-view/tests/encoding/big5_in.txt
Normal file
19787
helix-view/tests/encoding/big5_in.txt
Normal file
File diff suppressed because it is too large
Load Diff
19787
helix-view/tests/encoding/big5_in_ref.txt
Normal file
19787
helix-view/tests/encoding/big5_in_ref.txt
Normal file
File diff suppressed because it is too large
Load Diff
14601
helix-view/tests/encoding/big5_out.txt
Normal file
14601
helix-view/tests/encoding/big5_out.txt
Normal file
File diff suppressed because it is too large
Load Diff
14601
helix-view/tests/encoding/big5_out_ref.txt
Normal file
14601
helix-view/tests/encoding/big5_out_ref.txt
Normal file
File diff suppressed because it is too large
Load Diff
23945
helix-view/tests/encoding/euc_kr_in.txt
Normal file
23945
helix-view/tests/encoding/euc_kr_in.txt
Normal file
File diff suppressed because it is too large
Load Diff
23945
helix-view/tests/encoding/euc_kr_in_ref.txt
Normal file
23945
helix-view/tests/encoding/euc_kr_in_ref.txt
Normal file
File diff suppressed because it is too large
Load Diff
17053
helix-view/tests/encoding/euc_kr_out.txt
Normal file
17053
helix-view/tests/encoding/euc_kr_out.txt
Normal file
File diff suppressed because it is too large
Load Diff
17053
helix-view/tests/encoding/euc_kr_out_ref.txt
Normal file
17053
helix-view/tests/encoding/euc_kr_out_ref.txt
Normal file
File diff suppressed because it is too large
Load Diff
23945
helix-view/tests/encoding/gb18030_in.txt
Normal file
23945
helix-view/tests/encoding/gb18030_in.txt
Normal file
File diff suppressed because it is too large
Load Diff
23945
helix-view/tests/encoding/gb18030_in_ref.txt
Normal file
23945
helix-view/tests/encoding/gb18030_in_ref.txt
Normal file
File diff suppressed because it is too large
Load Diff
23944
helix-view/tests/encoding/gb18030_out.txt
Normal file
23944
helix-view/tests/encoding/gb18030_out.txt
Normal file
File diff suppressed because it is too large
Load Diff
23944
helix-view/tests/encoding/gb18030_out_ref.txt
Normal file
23944
helix-view/tests/encoding/gb18030_out_ref.txt
Normal file
File diff suppressed because it is too large
Load Diff
8841
helix-view/tests/encoding/iso_2022_jp_in.txt
Normal file
8841
helix-view/tests/encoding/iso_2022_jp_in.txt
Normal file
File diff suppressed because it is too large
Load Diff
8841
helix-view/tests/encoding/iso_2022_jp_in_ref.txt
Normal file
8841
helix-view/tests/encoding/iso_2022_jp_in_ref.txt
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user