diff options
268 files changed, 10621 insertions, 2643 deletions
diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 00000000..35049cbc --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 958407bb..41b00230 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -17,6 +17,7 @@ Please search on the issue tracker before creating one. --> ### Environment - Platform: <!-- macOS / Windows / Linux --> +- Terminal emulator: - Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master --> <details><summary>~/.cache/helix/helix.log</summary> diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21629180..65c2f949 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,4 +136,52 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: --all-targets -- -D warnings + + docs: + name: Docs + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + with: + submodules: true + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Cache cargo registry + uses: actions/cache@v2.1.6 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v2.1.6 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo target dir + uses: actions/cache@v2.1.6 + with: + path: target + key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Generate docs + uses: actions-rs/cargo@v1 + with: + command: xtask + args: docgen + + - name: Check uncommitted documentation changes + run: | + git diff + git diff-files --quiet \ + || (echo "Run 'cargo xtask docgen', commit the changes and push again" \ + && exit 1) + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b16fa428..7b0b7ee2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,7 +102,7 @@ jobs: fi cp -r runtime dist - - uses: actions/upload-artifact@v2.2.4 + - uses: actions/upload-artifact@v2.3.1 with: name: bins-${{ matrix.build }} path: dist diff --git a/.gitmodules b/.gitmodules index 6295b9e9..55ed97b3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -142,11 +142,87 @@ path = helix-syntax/languages/tree-sitter-perl url = https://github.com/ganezdragon/tree-sitter-perl shallow = true +[submodule "helix-syntax/languages/tree-sitter-comment"] + path = helix-syntax/languages/tree-sitter-comment + url = https://github.com/stsewd/tree-sitter-comment + shallow = true [submodule "helix-syntax/languages/tree-sitter-wgsl"] path = helix-syntax/languages/tree-sitter-wgsl url = https://github.com/szebniok/tree-sitter-wgsl shallow = true -[submodule "helix-syntax/tree-sitter-llvm"] +[submodule "helix-syntax/languages/tree-sitter-llvm"] path = helix-syntax/languages/tree-sitter-llvm url = https://github.com/benwilliamgraham/tree-sitter-llvm shallow = true +[submodule "helix-syntax/languages/tree-sitter-markdown"] + path = helix-syntax/languages/tree-sitter-markdown + url = https://github.com/MDeiml/tree-sitter-markdown + shallow = true +[submodule "helix-syntax/languages/tree-sitter-dart"] + path = helix-syntax/languages/tree-sitter-dart + url = https://github.com/UserNobody14/tree-sitter-dart.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-dockerfile"] + path = helix-syntax/languages/tree-sitter-dockerfile + url = https://github.com/camdencheek/tree-sitter-dockerfile.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-fish"] + path = helix-syntax/languages/tree-sitter-fish + url = https://github.com/ram02z/tree-sitter-fish + shallow = true +[submodule "helix-syntax/languages/tree-sitter-git-commit"] + path = helix-syntax/languages/tree-sitter-git-commit + url = https://github.com/the-mikedavis/tree-sitter-git-commit.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-llvm-mir"] + path = helix-syntax/languages/tree-sitter-llvm-mir + url = https://github.com/Flakebi/tree-sitter-llvm-mir.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-git-diff"] + path = helix-syntax/languages/tree-sitter-git-diff + url = https://github.com/the-mikedavis/tree-sitter-git-diff.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-tablegen"] + path = helix-syntax/languages/tree-sitter-tablegen + url = https://github.com/Flakebi/tree-sitter-tablegen + shallow = true +[submodule "helix-syntax/languages/tree-sitter-git-rebase"] + path = helix-syntax/languages/tree-sitter-git-rebase + url = https://github.com/the-mikedavis/tree-sitter-git-rebase.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-lean"] + path = helix-syntax/languages/tree-sitter-lean + url = https://github.com/Julian/tree-sitter-lean + shallow = true +[submodule "helix-syntax/languages/tree-sitter-regex"] + path = helix-syntax/languages/tree-sitter-regex + url = https://github.com/tree-sitter/tree-sitter-regex.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-make"] + path = helix-syntax/languages/tree-sitter-make + url = https://github.com/alemuller/tree-sitter-make + shallow = true +[submodule "helix-syntax/languages/tree-sitter-git-config"] + path = helix-syntax/languages/tree-sitter-git-config + url = https://github.com/the-mikedavis/tree-sitter-git-config.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-graphql"] + path = helix-syntax/languages/tree-sitter-graphql + url = https://github.com/bkegley/tree-sitter-graphql + shallow = true +[submodule "helix-syntax/languages/tree-sitter-elm"] + path = helix-syntax/languages/tree-sitter-elm + url = https://github.com/elm-tooling/tree-sitter-elm + shallow = true +[submodule "helix-syntax/languages/tree-sitter-iex"] + path = helix-syntax/languages/tree-sitter-iex + url = https://github.com/elixir-lang/tree-sitter-iex + shallow = true +[submodule "helix-syntax/languages/tree-sitter-twig"] + path = helix-syntax/languages/tree-sitter-twig + url = https://github.com/eirabben/tree-sitter-twig.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-rescript"] + path = helix-syntax/languages/tree-sitter-rescript + url = https://github.com/jaredramirez/tree-sitter-rescript + shallow = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ca2d60..38927991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,123 @@ +# 0.6.0 (2022-01-04) + +Happy new year and a big shout out to all the contributors! We had 55 contributors in this release. + +Helix has popped up in DPorts and Fedora Linux via COPR ([#1270](https://github.com/helix-editor/helix/pull/1270)) + +As usual the following is a brief summary, refer to the git history for a full log: + +Breaking changes: + +- fix: Normalize backtab into shift-tab + +Features: + +- Macros ([#1234](https://github.com/helix-editor/helix/pull/1234)) +- Add reverse search functionality ([#958](https://github.com/helix-editor/helix/pull/958)) +- Allow keys to be mapped to sequences of commands ([#589](https://github.com/helix-editor/helix/pull/589)) +- Make it possible to keybind TypableCommands ([#1169](https://github.com/helix-editor/helix/pull/1169)) +- Detect workspace root using language markers ([#1370](https://github.com/helix-editor/helix/pull/1370)) +- Add WORD textobject ([#991](https://github.com/helix-editor/helix/pull/991)) +- Add LSP rename_symbol (space-r) ([#1011](https://github.com/helix-editor/helix/pull/1011)) +- Added workspace_symbol_picker ([#1041](https://github.com/helix-editor/helix/pull/1041)) +- Detect filetype from shebang line ([#1001](https://github.com/helix-editor/helix/pull/1001)) +- Allow piping from stdin into a buffer on startup ([#996](https://github.com/helix-editor/helix/pull/996)) +- Add auto pairs for same-char pairs ([#1219](https://github.com/helix-editor/helix/pull/1219)) +- Update settings at runtime ([#798](https://github.com/helix-editor/helix/pull/798)) +- Enable thin LTO (cccc194) + +Commands: +- :wonly -- window only ([#1057](https://github.com/helix-editor/helix/pull/1057)) +- buffer-close (:bc, :bclose) ([#1035](https://github.com/helix-editor/helix/pull/1035)) +- Add :<line> and :goto <line> commands ([#1128](https://github.com/helix-editor/helix/pull/1128)) +- :sort command ([#1288](https://github.com/helix-editor/helix/pull/1288)) +- Add m textobject for pair under cursor ([#961](https://github.com/helix-editor/helix/pull/961)) +- Implement "Goto next buffer / Goto previous buffer" commands ([#950](https://github.com/helix-editor/helix/pull/950)) +- Implement "Goto last modification" command ([#1067](https://github.com/helix-editor/helix/pull/1067)) +- Add trim_selections command ([#1092](https://github.com/helix-editor/helix/pull/1092)) +- Add movement shortcut for history ([#1088](https://github.com/helix-editor/helix/pull/1088)) +- Add command to inc/dec number under cursor ([#1027](https://github.com/helix-editor/helix/pull/1027)) + - Add support for dates for increment/decrement +- Align selections (&) ([#1101](https://github.com/helix-editor/helix/pull/1101)) +- Implement no-yank delete/change ([#1099](https://github.com/helix-editor/helix/pull/1099)) +- Implement black hole register ([#1165](https://github.com/helix-editor/helix/pull/1165)) +- gf as goto_file (gf) ([#1102](https://github.com/helix-editor/helix/pull/1102)) +- Add last modified file (gm) ([#1093](https://github.com/helix-editor/helix/pull/1093)) +- ensure_selections_forward ([#1393](https://github.com/helix-editor/helix/pull/1393)) +- Readline style insert mode ([#1039](https://github.com/helix-editor/helix/pull/1039)) + +Usability improvements and fixes: + +- Detect filetype on :write ([#1141](https://github.com/helix-editor/helix/pull/1141)) +- Add single and double quotes to matching pairs ([#995](https://github.com/helix-editor/helix/pull/995)) +- Launch with defaults upon invalid config/theme (rather than panicking) ([#982](https://github.com/helix-editor/helix/pull/982)) +- If switching away from an empty scratch buffer, remove it ([#935](https://github.com/helix-editor/helix/pull/935)) +- Truncate the starts of file paths instead of the ends in picker ([#951](https://github.com/helix-editor/helix/pull/951)) +- Truncate the start of file paths in the StatusLine ([#1351](https://github.com/helix-editor/helix/pull/1351)) +- Prevent picker from previewing binaries or large file ([#939](https://github.com/helix-editor/helix/pull/939)) +- Inform when reaching undo/redo bounds ([#981](https://github.com/helix-editor/helix/pull/981)) +- search_impl will only align cursor center when it isn't in view ([#959](https://github.com/helix-editor/helix/pull/959)) +- Add <C-h>, <C-u>, <C-d>, Delete in prompt mode ([#1034](https://github.com/helix-editor/helix/pull/1034)) +- Restore screen position when aborting search ([#1047](https://github.com/helix-editor/helix/pull/1047)) +- Buffer picker: show is_modifier flag ([#1020](https://github.com/helix-editor/helix/pull/1020)) +- Add commit hash to version info, if present ([#957](https://github.com/helix-editor/helix/pull/957)) +- Implement indent-aware delete ([#1120](https://github.com/helix-editor/helix/pull/1120)) +- Jump to end char of surrounding pair from any cursor pos ([#1121](https://github.com/helix-editor/helix/pull/1121)) +- File picker configuration ([#988](https://github.com/helix-editor/helix/pull/988)) +- Fix surround cursor position calculation ([#1183](https://github.com/helix-editor/helix/pull/1183)) +- Accept count for goto_window ([#1033](https://github.com/helix-editor/helix/pull/1033)) +- Make kill_to_line_end behave like emacs ([#1235](https://github.com/helix-editor/helix/pull/1235)) +- Only use a single documentation popup ([#1241](https://github.com/helix-editor/helix/pull/1241)) +- ui: popup: Don't allow scrolling past the end of content (3307f44c) +- Open files with spaces in filename, allow opening multiple files ([#1231](https://github.com/helix-editor/helix/pull/1231)) +- Allow paste commands to take a count ([#1261](https://github.com/helix-editor/helix/pull/1261)) +- Auto pairs selection ([#1254](https://github.com/helix-editor/helix/pull/1254)) +- Use a fuzzy matcher for commands ([#1386](https://github.com/helix-editor/helix/pull/1386)) +- Add c-s to pick word under doc cursor to prompt line & search completion ([#831](https://github.com/helix-editor/helix/pull/831)) +- Fix :earlier/:later missing changeset update ([#1069](https://github.com/helix-editor/helix/pull/1069)) +- Support extend for multiple goto ([#909](https://github.com/helix-editor/helix/pull/909)) +- Add arrow-key bindings for window switching ([#933](https://github.com/helix-editor/helix/pull/933)) +- Implement key ordering for info box ([#952](https://github.com/helix-editor/helix/pull/952)) + +LSP: +- Implement MarkedString rendering (e128a8702) +- Don't panic if init fails (d31bef7) +- Configurable diagnostic severity ([#1325](https://github.com/helix-editor/helix/pull/1325)) +- Resolve completion item ([#1315](https://github.com/helix-editor/helix/pull/1315)) +- Code action command support ([#1304](https://github.com/helix-editor/helix/pull/1304)) + +Grammars: + +- Adds mint language server ([#974](https://github.com/helix-editor/helix/pull/974)) +- Perl ([#978](https://github.com/helix-editor/helix/pull/978)) ([#1280](https://github.com/helix-editor/helix/pull/1280)) +- GLSL ([#993](https://github.com/helix-editor/helix/pull/993)) +- Racket ([#1143](https://github.com/helix-editor/helix/pull/1143)) +- WGSL ([#1166](https://github.com/helix-editor/helix/pull/1166)) +- LLVM ([#1167](https://github.com/helix-editor/helix/pull/1167)) ([#1388](https://github.com/helix-editor/helix/pull/1388)) ([#1409](https://github.com/helix-editor/helix/pull/1409)) ([#1398](https://github.com/helix-editor/helix/pull/1398)) +- Markdown (49e06787) +- Scala ([#1278](https://github.com/helix-editor/helix/pull/1278)) +- Dart ([#1250](https://github.com/helix-editor/helix/pull/1250)) +- Fish ([#1308](https://github.com/helix-editor/helix/pull/1308)) +- Dockerfile ([#1303](https://github.com/helix-editor/helix/pull/1303)) +- Git (commit, rebase, diff) ([#1338](https://github.com/helix-editor/helix/pull/1338)) ([#1402](https://github.com/helix-editor/helix/pull/1402)) ([#1373](https://github.com/helix-editor/helix/pull/1373)) +- tree-sitter-comment ([#1300](https://github.com/helix-editor/helix/pull/1300)) +- Highlight comments in c, cpp, cmake and llvm ([#1309](https://github.com/helix-editor/helix/pull/1309)) +- Improve yaml syntax highlighting highlighting ([#1294](https://github.com/helix-editor/helix/pull/1294)) +- Improve rust syntax highlighting ([#1295](https://github.com/helix-editor/helix/pull/1295)) +- Add textobjects and indents to cmake ([#1307](https://github.com/helix-editor/helix/pull/1307)) +- Add textobjects and indents to c and cpp ([#1293](https://github.com/helix-editor/helix/pull/1293)) + +New themes: + +- Solarized dark ([#999](https://github.com/helix-editor/helix/pull/999)) +- Solarized light ([#1010](https://github.com/helix-editor/helix/pull/1010)) +- Spacebones light ([#1131](https://github.com/helix-editor/helix/pull/1131)) +- Monokai Pro ([#1206](https://github.com/helix-editor/helix/pull/1206)) +- Base16 Light and Terminal ([#1078](https://github.com/helix-editor/helix/pull/1078)) + - and a default 16 color theme, truecolor detection +- Dracula ([#1258](https://github.com/helix-editor/helix/pull/1258)) + # 0.5.0 (2021-10-28) A big shout out to all the contributors! We had 46 contributors in this release. @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.51" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" [[package]] name = "arc-swap" @@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chardetng" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" dependencies = [ "cfg-if", "encoding_rs", @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "4.2.2" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed" +checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" dependencies = [ "error-code", "str-buf", @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if", "lazy_static", @@ -131,16 +131,16 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" +checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432" dependencies = [ "bitflags", "crossterm_winapi", "futures-core", "libc", "mio", - "parking_lot", + "parking_lot 0.12.0", "signal-hook", "signal-hook-mio", "winapi", @@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" -version = "0.8.29" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ "cfg-if", ] @@ -202,9 +202,9 @@ dependencies = [ [[package]] name = "error-code" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" dependencies = [ "libc", "str-buf", @@ -247,26 +247,16 @@ dependencies = [ ] [[package]] -name = "futf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] name = "futures-core" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -275,15 +265,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-core", "futures-task", @@ -303,9 +293,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if", "libc", @@ -366,9 +356,11 @@ dependencies = [ [[package]] name = "helix-core" -version = "0.5.0" +version = "0.6.0" dependencies = [ "arc-swap", + "chrono", + "encoding_rs", "etcetera", "helix-syntax", "log", @@ -379,8 +371,9 @@ dependencies = [ "serde", "serde_json", "similar", + "slotmap", "smallvec", - "tendril", + "smartstring", "toml", "tree-sitter", "unicode-general-category", @@ -390,7 +383,7 @@ dependencies = [ [[package]] name = "helix-dap" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "fern", @@ -404,7 +397,7 @@ dependencies = [ [[package]] name = "helix-lsp" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "futures-executor", @@ -422,7 +415,7 @@ dependencies = [ [[package]] name = "helix-syntax" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "cc", @@ -433,7 +426,7 @@ dependencies = [ [[package]] name = "helix-term" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "chrono", @@ -465,7 +458,7 @@ dependencies = [ [[package]] name = "helix-tui" -version = "0.5.0" +version = "0.6.0" dependencies = [ "bitflags", "cassowary", @@ -478,14 +471,13 @@ dependencies = [ [[package]] name = "helix-view" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "bitflags", "chardetng", "clipboard-win", "crossterm", - "encoding_rs", "futures-util", "helix-core", "helix-dap", @@ -551,9 +543,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "jsonrpc-core" @@ -576,15 +568,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.104" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" [[package]] name = "libloading" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" dependencies = [ "cfg-if", "winapi", @@ -592,9 +584,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -610,9 +602,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.91.1" +version = "0.92.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae" +checksum = "e8a69d4142d51b208c9fc3cea68b1a7fcef30354e7aa6ccad07250fd8430fc76" dependencies = [ "bitflags", "serde", @@ -622,12 +614,6 @@ dependencies = [ ] [[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] name = "matches" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -671,12 +657,6 @@ dependencies = [ ] [[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - -[[package]] name = "ntapi" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -706,9 +686,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -716,9 +696,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "parking_lot" @@ -728,7 +708,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.1", ] [[package]] @@ -746,6 +736,19 @@ dependencies = [ ] [[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -753,9 +756,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -765,18 +768,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.30" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] [[package]] name = "pulldown-cmark" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6" dependencies = [ "bitflags", "memchr", @@ -794,9 +797,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] @@ -863,18 +866,18 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "ropey" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28" +checksum = "e6b9aa65bcd9f308d37c7158b4a1afaaa32b8450213e20c9b98e7d5b3cc2fec3" dependencies = [ "smallvec", ] [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "same-file" @@ -893,18 +896,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.130" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -913,9 +916,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.72" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" dependencies = [ "itoa", "ryu", @@ -935,9 +938,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" dependencies = [ "libc", "signal-hook-registry", @@ -965,9 +968,9 @@ dependencies = [ [[package]] name = "signal-hook-tokio" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" dependencies = [ "futures-core", "libc", @@ -998,9 +1001,24 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "smartstring" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa6a31c0c2b21327ce875f7e8952322acfcfd0c27569a6e18a647281352c9b" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "str-buf" @@ -1010,9 +1028,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" [[package]] name = "syn" -version = "1.0.80" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", @@ -1020,17 +1038,6 @@ dependencies = [ ] [[package]] -name = "tendril" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] name = "thiserror" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1052,9 +1059,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ "once_cell", ] @@ -1070,9 +1077,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -1085,18 +1092,17 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.14.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" dependencies = [ - "autocfg", "bytes", "libc", "memchr", "mio", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.11.2", "pin-project-lite", "signal-hook-registry", "tokio-macros", @@ -1105,9 +1111,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -1136,9 +1142,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.1" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1" +checksum = "4e34327f8eac545e3f037382471b2b19367725a242bba7bc45edb9efb49fe39a" dependencies = [ "cc", "regex", @@ -1161,9 +1167,9 @@ checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-general-category" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742" +checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6" [[package]] name = "unicode-normalization" @@ -1176,9 +1182,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" @@ -1206,16 +1212,10 @@ dependencies = [ ] [[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" @@ -1236,9 +1236,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "which" -version = "4.2.2" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" dependencies = [ "either", "lazy_static", @@ -1275,3 +1275,55 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + +[[package]] +name = "xtask" +version = "0.6.0" +dependencies = [ + "helix-core", + "helix-term", + "toml", +] @@ -7,6 +7,7 @@ members = [ "helix-syntax", "helix-lsp", "helix-dap", + "xtask", ] # Build helix-syntax in release mode to make the code path faster in development. @@ -18,3 +19,4 @@ split-debuginfo = "unpacked" [profile.release] lto = "thin" +# debug = true @@ -44,8 +44,8 @@ cargo install --path helix-term This will install the `hx` binary to `$HOME/.cargo/bin`. Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the -config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden -via the `HELIX_RUNTIME` environment variable. +config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows). +This location can be overriden via the `HELIX_RUNTIME` environment variable. Packages already solve this for you by wrapping the `hx` binary with a wrapper that sets the variable to the install dir. @@ -65,21 +65,7 @@ brew install helix # Contributing -Contributors are very welcome! **No contribution is too small and all contributions are valued.** - -Some suggestions to get started: - -- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker. -- Help with packaging on various distributions needed! -- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must: - * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`) - * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive) -- If your preferred language is missing, integrating a tree-sitter grammar for - it and defining syntax highlight queries for it is straight forward and - doesn't require much knowledge of the internals. - -We provide an [architecture.md](./docs/architecture.md) that should give you -a good overview of the internals. +Contributing guidelines can be found [here](./docs/CONTRIBUTING.md). # Getting help @@ -1,25 +1,12 @@ -- tree sitter: - - markdown - - regex - - kotlin - - clojure - - erlang - - [ ] completion isIncomplete support - -1 - [ ] respect view fullscreen flag - [ ] Implement marks (superset of Selection/Range) - [ ] = for auto indent line/selection -- [ ] :x for closing buffers - [ ] lsp: signature help 2 -- [ ] macro recording -- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc ) -- [ ] selection align - [ ] store some state between restarts: file positions, prompt history - [ ] highlight matched characters in picker diff --git a/base16_theme.toml b/base16_theme.toml new file mode 100644 index 00000000..42e02a98 --- /dev/null +++ b/base16_theme.toml @@ -0,0 +1,51 @@ +# Author: NNB <nnbnh@protonmail.com> + +"ui.menu" = "black" +"ui.menu.selected" = { modifiers = ["reversed"] } +"ui.linenr" = { fg = "gray", bg = "black" } +"ui.popup" = { modifiers = ["reversed"] } +"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] } +"ui.selection" = { fg = "black", bg = "blue" } +"ui.selection.primary" = { fg = "white", bg = "blue" } +"comment" = { fg = "gray" } +"ui.statusline" = { fg = "black", bg = "white" } +"ui.statusline.inactive" = { fg = "gray", bg = "white" } +"ui.help" = { modifiers = ["reversed"] } +"ui.cursor" = { fg = "white", modifiers = ["reversed"] } +"variable" = "red" +"constant.numeric" = "yellow" +"constant" = "yellow" +"attributes" = "yellow" +"type" = "yellow" +"ui.cursor.match" = { fg = "yellow", modifiers = ["underlined"] } +"string" = "green" +"variable.other.member" = "green" +"constant.character.escape" = "cyan" +"function" = "blue" +"constructor" = "blue" +"special" = "blue" +"keyword" = "magenta" +"label" = "magenta" +"namespace" = "magenta" +"ui.help" = { fg = "white", bg = "black" } + +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = "green" +"diff.delta" = "yellow" +"diff.minus" = "red" + +"diagnostic" = { modifiers = ["underlined"] } +"ui.gutter" = { bg = "black" } +"info" = "blue" +"hint" = "gray" +"debug" = "gray" +"warning" = "yellow" +"error" = "red" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8cadb663..a8f165c0 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -2,10 +2,12 @@ - [Installation](./install.md) - [Usage](./usage.md) + - [Keymap](./keymap.md) + - [Commands](./commands.md) + - [Language Support](./lang-support.md) - [Migrating from Vim](./from-vim.md) - [Configuration](./configuration.md) - [Themes](./themes.md) - - [Keymap](./keymap.md) - [Key Remapping](./remapping.md) - [Hooks](./hooks.md) - [Languages](./languages.md) diff --git a/book/src/commands.md b/book/src/commands.md new file mode 100644 index 00000000..4c4a5c05 --- /dev/null +++ b/book/src/commands.md @@ -0,0 +1,5 @@ +# Commands + +Command mode can be activated by pressing `:`, similar to vim. Built-in commands: + +{{#include ./generated/typable-cmd.md}} diff --git a/book/src/configuration.md b/book/src/configuration.md index 2ed48d51..8048f548 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -5,9 +5,27 @@ To override global configuration parameters, create a `config.toml` file located * Linux and Mac: `~/.config/helix/config.toml` * Windows: `%AppData%\helix\config.toml` +Example config: + +```toml +theme = "onedark" + +[editor] +line-number = "relative" +mouse = false + +[editor.cursor-shape] +insert = "bar" +normal = "block" +select = "underline" + +[editor.file-picker] +hidden = false +``` + ## Editor -`[editor]` section of the config. +### `[editor]` Section | Key | Description | Default | |--|--|---------| @@ -16,15 +34,37 @@ To override global configuration parameters, create a `config.toml` file located | `middle-click-paste` | Middle click paste support. | `true` | | `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | | `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` | -| `line-number` | Line number display (`absolute`, `relative`) | `absolute` | +| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` | | `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | | `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` | | `auto-completion` | Enable automatic pop up of auto-completion. | `true` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `auto-info` | Whether to display infoboxes | `true` | +| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | + +### `[editor.cursor-shape]` Section + +Defines the shape of cursor in each mode. Note that due to limitations +of the terminal environment, only the primary cursor can change shape. + +| Key | Description | Default | +| --- | ----------- | ------- | +| `normal` | Cursor shape in [normal mode][normal mode] | `block` | +| `insert` | Cursor shape in [insert mode][insert mode] | `block` | +| `select` | Cursor shape in [select mode][select mode] | `block` | + +[normal mode]: ./keymap.md#normal-mode +[insert mode]: ./keymap.md#insert-mode +[select mode]: ./keymap.md#select--extend-mode + +### `[editor.file-picker]` Section -`[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default. +Sets options for file picker and global search. All but the last key listed in +the default file-picker configuration below are IgnoreOptions: whether hidden +files and files listed within ignore files are ignored by (not visible in) the +helix file picker and global search. There is also one other key, `max-depth` +available, which is not defined by default. | Key | Description | Default | |--|--|---------| diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md new file mode 100644 index 00000000..64dab6d3 --- /dev/null +++ b/book/src/generated/lang-support.md @@ -0,0 +1,63 @@ +| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP | +| --- | --- | --- | --- | --- | +| bash | ✓ | | | `bash-language-server` | +| c | ✓ | ✓ | ✓ | `clangd` | +| c-sharp | ✓ | | | | +| cmake | ✓ | ✓ | ✓ | `cmake-language-server` | +| comment | ✓ | | | | +| cpp | ✓ | ✓ | ✓ | `clangd` | +| css | ✓ | | | | +| dart | ✓ | | ✓ | `dart` | +| dockerfile | ✓ | | | `docker-langserver` | +| elixir | ✓ | | | `elixir-ls` | +| elm | ✓ | | | `elm-language-server` | +| fish | ✓ | ✓ | ✓ | | +| git-commit | ✓ | | | | +| git-config | ✓ | | | | +| git-diff | ✓ | | | | +| git-rebase | ✓ | | | | +| glsl | ✓ | | ✓ | | +| go | ✓ | ✓ | ✓ | `gopls` | +| graphql | ✓ | | | | +| haskell | ✓ | | | `haskell-language-server-wrapper` | +| html | ✓ | | | | +| iex | ✓ | | | | +| java | ✓ | | | | +| javascript | ✓ | | ✓ | `typescript-language-server` | +| json | ✓ | | ✓ | | +| julia | ✓ | | | `julia` | +| latex | ✓ | | | | +| lean | ✓ | | | `lean` | +| ledger | ✓ | | | | +| llvm | ✓ | ✓ | ✓ | | +| llvm-mir | ✓ | ✓ | ✓ | | +| llvm-mir-yaml | ✓ | | ✓ | | +| lua | ✓ | | ✓ | | +| make | ✓ | | | | +| markdown | ✓ | | | | +| mint | | | | `mint` | +| nix | ✓ | | ✓ | `rnix-lsp` | +| ocaml | ✓ | | ✓ | | +| ocaml-interface | ✓ | | | | +| perl | ✓ | ✓ | ✓ | | +| php | ✓ | ✓ | ✓ | | +| prolog | | | | `swipl` | +| protobuf | ✓ | | ✓ | | +| python | ✓ | ✓ | ✓ | `pylsp` | +| racket | | | | `racket` | +| regex | ✓ | | | | +| rescript | ✓ | ✓ | | `rescript-language-server` | +| ruby | ✓ | | ✓ | `solargraph` | +| rust | ✓ | ✓ | ✓ | `rust-analyzer` | +| scala | ✓ | | ✓ | `metals` | +| svelte | ✓ | | ✓ | `svelteserver` | +| tablegen | ✓ | ✓ | ✓ | | +| toml | ✓ | | | | +| tsq | ✓ | | | | +| tsx | ✓ | | | `typescript-language-server` | +| twig | ✓ | | | | +| typescript | ✓ | | ✓ | `typescript-language-server` | +| vue | ✓ | | | | +| wgsl | ✓ | | | | +| yaml | ✓ | | ✓ | | +| zig | ✓ | | ✓ | `zls` | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md new file mode 100644 index 00000000..aed75cbd --- /dev/null +++ b/book/src/generated/typable-cmd.md @@ -0,0 +1,48 @@ +| Name | Description | +| --- | --- | +| `:quit`, `:q` | Close the current view. | +| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). | +| `:open`, `:o` | Open a file from disk into the current view. | +| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. | +| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). | +| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) | +| `:new`, `:n` | Create a new scratch buffer. | +| `:format`, `:fmt` | Format the file using the LSP formatter. | +| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) | +| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. | +| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. | +| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. | +| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) | +| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) | +| `:write-all`, `:wa` | Write changes from all views to disk. | +| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. | +| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). | +| `:quit-all`, `:qa` | Close all views. | +| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). | +| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | +| `:cquit!`, `:cq!` | Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2). | +| `:theme` | Change the editor theme. | +| `:clipboard-yank` | Yank main selection into system clipboard. | +| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. | +| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. | +| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. | +| `:clipboard-paste-after` | Paste system clipboard after selections. | +| `:clipboard-paste-before` | Paste system clipboard before selections. | +| `:clipboard-paste-replace` | Replace selections with content of system clipboard. | +| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. | +| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. | +| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. | +| `:show-clipboard-provider` | Show clipboard provider name in status bar. | +| `:change-current-directory`, `:cd` | Change the current working directory. | +| `:show-directory`, `:pwd` | Show the current working directory. | +| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` | +| `:reload` | Discard changes and reload from the source file. | +| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | +| `:vsplit`, `:vs` | Open the file in a vertical split. | +| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. | +| `:tutor` | Open the tutorial. | +| `:goto`, `:g` | Go to line number. | +| `:set-option`, `:set` | Set a config option at runtime | +| `:sort` | Sort ranges in selection. | +| `:rsort` | Sort ranges in selection in reverse order. | +| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 446eb479..5844a48e 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -2,7 +2,7 @@ ## Submodules -To add a new langauge, you should first add a tree-sitter submodule. To do this, +To add a new language, you should first add a tree-sitter submodule. To do this, you can run the command ```sh git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name> @@ -27,22 +27,32 @@ directory](../configuration.md). These are the available keys and descriptions for the file. -| Key | Description | -| ---- | ----------- | -| name | The name of the language | -| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages | -| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. | -| file-types | The filetypes of the language, for example `["yml", "yaml"]` | -| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` | -| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` | -| auto-format | Whether to autoformat this language when saving | -| comment-token | The token to use as a comment-token | -| indent | The indent to use. Has sub keys `tab-width` and `unit` | -| config | Language server configuration | +| Key | Description | +| ---- | ----------- | +| name | The name of the language | +| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages | +| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. | +| file-types | The filetypes of the language, for example `["yml", "yaml"]` | +| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` | +| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` | +| auto-format | Whether to autoformat this language when saving | +| diagnostic-severity | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) | +| comment-token | The token to use as a comment-token | +| indent | The indent to use. Has sub keys `tab-width` and `unit` | +| config | Language server configuration | ## Queries -For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries/<name>/`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries. +For a language to have syntax-highlighting and indentation among +other things, you have to add queries. Add a directory for your +language with the path `runtime/queries/<name>/`. The tree-sitter +[website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) +gives more info on how to write queries. + +> NOTE: When evaluating queries, the first matching query takes +precedence, which is different from other editors like neovim where +the last matching query supercedes the ones before it. See +[this issue][neovim-query-precedence] for an example. ## Common Issues @@ -58,3 +68,4 @@ For a language to have syntax-highlighting and indentation among other things, y [treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection [languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml +[neovim-query-precedence]: https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090 diff --git a/book/src/install.md b/book/src/install.md index d831934c..1a5a9daa 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -27,6 +27,15 @@ Releases are available in the `community` repository. A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch. +### Fedora Linux + +You can install the COPR package for Helix via + +``` +sudo dnf copr enable varlad/helix +sudo dnf install helix +``` + ## Build from source ``` diff --git a/book/src/keymap.md b/book/src/keymap.md index 865a700b..19fd21bb 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -25,7 +25,9 @@ | `f` | Find next char | `find_next_char` | | `T` | Find 'till previous char | `till_prev_char` | | `F` | Find previous char | `find_prev_char` | +| `G` | Go to line number `<n>` | `goto_line` | | `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` | +| `Alt-:` | Ensures the selection is in forward direction | `ensure_selections_forward` | | `Home` | Move to the start of the line | `goto_line_start` | | `End` | Move to the end of the line | `goto_line_end` | | `PageUp` | Move page up | `page_up` | @@ -34,6 +36,7 @@ | `Ctrl-d` | Move half page down | `half_page_down` | | `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | | `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | +| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` | | `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | | `g` | Enter [goto mode](#goto-mode) | N/A | | `m` | Enter [match mode](#match-mode) | N/A | @@ -45,37 +48,39 @@ ### Changes -| Key | Description | Command | -| ----- | ----------- | ------- | -| `r` | Replace with a character | `replace` | -| `R` | Replace with yanked text | `replace_with_yanked` | -| `~` | Switch case of the selected text | `switch_case` | -| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` | -| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` | -| `i` | Insert before selection | `insert_mode` | -| `a` | Insert after selection (append) | `append_mode` | -| `I` | Insert at the start of the line | `prepend_to_line` | -| `A` | Insert at the end of the line | `append_to_line` | -| `o` | Open new line below selection | `open_below` | -| `O` | Open new line above selection | `open_above` | -| `.` | Repeat last change | N/A | -| `u` | Undo change | `undo` | -| `U` | Redo change | `redo` | -| `Alt-u` | Move backward in history | `earlier` | -| `Alt-U` | Move forward in history | `later` | -| `y` | Yank selection | `yank` | -| `p` | Paste after selection | `paste_after` | -| `P` | Paste before selection | `paste_before` | -| `"` `<reg>` | Select a register to yank to or paste from | `select_register` | -| `>` | Indent selection | `indent` | -| `<` | Unindent selection | `unindent` | -| `=` | Format selection (**LSP**) | `format_selections` | -| `d` | Delete selection | `delete_selection` | -| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` | -| `c` | Change selection (delete and enter insert mode) | `change_selection` | -| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` | -| `Ctrl-a` | Increment object (number) under cursor | `increment` | -| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `r` | Replace with a character | `replace` | +| `R` | Replace with yanked text | `replace_with_yanked` | +| `~` | Switch case of the selected text | `switch_case` | +| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` | +| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` | +| `i` | Insert before selection | `insert_mode` | +| `a` | Insert after selection (append) | `append_mode` | +| `I` | Insert at the start of the line | `prepend_to_line` | +| `A` | Insert at the end of the line | `append_to_line` | +| `o` | Open new line below selection | `open_below` | +| `O` | Open new line above selection | `open_above` | +| `.` | Repeat last change | N/A | +| `u` | Undo change | `undo` | +| `U` | Redo change | `redo` | +| `Alt-u` | Move backward in history | `earlier` | +| `Alt-U` | Move forward in history | `later` | +| `y` | Yank selection | `yank` | +| `p` | Paste after selection | `paste_after` | +| `P` | Paste before selection | `paste_before` | +| `"` `<reg>` | Select a register to yank to or paste from | `select_register` | +| `>` | Indent selection | `indent` | +| `<` | Unindent selection | `unindent` | +| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` | +| `d` | Delete selection | `delete_selection` | +| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` | +| `c` | Change selection (delete and enter insert mode) | `change_selection` | +| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` | +| `Ctrl-a` | Increment object (number) under cursor | `increment` | +| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | +| `Q` | Start/stop macro recording to the selected register (experimental) | `record_macro` | +| `q` | Play back a recorded macro from the selected register (experimental) | `replay_macro` | #### Shell @@ -85,6 +90,7 @@ | <code>Alt-|</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | | `!` | Run shell command, inserting output before each selection | `shell_insert_output` | | `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` | +| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | ### Selection manipulation @@ -109,12 +115,14 @@ | `%` | Select entire file | `select_all` | | `x` | Select current line, if already selected, extend to next line | `extend_line` | | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | -| | Expand selection to parent syntax node TODO: pick a key (**TS**) | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | | `K` | Keep selections matching the regex | `keep_selections` | | `Alt-K` | Remove selections matching the regex | `remove_selections` | -| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | +| `Alt-k` | Expand selection to parent syntax node (**TS**) | `expand_selection` | +| `Alt-j` | Shrink syntax tree object selection (**TS**) | `shrink_selection` | +| `Alt-h` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` | +| `Alt-l` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` | ### Search @@ -147,10 +155,10 @@ over text and not actively editing it). | `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` | | `j` , `down` | Scroll the view downwards | `scroll_down` | | `k` , `up` | Scroll the view upwards | `scroll_up` | -| `f` | Move page down | `page_down` | -| `b` | Move page up | `page_up` | -| `d` | Move half page down | `half_page_down` | -| `u` | Move half page up | `half_page_up` | +| `Ctrl-f` | Move page down | `page_down` | +| `Ctrl-b` | Move page up | `page_up` | +| `Ctrl-d` | Move half page down | `half_page_down` | +| `Ctrl-u` | Move half page up | `half_page_up` | #### Goto mode @@ -158,20 +166,21 @@ Jumps to various locations. | Key | Description | Command | | ----- | ----------- | ------- | -| `g` | Go to the start of the file | `goto_file_start` | +| `g` | Go to line number `<n>` else start of file | `goto_file_start` | | `e` | Go to the end of the file | `goto_last_line` | | `f` | Go to files in the selection | `goto_file` | | `h` | Go to the start of the line | `goto_line_start` | | `l` | Go to the end of the line | `goto_line_end` | | `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` | | `t` | Go to the top of the screen | `goto_window_top` | -| `m` | Go to the middle of the screen | `goto_window_middle` | +| `c` | Go to the middle of the screen | `goto_window_center` | | `b` | Go to the bottom of the screen | `goto_window_bottom` | | `d` | Go to definition (**LSP**) | `goto_definition` | | `y` | Go to type definition (**LSP**) | `goto_type_definition` | | `r` | Go to references (**LSP**) | `goto_reference` | | `i` | Go to implementation (**LSP**) | `goto_implementation` | | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | +| `m` | Go to the last modified/alternate file | `goto_last_modified_file` | | `n` | Go to next buffer | `goto_next_buffer` | | `p` | Go to previous buffer | `goto_previous_buffer` | | `.` | Go to last modification in current file | `goto_last_modification` | diff --git a/book/src/lang-support.md b/book/src/lang-support.md new file mode 100644 index 00000000..3920f342 --- /dev/null +++ b/book/src/lang-support.md @@ -0,0 +1,10 @@ +# Language Support + +For more information like arguments passed to default LSP server, +extensions assosciated with a filetype, custom LSP settings, filetype +specific indent settings, etc see the default +[`languages.toml`][languages.toml] file. + +{{#include ./generated/lang-support.md}} + +[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml diff --git a/book/src/languages.md b/book/src/languages.md index cef61501..4c4dc326 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -11,4 +11,3 @@ Changes made to the `languages.toml` file in a user's [configuration directory]( name = "rust" auto-format = false ``` - diff --git a/book/src/remapping.md b/book/src/remapping.md index fffd189b..1cdf9b1f 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -11,6 +11,8 @@ this: ```toml # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select' [keys.normal] +C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file) +C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file a = "move_char_left" # Maps the 'a' key to the move_char_left command w = "move_line_up" # Maps the 'w' key move_line_up "C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line @@ -21,6 +23,7 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions "A-x" = "normal_mode" # Maps Alt-X to enter normal mode j = { k = "normal_mode" } # Maps `jk` to exit insert mode ``` +> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command. Control, Shift and Alt modifiers are encoded respectively with the prefixes `C-`, `S-` and `A-`. Special keys are encoded as follows: @@ -42,10 +45,9 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes | Down | `"down"` | | Home | `"home"` | | End | `"end"` | -| Page | `"pageup"` | -| Page | `"pagedown"` | +| Page Up | `"pageup"` | +| Page Down | `"pagedown"` | | Tab | `"tab"` | -| Back | `"backtab"` | | Delete | `"del"` | | Insert | `"ins"` | | Null | `"null"` | @@ -54,4 +56,4 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes Keys can be disabled by binding them to the `no_op` command. Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands. -> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro. +> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`. diff --git a/book/src/themes.md b/book/src/themes.md index fd3f5b1e..9abcfe8c 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -1,14 +1,14 @@ # 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 theme add `theme = "<name>"` to your [`config.toml`](./configuration.md) at the very top of the file before the first section or select it during runtime using `:theme <name>`. -To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`. +## Creating a theme -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). +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`). The directory might have to be created beforehand. -## Creating a theme +The names "default" and "base16_default" are reserved for the builtin themes and cannot be overridden by user defined themes. -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`). +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). Each line in the theme file is specified as below: @@ -105,6 +105,7 @@ We use a similar set of scopes as - `type` - Types - `builtin` - Primitive types provided by the language (`int`, `usize`) +- `constructor` - `constant` (TODO: constant.other.placeholder for %v) - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) @@ -146,6 +147,7 @@ We use a similar set of scopes as - `repeat` - `for`, `while`, `loop` - `import` - `import`, `export` - `return` + - `exception` - `operator` - `or`, `in` - `directive` - Preprocessor directives (`#if` in C) - `function` - `fn`, `func` @@ -162,10 +164,44 @@ We use a similar set of scopes as - `namespace` +- `markup` + - `heading` + - `list` + - `unnumbered` + - `numbered` + - `bold` + - `italic` + - `link` + - `url` - urls pointed to by links + - `label` - non-url link references + - `text` - url and image descriptions in links + - `quote` + - `raw` + - `inline` + - `block` + +- `diff` - version control changes + - `plus` - additions + - `minus` - deletions + - `delta` - modifications + - `moved` - renamed or moved files/changes + #### Interface These scopes are used for theming the editor interface. +- `markup` + - `normal` + - `completion` - for completion doc popup ui + - `hover` - for hover popup ui + - `heading` + - `completion` - for completion doc popup ui + - `hover` - for hover popup ui + - `raw` + - `inline` + - `completion` - for completion doc popup ui + - `hover` - for hover popup ui + | Key | Notes | | --- | --- | diff --git a/book/src/usage.md b/book/src/usage.md index cf7d9d48..a76bfafc 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -42,7 +42,7 @@ helix. The keymappings have been inspired from [vim-sandwich](https://github.com `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]`: +It can also act on multiple selections (yay!). For example, to change every occurrence of `(use)` to `[use]`: - `%` to select the whole file - `s` to split the selections on a search term diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..bdd771aa --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +Contributors are very welcome! **No contribution is too small and all contributions are valued.** + +Some suggestions to get started: + +- You can look at the [good first issue][good-first-issue] label on the issue tracker. +- Help with packaging on various distributions needed! +- To use print debugging to the [Helix log file][log-file], you must: + * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`) + * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive) +- If your preferred language is missing, integrating a tree-sitter grammar for + it and defining syntax highlight queries for it is straight forward and + doesn't require much knowledge of the internals. + +We provide an [architecture.md][architecture.md] that should give you +a good overview of the internals. + +# Auto generated documentation + +Some parts of [the book][docs] are autogenerated from the code itself, +like the list of `:commands` and supported languages. To generate these +files, run + +```shell +cargo xtask docgen +``` + +inside the project. We use [xtask][xtask] as an ad-hoc task runner and +thus do not require any dependencies other than `cargo` (You don't have +to `cargo install` anything either). + +[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy +[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file +[architecture.md]: ./architecture.md +[docs]: https://docs.helix-editor.com/ +[xtask]: https://github.com/matklad/cargo-xtask @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1632436039, - "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=", + "lastModified": 1641980203, + "narHash": "sha256-RiWJ3+6V267Ji+P54K1Xrj1Nsah9BfG/aLfIhqgVyBY=", "owner": "numtide", "repo": "devshell", - "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6", + "rev": "d897c1ddb4eab66cc2b783c7868d78555b9880ad", "type": "github" }, "original": { @@ -17,11 +17,11 @@ }, "flake-utils": { "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "lastModified": 1637014545, + "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", "owner": "numtide", "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", "type": "github" }, "original": { @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1634796585, - "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=", + "lastModified": 1642054253, + "narHash": "sha256-kHh9VmaB7gbS6pheheC4x0uT84LEmhfbsbWEQJgU2E4=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b", + "rev": "f8fa9af990195a3f63fe2dde84aa187e193da793", "type": "github" }, "original": { @@ -56,11 +56,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1634782485, - "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=", + "lastModified": 1641887635, + "narHash": "sha256-kDGpufwzVaiGe5e1sBUBPo9f1YN+nYHJlYqCaVpZTQQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be", + "rev": "b2737d4980a17cc2b7d600d7d0b32fd7333aca88", "type": "github" }, "original": { @@ -72,15 +72,16 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1628186154, - "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=", + "lastModified": 1637453606, + "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "06552b72346632b6943c8032e57e702ea12413bf", + "rev": "8afc4e543663ca0a6a4f496262cd05233737e732", "type": "github" }, "original": { "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -98,11 +99,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1634869268, - "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=", + "lastModified": 1642128126, + "narHash": "sha256-av8JUACdrTfQYl/ftZJvKpZEmZfa0avCq7tt5Usdoq0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c02c2d86354327317546501af001886fbb53d374", + "rev": "ce4ef6f2d74f2b68f7547df1de22d1b0037ce4ad", "type": "github" }, "original": { @@ -20,50 +20,63 @@ # Set default package to helix-term release build defaultOutputs = { app = "hx"; package = "helix"; }; overrides = { - crateOverrides = common: _: { - 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 ]; - }; + crateOverrides = common: _: rec { # 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: { + helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml,base16_theme.toml} .."; }; + helix-syntax = prev: { + src = + let + pkgs = common.pkgs; + helix = pkgs.fetchgit { + url = "https://github.com/helix-editor/helix.git"; + rev = "a8fd33ac012a79069ef1409503a2edcf3a585153"; + fetchSubmodules = true; + sha256 = "sha256-5AtOC55ttWT+7RYMboaFxpGZML51ix93wAkYJTt+8JI="; + }; + in + pkgs.runCommand prev.src.name { } '' + mkdir -p $out + ln -s ${prev.src}/* $out + ln -sf ${helix}/helix-syntax/languages $out + ''; preConfigure = "mkdir -p ../runtime/grammars"; postInstall = "cp -r ../runtime $out/runtime"; }; - }; - mainBuild = common: prev: - let - inherit (common) pkgs lib; - helixSyntax = lib.buildCrate { - root = self; - memberName = "helix-syntax"; - defaultCrateOverrides = { - helix-syntax = common.crateOverrides.helix-syntax; + helix-term = prev: + let + inherit (common) pkgs lib; + helixSyntax = lib.buildCrate { + root = self; + memberName = "helix-syntax"; + defaultCrateOverrides = { + helix-syntax = helix-syntax; + }; + release = false; }; - release = false; + runtimeDir = pkgs.runCommand "helix-runtime" { } '' + mkdir -p $out + ln -s ${common.root}/runtime/* $out + ln -sf ${helixSyntax}/runtime/grammars $out + ''; + in + { + # link languages and theme toml files since helix-term expects them (for tests) + preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml,base16_theme.toml} .."; + buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postFixup = '' + if [ -f "$out/bin/hx" ]; then + wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}" + fi + ''; }; - runtimeDir = pkgs.runCommand "helix-runtime" { } '' - mkdir -p $out - ln -s ${common.root}/runtime/* $out - ln -sf ${helixSyntax}/runtime/grammars $out - ''; - in - lib.optionalAttrs (common.memberName == "helix-term") { - nativeBuildInputs = [ pkgs.makeWrapper ]; - postFixup = '' - if [ -f "$out/bin/hx" ]; then - wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}" - fi - ''; - }; + }; shell = common: prev: { - packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]); + packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin cargo-flamegraph ]); env = prev.env ++ [ { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; } { name = "RUST_BACKTRACE"; value = "1"; } - { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; } + { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"; } ]; }; }; diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index ea695d34..7ff91cfd 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-core" -version = "0.5.0" +version = "0.6.0" authors = ["Blaž Hrastnik <blaz@mxxn.io>"] edition = "2021" license = "MPL-2.0" @@ -13,17 +13,18 @@ include = ["src/**/*", "README.md"] [features] [dependencies] -helix-syntax = { version = "0.5", path = "../helix-syntax" } +helix-syntax = { version = "0.6", path = "../helix-syntax" } ropey = "1.3" -smallvec = "1.7" -tendril = "0.4.2" -unicode-segmentation = "1.8" +smallvec = "1.8" +smartstring = "0.2.9" +unicode-segmentation = "1.9" unicode-width = "0.1" -unicode-general-category = "0.4" +unicode-general-category = "0.5" # slab = "0.4.2" +slotmap = "1.0" tree-sitter = "0.20" -once_cell = "1.8" +once_cell = "1.9" arc-swap = "1" regex = "1" @@ -35,6 +36,9 @@ toml = "0.5" similar = "2.1" etcetera = "0.3" +encoding_rs = "0.8" + +chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index cc966852..f4359a34 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -1,7 +1,10 @@ //! When typing the opening character of one of the possible pairs defined below, //! this module provides the functionality to insert the paired closing character. -use crate::{Range, Rope, Selection, Tendril, Transaction}; +use crate::{ + graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction, +}; +use log::debug; use smallvec::SmallVec; // Heavily based on https://github.com/codemirror/closebrackets/ @@ -15,7 +18,9 @@ pub const PAIRS: &[(char, char)] = &[ ('`', '`'), ]; -const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines +// [TODO] build this dynamically in language config. see #992 +const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; +const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines // insert hook: // Fn(doc, selection, char) => Option<Transaction> @@ -25,14 +30,19 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202 // // to simplify, maybe return Option<Transaction> and just reimplement the default -// TODO: delete implementation where it erases the whole bracket (|) -> | +// [TODO] +// * delete implementation where it erases the whole bracket (|) -> | +// * change to multi character pairs to handle cases like placing the cursor in the +// middle of triple quotes, and more exotic pairs like Jinja's {% %} #[must_use] pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> { + debug!("autopairs hook selection: {:#?}", selection); + for &(open, close) in PAIRS { if open == ch { if open == close { - return handle_same(doc, selection, open); + return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE)); } else { return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE)); } @@ -47,18 +57,145 @@ pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> None } -// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close ' -// for example "&'a mut", or "fn<'a>" - -fn next_char(doc: &Rope, pos: usize) -> Option<char> { - if pos >= doc.len_chars() { +fn prev_char(doc: &Rope, pos: usize) -> Option<char> { + if pos == 0 { return None; } - Some(doc.char(pos)) + + doc.get_char(pos - 1) +} + +fn is_single_grapheme(doc: &Rope, range: &Range) -> bool { + let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to())); + let first = graphemes.next(); + let second = graphemes.next(); + debug!("first: {:#?}, second: {:#?}", first, second); + first.is_some() && second.is_none() +} + +/// calculate what the resulting range should be for an auto pair insertion +fn get_next_range( + doc: &Rope, + start_range: &Range, + offset: usize, + typed_char: char, + len_inserted: usize, +) -> Range { + // When the character under the cursor changes due to complete pair + // insertion, we must look backward a grapheme and then add the length + // of the insertion to put the resulting cursor in the right place, e.g. + // + // foo[\r\n] - anchor: 3, head: 5 + // foo([)]\r\n - anchor: 4, head: 5 + // + // foo[\r\n] - anchor: 3, head: 5 + // foo'[\r\n] - anchor: 4, head: 6 + // + // foo([)]\r\n - anchor: 4, head: 5 + // foo()[\r\n] - anchor: 5, head: 7 + // + // [foo]\r\n - anchor: 0, head: 3 + // [foo(])\r\n - anchor: 0, head: 5 + + // inserting at the very end of the document after the last newline + if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() { + return Range::new( + start_range.anchor + offset + typed_char.len_utf8(), + start_range.head + offset + typed_char.len_utf8(), + ); + } + + let single_grapheme = is_single_grapheme(doc, start_range); + let doc_slice = doc.slice(..); + + // just skip over graphemes + if len_inserted == 0 { + let end_anchor = if single_grapheme { + graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset + + // even for backward inserts with multiple grapheme selections, + // we want the anchor to stay where it is so that the relative + // selection does not change, e.g.: + // + // foo([) wor]d -> insert ) -> foo()[ wor]d + } else { + start_range.anchor + offset + }; + + return Range::new( + end_anchor, + graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset, + ); + } + + // trivial case: only inserted a single-char opener, just move the selection + if len_inserted == 1 { + let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward { + start_range.anchor + offset + typed_char.len_utf8() + } else { + start_range.anchor + offset + }; + + return Range::new( + end_anchor, + start_range.head + offset + typed_char.len_utf8(), + ); + } + + // If the head = 0, then we must be in insert mode with a backward + // cursor, which implies the head will just move + let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward { + start_range.head + offset + typed_char.len_utf8() + } else { + // We must have a forward cursor, which means we must move to the + // other end of the grapheme to get to where the new characters + // are inserted, then move the head to where it should be + let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head); + debug!( + "prev_bound: {}, offset: {}, len_inserted: {}", + prev_bound, offset, len_inserted + ); + prev_bound + offset + len_inserted + }; + + let end_anchor = match (start_range.len(), start_range.direction()) { + // if we have a zero width cursor, it shifts to the same number + (0, _) => end_head, + + // If we are inserting for a regular one-width cursor, the anchor + // moves with the head. This is the fast path for ASCII. + (1, Direction::Forward) => end_head - 1, + (1, Direction::Backward) => end_head + 1, + + (_, Direction::Forward) => { + if single_grapheme { + graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head) + + typed_char.len_utf8() + + // if we are appending, the anchor stays where it is; only offset + // for multiple range insertions + } else { + start_range.anchor + offset + } + } + + (_, Direction::Backward) => { + if single_grapheme { + // if we're backward, then the head is at the first char + // of the typed char, so we need to add the length of + // the closing char + graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted + } else { + // when we are inserting in front of a selection, we need to move + // the anchor over by however many characters were inserted overall + start_range.anchor + offset + len_inserted + } + } + }; + + Range::new(end_anchor, end_head) } -// TODO: selections should be extended if range, moved if point. -// TODO: if not cursor but selection, wrap on both sides of selection (surround) fn handle_open( doc: &Rope, selection: &Selection, @@ -66,98 +203,584 @@ fn handle_open( close: char, close_before: &str, ) -> Transaction { - let mut ranges = SmallVec::with_capacity(selection.len()); - + let mut end_ranges = SmallVec::with_capacity(selection.len()); let mut offs = 0; - let transaction = Transaction::change_by_selection(doc, selection, |range| { - let pos = range.head; - let next = next_char(doc, pos); - - let head = pos + offs + open.len_utf8(); - // if selection, retain anchor, if cursor, move over - ranges.push(Range::new( - if range.is_empty() { - head - } else { - range.anchor + offs - }, - head, - )); + let transaction = Transaction::change_by_selection(doc, selection, |start_range| { + let cursor = start_range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor); + let len_inserted; - match next { + let change = match next_char { Some(ch) if !close_before.contains(ch) => { - offs += 1; - // TODO: else return (use default handler that inserts open) - (pos, pos, Some(Tendril::from_char(open))) + len_inserted = open.len_utf8(); + let mut tendril = Tendril::new(); + tendril.push(open); + (cursor, cursor, Some(tendril)) } // None | Some(ch) if close_before.contains(ch) => {} _ => { // insert open & close - let mut pair = Tendril::with_capacity(2); - pair.push_char(open); - pair.push_char(close); + let pair = Tendril::from_iter([open, close]); + len_inserted = open.len_utf8() + close.len_utf8(); + (cursor, cursor, Some(pair)) + } + }; - offs += 2; + let next_range = get_next_range(doc, start_range, offs, open, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; - (pos, pos, Some(pair)) - } - } + change }); - transaction.with_selection(Selection::new(ranges, selection.primary_index())) + let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); + debug!("auto pair transaction: {:#?}", t); + t } fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction { - let mut ranges = SmallVec::with_capacity(selection.len()); + let mut end_ranges = SmallVec::with_capacity(selection.len()); let mut offs = 0; - let transaction = Transaction::change_by_selection(doc, selection, |range| { - let pos = range.head; - let next = next_char(doc, pos); + let transaction = Transaction::change_by_selection(doc, selection, |start_range| { + let cursor = start_range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor); + let mut len_inserted = 0; - let head = pos + offs + close.len_utf8(); - // if selection, retain anchor, if cursor, move over - ranges.push(Range::new( - if range.is_empty() { - head - } else { - range.anchor + offs - }, - head, - )); + let change = if next_char == Some(close) { + // return transaction that moves past close + (cursor, cursor, None) // no-op + } else { + len_inserted += close.len_utf8(); + let mut tendril = Tendril::new(); + tendril.push(close); + (cursor, cursor, Some(tendril)) + }; + + let next_range = get_next_range(doc, start_range, offs, close, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; + + change + }); + + let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); + debug!("auto pair transaction: {:#?}", t); + t +} + +/// handle cases where open and close is the same, or in triples ("""docstring""") +fn handle_same( + doc: &Rope, + selection: &Selection, + token: char, + close_before: &str, + open_before: &str, +) -> Transaction { + let mut end_ranges = SmallVec::with_capacity(selection.len()); + + let mut offs = 0; - if next == Some(close) { + let transaction = Transaction::change_by_selection(doc, selection, |start_range| { + let cursor = start_range.cursor(doc.slice(..)); + let mut len_inserted = 0; + + let next_char = doc.get_char(cursor); + let prev_char = prev_char(doc, cursor); + + let change = if next_char == Some(token) { // return transaction that moves past close - (pos, pos, None) // no-op + (cursor, cursor, None) // no-op } else { - offs += close.len_utf8(); + let mut pair = Tendril::new(); + pair.push(token); - // TODO: else return (use default handler that inserts close) - (pos, pos, Some(Tendril::from_char(close))) - } + // for equal pairs, don't insert both open and close if either + // side has a non-pair char + if (next_char.is_none() || close_before.contains(next_char.unwrap())) + && (prev_char.is_none() || open_before.contains(prev_char.unwrap())) + { + pair.push(token); + } + + len_inserted += pair.len(); + (cursor, cursor, Some(pair)) + }; + + let next_range = get_next_range(doc, start_range, offs, token, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; + + change }); - transaction.with_selection(Selection::new(ranges, selection.primary_index())) + let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); + debug!("auto pair transaction: {:#?}", t); + t } -// handle cases where open and close is the same, or in triples ("""docstring""") -fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> { - // if not cursor but selection, wrap - // let next = next char - - // if next == bracket { - // // if start of syntax node, insert token twice (new pair because node is complete) - // // elseif colsedBracketAt - // // is_triple == allow triple && next 3 is equal - // // cursor jump over - // } - //} else if allow_triple && followed by triple { - //} - //} else if next != word char && prev != bracket && prev != word char { - // // condition checks for cases like I' where you don't want I'' (or I'm) - // insert pair ("") - //} - None +#[cfg(test)] +mod test { + use super::*; + use smallvec::smallvec; + + const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str(); + + fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> { + PAIRS.iter().filter(|(open, close)| open != close) + } + + fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> { + PAIRS.iter().filter(|(open, close)| open == close) + } + + fn test_hooks( + in_doc: &Rope, + in_sel: &Selection, + ch: char, + expected_doc: &Rope, + expected_sel: &Selection, + ) { + let trans = hook(in_doc, in_sel, ch).unwrap(); + let mut actual_doc = in_doc.clone(); + assert!(trans.apply(&mut actual_doc)); + assert_eq!(expected_doc, &actual_doc); + assert_eq!(expected_sel, trans.selection().unwrap()); + } + + fn test_hooks_with_pairs<I, F, R>( + in_doc: &Rope, + in_sel: &Selection, + pairs: I, + get_expected_doc: F, + actual_sel: &Selection, + ) where + I: IntoIterator<Item = &'static (char, char)>, + F: Fn(char, char) -> R, + R: Into<Rope>, + Rope: From<R>, + { + pairs.into_iter().for_each(|(open, close)| { + test_hooks( + in_doc, + in_sel, + *open, + &Rope::from(get_expected_doc(*open, *close)), + actual_sel, + ) + }); + } + + // [] indicates range + + /// [] -> insert ( -> ([]) + #[test] + fn test_insert_blank() { + test_hooks_with_pairs( + &Rope::from(LINE_END), + &Selection::single(1, 0), + PAIRS, + |open, close| format!("{}{}{}", open, close, LINE_END), + &Selection::single(2, 1), + ); + + let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)); + + test_hooks_with_pairs( + &empty_doc, + &Selection::single(empty_doc.len_chars(), LINE_END.len()), + PAIRS, + |open, close| { + format!( + "{line_end}{open}{close}{line_end}", + open = open, + close = close, + line_end = LINE_END + ) + }, + &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1), + ); + } + + #[test] + fn test_insert_before_multi_code_point_graphemes() { + test_hooks_with_pairs( + &Rope::from(format!("hello 👨👩👧👦 goodbye{}", LINE_END)), + &Selection::single(13, 6), + PAIRS, + |open, _| format!("hello {}👨👩👧👦 goodbye{}", open, LINE_END), + &Selection::single(14, 7), + ); + } + + #[test] + fn test_insert_at_end_of_document() { + test_hooks_with_pairs( + &Rope::from(LINE_END), + &Selection::single(LINE_END.len(), LINE_END.len()), + PAIRS, + |open, close| format!("{}{}{}", LINE_END, open, close), + &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1), + ); + + test_hooks_with_pairs( + &Rope::from(format!("foo{}", LINE_END)), + &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()), + PAIRS, + |open, close| format!("foo{}{}{}", LINE_END, open, close), + &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4), + ); + } + + /// [] -> append ( -> ([]) + #[test] + fn test_append_blank() { + test_hooks_with_pairs( + // this is what happens when you have a totally blank document and then append + &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)), + // before inserting the pair, the cursor covers all of both empty lines + &Selection::single(0, LINE_END.len() * 2), + PAIRS, + |open, close| { + format!( + "{line_end}{open}{close}{line_end}", + line_end = LINE_END, + open = open, + close = close + ) + }, + // after inserting pair, the cursor covers the first new line and the open char + &Selection::single(0, LINE_END.len() + 2), + ); + } + + /// [] ([]) + /// [] -> insert -> ([]) + /// [] ([]) + #[test] + fn test_insert_blank_multi_cursor() { + test_hooks_with_pairs( + &Rope::from("\n\n\n"), + &Selection::new( + smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),), + 0, + ), + PAIRS, + |open, close| { + format!( + "{open}{close}\n{open}{close}\n{open}{close}\n", + open = open, + close = close + ) + }, + &Selection::new( + smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),), + 0, + ), + ); + } + + /// fo[o] -> append ( -> fo[o(]) + #[test] + fn test_append() { + test_hooks_with_pairs( + &Rope::from("foo\n"), + &Selection::single(2, 4), + differing_pairs(), + |open, close| format!("foo{}{}\n", open, close), + &Selection::single(2, 5), + ); + } + + /// foo[] -> append to end of line ( -> foo([]) + #[test] + fn test_append_single_cursor() { + test_hooks_with_pairs( + &Rope::from(format!("foo{}", LINE_END)), + &Selection::single(3, 3 + LINE_END.len()), + differing_pairs(), + |open, close| format!("foo{}{}{}", open, close, LINE_END), + &Selection::single(4, 5), + ); + } + + /// fo[o] fo[o(]) + /// fo[o] -> append ( -> fo[o(]) + /// fo[o] fo[o(]) + #[test] + fn test_append_multi() { + test_hooks_with_pairs( + &Rope::from("foo\nfoo\nfoo\n"), + &Selection::new( + smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)), + 0, + ), + differing_pairs(), + |open, close| { + format!( + "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n", + open = open, + close = close + ) + }, + &Selection::new( + smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)), + 0, + ), + ); + } + + /// ([)] -> insert ) -> ()[] + #[test] + fn test_insert_close_inside_pair() { + for (open, close) in PAIRS { + let doc = Rope::from(format!("{}{}{}", open, close, LINE_END)); + + test_hooks( + &doc, + &Selection::single(2, 1), + *close, + &doc, + &Selection::single(2 + LINE_END.len(), 2), + ); + } + } + + /// [(]) -> append ) -> [()] + #[test] + fn test_append_close_inside_pair() { + for (open, close) in PAIRS { + let doc = Rope::from(format!("{}{}{}", open, close, LINE_END)); + + test_hooks( + &doc, + &Selection::single(0, 2), + *close, + &doc, + &Selection::single(0, 2 + LINE_END.len()), + ); + } + } + + /// ([]) ()[] + /// ([]) -> insert ) -> ()[] + /// ([]) ()[] + #[test] + fn test_insert_close_inside_pair_multi_cursor() { + let sel = Selection::new( + smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),), + 0, + ); + + let expected_sel = Selection::new( + smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),), + 0, + ); + + for (open, close) in PAIRS { + let doc = Rope::from(format!( + "{open}{close}\n{open}{close}\n{open}{close}\n", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *close, &doc, &expected_sel); + } + } + + /// [(]) [()] + /// [(]) -> append ) -> [()] + /// [(]) [()] + #[test] + fn test_append_close_inside_pair_multi_cursor() { + let sel = Selection::new( + smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),), + 0, + ); + + let expected_sel = Selection::new( + smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),), + 0, + ); + + for (open, close) in PAIRS { + let doc = Rope::from(format!( + "{open}{close}\n{open}{close}\n{open}{close}\n", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *close, &doc, &expected_sel); + } + } + + /// ([]) -> insert ( -> (([])) + #[test] + fn test_insert_open_inside_pair() { + let sel = Selection::single(2, 1); + let expected_sel = Selection::single(3, 2); + + for (open, close) in differing_pairs() { + let doc = Rope::from(format!("{}{}", open, close)); + let expected_doc = Rope::from(format!( + "{open}{open}{close}{close}", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel); + } + } + + /// [word(]) -> append ( -> [word((])) + #[test] + fn test_append_open_inside_pair() { + let sel = Selection::single(0, 6); + let expected_sel = Selection::single(0, 7); + + for (open, close) in differing_pairs() { + let doc = Rope::from(format!("word{}{}", open, close)); + let expected_doc = Rope::from(format!( + "word{open}{open}{close}{close}", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel); + } + } + + /// ([]) -> insert " -> ("[]") + #[test] + fn test_insert_nested_open_inside_pair() { + let sel = Selection::single(2, 1); + let expected_sel = Selection::single(3, 2); + + for (outer_open, outer_close) in differing_pairs() { + let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); + + for (inner_open, inner_close) in matching_pairs() { + let expected_doc = Rope::from(format!( + "{}{}{}{}", + outer_open, inner_open, inner_close, outer_close + )); + + test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel); + } + } + } + + /// [(]) -> append " -> [("]") + #[test] + fn test_append_nested_open_inside_pair() { + let sel = Selection::single(0, 2); + let expected_sel = Selection::single(0, 3); + + for (outer_open, outer_close) in differing_pairs() { + let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); + + for (inner_open, inner_close) in matching_pairs() { + let expected_doc = Rope::from(format!( + "{}{}{}{}", + outer_open, inner_open, inner_close, outer_close + )); + + test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel); + } + } + } + + /// []word -> insert ( -> ([]word + #[test] + fn test_insert_open_before_non_pair() { + test_hooks_with_pairs( + &Rope::from("word"), + &Selection::single(1, 0), + PAIRS, + |open, _| format!("{}word", open), + &Selection::single(2, 1), + ) + } + + /// [wor]d -> insert ( -> ([wor]d + #[test] + fn test_insert_open_with_selection() { + test_hooks_with_pairs( + &Rope::from("word"), + &Selection::single(3, 0), + PAIRS, + |open, _| format!("{}word", open), + &Selection::single(4, 1), + ) + } + + /// [wor]d -> append ) -> [wor)]d + #[test] + fn test_append_close_inside_non_pair_with_selection() { + let sel = Selection::single(0, 4); + let expected_sel = Selection::single(0, 5); + + for (_, close) in PAIRS { + let doc = Rope::from("word"); + let expected_doc = Rope::from(format!("wor{}d", close)); + test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel); + } + } + + /// foo[ wor]d -> insert ( -> foo([) wor]d + #[test] + fn test_insert_open_trailing_word_with_selection() { + test_hooks_with_pairs( + &Rope::from("foo word"), + &Selection::single(7, 3), + differing_pairs(), + |open, close| format!("foo{}{} word", open, close), + &Selection::single(9, 4), + ) + } + + /// foo([) wor]d -> insert ) -> foo()[ wor]d + #[test] + fn test_insert_close_inside_pair_trailing_word_with_selection() { + for (open, close) in differing_pairs() { + test_hooks( + &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)), + &Selection::single(9, 4), + *close, + &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)), + &Selection::single(9, 5), + ) + } + } + + /// we want pairs that are *not* the same char to be inserted after + /// a non-pair char, for cases like functions, but for pairs that are + /// the same char, we want to *not* insert a pair to handle cases like "I'm" + /// + /// word[] -> insert ( -> word([]) + /// word[] -> insert ' -> word'[] + #[test] + fn test_insert_open_after_non_pair() { + let doc = Rope::from(format!("word{}", LINE_END)); + let sel = Selection::single(5, 4); + let expected_sel = Selection::single(6, 5); + + test_hooks_with_pairs( + &doc, + &sel, + differing_pairs(), + |open, close| format!("word{}{}{}", open, close, LINE_END), + &expected_sel, + ); + + test_hooks_with_pairs( + &doc, + &sel, + matching_pairs(), + |open, _| format!("word{}{}", open, LINE_END), + &expected_sel, + ); + } } diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs index c8e5efbd..54991574 100644 --- a/helix-core/src/chars.rs +++ b/helix-core/src/chars.rs @@ -91,12 +91,11 @@ mod test { #[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 EOL_TEST_CASE: &str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; + const WORD_TEST_CASE: &str = "_hello_world_あいうえおー12345678901234567890"; + const PUNCTUATION_TEST_CASE: &str = "!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~"; - const WHITESPACE_TEST_CASE: &'static str = " "; + const WHITESPACE_TEST_CASE: &str = " "; for ch in EOL_TEST_CASE.chars() { assert_eq!(CharCategory::Eol, categorize_char(ch)); diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 4fcf51c9..210ad639 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -1,12 +1,19 @@ //! LSP diagnostic utility types. +use serde::{Deserialize, Serialize}; /// Describes the severity level of a [`Diagnostic`]. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] pub enum Severity { - Error, - Warning, - Info, Hint, + Info, + Warning, + Error, +} + +impl Default for Severity { + fn default() -> Self { + Self::Hint + } } /// A range of `char`s within the text. diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs index a83db333..6960c679 100644 --- a/helix-core/src/diff.rs +++ b/helix-core/src/diff.rs @@ -11,10 +11,6 @@ pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction { // A timeout is set so after 1 seconds, the algorithm will start // approximating. This is especially important for big `Rope`s or // `Rope`s that are extremely dissimilar to each other. - // - // Note: Ignore the clippy warning, as the trait bounds of - // `Transaction::change()` require an iterator implementing - // `ExactIterator`. let mut config = similar::TextDiff::configure(); config.timeout(std::time::Duration::from_secs(1)); @@ -62,7 +58,7 @@ mod tests { let mut old = Rope::from(a); let new = Rope::from(b); compare_ropes(&old, &new).apply(&mut old); - old.to_string() == new.to_string() + old == new } } } diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index c6398875..aa898684 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -120,6 +120,43 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) - chunk_char_idx + tmp } +#[must_use] +pub fn nth_next_grapheme_boundary_byte(slice: RopeSlice, mut byte_idx: usize, n: usize) -> usize { + // Bounds check + debug_assert!(byte_idx <= slice.len_bytes()); + + // Get the chunk with our byte index in it. + let (mut chunk, mut chunk_byte_idx, mut _chunk_char_idx, _) = slice.chunk_at_byte(byte_idx); + + // Set up the grapheme cursor. + let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true); + + // Find the nth next grapheme cluster boundary. + for _ in 0..n { + loop { + match gc.next_boundary(chunk, chunk_byte_idx) { + Ok(None) => return slice.len_bytes(), + Ok(Some(n)) => { + byte_idx = n; + break; + } + Err(GraphemeIncomplete::NextChunk) => { + chunk_byte_idx += chunk.len(); + let (a, _, _c, _) = slice.chunk_at_byte(chunk_byte_idx); + chunk = a; + // chunk_char_idx = c; + } + Err(GraphemeIncomplete::PreContext(n)) => { + let ctx_chunk = slice.chunk_at_byte(n - 1).0; + gc.provide_context(ctx_chunk, n - ctx_chunk.len()); + } + _ => unreachable!(), + } + } + } + byte_idx +} + /// Finds the next grapheme boundary after the given char position. #[must_use] #[inline(always)] @@ -127,6 +164,13 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize { nth_next_grapheme_boundary(slice, char_idx, 1) } +/// Finds the next grapheme boundary after the given byte position. +#[must_use] +#[inline(always)] +pub fn next_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> usize { + nth_next_grapheme_boundary_byte(slice, byte_idx, 1) +} + /// Returns the passed char index if it's already a grapheme boundary, /// or the next grapheme boundary char index if not. #[must_use] @@ -151,6 +195,23 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize } } +/// Returns the passed byte index if it's already a grapheme boundary, +/// or the next grapheme boundary byte index if not. +#[must_use] +#[inline] +pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize { + if byte_idx == 0 { + byte_idx + } else { + // TODO: optimize so we're not constructing grapheme cursor twice + if is_grapheme_boundary_byte(slice, byte_idx) { + byte_idx + } else { + next_grapheme_boundary_byte(slice, byte_idx) + } + } +} + /// Returns whether the given char position is a grapheme boundary. #[must_use] pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool { @@ -179,6 +240,31 @@ pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool { } } +/// Returns whether the given byte position is a grapheme boundary. +#[must_use] +pub fn is_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> bool { + // Bounds check + debug_assert!(byte_idx <= slice.len_bytes()); + + // Get the chunk with our byte index in it. + let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx); + + // Set up the grapheme cursor. + let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true); + + // Determine if the given position is a grapheme cluster boundary. + loop { + match gc.is_boundary(chunk, chunk_byte_idx) { + Ok(n) => return n, + Err(GraphemeIncomplete::PreContext(n)) => { + let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1); + gc.provide_context(ctx_chunk, ctx_byte_start); + } + Err(_) => unreachable!(), + } + } +} + /// An iterator over the graphemes of a `RopeSlice`. #[derive(Clone)] pub struct RopeGraphemes<'a> { diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 4b1c8d3b..bb95213c 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -448,8 +448,8 @@ mod test { change: crate::transaction::Change, instant: Instant, ) { - let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter()); - history.commit_revision_at_timestamp(&txn, &state, instant); + let txn = Transaction::change(&state.doc, vec![change].into_iter()); + history.commit_revision_at_timestamp(&txn, state, instant); txn.apply(&mut state.doc); } diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs new file mode 100644 index 00000000..91fa5963 --- /dev/null +++ b/helix-core/src/increment/date_time.rs @@ -0,0 +1,490 @@ +use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +use once_cell::sync::Lazy; +use regex::Regex; +use ropey::RopeSlice; + +use std::borrow::Cow; +use std::cmp; + +use super::Increment; +use crate::{Range, Tendril}; + +#[derive(Debug, PartialEq, Eq)] +pub struct DateTimeIncrementor { + date_time: NaiveDateTime, + range: Range, + fmt: &'static str, + field: DateField, +} + +impl DateTimeIncrementor { + pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> { + let range = if range.is_empty() { + if range.anchor < text.len_chars() { + // Treat empty range as a cursor range. + range.put_cursor(text, range.anchor + 1, true) + } else { + // The range is empty and at the end of the text. + return None; + } + } else { + range + }; + + FORMATS.iter().find_map(|format| { + let from = range.from().saturating_sub(format.max_len); + let to = (range.from() + format.max_len).min(text.len_chars()); + + let (from_in_text, to_in_text) = (range.from() - from, range.to() - from); + let text: Cow<str> = text.slice(from..to).into(); + + let captures = format.regex.captures(&text)?; + if captures.len() - 1 != format.fields.len() { + return None; + } + + let date_time = captures.get(0)?; + let offset = range.from() - from_in_text; + let range = Range::new(date_time.start() + offset, date_time.end() + offset); + + let field = captures + .iter() + .skip(1) + .enumerate() + .find_map(|(i, capture)| { + let capture = capture?; + let capture_range = capture.range(); + + if capture_range.contains(&from_in_text) + && capture_range.contains(&(to_in_text - 1)) + { + Some(format.fields[i]) + } else { + None + } + })?; + + let has_date = format.fields.iter().any(|f| f.unit.is_date()); + let has_time = format.fields.iter().any(|f| f.unit.is_time()); + + let date_time = &text[date_time.start()..date_time.end()]; + let date_time = match (has_date, has_time) { + (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?, + (true, false) => { + let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?; + + date.and_hms(0, 0, 0) + } + (false, true) => { + let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?; + + NaiveDate::from_ymd(0, 1, 1).and_time(time) + } + (false, false) => return None, + }; + + Some(DateTimeIncrementor { + date_time, + range, + fmt: format.fmt, + field, + }) + }) + } +} + +impl Increment for DateTimeIncrementor { + fn increment(&self, amount: i64) -> (Range, Tendril) { + let date_time = match self.field.unit { + DateUnit::Years => add_years(self.date_time, amount), + DateUnit::Months => add_months(self.date_time, amount), + DateUnit::Days => add_duration(self.date_time, Duration::days(amount)), + DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)), + DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)), + DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)), + DateUnit::AmPm => toggle_am_pm(self.date_time), + } + .unwrap_or(self.date_time); + + (self.range, date_time.format(self.fmt).to_string().into()) + } +} + +static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| { + vec![ + Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23 + Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23 + Format::new("%Y-%m-%d %H:%M"), // 2021-11-24 07:12 + Format::new("%Y/%m/%d %H:%M"), // 2021/11/24 07:12 + Format::new("%Y-%m-%d"), // 2021-11-24 + Format::new("%Y/%m/%d"), // 2021/11/24 + Format::new("%a %b %d %Y"), // Wed Nov 24 2021 + Format::new("%d-%b-%Y"), // 24-Nov-2021 + Format::new("%Y %b %d"), // 2021 Nov 24 + Format::new("%b %d, %Y"), // Nov 24, 2021 + Format::new("%-I:%M:%S %P"), // 7:21:53 am + Format::new("%-I:%M %P"), // 7:21 am + Format::new("%-I:%M:%S %p"), // 7:21:53 AM + Format::new("%-I:%M %p"), // 7:21 AM + Format::new("%H:%M:%S"), // 23:24:23 + Format::new("%H:%M"), // 23:24 + ] +}); + +#[derive(Debug)] +struct Format { + fmt: &'static str, + fields: Vec<DateField>, + regex: Regex, + max_len: usize, +} + +impl Format { + fn new(fmt: &'static str) -> Self { + let mut remaining = fmt; + let mut fields = Vec::new(); + let mut regex = String::new(); + let mut max_len = 0; + + while let Some(i) = remaining.find('%') { + let after = &remaining[i + 1..]; + let mut chars = after.chars(); + let c = chars.next().unwrap(); + + let spec_len = if c == '-' { + 1 + chars.next().unwrap().len_utf8() + } else { + c.len_utf8() + }; + + let specifier = &after[..spec_len]; + let field = DateField::from_specifier(specifier).unwrap(); + fields.push(field); + max_len += field.max_len + remaining[..i].len(); + regex += &remaining[..i]; + regex += &format!("({})", field.regex); + remaining = &after[spec_len..]; + } + + let regex = Regex::new(®ex).unwrap(); + + Self { + fmt, + fields, + regex, + max_len, + } + } +} + +impl PartialEq for Format { + fn eq(&self, other: &Self) -> bool { + self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len + } +} + +impl Eq for Format {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct DateField { + regex: &'static str, + unit: DateUnit, + max_len: usize, +} + +impl DateField { + fn from_specifier(specifier: &str) -> Option<Self> { + match specifier { + "Y" => Some(Self { + regex: r"\d{4}", + unit: DateUnit::Years, + max_len: 5, + }), + "y" => Some(Self { + regex: r"\d\d", + unit: DateUnit::Years, + max_len: 2, + }), + "m" => Some(Self { + regex: r"[0-1]\d", + unit: DateUnit::Months, + max_len: 2, + }), + "d" => Some(Self { + regex: r"[0-3]\d", + unit: DateUnit::Days, + max_len: 2, + }), + "-d" => Some(Self { + regex: r"[1-3]?\d", + unit: DateUnit::Days, + max_len: 2, + }), + "a" => Some(Self { + regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat", + unit: DateUnit::Days, + max_len: 3, + }), + "A" => Some(Self { + regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday", + unit: DateUnit::Days, + max_len: 9, + }), + "b" | "h" => Some(Self { + regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", + unit: DateUnit::Months, + max_len: 3, + }), + "B" => Some(Self { + regex: r"January|February|March|April|May|June|July|August|September|October|November|December", + unit: DateUnit::Months, + max_len: 9, + }), + "H" => Some(Self { + regex: r"[0-2]\d", + unit: DateUnit::Hours, + max_len: 2, + }), + "M" => Some(Self { + regex: r"[0-5]\d", + unit: DateUnit::Minutes, + max_len: 2, + }), + "S" => Some(Self { + regex: r"[0-5]\d", + unit: DateUnit::Seconds, + max_len: 2, + }), + "I" => Some(Self { + regex: r"[0-1]\d", + unit: DateUnit::Hours, + max_len: 2, + }), + "-I" => Some(Self { + regex: r"1?\d", + unit: DateUnit::Hours, + max_len: 2, + }), + "P" => Some(Self { + regex: r"am|pm", + unit: DateUnit::AmPm, + max_len: 2, + }), + "p" => Some(Self { + regex: r"AM|PM", + unit: DateUnit::AmPm, + max_len: 2, + }), + _ => None, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum DateUnit { + Years, + Months, + Days, + Hours, + Minutes, + Seconds, + AmPm, +} + +impl DateUnit { + fn is_date(self) -> bool { + matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days) + } + + fn is_time(self) -> bool { + matches!( + self, + DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds + ) + } +} + +fn ndays_in_month(year: i32, month: u32) -> u32 { + // The first day of the next month... + let (y, m) = if month == 12 { + (year + 1, 1) + } else { + (year, month + 1) + }; + let d = NaiveDate::from_ymd(y, m, 1); + + // ...is preceded by the last day of the original month. + d.pred().day() +} + +fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> { + let month = (date_time.month0() as i64).checked_add(amount)?; + let year = date_time.year() + i32::try_from(month / 12).ok()?; + let year = if month.is_negative() { year - 1 } else { year }; + + // Normalize month + let month = month % 12; + let month = if month.is_negative() { + month + 12 + } else { + month + } as u32 + + 1; + + let day = cmp::min(date_time.day(), ndays_in_month(year, month)); + + Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time())) +} + +fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> { + let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?; + let ndays = ndays_in_month(year, date_time.month()); + + if date_time.day() > ndays { + let d = NaiveDate::from_ymd(year, date_time.month(), ndays); + Some(d.succ().and_time(date_time.time())) + } else { + date_time.with_year(year) + } +} + +fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> { + date_time.checked_add_signed(duration) +} + +fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> { + if date_time.hour() < 12 { + add_duration(date_time, Duration::hours(12)) + } else { + add_duration(date_time, Duration::hours(-12)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_increment_date_times() { + let tests = [ + // (original, cursor, amount, expected) + ("2020-02-28", 0, 1, "2021-02-28"), + ("2020-02-29", 0, 1, "2021-03-01"), + ("2020-01-31", 5, 1, "2020-02-29"), + ("2020-01-20", 5, 1, "2020-02-20"), + ("2021-01-01", 5, -1, "2020-12-01"), + ("2021-01-31", 5, -2, "2020-11-30"), + ("2020-02-28", 8, 1, "2020-02-29"), + ("2021-02-28", 8, 1, "2021-03-01"), + ("2021-02-28", 0, -1, "2020-02-28"), + ("2021-03-01", 0, -1, "2020-03-01"), + ("2020-02-29", 5, -1, "2020-01-29"), + ("2020-02-20", 5, -1, "2020-01-20"), + ("2020-02-29", 8, -1, "2020-02-28"), + ("2021-03-01", 8, -1, "2021-02-28"), + ("1980/12/21", 8, 100, "1981/03/31"), + ("1980/12/21", 8, -100, "1980/09/12"), + ("1980/12/21", 8, 1000, "1983/09/17"), + ("1980/12/21", 8, -1000, "1978/03/27"), + ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"), + ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"), + ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"), + ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"), + ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"), + ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"), + ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"), + ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"), + ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"), + ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"), + ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"), + ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"), + ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"), + ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"), + ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"), + ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"), + ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"), + ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"), + ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"), + ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"), + ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"), + ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"), + ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"), + ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"), + ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"), + ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"), + ("24-Nov-2021", 0, 1, "25-Nov-2021"), + ("24-Nov-2021", 3, 1, "24-Dec-2021"), + ("24-Nov-2021", 7, 1, "24-Nov-2022"), + ("2021 Nov 24", 0, 1, "2022 Nov 24"), + ("2021 Nov 24", 5, 1, "2021 Dec 24"), + ("2021 Nov 24", 9, 1, "2021 Nov 25"), + ("Nov 24, 2021", 0, 1, "Dec 24, 2021"), + ("Nov 24, 2021", 4, 1, "Nov 25, 2021"), + ("Nov 24, 2021", 8, 1, "Nov 24, 2022"), + ("7:21:53 am", 0, 1, "8:21:53 am"), + ("7:21:53 am", 3, 1, "7:22:53 am"), + ("7:21:53 am", 5, 1, "7:21:54 am"), + ("7:21:53 am", 8, 1, "7:21:53 pm"), + ("7:21:53 AM", 0, 1, "8:21:53 AM"), + ("7:21:53 AM", 3, 1, "7:22:53 AM"), + ("7:21:53 AM", 5, 1, "7:21:54 AM"), + ("7:21:53 AM", 8, 1, "7:21:53 PM"), + ("7:21 am", 0, 1, "8:21 am"), + ("7:21 am", 3, 1, "7:22 am"), + ("7:21 am", 5, 1, "7:21 pm"), + ("7:21 AM", 0, 1, "8:21 AM"), + ("7:21 AM", 3, 1, "7:22 AM"), + ("7:21 AM", 5, 1, "7:21 PM"), + ("23:24:23", 1, 1, "00:24:23"), + ("23:24:23", 3, 1, "23:25:23"), + ("23:24:23", 6, 1, "23:24:24"), + ("23:24", 1, 1, "00:24"), + ("23:24", 3, 1, "23:25"), + ]; + + for (original, cursor, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateTimeIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + Tendril::from(expected) + ); + } + } + + #[test] + fn test_invalid_date_times() { + let tests = [ + "0000-00-00", + "1980-2-21", + "1980-12-1", + "12345", + "2020-02-30", + "1999-12-32", + "19-12-32", + "1-2-3", + "0000/00/00", + "1980/2/21", + "1980/12/1", + "12345", + "2020/02/30", + "1999/12/32", + "19/12/32", + "1/2/3", + "123:456:789", + "11:61", + "2021-55-12 08:12:54", + ]; + + for invalid in tests { + let rope = Rope::from_str(invalid); + let range = Range::new(0, 1); + + assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None) + } + } +} diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs new file mode 100644 index 00000000..f5945774 --- /dev/null +++ b/helix-core/src/increment/mod.rs @@ -0,0 +1,8 @@ +pub mod date_time; +pub mod number; + +use crate::{Range, Tendril}; + +pub trait Increment { + fn increment(&self, amount: i64) -> (Range, Tendril); +} diff --git a/helix-core/src/numbers.rs b/helix-core/src/increment/number.rs index e9f3c898..57171f67 100644 --- a/helix-core/src/numbers.rs +++ b/helix-core/src/increment/number.rs @@ -2,6 +2,8 @@ use std::borrow::Cow; use ropey::RopeSlice; +use super::Increment; + use crate::{ textobject::{textobject_word, TextObject}, Range, Tendril, @@ -9,9 +11,9 @@ use crate::{ #[derive(Debug, PartialEq, Eq)] pub struct NumberIncrementor<'a> { - pub range: Range, - pub value: i64, - pub radix: u32, + value: i64, + radix: u32, + range: Range, text: RopeSlice<'a>, } @@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> { text, }) } +} - /// Add `amount` to the number and return the formatted text. - pub fn incremented_text(&self, amount: i64) -> Tendril { +impl<'a> Increment for NumberIncrementor<'a> { + fn increment(&self, amount: i64) -> (Range, Tendril) { let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into(); let old_length = old_text.len(); let new_value = self.value.wrapping_add(amount); @@ -144,7 +147,7 @@ impl<'a> NumberIncrementor<'a> { } } - new_text.into() + (self.range, new_text.into()) } } @@ -366,8 +369,9 @@ mod test { assert_eq!( NumberIncrementor::from_range(rope.slice(..), range) .unwrap() - .incremented_text(amount), - expected.into() + .increment(amount) + .1, + Tendril::from(expected) ); } } @@ -392,8 +396,9 @@ mod test { assert_eq!( NumberIncrementor::from_range(rope.slice(..), range) .unwrap() - .incremented_text(amount), - expected.into() + .increment(amount) + .1, + Tendril::from(expected) ); } } @@ -419,8 +424,9 @@ mod test { assert_eq!( NumberIncrementor::from_range(rope.slice(..), range) .unwrap() - .incremented_text(amount), - expected.into() + .increment(amount) + .1, + Tendril::from(expected) ); } } @@ -464,8 +470,9 @@ mod test { assert_eq!( NumberIncrementor::from_range(rope.slice(..), range) .unwrap() - .incremented_text(amount), - expected.into() + .increment(amount) + .1, + Tendril::from(expected) ); } } @@ -491,8 +498,9 @@ mod test { assert_eq!( NumberIncrementor::from_range(rope.slice(..), range) .unwrap() - .incremented_text(amount), - expected.into() + .increment(amount) + .1, + Tendril::from(expected) ); } } diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 8ccc0120..5d20edc1 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,6 +1,5 @@ use crate::{ chars::{char_is_line_ending, char_is_whitespace}, - find_first_non_whitespace_char, syntax::{IndentQuery, LanguageConfiguration, Syntax}, tree_sitter::Node, Rope, RopeSlice, @@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> { /// To determine indentation of a newly inserted line, figure out the indentation at the last col /// of the previous line. -#[allow(dead_code)] -fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { +pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { let mut len = 0; for ch in line.chars() { match ch { @@ -207,10 +205,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod Some(node) } -fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize { - // NOTE: can't use contains() on query because of comparing Vec<String> and &str - // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains - +/// Calculate the indentation at a given treesitter node. +/// If newline is false, then any "indent" nodes on the line are ignored ("outdent" still applies). +/// This is because the indentation is only increased starting at the second line of the node. +fn calculate_indentation( + query: &IndentQuery, + node: Option<Node>, + line: usize, + newline: bool, +) -> usize { let mut increment: isize = 0; let mut node = match node { @@ -218,70 +221,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) None => return 0, }; - let mut prev_start = node.start_position().row; - - // if we're calculating indentation for a brand new line then the current node will become the - // parent node. We need to take it's indentation level into account too. - let node_kind = node.kind(); - if newline && query.indent.contains(node_kind) { - increment += 1; - } - - while let Some(parent) = node.parent() { - let parent_kind = parent.kind(); - let start = parent.start_position().row; - - // detect deeply nested indents in the same line - // .map(|a| { <-- ({ is two scopes - // let len = 1; <-- indents one level - // }) <-- }) is two scopes - let starts_same_line = start == prev_start; - - if query.outdent.contains(node.kind()) && !starts_same_line { - // we outdent by skipping the rules for the current level and jumping up - // node = parent; - increment -= 1; - // continue; + let mut current_line = line; + let mut consider_indent = newline; + let mut increment_from_line: isize = 0; + + loop { + let node_kind = node.kind(); + let start = node.start_position().row; + if current_line != start { + // Indent/dedent by at most one per line: + // .map(|a| { <-- ({ is two scopes + // let len = 1; <-- indents one level + // }) <-- }) is two scopes + if consider_indent || increment_from_line < 0 { + increment += increment_from_line.signum(); + } + increment_from_line = 0; + current_line = start; + consider_indent = true; } - if query.indent.contains(parent_kind) // && not_first_or_last_sibling - && !starts_same_line - { - // println!("is_scope {}", parent_kind); - prev_start = start; - increment += 1 + if query.outdent.contains(node_kind) { + increment_from_line -= 1; + } + if query.indent.contains(node_kind) { + increment_from_line += 1; } - // if last_scope && increment > 0 && ...{ ignore } - - node = parent; + if let Some(parent) = node.parent() { + node = parent; + } else { + break; + } + } + if consider_indent || increment_from_line < 0 { + increment += increment_from_line.signum(); } - increment.max(0) as usize } -#[allow(dead_code)] -fn suggested_indent_for_line( - language_config: &LanguageConfiguration, - syntax: Option<&Syntax>, - text: RopeSlice, - line_num: usize, - _tab_width: usize, -) -> usize { - if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) { - return suggested_indent_for_pos( - Some(language_config), - syntax, - text, - start + text.line_to_char(line_num), - false, - ); - }; - - // if the line is blank, indent should be zero - 0 -} - // TODO: two usecases: if we are triggering this for a new, blank line: // - it should return 0 when mass indenting stuff // - it should look up the wrapper node and count it too when we press o/O @@ -290,23 +268,20 @@ pub fn suggested_indent_for_pos( syntax: Option<&Syntax>, text: RopeSlice, pos: usize, + line: usize, new_line: bool, -) -> usize { +) -> Option<usize> { if let (Some(query), Some(syntax)) = ( language_config.and_then(|config| config.indent_query()), syntax, ) { let byte_start = text.char_to_byte(pos); let node = get_highest_syntax_node_at_bytepos(syntax, byte_start); - - // let config = load indentation query config from Syntax(should contain language_config) - // TODO: special case for comments // TODO: if preserve_leading_whitespace - calculate_indentation(query, node, new_line) + Some(calculate_indentation(query, node, line, new_line)) } else { - // TODO: heuristics for non-tree sitter grammars - 0 + None } } @@ -438,7 +413,8 @@ where ", ); - let doc = Rope::from(doc); + let doc = doc; + use crate::diagnostic::Severity; use crate::syntax::{ Configuration, IndentationConfiguration, LanguageConfiguration, Loader, }; @@ -456,6 +432,8 @@ where roots: vec![], comment_token: None, auto_format: false, + diagnostic_severity: Severity::Warning, + tree_sitter_library: None, language_server: None, indent: Some(IndentationConfiguration { tab_width: 4, @@ -474,20 +452,29 @@ where let language_config = loader.language_config_for_scope("source.rust").unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap(); - let syntax = Syntax::new(&doc, highlight_config.clone()); + let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)); let text = doc.slice(..); let tab_width = 4; for i in 0..doc.len_lines() { let line = text.line(i); - let indent = indent_level_for_line(line, tab_width); - assert_eq!( - suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width), - indent, - "line {}: {}", - i, - line - ); + if let Some(pos) = crate::find_first_non_whitespace_char(line) { + let indent = indent_level_for_line(line, tab_width); + assert_eq!( + suggested_indent_for_pos( + Some(&language_config), + Some(&syntax), + text, + text.line_to_char(i) + pos, + i, + false + ), + Some(indent), + "line {}: \"{}\"", + i, + line + ); + } } } } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 7d790406..fa8566ab 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,3 +1,5 @@ +pub use encoding_rs as encoding; + pub mod auto_pairs; pub mod chars; pub mod comment; @@ -5,18 +7,19 @@ pub mod diagnostic; pub mod diff; pub mod graphemes; pub mod history; +pub mod increment; pub mod indent; pub mod line_ending; pub mod macros; pub mod match_brackets; pub mod movement; -pub mod numbers; pub mod object; pub mod path; mod position; pub mod register; pub mod search; pub mod selection; +pub mod shellwords; mod state; pub mod surround; pub mod syntax; @@ -36,8 +39,14 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> { line.chars().position(|ch| !ch.is_whitespace()) } -/// Find `.git` root. -pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> { +/// Find project root. +/// +/// Order of detection: +/// * Top-most folder containing a root marker in current git repository +/// * Git repostory root if no marker detected +/// * Top-most folder containing a root marker if not git repository detected +/// * Current working directory as fallback +pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::path::PathBuf> { let current_dir = std::env::current_dir().expect("unable to determine current directory"); let root = match root { @@ -49,16 +58,30 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> { current_dir.join(root) } } - None => current_dir, + None => current_dir.clone(), }; + let mut top_marker = None; for ancestor in root.ancestors() { - // TODO: also use defined roots if git isn't found + for marker in root_markers { + if ancestor.join(marker).exists() { + top_marker = Some(ancestor); + break; + } + } + // don't go higher than repo if ancestor.join(".git").is_dir() { - return Some(ancestor.to_path_buf()); + // Use workspace if detected from marker + return Some(top_marker.unwrap_or(ancestor).to_path_buf()); } } - None + + // In absence of git repo, use workspace if detected + if top_marker.is_some() { + top_marker.map(|a| a.to_path_buf()) + } else { + Some(current_dir) + } } pub fn runtime_dir() -> std::path::PathBuf { @@ -158,7 +181,7 @@ mod merge_toml_tests { "; let base: Value = toml::from_slice(include_bytes!("../../languages.toml")) - .expect("Couldn't parse built-in langauges config"); + .expect("Couldn't parse built-in languages config"); let user: Value = toml::from_str(USER).unwrap(); let merged = merge_toml_values(base, user); @@ -189,7 +212,10 @@ use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; pub use ropey::{Rope, RopeBuilder, RopeSlice}; -pub use tendril::StrTendril as Tendril; +// pub use tendril::StrTendril as Tendril; +pub use smartstring::SmartString; + +pub type Tendril = SmartString<smartstring::LazyCompact>; #[doc(inline)] pub use {regex, tree_sitter}; diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs index 3541305c..8eb426e1 100644 --- a/helix-core/src/line_ending.rs +++ b/helix-core/src/line_ending.rs @@ -250,7 +250,7 @@ mod line_ending_tests { 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); + assert_eq!(get_line_ending_of_str(text), None); } #[test] diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index cd554005..0189dedd 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -11,7 +11,7 @@ const PAIRS: &[(char, char)] = &[ ('\"', '\"'), ]; -// limit matching pairs to only ( ) { } [ ] < > +// limit matching pairs to only ( ) { } [ ] < > ' ' " " // Returns the position of the matching bracket under cursor. // diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 01a8f890..47fe6827 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -307,8 +307,6 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo #[cfg(test)] mod test { - use std::array::{self, IntoIter}; - use ropey::Rope; use super::*; @@ -360,7 +358,7 @@ mod test { ((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line ]; - for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) { + for ((direction, amount), coordinates) in moves_and_expected_coordinates { range = move_horizontally(slice, range, direction, amount, Movement::Move); assert_eq!(coords_at_pos(slice, range.head), coordinates.into()) } @@ -374,7 +372,7 @@ mod test { let mut range = Range::point(position); - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ ((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n... ((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n... ((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n... @@ -384,7 +382,7 @@ mod test { ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n... ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n| ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n| - ]); + ]; for ((direction, amount), coordinates) in moves_and_expected_coordinates { range = move_horizontally(slice, range, direction, amount, Movement::Move); @@ -402,11 +400,11 @@ mod test { let mut range = Range::point(position); let original_anchor = range.anchor; - let moves = IntoIter::new([ + let moves = [ (Direction::Forward, 1usize), (Direction::Forward, 5usize), (Direction::Backward, 3usize), - ]); + ]; for (direction, amount) in moves { range = move_horizontally(slice, range, direction, amount, Movement::Extend); @@ -420,7 +418,7 @@ mod test { let slice = text.slice(..); let position = pos_at_coords(slice, (0, 0).into(), true); let mut range = Range::point(position); - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ ((Direction::Forward, 1usize), (1, 0)), ((Direction::Forward, 2usize), (3, 0)), ((Direction::Forward, 1usize), (4, 0)), @@ -430,7 +428,7 @@ mod test { ((Direction::Backward, 0usize), (4, 0)), ((Direction::Forward, 5), (5, 0)), ((Direction::Forward, 999usize), (5, 0)), - ]); + ]; for ((direction, amount), coordinates) in moves_and_expected_coordinates { range = move_vertically(slice, range, direction, amount, Movement::Move); @@ -450,7 +448,7 @@ mod test { H, V, } - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ // Places cursor at the end of line ((Axis::H, Direction::Forward, 8usize), (0, 8)), // First descent preserves column as the target line is wider @@ -463,7 +461,7 @@ mod test { ((Axis::V, Direction::Backward, 999usize), (0, 8)), ((Axis::V, Direction::Forward, 4usize), (4, 8)), ((Axis::V, Direction::Forward, 999usize), (5, 0)), - ]); + ]; for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { range = match axis { @@ -489,7 +487,7 @@ mod test { H, V, } - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ // Places cursor at the fourth kana. ((Axis::H, Direction::Forward, 4), (0, 4)), // Descent places cursor at the 4th character. @@ -498,7 +496,7 @@ mod test { ((Axis::H, Direction::Backward, 1usize), (1, 3)), // Jumping back up 1 line. ((Axis::V, Direction::Backward, 1usize), (0, 3)), - ]); + ]; for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { range = match axis { @@ -530,7 +528,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_next_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion stops at the first space", vec![(1, Range::new(0, 0), Range::new(0, 6))]), (" Starting from a boundary advances the anchor", @@ -604,7 +602,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 6)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -616,7 +614,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_next_long_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion stops at the first space", vec![(1, Range::new(0, 0), Range::new(0, 6))]), (" Starting from a boundary advances the anchor", @@ -688,7 +686,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 8)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -700,7 +698,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_previous_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic backward motion from the middle of a word", vec![(1, Range::new(3, 3), Range::new(4, 0))]), @@ -773,7 +771,7 @@ mod test { vec![ (1, Range::new(0, 6), Range::new(6, 0)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -785,7 +783,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_previous_long_words() { - let tests = array::IntoIter::new([ + let tests = [ ( "Basic backward motion from the middle of a word", vec![(1, Range::new(3, 3), Range::new(4, 0))], @@ -870,7 +868,7 @@ mod test { vec![ (1, Range::new(0, 8), Range::new(8, 0)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -882,7 +880,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_end_of_next_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion from the start of a word to the end of it", vec![(1, Range::new(0, 0), Range::new(0, 5))]), ("Basic forward motion from the end of a word to the end of the next", @@ -954,7 +952,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 5)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -966,7 +964,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_end_of_previous_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic backward motion from the middle of a word", vec![(1, Range::new(9, 9), Range::new(10, 5))]), ("Starting from after boundary retreats the anchor", @@ -1036,7 +1034,7 @@ mod test { vec![ (1, Range::new(0, 10), Range::new(10, 4)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -1048,7 +1046,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_end_of_next_long_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion from the start of a word to the end of it", vec![(1, Range::new(0, 0), Range::new(0, 5))]), ("Basic forward motion from the end of a word to the end of the next", @@ -1118,7 +1116,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 7)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 717c5994..b06f4144 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,31 +1,72 @@ use crate::{Range, RopeSlice, Selection, Syntax}; +use tree_sitter::Node; -// TODO: to contract_selection we'd need to store the previous ranges before expand. -// Maybe just contract to the first child node? -pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { +pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + select_node_impl(syntax, text, selection, |descendant, from, to| { + if descendant.start_byte() == from && descendant.end_byte() == to { + descendant.parent() + } else { + Some(descendant) + } + }) +} + +pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + select_node_impl(syntax, text, selection, |descendant, _from, _to| { + descendant.child(0).or(Some(descendant)) + }) +} + +pub fn select_sibling<F>( + syntax: &Syntax, + text: RopeSlice, + selection: Selection, + sibling_fn: &F, +) -> Selection +where + F: Fn(Node) -> Option<Node>, +{ + select_node_impl(syntax, text, selection, |descendant, _from, _to| { + find_sibling_recursive(descendant, sibling_fn) + }) +} + +fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node> +where + F: Fn(Node) -> Option<Node>, +{ + sibling_fn(node).or_else(|| { + node.parent() + .and_then(|node| find_sibling_recursive(node, sibling_fn)) + }) +} + +fn select_node_impl<F>( + syntax: &Syntax, + text: RopeSlice, + selection: Selection, + select_fn: F, +) -> Selection +where + F: Fn(Node, usize, usize) -> Option<Node>, +{ let tree = syntax.tree(); - selection.clone().transform(|range| { + selection.transform(|range| { let from = text.char_to_byte(range.from()); let to = text.char_to_byte(range.to()); - // find parent of a descendant that matches the range - let parent = match tree + let node = match tree .root_node() .descendant_for_byte_range(from, to) - .and_then(|node| { - if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) { - node.parent() - } else { - Some(node) - } - }) { - Some(parent) => parent, + .and_then(|node| select_fn(node, from, to)) + { + Some(node) => node, None => return range, }; - let from = text.byte_to_char(parent.start_byte()); - let to = text.byte_to_char(parent.end_byte()); + let from = text.byte_to_char(node.start_byte()); + let to = text.byte_to_char(node.end_byte()); if range.head < range.anchor { Range::new(to, from) diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index c6018ce6..93362c77 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -109,7 +109,10 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po /// TODO: this should be changed to work in terms of visual row/column, not /// graphemes. pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending: bool) -> usize { - let Position { row, col } = coords; + let Position { mut row, col } = coords; + if limit_before_line_ending { + row = row.min(text.len_lines() - 1); + }; let line_start = text.line_to_char(row); let line_end = if limit_before_line_ending { line_end_char_index(&text, row) @@ -290,5 +293,12 @@ mod test { assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0); assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1); assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2); + + // Test out of bounds. + let text = Rope::new(); + let slice = text.slice(..); + assert_eq!(pos_at_coords(slice, (10, 0).into(), true), 0); + assert_eq!(pos_at_coords(slice, (0, 10).into(), true), 0); + assert_eq!(pos_at_coords(slice, (10, 10).into(), true), 0); } } diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs index b9eb497d..b39e4034 100644 --- a/helix-core/src/register.rs +++ b/helix-core/src/register.rs @@ -68,4 +68,8 @@ impl Registers { pub fn read(&self, name: char) -> Option<&[String]> { self.get(name).map(|reg| reg.read()) } + + pub fn inner(&self) -> &HashMap<char, Register> { + &self.inner + } } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 116a1c7c..c6eceb4b 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -7,6 +7,7 @@ use crate::{ ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, prev_grapheme_boundary, }, + movement::Direction, Assoc, ChangeSet, RopeSlice, }; use smallvec::{smallvec, SmallVec}; @@ -82,6 +83,13 @@ impl Range { std::cmp::max(self.anchor, self.head) } + /// Total length of the range. + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.to() - self.from() + } + /// The (inclusive) range of lines that the range overlaps. #[inline] #[must_use] @@ -102,6 +110,27 @@ impl Range { self.anchor == self.head } + /// `Direction::Backward` when head < anchor. + /// `Direction::Backward` otherwise. + #[inline] + #[must_use] + pub fn direction(&self) -> Direction { + if self.head < self.anchor { + Direction::Backward + } else { + Direction::Forward + } + } + + // flips the direction of the selection + pub fn flip(&self) -> Self { + Self { + anchor: self.head, + head: self.anchor, + horiz: self.horiz, + } + } + /// Check two ranges for overlap. #[must_use] pub fn overlaps(&self, other: &Self) -> bool { @@ -111,6 +140,11 @@ impl Range { self.from() == other.from() || (self.to() > other.from() && other.to() > self.from()) } + #[inline] + pub fn contains_range(&self, other: &Self) -> bool { + self.from() <= other.from() && self.to() >= other.to() + } + pub fn contains(&self, pos: usize) -> bool { self.from() <= pos && pos < self.to() } @@ -515,6 +549,39 @@ impl Selection { pub fn len(&self) -> usize { self.ranges.len() } + + // returns true if self ⊇ other + pub fn contains(&self, other: &Selection) -> bool { + // can't contain other if it is larger + if other.len() > self.len() { + return false; + } + + let (mut iter_self, mut iter_other) = (self.iter(), other.iter()); + let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next()); + + loop { + match (ele_self, ele_other) { + (Some(ra), Some(rb)) => { + if !ra.contains_range(rb) { + // `self` doesn't contain next element from `other`, advance `self`, we need to match all from `other` + ele_self = iter_self.next(); + } else { + // matched element from `other`, advance `other` + ele_other = iter_other.next(); + }; + } + (None, Some(_)) => { + // exhausted `self`, we can't match the reminder of `other` + return false; + } + (_, None) => { + // no elements from `other` left to match, `self` contains `other` + return true; + } + } + } + } } impl<'a> IntoIterator for &'a Selection { @@ -699,16 +766,16 @@ mod test { fn test_contains() { let range = Range::new(10, 12); - assert_eq!(range.contains(9), false); - assert_eq!(range.contains(10), true); - assert_eq!(range.contains(11), true); - assert_eq!(range.contains(12), false); - assert_eq!(range.contains(13), false); + assert!(!range.contains(9)); + assert!(range.contains(10)); + assert!(range.contains(11)); + assert!(!range.contains(12)); + assert!(!range.contains(13)); let range = Range::new(9, 6); - assert_eq!(range.contains(9), false); - assert_eq!(range.contains(7), true); - assert_eq!(range.contains(6), true); + assert!(!range.contains(9)); + assert!(range.contains(7)); + assert!(range.contains(6)); } #[test] @@ -953,4 +1020,30 @@ mod test { &["", "abcd", "efg", "rs", "xyz"] ); } + #[test] + fn test_selection_contains() { + fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool { + let sela = Selection::new(a.iter().map(|a| Range::new(a.0, a.1)).collect(), 0); + let selb = Selection::new(b.iter().map(|b| Range::new(b.0, b.1)).collect(), 0); + sela.contains(&selb) + } + + // exact match + assert!(contains(vec!((1, 1)), vec!((1, 1)))); + + // larger set contains smaller + assert!(contains(vec!((1, 1), (2, 2), (3, 3)), vec!((2, 2)))); + + // multiple matches + assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2)))); + + // smaller set can't contain bigger + assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2)))); + + assert!(contains( + vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)), + vec!((3, 4), (7, 9)) + )); + assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6)))); + } } diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs new file mode 100644 index 00000000..13f6f3e9 --- /dev/null +++ b/helix-core/src/shellwords.rs @@ -0,0 +1,164 @@ +use std::borrow::Cow; + +/// Get the vec of escaped / quoted / doublequoted filenames from the input str +pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> { + enum State { + Normal, + NormalEscaped, + Quoted, + QuoteEscaped, + Dquoted, + DquoteEscaped, + } + + use State::*; + + let mut state = Normal; + let mut args: Vec<Cow<str>> = Vec::new(); + let mut escaped = String::with_capacity(input.len()); + + let mut start = 0; + let mut end = 0; + + for (i, c) in input.char_indices() { + state = match state { + Normal => match c { + '\\' => { + escaped.push_str(&input[start..i]); + start = i + 1; + NormalEscaped + } + '"' => { + end = i; + Dquoted + } + '\'' => { + end = i; + Quoted + } + c if c.is_ascii_whitespace() => { + end = i; + Normal + } + _ => Normal, + }, + NormalEscaped => Normal, + Quoted => match c { + '\\' => { + escaped.push_str(&input[start..i]); + start = i + 1; + QuoteEscaped + } + '\'' => { + end = i; + Normal + } + _ => Quoted, + }, + QuoteEscaped => Quoted, + Dquoted => match c { + '\\' => { + escaped.push_str(&input[start..i]); + start = i + 1; + DquoteEscaped + } + '"' => { + end = i; + Normal + } + _ => Dquoted, + }, + DquoteEscaped => Dquoted, + }; + + if i >= input.len() - 1 && end == 0 { + end = i + 1; + } + + if end > 0 { + let esc_trim = escaped.trim(); + let inp = &input[start..end]; + + if !(esc_trim.is_empty() && inp.trim().is_empty()) { + if esc_trim.is_empty() { + args.push(inp.into()); + } else { + args.push([escaped, inp.into()].concat().into()); + escaped = "".to_string(); + } + } + start = i + 1; + end = 0; + } + } + args +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_normal() { + let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; + let result = shellwords(input); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó"), + Cow::from("wörds"), + Cow::from(r#"three "with escaping\"#), + ]; + // TODO test is_owned and is_borrowed, once they get stabilized. + assert_eq!(expected, result); + } + + #[test] + fn test_quoted() { + let quoted = + r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#; + let result = shellwords(quoted); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó wörds"), + Cow::from(r#"three' "with escaping\"#), + Cow::from("quote incomplete"), + ]; + assert_eq!(expected, result); + } + + #[test] + fn test_dquoted() { + let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#; + let result = shellwords(dquoted); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó wörds"), + Cow::from(r#"three' "with escaping\"#), + Cow::from("dquote incomplete"), + ]; + assert_eq!(expected, result); + } + + #[test] + fn test_mixed() { + let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#; + let result = shellwords(dquoted); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó wörds"), + Cow::from("three' \"with escaping\\"), + Cow::from("no space before"), + Cow::from("and after"), + Cow::from("$#%^@"), + Cow::from("%^&(%^"), + Cow::from(")(*&^%"), + Cow::from(r#"a\\b"#), + //last ' just changes to quoted but since we dont have anything after it, it should be ignored + ]; + assert_eq!(expected, result); + } +} diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index b53b0a78..58eb23cf 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -172,6 +172,7 @@ mod test { use ropey::Rope; use smallvec::SmallVec; + #[allow(clippy::type_complexity)] fn check_find_nth_pair_pos( text: &str, cases: Vec<(usize, char, usize, Option<(usize, usize)>)>, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f1c399d2..a5c5e498 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1,5 +1,6 @@ use crate::{ chars::char_is_line_ending, + diagnostic::Severity, regex::Regex, transaction::{ChangeSet, Operation}, Rope, RopeSlice, Tendril, @@ -7,12 +8,13 @@ use crate::{ pub use helix_syntax::get_language; -use arc_swap::ArcSwap; +use arc_swap::{ArcSwap, Guard}; +use slotmap::{DefaultKey as LayerId, HopSlotMap}; use std::{ borrow::Cow, cell::RefCell, - collections::{HashMap, HashSet}, + collections::{HashMap, HashSet, VecDeque}, fmt, path::Path, sync::Arc, @@ -50,7 +52,7 @@ pub struct Configuration { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub language_id: String, + pub language_id: String, // c-sharp, rust pub scope: String, // source.rust pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc> #[serde(default)] @@ -63,6 +65,10 @@ pub struct LanguageConfiguration { #[serde(default)] pub auto_format: bool, + #[serde(default)] + pub diagnostic_severity: Severity, + + pub tree_sitter_library: Option<String>, // tree-sitter library name, defaults to language_id // content_regex #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")] @@ -92,6 +98,7 @@ pub struct LanguageServerConfiguration { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub args: Vec<String>, + pub language_id: Option<String>, } #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] @@ -245,20 +252,22 @@ impl LanguageConfiguration { if highlights_query.is_empty() { None } else { - let language = get_language(&crate::RUNTIME_DIR, &self.language_id) - .map_err(|e| log::info!("{}", e)) - .ok()?; + let language = get_language( + &crate::RUNTIME_DIR, + self.tree_sitter_library + .as_deref() + .unwrap_or(&self.language_id), + ) + .map_err(|e| log::info!("{}", e)) + .ok()?; let config = HighlightConfiguration::new( language, &highlights_query, &injections_query, &locals_query, - ); + ) + .unwrap(); // TODO: avoid panic - let config = match config { - Ok(config) => config, - Err(err) => panic!("{}", err), - }; // TODO: avoid panic config.configure(scopes); Some(Arc::new(config)) } @@ -308,12 +317,16 @@ impl LanguageConfiguration { } } +// Expose loader as Lazy<> global since it's always static? + #[derive(Debug)] pub struct Loader { // highlight_names ? language_configs: Vec<Arc<LanguageConfiguration>>, language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize> language_config_ids_by_shebang: HashMap<String, usize>, + + scopes: ArcSwap<Vec<String>>, } impl Loader { @@ -322,6 +335,7 @@ impl Loader { language_configs: Vec::new(), language_config_ids_by_file_type: HashMap::new(), language_config_ids_by_shebang: HashMap::new(), + scopes: ArcSwap::from_pointee(Vec::new()), }; for config in config.language { @@ -366,8 +380,9 @@ impl Loader { pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> { let line = Cow::from(source.line(0)); - static SHEBANG_REGEX: Lazy<Regex> = - Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap()); + static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| { + Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap() + }); let configuration_id = SHEBANG_REGEX .captures(&line) .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1])); @@ -406,8 +421,22 @@ impl Loader { } None } - pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> { - self.language_configs.iter() + + pub fn set_scopes(&self, scopes: Vec<String>) { + self.scopes.store(Arc::new(scopes)); + + // Reconfigure existing grammars + for config in self + .language_configs + .iter() + .filter(|cfg| cfg.is_highlight_initialized()) + { + config.reconfigure(&self.scopes()); + } + } + + pub fn scopes(&self) -> Guard<Arc<Vec<String>>> { + self.scopes.load() } } @@ -416,12 +445,6 @@ pub struct TsParser { cursors: Vec<QueryCursor>, } -impl fmt::Debug for TsParser { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TsParser").finish() - } -} - // could also just use a pool, or a single instance? thread_local! { pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser { @@ -432,9 +455,9 @@ thread_local! { #[derive(Debug)] pub struct Syntax { - config: Arc<HighlightConfiguration>, - - root_layer: LanguageLayer, + layers: HopSlotMap<LayerId, LanguageLayer>, + root: LayerId, + loader: Arc<Loader>, } fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> { @@ -444,38 +467,34 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st } impl Syntax { - // buffer, grammar, config, grammars, sync_timeout? - pub fn new( - /*language: Lang,*/ source: &Rope, - config: Arc<HighlightConfiguration>, - ) -> Self { - let root_layer = LanguageLayer { tree: None }; + pub fn new(source: &Rope, config: Arc<HighlightConfiguration>, loader: Arc<Loader>) -> Self { + let root_layer = LanguageLayer { + tree: None, + config, + depth: 0, + ranges: vec![Range { + start_byte: 0, + end_byte: usize::MAX, + start_point: Point::new(0, 0), + end_point: Point::new(usize::MAX, usize::MAX), + }], + }; - // track markers of injections // track scope_descriptor: a Vec of scopes for item in tree + let mut layers = HopSlotMap::default(); + let root = layers.insert(root_layer); + let mut syntax = Self { - // grammar, - config, - root_layer, + root, + layers, + loader, }; - // update root layer - PARSER.with(|ts_parser| { - // TODO: handle the returned `Result` properly. - let _ = syntax.root_layer.parse( - &mut ts_parser.borrow_mut(), - &syntax.config, - source, - 0, - vec![Range { - start_byte: 0, - end_byte: usize::MAX, - start_point: Point::new(0, 0), - end_point: Point::new(usize::MAX, usize::MAX), - }], - ); - }); + syntax + .update(source, source, &ChangeSet::new(source)) + .unwrap(); + syntax } @@ -485,32 +504,255 @@ impl Syntax { source: &Rope, changeset: &ChangeSet, ) -> Result<(), Error> { + let mut queue = VecDeque::new(); + queue.push_back(self.root); + + let scopes = self.loader.scopes.load(); + let injection_callback = |language: &str| { + self.loader + .language_configuration_for_injection_string(language) + .and_then(|language_config| language_config.highlight_config(&scopes)) + }; + + // Convert the changeset into tree sitter edits. + let edits = generate_edits(old_source, changeset); + + // Use the edits to update all layers markers + if !edits.is_empty() { + fn point_add(a: Point, b: Point) -> Point { + if b.row > 0 { + Point::new(a.row.saturating_add(b.row), b.column) + } else { + Point::new(0, a.column.saturating_add(b.column)) + } + } + fn point_sub(a: Point, b: Point) -> Point { + if a.row > b.row { + Point::new(a.row.saturating_sub(b.row), a.column) + } else { + Point::new(0, a.column.saturating_sub(b.column)) + } + } + + for layer in &mut self.layers.values_mut() { + // The root layer always covers the whole range (0..usize::MAX) + if layer.depth == 0 { + continue; + } + + for range in &mut layer.ranges { + // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720 + for edit in edits.iter().rev() { + let is_pure_insertion = edit.old_end_byte == edit.start_byte; + + // if edit is after range, skip + if edit.start_byte > range.end_byte { + // TODO: || (is_noop && edit.start_byte == range.end_byte) + continue; + } + + // if edit is before range, shift entire range by len + if edit.old_end_byte < range.start_byte { + range.start_byte = + edit.new_end_byte + (range.start_byte - edit.old_end_byte); + range.start_point = point_add( + edit.new_end_position, + point_sub(range.start_point, edit.old_end_position), + ); + + range.end_byte = edit + .new_end_byte + .saturating_add(range.end_byte - edit.old_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); + } + // if the edit starts in the space before and extends into the range + else if edit.start_byte < range.start_byte { + range.start_byte = edit.new_end_byte; + range.start_point = edit.new_end_position; + + range.end_byte = range + .end_byte + .saturating_sub(edit.old_end_byte) + .saturating_add(edit.new_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); + } + // If the edit is an insertion at the start of the tree, shift + else if edit.start_byte == range.start_byte && is_pure_insertion { + range.start_byte = edit.new_end_byte; + range.start_point = edit.new_end_position; + } else { + range.end_byte = range + .end_byte + .saturating_sub(edit.old_end_byte) + .saturating_add(edit.new_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); + } + } + } + } + } + PARSER.with(|ts_parser| { - self.root_layer.update( - &mut ts_parser.borrow_mut(), - &self.config, - old_source, - source, - changeset, - ) - }) + let ts_parser = &mut ts_parser.borrow_mut(); + let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); + // TODO: might need to set cursor range + cursor.set_byte_range(0..usize::MAX); - // TODO: deal with injections and update them too - } + let source_slice = source.slice(..); - // fn buffer_changed -> call layer.update(range, new_text) on root layer and then all marker layers + let mut touched = HashSet::new(); - // call this on transaction.apply() -> buffer_changed(changes) - // - // fn parse(language, old_tree, ranges) - // - pub fn tree(&self) -> &Tree { - self.root_layer.tree() + // TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit + + while let Some(layer_id) = queue.pop_front() { + // Mark the layer as touched + touched.insert(layer_id); + + let layer = &mut self.layers[layer_id]; + + // If a tree already exists, notify it of changes. + if let Some(tree) = &mut layer.tree { + for edit in edits.iter().rev() { + // Apply the edits in reverse. + // If we applied them in order then edit 1 would disrupt the positioning of edit 2. + tree.edit(edit); + } + } + + // Re-parse the tree. + layer.parse(&mut ts_parser.parser, source)?; + + // Switch to an immutable borrow. + let layer = &self.layers[layer_id]; + + // Process injections. + let matches = cursor.matches( + &layer.config.injections_query, + layer.tree().root_node(), + RopeProvider(source_slice), + ); + let mut injections = Vec::new(); + for mat in matches { + let (language_name, content_node, include_children) = injection_for_match( + &layer.config, + &layer.config.injections_query, + &mat, + source_slice, + ); + + // Explicitly remove this match so that none of its other captures will remain + // in the stream of captures. + mat.remove(); + + // If a language is found with the given name, then add a new language layer + // to the highlighted document. + if let (Some(language_name), Some(content_node)) = (language_name, content_node) + { + if let Some(config) = (injection_callback)(&language_name) { + let ranges = + intersect_ranges(&layer.ranges, &[content_node], include_children); + + if !ranges.is_empty() { + injections.push((config, ranges)); + } + } + } + } + + // Process combined injections. + if let Some(combined_injections_query) = &layer.config.combined_injections_query { + let mut injections_by_pattern_index = + vec![(None, Vec::new(), false); combined_injections_query.pattern_count()]; + let matches = cursor.matches( + combined_injections_query, + layer.tree().root_node(), + RopeProvider(source_slice), + ); + for mat in matches { + let entry = &mut injections_by_pattern_index[mat.pattern_index]; + let (language_name, content_node, include_children) = injection_for_match( + &layer.config, + combined_injections_query, + &mat, + source_slice, + ); + if language_name.is_some() { + entry.0 = language_name; + } + if let Some(content_node) = content_node { + entry.1.push(content_node); + } + entry.2 = include_children; + } + for (lang_name, content_nodes, includes_children) in injections_by_pattern_index + { + if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) { + if let Some(config) = (injection_callback)(&lang_name) { + let ranges = intersect_ranges( + &layer.ranges, + &content_nodes, + includes_children, + ); + if !ranges.is_empty() { + injections.push((config, ranges)); + } + } + } + } + } + + let depth = layer.depth + 1; + // TODO: can't inline this since matches borrows self.layers + for (config, ranges) in injections { + // Find an existing layer + let layer = self + .layers + .iter_mut() + .find(|(_, layer)| { + layer.depth == depth && // TODO: track parent id instead + layer.config.language == config.language && layer.ranges == ranges + }) + .map(|(id, _layer)| id); + + // ...or insert a new one. + let layer_id = layer.unwrap_or_else(|| { + self.layers.insert(LanguageLayer { + tree: None, + config, + depth, + ranges, + }) + }); + + queue.push_back(layer_id); + } + + // TODO: pre-process local scopes at this time, rather than highlight? + // would solve problems with locals not working across boundaries + } + + // Return the cursor back in the pool. + ts_parser.cursors.push(cursor); + + // Remove all untouched layers + self.layers.retain(|id, _| touched.contains(&id)); + + Ok(()) + }) } - // - // <!--update_for_injection(grammar)--> - // Highlighting + pub fn tree(&self) -> &Tree { + self.layers[self.root].tree() + } /// Iterate over the highlighted regions for a given slice of source code. pub fn highlight_iter<'a>( @@ -518,65 +760,76 @@ impl Syntax { source: RopeSlice<'a>, range: Option<std::ops::Range<usize>>, cancellation_flag: Option<&'a AtomicUsize>, - injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, ) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a { - // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which - // prevents them from being moved. But both of these values are really just - // pointers, so it's actually ok to move them. - - // reuse a cursor from the pool if possible - let mut cursor = PARSER.with(|ts_parser| { - let highlighter = &mut ts_parser.borrow_mut(); - highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) + let mut layers = self + .layers + .iter() + .filter_map(|(_, layer)| { + // TODO: if range doesn't overlap layer range, skip it + + // Reuse a cursor from the pool if available. + let mut cursor = PARSER.with(|ts_parser| { + let highlighter = &mut ts_parser.borrow_mut(); + highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) + }); + + // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which + // prevents them from being moved. But both of these values are really just + // pointers, so it's actually ok to move them. + let cursor_ref = + unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; + + // if reusing cursors & no range this resets to whole range + cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); + + let mut captures = cursor_ref + .captures( + &layer.config.query, + layer.tree().root_node(), + RopeProvider(source), + ) + .peekable(); + + // If there's no captures, skip the layer + captures.peek()?; + + Some(HighlightIterLayer { + highlight_end_stack: Vec::new(), + scope_stack: vec![LocalScope { + inherits: false, + range: 0..usize::MAX, + local_defs: Vec::new(), + }], + cursor, + _tree: None, + captures, + config: layer.config.as_ref(), // TODO: just reuse `layer` + depth: layer.depth, // TODO: just reuse `layer` + ranges: &layer.ranges, // TODO: temp + }) + }) + .collect::<Vec<_>>(); + + // HAXX: arrange layers by byte range, with deeper layers positioned first + layers.sort_by_key(|layer| { + ( + layer.ranges.first().cloned(), + std::cmp::Reverse(layer.depth), + ) }); - let tree_ref = self.tree(); - let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; - let query_ref = &self.config.query; - let config_ref = self.config.as_ref(); - - // if reusing cursors & no range this resets to whole range - cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); - - let captures = cursor_ref - .captures(query_ref, tree_ref.root_node(), RopeProvider(source)) - .peekable(); - - // manually craft the root layer based on the existing tree - let layer = HighlightIterLayer { - highlight_end_stack: Vec::new(), - scope_stack: vec![LocalScope { - inherits: false, - range: 0..usize::MAX, - local_defs: Vec::new(), - }], - cursor, - depth: 0, - _tree: None, - captures, - config: config_ref, - ranges: vec![Range { - start_byte: 0, - end_byte: usize::MAX, - start_point: Point::new(0, 0), - end_point: Point::new(usize::MAX, usize::MAX), - }], - }; let mut result = HighlightIter { source, - byte_offset: range.map_or(0, |r| r.start), // TODO: simplify - injection_callback, + byte_offset: range.map_or(0, |r| r.start), cancellation_flag, iter_count: 0, - layers: vec![layer], + layers, next_event: None, last_highlight_range: None, }; result.sort_layers(); result } - // on_tokenize - // on_change_highlighting // Commenting // comment_strings_for_pos @@ -588,246 +841,157 @@ impl Syntax { // indent_level_for_line // TODO: Folding - - // Syntax APIs - // get_syntax_node_containing_range -> - // ... - // get_syntax_node_at_pos - // buffer_range_for_scope_at_pos } #[derive(Debug)] pub struct LanguageLayer { // mode // grammar - // depth + pub config: Arc<HighlightConfiguration>, pub(crate) tree: Option<Tree>, + pub ranges: Vec<Range>, + pub depth: usize, } impl LanguageLayer { - // pub fn new() -> Self { - // Self { tree: None } - // } - pub fn tree(&self) -> &Tree { // TODO: no unwrap self.tree.as_ref().unwrap() } - fn parse( - &mut self, - ts_parser: &mut TsParser, - config: &HighlightConfiguration, - source: &Rope, - _depth: usize, - ranges: Vec<Range>, - ) -> Result<(), Error> { - if ts_parser.parser.set_included_ranges(&ranges).is_ok() { - ts_parser - .parser - .set_language(config.language) - .map_err(|_| Error::InvalidLanguage)?; - - // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) }; - let tree = ts_parser - .parser - .parse_with( - &mut |byte, _| { - if byte <= source.len_bytes() { - let (chunk, start_byte, _, _) = source.chunk_at_byte(byte); - chunk[byte - start_byte..].as_bytes() - } else { - // out of range - &[] - } - }, - self.tree.as_ref(), - ) - .ok_or(Error::Cancelled)?; + fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> { + parser.set_included_ranges(&self.ranges).unwrap(); - self.tree = Some(tree) - } + parser + .set_language(self.config.language) + .map_err(|_| Error::InvalidLanguage)?; + + // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) }; + let tree = parser + .parse_with( + &mut |byte, _| { + if byte <= source.len_bytes() { + let (chunk, start_byte, _, _) = source.chunk_at_byte(byte); + chunk[byte - start_byte..].as_bytes() + } else { + // out of range + &[] + } + }, + self.tree.as_ref(), + ) + .ok_or(Error::Cancelled)?; + // unsafe { ts_parser.parser.set_cancellation_flag(None) }; + self.tree = Some(tree); Ok(()) } +} - pub(crate) fn generate_edits( - old_text: RopeSlice, - changeset: &ChangeSet, - ) -> Vec<tree_sitter::InputEdit> { - use Operation::*; - let mut old_pos = 0; +pub(crate) fn generate_edits( + old_text: &Rope, + changeset: &ChangeSet, +) -> Vec<tree_sitter::InputEdit> { + use Operation::*; + let mut old_pos = 0; - let mut edits = Vec::new(); + let mut edits = Vec::new(); - let mut iter = changeset.changes.iter().peekable(); + if changeset.changes.is_empty() { + return edits; + } - // TODO; this is a lot easier with Change instead of Operation. + let mut iter = changeset.changes.iter().peekable(); - fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) { - let byte = text.char_to_byte(pos); // <- attempted to index past end - let line = text.char_to_line(pos); - let line_start_byte = text.line_to_byte(line); - let col = byte - line_start_byte; + // TODO; this is a lot easier with Change instead of Operation. - (byte, Point::new(line, col)) - } + fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) { + let byte = text.char_to_byte(pos); // <- attempted to index past end + let line = text.char_to_line(pos); + let line_start_byte = text.line_to_byte(line); + let col = byte - line_start_byte; - fn traverse(point: Point, text: &Tendril) -> Point { - let Point { - mut row, - mut column, - } = point; - - // 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 { - column += 1; - } + (byte, Point::new(line, col)) + } + + fn traverse(point: Point, text: &Tendril) -> Point { + let Point { + mut row, + mut column, + } = point; + + // 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 { + column += 1; } - Point { row, column } } + Point { row, column } + } - while let Some(change) = iter.next() { - let len = match change { - Delete(i) | Retain(i) => *i, - Insert(_) => 0, - }; - let mut old_end = old_pos + len; + while let Some(change) = iter.next() { + let len = match change { + Delete(i) | Retain(i) => *i, + Insert(_) => 0, + }; + let mut old_end = old_pos + len; + + match change { + Retain(_) => {} + Delete(_) => { + let (start_byte, start_position) = point_at_pos(old_text, old_pos); + let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end); + + // deletion + edits.push(tree_sitter::InputEdit { + start_byte, // old_pos to byte + old_end_byte, // old_end to byte + new_end_byte: start_byte, // old_pos to byte + start_position, // old pos to coords + old_end_position, // old_end to coords + new_end_position: start_position, // old pos to coords + }); + } + Insert(s) => { + let (start_byte, start_position) = point_at_pos(old_text, old_pos); - match change { - Retain(_) => {} - Delete(_) => { - let (start_byte, start_position) = point_at_pos(old_text, old_pos); + // a subsequent delete means a replace, consume it + if let Some(Delete(len)) = iter.peek() { + old_end = old_pos + len; let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end); - // TODO: Position also needs to be byte based... - // let byte = char_to_byte(old_pos) - // let line = char_to_line(old_pos) - // let line_start_byte = line_to_byte() - // Position::new(line, line_start_byte - byte) + iter.next(); - // deletion + // replacement edits.push(tree_sitter::InputEdit { - start_byte, // old_pos to byte - old_end_byte, // old_end to byte - new_end_byte: start_byte, // old_pos to byte - start_position, // old pos to coords - old_end_position, // old_end to coords - new_end_position: start_position, // old pos to coords + start_byte, // old_pos to byte + old_end_byte, // old_end to byte + new_end_byte: start_byte + s.len(), // old_pos to byte + s.len() + start_position, // old pos to coords + old_end_position, // old_end to coords + new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over) + }); + } else { + // insert + edits.push(tree_sitter::InputEdit { + start_byte, // old_pos to byte + old_end_byte: start_byte, // same + new_end_byte: start_byte + s.len(), // old_pos + s.len() + start_position, // old pos to coords + old_end_position: start_position, // same + new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over) }); - } - Insert(s) => { - let (start_byte, start_position) = point_at_pos(old_text, old_pos); - - // a subsequent delete means a replace, consume it - if let Some(Delete(len)) = iter.peek() { - old_end = old_pos + len; - let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end); - - iter.next(); - - // replacement - edits.push(tree_sitter::InputEdit { - start_byte, // old_pos to byte - old_end_byte, // old_end to byte - new_end_byte: start_byte + s.len(), // old_pos to byte + s.len() - start_position, // old pos to coords - old_end_position, // old_end to coords - new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over) - }); - } else { - // insert - edits.push(tree_sitter::InputEdit { - start_byte, // old_pos to byte - old_end_byte: start_byte, // same - new_end_byte: start_byte + s.len(), // old_pos + s.len() - start_position, // old pos to coords - old_end_position: start_position, // same - new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over) - }); - } } } - old_pos = old_end; } - edits - } - - fn update( - &mut self, - ts_parser: &mut TsParser, - config: &HighlightConfiguration, - old_source: &Rope, - source: &Rope, - changeset: &ChangeSet, - ) -> Result<(), Error> { - if changeset.is_empty() { - return Ok(()); - } - - let edits = Self::generate_edits(old_source.slice(..), changeset); - - // Notify the tree about all the changes - for edit in edits.iter().rev() { - // apply the edits in reverse. If we applied them in order then edit 1 would disrupt - // the positioning of edit 2 - self.tree.as_mut().unwrap().edit(edit); - } - - self.parse( - ts_parser, - config, - source, - 0, - // TODO: what to do about this range on update - vec![Range { - start_byte: 0, - end_byte: usize::MAX, - start_point: Point::new(0, 0), - end_point: Point::new(usize::MAX, usize::MAX), - }], - ) + old_pos = old_end; } - - // fn highlight_iter() -> same as Mode but for this layer. Mode composits these - // fn buffer_changed - // fn update(range) - // fn update_injections() + edits } -// -- refactored from tree-sitter-highlight to be able to retain state -// TODO: add seek() to iter - -// problem: any time a layer is updated it must update it's injections on the parent (potentially -// removing some from use) -// can't modify to vec and exist in it at the same time since that would violate borrows -// maybe we can do with an arena -// maybe just caching on the top layer and nevermind the injections for now? -// -// Grammar { -// layers: Vec<Box<Layer>> to prevent memory moves when vec is modified -// } -// injections tracked by marker: -// if marker areas match it's fine and update -// if not found add new layer -// if length 0 then area got removed, clean up the layer -// -// layer update: -// if range.len = 0 then remove the layer -// for change in changes { tree.edit(change) } -// tree = parser.parse(.., tree, ..) -// calculate affected range and update injections -// injection update: -// look for existing injections -// if present, range = (first injection start, last injection end) -// -// For now cheat and just throw out non-root layers if they exist. This should still improve -// parsing in majority of cases. - use std::sync::atomic::{AtomicUsize, Ordering}; use std::{iter, mem, ops, str, usize}; use tree_sitter::{ @@ -864,8 +1028,8 @@ pub enum HighlightEvent { pub struct HighlightConfiguration { pub language: Grammar, pub query: Query, + injections_query: Query, combined_injections_query: Option<Query>, - locals_pattern_index: usize, highlights_pattern_index: usize, highlight_indices: ArcSwap<Vec<Option<Highlight>>>, non_local_variable_patterns: Vec<bool>, @@ -892,13 +1056,9 @@ struct LocalScope<'a> { } #[derive(Debug)] -struct HighlightIter<'a, F> -where - F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, -{ +struct HighlightIter<'a> { source: RopeSlice<'a>, byte_offset: usize, - injection_callback: F, cancellation_flag: Option<&'a AtomicUsize>, layers: Vec<HighlightIterLayer<'a>>, iter_count: usize, @@ -938,8 +1098,8 @@ struct HighlightIterLayer<'a> { config: &'a HighlightConfiguration, highlight_end_stack: Vec<usize>, scope_stack: Vec<LocalScope<'a>>, - ranges: Vec<Range>, depth: usize, + ranges: &'a [Range], } impl<'a> fmt::Debug for HighlightIterLayer<'a> { @@ -971,38 +1131,32 @@ impl HighlightConfiguration { ) -> Result<Self, QueryError> { // Concatenate the query strings, keeping track of the start offset of each section. let mut query_source = String::new(); - query_source.push_str(injection_query); - let locals_query_offset = query_source.len(); query_source.push_str(locals_query); let highlights_query_offset = query_source.len(); query_source.push_str(highlights_query); // Construct a single query by concatenating the three query strings, but record the // range of pattern indices that belong to each individual string. - let mut query = Query::new(language, &query_source)?; - let mut locals_pattern_index = 0; + let query = Query::new(language, &query_source)?; let mut highlights_pattern_index = 0; for i in 0..(query.pattern_count()) { let pattern_offset = query.start_byte_for_pattern(i); if pattern_offset < highlights_query_offset { - if pattern_offset < highlights_query_offset { - highlights_pattern_index += 1; - } - if pattern_offset < locals_query_offset { - locals_pattern_index += 1; - } + highlights_pattern_index += 1; } } + let mut injections_query = Query::new(language, injection_query)?; + // Construct a separate query just for dealing with the 'combined injections'. // Disable the combined injection patterns in the main query. let mut combined_injections_query = Query::new(language, injection_query)?; let mut has_combined_queries = false; - for pattern_index in 0..locals_pattern_index { - let settings = query.property_settings(pattern_index); + for pattern_index in 0..injections_query.pattern_count() { + let settings = injections_query.property_settings(pattern_index); if settings.iter().any(|s| &*s.key == "injection.combined") { has_combined_queries = true; - query.disable_pattern(pattern_index); + injections_query.disable_pattern(pattern_index); } else { combined_injections_query.disable_pattern(pattern_index); } @@ -1034,8 +1188,6 @@ impl HighlightConfiguration { for (i, name) in query.capture_names().iter().enumerate() { let i = Some(i as u32); match name.as_str() { - "injection.content" => injection_content_capture_index = i, - "injection.language" => injection_language_capture_index = i, "local.definition" => local_def_capture_index = i, "local.definition-value" => local_def_value_capture_index = i, "local.reference" => local_ref_capture_index = i, @@ -1044,12 +1196,21 @@ impl HighlightConfiguration { } } + for (i, name) in injections_query.capture_names().iter().enumerate() { + let i = Some(i as u32); + match name.as_str() { + "injection.content" => injection_content_capture_index = i, + "injection.language" => injection_language_capture_index = i, + _ => {} + } + } + let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]); Ok(Self { language, query, + injections_query, combined_injections_query, - locals_pattern_index, highlights_pattern_index, highlight_indices, non_local_variable_patterns, @@ -1114,238 +1275,6 @@ impl HighlightConfiguration { } impl<'a> HighlightIterLayer<'a> { - /// Create a new 'layer' of highlighting for this document. - /// - /// In the even that the new layer contains "combined injections" (injections where multiple - /// disjoint ranges are parsed as one syntax tree), these will be eagerly processed and - /// added to the returned vector. - fn new<F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a>( - source: RopeSlice<'a>, - cancellation_flag: Option<&'a AtomicUsize>, - injection_callback: &mut F, - mut config: &'a HighlightConfiguration, - mut depth: usize, - mut ranges: Vec<Range>, - ) -> Result<Vec<Self>, Error> { - let mut result = Vec::with_capacity(1); - let mut queue = Vec::new(); - loop { - // --> Tree parsing part - - PARSER.with(|ts_parser| { - let highlighter = &mut ts_parser.borrow_mut(); - - if highlighter.parser.set_included_ranges(&ranges).is_ok() { - highlighter - .parser - .set_language(config.language) - .map_err(|_| Error::InvalidLanguage)?; - - unsafe { highlighter.parser.set_cancellation_flag(cancellation_flag) }; - let tree = highlighter - .parser - .parse_with( - &mut |byte, _| { - if byte <= source.len_bytes() { - let (chunk, start_byte, _, _) = source.chunk_at_byte(byte); - chunk[byte - start_byte..].as_bytes() - } else { - // out of range - &[] - } - }, - None, - ) - .ok_or(Error::Cancelled)?; - unsafe { highlighter.parser.set_cancellation_flag(None) }; - let mut cursor = highlighter.cursors.pop().unwrap_or_else(QueryCursor::new); - - // Process combined injections. - if let Some(combined_injections_query) = &config.combined_injections_query { - let mut injections_by_pattern_index = vec![ - (None, Vec::new(), false); - combined_injections_query - .pattern_count() - ]; - let matches = cursor.matches( - combined_injections_query, - tree.root_node(), - RopeProvider(source), - ); - for mat in matches { - let entry = &mut injections_by_pattern_index[mat.pattern_index]; - let (language_name, content_node, include_children) = - injection_for_match( - config, - combined_injections_query, - &mat, - source, - ); - if language_name.is_some() { - entry.0 = language_name; - } - if let Some(content_node) = content_node { - entry.1.push(content_node); - } - entry.2 = include_children; - } - for (lang_name, content_nodes, includes_children) in - injections_by_pattern_index - { - if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) - { - if let Some(next_config) = (injection_callback)(&lang_name) { - let ranges = Self::intersect_ranges( - &ranges, - &content_nodes, - includes_children, - ); - if !ranges.is_empty() { - queue.push((next_config, depth + 1, ranges)); - } - } - } - } - } - - // --> Highlighting query part - - // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which - // prevents them from being moved. But both of these values are really just - // pointers, so it's actually ok to move them. - let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(&tree) }; - let cursor_ref = - unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; - let captures = cursor_ref - .captures(&config.query, tree_ref.root_node(), RopeProvider(source)) - .peekable(); - - result.push(HighlightIterLayer { - highlight_end_stack: Vec::new(), - scope_stack: vec![LocalScope { - inherits: false, - range: 0..usize::MAX, - local_defs: Vec::new(), - }], - cursor, - depth, - _tree: Some(tree), - captures, - config, - ranges, - }); - } - - Ok(()) // so we can use the try operator - })?; - - if queue.is_empty() { - break; - } - - let (next_config, next_depth, next_ranges) = queue.remove(0); - config = next_config; - depth = next_depth; - ranges = next_ranges; - } - - Ok(result) - } - - // Compute the ranges that should be included when parsing an injection. - // This takes into account three things: - // * `parent_ranges` - The ranges must all fall within the *current* layer's ranges. - // * `nodes` - Every injection takes place within a set of nodes. The injection ranges - // are the ranges of those nodes. - // * `includes_children` - For some injections, the content nodes' children should be - // excluded from the nested document, so that only the content nodes' *own* content - // is reparsed. For other injections, the content nodes' entire ranges should be - // reparsed, including the ranges of their children. - fn intersect_ranges( - parent_ranges: &[Range], - nodes: &[Node], - includes_children: bool, - ) -> Vec<Range> { - let mut cursor = nodes[0].walk(); - let mut result = Vec::new(); - let mut parent_range_iter = parent_ranges.iter(); - let mut parent_range = parent_range_iter - .next() - .expect("Layers should only be constructed with non-empty ranges vectors"); - for node in nodes.iter() { - let mut preceding_range = Range { - start_byte: 0, - start_point: Point::new(0, 0), - end_byte: node.start_byte(), - end_point: node.start_position(), - }; - let following_range = Range { - start_byte: node.end_byte(), - start_point: node.end_position(), - end_byte: usize::MAX, - end_point: Point::new(usize::MAX, usize::MAX), - }; - - for excluded_range in node - .children(&mut cursor) - .filter_map(|child| { - if includes_children { - None - } else { - Some(child.range()) - } - }) - .chain([following_range].iter().cloned()) - { - let mut range = Range { - start_byte: preceding_range.end_byte, - start_point: preceding_range.end_point, - end_byte: excluded_range.start_byte, - end_point: excluded_range.start_point, - }; - preceding_range = excluded_range; - - if range.end_byte < parent_range.start_byte { - continue; - } - - while parent_range.start_byte <= range.end_byte { - if parent_range.end_byte > range.start_byte { - if range.start_byte < parent_range.start_byte { - range.start_byte = parent_range.start_byte; - range.start_point = parent_range.start_point; - } - - if parent_range.end_byte < range.end_byte { - if range.start_byte < parent_range.end_byte { - result.push(Range { - start_byte: range.start_byte, - start_point: range.start_point, - end_byte: parent_range.end_byte, - end_point: parent_range.end_point, - }); - } - range.start_byte = parent_range.end_byte; - range.start_point = parent_range.end_point; - } else { - if range.start_byte < range.end_byte { - result.push(range); - } - break; - } - } - - if let Some(next_range) = parent_range_iter.next() { - parent_range = next_range; - } else { - return result; - } - } - } - } - result - } - // First, sort scope boundaries by their byte offset in the document. At a // given position, emit scope endings before scope beginnings. Finally, emit // scope boundaries from deeper layers first. @@ -1371,10 +1300,101 @@ impl<'a> HighlightIterLayer<'a> { } } -impl<'a, F> HighlightIter<'a, F> -where - F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, -{ +// Compute the ranges that should be included when parsing an injection. +// This takes into account three things: +// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges. +// * `nodes` - Every injection takes place within a set of nodes. The injection ranges +// are the ranges of those nodes. +// * `includes_children` - For some injections, the content nodes' children should be +// excluded from the nested document, so that only the content nodes' *own* content +// is reparsed. For other injections, the content nodes' entire ranges should be +// reparsed, including the ranges of their children. +fn intersect_ranges( + parent_ranges: &[Range], + nodes: &[Node], + includes_children: bool, +) -> Vec<Range> { + let mut cursor = nodes[0].walk(); + let mut result = Vec::new(); + let mut parent_range_iter = parent_ranges.iter(); + let mut parent_range = parent_range_iter + .next() + .expect("Layers should only be constructed with non-empty ranges vectors"); + for node in nodes.iter() { + let mut preceding_range = Range { + start_byte: 0, + start_point: Point::new(0, 0), + end_byte: node.start_byte(), + end_point: node.start_position(), + }; + let following_range = Range { + start_byte: node.end_byte(), + start_point: node.end_position(), + end_byte: usize::MAX, + end_point: Point::new(usize::MAX, usize::MAX), + }; + + for excluded_range in node + .children(&mut cursor) + .filter_map(|child| { + if includes_children { + None + } else { + Some(child.range()) + } + }) + .chain([following_range].iter().cloned()) + { + let mut range = Range { + start_byte: preceding_range.end_byte, + start_point: preceding_range.end_point, + end_byte: excluded_range.start_byte, + end_point: excluded_range.start_point, + }; + preceding_range = excluded_range; + + if range.end_byte < parent_range.start_byte { + continue; + } + + while parent_range.start_byte <= range.end_byte { + if parent_range.end_byte > range.start_byte { + if range.start_byte < parent_range.start_byte { + range.start_byte = parent_range.start_byte; + range.start_point = parent_range.start_point; + } + + if parent_range.end_byte < range.end_byte { + if range.start_byte < parent_range.end_byte { + result.push(Range { + start_byte: range.start_byte, + start_point: range.start_point, + end_byte: parent_range.end_byte, + end_point: parent_range.end_point, + }); + } + range.start_byte = parent_range.end_byte; + range.start_point = parent_range.end_point; + } else { + if range.start_byte < range.end_byte { + result.push(range); + } + break; + } + } + + if let Some(next_range) = parent_range_iter.next() { + parent_range = next_range; + } else { + return result; + } + } + } + } + result +} + +impl<'a> HighlightIter<'a> { fn emit_event( &mut self, offset: usize, @@ -1405,6 +1425,12 @@ where i += 1; continue; } + } else { + let layer = self.layers.remove(i + 1); + PARSER.with(|ts_parser| { + let highlighter = &mut ts_parser.borrow_mut(); + highlighter.cursors.push(layer.cursor); + }); } break; } @@ -1421,30 +1447,9 @@ where } } } - - fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) { - if let Some(sort_key) = layer.sort_key() { - let mut i = 1; - while i < self.layers.len() { - if let Some(sort_key_i) = self.layers[i].sort_key() { - if sort_key_i > sort_key { - self.layers.insert(i, layer); - return; - } - i += 1; - } else { - self.layers.remove(i); - } - } - self.layers.push(layer); - } - } } -impl<'a, F> Iterator for HighlightIter<'a, F> -where - F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, -{ +impl<'a> Iterator for HighlightIter<'a> { type Item = Result<HighlightEvent, Error>; fn next(&mut self) -> Option<Self::Item> { @@ -1504,55 +1509,12 @@ where layer.highlight_end_stack.pop(); return self.emit_event(end_byte, Some(HighlightEvent::HighlightEnd)); } else { - // return self.emit_event(self.source.len(), None); - return None; + return self.emit_event(self.source.len_bytes(), None); }; let (mut match_, capture_index) = layer.captures.next().unwrap(); let mut capture = match_.captures[capture_index]; - // If this capture represents an injection, then process the injection. - if match_.pattern_index < layer.config.locals_pattern_index { - let (language_name, content_node, include_children) = - injection_for_match(layer.config, &layer.config.query, &match_, self.source); - - // Explicitly remove this match so that none of its other captures will remain - // in the stream of captures. - match_.remove(); - - // If a language is found with the given name, then add a new language layer - // to the highlighted document. - if let (Some(language_name), Some(content_node)) = (language_name, content_node) { - if let Some(config) = (self.injection_callback)(&language_name) { - let ranges = HighlightIterLayer::intersect_ranges( - &self.layers[0].ranges, - &[content_node], - include_children, - ); - if !ranges.is_empty() { - match HighlightIterLayer::new( - self.source, - self.cancellation_flag, - &mut self.injection_callback, - config, - self.layers[0].depth + 1, - ranges, - ) { - Ok(layers) => { - for layer in layers { - self.insert_layer(layer); - } - } - Err(e) => return Some(Err(e)), - } - } - } - } - - self.sort_layers(); - continue 'main; - } - // Remove from the local scope stack any local scopes that have already ended. while range.start > layer.scope_stack.last().unwrap().range.end { layer.scope_stack.pop(); @@ -1747,14 +1709,6 @@ fn injection_for_match<'a>( (language_name, content_node, include_children) } -// fn shrink_and_clear<T>(vec: &mut Vec<T>, capacity: usize) { -// if vec.len() > capacity { -// vec.truncate(capacity); -// vec.shrink_to_fit(); -// } -// vec.clear(); -// } - pub struct Merge<I> { iter: I, spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>, @@ -1921,6 +1875,8 @@ mod test { .map(String::from) .collect(); + let loader = Loader::new(Configuration { language: vec![] }); + let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap(); let config = HighlightConfiguration::new( language, @@ -1943,7 +1899,7 @@ mod test { fn main() {} ", ); - let syntax = Syntax::new(&source, Arc::new(config)); + let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)); let tree = syntax.tree(); let root = tree.root_node(); assert_eq!(root.kind(), "source_file"); @@ -1970,7 +1926,7 @@ mod test { &doc, vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(), ); - let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes()); + let edits = generate_edits(&doc, transaction.changes()); // transaction.apply(&mut state); assert_eq!( @@ -1999,7 +1955,7 @@ mod test { let mut doc = Rope::from("fn test() {}"); let transaction = Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter()); - let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes()); + let edits = generate_edits(&doc, transaction.changes()); transaction.apply(&mut doc); assert_eq!(doc, "fn test(a: u32) {}"); diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index dfc18fbe..2e34a986 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -22,7 +22,7 @@ pub enum Assoc { } // ChangeSpec = Change | ChangeSet | Vec<Change> -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ChangeSet { pub(crate) changes: Vec<Operation>, /// The required document length. Will refuse to apply changes unless it matches. @@ -30,16 +30,6 @@ pub struct ChangeSet { len_after: usize, } -impl Default for ChangeSet { - fn default() -> Self { - Self { - changes: Vec::new(), - len: 0, - len_after: 0, - } - } -} - impl ChangeSet { pub fn with_capacity(capacity: usize) -> Self { Self { @@ -95,7 +85,7 @@ impl ChangeSet { let new_last = match self.changes.as_mut_slice() { [.., Insert(prev)] | [.., Insert(prev), Delete(_)] => { - prev.push_tendril(&fragment); + prev.push_str(&fragment); return; } [.., last @ Delete(_)] => std::mem::replace(last, Insert(fragment)), @@ -199,7 +189,7 @@ impl ChangeSet { // TODO: cover this with a test // figure out the byte index of the truncated string end let (pos, _) = s.char_indices().nth(j).unwrap(); - s.pop_front(pos as u32); + s.replace_range(0..pos, ""); head_a = Some(Insert(s)); head_b = changes_b.next(); } @@ -221,9 +211,11 @@ impl ChangeSet { Ordering::Greater => { // figure out the byte index of the truncated string end let (pos, _) = s.char_indices().nth(j).unwrap(); - let pos = pos as u32; - changes.insert(s.subtendril(0, pos)); - head_a = Some(Insert(s.subtendril(pos, s.len() as u32 - pos))); + let mut before = s; + let after = before.split_off(pos); + + changes.insert(before); + head_a = Some(Insert(after)); head_b = changes_b.next(); } } @@ -287,7 +279,7 @@ impl ChangeSet { } Delete(n) => { let text = Cow::from(original_doc.slice(pos..pos + *n)); - changes.insert(Tendril::from_slice(&text)); + changes.insert(Tendril::from(text.as_ref())); pos += n; } Insert(s) => { @@ -330,7 +322,7 @@ impl ChangeSet { /// `true` when the set is empty. #[inline] pub fn is_empty(&self) -> bool { - self.changes.is_empty() + self.changes.is_empty() || self.changes == [Operation::Retain(self.len)] } /// Map a position through the changes. @@ -419,7 +411,7 @@ impl ChangeSet { /// Transaction represents a single undoable unit of changes. Several changes can be grouped into /// a single transaction. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Transaction { changes: ChangeSet, selection: Option<Selection>, @@ -720,19 +712,19 @@ mod test { #[test] fn optimized_composition() { let mut state = State::new("".into()); - let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('h')); + let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from("h")); t1.apply(&mut state.doc); state.selection = state.selection.clone().map(t1.changes()); - let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('e')); + let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from("e")); t2.apply(&mut state.doc); state.selection = state.selection.clone().map(t2.changes()); - let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l')); + let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l")); t3.apply(&mut state.doc); state.selection = state.selection.clone().map(t3.changes()); - let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l')); + let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l")); t4.apply(&mut state.doc); state.selection = state.selection.clone().map(t4.changes()); - let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('o')); + let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from("o")); t5.apply(&mut state.doc); state.selection = state.selection.clone().map(t5.changes()); @@ -771,7 +763,7 @@ mod test { #[test] fn combine_with_utf8() { - const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!"; + const TEST_CASE: &str = "Hello, これはヘリックスエディターです!"; let empty = Rope::from(""); let a = ChangeSet::new(&empty); diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml index 42dd29a8..24288697 100644 --- a/helix-dap/Cargo.toml +++ b/helix-dap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-dap" -version = "0.5.0" +version = "0.6.0" authors = ["Blaž Hrastnik <blaz@mxxn.io>"] edition = "2018" license = "MPL-2.0" @@ -12,7 +12,7 @@ homepage = "https://helix-editor.com" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -helix-core = { version = "0.5", path = "../helix-core" } +helix-core = { version = "0.6", path = "../helix-core" } anyhow = "1.0" log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/helix-dap/src/transport.rs b/helix-dap/src/transport.rs index 40474e99..783a6f5d 100644 --- a/helix-dap/src/transport.rs +++ b/helix-dap/src/transport.rs @@ -36,7 +36,7 @@ pub struct Response { #[serde(tag = "type", rename_all = "camelCase")] pub enum Payload { // type = "event" - Event(Event), + Event(Box<Event>), // type = "response" Response(Response), // type = "request" @@ -45,6 +45,7 @@ pub enum Payload { #[derive(Debug)] pub struct Transport { + #[allow(unused)] id: usize, pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>, } diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 83b2978d..39b53706 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-lsp" -version = "0.5.0" +version = "0.6.0" authors = ["Blaž Hrastnik <blaz@mxxn.io>"] edition = "2021" license = "MPL-2.0" @@ -12,16 +12,16 @@ homepage = "https://helix-editor.com" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -helix-core = { version = "0.5", path = "../helix-core" } +helix-core = { version = "0.6", path = "../helix-core" } anyhow = "1.0" futures-executor = "0.3" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures log = "0.4" -lsp-types = { version = "0.91", features = ["proposed"] } +lsp-types = { version = "0.92", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.16", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.8" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 271fd9d5..15cbca0e 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -31,6 +31,7 @@ pub struct Client { pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>, offset_encoding: OffsetEncoding, config: Option<Value>, + root_markers: Vec<String>, } impl Client { @@ -39,6 +40,7 @@ impl Client { cmd: &str, args: &[String], config: Option<Value>, + root_markers: Vec<String>, id: usize, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> { let process = Command::new(cmd) @@ -68,6 +70,7 @@ impl Client { capabilities: OnceCell::new(), offset_encoding: OffsetEncoding::Utf8, config, + root_markers, }; Ok((client, server_rx, initialize_notify)) @@ -202,7 +205,7 @@ impl Client { Ok(result) => Output::Success(Success { jsonrpc: Some(Version::V2), id, - result, + result: serde_json::to_value(result)?, }), Err(error) => Output::Failure(Failure { jsonrpc: Some(Version::V2), @@ -225,7 +228,8 @@ impl Client { pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> { // TODO: delay any requests that are triggered prior to initialize - let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok()); + let root = find_root(None, &self.root_markers) + .and_then(|root| lsp::Url::from_file_path(root).ok()); if self.config.is_some() { log::info!("Using custom LSP config: {}", self.config.as_ref().unwrap()); @@ -434,7 +438,7 @@ impl Client { changes.push(lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range::new(start, end)), - text: s.into(), + text: s.to_string(), range_length: None, }); } @@ -556,6 +560,14 @@ impl Client { self.call::<lsp::request::Completion>(params) } + pub async fn resolve_completion_item( + &self, + completion_item: lsp::CompletionItem, + ) -> Result<lsp::CompletionItem> { + self.request::<lsp::request::ResolveCompletionItem>(completion_item) + .await + } + pub fn text_document_signature_help( &self, text_document: lsp::TextDocumentIdentifier, @@ -800,4 +812,16 @@ impl Client { let response = self.request::<lsp::request::Rename>(params).await?; Ok(response.unwrap_or_default()) } + + pub fn command(&self, command: lsp::Command) -> impl Future<Output = Result<Value>> { + let params = lsp::ExecuteCommandParams { + command: command.command, + arguments: command.arguments.unwrap_or_default(), + work_done_progress_params: lsp::WorkDoneProgressParams { + work_done_token: None, + }, + }; + + self.call::<lsp::request::ExecuteCommand>(params) + } } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 7fa65928..109546d0 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -66,39 +66,26 @@ pub mod util { pos: lsp::Position, offset_encoding: OffsetEncoding, ) -> Option<usize> { - let max_line = doc.lines().count().saturating_sub(1); let pos_line = pos.line as usize; - let pos_line = if pos_line > max_line { + if pos_line > doc.len_lines() - 1 { return None; - } else { - pos_line - }; + } + match offset_encoding { OffsetEncoding::Utf8 => { - let max_char = doc - .line_to_char(max_line) - .checked_add(doc.line(max_line).len_chars())?; let line = doc.line_to_char(pos_line); let pos = line.checked_add(pos.character as usize)?; - if pos <= max_char { + if pos <= doc.len_chars() { Some(pos) } else { None } } OffsetEncoding::Utf16 => { - let max_char = doc - .line_to_char(max_line) - .checked_add(doc.line(max_line).len_chars())?; - let max_cu = doc.char_to_utf16_cu(max_char); let line = doc.line_to_char(pos_line); let line_start = doc.char_to_utf16_cu(line); let pos = line_start.checked_add(pos.character as usize)?; - if pos <= max_cu { - Some(doc.utf16_cu_to_char(pos)) - } else { - None - } + doc.try_utf16_cu_to_char(pos).ok() } } } @@ -203,6 +190,7 @@ pub mod util { #[derive(Debug, PartialEq, Clone)] pub enum MethodCall { WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams), + ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams), } impl MethodCall { @@ -215,6 +203,12 @@ impl MethodCall { .expect("Failed to parse WorkDoneCreate params"); Self::WorkDoneProgressCreate(params) } + lsp::request::ApplyWorkspaceEdit::METHOD => { + let params: lsp::ApplyWorkspaceEditParams = params + .parse() + .expect("Failed to parse ApplyWorkspaceEdit params"); + Self::ApplyWorkspaceEdit(params) + } _ => { log::warn!("unhandled lsp request: {}", method); return None; @@ -319,6 +313,7 @@ impl Registry { &config.command, &config.args, language_config.config.clone(), + language_config.roots.clone(), id, )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); @@ -337,7 +332,10 @@ impl Registry { }) .await; - value.expect("failed to initialize capabilities"); + if let Err(e) = value { + log::error!("failed to initialize language server: {}", e); + return; + } // next up, notify<initialized> _client diff --git a/helix-syntax/Cargo.toml b/helix-syntax/Cargo.toml index cceec412..855839be 100644 --- a/helix-syntax/Cargo.toml +++ b/helix-syntax/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-syntax" -version = "0.5.0" +version = "0.6.0" authors = ["Blaž Hrastnik <blaz@mxxn.io>"] edition = "2021" license = "MPL-2.0" diff --git a/helix-syntax/README.md b/helix-syntax/README.md new file mode 100644 index 00000000..bba2197a --- /dev/null +++ b/helix-syntax/README.md @@ -0,0 +1,13 @@ +helix-syntax +============ + +Syntax highlighting for helix, (shallow) submodules resides here. + +Differences from nvim-treesitter +-------------------------------- + +As the syntax are commonly ported from +<https://github.com/nvim-treesitter/nvim-treesitter>. + +Note that we do not support the custom `#any-of` predicate which is +supported by neovim so one needs to change it to `#match` with regex. diff --git a/helix-syntax/build.rs b/helix-syntax/build.rs index 28f85e74..fa8be8b3 100644 --- a/helix-syntax/build.rs +++ b/helix-syntax/build.rs @@ -175,7 +175,6 @@ fn build_dir(dir: &str, language: &str) { fn main() { let ignore = vec![ "tree-sitter-typescript".to_string(), - "tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34 "tree-sitter-ocaml".to_string(), ]; let dirs = collect_tree_sitter_dirs(&ignore).unwrap(); diff --git a/helix-syntax/languages/tree-sitter-comment b/helix-syntax/languages/tree-sitter-comment new file mode 160000 +Subproject 5dd3c62f1bbe378b220fe16b317b85247898639 diff --git a/helix-syntax/languages/tree-sitter-dart b/helix-syntax/languages/tree-sitter-dart new file mode 160000 +Subproject 6a25376685d1d47968c2cef06d4db8d84a70025 diff --git a/helix-syntax/languages/tree-sitter-dockerfile b/helix-syntax/languages/tree-sitter-dockerfile new file mode 160000 +Subproject 7af32bc04a66ab196f5b9f92ac471f29372ae2c diff --git a/helix-syntax/languages/tree-sitter-elm b/helix-syntax/languages/tree-sitter-elm new file mode 160000 +Subproject bd50ccf66b42c55252ac8efc1086af4ac6bab8c diff --git a/helix-syntax/languages/tree-sitter-fish b/helix-syntax/languages/tree-sitter-fish new file mode 160000 +Subproject 04e54ab6585dfd4fee6ddfe5849af56f101b6d4 diff --git a/helix-syntax/languages/tree-sitter-git-commit b/helix-syntax/languages/tree-sitter-git-commit new file mode 160000 +Subproject 066e395e1107df17183cf3ae4230f1a1406cc97 diff --git a/helix-syntax/languages/tree-sitter-git-config b/helix-syntax/languages/tree-sitter-git-config new file mode 160000 +Subproject 0e4f0baf90b57e5aeb62dcdbf03062c6315d43e diff --git a/helix-syntax/languages/tree-sitter-git-diff b/helix-syntax/languages/tree-sitter-git-diff new file mode 160000 +Subproject c12e6ecb54485f764250556ffd7ccb18f8e2942 diff --git a/helix-syntax/languages/tree-sitter-git-rebase b/helix-syntax/languages/tree-sitter-git-rebase new file mode 160000 +Subproject 332dc528f27044bc4427024dbb33e6941fc131f diff --git a/helix-syntax/languages/tree-sitter-go b/helix-syntax/languages/tree-sitter-go -Subproject 2a83dfdd759a632651f852aa4dc0af2525fae5c +Subproject 0fa917a7022d1cd2e9b779a6a8fc5dc7fad69c7 diff --git a/helix-syntax/languages/tree-sitter-graphql b/helix-syntax/languages/tree-sitter-graphql new file mode 160000 +Subproject 5e66e961eee421786bdda8495ed1db045e06b5f diff --git a/helix-syntax/languages/tree-sitter-haskell b/helix-syntax/languages/tree-sitter-haskell -Subproject 237f4eb4417c28f643a29d795ed227246afb66f +Subproject b6ec26f181dd059eedd506fa5fbeae1b8e5556c diff --git a/helix-syntax/languages/tree-sitter-iex b/helix-syntax/languages/tree-sitter-iex new file mode 160000 +Subproject 3ec55082cf0be015d03148be8edfdfa8c56e77f diff --git a/helix-syntax/languages/tree-sitter-lean b/helix-syntax/languages/tree-sitter-lean new file mode 160000 +Subproject d98426109258b266e1e92358c5f11716d2e8f63 diff --git a/helix-syntax/languages/tree-sitter-llvm b/helix-syntax/languages/tree-sitter-llvm new file mode 160000 +Subproject 3b213925b9c4f42c1acfe2e10bfbb438d9c6834 diff --git a/helix-syntax/languages/tree-sitter-llvm-mir b/helix-syntax/languages/tree-sitter-llvm-mir new file mode 160000 +Subproject 06fabca19454b2dc00c1b211a7cb7ad0bc2585f diff --git a/helix-syntax/languages/tree-sitter-make b/helix-syntax/languages/tree-sitter-make new file mode 160000 +Subproject a4b9187417d6be349ee5fd4b6e77b4172c6827d diff --git a/helix-syntax/languages/tree-sitter-markdown b/helix-syntax/languages/tree-sitter-markdown new file mode 160000 +Subproject ad8c32917a16dfbb387d1da567bf0c3fb6fffde diff --git a/helix-syntax/languages/tree-sitter-php b/helix-syntax/languages/tree-sitter-php -Subproject 0d63eaf94e8d6c0694551b016c802787e61b3fb +Subproject 57f855461aeeca73bd4218754fb26b5ac143f98 diff --git a/helix-syntax/languages/tree-sitter-regex b/helix-syntax/languages/tree-sitter-regex new file mode 160000 +Subproject e1cfca3c79896ff79842f057ea13e529b66af63 diff --git a/helix-syntax/languages/tree-sitter-rescript b/helix-syntax/languages/tree-sitter-rescript new file mode 160000 +Subproject 761eb9126b65e078b1b5770ac296b4af8870f93 diff --git a/helix-syntax/languages/tree-sitter-scala b/helix-syntax/languages/tree-sitter-scala -Subproject fb23ed9a99da012d86b7a5059b9d8928607cce2 +Subproject 0a3dd53a7fc4b352a538397d054380aaa28be54 diff --git a/helix-syntax/languages/tree-sitter-tablegen b/helix-syntax/languages/tree-sitter-tablegen new file mode 160000 +Subproject 568dd8a937347175fd58db83d4c4cdaeb6069bd diff --git a/helix-syntax/languages/tree-sitter-twig b/helix-syntax/languages/tree-sitter-twig new file mode 160000 +Subproject b7444181fb38e603e25ea8fcdac55f9492e49c2 diff --git a/helix-syntax/languages/tree-sitter-zig b/helix-syntax/languages/tree-sitter-zig -Subproject 1f27fd1dfe7f352408f01b4894c7825f3a1d6c4 +Subproject 93331b8bd8b4ebee2b575490b2758f16ad4e9f3 diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 43268291..e62496f2 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-term" -version = "0.5.0" +version = "0.6.0" description = "A post-modern text editor." authors = ["Blaž Hrastnik <blaz@mxxn.io>"] edition = "2021" @@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"] repository = "https://github.com/helix-editor/helix" homepage = "https://helix-editor.com" include = ["src/**/*", "README.md"] +default-run = "hx" [package.metadata.nix] build = true @@ -21,18 +22,18 @@ name = "hx" path = "src/main.rs" [dependencies] -helix-core = { version = "0.5", path = "../helix-core" } -helix-view = { version = "0.5", path = "../helix-view" } -helix-lsp = { version = "0.5", path = "../helix-lsp" } -helix-dap = { version = "0.5", path = "../helix-dap" } +helix-core = { version = "0.6", path = "../helix-core" } +helix-view = { version = "0.6", path = "../helix-view" } +helix-lsp = { version = "0.6", path = "../helix-lsp" } +helix-dap = { version = "0.6", path = "../helix-dap" } anyhow = "1" -once_cell = "1.8" +once_cell = "1.9" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } num_cpus = "1" tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } -crossterm = { version = "0.22", features = ["event-stream"] } +crossterm = { version = "0.23", features = ["event-stream"] } signal-hook = "0.3" tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } @@ -46,7 +47,7 @@ log = "0.4" fuzzy-matcher = "0.3" ignore = "0.4" # markdown doc rendering -pulldown-cmark = { version = "0.8", default-features = false } +pulldown-cmark = { version = "0.9", default-features = false } # file type detection content_inspector = "0.2.4" diff --git a/helix-term/build.rs b/helix-term/build.rs index 61ffa6f4..21dd5612 100644 --- a/helix-term/build.rs +++ b/helix-term/build.rs @@ -1,12 +1,17 @@ +use std::borrow::Cow; use std::process::Command; fn main() { let git_hash = Command::new("git") - .args(&["describe", "--dirty"]) + .args(&["rev-parse", "HEAD"]) .output() - .map(|x| String::from_utf8(x.stdout).ok()) .ok() - .flatten() - .unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION"))); - println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash); + .and_then(|x| String::from_utf8(x.stdout).ok()); + + let version: Cow<_> = match git_hash { + Some(git_hash) => format!("{} ({})", env!("CARGO_PKG_VERSION"), &git_hash[..8]).into(), + None => env!("CARGO_PKG_VERSION").into(), + }; + + println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version); } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 55e4bb03..52a5321f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,10 +1,16 @@ -use helix_core::{merge_toml_values, syntax}; +use helix_core::{merge_toml_values, pos_at_coords, syntax, Selection}; use helix_dap::{self as dap, Payload, Request}; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{editor::Breakpoint, theme, Editor}; +use serde_json::json; use crate::{ - args::Args, commands::fetch_stack_trace, compositor::Compositor, config::Config, job::Jobs, ui, + args::Args, + commands::{align_view, apply_workspace_edit, fetch_stack_trace, Align}, + compositor::Compositor, + config::Config, + job::Jobs, + ui, }; use log::{error, warn}; @@ -78,17 +84,27 @@ impl Application { None => Ok(def_lang_conf), }; - let theme = if let Some(theme) = &config.theme { - match theme_loader.load(theme) { - Ok(theme) => theme, - Err(e) => { - log::warn!("failed to load theme `{}` - {}", theme, e); + let true_color = config.editor.true_color || crate::true_color(); + let theme = config + .theme + .as_ref() + .and_then(|theme| { + theme_loader + .load(theme) + .map_err(|e| { + log::warn!("failed to load theme `{}` - {}", theme, e); + e + }) + .ok() + .filter(|theme| (true_color || theme.is_16_color())) + }) + .unwrap_or_else(|| { + if true_color { theme_loader.default() + } else { + theme_loader.base16_default() } - } - } else { - theme_loader.default() - }; + }); let syn_loader_conf: helix_core::syntax::Configuration = lang_conf .and_then(|conf| conf.try_into()) @@ -118,7 +134,7 @@ impl Application { // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; } else if !args.files.is_empty() { - let first = &args.files[0]; // we know it's not empty + let first = &args.files[0].0; // we know it's not empty if first.is_dir() { std::env::set_current_dir(&first)?; editor.new_file(Action::VerticalSplit); @@ -126,16 +142,25 @@ impl Application { } else { let nr_of_files = args.files.len(); editor.open(first.to_path_buf(), Action::VerticalSplit)?; - for file in args.files { + for (file, pos) in args.files { if file.is_dir() { return Err(anyhow::anyhow!( "expected a path to file, found a directory. (to open a directory pass it as first argument)" )); } else { - editor.open(file.to_path_buf(), Action::Load)?; + let doc_id = editor.open(file, Action::Load)?; + // with Action::Load all documents have the same view + let view_id = editor.tree.focus; + let doc = editor.document_mut(doc_id).unwrap(); + let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true)); + doc.set_selection(view_id, pos); } } editor.set_status(format!("Loaded {} files.", nr_of_files)); + // align the view to center after all files are loaded, + // does not affect views without pos since it is at the top + let (view, doc) = current!(editor); + align_view(doc, view, Align::Center); } } else if stdin().is_tty() { editor.new_file(Action::VerticalSplit); @@ -197,7 +222,6 @@ impl Application { loop { if self.editor.should_close() { - self.jobs.finish(); break; } @@ -328,7 +352,7 @@ impl Application { None => return, }; match payload { - Payload::Event(ev) => match ev { + Payload::Event(ev) => match *ev { Event::Stopped(events::Stopped { thread_id, description, @@ -529,12 +553,8 @@ impl Application { // trigger textDocument/didOpen for docs that are already open for doc in docs { - // TODO: extract and share with editor.open - let language_id = doc - .language() - .and_then(|s| s.split('.').last()) // source.rust - .map(ToOwned::to_owned) - .unwrap_or_default(); + let language_id = + doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); tokio::spawn(language_server.text_document_did_open( doc.url().unwrap(), @@ -549,6 +569,7 @@ impl Application { let doc = self.editor.document_by_path_mut(&path); if let Some(doc) = doc { + let lang_conf = doc.language_config(); let text = doc.text(); let diagnostics = params @@ -586,19 +607,31 @@ impl Application { return None; }; + let severity = + diagnostic.severity.map(|severity| match severity { + DiagnosticSeverity::ERROR => Error, + DiagnosticSeverity::WARNING => Warning, + DiagnosticSeverity::INFORMATION => Info, + DiagnosticSeverity::HINT => Hint, + severity => unreachable!( + "unrecognized diagnostic severity: {:?}", + severity + ), + }); + + if let Some(lang_conf) = lang_conf { + if let Some(severity) = severity { + if severity < lang_conf.diagnostic_severity { + return None; + } + } + }; + Some(Diagnostic { range: Range { start, end }, line: diagnostic.range.start.line as usize, message: diagnostic.message, - severity: diagnostic.severity.map( - |severity| match severity { - DiagnosticSeverity::ERROR => Error, - DiagnosticSeverity::WARNING => Warning, - DiagnosticSeverity::INFORMATION => Info, - DiagnosticSeverity::HINT => Hint, - severity => unimplemented!("{:?}", severity), - }, - ), + severity, // code // source }) @@ -705,14 +738,6 @@ impl Application { Call::MethodCall(helix_lsp::jsonrpc::MethodCall { method, params, id, .. }) => { - let language_server = match self.editor.language_servers.get_by_id(server_id) { - Some(language_server) => language_server, - None => { - warn!("can't find language server with id `{}`", server_id); - return; - } - }; - let call = match MethodCall::parse(&method, params) { Some(call) => call, None => { @@ -742,8 +767,42 @@ impl Application { if spinner.is_stopped() { spinner.start(); } + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null))); } + MethodCall::ApplyWorkspaceEdit(params) => { + apply_workspace_edit( + &mut self.editor, + helix_lsp::OffsetEncoding::Utf8, + ¶ms.edit, + ); + + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + + tokio::spawn(language_server.reply( + id, + Ok(json!(lsp::ApplyWorkspaceEditResponse { + applied: true, + failure_reason: None, + failed_change: None, + })), + )); + } } } e => unreachable!("{:?}", e), @@ -789,6 +848,8 @@ impl Application { self.event_loop().await; + self.jobs.finish().await; + if self.editor.close_language_servers(None).await.is_err() { log::error!("Timed out waiting for language servers to shutdown"); }; diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 40113db9..247d5b32 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -1,5 +1,6 @@ use anyhow::{Error, Result}; -use std::path::PathBuf; +use helix_core::Position; +use std::path::{Path, PathBuf}; #[derive(Default)] pub struct Args { @@ -7,7 +8,7 @@ pub struct Args { pub display_version: bool, pub load_tutor: bool, pub verbosity: u64, - pub files: Vec<PathBuf>, + pub files: Vec<(PathBuf, Position)>, } impl Args { @@ -41,15 +42,49 @@ impl Args { } } } - arg => args.files.push(PathBuf::from(arg)), + arg => args.files.push(parse_file(arg)), } } // push the remaining args, if any to the files - for filename in iter { - args.files.push(PathBuf::from(filename)); + for arg in iter { + args.files.push(parse_file(arg)); } Ok(args) } } + +/// Parse arg into [`PathBuf`] and position. +pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) { + let def = || (PathBuf::from(s), Position::default()); + if Path::new(s).exists() { + return def(); + } + split_path_row_col(s) + .or_else(|| split_path_row(s)) + .unwrap_or_else(def) +} + +/// Split file.rs:10:2 into [`PathBuf`], row and col. +/// +/// Does not validate if file.rs is a file or directory. +fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> { + let mut s = s.rsplitn(3, ':'); + let col: usize = s.next()?.parse().ok()?; + let row: usize = s.next()?.parse().ok()?; + let path = s.next()?.into(); + let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); + Some((path, pos)) +} + +/// Split file.rs:10 into [`PathBuf`] and row. +/// +/// Does not validate if file.rs is a file or directory. +fn split_path_row(s: &str) -> Option<(PathBuf, Position)> { + let (row, path) = s.rsplit_once(':')?; + let row: usize = row.parse().ok()?; + let path = path.into(); + let pos = Position::new(row.saturating_sub(1), 0); + Some((path, pos)) +} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1871c67e..677943e8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5,15 +5,17 @@ pub use dap::*; use helix_core::{ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, + increment::date_time::DateTimeIncrementor, + increment::{number::NumberIncrementor, Increment}, indent, indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, Direction}, - numbers::NumberIncrementor, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, - search, selection, surround, textobject, + search, selection, shellwords, surround, textobject, + tree_sitter::Node, unicode::width::UnicodeWidthChar, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -22,13 +24,15 @@ use helix_view::{ clipboard::ClipboardType, document::{Mode, SCRATCH_BUFFER_NAME}, editor::{Action, Motion}, + info::Info, input::KeyEvent, keyboard::KeyCode, view::View, Document, DocumentId, Editor, ViewId, }; -use anyhow::{anyhow, bail, Context as _}; +use anyhow::{anyhow, bail, ensure, Context as _}; +use fuzzy_matcher::FuzzyMatcher; use helix_lsp::{ block_on, lsp, util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range}, @@ -38,14 +42,15 @@ use insert::*; use movement::Movement; use crate::{ + args, compositor::{self, Component, Compositor}, - ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent}, + ui::{self, FilePicker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Job, Jobs}; use futures_util::{FutureExt, StreamExt}; -use std::num::NonZeroUsize; use std::{collections::HashMap, fmt, future::Future}; +use std::{collections::HashSet, num::NonZeroUsize}; use std::{ borrow::Cow, @@ -73,7 +78,7 @@ pub struct Context<'a> { impl<'a> Context<'a> { /// Push a new component onto the compositor. pub fn push_layer(&mut self, component: Box<dyn Component>) { - self.callback = Some(Box::new(|compositor: &mut Compositor| { + self.callback = Some(Box::new(|compositor: &mut Compositor, _| { compositor.push(component) })); } @@ -138,47 +143,76 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) { view.offset.row = line.saturating_sub(relative); } -/// A command is composed of a static name, and a function that takes the current state plus a count, -/// and does a side-effect on the state (usually by creating and applying a transaction). -#[derive(Copy, Clone)] -pub struct Command { - name: &'static str, - fun: fn(cx: &mut Context), - doc: &'static str, -} - -macro_rules! commands { +/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like +/// :format. It causes a side-effect on the state (usually by creating and applying a transaction). +/// Both of these types of commands can be mapped with keybindings in the config.toml. +#[derive(Clone)] +pub enum MappableCommand { + Typable { + name: String, + args: Vec<String>, + doc: String, + }, + Static { + name: &'static str, + fun: fn(cx: &mut Context), + doc: &'static str, + }, +} + +macro_rules! static_commands { ( $($name:ident, $doc:literal,)* ) => { $( #[allow(non_upper_case_globals)] - pub const $name: Self = Self { + pub const $name: Self = Self::Static { name: stringify!($name), fun: $name, doc: $doc }; )* - pub const COMMAND_LIST: &'static [Self] = &[ + pub const STATIC_COMMAND_LIST: &'static [Self] = &[ $( Self::$name, )* ]; } } -impl Command { +impl MappableCommand { pub fn execute(&self, cx: &mut Context) { - (self.fun)(cx); + match &self { + Self::Typable { name, args, doc: _ } => { + let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect(); + if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) { + let mut cx = compositor::Context { + editor: cx.editor, + jobs: cx.jobs, + scroll: None, + }; + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { + cx.editor.set_error(format!("{}", e)); + } + } + } + Self::Static { fun, .. } => (fun)(cx), + } } - pub fn name(&self) -> &'static str { - self.name + pub fn name(&self) -> &str { + match &self { + Self::Typable { name, .. } => name, + Self::Static { name, .. } => name, + } } - pub fn doc(&self) -> &'static str { - self.doc + pub fn doc(&self) -> &str { + match &self { + Self::Typable { doc, .. } => doc, + Self::Static { doc, .. } => doc, + } } #[rustfmt::skip] - commands!( + static_commands!( no_op, "Do nothing", move_char_left, "Move left", move_char_right, "Move right", @@ -240,6 +274,7 @@ impl Command { change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)", collapse_selection, "Collapse selection onto a single cursor", flip_selections, "Flip selection cursor and anchor", + ensure_selections_forward, "Ensure the selection is in forward direction", insert_mode, "Insert before selection", append_mode, "Insert after selection (append)", command_mode, "Enter command mode", @@ -261,16 +296,17 @@ impl Command { add_newline_below, "Add newline below", goto_type_definition, "Goto type definition", goto_implementation, "Goto implementation", - goto_file_start, "Goto file start/line", + goto_file_start, "Goto line number <n> else file start", goto_file_end, "Goto file end", - goto_file, "Goto files in the selection", - goto_file_hsplit, "Goto files in the selection in horizontal splits", - goto_file_vsplit, "Goto files in the selection in vertical splits", + goto_file, "Goto files in selection", + goto_file_hsplit, "Goto files in selection (hsplit)", + goto_file_vsplit, "Goto files in selection (vsplit)", goto_reference, "Goto references", goto_window_top, "Goto window top", - goto_window_middle, "Goto window middle", + goto_window_center, "Goto window center", goto_window_bottom, "Goto window bottom", goto_last_accessed_file, "Goto last accessed file", + goto_last_modified_file, "Goto last modified file", goto_last_modification, "Goto last modification", goto_line, "Goto line", goto_last_line, "Goto last line", @@ -333,8 +369,12 @@ impl Command { rotate_selection_contents_forward, "Rotate selection contents forward", rotate_selection_contents_backward, "Rotate selections contents backward", expand_selection, "Expand selection to parent syntax node", + shrink_selection, "Shrink selection to previously expanded syntax node", + select_next_sibling, "Select the next sibling in the syntax tree", + select_prev_sibling, "Select the previous sibling in the syntax tree", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", + save_selection, "Save the current selection to the jumplist", jump_view_right, "Jump to the split to the right", jump_view_left, "Jump to the split to the left", jump_view_up, "Jump to the split above", @@ -382,36 +422,56 @@ impl Command { rename_symbol, "Rename symbol", increment, "Increment", decrement, "Decrement", + record_macro, "Record macro", + replay_macro, "Replay macro", ); } -impl fmt::Debug for Command { +impl fmt::Debug for MappableCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command { name, .. } = self; - f.debug_tuple("Command").field(name).finish() + f.debug_tuple("MappableCommand") + .field(&self.name()) + .finish() } } -impl fmt::Display for Command { +impl fmt::Display for MappableCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command { name, .. } = self; - f.write_str(name) + f.write_str(self.name()) } } -impl std::str::FromStr for Command { +impl std::str::FromStr for MappableCommand { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { - Command::COMMAND_LIST - .iter() - .copied() - .find(|cmd| cmd.name == s) - .ok_or_else(|| anyhow!("No command named '{}'", s)) + if let Some(suffix) = s.strip_prefix(':') { + let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim()); + let name = typable_command + .next() + .ok_or_else(|| anyhow!("Expected typable command name"))?; + let args = typable_command + .map(|s| s.to_owned()) + .collect::<Vec<String>>(); + cmd::TYPABLE_COMMAND_MAP + .get(name) + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + doc: format!(":{} {:?}", cmd.name, args), + args, + }) + .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) + } else { + MappableCommand::STATIC_COMMAND_LIST + .iter() + .find(|cmd| cmd.name() == s) + .cloned() + .ok_or_else(|| anyhow!("No command named '{}'", s)) + } } } -impl<'de> Deserialize<'de> for Command { +impl<'de> Deserialize<'de> for MappableCommand { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, @@ -421,9 +481,27 @@ impl<'de> Deserialize<'de> for Command { } } -impl PartialEq for Command { +impl PartialEq for MappableCommand { fn eq(&self, other: &Self) -> bool { - self.name() == other.name() + match (self, other) { + ( + MappableCommand::Typable { + name: first_name, .. + }, + MappableCommand::Typable { + name: second_name, .. + }, + ) => first_name == second_name, + ( + MappableCommand::Static { + name: first_name, .. + }, + MappableCommand::Static { + name: second_name, .. + }, + ) => first_name == second_name, + _ => false, + } } } @@ -622,8 +700,15 @@ fn kill_to_line_end(cx: &mut Context) { let selection = doc.selection(view.id).clone().transform(|range| { let line = range.cursor_line(text); - let pos = line_end_char_index(&text, line); - range.put_cursor(text, pos, true) + let line_end_pos = line_end_char_index(&text, line); + let pos = range.cursor(text); + + let mut new_range = range.put_cursor(text, line_end_pos, true); + // don't want to remove the line separator itself if the cursor doesn't reach the end of line. + if pos != line_end_pos { + new_range.head = line_end_pos; + } + new_range }); delete_selection_insert_mode(doc, view, &selection); } @@ -736,7 +821,6 @@ fn align_selections(cx: &mut Context) { }); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String { @@ -770,8 +854,8 @@ fn goto_window(cx: &mut Context, align: Align) { Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)), Align::Bottom => last_line.saturating_sub(scrolloff + count), } - .min(last_line.saturating_sub(scrolloff)) - .max(view.offset.row + scrolloff); + .max(view.offset.row + scrolloff) + .min(last_line.saturating_sub(scrolloff)); let pos = doc.text().line_to_char(line); @@ -782,7 +866,7 @@ fn goto_window_top(cx: &mut Context) { goto_window(cx, Align::Top) } -fn goto_window_middle(cx: &mut Context) { +fn goto_window_center(cx: &mut Context) { goto_window(cx, Align::Center) } @@ -1139,7 +1223,6 @@ fn replace(cx: &mut Context) { }); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } }) } @@ -1157,7 +1240,6 @@ where }); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn switch_case(cx: &mut Context) { @@ -1222,16 +1304,23 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { .max(view.offset.row + scrolloff) .min(last_line.saturating_sub(scrolloff)); - let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end + // If cursor needs moving, replace primary selection + if line != cursor.row { + let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end - let anchor = if doc.mode == Mode::Select { - range.anchor - } else { - head - }; + let anchor = if doc.mode == Mode::Select { + range.anchor + } else { + head + }; - // TODO: only manipulate main selection - doc.set_selection(view.id, Selection::single(anchor, head)); + // replace primary selection with an empty selection at cursor pos + let prim_sel = Range::new(anchor, head); + let mut sel = doc.selection(view.id).clone(); + let idx = sel.primary_index(); + sel = sel.replace(idx, prim_sel); + doc.set_selection(view.id, sel); + } } fn page_up(cx: &mut Context) { @@ -1389,6 +1478,7 @@ fn split_selection_on_newline(cx: &mut Context) { doc.set_selection(view.id, selection); } +#[allow(clippy::too_many_arguments)] fn search_impl( doc: &mut Document, view: &mut View, @@ -1397,6 +1487,7 @@ fn search_impl( movement: Movement, direction: Direction, scrolloff: usize, + wrap_around: bool, ) { let text = doc.text().slice(..); let selection = doc.selection(view.id); @@ -1422,16 +1513,22 @@ fn search_impl( // use find_at to find the next match after the cursor, loop around the end // Careful, `Regex` uses `bytes` as offsets, not character indices! - let mat = match direction { - Direction::Forward => regex - .find_at(contents, start) - .or_else(|| regex.find(contents)), - Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| { - offset = start; - regex.find_iter(&contents[start..]).last() - }), + let mut mat = match direction { + Direction::Forward => regex.find_at(contents, start), + Direction::Backward => regex.find_iter(&contents[..start]).last(), }; - // TODO: message on wraparound + + if wrap_around && mat.is_none() { + mat = match direction { + Direction::Forward => regex.find(contents), + Direction::Backward => { + offset = start; + regex.find_iter(&contents[start..]).last() + } + } + // TODO: message on wraparound + } + if let Some(mat) = mat { let start = text.byte_to_char(mat.start() + offset); let end = text.byte_to_char(mat.end() + offset); @@ -1483,8 +1580,9 @@ fn rsearch(cx: &mut Context) { fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); let scrolloff = cx.editor.config.scrolloff; + let wrap_around = cx.editor.config.search.wrap_around; - let (_, doc) = current!(cx.editor); + let doc = doc!(cx.editor); // TODO: could probably share with select_on_matches? @@ -1516,6 +1614,7 @@ fn searcher(cx: &mut Context, direction: Direction) { Movement::Move, direction, scrolloff, + wrap_around, ); }, ); @@ -1530,16 +1629,27 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir if let Some(query) = registers.read('/') { let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); - let case_insensitive = if cx.editor.config.smart_case { + let search_config = &cx.editor.config.search; + let case_insensitive = if search_config.smart_case { !query.chars().any(char::is_uppercase) } else { false }; + let wrap_around = search_config.wrap_around; if let Ok(regex) = RegexBuilder::new(query) .case_insensitive(case_insensitive) .build() { - search_impl(doc, view, &contents, ®ex, movement, direction, scrolloff); + search_impl( + doc, + view, + &contents, + ®ex, + movement, + direction, + scrolloff, + wrap_around, + ); } else { // get around warning `mutable_borrow_reservation_conflict` // which will be a hard error in the future @@ -1571,14 +1681,14 @@ fn search_selection(cx: &mut Context) { let query = doc.selection(view.id).primary().fragment(contents); let regex = regex::escape(&query); cx.editor.registers.get_mut('/').push(regex); - let msg = format!("register '{}' set to '{}'", '\\', query); + let msg = format!("register '{}' set to '{}'", '/', query); cx.editor.set_status(msg); } fn global_search(cx: &mut Context) { let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); - let smart_case = cx.editor.config.smart_case; + let smart_case = cx.editor.config.search.smart_case; let file_picker_config = cx.editor.config.file_picker.clone(); let completions = search_completions(cx, None); @@ -1789,7 +1899,6 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) { match op { Operation::Delete => { - doc.append_changes_to_history(view.id); // exit select mode, if currently in select mode exit_select_mode(cx); } @@ -1845,7 +1954,21 @@ fn flip_selections(cx: &mut Context) { let selection = doc .selection(view.id) .clone() - .transform(|range| Range::new(range.head, range.anchor)); + .transform(|range| range.flip()); + doc.set_selection(view.id, selection); +} + +fn ensure_selections_forward(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let selection = doc + .selection(view.id) + .clone() + .transform(|r| match r.direction() { + Direction::Forward => r, + Direction::Backward => r.flip(), + }); + doc.set_selection(view.id, selection); } @@ -1879,7 +2002,7 @@ fn append_mode(cx: &mut Context) { if !last_range.is_empty() && last_range.head == end { let transaction = Transaction::change( doc.text(), - std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]), + [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(), ); doc.apply(&transaction, view.id); } @@ -1893,7 +2016,7 @@ fn append_mode(cx: &mut Context) { doc.set_selection(view.id, selection); } -mod cmd { +pub mod cmd { use super::*; use helix_view::editor::Action; @@ -1905,13 +2028,13 @@ mod cmd { pub aliases: &'static [&'static str], pub doc: &'static str, // params, flags, helper, completer - pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>, + pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>, pub completer: Option<Completer>, } fn quit( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { // last view and we have unsaved changes @@ -1926,7 +2049,7 @@ mod cmd { fn force_quit( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor.close(view!(cx.editor).id); @@ -1936,17 +2059,25 @@ mod cmd { fn open( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - let path = args.get(0).context("wrong argument count")?; - let _ = cx.editor.open(path.into(), Action::Replace)?; + ensure!(!args.is_empty(), "wrong argument count"); + for arg in args { + let (path, pos) = args::parse_file(arg); + let _ = cx.editor.open(path, Action::Replace)?; + let (view, doc) = current!(cx.editor); + let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true)); + doc.set_selection(view.id, pos); + // does not affect opening a buffer without pos + align_view(doc, view, Align::Center); + } Ok(()) } fn buffer_close( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let view = view!(cx.editor); @@ -1957,7 +2088,7 @@ mod cmd { fn force_buffer_close( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let view = view!(cx.editor); @@ -1966,15 +2097,12 @@ mod cmd { Ok(()) } - fn write_impl<P: AsRef<Path>>( - cx: &mut compositor::Context, - path: Option<P>, - ) -> anyhow::Result<()> { + fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> { let jobs = &mut cx.jobs; - let (_, doc) = current!(cx.editor); + let doc = doc_mut!(cx.editor); if let Some(ref path) = path { - doc.set_path(Some(path.as_ref())) + doc.set_path(Some(path.as_ref().as_ref())) .context("invalid filepath")?; } if doc.path().is_none() { @@ -2003,7 +2131,7 @@ mod cmd { fn write( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first()) @@ -2011,7 +2139,7 @@ mod cmd { fn new_file( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor.new_file(Action::Replace); @@ -2021,11 +2149,10 @@ mod cmd { fn format( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - + let doc = doc!(cx.editor); if let Some(format) = doc.format() { let callback = make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format); @@ -2036,7 +2163,7 @@ mod cmd { } fn set_indent_style( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { use IndentStyle::*; @@ -2056,7 +2183,7 @@ mod cmd { // Attempt to parse argument as an indent style. let style = match args.get(0) { Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs), - Some(&"0") => Some(Tabs), + Some(Cow::Borrowed("0")) => Some(Tabs), Some(arg) => arg .parse::<u8>() .ok() @@ -2075,7 +2202,7 @@ mod cmd { /// Sets or reports the current document's line ending setting. fn set_line_ending( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { use LineEnding::*; @@ -2119,7 +2246,7 @@ mod cmd { fn earlier( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?; @@ -2135,7 +2262,7 @@ mod cmd { fn later( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?; @@ -2150,7 +2277,7 @@ mod cmd { fn write_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first())?; @@ -2159,7 +2286,7 @@ mod cmd { fn force_write_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first())?; @@ -2190,13 +2317,13 @@ mod cmd { fn write_all_impl( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, quit: bool, force: bool, ) -> anyhow::Result<()> { let mut errors = String::new(); - + let jobs = &mut cx.jobs; // save all documents for doc in &mut cx.editor.documents.values_mut() { if doc.path().is_none() { @@ -2204,9 +2331,23 @@ mod cmd { continue; } - // TODO: handle error. - let handle = doc.save(); - cx.jobs.add(Job::new(handle).wait_before_exiting()); + if !doc.is_modified() { + continue; + } + + let fmt = doc.auto_format().map(|fmt| { + let shared = fmt.shared(); + let callback = make_format_callback( + doc.id(), + doc.version(), + Modified::SetUnmodified, + shared.clone(), + ); + jobs.callback(callback); + shared + }); + let future = doc.format_and_save(fmt); + jobs.add(Job::new(future).wait_before_exiting()); } if quit { @@ -2226,7 +2367,7 @@ mod cmd { fn write_all( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, false, false) @@ -2234,7 +2375,7 @@ mod cmd { fn write_all_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, true, false) @@ -2242,18 +2383,13 @@ mod cmd { fn force_write_all_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, true, true) } - fn quit_all_impl( - editor: &mut Editor, - _args: &[&str], - _event: PromptEvent, - force: bool, - ) -> anyhow::Result<()> { + fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> { if !force { buffers_remaining_impl(editor)?; } @@ -2269,23 +2405,23 @@ mod cmd { fn quit_all( cx: &mut compositor::Context, - args: &[&str], - event: PromptEvent, + _args: &[Cow<str>], + _event: PromptEvent, ) -> anyhow::Result<()> { - quit_all_impl(&mut cx.editor, args, event, false) + quit_all_impl(cx.editor, false) } fn force_quit_all( cx: &mut compositor::Context, - args: &[&str], - event: PromptEvent, + _args: &[Cow<str>], + _event: PromptEvent, ) -> anyhow::Result<()> { - quit_all_impl(&mut cx.editor, args, event, true) + quit_all_impl(cx.editor, true) } fn cquit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let exit_code = args @@ -2294,95 +2430,110 @@ mod cmd { .unwrap_or(1); cx.editor.exit_code = exit_code; - let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); - for view_id in views { - cx.editor.close(view_id); - } + quit_all_impl(cx.editor, false) + } - Ok(()) + fn force_cquit( + cx: &mut compositor::Context, + args: &[Cow<str>], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let exit_code = args + .first() + .and_then(|code| code.parse::<i32>().ok()) + .unwrap_or(1); + cx.editor.exit_code = exit_code; + + quit_all_impl(cx.editor, true) } fn theme( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - let theme = args.first().context("theme not provided")?; - cx.editor.set_theme_from_name(theme) + let theme = args.first().context("Theme not provided")?; + let theme = cx + .editor + .theme_loader + .load(theme) + .with_context(|| format!("Failed setting theme {}", theme))?; + let true_color = cx.editor.config.true_color || crate::true_color(); + if !(true_color || theme.is_16_color()) { + bail!("Unsupported theme: theme requires true color support"); + } + cx.editor.set_theme(theme); + Ok(()) } fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard) + yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard) } fn yank_joined_to_clipboard( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let separator = args - .first() - .copied() - .unwrap_or_else(|| doc.line_ending.as_str()); - yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard) + let doc = doc!(cx.editor); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); + yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard) } fn yank_main_selection_to_primary_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection) + yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection) } fn yank_joined_to_primary_clipboard( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let separator = args - .first() - .copied() - .unwrap_or_else(|| doc.line_ending.as_str()); - yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection) + let doc = doc!(cx.editor); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); + yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection) } fn paste_clipboard_after( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1) } fn paste_clipboard_before( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1) } fn paste_primary_clipboard_after( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1) } fn paste_primary_clipboard_before( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1) } fn replace_selections_with_clipboard_impl( @@ -2409,7 +2560,7 @@ mod cmd { fn replace_selections_with_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard) @@ -2417,7 +2568,7 @@ mod cmd { fn replace_selections_with_primary_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { replace_selections_with_clipboard_impl(cx, ClipboardType::Selection) @@ -2425,7 +2576,7 @@ mod cmd { fn show_clipboard_provider( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor @@ -2435,12 +2586,13 @@ mod cmd { fn change_current_directory( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let dir = helix_core::path::expand_tilde( args.first() .context("target directory not provided")? + .as_ref() .as_ref(), ); @@ -2458,7 +2610,7 @@ mod cmd { fn show_current_directory( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let cwd = std::env::current_dir().context("Couldn't get the new working directory")?; @@ -2470,10 +2622,10 @@ mod cmd { /// Sets the [`Document`]'s encoding.. fn set_encoding( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); + let doc = doc_mut!(cx.editor); if let Some(label) = args.first() { doc.set_encoding(label) } else { @@ -2486,7 +2638,7 @@ mod cmd { /// Reload the [`Document`] from its source file. fn reload( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let (view, doc) = current!(cx.editor); @@ -2495,7 +2647,7 @@ mod cmd { fn tree_sitter_scopes( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let (view, doc) = current!(cx.editor); @@ -2509,15 +2661,18 @@ mod cmd { fn vsplit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let id = view!(cx.editor).doc; - if let Some(path) = args.get(0) { - cx.editor.open(path.into(), Action::VerticalSplit)?; - } else { + if args.is_empty() { cx.editor.switch(id, Action::VerticalSplit); + } else { + for arg in args { + cx.editor + .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?; + } } Ok(()) @@ -2525,15 +2680,18 @@ mod cmd { fn hsplit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let id = view!(cx.editor).doc; - if let Some(path) = args.get(0) { - cx.editor.open(path.into(), Action::HorizontalSplit)?; - } else { + if args.is_empty() { cx.editor.switch(id, Action::HorizontalSplit); + } else { + for arg in args { + cx.editor + .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?; + } } Ok(()) @@ -2541,7 +2699,7 @@ mod cmd { fn debug_eval( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { if let Some(debugger) = cx.editor.debugger.as_mut() { @@ -2563,7 +2721,7 @@ mod cmd { fn debug_start( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let mut args = args.to_owned(); @@ -2571,12 +2729,12 @@ mod cmd { 0 => None, _ => Some(args.remove(0)), }; - dap_start_impl(cx, name, None, Some(args)) + dap_start_impl(cx, name.as_deref(), None, Some(args)) } fn debug_remote( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let mut args = args.to_owned(); @@ -2588,12 +2746,12 @@ mod cmd { 0 => None, _ => Some(args.remove(0)), }; - dap_start_impl(cx, name, address, Some(args)) + dap_start_impl(cx, name.as_deref(), address, Some(args)) } fn tutor( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { let path = helix_core::runtime_dir().join("tutor.txt"); @@ -2605,20 +2763,135 @@ mod cmd { pub(super) fn goto_line_number( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow<str>], _event: PromptEvent, ) -> anyhow::Result<()> { - if args.is_empty() { - bail!("Line number required"); - } + ensure!(!args.is_empty(), "Line number required"); let line = args[0].parse::<usize>()?; - goto_line_impl(&mut cx.editor, NonZeroUsize::new(line)); + goto_line_impl(cx.editor, NonZeroUsize::new(line)); let (view, doc) = current!(cx.editor); view.ensure_cursor_in_view(doc, line); + Ok(()) + } + + fn setting( + cx: &mut compositor::Context, + args: &[Cow<str>], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let runtime_config = &mut cx.editor.config; + + if args.len() != 2 { + anyhow::bail!("Bad arguments. Usage: `:set key field`"); + } + + let (key, arg) = (&args[0].to_lowercase(), &args[1]); + + match key.as_ref() { + "scrolloff" => runtime_config.scrolloff = arg.parse()?, + "scroll-lines" => runtime_config.scroll_lines = arg.parse()?, + "mouse" => runtime_config.mouse = arg.parse()?, + "line-number" => runtime_config.line_number = arg.parse()?, + "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?, + "auto-pairs" => runtime_config.auto_pairs = arg.parse()?, + "auto-completion" => runtime_config.auto_completion = arg.parse()?, + "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?, + "auto-info" => runtime_config.auto_info = arg.parse()?, + "true-color" => runtime_config.true_color = arg.parse()?, + "search.smart-case" => runtime_config.search.smart_case = arg.parse()?, + "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?, + _ => anyhow::bail!("Unknown key `{}`.", args[0]), + } + + Ok(()) + } + + fn sort( + cx: &mut compositor::Context, + args: &[Cow<str>], + _event: PromptEvent, + ) -> anyhow::Result<()> { + sort_impl(cx, args, false) + } + + fn sort_reverse( + cx: &mut compositor::Context, + args: &[Cow<str>], + _event: PromptEvent, + ) -> anyhow::Result<()> { + sort_impl(cx, args, true) + } + + fn sort_impl( + cx: &mut compositor::Context, + _args: &[Cow<str>], + reverse: bool, + ) -> anyhow::Result<()> { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id); + + let mut fragments: Vec<_> = selection + .fragments(text) + .map(|fragment| Tendril::from(fragment.as_ref())) + .collect(); + + fragments.sort_by(match reverse { + true => |a: &Tendril, b: &Tendril| b.cmp(a), + false => |a: &Tendril, b: &Tendril| a.cmp(b), + }); + + let transaction = Transaction::change( + doc.text(), + selection + .into_iter() + .zip(fragments) + .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))), + ); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + + Ok(()) + } + + fn tree_sitter_subtree( + cx: &mut compositor::Context, + _args: &[Cow<str>], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let (view, doc) = current!(cx.editor); + + if let Some(syntax) = doc.syntax() { + let primary_selection = doc.selection(view.id).primary(); + let text = doc.text(); + let from = text.char_to_byte(primary_selection.from()); + let to = text.char_to_byte(primary_selection.to()); + if let Some(selected_node) = syntax + .tree() + .root_node() + .descendant_for_byte_range(from, to) + { + let contents = format!("```tsq\n{}\n```", selected_node.to_sexp()); + + let callback = async move { + let call: job::Callback = + Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); + let popup = Popup::new("hover", contents); + compositor.replace_or_push("hover", Box::new(popup)); + }); + Ok(call) + }; + + cx.jobs.callback(callback); + } + } Ok(()) } @@ -2646,18 +2919,18 @@ mod cmd { completer: Some(completers::filename), }, TypableCommand { - name: "buffer-close", - aliases: &["bc", "bclose"], - doc: "Close the current buffer.", - fun: buffer_close, - completer: None, // FIXME: buffer completer + name: "buffer-close", + aliases: &["bc", "bclose"], + doc: "Close the current buffer.", + fun: buffer_close, + completer: None, // FIXME: buffer completer }, TypableCommand { - name: "buffer-close!", - aliases: &["bc!", "bclose!"], - doc: "Close the current buffer forcefully (ignoring unsaved changes).", - fun: force_buffer_close, - completer: None, // FIXME: buffer completer + name: "buffer-close!", + aliases: &["bc!", "bclose!"], + doc: "Close the current buffer forcefully (ignoring unsaved changes).", + fun: force_buffer_close, + completer: None, // FIXME: buffer completer }, TypableCommand { name: "write", @@ -2676,7 +2949,7 @@ mod cmd { TypableCommand { name: "format", aliases: &["fmt"], - doc: "Format the file using a formatter.", + doc: "Format the file using the LSP formatter.", fun: format, completer: None, }, @@ -2765,9 +3038,16 @@ mod cmd { completer: None, }, TypableCommand { + name: "cquit!", + aliases: &["cq!"], + doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).", + fun: force_cquit, + completer: None, + }, + TypableCommand { name: "theme", aliases: &[], - doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)", + doc: "Change the editor theme.", fun: theme, completer: Some(completers::theme), }, @@ -2851,7 +3131,7 @@ mod cmd { TypableCommand { name: "change-current-directory", aliases: &["cd"], - doc: "Change the current working directory (:cd <dir>).", + doc: "Change the current working directory.", fun: change_current_directory, completer: Some(completers::directory), }, @@ -2931,18 +3211,47 @@ mod cmd { doc: "Go to line number.", fun: goto_line_number, completer: None, - } + }, + TypableCommand { + name: "set-option", + aliases: &["set"], + doc: "Set a config option at runtime", + fun: setting, + completer: Some(completers::setting), + }, + TypableCommand { + name: "sort", + aliases: &[], + doc: "Sort ranges in selection.", + fun: sort, + completer: None, + }, + TypableCommand { + name: "rsort", + aliases: &[], + doc: "Sort ranges in selection in reverse order.", + fun: sort_reverse, + completer: None, + }, + TypableCommand { + name: "tree-sitter-subtree", + aliases: &["ts-subtree"], + doc: "Display tree sitter subtree under cursor, primarily for debugging queries.", + fun: tree_sitter_subtree, + completer: None, + }, ]; - pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| { - TYPABLE_COMMAND_LIST - .iter() - .flat_map(|cmd| { - std::iter::once((cmd.name, cmd)) - .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd))) - }) - .collect() - }); + pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> = + Lazy::new(|| { + TYPABLE_COMMAND_LIST + .iter() + .flat_map(|cmd| { + std::iter::once((cmd.name, cmd)) + .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd))) + }) + .collect() + }); } fn command_mode(cx: &mut Context) { @@ -2950,17 +3259,28 @@ fn command_mode(cx: &mut Context) { ":".into(), Some(':'), |input: &str| { + static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> = + Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); + // we use .this over split_whitespace() because we care about empty segments let parts = input.split(' ').collect::<Vec<&str>>(); // simple heuristic: if there's no just one part, complete command name. // if there's a space, per command completion kicks in. if parts.len() <= 1 { - let end = 0..; - cmd::TYPABLE_COMMAND_LIST + let mut matches: Vec<_> = cmd::TYPABLE_COMMAND_LIST .iter() - .filter(|command| command.name.contains(input)) - .map(|command| (end.clone(), Cow::Borrowed(command.name))) + .filter_map(|command| { + FUZZY_MATCHER + .fuzzy_match(command.name, input) + .map(|score| (command.name, score)) + }) + .collect(); + + matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score)); + matches + .into_iter() + .map(|(name, _)| (0.., name.into())) .collect() } else { let part = parts.last().unwrap(); @@ -2968,7 +3288,7 @@ fn command_mode(cx: &mut Context) { if let Some(cmd::TypableCommand { completer: Some(completer), .. - }) = cmd::COMMANDS.get(parts[0]) + }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { completer(part) .into_iter() @@ -2996,15 +3316,25 @@ fn command_mode(cx: &mut Context) { // If command is numeric, interpret as line number and go there. if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() { - if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) { + if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) { cx.editor.set_error(format!("{}", e)); } return; } // Handle typable commands - if let Some(cmd) = cmd::COMMANDS.get(parts[0]) { - if let Err(e) = (cmd.fun)(cx, &parts[1..], event) { + if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { + let args = if cfg!(unix) { + shellwords::shellwords(input) + } else { + // Windows doesn't support POSIX, so fallback for now + parts + .into_iter() + .map(|part| part.into()) + .collect::<Vec<_>>() + }; + + if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); } } else { @@ -3016,7 +3346,7 @@ fn command_mode(cx: &mut Context) { prompt.doc_fn = Box::new(|input: &str| { let part = input.split(' ').next().unwrap_or_default(); - if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) { + if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) { return Some(doc); } @@ -3027,7 +3357,8 @@ fn command_mode(cx: &mut Context) { } fn file_picker(cx: &mut Context) { - let root = find_root(None).unwrap_or_else(|| PathBuf::from("./")); + // We don't specify language markers, root will be the root of the current git repo + let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./")); let picker = ui::file_picker(root, &cx.editor.config); cx.push_layer(Box::new(picker)); } @@ -3084,8 +3415,8 @@ fn buffer_picker(cx: &mut Context) { .map(|(_, doc)| new_meta(doc)) .collect(), BufferMeta::format, - |cx, meta, _action| { - cx.editor.switch(meta.id, Action::Replace); + |cx, meta, action| { + cx.editor.switch(meta.id, action); }, |editor, meta| { let doc = &editor.documents.get(&meta.id)?; @@ -3119,7 +3450,7 @@ fn symbol_picker(cx: &mut Context) { nested_to_flat(list, file, child); } } - let (_, doc) = current!(cx.editor); + let doc = doc!(cx.editor); let language_server = match doc.language_server() { Some(language_server) => language_server, @@ -3140,7 +3471,7 @@ fn symbol_picker(cx: &mut Context) { let symbols = match symbols { lsp::DocumentSymbolResponse::Flat(symbols) => symbols, lsp::DocumentSymbolResponse::Nested(symbols) => { - let (_view, doc) = current!(editor); + let doc = doc!(editor); let mut flat_symbols = Vec::new(); for symbol in symbols { nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol) @@ -3182,17 +3513,15 @@ fn symbol_picker(cx: &mut Context) { } fn workspace_symbol_picker(cx: &mut Context) { - let (_, doc) = current!(cx.editor); - + let doc = doc!(cx.editor); + let current_path = doc.path().cloned(); let language_server = match doc.language_server() { Some(language_server) => language_server, None => return, }; let offset_encoding = language_server.offset_encoding(); - let future = language_server.workspace_symbols("".to_string()); - let current_path = doc_mut!(cx.editor).path().cloned(); cx.callback( future, move |_editor: &mut Editor, @@ -3243,6 +3572,15 @@ fn workspace_symbol_picker(cx: &mut Context) { ) } +impl ui::menu::Item for lsp::CodeActionOrCommand { + fn label(&self) -> &str { + match self { + lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(), + lsp::CodeActionOrCommand::Command(command) => command.title.as_str(), + } + } +} + pub fn code_action(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -3262,49 +3600,85 @@ pub fn code_action(cx: &mut Context) { cx.callback( future, - move |_editor: &mut Editor, + move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::CodeActionResponse>| { - if let Some(actions) = response { - let picker = Picker::new( - true, - actions, - |action| match action { - lsp::CodeActionOrCommand::CodeAction(action) => { - action.title.as_str().into() - } - lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), - }, - move |cx, code_action, _action| match code_action { - lsp::CodeActionOrCommand::Command(command) => { - log::debug!("code action command: {:?}", command); - cx.editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + let actions = match response { + Some(a) => a, + None => return, + }; + if actions.is_empty() { + editor.set_status("No code actions available".to_owned()); + return; + } + + let mut picker = ui::Menu::new(actions, move |editor, code_action, event| { + if event != PromptEvent::Validate { + return; + } + + // always present here + let code_action = code_action.unwrap(); + + match code_action { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); + execute_lsp_command(editor, command.clone()); + } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + if let Some(ref workspace_edit) = code_action.edit { + log::debug!("edit: {:?}", workspace_edit); + apply_workspace_edit(editor, offset_encoding, workspace_edit); } - lsp::CodeActionOrCommand::CodeAction(code_action) => { - log::debug!("code action: {:?}", code_action); - if let Some(ref workspace_edit) = code_action.edit { - apply_workspace_edit(cx.editor, offset_encoding, workspace_edit) - } + + // if code action provides both edit and command first the edit + // should be applied and then the command + if let Some(command) = &code_action.command { + execute_lsp_command(editor, command.clone()); } - }, - ); - compositor.push(Box::new(picker)) - } + } + } + }); + picker.move_down(); // pre-select the first item + + let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin { + vertical: 1, + horizontal: 1, + }); + compositor.replace_or_push("code-action", Box::new(popup)); }, ) } +pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) { + let doc = doc!(editor); + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + // the command is executed on the server and communicated back + // to the client asynchronously using workspace edits + let command_future = language_server.command(cmd); + tokio::spawn(async move { + let res = command_future.await; + + if let Err(e) = res { + log::error!("execute LSP command: {}", e); + } + }); +} + pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { use lsp::ResourceOp; use std::fs; match op { ResourceOp::Create(op) => { let path = op.uri.to_file_path().unwrap(); - let ignore_if_exists = if let Some(options) = &op.options { + let ignore_if_exists = op.options.as_ref().map_or(false, |options| { !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) - } else { - false - }; + }); if ignore_if_exists && path.exists() { Ok(()) } else { @@ -3314,11 +3688,12 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { ResourceOp::Delete(op) => { let path = op.uri.to_file_path().unwrap(); if path.is_dir() { - let recursive = if let Some(options) = &op.options { - options.recursive.unwrap_or(false) - } else { - false - }; + let recursive = op + .options + .as_ref() + .and_then(|options| options.recursive) + .unwrap_or(false); + if recursive { fs::remove_dir_all(&path) } else { @@ -3333,11 +3708,9 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { ResourceOp::Rename(op) => { let from = op.old_uri.to_file_path().unwrap(); let to = op.new_uri.to_file_path().unwrap(); - let ignore_if_exists = if let Some(options) = &op.options { + let ignore_if_exists = op.options.as_ref().map_or(false, |options| { !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) - } else { - false - }; + }); if ignore_if_exists && to.exists() { Ok(()) } else { @@ -3347,7 +3720,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { } } -fn apply_workspace_edit( +pub fn apply_workspace_edit( editor: &mut Editor, offset_encoding: OffsetEncoding, workspace_edit: &lsp::WorkspaceEdit, @@ -3454,7 +3827,7 @@ fn apply_workspace_edit( fn last_picker(cx: &mut Context) { // TODO: last picker does not seem to work well with buffer_picker - cx.callback = Some(Box::new(|compositor: &mut Compositor| { + cx.callback = Some(Box::new(|compositor: &mut Compositor, _| { if let Some(picker) = compositor.last_picker.take() { compositor.push(picker); } @@ -3538,22 +3911,22 @@ fn open(cx: &mut Context, open: Open) { let mut offs = 0; let mut transaction = Transaction::change_by_selection(contents, selection, |range| { - let line = range.cursor_line(text); + let cursor_line = range.cursor_line(text); - let line = match open { + let new_line = match open { // adjust position to the end of the line (next line - 1) - Open::Below => line + 1, + Open::Below => cursor_line + 1, // adjust position to the end of the previous line (current line - 1) - Open::Above => line, + Open::Above => cursor_line, }; // Index to insert newlines after, as well as the char width // to use to compensate for those inserted newlines. - let (line_end_index, line_end_offset_width) = if line == 0 { + let (line_end_index, line_end_offset_width) = if new_line == 0 { (0, 0) } else { ( - line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)), + line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)), doc.line_ending.len_chars(), ) }; @@ -3564,8 +3937,10 @@ fn open(cx: &mut Context, open: Open) { doc.syntax(), text, line_end_index, + new_line.saturating_sub(1), true, - ); + ) + .unwrap_or_else(|| indent::indent_level_for_line(text.line(cursor_line), doc.tab_width())); let indent = doc.indent_unit().repeat(indent_level); let indent_len = indent.len(); let mut text = String::with_capacity(1 + indent_len); @@ -3611,7 +3986,7 @@ fn normal_mode(cx: &mut Context) { doc.mode = Mode::Normal; - doc.append_changes_to_history(view.id); + try_restore_indent(doc, view.id); // if leaving append mode, move cursor back by 1 if doc.restore_cursor { @@ -3628,6 +4003,40 @@ fn normal_mode(cx: &mut Context) { } } +fn try_restore_indent(doc: &mut Document, view_id: ViewId) { + use helix_core::chars::char_is_whitespace; + use helix_core::Operation; + + fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool { + if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] = + changes + { + move_pos + inserted_str.len() == pos + && inserted_str.starts_with('\n') + && inserted_str.chars().skip(1).all(char_is_whitespace) + && pos == line_end_pos // ensure no characters exists after current position + } else { + false + } + } + + let doc_changes = doc.changes().changes(); + let text = doc.text().slice(..); + let range = doc.selection(view_id).primary(); + let pos = range.cursor(text); + let line_end_pos = line_end_char_index(&text, range.cursor_line(text)); + + if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) { + // Removes tailing whitespaces. + let transaction = + Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| { + let line_start_pos = text.line_to_char(range.cursor_line(text)); + (line_start_pos, pos, None) + }); + doc.apply(&transaction, view_id); + } +} + // Store a jump on the jumplist. fn push_jump(editor: &mut Editor) { let (view, doc) = current!(editor); @@ -3636,7 +4045,7 @@ fn push_jump(editor: &mut Editor) { } fn goto_line(cx: &mut Context) { - goto_line_impl(&mut cx.editor, cx.count) + goto_line_impl(cx.editor, cx.count) } fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) { @@ -3702,6 +4111,20 @@ fn goto_last_modification(cx: &mut Context) { } } +fn goto_last_modified_file(cx: &mut Context) { + let view = view!(cx.editor); + let alternate_file = view + .last_modified_docs + .into_iter() + .flatten() + .find(|&id| id != view.doc); + if let Some(alt) = alternate_file { + cx.editor.switch(alt, Action::Replace); + } else { + cx.editor.set_error("no last modified buffer".to_owned()) + } +} + fn select_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -3979,27 +4402,21 @@ fn goto_pos(editor: &mut Editor, pos: usize) { } fn goto_first_diag(cx: &mut Context) { - let editor = &mut cx.editor; - let (_, doc) = current!(editor); - + let doc = doc!(cx.editor); let pos = match doc.diagnostics().first() { Some(diag) => diag.range.start, None => return, }; - - goto_pos(editor, pos); + goto_pos(cx.editor, pos); } fn goto_last_diag(cx: &mut Context) { - let editor = &mut cx.editor; - let (_, doc) = current!(editor); - + let doc = doc!(cx.editor); let pos = match doc.diagnostics().last() { Some(diag) => diag.range.start, None => return, }; - - goto_pos(editor, pos); + goto_pos(cx.editor, pos); } fn goto_next_diag(cx: &mut Context) { @@ -4089,7 +4506,6 @@ fn signature_help(cx: &mut Context) { ); } -// NOTE: Transactions in this module get appended to history when we switch back to normal mode. pub mod insert { use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>; @@ -4184,8 +4600,10 @@ pub mod insert { // The default insert hook: simply insert the character #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> { - let t = Tendril::from_char(ch); - let transaction = Transaction::insert(doc, selection, t); + let cursors = selection.clone().cursors(doc.slice(..)); + let mut t = Tendril::new(); + t.push(ch); + let transaction = Transaction::insert(doc, &cursors, t); Some(transaction) } @@ -4200,11 +4618,11 @@ pub mod insert { }; let text = doc.text(); - let selection = doc.selection(view.id).clone().cursors(text.slice(..)); + let selection = doc.selection(view.id); // run through insert hooks, stopping on the first one that returns Some(t) for hook in hooks { - if let Some(transaction) = hook(text, &selection, c) { + if let Some(transaction) = hook(text, selection, c) { doc.apply(&transaction, view.id); break; } @@ -4254,48 +4672,48 @@ pub mod insert { }; let curr = contents.get_char(pos).unwrap_or(' '); - // TODO: offset range.head by 1? when calculating? + let current_line = text.char_to_line(pos); let indent_level = indent::suggested_indent_for_pos( doc.language_config(), doc.syntax(), text, - pos.saturating_sub(1), + pos, + current_line, true, - ); - let indent = doc.indent_unit().repeat(indent_level); - let mut text = String::with_capacity(1 + indent.len()); - text.push_str(doc.line_ending.as_str()); - text.push_str(&indent); + ) + .unwrap_or_else(|| { + indent::indent_level_for_line(text.line(current_line), doc.tab_width()) + }); - let head = pos + offs + text.chars().count(); + let indent = doc.indent_unit().repeat(indent_level); + let mut text = String::new(); + // If we are between pairs (such as brackets), we want to insert an additional line which is indented one level more and place the cursor there + let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) { + let inner_indent = doc.indent_unit().repeat(indent_level + 1); + text.reserve_exact(2 + indent.len() + inner_indent.len()); + text.push_str(doc.line_ending.as_str()); + text.push_str(&inner_indent); + let new_head_pos = pos + offs + text.chars().count(); + text.push_str(doc.line_ending.as_str()); + text.push_str(&indent); + new_head_pos + } else { + text.reserve_exact(1 + indent.len()); + text.push_str(doc.line_ending.as_str()); + text.push_str(&indent); + pos + offs + text.chars().count() + }; // TODO: range replace or extend // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos // can be used with cx.mode to do replace or extend on most changes - ranges.push(Range::new( - if range.is_empty() { - head - } else { - range.anchor + offs - }, - head, - )); - - // if between a bracket pair - if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) { - // another newline, indent the end bracket one level less - let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1)); - text.push_str(doc.line_ending.as_str()); - text.push_str(&indent); - } - + ranges.push(Range::new(new_head_pos, new_head_pos)); offs += text.chars().count(); (pos, pos, Some(text.into())) }); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); - // doc.apply(&transaction, view.id); } @@ -4519,11 +4937,8 @@ fn yank_joined_to_clipboard_impl( fn yank_joined_to_clipboard(cx: &mut Context) { let line_ending = doc!(cx.editor).line_ending; - let _ = yank_joined_to_clipboard_impl( - &mut cx.editor, - line_ending.as_str(), - ClipboardType::Clipboard, - ); + let _ = + yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard); exit_select_mode(cx); } @@ -4548,20 +4963,17 @@ fn yank_main_selection_to_clipboard_impl( } fn yank_main_selection_to_clipboard(cx: &mut Context) { - let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard); + let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard); } fn yank_joined_to_primary_clipboard(cx: &mut Context) { let line_ending = doc!(cx.editor).line_ending; - let _ = yank_joined_to_clipboard_impl( - &mut cx.editor, - line_ending.as_str(), - ClipboardType::Selection, - ); + let _ = + yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection); } fn yank_main_selection_to_primary_clipboard(cx: &mut Context) { - let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection); + let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection); exit_select_mode(cx); } @@ -4576,11 +4988,12 @@ fn paste_impl( doc: &mut Document, view: &View, action: Paste, + count: usize, ) -> Option<Transaction> { let repeat = std::iter::repeat( values .last() - .map(|value| Tendril::from_slice(value)) + .map(|value| Tendril::from(value.repeat(count))) .unwrap(), ); @@ -4595,7 +5008,7 @@ fn paste_impl( let mut values = values .iter() .map(|value| REGEX.replace_all(value, doc.line_ending.as_str())) - .map(|value| Tendril::from(value.as_ref())) + .map(|value| Tendril::from(value.as_ref().repeat(count))) .chain(repeat); let text = doc.text(); @@ -4615,7 +5028,7 @@ fn paste_impl( // paste append (Paste::After, false) => range.to(), }; - (pos, pos, Some(values.next().unwrap())) + (pos, pos, values.next()) }); Some(transaction) @@ -4625,13 +5038,14 @@ fn paste_clipboard_impl( editor: &mut Editor, action: Paste, clipboard_type: ClipboardType, + count: usize, ) -> anyhow::Result<()> { let (view, doc) = current!(editor); match editor .clipboard_provider .get_contents(clipboard_type) - .map(|contents| paste_impl(&[contents], doc, view, action)) + .map(|contents| paste_impl(&[contents], doc, view, action, count)) { Ok(Some(transaction)) => { doc.apply(&transaction, view.id); @@ -4644,22 +5058,43 @@ fn paste_clipboard_impl( } fn paste_clipboard_after(cx: &mut Context) { - let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard); + let _ = paste_clipboard_impl( + cx.editor, + Paste::After, + ClipboardType::Clipboard, + cx.count(), + ); } fn paste_clipboard_before(cx: &mut Context) { - let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard); + let _ = paste_clipboard_impl( + cx.editor, + Paste::Before, + ClipboardType::Clipboard, + cx.count(), + ); } fn paste_primary_clipboard_after(cx: &mut Context) { - let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection); + let _ = paste_clipboard_impl( + cx.editor, + Paste::After, + ClipboardType::Selection, + cx.count(), + ); } fn paste_primary_clipboard_before(cx: &mut Context) { - let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection); + let _ = paste_clipboard_impl( + cx.editor, + Paste::Before, + ClipboardType::Selection, + cx.count(), + ); } fn replace_with_yanked(cx: &mut Context) { + let count = cx.count(); let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -4669,12 +5104,12 @@ fn replace_with_yanked(cx: &mut Context) { let repeat = std::iter::repeat( values .last() - .map(|value| Tendril::from_slice(value)) + .map(|value| Tendril::from(&value.repeat(count))) .unwrap(), ); let mut values = values .iter() - .map(|value| Tendril::from_slice(value)) + .map(|value| Tendril::from(&value.repeat(count))) .chain(repeat); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { @@ -4686,7 +5121,6 @@ fn replace_with_yanked(cx: &mut Context) { }); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } } } @@ -4694,6 +5128,7 @@ fn replace_with_yanked(cx: &mut Context) { fn replace_selections_with_clipboard_impl( editor: &mut Editor, clipboard_type: ClipboardType, + count: usize, ) -> anyhow::Result<()> { let (view, doc) = current!(editor); @@ -4701,7 +5136,11 @@ fn replace_selections_with_clipboard_impl( Ok(contents) => { let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - (range.from(), range.to(), Some(contents.as_str().into())) + ( + range.from(), + range.to(), + Some(contents.repeat(count).as_str().into()), + ) }); doc.apply(&transaction, view.id); @@ -4713,38 +5152,38 @@ fn replace_selections_with_clipboard_impl( } fn replace_selections_with_clipboard(cx: &mut Context) { - let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard); + let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count()); } fn replace_selections_with_primary_clipboard(cx: &mut Context) { - let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection); + let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count()); } fn paste_after(cx: &mut Context) { + let count = cx.count(); let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; if let Some(transaction) = registers .read(reg_name) - .and_then(|values| paste_impl(values, doc, view, Paste::After)) + .and_then(|values| paste_impl(values, doc, view, Paste::After, count)) { doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } } fn paste_before(cx: &mut Context) { + let count = cx.count(); let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; if let Some(transaction) = registers .read(reg_name) - .and_then(|values| paste_impl(values, doc, view, Paste::Before)) + .and_then(|values| paste_impl(values, doc, view, Paste::Before, count)) { doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } } @@ -4780,7 +5219,6 @@ fn indent(cx: &mut Context) { }), ); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn unindent(cx: &mut Context) { @@ -4820,7 +5258,6 @@ fn unindent(cx: &mut Context) { let transaction = Transaction::change(doc.text(), changes.into_iter()); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn format_selections(cx: &mut Context) { @@ -4867,8 +5304,6 @@ fn format_selections(cx: &mut Context) { // doc.apply(&transaction, view.id); } - - doc.append_changes_to_history(view.id); } fn join_selections(cx: &mut Context) { @@ -4911,7 +5346,6 @@ fn join_selections(cx: &mut Context) { // .with_selection(selection); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { @@ -5039,7 +5473,7 @@ pub fn completion(cx: &mut Context) { move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::CompletionResponse>| { - let (_, doc) = current!(editor); + let doc = doc!(editor); if doc.mode() != Mode::Insert { // we're not in insert mode anymore return; @@ -5136,9 +5570,10 @@ fn hover(cx: &mut Context) { // skip if contents empty - let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); - let popup = Popup::new(contents); - compositor.push(Box::new(popup)); + let contents = + ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover"); + let popup = Popup::new("hover", contents); + compositor.replace_or_push("hover", Box::new(popup)); } }, ); @@ -5154,7 +5589,6 @@ fn toggle_comments(cx: &mut Context) { let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); exit_select_mode(cx); } @@ -5185,7 +5619,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { let selection = doc.selection(view.id); let mut fragments: Vec<_> = selection .fragments(text) - .map(|fragment| Tendril::from_slice(&fragment)) + .map(|fragment| Tendril::from(fragment.as_ref())) .collect(); let group = count @@ -5211,8 +5645,8 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { ); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } + fn rotate_selection_contents_forward(cx: &mut Context) { rotate_selection_contents(cx, Direction::Forward) } @@ -5228,14 +5662,73 @@ fn expand_selection(cx: &mut Context) { if let Some(syntax) = doc.syntax() { let text = doc.text().slice(..); - let selection = object::expand_selection(syntax, text, doc.selection(view.id)); + + let current_selection = doc.selection(view.id); + + // save current selection so it can be restored using shrink_selection + view.object_selections.push(current_selection.clone()); + + let selection = object::expand_selection(syntax, text, current_selection.clone()); + doc.set_selection(view.id, selection); + } + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + +fn shrink_selection(cx: &mut Context) { + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); + let current_selection = doc.selection(view.id); + // try to restore previous selection + if let Some(prev_selection) = view.object_selections.pop() { + if current_selection.contains(&prev_selection) { + // allow shrinking the selection only if current selection contains the previous object selection + doc.set_selection(view.id, prev_selection); + return; + } else { + // clear existing selection as they can't be shrinked to anyway + view.object_selections.clear(); + } + } + // if not previous selection, shrink to first child + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let selection = object::shrink_selection(syntax, text, current_selection.clone()); doc.set_selection(view.id, selection); } }; - motion(&mut cx.editor); + motion(cx.editor); cx.editor.last_motion = Some(Motion(Box::new(motion))); } +fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F) +where + F: Fn(Node) -> Option<Node>, +{ + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); + + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let current_selection = doc.selection(view.id); + let selection = + object::select_sibling(syntax, text, current_selection.clone(), sibling_fn); + doc.set_selection(view.id, selection); + } + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + +fn select_next_sibling(cx: &mut Context) { + select_sibling_impl(cx, &|node| Node::next_sibling(&node)) +} + +fn select_prev_sibling(cx: &mut Context) { + select_sibling_impl(cx, &|node| Node::prev_sibling(&node)) +} + fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -5288,6 +5781,12 @@ fn jump_backward(cx: &mut Context) { }; } +fn save_selection(cx: &mut Context) { + push_jump(cx.editor); + cx.editor + .set_status("Selection saved to jumplist".to_owned()); +} + fn rotate_view(cx: &mut Context) { cx.editor.focus_next() } @@ -5358,8 +5857,10 @@ fn wonly(cx: &mut Context) { } fn select_register(cx: &mut Context) { + cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers)); cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { + cx.editor.autoinfo = None; cx.editor.selected_register = Some(ch); } }) @@ -5464,7 +5965,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { }); doc.set_selection(view.id, selection); }; - textobject(&mut cx.editor); + textobject(cx.editor); cx.editor.last_motion = Some(Motion(Box::new(textobject))); } }) @@ -5479,13 +5980,16 @@ fn surround_add(cx: &mut Context) { let mut changes = Vec::with_capacity(selection.len() * 2); for range in selection.iter() { - changes.push((range.from(), range.from(), Some(Tendril::from_char(open)))); - changes.push((range.to(), range.to(), Some(Tendril::from_char(close)))); + let mut o = Tendril::new(); + o.push(open); + let mut c = Tendril::new(); + c.push(close); + changes.push((range.from(), range.from(), Some(o))); + changes.push((range.to(), range.to(), Some(c))); } let transaction = Transaction::change(doc.text(), changes.into_iter()); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } }) } @@ -5510,15 +6014,12 @@ fn surround_replace(cx: &mut Context) { let transaction = Transaction::change( doc.text(), change_pos.iter().enumerate().map(|(i, &pos)| { - ( - pos, - pos + 1, - Some(Tendril::from_char(if i % 2 == 0 { open } else { close })), - ) + let mut t = Tendril::new(); + t.push(if i % 2 == 0 { open } else { close }); + (pos, pos + 1, Some(t)) }), ); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } }); } @@ -5541,7 +6042,6 @@ fn surround_delete(cx: &mut Context) { let transaction = Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None))); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } }) } @@ -5630,9 +6130,7 @@ fn shell_impl( ) -> anyhow::Result<(Tendril, bool)> { use std::io::Write; use std::process::{Command, Stdio}; - if shell.is_empty() { - bail!("No shell set"); - } + ensure!(!shell.is_empty(), "No shell set"); let mut process = match Command::new(&shell[0]) .args(&shell[1..]) @@ -5658,8 +6156,9 @@ fn shell_impl( log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr)); } - let tendril = Tendril::try_from_byte_slice(&output.stdout) + let str = std::str::from_utf8(&output.stdout) .map_err(|_| anyhow!("Process did not output valid UTF-8"))?; + let tendril = Tendril::from(str); Ok((tendril, output.status.success())) } @@ -5714,7 +6213,6 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { if behavior != ShellBehavior::Ignore { let transaction = Transaction::change(doc.text(), changes.into_iter()); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } // after replace cursor may be out of bounds, do this to @@ -5762,7 +6260,6 @@ fn add_newline_impl(cx: &mut Context, open: Open) { let transaction = Transaction::change(text, changes); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn rename_symbol(cx: &mut Context) { @@ -5796,7 +6293,7 @@ fn rename_symbol(cx: &mut Context) { let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string()); let edits = block_on(task).unwrap_or_default(); log::debug!("Edits from LSP: {:?}", edits); - apply_workspace_edit(&mut cx.editor, offset_encoding, &edits); + apply_workspace_edit(cx.editor, offset_encoding, &edits); }, ); cx.push_layer(Box::new(prompt)); @@ -5816,16 +6313,45 @@ fn decrement(cx: &mut Context) { fn increment_impl(cx: &mut Context, amount: i64) { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); - let text = doc.text(); + let text = doc.text().slice(..); + + let changes: Vec<_> = selection + .ranges() + .iter() + .filter_map(|range| { + let incrementor: Box<dyn Increment> = + if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) { + Box::new(incrementor) + } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) { + Box::new(incrementor) + } else { + return None; + }; - let changes = selection.ranges().iter().filter_map(|range| { - let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?; - let new_text = incrementor.incremented_text(amount); - Some(( - incrementor.range.from(), - incrementor.range.to(), - Some(new_text), - )) + let (range, new_text) = incrementor.increment(amount); + + Some((range.from(), range.to(), Some(new_text))) + }) + .collect(); + + // Overlapping changes in a transaction will panic, so we need to find and remove them. + // For example, if there are cursors on each of the year, month, and day of `2021-11-29`, + // incrementing will give overlapping changes, with each change incrementing a different part of + // the date. Since these conflict with each other we remove these changes from the transaction + // so nothing happens. + let mut overlapping_indexes = HashSet::new(); + for (i, changes) in changes.windows(2).enumerate() { + if changes[0].1 > changes[1].0 { + overlapping_indexes.insert(i); + overlapping_indexes.insert(i + 1); + } + } + let changes = changes.into_iter().enumerate().filter_map(|(i, change)| { + if overlapping_indexes.contains(&i) { + None + } else { + Some(change) + } }); if changes.clone().count() > 0 { @@ -5833,6 +6359,58 @@ fn increment_impl(cx: &mut Context, amount: i64) { let transaction = transaction.with_selection(selection.clone()); doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } } + +fn record_macro(cx: &mut Context) { + if let Some((reg, mut keys)) = cx.editor.macro_recording.take() { + // Remove the keypress which ends the recording + keys.pop(); + let s = keys + .into_iter() + .map(|key| { + let s = key.to_string(); + if s.chars().count() == 1 { + s + } else { + format!("<{}>", s) + } + }) + .collect::<String>(); + cx.editor.registers.get_mut(reg).write(vec![s]); + cx.editor + .set_status(format!("Recorded to register [{}]", reg)); + } else { + let reg = cx.register.take().unwrap_or('@'); + cx.editor.macro_recording = Some((reg, Vec::new())); + cx.editor + .set_status(format!("Recording to register [{}]", reg)); + } +} + +fn replay_macro(cx: &mut Context) { + let reg = cx.register.unwrap_or('@'); + let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) { + match helix_view::input::parse_macro(keys_str) { + Ok(keys) => keys, + Err(err) => { + cx.editor.set_error(format!("Invalid macro: {}", err)); + return; + } + } + } else { + cx.editor.set_error(format!("Register [{}] empty", reg)); + return; + }; + + let count = cx.count(); + cx.callback = Some(Box::new( + move |compositor: &mut Compositor, cx: &mut compositor::Context| { + for _ in 0..count { + for &key in keys.iter() { + compositor.handle_event(crossterm::event::Event::Key(key.into()), cx); + } + } + }, + )); +} diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 58ef99f5..c73f9611 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -194,7 +194,7 @@ pub fn dap_start_impl( cx: &mut compositor::Context, name: Option<&str>, socket: Option<std::net::SocketAddr>, - params: Option<Vec<&str>>, + params: Option<Vec<std::borrow::Cow<str>>>, ) -> Result<(), anyhow::Error> { let doc = doc!(cx.editor); @@ -242,7 +242,7 @@ pub fn dap_start_impl( let mut param = x.to_string(); if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) { if matches!(cfg.completion.as_deref(), Some("filename" | "directory")) { - param = std::fs::canonicalize(x) + param = std::fs::canonicalize(x.as_ref()) .ok() .and_then(|pb| pb.into_os_string().into_string().ok()) .unwrap_or_else(|| x.to_string()); @@ -408,7 +408,7 @@ fn debug_parameter_prompt( cx, Some(&config_name), None, - Some(params.iter().map(|x| x.as_str()).collect()), + Some(params.iter().map(|x| x.into()).collect()), ) { cx.editor.set_error(e.to_string()); } @@ -651,7 +651,7 @@ pub fn dap_variables(cx: &mut Context) { } let contents = Text::from(tui::text::Text::from(variables)); - let popup = Popup::new(contents); + let popup = Popup::new("dap-variables", contents); cx.push_layer(Box::new(popup)); } diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 3a644750..dd7ebe1d 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect}; use crossterm::event::Event; use tui::buffer::Buffer as Surface; -pub type Callback = Box<dyn FnOnce(&mut Compositor)>; +pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>; // --> EventResult should have a callback that takes a context with methods like .popup(), // .prompt() etc. That way we can abstract it from the renderer. @@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent { /// May be used by the parent component to compute the child area. /// viewport is the maximum allowed area, and the child should stay within those bounds. + /// + /// The returned size might be larger than the viewport if the child is too big to fit. + /// In this case the parent can use the values to calculate scroll. fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { - // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context - // that way render can use it None } fn type_name(&self) -> &'static str { std::any::type_name::<Self>() } + + fn id(&self) -> Option<&'static str> { + None + } } use anyhow::Error; @@ -121,17 +126,32 @@ impl Compositor { self.layers.push(layer); } + /// Replace a component that has the given `id` with the new layer and if + /// no component is found, push the layer normally. + pub fn replace_or_push(&mut self, id: &'static str, layer: Box<dyn Component>) { + if let Some(component) = self.find_id(id) { + *component = layer; + } else { + self.push(layer) + } + } + pub fn pop(&mut self) -> Option<Box<dyn Component>> { self.layers.pop() } pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { + // If it is a key event and a macro is being recorded, push the key event to the recording. + if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) { + keys.push(key.into()); + } + // propagate events through the layers until we either find a layer that consumes it or we // run out of layers (event bubbling) for layer in self.layers.iter_mut().rev() { match layer.handle_event(event, cx) { EventResult::Consumed(Some(callback)) => { - callback(self); + callback(self, cx); return true; } EventResult::Consumed(None) => return true, @@ -184,6 +204,14 @@ impl Compositor { .find(|component| component.type_name() == type_name) .and_then(|component| component.as_any_mut().downcast_mut()) } + + pub fn find_id<T: 'static>(&mut self, id: &'static str) -> Option<&mut T> { + let type_name = std::any::type_name::<T>(); + self.layers + .iter_mut() + .find(|component| component.type_name() == type_name && component.id() == Some(id)) + .and_then(|component| component.as_any_mut().downcast_mut()) + } } // View casting, taken straight from Cursive diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 3745f871..6b8bbc1b 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -20,14 +20,18 @@ pub struct LspConfig { pub display_messages: bool, } -#[test] -fn parsing_keymaps_config_file() { - use crate::keymap; - use crate::keymap::Keymap; - use helix_core::hashmap; - use helix_view::document::Mode; - - let sample_keymaps = r#" +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parsing_keymaps_config_file() { + use crate::keymap; + use crate::keymap::Keymap; + use helix_core::hashmap; + use helix_view::document::Mode; + + let sample_keymaps = r#" [keys.insert] y = "move_line_down" S-C-a = "delete_selection" @@ -36,19 +40,20 @@ fn parsing_keymaps_config_file() { A-F12 = "move_next_word_end" "#; - assert_eq!( - toml::from_str::<Config>(sample_keymaps).unwrap(), - Config { - keys: Keymaps(hashmap! { - Mode::Insert => Keymap::new(keymap!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - })), - Mode::Normal => Keymap::new(keymap!({ "Normal mode" - "A-F12" => move_next_word_end, - })), - }), - ..Default::default() - } - ); + assert_eq!( + toml::from_str::<Config>(sample_keymaps).unwrap(), + Config { + keys: Keymaps(hashmap! { + Mode::Insert => Keymap::new(keymap!({ "Insert mode" + "y" => move_line_down, + "S-C-a" => delete_selection, + })), + Mode::Normal => Keymap::new(keymap!({ "Normal mode" + "A-F12" => move_next_word_end, + })), + }), + ..Default::default() + } + ); + } } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 4fa38174..a6a77021 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -22,8 +22,8 @@ pub struct Jobs { } impl Job { - pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Job { - Job { + pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Self { + Self { future: f.map(|r| r.map(|()| None)).boxed(), wait: false, } @@ -31,22 +31,22 @@ impl Job { pub fn with_callback<F: Future<Output = anyhow::Result<Callback>> + Send + 'static>( f: F, - ) -> Job { - Job { + ) -> Self { + Self { future: f.map(|r| r.map(Some)).boxed(), wait: false, } } - pub fn wait_before_exiting(mut self) -> Job { + pub fn wait_before_exiting(mut self) -> Self { self.wait = true; self } } impl Jobs { - pub fn new() -> Jobs { - Jobs::default() + pub fn new() -> Self { + Self::default() } pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) { @@ -93,8 +93,8 @@ impl Jobs { } /// Blocks until all the jobs that need to be waited on are done. - pub fn finish(&mut self) { + pub async fn finish(&mut self) { let wait_futures = std::mem::take(&mut self.wait_futures); - helix_lsp::block_on(wait_futures.for_each(|_| future::ready(()))); + wait_futures.for_each(|_| future::ready(())).await } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b317242d..e08d7e44 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,4 +1,4 @@ -pub use crate::commands::Command; +pub use crate::commands::MappableCommand; use crate::config::Config; use helix_core::hashmap; use helix_view::{document::Mode, info::Info, input::KeyEvent}; @@ -92,7 +92,7 @@ macro_rules! alt { #[macro_export] macro_rules! keymap { (@trie $cmd:ident) => { - $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd) + $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd) }; (@trie @@ -120,7 +120,7 @@ macro_rules! keymap { _key, keymap!(@trie $value) ); - debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap()); + assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap()); _order.push(_key); )+ )* @@ -222,9 +222,8 @@ impl KeyTrieNode { .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) .collect(); } - Info::new(self.name(), body) + Info::from_keymap(self.name(), body) } - /// Get a reference to the key trie node's order. pub fn order(&self) -> &[KeyEvent] { self.order.as_slice() @@ -260,8 +259,8 @@ impl DerefMut for KeyTrieNode { #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(untagged)] pub enum KeyTrie { - Leaf(Command), - Sequence(Vec<Command>), + Leaf(MappableCommand), + Sequence(Vec<MappableCommand>), Node(KeyTrieNode), } @@ -304,9 +303,9 @@ impl KeyTrie { pub enum KeymapResultKind { /// Needs more keys to execute a command. Contains valid keys for next keystroke. Pending(KeyTrieNode), - Matched(Command), + Matched(MappableCommand), /// Matched a sequence of commands to execute. - MatchedSequence(Vec<Command>), + MatchedSequence(Vec<MappableCommand>), /// Key was not found in the root keymap NotFound, /// Key is invalid in combination with previous keys. Contains keys leading upto @@ -344,7 +343,7 @@ pub struct Keymap { impl Keymap { pub fn new(root: KeyTrie) -> Self { - Keymap { + Self { root, state: Vec::new(), sticky: None, @@ -368,7 +367,7 @@ impl Keymap { /// key cancels pending keystrokes. If there are no pending keystrokes but a /// sticky node is in use, it will be cleared. pub fn get(&mut self, key: KeyEvent) -> KeymapResult { - if let key!(Esc) = key { + if key!(Esc) == key { if !self.state.is_empty() { return KeymapResult::new( // Note that Esc is not included here @@ -386,10 +385,10 @@ impl Keymap { }; let trie = match trie_node.search(&[*first]) { - Some(&KeyTrie::Leaf(cmd)) => { - return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()) + Some(KeyTrie::Leaf(ref cmd)) => { + return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky()) } - Some(&KeyTrie::Sequence(ref cmds)) => { + Some(KeyTrie::Sequence(ref cmds)) => { return KeymapResult::new( KeymapResultKind::MatchedSequence(cmds.clone()), self.sticky(), @@ -408,9 +407,9 @@ impl Keymap { } KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky()) } - Some(&KeyTrie::Leaf(cmd)) => { + Some(&KeyTrie::Leaf(ref cmd)) => { self.state.clear(); - return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()); + return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky()); } Some(&KeyTrie::Sequence(ref cmds)) => { self.state.clear(); @@ -477,7 +476,7 @@ impl DerefMut for Keymaps { } impl Default for Keymaps { - fn default() -> Keymaps { + fn default() -> Self { let normal = keymap!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, @@ -521,9 +520,10 @@ impl Default for Keymaps { "r" => goto_reference, "i" => goto_implementation, "t" => goto_window_top, - "m" => goto_window_middle, + "c" => goto_window_center, "b" => goto_window_bottom, "a" => goto_last_accessed_file, + "m" => goto_last_modified_file, "n" => goto_next_buffer, "p" => goto_previous_buffer, "." => goto_last_modification, @@ -551,6 +551,11 @@ impl Default for Keymaps { "S" => split_selection, ";" => collapse_selection, "A-;" => flip_selections, + "A-k" => expand_selection, + "A-j" => shrink_selection, + "A-h" => select_prev_sibling, + "A-l" => select_next_sibling, + "%" => select_all, "x" => extend_line, "X" => extend_to_line_bounds, @@ -592,6 +597,9 @@ impl Default for Keymaps { // paste_all "P" => paste_before, + "Q" => record_macro, + "q" => replay_macro, + ">" => indent, "<" => unindent, "=" => format_selections, @@ -613,6 +621,8 @@ impl Default for Keymaps { "A-(" => rotate_selection_contents_backward, "A-)" => rotate_selection_contents_forward, + "A-:" => ensure_selections_forward, + "esc" => normal_mode, "C-b" | "pageup" => page_up, "C-f" | "pagedown" => page_down, @@ -640,7 +650,7 @@ impl Default for Keymaps { "tab" => jump_forward, // tab == <C-i> "C-o" => jump_backward, - // "C-s" => save_selection, + "C-s" => save_selection, "space" => { "Space" "f" => file_picker, @@ -763,8 +773,10 @@ impl Default for Keymaps { "del" => delete_char_forward, "C-d" => delete_char_forward, "ret" => insert_newline, + "C-j" => insert_newline, "tab" => insert_tab, "C-w" => delete_word_backward, + "A-backspace" => delete_word_backward, "A-d" => delete_word_forward, "left" => move_char_left, @@ -779,6 +791,8 @@ impl Default for Keymaps { "A-left" => move_prev_word_end, "A-f" => move_next_word_start, "A-right" => move_next_word_start, + "A-<" => goto_file_start, + "A->" => goto_file_end, "pageup" => page_up, "pagedown" => page_down, "home" => goto_line_start, @@ -792,7 +806,7 @@ impl Default for Keymaps { "C-x" => completion, "C-r" => insert_register, }); - Keymaps(hashmap!( + Self(hashmap!( Mode::Normal => Keymap::new(normal), Mode::Select => Keymap::new(select), Mode::Insert => Keymap::new(insert), @@ -852,36 +866,36 @@ mod tests { let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); assert_eq!( keymap.get(key!('i')).kind, - KeymapResultKind::Matched(Command::normal_mode), + KeymapResultKind::Matched(MappableCommand::normal_mode), "Leaf should replace leaf" ); assert_eq!( keymap.get(key!('无')).kind, - KeymapResultKind::Matched(Command::insert_mode), + KeymapResultKind::Matched(MappableCommand::insert_mode), "New leaf should be present in merged keymap" ); // Assumes that z is a node in the default keymap assert_eq!( keymap.get(key!('z')).kind, - KeymapResultKind::Matched(Command::jump_backward), + KeymapResultKind::Matched(MappableCommand::jump_backward), "Leaf should replace node" ); // Assumes that `g` is a node in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('$')]).unwrap(), - &KeyTrie::Leaf(Command::goto_line_end), + &KeyTrie::Leaf(MappableCommand::goto_line_end), "Leaf should be present in merged subnode" ); // Assumes that `gg` is in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('g')]).unwrap(), - &KeyTrie::Leaf(Command::delete_char_forward), + &KeyTrie::Leaf(MappableCommand::delete_char_forward), "Leaf should replace old leaf in merged subnode" ); // Assumes that `ge` is in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('e')]).unwrap(), - &KeyTrie::Leaf(Command::goto_last_line), + &KeyTrie::Leaf(MappableCommand::goto_last_line), "Old leaves in subnode should be present in merged node" ); @@ -915,7 +929,7 @@ mod tests { .root() .search(&[key!(' '), key!('s'), key!('v')]) .unwrap(), - &KeyTrie::Leaf(Command::vsplit), + &KeyTrie::Leaf(MappableCommand::vsplit), "Leaf should be present in merged subnode" ); // Make sure an order was set during merge diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index f5e3a8cd..58cb139c 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -9,3 +9,14 @@ pub mod config; pub mod job; pub mod keymap; pub mod ui; + +#[cfg(not(windows))] +fn true_color() -> bool { + std::env::var("COLORTERM") + .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) + .unwrap_or(false) +} +#[cfg(windows)] +fn true_color() -> bool { + true +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 88140130..0f504046 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -56,7 +56,7 @@ USAGE: hx [FLAGS] [files]... ARGS: - <files>... Sets the input file to use + <files>... Sets the input file to use, position can also be specified via file[:row[:col]] FLAGS: -h, --help Prints help information diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index dd782d29..35afe81e 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -154,8 +154,19 @@ impl Completion { ); doc.apply(&transaction, view.id); - if let Some(additional_edits) = &item.additional_text_edits { - // gopls uses this to add extra imports + // apply additional edits, mostly used to auto import unqualified types + let resolved_additional_text_edits = if item.additional_text_edits.is_some() { + None + } else { + Self::resolve_completion_item(doc, item.clone()) + .and_then(|item| item.additional_text_edits) + }; + + if let Some(additional_edits) = item + .additional_text_edits + .as_ref() + .or_else(|| resolved_additional_text_edits.as_ref()) + { if !additional_edits.is_empty() { let transaction = util::generate_transaction_from_edits( doc.text(), @@ -168,7 +179,7 @@ impl Completion { } }; }); - let popup = Popup::new(menu); + let popup = Popup::new("completion", menu); let mut completion = Self { popup, start_offset, @@ -181,6 +192,31 @@ impl Completion { completion } + fn resolve_completion_item( + doc: &Document, + completion_item: lsp::CompletionItem, + ) -> Option<CompletionItem> { + let language_server = doc.language_server()?; + let completion_resolve_provider = language_server + .capabilities() + .completion_provider + .as_ref()? + .resolve_provider; + if completion_resolve_provider != Some(true) { + return None; + } + + let future = language_server.resolve_completion_item(completion_item); + let response = helix_lsp::block_on(future); + match response { + Ok(completion_item) => Some(completion_item), + Err(err) => { + log::error!("execute LSP command: {}", err); + None + } + } + } + pub fn recompute_filter(&mut self, editor: &Editor) { // recompute menu based on matches let menu = self.popup.contents_mut(); @@ -268,6 +304,9 @@ impl Component for Completion { let cursor_pos = doc.selection(view.id).primary().cursor(text); let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width()); let cursor_pos = (coords.row - view.offset.row) as u16; + + let markdown_ui = + |content, syn_loader| Markdown::new(content, syn_loader).style_group("completion"); let mut markdown_doc = match &option.documentation { Some(lsp::Documentation::String(contents)) | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { @@ -275,7 +314,7 @@ impl Component for Completion { value: contents, })) => { // TODO: convert to wrapped text - Markdown::new( + markdown_ui( format!( "```{}\n{}\n```\n{}", language, @@ -290,7 +329,7 @@ impl Component for Completion { value: contents, })) => { // TODO: set language based on doc scope - Markdown::new( + markdown_ui( format!( "```{}\n{}\n```\n{}", language, @@ -304,7 +343,7 @@ impl Component for Completion { // TODO: copied from above // TODO: set language based on doc scope - Markdown::new( + markdown_ui( format!( "```{}\n{}\n```", language, @@ -328,8 +367,8 @@ impl Component for Completion { let y = popup_y; if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { - width = rel_width; - height = rel_height; + width = rel_width.min(width); + height = rel_height.min(height); } Rect::new(x, y, width, height) } else { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ac11d298..a2131abe 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -7,8 +7,10 @@ use crate::{ }; use helix_core::{ - coords_at_pos, - graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, + coords_at_pos, encoding, + graphemes::{ + ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, + }, movement::Direction, syntax::{self, HighlightEvent}, unicode::segmentation::UnicodeSegmentation, @@ -17,8 +19,8 @@ use helix_core::{ }; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, + editor::CursorShapeConfig, graphics::{CursorKind, Modifier, Rect, Style}, - info::Info, input::KeyEvent, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, @@ -31,10 +33,9 @@ use tui::buffer::Buffer as Surface; pub struct EditorView { keymaps: Keymaps, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, - last_insert: (commands::Command, Vec<KeyEvent>), + last_insert: (commands::MappableCommand, Vec<KeyEvent>), pub(crate) completion: Option<Completion>, spinners: ProgressSpinners, - autoinfo: Option<Info>, } impl Default for EditorView { @@ -48,10 +49,9 @@ impl EditorView { Self { keymaps, on_next_key: None, - last_insert: (commands::Command::normal_mode, Vec::new()), + last_insert: (commands::MappableCommand::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), - autoinfo: None, } } @@ -106,13 +106,12 @@ impl EditorView { } } - let highlights = - Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, &editor.syn_loader); + let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme); let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused { Box::new(syntax::merge( highlights, - Self::doc_selection_highlights(doc, view, theme), + Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape), )) } else { Box::new(highlights) @@ -130,8 +129,7 @@ impl EditorView { let x = area.right(); let border_style = theme.get("ui.window"); for y in area.top()..area.bottom() { - surface - .get_mut(x, y) + surface[(x, y)] .set_symbol(tui::symbols::line::VERTICAL) //.set_symbol(" ") .set_style(border_style); @@ -154,8 +152,7 @@ impl EditorView { doc: &'doc Document, offset: Position, height: u16, - theme: &Theme, - loader: &syntax::Loader, + _theme: &Theme, ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> { let text = doc.text().slice(..); let last_line = std::cmp::min( @@ -172,48 +169,34 @@ impl EditorView { start..end }; - // TODO: range doesn't actually restrict source, just highlight range - let highlights = match doc.syntax() { + match doc.syntax() { Some(syntax) => { - let scopes = theme.scopes(); - syntax - .highlight_iter(text.slice(..), Some(range), None, |language| { - loader.language_configuration_for_injection_string(language) - .and_then(|language_config| { - let config = language_config.highlight_config(scopes)?; - let config_ref = config.as_ref(); - // SAFETY: the referenced `HighlightConfiguration` behind - // the `Arc` is guaranteed to remain valid throughout the - // duration of the highlight. - let config_ref = unsafe { - std::mem::transmute::< - _, - &'static syntax::HighlightConfiguration, - >(config_ref) - }; - Some(config_ref) - }) - }) + let iter = syntax + // TODO: range doesn't actually restrict source, just highlight range + .highlight_iter(text.slice(..), Some(range), None) .map(|event| event.unwrap()) - .collect() // TODO: we collect here to avoid holding the lock, fix later + .map(move |event| match event { + // convert byte offsets to char offset + HighlightEvent::Source { start, end } => { + let start = + text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start)); + let end = + text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end)); + HighlightEvent::Source { start, end } + } + event => event, + }); + + Box::new(iter) } - None => vec![HighlightEvent::Source { - start: range.start, - end: range.end, - }], + None => Box::new( + [HighlightEvent::Source { + start: text.byte_to_char(range.start), + end: text.byte_to_char(range.end), + }] + .into_iter(), + ), } - .into_iter() - .map(move |event| match event { - // convert byte offsets to char offset - HighlightEvent::Source { start, end } => { - let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start)); - let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end)); - HighlightEvent::Source { start, end } - } - event => event, - }); - - Box::new(highlights) } /// Get highlight spans for document diagnostics @@ -245,11 +228,16 @@ impl EditorView { doc: &Document, view: &View, theme: &Theme, + cursor_shape_config: &CursorShapeConfig, ) -> Vec<(usize, std::ops::Range<usize>)> { let text = doc.text().slice(..); let selection = doc.selection(view.id); let primary_idx = selection.primary_index(); + let mode = doc.mode(); + let cursorkind = cursor_shape_config.from_mode(mode); + let cursor_is_block = cursorkind == CursorKind::Block; + let selection_scope = theme .find_scope_index("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); @@ -257,7 +245,7 @@ impl EditorView { .find_scope_index("ui.cursor") .unwrap_or(selection_scope); - let cursor_scope = match doc.mode() { + let cursor_scope = match mode { Mode::Insert => theme.find_scope_index("ui.cursor.insert"), Mode::Select => theme.find_scope_index("ui.cursor.select"), Mode::Normal => Some(base_cursor_scope), @@ -273,7 +261,8 @@ impl EditorView { let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new(); for (i, range) in selection.iter().enumerate() { - let (cursor_scope, selection_scope) = if i == primary_idx { + let selection_is_primary = i == primary_idx; + let (cursor_scope, selection_scope) = if selection_is_primary { (primary_cursor_scope, primary_selection_scope) } else { (cursor_scope, selection_scope) @@ -281,7 +270,14 @@ impl EditorView { // Special-case: cursor at end of the rope. if range.head == range.anchor && range.head == text.len_chars() { - spans.push((cursor_scope, range.head..range.head + 1)); + if !selection_is_primary || cursor_is_block { + // Bar and underline cursors are drawn by the terminal + // BUG: If the editor area loses focus while having a bar or + // underline cursor (eg. when a regex prompt has focus) then + // the primary cursor will be invisible. This doesn't happen + // with block cursors since we manually draw *all* cursors. + spans.push((cursor_scope, range.head..range.head + 1)); + } continue; } @@ -290,11 +286,15 @@ impl EditorView { // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); spans.push((selection_scope, range.anchor..cursor_start)); - spans.push((cursor_scope, cursor_start..range.head)); + if !selection_is_primary || cursor_is_block { + spans.push((cursor_scope, cursor_start..range.head)); + } } else { // Reverse case. let cursor_end = next_grapheme_boundary(text, range.head); - spans.push((cursor_scope, range.head..cursor_end)); + if !selection_is_primary || cursor_is_block { + spans.push((cursor_scope, range.head..cursor_end)); + } spans.push((selection_scope, cursor_end..range.anchor)); } } @@ -320,6 +320,10 @@ impl EditorView { let text_style = theme.get("ui.text"); + // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch + // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light). + let text = text.slice(..); + 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { @@ -336,17 +340,16 @@ impl EditorView { use helix_core::graphemes::{grapheme_width, RopeGraphemes}; - let style = spans.iter().fold(text_style, |acc, span| { - let style = theme.get(theme.scopes()[span.0].as_str()); - acc.patch(style) - }); - for grapheme in RopeGraphemes::new(text) { let out_of_bounds = visual_x < offset.col as u16 || visual_x >= viewport.width + offset.col as u16; if LineEnding::from_rope_slice(&grapheme).is_some() { if !out_of_bounds { + let style = spans.iter().fold(text_style, |acc, span| { + acc.patch(theme.highlight(span.0)) + }); + // we still want to render an empty cell with the style surface.set_string( viewport.x + visual_x - offset.col as u16, @@ -377,6 +380,10 @@ impl EditorView { }; if !out_of_bounds { + let style = spans.iter().fold(text_style, |acc, span| { + acc.patch(theme.highlight(span.0)) + }); + // if we're offscreen just keep going until we hit a new line surface.set_string( viewport.x + visual_x - offset.col as u16, @@ -422,8 +429,7 @@ impl EditorView { .add_modifier(Modifier::DIM) }); - surface - .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16) + surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)] .set_style(style); } } @@ -453,6 +459,8 @@ impl EditorView { let mut offset = 0; + let gutter_style = theme.get("ui.gutter"); + // avoid lots of small allocations by reusing a text buffer for each line let mut text = String::with_capacity(8); @@ -468,7 +476,7 @@ impl EditorView { viewport.y + i as u16, &text, *width, - style, + gutter_style.patch(style), ); } text.clear(); @@ -574,21 +582,6 @@ impl EditorView { } surface.set_string(viewport.x + 5, viewport.y, progress, base_style); - let rel_path = doc.relative_path(); - let path = rel_path - .as_ref() - .map(|p| p.to_string_lossy()) - .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); - - let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" }); - surface.set_stringn( - viewport.x + 8, - viewport.y, - title, - viewport.width.saturating_sub(6) as usize, - base_style, - ); - //------------------------------- // Right side of the status line. //------------------------------- @@ -662,6 +655,13 @@ impl EditorView { base_style, )); + let enc = doc.encoding(); + if enc != encoding::UTF_8 { + right_side_text + .0 + .push(Span::styled(format!(" {} ", enc.name()), base_style)); + } + // Render to the statusline. surface.set_spans( viewport.x @@ -672,6 +672,31 @@ impl EditorView { &right_side_text, right_side_text.width() as u16, ); + + //------------------------------- + // Middle / File path / Title + //------------------------------- + let title = { + let rel_path = doc.relative_path(); + let path = rel_path + .as_ref() + .map(|p| p.to_string_lossy()) + .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); + format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" }) + }; + + surface.set_string_truncated( + viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space + viewport.y, + title, + viewport + .width + .saturating_sub(6) + .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info + base_style, + true, + true, + ); } /// Handle events by looking them up in `self.keymaps`. Returns None @@ -684,12 +709,13 @@ impl EditorView { cxt: &mut commands::Context, event: KeyEvent, ) -> Option<KeymapResult> { + cxt.editor.autoinfo = None; let key_result = self.keymaps.get_mut(&mode).unwrap().get(event); - self.autoinfo = key_result.sticky.map(|node| node.infobox()); + cxt.editor.autoinfo = key_result.sticky.map(|node| node.infobox()); match &key_result.kind { KeymapResultKind::Matched(command) => command.execute(cxt), - KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), + KeymapResultKind::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), KeymapResultKind::MatchedSequence(commands) => { for command in commands { command.execute(cxt); @@ -789,8 +815,9 @@ impl EditorView { pub fn clear_completion(&mut self, editor: &mut Editor) { self.completion = None; + // Clear any savepoints - let (_, doc) = current!(editor); + let doc = doc_mut!(editor); doc.savepoint = None; editor.clear_idle_timer(); // don't retrigger } @@ -927,7 +954,7 @@ impl EditorView { return EventResult::Ignored; } - commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt); + commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt); EventResult::Consumed(None) } @@ -953,9 +980,9 @@ impl EditorView { if let Ok(pos) = doc.text().try_line_to_char(line) { doc.set_selection(view_id, Selection::point(pos)); if modifiers == crossterm::event::KeyModifiers::ALT { - commands::Command::dap_edit_log.execute(cxt); + commands::MappableCommand::dap_edit_log.execute(cxt); } else { - commands::Command::dap_edit_condition.execute(cxt); + commands::MappableCommand::dap_edit_condition.execute(cxt); } return EventResult::Consumed(None); @@ -977,7 +1004,8 @@ impl EditorView { } if modifiers == crossterm::event::KeyModifiers::ALT { - commands::Command::replace_selections_with_primary_clipboard.execute(cxt); + commands::MappableCommand::replace_selections_with_primary_clipboard + .execute(cxt); return EventResult::Consumed(None); } @@ -991,7 +1019,7 @@ impl EditorView { let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap(); doc.set_selection(view_id, Selection::point(pos)); editor.tree.focus = view_id; - commands::Command::paste_primary_clipboard_before.execute(cxt); + commands::MappableCommand::paste_primary_clipboard_before.execute(cxt); return EventResult::Consumed(None); } @@ -1004,14 +1032,18 @@ impl EditorView { } impl Component for EditorView { - fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { - let mut cxt = commands::Context { - editor: &mut cx.editor, + fn handle_event( + &mut self, + event: Event, + context: &mut crate::compositor::Context, + ) -> EventResult { + let mut cx = commands::Context { + editor: context.editor, count: None, register: None, callback: None, on_next_key_callback: None, - jobs: cx.jobs, + jobs: context.jobs, }; match event { @@ -1021,18 +1053,19 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(key) => { - cxt.editor.reset_idle_timer(); + cx.editor.reset_idle_timer(); let mut key = KeyEvent::from(key); canonicalize_key(&mut key); + // clear status - cxt.editor.status_msg = None; + cx.editor.status_msg = None; - let (_, doc) = current!(cxt.editor); + let doc = doc!(cx.editor); let mode = doc.mode(); if let Some(on_next_key) = self.on_next_key.take() { // if there's a command waiting input, do that first - on_next_key(&mut cxt, key); + on_next_key(&mut cx, key); } else { match mode { Mode::Insert => { @@ -1044,8 +1077,8 @@ impl Component for EditorView { if let Some(completion) = &mut self.completion { // use a fake context here let mut cx = Context { - editor: cxt.editor, - jobs: cxt.jobs, + editor: cx.editor, + jobs: cx.jobs, scroll: None, }; let res = completion.handle_event(event, &mut cx); @@ -1055,40 +1088,46 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn - self.clear_completion(cxt.editor); + self.clear_completion(cx.editor); } } } // if completion didn't take the event, we pass it onto commands if !consumed { - self.insert_mode(&mut cxt, key); + self.insert_mode(&mut cx, key); // lastly we recalculate completion if let Some(completion) = &mut self.completion { - completion.update(&mut cxt); + completion.update(&mut cx); if completion.is_empty() { - self.clear_completion(cxt.editor); + self.clear_completion(cx.editor); } } } } - mode => self.command_mode(mode, &mut cxt, key), + mode => self.command_mode(mode, &mut cx, key), } } - self.on_next_key = cxt.on_next_key_callback.take(); + self.on_next_key = cx.on_next_key_callback.take(); // appease borrowck - let callback = cxt.callback.take(); + let callback = cx.callback.take(); // if the command consumed the last view, skip the render. // on the next loop cycle the Application will then terminate. - if cxt.editor.should_close() { + if cx.editor.should_close() { return EventResult::Ignored; } - let (view, doc) = current!(cxt.editor); - view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff); + let (view, doc) = current!(cx.editor); + view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); + + // Store a history state if not in insert mode. This also takes care of + // commiting changes when leaving insert mode. + if doc.mode() != Mode::Insert { + doc.append_changes_to_history(view.id); + } // mode transitions match (mode, doc.mode()) { @@ -1117,7 +1156,7 @@ impl Component for EditorView { EventResult::Consumed(callback) } - Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt), + Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), } } @@ -1134,8 +1173,9 @@ impl Component for EditorView { } if cx.editor.config.auto_info { - if let Some(ref mut info) = self.autoinfo { + if let Some(mut info) = cx.editor.autoinfo.take() { info.render(area, surface, cx); + cx.editor.autoinfo = Some(info) } } @@ -1173,13 +1213,31 @@ impl Component for EditorView { disp.push_str(&s); } } + let style = cx.editor.theme.get("ui.text"); + let macro_width = if cx.editor.macro_recording.is_some() { + 3 + } else { + 0 + }; surface.set_string( - area.x + area.width.saturating_sub(key_width), + area.x + area.width.saturating_sub(key_width + macro_width), area.y + area.height.saturating_sub(1), disp.get(disp.len().saturating_sub(key_width as usize)..) .unwrap_or(&disp), - cx.editor.theme.get("ui.text"), + style, ); + if let Some((reg, _)) = cx.editor.macro_recording { + let disp = format!("[{}]", reg); + let style = style + .fg(helix_view::graphics::Color::Yellow) + .add_modifier(Modifier::BOLD); + surface.set_string( + area.x + area.width.saturating_sub(3), + area.y + area.height.saturating_sub(1), + &disp, + style, + ); + } } if let Some(completion) = self.completion.as_mut() { @@ -1188,11 +1246,11 @@ impl Component for EditorView { } 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"), - // }; - editor.cursor() + match editor.cursor() { + // All block cursors are drawn manually + (pos, CursorKind::Block) => (pos, CursorKind::Hidden), + cursor => cursor, + } } } diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index ca8303dd..6a7b641a 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -21,6 +21,9 @@ pub struct Markdown { contents: String, config_loader: Arc<syntax::Loader>, + + block_style: String, + heading_style: String, } // TODO: pre-render and self reference via Pin @@ -31,120 +34,137 @@ impl Markdown { Self { contents, config_loader, + block_style: "markup.raw.inline".into(), + heading_style: "markup.heading".into(), } } -} -fn parse<'a>( - contents: &'a str, - theme: Option<&Theme>, - loader: &syntax::Loader, -) -> tui::text::Text<'a> { - // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}} - // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```"; - - let mut options = Options::empty(); - options.insert(Options::ENABLE_STRIKETHROUGH); - let parser = Parser::new_ext(contents, options); - - // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda - let mut tags = Vec::new(); - let mut spans = Vec::new(); - let mut lines = Vec::new(); - - fn to_span(text: pulldown_cmark::CowStr) -> Span { - use std::ops::Deref; - Span::raw::<std::borrow::Cow<_>>(match text { - CowStr::Borrowed(s) => s.into(), - CowStr::Boxed(s) => s.to_string().into(), - CowStr::Inlined(s) => s.deref().to_owned().into(), - }) + pub fn style_group(mut self, suffix: &str) -> Self { + self.block_style = format!("markup.raw.inline.{}", suffix); + self.heading_style = format!("markup.heading.{}", suffix); + self } - let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default(); - - // TODO: use better scopes for these, `markup.raw.block`, `markup.heading` - let code_style = theme - .map(|theme| theme.get("ui.text.focus")) - .unwrap_or_default(); // white - let heading_style = theme - .map(|theme| theme.get("ui.linenr.selected")) - .unwrap_or_default(); // lilac - - for event in parser { - match event { - Event::Start(tag) => tags.push(tag), - Event::End(tag) => { - tags.pop(); - match tag { - Tag::Heading(_) | Tag::Paragraph | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => { - // whenever code block or paragraph closes, new line - let spans = std::mem::take(&mut spans); - if !spans.is_empty() { - lines.push(Spans::from(spans)); + fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> { + // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}} + // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```"; + + let mut options = Options::empty(); + options.insert(Options::ENABLE_STRIKETHROUGH); + let parser = Parser::new_ext(&self.contents, options); + + // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + let mut tags = Vec::new(); + let mut spans = Vec::new(); + let mut lines = Vec::new(); + + fn to_span(text: pulldown_cmark::CowStr) -> Span { + use std::ops::Deref; + Span::raw::<std::borrow::Cow<_>>(match text { + CowStr::Borrowed(s) => s.into(), + CowStr::Boxed(s) => s.to_string().into(), + CowStr::Inlined(s) => s.deref().to_owned().into(), + }) + } + + macro_rules! get_theme { + ($s1: expr) => { + theme + .map(|theme| theme.try_get($s1.as_str())) + .flatten() + .unwrap_or_default() + }; + } + let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default(); + let code_style = get_theme!(self.block_style); + let heading_style = get_theme!(self.heading_style); + + for event in parser { + match event { + Event::Start(tag) => tags.push(tag), + Event::End(tag) => { + tags.pop(); + match tag { + Tag::Heading(_, _, _) + | Tag::Paragraph + | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => { + // whenever code block or paragraph closes, new line + let spans = std::mem::take(&mut spans); + if !spans.is_empty() { + lines.push(Spans::from(spans)); + } + lines.push(Spans::default()); } - lines.push(Spans::default()); + _ => (), } - _ => (), } - } - Event::Text(text) => { - // TODO: temp workaround - if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() { - if let Some(theme) = theme { - let rope = Rope::from(text.as_ref()); - let syntax = loader - .language_configuration_for_injection_string(language) - .and_then(|config| config.highlight_config(theme.scopes())) - .map(|config| Syntax::new(&rope, config)); - - if let Some(syntax) = syntax { - // if we have a syntax available, highlight_iter and generate spans - let mut highlights = Vec::new(); - - for event in syntax.highlight_iter(rope.slice(..), None, None, |_| None) - { - match event.unwrap() { - HighlightEvent::HighlightStart(span) => { - highlights.push(span); - } - HighlightEvent::HighlightEnd => { - highlights.pop(); - } - HighlightEvent::Source { start, end } => { - let style = match highlights.first() { - 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]; - let text = text.replace('\t', " "); // replace tabs - let span = Span::styled(text, style); - spans.push(span); - - // truncate slice to after newline - slice = &slice[end + 1..]; - - // make a new line - let spans = std::mem::take(&mut spans); - lines.push(Spans::from(spans)); + Event::Text(text) => { + // TODO: temp workaround + if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() { + if let Some(theme) = theme { + let rope = Rope::from(text.as_ref()); + let syntax = self + .config_loader + .language_configuration_for_injection_string(language) + .and_then(|config| config.highlight_config(theme.scopes())) + .map(|config| { + Syntax::new(&rope, config, self.config_loader.clone()) + }); + + if let Some(syntax) = syntax { + // if we have a syntax available, highlight_iter and generate spans + let mut highlights = Vec::new(); + + for event in syntax.highlight_iter(rope.slice(..), None, None) { + match event.unwrap() { + HighlightEvent::HighlightStart(span) => { + highlights.push(span); + } + HighlightEvent::HighlightEnd => { + highlights.pop(); } + HighlightEvent::Source { start, end } => { + let style = match highlights.first() { + Some(span) => theme.get(&theme.scopes()[span.0]), + None => text_style, + }; - // if there's anything left, emit it too - if !slice.is_empty() { - let span = - Span::styled(slice.replace('\t', " "), style); - spans.push(span); + // 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]; + let text = text.replace('\t', " "); // replace tabs + let span = Span::styled(text, style); + spans.push(span); + + // truncate slice to after newline + slice = &slice[end + 1..]; + + // make a new line + let spans = std::mem::take(&mut spans); + lines.push(Spans::from(spans)); + } + + // if there's anything left, emit it too + if !slice.is_empty() { + let span = Span::styled( + slice.replace('\t', " "), + style, + ); + spans.push(span); + } } } } + } else { + for line in text.lines() { + let span = Span::styled(line.to_string(), code_style); + lines.push(Spans::from(span)); + } } } else { for line in text.lines() { @@ -152,64 +172,60 @@ fn parse<'a>( lines.push(Spans::from(span)); } } + } else if let Some(Tag::Heading(_, _, _)) = tags.last() { + let mut span = to_span(text); + span.style = heading_style; + spans.push(span); } else { - for line in text.lines() { - let span = Span::styled(line.to_string(), code_style); - lines.push(Spans::from(span)); - } + let mut span = to_span(text); + span.style = text_style; + spans.push(span); } - } else if let Some(Tag::Heading(_)) = tags.last() { - let mut span = to_span(text); - span.style = heading_style; - spans.push(span); - } else { + } + Event::Code(text) | Event::Html(text) => { let mut span = to_span(text); - span.style = text_style; + span.style = code_style; spans.push(span); } + Event::SoftBreak | Event::HardBreak => { + // let spans = std::mem::replace(&mut spans, Vec::new()); + // lines.push(Spans::from(spans)); + spans.push(Span::raw(" ")); + } + Event::Rule => { + let mut span = Span::raw("---"); + span.style = code_style; + lines.push(Spans::from(span)); + lines.push(Spans::default()); + } + // TaskListMarker(bool) true if checked + _ => { + log::warn!("unhandled markdown event {:?}", event); + } } - Event::Code(text) | Event::Html(text) => { - let mut span = to_span(text); - span.style = code_style; - spans.push(span); - } - Event::SoftBreak | Event::HardBreak => { - // let spans = std::mem::replace(&mut spans, Vec::new()); - // lines.push(Spans::from(spans)); - spans.push(Span::raw(" ")); - } - Event::Rule => { - let mut span = Span::raw("---"); - span.style = code_style; - lines.push(Spans::from(span)); - lines.push(Spans::default()); - } - // TaskListMarker(bool) true if checked - _ => { - log::warn!("unhandled markdown event {:?}", event); - } + // build up a vec of Paragraph tui widgets } - // build up a vec of Paragraph tui widgets - } - if !spans.is_empty() { - lines.push(Spans::from(spans)); - } + if !spans.is_empty() { + lines.push(Spans::from(spans)); + } - // if last line is empty, remove it - if let Some(line) = lines.last() { - if line.0.is_empty() { - lines.pop(); + // if last line is empty, remove it + if let Some(line) = lines.last() { + if line.0.is_empty() { + lines.pop(); + } } - } - Text::from(lines) + Text::from(lines) + } } + impl Component for Markdown { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { use tui::widgets::{Paragraph, Widget, Wrap}; - let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader); + let text = self.parse(Some(&cx.editor.theme)); let par = Paragraph::new(text) .wrap(Wrap { trim: false }) @@ -227,7 +243,8 @@ impl Component for Markdown { if padding >= viewport.1 || padding >= viewport.0 { return None; } - let contents = parse(&self.contents, None, &self.config_loader); + let contents = self.parse(None); + // TODO: account for tab width let max_text_width = (viewport.0 - padding).min(120); let mut text_width = 0; @@ -241,11 +258,6 @@ impl Component for Markdown { } else if content_width > text_width { text_width = content_width; } - - if height >= viewport.1 { - height = viewport.1; - break; - } } Some((text_width + padding, height)) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index e891c149..f9a0438c 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -14,11 +14,18 @@ use helix_view::{graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item { - fn sort_text(&self) -> &str; - fn filter_text(&self) -> &str; - fn label(&self) -> &str; - fn row(&self) -> Row; + + fn sort_text(&self) -> &str { + self.label() + } + fn filter_text(&self) -> &str { + self.label() + } + + fn row(&self) -> Row { + Row::new(vec![Cell::from(self.label())]) + } } pub struct Menu<T: Item> { @@ -132,7 +139,17 @@ impl<T: Item> Menu<T> { acc }); - let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar + + let height = self.matches.len().min(10).min(viewport.1 as usize); + // do all the matches fit on a single screen? + let fits = self.matches.len() <= height; + + let mut len = max_lens.iter().sum::<usize>() + n; + + if !fits { + len += 1; // +1: reserve some space for scrollbar + } + let width = len.min(viewport.0 as usize); self.widths = max_lens @@ -140,8 +157,6 @@ impl<T: Item> Menu<T> { .map(|len| Constraint::Length(len as u16)) .collect(); - let height = self.matches.len().min(10).min(viewport.1 as usize); - self.size = (width as u16, height as u16); // adjust scroll offsets if size changed @@ -190,7 +205,7 @@ impl<T: Item + 'static> Component for Menu<T> { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); @@ -202,7 +217,7 @@ impl<T: Item + 'static> Component for Menu<T> { return close_fn; } // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) - shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => { + shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { self.move_up(); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); return EventResult::Consumed(None); @@ -297,12 +312,14 @@ impl<T: Item + 'static> Component for Menu<T> { }, ); + let fits = len <= win_height; + for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() { let is_marked = i >= scroll_line && i < scroll_line + scroll_height; - if is_marked { - let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16); - cell.set_symbol("▐ "); + if !fits && is_marked { + let cell = &mut surface[(area.x + area.width - 2, area.y + i as u16)]; + cell.set_symbol("▐"); // cell.set_style(selected); // cell.set_style(if is_marked { selected } else { style }); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3c203326..49f7b2fa 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -2,7 +2,7 @@ mod completion; pub(crate) mod editor; mod info; mod markdown; -mod menu; +pub mod menu; mod picker; mod popup; mod prompt; @@ -65,7 +65,7 @@ pub fn regex_prompt( return; } - let case_insensitive = if cx.editor.config.smart_case { + let case_insensitive = if cx.editor.config.search.smart_case { !input.chars().any(char::is_uppercase) } else { false @@ -174,7 +174,9 @@ pub mod completers { use crate::ui::prompt::Completion; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; + use helix_view::editor::Config; use helix_view::theme; + use once_cell::sync::Lazy; use std::borrow::Cow; use std::cmp::Reverse; @@ -186,6 +188,7 @@ pub mod completers { &helix_core::config_dir().join("themes"), )); names.push("default".into()); + names.push("base16_default".into()); let mut names: Vec<_> = names .into_iter() @@ -207,6 +210,31 @@ pub mod completers { names } + pub fn setting(input: &str) -> Vec<Completion> { + static KEYS: Lazy<Vec<String>> = Lazy::new(|| { + serde_json::to_value(Config::default()) + .unwrap() + .as_object() + .unwrap() + .keys() + .cloned() + .collect() + }); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = KEYS + .iter() + .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score))) + .collect(); + + matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); + matches + .into_iter() + .map(|(name, _)| ((0..), name.into())) + .collect() + } + pub fn filename(input: &str) -> Vec<Completion> { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); @@ -255,7 +283,7 @@ pub mod completers { let is_tilde = input.starts_with('~') && input.len() == 1; let path = helix_core::path::expand_tilde(Path::new(input)); - let (dir, file_name) = if input.ends_with('/') { + let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) { (path, None) } else { let file_name = path diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index eaca470e..2c7db7f2 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -46,7 +46,7 @@ pub struct FilePicker<T> { } pub enum CachedPreview { - Document(Document), + Document(Box<Document>), Binary, LargeFile, NotFound, @@ -139,8 +139,8 @@ impl<T> FilePicker<T> { (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile, _ => { // TODO: enable syntax highlighting; blocked by async rendering - Document::open(path, None, Some(&editor.theme), None) - .map(CachedPreview::Document) + Document::open(path, None, None) + .map(|doc| CachedPreview::Document(Box::new(doc))) .unwrap_or(CachedPreview::NotFound) } }, @@ -159,6 +159,7 @@ impl<T: 'static> Component for FilePicker<T> { // |picker | | | // | | | | // +---------+ +---------+ + let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let area = inner_rect(area); // -- Render the frame: @@ -220,13 +221,8 @@ impl<T: 'static> Component for FilePicker<T> { let offset = Position::new(first_line, 0); - let highlights = EditorView::doc_syntax_highlights( - doc, - offset, - area.height, - &cx.editor.theme, - &cx.editor.syn_loader, - ); + let highlights = + EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme); EditorView::render_text_highlights( doc, offset, @@ -397,6 +393,16 @@ fn inner_rect(area: Rect) -> Rect { } impl<T: 'static> Component for Picker<T> { + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let max_width = 50.min(viewport.0); + let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport + + let height = (self.options.len() as u16 + 4) // add some spacing for input + padding + .min(max_height); + let width = max_width; + Some((width, height)) + } + fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let key_event = match event { Event::Key(event) => event, @@ -404,13 +410,13 @@ impl<T: 'static> Component for Picker<T> { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.last_picker = compositor.pop(); }))); match key_event.into() { - shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => { + shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { self.move_up(); } key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => { @@ -492,10 +498,9 @@ impl<T: 'static> Component for Picker<T> { let sep_style = Style::default().fg(Color::Rgb(90, 89, 119)); let borders = BorderType::line_symbols(BorderType::Plain); for x in inner.left()..inner.right() { - surface - .get_mut(x, inner.y + 1) - .set_symbol(borders.horizontal) - .set_style(sep_style); + if let Some(cell) = surface.get_mut(x, inner.y + 1) { + cell.set_symbol(borders.horizontal).set_style(sep_style); + } } // -- Render the contents: @@ -505,7 +510,7 @@ impl<T: 'static> Component for Picker<T> { let selected = cx.editor.theme.get("ui.text.focus"); let rows = inner.height; - let offset = self.cursor / (rows as usize) * (rows as usize); + let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize)); let files = self.matches.iter().skip(offset).map(|(index, _score)| { (index, self.options.get(*index).unwrap()) // get_unchecked @@ -513,7 +518,7 @@ impl<T: 'static> Component for Picker<T> { for (i, (_index, option)) in files.take(rows as usize).enumerate() { if i == (self.cursor - offset) { - surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected); + surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected); } surface.set_string_truncated( diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 8f7921a1..4d319423 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -6,7 +6,7 @@ use crossterm::event::Event; use tui::buffer::Buffer as Surface; use helix_core::Position; -use helix_view::graphics::Rect; +use helix_view::graphics::{Margin, Rect}; // 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>) @@ -14,17 +14,26 @@ use helix_view::graphics::Rect; pub struct Popup<T: Component> { contents: T, position: Option<Position>, + margin: Margin, size: (u16, u16), + child_size: (u16, u16), scroll: usize, + id: &'static str, } impl<T: Component> Popup<T> { - pub fn new(contents: T) -> Self { + pub fn new(id: &'static str, contents: T) -> Self { Self { contents, position: None, + margin: Margin { + vertical: 0, + horizontal: 0, + }, size: (0, 0), + child_size: (0, 0), scroll: 0, + id, } } @@ -32,6 +41,11 @@ impl<T: Component> Popup<T> { self.position = pos; } + pub fn margin(mut self, margin: Margin) -> Self { + self.margin = margin; + self + } + pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { let position = self .position @@ -68,6 +82,9 @@ impl<T: Component> Popup<T> { pub fn scroll(&mut self, offset: usize, direction: bool) { if direction { self.scroll += offset; + + let max_offset = self.child_size.1.saturating_sub(self.size.1); + self.scroll = (self.scroll + offset).min(max_offset as usize); } else { self.scroll = self.scroll.saturating_sub(offset); } @@ -93,7 +110,7 @@ impl<T: Component> Component for Popup<T> { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); @@ -115,13 +132,26 @@ impl<T: Component> Component for Popup<T> { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } - fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let max_width = 120.min(viewport.0); + let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport + + let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin); + let (width, height) = self .contents - .required_size((120, 26)) // max width, max height + .required_size((inner.width, inner.height)) .expect("Component needs required_size implemented in order to be embedded in a popup"); - self.size = (width, height); + self.child_size = (width, height); + self.size = ( + (width + self.margin.horizontal * 2).min(max_width), + (height + self.margin.vertical * 2).min(max_height), + ); + + // re-clamp scroll offset + let max_offset = self.child_size.1.saturating_sub(self.size.1); + self.scroll = self.scroll.min(max_offset as usize); Some(self.size) } @@ -141,6 +171,11 @@ impl<T: Component> Component for Popup<T> { let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); - self.contents.render(area, surface, cx); + let inner = area.inner(&self.margin); + self.contents.render(inner, surface, cx); + } + + fn id(&self) -> Option<&'static str> { + Some(self.id) } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index e90b0772..4c4fef26 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -127,7 +127,7 @@ impl Prompt { let mut char_position = char_indices .iter() .position(|(idx, _)| *idx == self.cursor) - .unwrap_or_else(|| char_indices.len()); + .unwrap_or(char_indices.len()); for _ in 0..rep { // Skip any non-whitespace characters @@ -330,7 +330,7 @@ impl Prompt { .max(BASE_WIDTH); let cols = std::cmp::max(1, area.width / max_len); - let col_width = (area.width - (cols)) / cols; + let col_width = (area.width.saturating_sub(cols)) / cols; let height = ((self.completion.len() as u16 + cols - 1) / cols) .min(10) // at most 10 rows (or less) @@ -426,7 +426,7 @@ impl Component for Prompt { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); @@ -473,7 +473,7 @@ impl Component for Prompt { } } key!(Enter) => { - if self.selection.is_some() && self.line.ends_with('/') { + if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) { self.completion = (self.completion_fn)(&self.line); self.exit_selection(); } else { @@ -505,7 +505,7 @@ impl Component for Prompt { self.change_completion_selection(CompletionDirection::Forward); (self.callback_fn)(cx, &self.line, PromptEvent::Update) } - shift!(BackTab) => { + shift!(Tab) => { self.change_completion_selection(CompletionDirection::Backward); (self.callback_fn)(cx, &self.line, PromptEvent::Update) } diff --git a/helix-term/src/ui/spinner.rs b/helix-term/src/ui/spinner.rs index e8a43b48..68965469 100644 --- a/helix-term/src/ui/spinner.rs +++ b/helix-term/src/ui/spinner.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, time::SystemTime}; +use std::{collections::HashMap, time::Instant}; #[derive(Default, Debug)] pub struct ProgressSpinners { @@ -25,7 +25,7 @@ impl Default for Spinner { pub struct Spinner { frames: Vec<&'static str>, count: usize, - start: Option<SystemTime>, + start: Option<Instant>, interval: u64, } @@ -50,14 +50,13 @@ impl Spinner { } pub fn start(&mut self) { - self.start = Some(SystemTime::now()); + self.start = Some(Instant::now()); } pub fn frame(&self) -> Option<&str> { let idx = (self .start - .map(|time| SystemTime::now().duration_since(time))? - .ok()? + .map(|time| Instant::now().duration_since(time))? .as_millis() / self.interval as u128) as usize % self.count; diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 6df65d36..e4cfbe4c 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-tui" -version = "0.5.0" +version = "0.6.0" authors = ["Blaž Hrastnik <blaz@mxxn.io>"] description = """ A library to build rich terminal user interfaces or dashboards @@ -18,8 +18,8 @@ default = ["crossterm"] [dependencies] bitflags = "1.3" cassowary = "0.3" -unicode-segmentation = "1.8" -crossterm = { version = "0.22", optional = true } +unicode-segmentation = "1.9" +crossterm = { version = "0.23", optional = true } serde = { version = "1", "optional" = true, features = ["derive"]} -helix-view = { version = "0.5", path = "../helix-view", features = ["term"] } -helix-core = { version = "0.5", path = "../helix-core" } +helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } +helix-core = { version = "0.6", path = "../helix-core" } diff --git a/helix-tui/README.md b/helix-tui/README.md index 97b3d1d9..5cc80aa6 100644 --- a/helix-tui/README.md +++ b/helix-tui/README.md @@ -2,5 +2,5 @@ This library is a fork of the great library [tui-rs](https://github.com/fdehau/tui-rs/). We've mainly relied on the double -buffer implementation and render diffing, side-stepping it's widget and +buffer implementation and render diffing, side-stepping its widget and layouting. diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs index 3f56b49c..52474148 100644 --- a/helix-tui/src/backend/test.rs +++ b/helix-tui/src/backend/test.rs @@ -111,8 +111,7 @@ impl Backend for TestBackend { I: Iterator<Item = (u16, u16, &'a Cell)>, { for (x, y, c) in content { - let cell = self.buffer.get_mut(x, y); - *cell = c.clone(); + self.buffer[(x, y)] = c.clone(); } Ok(()) } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index f480bc2f..f8673e43 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -90,19 +90,19 @@ impl Default for Cell { /// 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"); -/// assert_eq!(buf.get(0, 2).symbol, "x"); +/// buf[(0, 2)].set_symbol("x"); +/// assert_eq!(buf[(0, 2)].symbol, "x"); /// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White)); -/// assert_eq!(buf.get(5, 0), &Cell{ +/// assert_eq!(buf[(5, 0)], Cell{ /// symbol: String::from("r"), /// fg: Color::Red, /// bg: Color::White, /// modifier: Modifier::empty() /// }); -/// buf.get_mut(5, 0).set_char('x'); -/// assert_eq!(buf.get(5, 0).symbol, "x"); +/// buf[(5, 0)].set_char('x'); +/// assert_eq!(buf[(5, 0)].symbol, "x"); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Buffer { /// The area represented by this buffer pub area: Rect, @@ -111,15 +111,6 @@ pub struct Buffer { pub content: Vec<Cell>, } -impl Default for Buffer { - fn default() -> Buffer { - Buffer { - area: Default::default(), - content: Vec::new(), - } - } -} - impl Buffer { /// Returns a Buffer with all cells set to the default one pub fn empty(area: Rect) -> Buffer { @@ -171,15 +162,38 @@ impl Buffer { } /// Returns a reference to Cell at the given coordinates - pub fn get(&self, x: u16, y: u16) -> &Cell { - let i = self.index_of(x, y); - &self.content[i] + pub fn get(&self, x: u16, y: u16) -> Option<&Cell> { + self.index_of_opt(x, y).map(|i| &self.content[i]) } /// Returns a mutable reference to Cell at the given coordinates - pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell { - let i = self.index_of(x, y); - &mut self.content[i] + pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> { + self.index_of_opt(x, y).map(|i| &mut self.content[i]) + } + + /// Tells whether the global (x, y) coordinates are inside the Buffer's area. + /// + /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). + /// + /// # Examples + /// + /// ``` + /// # use helix_tui::buffer::Buffer; + /// # use helix_view::graphics::Rect; + /// let rect = Rect::new(200, 100, 10, 10); + /// let buffer = Buffer::empty(rect); + /// // Global coordinates inside the Buffer's area + /// assert!(buffer.in_bounds(209, 100)); + /// // Global coordinates outside the Buffer's area + /// assert!(!buffer.in_bounds(210, 100)); + /// ``` + /// + /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). + pub fn in_bounds(&self, x: u16, y: u16) -> bool { + x >= self.area.left() + && x < self.area.right() + && y >= self.area.top() + && y < self.area.bottom() } /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates. @@ -193,7 +207,7 @@ impl Buffer { /// # 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 + /// // Global coordinates to the top corner of this Buffer's area /// assert_eq!(buffer.index_of(200, 100), 0); /// ``` /// @@ -202,10 +216,7 @@ impl Buffer { /// Panics when given an coordinate that is outside of this Buffer's area. pub fn index_of(&self, x: u16, y: u16) -> usize { debug_assert!( - x >= self.area.left() - && x < self.area.right() - && y >= self.area.top() - && y < self.area.bottom(), + self.in_bounds(x, y), "Trying to access position outside the buffer: x={}, y={}, area={:?}", x, y, @@ -214,6 +225,16 @@ impl Buffer { ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize } + /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates, + /// or `None` if the coordinates are outside the buffer's area. + fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> { + if self.in_bounds(x, y) { + Some(self.index_of(x, y)) + } else { + None + } + } + /// Returns the (global) coordinates of a cell given its index /// /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). @@ -287,6 +308,11 @@ impl Buffer { where S: AsRef<str>, { + // prevent panic if out of range + if !self.in_bounds(x, y) || width == 0 { + return (x, y); + } + let mut index = self.index_of(x, y); let mut x_offset = x as usize; let width = if ellipsis { width - 1 } else { width }; @@ -381,7 +407,7 @@ impl Buffer { pub fn set_background(&mut self, area: Rect, color: Color) { for y in area.top()..area.bottom() { for x in area.left()..area.right() { - self.get_mut(x, y).set_bg(color); + self[(x, y)].set_bg(color); } } } @@ -389,7 +415,7 @@ impl Buffer { pub fn set_style(&mut self, area: Rect, style: Style) { for y in area.top()..area.bottom() { for x in area.left()..area.right() { - self.get_mut(x, y).set_style(style); + self[(x, y)].set_style(style); } } } @@ -417,7 +443,7 @@ impl Buffer { pub fn clear(&mut self, area: Rect) { for x in area.left()..area.right() { for y in area.top()..area.bottom() { - self.get_mut(x, y).reset(); + self[(x, y)].reset(); } } } @@ -426,7 +452,7 @@ impl Buffer { pub fn clear_with(&mut self, area: Rect, style: Style) { for x in area.left()..area.right() { for y in area.top()..area.bottom() { - let cell = self.get_mut(x, y); + let cell = &mut self[(x, y)]; cell.reset(); cell.set_style(style); } @@ -509,15 +535,32 @@ impl Buffer { updates.push((x, y, &next_buffer[i])); } - to_skip = current.symbol.width().saturating_sub(1); + let current_width = current.symbol.width(); + to_skip = current_width.saturating_sub(1); - let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width()); + let affected_width = std::cmp::max(current_width, previous.symbol.width()); invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1); } updates } } +impl std::ops::Index<(u16, u16)> for Buffer { + type Output = Cell; + + fn index(&self, (x, y): (u16, u16)) -> &Self::Output { + let i = self.index_of(x, y); + &self.content[i] + } +} + +impl std::ops::IndexMut<(u16, u16)> for Buffer { + fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output { + let i = self.index_of(x, y); + &mut self.content[i] + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index b8e52479..8a974ddb 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -195,15 +195,9 @@ impl<'a> From<&'a str> for Span<'a> { } /// A string composed of clusters of graphemes, each with their own style. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Spans<'a>(pub Vec<Span<'a>>); -impl<'a> Default for Spans<'a> { - fn default() -> Spans<'a> { - Spans(Vec::new()) - } -} - impl<'a> Spans<'a> { /// Returns the width of the underlying string. /// @@ -280,17 +274,11 @@ impl<'a> From<Spans<'a>> for String { /// text.extend(Text::styled("Some more lines\nnow with more style!", style)); /// assert_eq!(6, text.height()); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Text<'a> { pub lines: Vec<Spans<'a>>, } -impl<'a> Default for Text<'a> { - fn default() -> Text<'a> { - Text { lines: Vec::new() } - } -} - impl<'a> Text<'a> { /// Create some text (potentially multiple lines) with no style. /// diff --git a/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs index 648c2d7e..26223c3e 100644 --- a/helix-tui/src/widgets/block.rs +++ b/helix-tui/src/widgets/block.rs @@ -15,12 +15,12 @@ pub enum BorderType { } impl BorderType { - pub fn line_symbols(border_type: BorderType) -> line::Set { + pub fn line_symbols(border_type: Self) -> line::Set { match border_type { - BorderType::Plain => line::NORMAL, - BorderType::Rounded => line::ROUNDED, - BorderType::Double => line::DOUBLE, - BorderType::Thick => line::THICK, + Self::Plain => line::NORMAL, + Self::Rounded => line::ROUNDED, + Self::Double => line::DOUBLE, + Self::Thick => line::THICK, } } } @@ -140,14 +140,14 @@ impl<'a> Widget for Block<'a> { // Sides if self.borders.intersects(Borders::LEFT) { for y in area.top()..area.bottom() { - buf.get_mut(area.left(), y) + buf[(area.left(), y)] .set_symbol(symbols.vertical) .set_style(self.border_style); } } if self.borders.intersects(Borders::TOP) { for x in area.left()..area.right() { - buf.get_mut(x, area.top()) + buf[(x, area.top())] .set_symbol(symbols.horizontal) .set_style(self.border_style); } @@ -155,7 +155,7 @@ impl<'a> Widget for Block<'a> { if self.borders.intersects(Borders::RIGHT) { let x = area.right() - 1; for y in area.top()..area.bottom() { - buf.get_mut(x, y) + buf[(x, y)] .set_symbol(symbols.vertical) .set_style(self.border_style); } @@ -163,7 +163,7 @@ impl<'a> Widget for Block<'a> { if self.borders.intersects(Borders::BOTTOM) { let y = area.bottom() - 1; for x in area.left()..area.right() { - buf.get_mut(x, y) + buf[(x, y)] .set_symbol(symbols.horizontal) .set_style(self.border_style); } @@ -171,22 +171,22 @@ impl<'a> Widget for Block<'a> { // Corners if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { - buf.get_mut(area.right() - 1, area.bottom() - 1) + buf[(area.right() - 1, area.bottom() - 1)] .set_symbol(symbols.bottom_right) .set_style(self.border_style); } if self.borders.contains(Borders::RIGHT | Borders::TOP) { - buf.get_mut(area.right() - 1, area.top()) + buf[(area.right() - 1, area.top())] .set_symbol(symbols.top_right) .set_style(self.border_style); } if self.borders.contains(Borders::LEFT | Borders::BOTTOM) { - buf.get_mut(area.left(), area.bottom() - 1) + buf[(area.left(), area.bottom() - 1)] .set_symbol(symbols.bottom_left) .set_style(self.border_style); } if self.borders.contains(Borders::LEFT | Borders::TOP) { - buf.get_mut(area.left(), area.top()) + buf[(area.left(), area.top())] .set_symbol(symbols.top_left) .set_style(self.border_style); } diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs index fee35d25..4e839162 100644 --- a/helix-tui/src/widgets/paragraph.rs +++ b/helix-tui/src/widgets/paragraph.rs @@ -166,7 +166,7 @@ impl<'a> Widget for Paragraph<'a> { Box::new(WordWrapper::new(&mut styled, text_area.width, trim)) } else { let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width)); - if let Alignment::Left = self.alignment { + if self.alignment == Alignment::Left { line_composer.set_horizontal_offset(self.scroll.1); } line_composer @@ -176,7 +176,7 @@ impl<'a> Widget for Paragraph<'a> { if y >= self.scroll.0 { let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); for StyledGrapheme { symbol, style } in current_line { - buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0) + buf[(text_area.left() + x, text_area.top() + y - self.scroll.0)] .set_symbol(if symbol.is_empty() { // If the symbol is empty, the last char which rendered last time will // leave on the line. It's a quick fix. diff --git a/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs index 21847783..33e52bb4 100644 --- a/helix-tui/src/widgets/reflow.rs +++ b/helix-tui/src/widgets/reflow.rs @@ -404,8 +404,8 @@ mod test { let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\ では、"; let (word_wrapper, word_wrapper_width) = - run_composer(Composer::WordWrapper { trim: true }, &text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width); + run_composer(Composer::WordWrapper { trim: true }, text, width); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); assert_eq!(line_truncator, vec!["コンピュータ上で文字"]); let wrapped = vec![ "コンピュータ上で文字", diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs index d7caa0b0..6aee5988 100644 --- a/helix-tui/src/widgets/table.rs +++ b/helix-tui/src/widgets/table.rs @@ -363,21 +363,12 @@ impl<'a> Table<'a> { } } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct TableState { pub offset: usize, pub selected: Option<usize>, } -impl Default for TableState { - fn default() -> TableState { - TableState { - offset: 0, - selected: None, - } - } -} - impl TableState { pub fn selected(&self) -> Option<usize> { self.selected diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index ffe6a111..932c3321 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-view" -version = "0.5.0" +version = "0.6.0" authors = ["Blaž Hrastnik <blaz@mxxn.io>"] edition = "2021" license = "MPL-2.0" @@ -16,13 +16,13 @@ term = ["crossterm"] [dependencies] bitflags = "1.3" anyhow = "1" -helix-core = { version = "0.5", path = "../helix-core" } -helix-lsp = { version = "0.5", path = "../helix-lsp"} -helix-dap = { version = "0.5", path = "../helix-dap"} -crossterm = { version = "0.22", optional = true } +helix-core = { version = "0.6", path = "../helix-core" } +helix-lsp = { version = "0.6", path = "../helix-lsp"} +helix-dap = { version = "0.6", path = "../helix-dap"} +crossterm = { version = "0.23", optional = true } # Conversion traits -once_cell = "1.8" +once_cell = "1.9" url = "2" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } @@ -31,7 +31,6 @@ futures-util = { version = "0.3", features = ["std", "async-await"], default-fea slotmap = "1" -encoding_rs = "0.8" chardetng = "0.1" serde = { version = "1.0", features = ["derive"] } @@ -41,7 +40,7 @@ log = "~0.4" which = "4.2" [target.'cfg(windows)'.dependencies] -clipboard-win = { version = "4.2", features = ["std"] } +clipboard-win = { version = "4.4", features = ["std"] } [dev-dependencies] helix-tui = { path = "../helix-tui" } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 76b19a07..c0186ee5 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,5 +1,6 @@ -use anyhow::{anyhow, Context, Error}; +use anyhow::{anyhow, bail, Context, Error}; use serde::de::{self, Deserialize, Deserializer}; +use serde::Serialize; use std::cell::Cell; use std::collections::HashMap; use std::fmt::Display; @@ -9,7 +10,8 @@ use std::str::FromStr; use std::sync::Arc; use helix_core::{ - history::History, + encoding, + history::{History, UndoKind}, indent::{auto_detect_indent_style, IndentStyle}, line_ending::auto_detect_line_ending, syntax::{self, LanguageConfiguration}, @@ -18,7 +20,7 @@ use helix_core::{ }; use helix_lsp::util::LspFormatting; -use crate::{DocumentId, Theme, ViewId}; +use crate::{DocumentId, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; @@ -29,9 +31,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { - Normal, - Select, - Insert, + Normal = 0, + Select = 1, + Insert = 2, } impl Display for Mode { @@ -52,7 +54,7 @@ impl FromStr for Mode { "normal" => Ok(Mode::Normal), "select" => Ok(Mode::Select), "insert" => Ok(Mode::Insert), - _ => Err(anyhow!("Invalid mode '{}'", s)), + _ => bail!("Invalid mode '{}'", s), } } } @@ -68,13 +70,22 @@ impl<'de> Deserialize<'de> for Mode { } } +impl Serialize for Mode { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + pub struct Document { pub(crate) id: DocumentId, text: Rope, pub(crate) selections: HashMap<ViewId, Selection>, path: Option<PathBuf>, - encoding: &'static encoding_rs::Encoding, + encoding: &'static encoding::Encoding, /// Current editing mode. pub mode: Mode, @@ -104,6 +115,7 @@ pub struct Document { last_saved_revision: usize, version: i32, // should be usize? + pub(crate) modified_since_accessed: bool, diagnostics: Vec<Diagnostic>, language_server: Option<Arc<helix_lsp::Client>>, @@ -127,6 +139,7 @@ impl fmt::Debug for Document { // .field("history", &self.history) .field("last_saved_revision", &self.last_saved_revision) .field("version", &self.version) + .field("modified_since_accessed", &self.modified_since_accessed) .field("diagnostics", &self.diagnostics) // .field("language_server", &self.language_server) .finish() @@ -141,8 +154,8 @@ impl fmt::Debug for Document { /// 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> { + encoding: Option<&'static encoding::Encoding>, +) -> Result<(Rope, &'static encoding::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 @@ -210,11 +223,11 @@ pub fn from_reader<R: std::io::Read + ?Sized>( total_read += read; total_written += written; match result { - encoding_rs::CoderResult::InputEmpty => { + encoding::CoderResult::InputEmpty => { debug_assert_eq!(slice.len(), total_read); break; } - encoding_rs::CoderResult::OutputFull => { + encoding::CoderResult::OutputFull => { debug_assert!(slice.len() > total_read); builder.append(&buf_str[..total_written]); total_written = 0; @@ -249,7 +262,7 @@ pub fn from_reader<R: std::io::Read + ?Sized>( /// 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, + encoding: &'static encoding::Encoding, rope: &'a Rope, ) -> Result<(), Error> { // Text inside a `Rope` is stored as non-contiguous blocks of data called @@ -284,12 +297,12 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>( total_read += read; total_written += written; match result { - encoding_rs::CoderResult::InputEmpty => { + encoding::CoderResult::InputEmpty => { debug_assert_eq!(chunk.len(), total_read); debug_assert!(buf.len() >= total_written); break; } - encoding_rs::CoderResult::OutputFull => { + encoding::CoderResult::OutputFull => { debug_assert!(chunk.len() > total_read); writer.write_all(&buf[..total_written]).await?; total_written = 0; @@ -320,8 +333,8 @@ use helix_lsp::lsp; use url::Url; impl Document { - pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self { - let encoding = encoding.unwrap_or(encoding_rs::UTF_8); + pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self { + let encoding = encoding.unwrap_or(encoding::UTF_8); let changes = ChangeSet::new(&text); let old_state = None; @@ -344,6 +357,7 @@ impl Document { history: Cell::new(History::default()), savepoint: None, last_saved_revision: 0, + modified_since_accessed: false, language_server: None, } } @@ -353,9 +367,8 @@ impl Document { /// overwritten with the `encoding` parameter. pub fn open( path: &Path, - encoding: Option<&'static encoding_rs::Encoding>, - theme: Option<&Theme>, - config_loader: Option<&syntax::Loader>, + encoding: Option<&'static encoding::Encoding>, + config_loader: Option<Arc<syntax::Loader>>, ) -> Result<Self, Error> { // Open the file if it exists, otherwise assume it is a new file (and thus empty). let (rope, encoding) = if path.exists() { @@ -363,7 +376,7 @@ impl Document { std::fs::File::open(path).context(format!("unable to open {:?}", path))?; from_reader(&mut file, encoding)? } else { - let encoding = encoding.unwrap_or(encoding_rs::UTF_8); + let encoding = encoding.unwrap_or(encoding::UTF_8); (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding) }; @@ -372,7 +385,7 @@ impl Document { // set the path and try detecting the language doc.set_path(Some(path))?; if let Some(loader) = config_loader { - doc.detect_language(theme, loader); + doc.detect_language(loader); } doc.detect_indent_and_line_ending(); @@ -383,7 +396,7 @@ impl Document { /// The same as [`format`], but only returns formatting changes if auto-formatting /// is configured. pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { - if self.language_config().map(|c| c.auto_format) == Some(true) { + if self.language_config()?.auto_format { self.format() } else { None @@ -393,30 +406,27 @@ impl Document { /// If supported, returns the changes that should be applied to this document in order /// to format it nicely. pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { - if let Some(language_server) = self.language_server() { - let text = self.text.clone(); - let offset_encoding = language_server.offset_encoding(); - let request = language_server.text_document_formatting( - self.identifier(), - lsp::FormattingOptions::default(), - None, - )?; - - let fut = async move { - let edits = request.await.unwrap_or_else(|e| { - log::warn!("LSP formatting failed: {}", e); - Default::default() - }); - LspFormatting { - doc: text, - edits, - offset_encoding, - } - }; - Some(fut) - } else { - None - } + let language_server = self.language_server()?; + let text = self.text.clone(); + let offset_encoding = language_server.offset_encoding(); + let request = language_server.text_document_formatting( + self.identifier(), + lsp::FormattingOptions::default(), + None, + )?; + + let fut = async move { + let edits = request.await.unwrap_or_else(|e| { + log::warn!("LSP formatting failed: {}", e); + Default::default() + }); + LspFormatting { + doc: text, + edits, + offset_encoding, + } + }; + Some(fut) } pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> { @@ -460,9 +470,7 @@ impl Document { if let Some(parent) = path.parent() { // TODO: display a prompt asking the user if the directories should be created if !parent.exists() { - return Err(Error::msg( - "can't save file, parent directory does not exist", - )); + bail!("can't save file, parent directory does not exist"); } } @@ -494,12 +502,12 @@ impl Document { } /// Detect the programming language based on the file type. - pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) { + pub fn detect_language(&mut self, config_loader: Arc<syntax::Loader>) { if let Some(path) = &self.path { let language_config = config_loader .language_config_for_file_name(path) .or_else(|| config_loader.language_config_for_shebang(self.text())); - self.set_language(theme, language_config); + self.set_language(language_config, Some(config_loader)); } } @@ -509,8 +517,7 @@ impl Document { /// line ending. pub fn detect_indent_and_line_ending(&mut self) { self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| { - self.language - .as_ref() + self.language_config() .and_then(|config| config.indent.as_ref()) .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit)) }); @@ -524,7 +531,7 @@ impl Document { // If there is no path or the path no longer exists. if path.is_none() { - return Err(anyhow!("can't find file to reload from")); + bail!("can't find file to reload from"); } let mut file = std::fs::File::open(path.unwrap())?; @@ -545,15 +552,13 @@ impl Document { /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`. pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> { - match encoding_rs::Encoding::for_label(label.as_bytes()) { - Some(encoding) => self.encoding = encoding, - None => return Err(anyhow::anyhow!("unknown encoding")), - } + self.encoding = encoding::Encoding::for_label(label.as_bytes()) + .ok_or_else(|| anyhow!("unknown encoding"))?; Ok(()) } /// Returns the [`Document`]'s current encoding. - pub fn encoding(&self) -> &'static encoding_rs::Encoding { + pub fn encoding(&self) -> &'static encoding::Encoding { self.encoding } @@ -573,15 +578,13 @@ impl Document { /// if it exists. pub fn set_language( &mut self, - theme: Option<&Theme>, language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>, + loader: Option<Arc<helix_core::syntax::Loader>>, ) { - 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); + if let (Some(language_config), Some(loader)) = (language_config, loader) { + if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) { + let syntax = Syntax::new(&self.text, highlight_config, loader); self.syntax = Some(syntax); - // TODO: config.configure(scopes) is now delayed, is that ok? } self.language = Some(language_config); @@ -593,15 +596,10 @@ impl Document { /// Set the programming language for the file if you know the name (scope) but don't have the /// [`syntax::LanguageConfiguration`] for it. - pub fn set_language2( - &mut self, - scope: &str, - theme: Option<&Theme>, - config_loader: Arc<syntax::Loader>, - ) { + pub fn set_language2(&mut self, scope: &str, config_loader: Arc<syntax::Loader>) { let language_config = config_loader.language_config_for_scope(scope); - self.set_language(theme, language_config); + self.set_language(language_config, Some(config_loader)); } /// Set the LSP. @@ -639,6 +637,8 @@ impl Document { selection.clone().ensure_invariants(self.text.slice(..)), ); } + + self.modified_since_accessed = true; } if !transaction.changes().is_empty() { @@ -680,7 +680,7 @@ impl Document { if let Some(notify) = notify { tokio::spawn(notify); - } //.expect("failed to emit textDocument/didChange"); + } } } success @@ -708,11 +708,11 @@ impl Document { success } - /// Undo the last modification to the [`Document`]. Returns whether the undo was successful. - pub fn undo(&mut self, view_id: ViewId) -> bool { + fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool { let mut history = self.history.take(); - let success = if let Some(transaction) = history.undo() { - self.apply_impl(transaction, view_id) + let txn = if undo { history.undo() } else { history.redo() }; + let success = if let Some(txn) = txn { + self.apply_impl(txn, view_id) } else { false }; @@ -725,21 +725,14 @@ impl Document { success } + /// Undo the last modification to the [`Document`]. Returns whether the undo was successful. + pub fn undo(&mut self, view_id: ViewId) -> bool { + self.undo_redo_impl(view_id, true) + } + /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful. pub fn redo(&mut self, view_id: ViewId) -> bool { - let mut history = self.history.take(); - let success = if let Some(transaction) = history.redo() { - self.apply_impl(transaction, view_id) - } else { - false - }; - self.history.set(history); - - if success { - // reset changeset to fix len - self.changes = ChangeSet::new(self.text()); - } - success + self.undo_redo_impl(view_id, false) } pub fn savepoint(&mut self) { @@ -752,9 +745,12 @@ impl Document { } } - /// Undo modifications to the [`Document`] according to `uk`. - pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { - let txns = self.history.get_mut().earlier(uk); + fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool { + let txns = if earlier { + self.history.get_mut().earlier(uk) + } else { + self.history.get_mut().later(uk) + }; let mut success = false; for txn in txns { if self.apply_impl(&txn, view_id) { @@ -768,20 +764,14 @@ impl Document { success } + /// Undo modifications to the [`Document`] according to `uk`. + pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool { + self.earlier_later_impl(view_id, uk, true) + } + /// Redo modifications to the [`Document`] according to `uk`. - pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { - let txns = self.history.get_mut().later(uk); - let mut success = false; - for txn in txns { - if self.apply_impl(&txn, view_id) { - success = true; - } - } - if success { - // reset changeset to fix len - self.changes = ChangeSet::new(self.text()); - } - success + pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool { + self.earlier_later_impl(view_id, uk, false) } /// Commit pending changes to history @@ -837,6 +827,16 @@ impl Document { .map(|language| language.scope.as_str()) } + /// Language ID for the document. Either the `language-id` from the + /// `language-server` configuration, or the document language if no + /// `language-id` has been specified. + pub fn language_id(&self) -> Option<&str> { + self.language_config() + .and_then(|config| config.language_server.as_ref()) + .and_then(|lsp_config| lsp_config.language_id.as_deref()) + .or_else(|| Some(self.language()?.rsplit_once('.')?.1)) + } + /// Corresponding [`LanguageConfiguration`]. pub fn language_config(&self) -> Option<&LanguageConfiguration> { self.language.as_deref() @@ -847,18 +847,10 @@ impl Document { self.version } + /// Language server if it has been initialized. pub fn language_server(&self) -> Option<&helix_lsp::Client> { - let server = self.language_server.as_deref(); - let initialized = server - .map(|server| server.is_initialized()) - .unwrap_or(false); - - // only resolve language_server if it's initialized - if initialized { - server - } else { - None - } + let server = self.language_server.as_deref()?; + server.is_initialized().then(|| server) } #[inline] @@ -869,8 +861,7 @@ impl Document { /// Tab size in columns. pub fn tab_width(&self) -> usize { - self.language - .as_ref() + self.language_config() .and_then(|config| config.indent.as_ref()) .map_or(4, |config| config.tab_width) // fallback to 4 columns } @@ -883,6 +874,10 @@ impl Document { self.indent_style.as_str() } + pub fn changes(&self) -> &ChangeSet { + &self.changes + } + #[inline] /// File path on disk. pub fn path(&self) -> Option<&PathBuf> { @@ -891,7 +886,7 @@ impl Document { /// File path as a URL. pub fn url(&self) -> Option<Url> { - self.path().map(|path| Url::from_file_path(path).unwrap()) + Url::from_file_path(self.path()?).ok() } #[inline] @@ -914,10 +909,6 @@ impl Document { .map(helix_core::path::get_relative_path) } - // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds { - // self.state.doc.slice - // } - // transact(Fn) ? // -- LSP methods @@ -938,7 +929,6 @@ impl Document { pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) { self.diagnostics = diagnostics; - // sort by range self.diagnostics .sort_unstable_by_key(|diagnostic| diagnostic.range); } @@ -1113,7 +1103,7 @@ mod test { macro_rules! test_decode { ($label:expr, $label_override:expr) => { - let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap(); + let encoding = encoding::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)); @@ -1132,7 +1122,7 @@ mod test { macro_rules! test_encode { ($label:expr, $label_override:expr) => { - let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap(); + let encoding = encoding::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)); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c7b3baef..2e6121bc 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,7 +1,9 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, - document::SCRATCH_BUFFER_NAME, + document::{Mode, SCRATCH_BUFFER_NAME}, graphics::{CursorKind, Rect}, + info::Info, + input::KeyEvent, theme::{self, Theme}, tree::{self, Tree}, Document, DocumentId, View, ViewId, @@ -22,7 +24,7 @@ use std::{ use tokio::time::{sleep, Duration, Instant, Sleep}; -use anyhow::{bail, Context, Error}; +use anyhow::{bail, Error}; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; @@ -30,7 +32,7 @@ use helix_core::syntax; use helix_core::{Position, Selection}; use helix_dap as dap; -use serde::Deserialize; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize}; fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error> where @@ -40,7 +42,7 @@ where Ok(Duration::from_millis(millis)) } -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct FilePickerConfig { /// IgnoreOptions @@ -80,7 +82,7 @@ impl Default for FilePickerConfig { } } -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. @@ -95,8 +97,6 @@ pub struct Config { pub line_number: LineNumber, /// Middle click paste support. Defaults to true. pub middle_click_paste: bool, - /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. - pub smart_case: bool, /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true. pub auto_pairs: bool, /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true. @@ -108,18 +108,101 @@ pub struct Config { /// Whether to display infoboxes. Defaults to true. pub auto_info: bool, pub file_picker: FilePickerConfig, + /// Shape for cursor in each mode + pub cursor_shape: CursorShapeConfig, + /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`. + pub true_color: bool, + /// Search configuration. + #[serde(default)] + pub search: SearchConfig, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +pub struct SearchConfig { + /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. + pub smart_case: bool, + /// Whether the search should wrap after depleting the matches. Default to true. + pub wrap_around: bool, +} + +// Cursor shape is read and used on every rendered frame and so needs +// to be fast. Therefore we avoid a hashmap and use an enum indexed array. +#[derive(Debug, Clone, PartialEq)] +pub struct CursorShapeConfig([CursorKind; 3]); + +impl CursorShapeConfig { + pub fn from_mode(&self, mode: Mode) -> CursorKind { + self.get(mode as usize).copied().unwrap_or_default() + } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] +impl<'de> Deserialize<'de> for CursorShapeConfig { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?; + let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default(); + Ok(CursorShapeConfig([ + into_cursor(Mode::Normal), + into_cursor(Mode::Select), + into_cursor(Mode::Insert), + ])) + } +} + +impl Serialize for CursorShapeConfig { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(self.len()))?; + let modes = [Mode::Normal, Mode::Select, Mode::Insert]; + for mode in modes { + map.serialize_entry(&mode, &self.from_mode(mode))?; + } + map.end() + } +} + +impl std::ops::Deref for CursorShapeConfig { + type Target = [CursorKind; 3]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for CursorShapeConfig { + fn default() -> Self { + Self([CursorKind::Block; 3]) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum LineNumber { /// Show absolute line number Absolute, - /// Show relative line number to the primary cursor + /// If focused and in normal/select mode, show relative line number to the primary cursor. + /// If unfocused or in insert mode, show absolute line number. Relative, } +impl std::str::FromStr for LineNumber { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "absolute" | "abs" => Ok(Self::Absolute), + "relative" | "rel" => Ok(Self::Relative), + _ => anyhow::bail!("Line number can only be `absolute` or `relative`."), + } + } +} + impl Default for Config { fn default() -> Self { Self { @@ -133,13 +216,24 @@ impl Default for Config { }, line_number: LineNumber::Absolute, middle_click_paste: true, - smart_case: true, auto_pairs: true, auto_completion: true, idle_timeout: Duration::from_millis(400), completion_trigger_len: 2, auto_info: true, file_picker: FilePickerConfig::default(), + cursor_shape: CursorShapeConfig::default(), + true_color: false, + search: SearchConfig::default(), + } + } +} + +impl Default for SearchConfig { + fn default() -> Self { + Self { + wrap_around: true, + smart_case: true, } } } @@ -177,6 +271,7 @@ pub struct Editor { pub count: Option<std::num::NonZeroUsize>, pub selected_register: Option<char>, pub registers: Registers, + pub macro_recording: Option<(char, Vec<KeyEvent>)>, pub theme: Theme, pub language_servers: helix_lsp::Registry, @@ -190,6 +285,7 @@ pub struct Editor { pub theme_loader: Arc<theme::Loader>, pub status_msg: Option<(String, Severity)>, + pub autoinfo: Option<Info>, pub config: Config, @@ -225,6 +321,7 @@ impl Editor { documents: BTreeMap::new(), count: None, selected_register: None, + macro_recording: None, theme: theme_loader.default(), language_servers, debugger: None, @@ -235,6 +332,7 @@ impl Editor { registers: Registers::default(), clipboard_provider: get_clipboard_provider(), status_msg: None, + autoinfo: None, idle_timer: Box::pin(sleep(config.idle_timeout)), last_motion: None, config, @@ -275,31 +373,16 @@ impl Editor { } let scopes = theme.scopes(); - for config in self - .syn_loader - .language_configs_iter() - .filter(|cfg| cfg.is_highlight_initialized()) - { - config.reconfigure(scopes); - } + self.syn_loader.set_scopes(scopes.to_vec()); self.theme = theme; self._refresh(); } - pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> { - let theme = self - .theme_loader - .load(theme.as_ref()) - .with_context(|| format!("failed setting theme `{}`", theme))?; - self.set_theme(theme); - Ok(()) - } - /// Refreshes the language server for a given document pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { let doc = self.documents.get_mut(&doc_id)?; - doc.detect_language(Some(&self.theme), &self.syn_loader); + doc.detect_language(self.syn_loader.clone()); Self::launch_language_server(&mut self.language_servers, doc) } @@ -323,11 +406,8 @@ impl Editor { if let Some(language_server) = doc.language_server() { tokio::spawn(language_server.text_document_did_close(doc.identifier())); } - let language_id = doc - .language() - .and_then(|s| s.split('.').last()) // source.rust - .map(ToOwned::to_owned) - .unwrap_or_default(); + + let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); // TODO: this now races with on_init code if the init happens too quickly tokio::spawn(language_server.text_document_did_open( @@ -394,7 +474,8 @@ impl Editor { .tree .traverse() .any(|(_, v)| v.doc == doc.id && v.id != view.id); - let view = view_mut!(self); + + let (view, doc) = current!(self); if remove_empty_scratch { // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable // borrow, invalidating direct access to `doc.id`. @@ -403,7 +484,16 @@ impl Editor { } else { let jump = (view.doc, doc.selection(view.id).clone()); view.jumps.push(jump); - view.last_accessed_doc = Some(view.doc); + // Set last accessed doc if it is a different document + if doc.id != id { + view.last_accessed_doc = Some(view.doc); + // Set last modified doc if modified and last modified doc is different + if std::mem::take(&mut doc.modified_since_accessed) + && view.last_modified_docs[0] != Some(view.doc) + { + view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]]; + } + } } let view_id = view.id; @@ -471,7 +561,7 @@ impl Editor { let id = if let Some(id) = id { id } else { - let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?; + let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?; let _ = Self::launch_language_server(&mut self.language_servers, &mut doc); @@ -629,9 +719,10 @@ impl Editor { let inner = view.inner_area(); pos.col += inner.x as usize; pos.row += inner.y as usize; - (Some(pos), CursorKind::Hidden) + let cursorkind = self.config.cursor_shape.from_mode(doc.mode()); + (Some(pos), cursorkind) } else { - (None, CursorKind::Hidden) + (None, CursorKind::default()) } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 0bfca04a..6d0a9292 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -1,10 +1,12 @@ use bitflags::bitflags;
+use serde::{Deserialize, Serialize};
use std::{
cmp::{max, min},
str::FromStr,
};
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
/// UNSTABLE
pub enum CursorKind {
/// █
@@ -17,6 +19,12 @@ pub enum CursorKind { Hidden,
}
+impl Default for CursorKind {
+ fn default() -> Self {
+ Self::Block
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,
@@ -25,7 +33,7 @@ pub struct Margin { /// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
@@ -33,17 +41,6 @@ pub struct Rect { pub height: u16,
}
-impl Default for Rect {
- fn default() -> Rect {
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- }
- }
-}
-
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
@@ -334,7 +331,7 @@ impl FromStr for Modifier { /// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
-/// buffer.get_mut(0, 0).set_style(*style);
+/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -343,7 +340,7 @@ impl FromStr for Modifier { /// add_modifier: Modifier::BOLD,
/// sub_modifier: Modifier::empty(),
/// },
-/// buffer.get(0, 0).style(),
+/// buffer[(0, 0)].style(),
/// );
/// ```
///
@@ -359,7 +356,7 @@ impl FromStr for Modifier { /// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
-/// buffer.get_mut(0, 0).set_style(*style);
+/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -368,7 +365,7 @@ impl FromStr for Modifier { /// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
-/// buffer.get(0, 0).style(),
+/// buffer[(0, 0)].style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index e156b9e5..6a77c41f 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -25,7 +25,8 @@ pub fn diagnostic<'doc>( Box::new(move |line: usize, _selected: bool, out: &mut String| { use helix_core::diagnostic::Severity; - if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) { + if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) { + let diagnostic = &diagnostics[index]; write!(out, "●").unwrap(); return Some(match diagnostic.severity { Some(Severity::Error) => error, @@ -60,29 +61,31 @@ pub fn line_number<'doc>( .char_to_line(doc.selection(view.id).primary().cursor(text)); let config = editor.config.line_number; + let mode = doc.mode; Box::new(move |line: usize, selected: bool, out: &mut String| { if line == last_line && !draw_last { write!(out, "{:>1$}", '~', width).unwrap(); Some(linenr) } else { - use crate::editor::LineNumber; - let line = match config { - LineNumber::Absolute => line + 1, - LineNumber::Relative => { - if current_line == line { - line + 1 - } else { - abs_diff(current_line, line) - } - } + use crate::{document::Mode, editor::LineNumber}; + + let relative = config == LineNumber::Relative + && mode != Mode::Insert + && is_focused + && current_line != line; + + let display_num = if relative { + abs_diff(current_line, line) + } else { + line + 1 }; let style = if selected && is_focused { linenr_select } else { linenr }; - write!(out, "{:>1$}", line, width).unwrap(); + write!(out, "{:>1$}", display_num, width).unwrap(); Some(style) } }) diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index b5a002fa..5ad6a60c 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -1,5 +1,5 @@ use crate::input::KeyEvent; -use helix_core::unicode::width::UnicodeWidthStr; +use helix_core::{register::Registers, unicode::width::UnicodeWidthStr}; use std::{collections::BTreeSet, fmt::Write}; #[derive(Debug)] @@ -16,33 +16,60 @@ pub struct Info { } impl Info { - pub fn new(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Info { - let body = body - .into_iter() - .map(|(desc, events)| { - let events = events.iter().map(ToString::to_string).collect::<Vec<_>>(); - (desc, events.join(", ")) - }) - .collect::<Vec<_>>(); + pub fn new(title: &str, body: Vec<(String, String)>) -> Self { + if body.is_empty() { + return Self { + title: title.to_string(), + height: 1, + width: title.len() as u16, + text: "".to_string(), + }; + } - let keymaps_width = body.iter().map(|r| r.1.len()).max().unwrap(); + let item_width = body.iter().map(|(item, _)| item.width()).max().unwrap(); let mut text = String::new(); - for (desc, keyevents) in &body { - let _ = writeln!( - text, - "{:width$} {}", - keyevents, - desc, - width = keymaps_width - ); + for (item, desc) in &body { + let _ = writeln!(text, "{:width$} {}", item, desc, width = item_width); } - Info { + Self { title: title.to_string(), width: text.lines().map(|l| l.width()).max().unwrap() as u16, height: body.len() as u16, text, } } + + pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Self { + let body = body + .into_iter() + .map(|(desc, events)| { + let events = events.iter().map(ToString::to_string).collect::<Vec<_>>(); + (events.join(", "), desc.to_string()) + }) + .collect(); + + Self::new(title, body) + } + + pub fn from_registers(registers: &Registers) -> Self { + let body = registers + .inner() + .iter() + .map(|(ch, reg)| { + let content = reg + .read() + .get(0) + .and_then(|s| s.lines().next()) + .map(String::from) + .unwrap_or_default(); + (ch.to_string(), content) + }) + .collect(); + + let mut infobox = Self::new("Registers", body); + infobox.width = 30; // copied content could be very long + infobox + } } diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 580204cc..14dadc3b 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -36,7 +36,6 @@ pub(crate) mod keys { pub(crate) const PAGEUP: &str = "pageup"; pub(crate) const PAGEDOWN: &str = "pagedown"; pub(crate) const TAB: &str = "tab"; - pub(crate) const BACKTAB: &str = "backtab"; pub(crate) const DELETE: &str = "del"; pub(crate) const INSERT: &str = "ins"; pub(crate) const NULL: &str = "null"; @@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent { KeyCode::PageUp => f.write_str(keys::PAGEUP)?, KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?, KeyCode::Tab => f.write_str(keys::TAB)?, - KeyCode::BackTab => f.write_str(keys::BACKTAB)?, KeyCode::Delete => f.write_str(keys::DELETE)?, KeyCode::Insert => f.write_str(keys::INSERT)?, KeyCode::Null => f.write_str(keys::NULL)?, @@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent { KeyCode::PageUp => keys::PAGEUP.len(), KeyCode::PageDown => keys::PAGEDOWN.len(), KeyCode::Tab => keys::TAB.len(), - KeyCode::BackTab => keys::BACKTAB.len(), KeyCode::Delete => keys::DELETE.len(), KeyCode::Insert => keys::INSERT.len(), KeyCode::Null => keys::NULL.len(), @@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent { keys::PAGEUP => KeyCode::PageUp, keys::PAGEDOWN => KeyCode::PageDown, keys::TAB => KeyCode::Tab, - keys::BACKTAB => KeyCode::BackTab, keys::DELETE => KeyCode::Delete, keys::INSERT => KeyCode::Insert, keys::NULL => KeyCode::Null, @@ -220,14 +216,79 @@ impl<'de> Deserialize<'de> for KeyEvent { #[cfg(feature = "term")] impl From<crossterm::event::KeyEvent> for KeyEvent { - fn from( - crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent, - ) -> KeyEvent { - KeyEvent { - code: code.into(), - modifiers: modifiers.into(), + fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self { + if code == crossterm::event::KeyCode::BackTab { + // special case for BackTab -> Shift-Tab + let mut modifiers: KeyModifiers = modifiers.into(); + modifiers.insert(KeyModifiers::SHIFT); + Self { + code: KeyCode::Tab, + modifiers, + } + } else { + Self { + code: code.into(), + modifiers: modifiers.into(), + } + } + } +} + +#[cfg(feature = "term")] +impl From<KeyEvent> for crossterm::event::KeyEvent { + fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self { + if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) { + // special case for Shift-Tab -> BackTab + let mut modifiers = modifiers; + modifiers.remove(KeyModifiers::SHIFT); + crossterm::event::KeyEvent { + code: crossterm::event::KeyCode::BackTab, + modifiers: modifiers.into(), + } + } else { + crossterm::event::KeyEvent { + code: code.into(), + modifiers: modifiers.into(), + } + } + } +} + +pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> { + use anyhow::Context; + let mut keys_res: anyhow::Result<_> = Ok(Vec::new()); + let mut i = 0; + while let Ok(keys) = &mut keys_res { + if i >= keys_str.len() { + break; + } + if !keys_str.is_char_boundary(i) { + i += 1; + continue; + } + + let s = &keys_str[i..]; + let mut end_i = 1; + while !s.is_char_boundary(end_i) { + end_i += 1; + } + let c = &s[..end_i]; + if c == ">" { + keys_res = Err(anyhow!("Unmatched '>'")); + } else if c != "<" { + keys.push(c); + i += end_i; + } else { + match s.find('>').context("'>' expected") { + Ok(end_i) => { + keys.push(&s[1..end_i]); + i += end_i + 1; + } + Err(err) => keys_res = Err(err), + } } } + keys_res.and_then(|keys| keys.into_iter().map(str::parse).collect()) } #[cfg(test)] @@ -315,4 +376,120 @@ mod test { assert!(str::parse::<KeyEvent>("123").is_err()); assert!(str::parse::<KeyEvent>("S--").is_err()); } + + #[test] + fn parsing_valid_macros() { + assert_eq!( + parse_macro("xdo").ok(), + Some(vec![ + KeyEvent { + code: KeyCode::Char('x'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('o'), + modifiers: KeyModifiers::NONE, + }, + ]), + ); + + assert_eq!( + parse_macro("<C-w>v<C-w>h<C-o>xx<A-s>").ok(), + Some(vec![ + KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::CONTROL, + }, + KeyEvent { + code: KeyCode::Char('v'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::CONTROL, + }, + KeyEvent { + code: KeyCode::Char('h'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('o'), + modifiers: KeyModifiers::CONTROL, + }, + KeyEvent { + code: KeyCode::Char('x'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('x'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('s'), + modifiers: KeyModifiers::ALT, + }, + ]) + ); + + assert_eq!( + parse_macro(":o foo.bar<ret>").ok(), + Some(vec![ + KeyEvent { + code: KeyCode::Char(':'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('o'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char(' '), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('f'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('o'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('o'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('.'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('b'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Char('r'), + modifiers: KeyModifiers::NONE, + }, + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + }, + ]) + ); + } + + #[test] + fn parsing_invalid_macros_fails() { + assert!(parse_macro("abc<C-").is_err()); + assert!(parse_macro("abc>123").is_err()); + assert!(parse_macro("wd<foo>").is_err()); + } } diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index 810aa063..f1717209 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -79,8 +79,6 @@ pub enum KeyCode { PageDown,
/// Tab key.
Tab,
- /// Shift + Tab key.
- BackTab,
/// Delete key.
Delete,
/// Insert key.
@@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode { KeyCode::PageUp => CKeyCode::PageUp,
KeyCode::PageDown => CKeyCode::PageDown,
KeyCode::Tab => CKeyCode::Tab,
- KeyCode::BackTab => CKeyCode::BackTab,
KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert,
KeyCode::F(f_number) => CKeyCode::F(f_number),
@@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode { CKeyCode::PageUp => KeyCode::PageUp,
CKeyCode::PageDown => KeyCode::PageDown,
CKeyCode::Tab => KeyCode::Tab,
- CKeyCode::BackTab => KeyCode::BackTab,
+ CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
CKeyCode::Delete => KeyCode::Delete,
CKeyCode::Insert => KeyCode::Insert,
CKeyCode::F(f_number) => KeyCode::F(f_number),
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 757316bd..00c1bbbd 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style}; pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") }); +pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { + toml::from_slice(include_bytes!("../../base16_theme.toml")) + .expect("Failed to parse base 16 default theme") +}); #[derive(Clone, Debug)] pub struct Loader { @@ -35,6 +39,9 @@ impl Loader { if name == "default" { return Ok(self.default()); } + if name == "base16_default" { + return Ok(self.base16_default()); + } let filename = format!("{}.toml", name); let user_path = self.user_dir.join(&filename); @@ -74,12 +81,20 @@ impl Loader { pub fn default(&self) -> Theme { DEFAULT_THEME.clone() } + + /// Returns the alternative 16-color default theme + pub fn base16_default(&self) -> Theme { + BASE16_DEFAULT_THEME.clone() + } } #[derive(Clone, Debug)] pub struct Theme { - scopes: Vec<String>, + // UI styles are stored in a HashMap styles: HashMap<String, Style>, + // tree-sitter highlight styles are stored in a Vec to optimize lookups + scopes: Vec<String>, + highlights: Vec<Style>, } impl<'de> Deserialize<'de> for Theme { @@ -88,6 +103,8 @@ impl<'de> Deserialize<'de> for Theme { D: Deserializer<'de>, { let mut styles = HashMap::new(); + let mut scopes = Vec::new(); + let mut highlights = Vec::new(); if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) { // TODO: alert user of parsing failures in editor @@ -102,24 +119,38 @@ impl<'de> Deserialize<'de> for Theme { .unwrap_or_default(); styles.reserve(colors.len()); + scopes.reserve(colors.len()); + highlights.reserve(colors.len()); + for (name, style_value) in colors { let mut style = Style::default(); if let Err(err) = palette.parse_style(&mut style, style_value) { warn!("{}", err); } - styles.insert(name, style); + + // these are used both as UI and as highlights + styles.insert(name.clone(), style); + scopes.push(name); + highlights.push(style); } } - let scopes = styles.keys().map(ToString::to_string).collect(); - Ok(Self { scopes, styles }) + Ok(Self { + scopes, + styles, + highlights, + }) } } impl Theme { + #[inline] + pub fn highlight(&self, index: usize) -> Style { + self.highlights[index] + } + pub fn get(&self, scope: &str) -> Style { - self.try_get(scope) - .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255))) + self.try_get(scope).unwrap_or_default() } pub fn try_get(&self, scope: &str) -> Option<Style> { @@ -134,6 +165,14 @@ impl Theme { pub fn find_scope_index(&self, scope: &str) -> Option<usize> { self.scopes().iter().position(|s| s == scope) } + + pub fn is_16_color(&self) -> bool { + self.styles.iter().all(|(_, style)| { + [style.fg, style.bg] + .into_iter() + .all(|color| !matches!(color, Some(Color::Rgb(..)))) + }) + } } struct ThemePalette { @@ -257,53 +296,58 @@ impl TryFrom<Value> for ThemePalette { } } -#[test] -fn test_parse_style_string() { - let fg = Value::String("#ffffff".to_string()); +#[cfg(test)] +mod tests { + use super::*; - let mut style = Style::default(); - let palette = ThemePalette::default(); - palette.parse_style(&mut style, fg).unwrap(); + #[test] + fn test_parse_style_string() { + let fg = Value::String("#ffffff".to_string()); - assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); -} + let mut style = Style::default(); + let palette = ThemePalette::default(); + palette.parse_style(&mut style, fg).unwrap(); -#[test] -fn test_palette() { - use helix_core::hashmap; - let fg = Value::String("my_color".to_string()); + assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); + } - let mut style = Style::default(); - let palette = - ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) }); - palette.parse_style(&mut style, fg).unwrap(); + #[test] + fn test_palette() { + use helix_core::hashmap; + let fg = Value::String("my_color".to_string()); - assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); -} + let mut style = Style::default(); + let palette = + ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) }); + palette.parse_style(&mut style, fg).unwrap(); -#[test] -fn test_parse_style_table() { - let table = toml::toml! { - "keyword" = { - fg = "#ffffff", - bg = "#000000", - modifiers = ["bold"], - } - }; + assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255))); + } - let mut style = Style::default(); - let palette = ThemePalette::default(); - if let Value::Table(entries) = table { - for (_name, value) in entries { - palette.parse_style(&mut style, value).unwrap(); + #[test] + fn test_parse_style_table() { + let table = toml::toml! { + "keyword" = { + fg = "#ffffff", + bg = "#000000", + modifiers = ["bold"], + } + }; + + let mut style = Style::default(); + let palette = ThemePalette::default(); + if let Value::Table(entries) = table { + for (_name, value) in entries { + palette.parse_style(&mut style, value).unwrap(); + } } - } - assert_eq!( - style, - Style::default() - .fg(Color::Rgb(255, 255, 255)) - .bg(Color::Rgb(0, 0, 0)) - .add_modifier(Modifier::BOLD) - ); + assert_eq!( + style, + Style::default() + .fg(Color::Rgb(255, 255, 255)) + .bg(Color::Rgb(0, 0, 0)) + .add_modifier(Modifier::BOLD) + ); + } } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 9336742b..6bc9435c 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -78,6 +78,13 @@ pub struct View { pub jumps: JumpList, /// the last accessed file before the current one pub last_accessed_doc: Option<DocumentId>, + /// the last modified files before the current one + /// ordered from most frequent to least frequent + // uses two docs because we want to be able to swap between the + // two last modified docs which we need to manually keep track of + pub last_modified_docs: [Option<DocumentId>; 2], + /// used to store previous selections of tree-sitter objecs + pub object_selections: Vec<Selection>, } impl View { @@ -89,6 +96,8 @@ impl View { area: Rect::default(), // will get calculated upon inserting into tree jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel last_accessed_doc: None, + last_modified_docs: [None, None], + object_selections: Vec::new(), } } @@ -370,7 +379,7 @@ mod tests { let text = rope.slice(..); assert_eq!( - view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4), + view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4), Some(0) ); @@ -403,7 +412,7 @@ mod tests { let text = rope.slice(..); assert_eq!( - view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4), + view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4), Some(0) ); diff --git a/languages.toml b/languages.toml index 2b501651..b6ab1d0e 100644 --- a/languages.toml +++ b/languages.toml @@ -3,14 +3,11 @@ name = "rust" scope = "source.rust" injection-regex = "rust" file-types = ["rs"] -roots = [] +roots = ["Cargo.toml", "Cargo.lock"] auto-format = true comment-token = "//" language-server = { command = "rust-analyzer" } indent = { tab-width = 4, unit = " " } -[language.config] -cargo = { loadOutDirsFromCheck = true } -procMacro = { enable = false } [language.debugger] name = "lldb-vscode" @@ -74,6 +71,17 @@ language-server = { command = "elixir-ls" } indent = { tab-width = 2, unit = " " } [[language]] +name = "fish" +scope = "source.fish" +injection-regex = "fish" +file-types = ["fish"] +shebangs = ["fish"] +roots = [] +comment-token = "#" + +indent = { tab-width = 4, unit = " " } + +[[language]] name = "mint" scope = "source.mint" injection-regex = "mint" @@ -226,6 +234,7 @@ roots = [] comment-token = "//" # TODO: highlights-jsx, highlights-params +language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" } indent = { tab-width = 2, unit = " " } [language.debugger] @@ -249,7 +258,7 @@ shebangs = [] roots = [] # TODO: highlights-jsx, highlights-params -language-server = { command = "typescript-language-server", args = ["--stdio"] } +language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"} indent = { tab-width = 2, unit = " " } [[language]] @@ -260,14 +269,14 @@ file-types = ["tsx"] roots = [] # TODO: highlights-jsx, highlights-params -language-server = { command = "typescript-language-server", args = ["--stdio"] } +language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" } indent = { tab-width = 2, unit = " " } [[language]] name = "css" scope = "source.css" injection-regex = "css" -file-types = ["css"] +file-types = ["css", "scss"] roots = [] indent = { tab-width = 2, unit = " " } @@ -322,7 +331,7 @@ indent = { tab-width = 2, unit = " " } name = "bash" scope = "source.bash" injection-regex = "bash" -file-types = ["sh", "bash"] +file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile", ".bashrc", ".profile", ".zshenv", ".zlogin", ".zlogout", ".zprofile", ".zshrc"] shebangs = ["sh", "bash", "dash"] roots = [] comment-token = "#" @@ -341,6 +350,15 @@ roots = [] indent = { tab-width = 4, unit = " " } [[language]] +name = "twig" +scope = "source.twig" +injection-regex = "twig" +file-types = ["twig"] +roots = [] + +indent = { tab-width = 2, unit = " " } + +[[language]] name = "latex" scope = "source.tex" injection-regex = "tex" @@ -351,6 +369,17 @@ comment-token = "%" indent = { tab-width = 4, unit = "\t" } [[language]] +name = "lean" +scope = "source.lean" +injection-regex = "lean" +file-types = ["lean"] +roots = [ "lakefile.lean" ] +comment-token = "--" +language-server = { command = "lean", args = [ "--server" ] } + +indent = { tab-width = 2, unit = " " } + +[[language]] name = "julia" scope = "source.julia" injection-regex = "julia" @@ -367,7 +396,6 @@ language-server = { command = "julia", args = [ using Pkg; import StaticLint; env_path = dirname(Pkg.Types.Context().env.project_file); - server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); server.runlinter = true; run(server); @@ -380,7 +408,7 @@ name = "java" scope = "source.java" injection-regex = "java" file-types = ["java"] -roots = [] +roots = ["pom.xml"] indent = { tab-width = 4, unit = " " } [[language]] @@ -445,16 +473,17 @@ file-types = ["yml", "yaml"] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } +injection-regex = "yml|yaml" -# [[language]] -# name = "haskell" -# scope = "source.haskell" -# injection-regex = "haskell" -# file-types = ["hs"] -# roots = [] -# comment-token = "--" -# -# indent = { tab-width = 2, unit = " " } +[[language]] +name = "haskell" +scope = "source.haskell" +injection-regex = "haskell" +file-types = ["hs"] +roots = [] +comment-token = "--" +language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] } +indent = { tab-width = 2, unit = " " } [[language]] name = "zig" @@ -487,6 +516,7 @@ scope = "source.tsq" file-types = ["scm"] roots = [] comment-token = ";" +injection-regex = "tsq" indent = { tab-width = 2, unit = " " } [[language]] @@ -497,6 +527,14 @@ roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } language-server = { command = "cmake-language-server" } +injection-regex = "cmake" + +[[language]] +name = "make" +scope = "source.make" +file-types = ["Makefile", "makefile", "justfile", ".justfile"] +roots =[] +comment-token = "#" [[language]] name = "glsl" @@ -505,6 +543,7 @@ file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ] roots = [] comment-token = "//" indent = { tab-width = 4, unit = " " } +injection-regex = "glsl" [[language]] name = "perl" @@ -525,6 +564,13 @@ comment-token = ";" language-server = { command = "racket", args = ["-l", "racket-langserver"] } [[language]] +name = "comment" +scope = "scope.comment" +roots = [] +file-types = [] +injection-regex = "comment" + +[[language]] name = "wgsl" scope = "source.wgsl" file-types = ["wgsl"] @@ -539,3 +585,149 @@ roots = [] file-types = ["ll"] comment-token = ";" indent = { tab-width = 2, unit = " " } +injection-regex = "llvm" + +[[language]] +name = "llvm-mir" +scope = "source.llvm_mir" +roots = [] +file-types = [] +comment-token = ";" +indent = { tab-width = 2, unit = " " } +injection-regex = "mir" + +[[language]] +name = "llvm-mir-yaml" +tree-sitter-library = "yaml" +scope = "source.yaml" +roots = [] +file-types = ["mir"] +comment-token = "#" +indent = { tab-width = 2, unit = " " } + +[[language]] +name = "tablegen" +scope = "source.tablegen" +roots = [] +file-types = ["td"] +comment-token = "//" +indent = { tab-width = 2, unit = " " } +injection-regex = "tablegen" + +[[language]] +name = "markdown" +scope = "source.md" +injection-regex = "md|markdown" +file-types = ["md"] +roots = [] + +indent = { tab-width = 2, unit = " " } + +[[language]] +name = "dart" +scope = "source.dart" +file-types = ["dart"] +roots = ["pubspec.yaml"] +auto-format = true +comment-token = "//" +language-server = { command = "dart", args = ["language-server", "--client-id=helix"] } +indent = { tab-width = 2, unit = " " } + +[[language]] +name = "scala" +scope = "source.scala" +roots = ["build.sbt", "pom.xml"] +file-types = ["scala", "sbt"] +comment-token = "//" +indent = { tab-width = 2, unit = " " } +language-server = { command = "metals" } + +[[language]] +name = "dockerfile" +scope = "source.dockerfile" +injection-regex = "docker|dockerfile" +roots = ["Dockerfile"] +file-types = ["Dockerfile", "dockerfile"] +comment-token = "#" +indent = { tab-width = 2, unit = " " } +language-server = { command = "docker-langserver", args = ["--stdio"] } + +[[language]] +name = "git-commit" +scope = "git.commitmsg" +roots = [] +file-types = ["COMMIT_EDITMSG"] +comment-token = "#" +indent = { tab-width = 2, unit = " " } + +[[language]] +name = "git-diff" +scope = "source.diff" +roots = [] +file-types = ["diff"] +injection-regex = "diff" +comment-token = "#" +indent = { tab-width = 2, unit = " " } + +[[language]] +name = "git-rebase" +scope = "source.gitrebase" +roots = [] +file-types = ["git-rebase-todo"] +injection-regex = "git-rebase" +comment-token = "#" +indent = { tab-width = 2, unit = " " } + +[[language]] +name = "regex" +scope = "source.regex" +injection-regex = "regex" +file-types = ["regex"] +roots = [] + +[[language]] +name = "git-config" +scope = "source.gitconfig" +roots = [] +# TODO: allow specifying file-types as a regex so we can read directory names (e.g. `.git/config`) +file-types = [".gitmodules", ".gitconfig"] +injection-regex = "git-config" +comment-token = "#" +indent = { tab-width = 4, unit = "\t" } + +[[language]] +name = "graphql" +scope = "source.graphql" +injection-regex = "graphql" +file-types = ["gql", "graphql"] +roots = [] +indent = { tab-width = 2, unit = " " } + +[[language]] +name = "elm" +scope = "source.elm" +injection-regex = "elm" +file-types = ["elm"] +roots = ["elm.json"] +auto-format = true +comment-token = "--" +language-server = { command = "elm-language-server" } +indent = { tab-width = 4, unit = " " } + +[[language]] +name = "iex" +scope = "source.iex" +injection-regex = "iex" +file-types = ["iex"] +roots = [] + +[[language]] +name = "rescript" +scope = "source.rescript" +injection-regex = "rescript" +file-types = ["res"] +roots = ["bsconfig.json"] +auto-format = true +comment-token = "//" +language-server = { command = "rescript-language-server", args = ["--stdio"] } +indent = { tab-width = 2, unit = " " } diff --git a/runtime/queries/bash/injections.scm b/runtime/queries/bash/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/bash/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/c-sharp/injections.scm b/runtime/queries/c-sharp/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/c-sharp/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/c/indents.toml b/runtime/queries/c/indents.toml new file mode 100644 index 00000000..f4076e17 --- /dev/null +++ b/runtime/queries/c/indents.toml @@ -0,0 +1,16 @@ +indent = [ + "compound_statement", + "field_declaration_list", + "enumerator_list", + "parameter_list", + "init_declarator", + "case_statement", + "condition_clause", + "expression_statement", +] + +outdent = [ + "case", + "}", + "]", +] diff --git a/runtime/queries/c/injections.scm b/runtime/queries/c/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/c/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/c/textobjects.scm b/runtime/queries/c/textobjects.scm new file mode 100644 index 00000000..b0f03668 --- /dev/null +++ b/runtime/queries/c/textobjects.scm @@ -0,0 +1,13 @@ +(function_definition + body: (_) @function.inside) @function.around + +(struct_specifier + body: (_) @class.inside) @class.around + +(enum_specifier + body: (_) @class.inside) @class.around + +(union_specifier + body: (_) @class.inside) @class.around + +(parameter_declaration) @parameter.inside diff --git a/runtime/queries/cmake/indents.toml b/runtime/queries/cmake/indents.toml new file mode 100644 index 00000000..8b886a4f --- /dev/null +++ b/runtime/queries/cmake/indents.toml @@ -0,0 +1,12 @@ +indent = [ + "if_condition", + "foreach_loop", + "while_loop", + "function_def", + "macro_def", + "normal_command", +] + +outdent = [ + ")" +] diff --git a/runtime/queries/cmake/injections.scm b/runtime/queries/cmake/injections.scm new file mode 100644 index 00000000..6cb6c254 --- /dev/null +++ b/runtime/queries/cmake/injections.scm @@ -0,0 +1,4 @@ +((line_comment) @injection.content + (#set! injection.language "comment")) +((bracket_comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/cmake/textobjects.scm b/runtime/queries/cmake/textobjects.scm new file mode 100644 index 00000000..b0d1b108 --- /dev/null +++ b/runtime/queries/cmake/textobjects.scm @@ -0,0 +1,3 @@ +(macro_def) @function.around + +(argument) @parameter.inside diff --git a/runtime/queries/comment/highlights.scm b/runtime/queries/comment/highlights.scm new file mode 100644 index 00000000..88685d59 --- /dev/null +++ b/runtime/queries/comment/highlights.scm @@ -0,0 +1,30 @@ +[ + "(" + ")" +] @punctuation.bracket + +":" @punctuation.delimiter + +((tag (name) @warning) + (#match? @warning "^(TODO|HACK|WARNING)$")) + +("text" @warning + (#match? @warning "^(TODO|HACK|WARNING)$")) + +((tag (name) @error) + (match? @error "^(FIXME|XXX|BUG)$")) + +("text" @error + (match? @error "^(FIXME|XXX|BUG)$")) + +(tag + (name) @ui.text + (user)? @constant) + +; Issue number (#123) +("text" @constant.numeric + (#match? @constant.numeric "^#[0-9]+$")) + +; User mention (@user) +("text" @tag + (#match? @tag "^[@][a-zA-Z0-9_-]+$")) diff --git a/runtime/queries/cpp/indents.toml b/runtime/queries/cpp/indents.toml new file mode 100644 index 00000000..0ca2ed8b --- /dev/null +++ b/runtime/queries/cpp/indents.toml @@ -0,0 +1,17 @@ +indent = [ + "compound_statement", + "field_declaration_list", + "enumerator_list", + "parameter_list", + "init_declarator", + "case_statement", + "condition_clause", + "expression_statement", +] + +outdent = [ + "case", + "access_specifier", + "}", + "]", +] diff --git a/runtime/queries/cpp/injections.scm b/runtime/queries/cpp/injections.scm new file mode 100644 index 00000000..a5a5208c --- /dev/null +++ b/runtime/queries/cpp/injections.scm @@ -0,0 +1 @@ +; inherits: c diff --git a/runtime/queries/cpp/textobjects.scm b/runtime/queries/cpp/textobjects.scm new file mode 100644 index 00000000..6e3de1a2 --- /dev/null +++ b/runtime/queries/cpp/textobjects.scm @@ -0,0 +1,7 @@ +; inherits: c + +(lambda_expression + body: (_) @function.inside) @function.around + +(class_specifier + body: (_) @class.inside) @class.around diff --git a/runtime/queries/css/injections.scm b/runtime/queries/css/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/css/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/dart/highlights.scm b/runtime/queries/dart/highlights.scm new file mode 100644 index 00000000..9f667d6b --- /dev/null +++ b/runtime/queries/dart/highlights.scm @@ -0,0 +1,237 @@ +(dotted_identifier_list) @string + +; Methods +; -------------------- +(super) @function.builtin + +(function_expression_body (identifier) @function.method) +((identifier)(selector (argument_part)) @function.method) + +; Annotations +; -------------------- +(annotation + name: (identifier) @attribute) +(marker_annotation + name: (identifier) @attribute) + +; Types +; -------------------- +(class_definition + name: (identifier) @type) + +(constructor_signature + name: (identifier) @function.method) + +(function_signature + name: (identifier) @function.method) + +(getter_signature + (identifier) @function.builtin) + +(setter_signature + name: (identifier) @function.builtin) + +(enum_declaration + name: (identifier) @type) + +(enum_constant + name: (identifier) @type.builtin) + +(void_type) @type.builtin + +((scoped_identifier + scope: (identifier) @type) + (#match? @type "^[a-zA-Z]")) + +((scoped_identifier + scope: (identifier) @type + name: (identifier) @type) + (#match? @type "^[a-zA-Z]")) + +; the DisabledDrawerButtons in : const DisabledDrawerButtons(history: true), +(type_identifier) @type.builtin + +; Variables +; -------------------- +; the "File" in var file = File(); +((identifier) @namespace + (#match? @namespace "^_?[A-Z].*[a-z]")) ; catch Classes or IClasses not CLASSES + +("Function" @type.builtin) +(inferred_type) @type.builtin + +; properties +(unconditional_assignable_selector + (identifier) @variable.other.member) + +(conditional_assignable_selector + (identifier) @variable.other.member) + +; assignments +; -------------------- +; the "strings" in : strings = "some string" +(assignment_expression + left: (assignable_expression) @variable) + +(this) @variable.builtin + +; Parameters +; -------------------- +(formal_parameter + name: (identifier) @variable) + +(named_argument + (label (identifier) @variable)) + +; Literals +; -------------------- +[ + (hex_integer_literal) + (decimal_integer_literal) + (decimal_floating_point_literal) + ;(octal_integer_literal) + ;(hex_floating_point_literal) +] @constant.numeric.integer + +(symbol_literal) @string.special.symbol +(string_literal) @string + +[ + (const_builtin) + (final_builtin) +] @variable.builtin + +[ + (true) + (false) +] @constant.builtin.boolean + +(null_literal) @constant.builtin + +(comment) @comment.line +(documentation_comment) @comment.block.documentation + +; Tokens +; -------------------- +(template_substitution + "$" @punctuation.special + "{" @punctuation.special + "}" @punctuation.special +) @embedded + +(template_substitution + "$" @punctuation.special + (identifier_dollar_escaped) @variable +) @embedded + +(escape_sequence) @constant.character.escape + +; Punctuation +;--------------------- +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + ";" + "." + "," + ":" +] @punctuation.delimiter + +; Operators +;--------------------- +[ + "@" + "?" + "=>" + ".." + "==" + "&&" + "%" + "<" + ">" + "=" + ">=" + "<=" + "||" + (multiplicative_operator) + (increment_operator) + (is_operator) + (prefix_operator) + (equality_operator) + (additive_operator) +] @operator + +; Keywords +; -------------------- +["import" "library" "export"] @keyword.control.import +["do" "while" "continue" "for"] @keyword.control.repeat +["return" "yield"] @keyword.control.return +["as" "in" "is"] @keyword.operator + +[ + "?." + "??" + "if" + "else" + "switch" + "default" + "late" +] @keyword.control.conditional + +[ + "try" + "throw" + "catch" + "finally" + (break_statement) +] @keyword.control.exception + +; Reserved words (cannot be used as identifiers) +[ + (case_builtin) + "abstract" + "async" + "async*" + "await" + "class" + "covariant" + "deferred" + "dynamic" + "enum" + "extends" + "extension" + "external" + "factory" + "Function" + "get" + "implements" + "interface" + "mixin" + "new" + "on" + "operator" + "part" + "required" + "set" + "show" + "static" + "super" + "sync*" + "typedef" + "with" +] @keyword + +; when used as an identifier: +((identifier) @variable.builtin + (#match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$")) + +; Error +(ERROR) @error + diff --git a/runtime/queries/dart/indents.toml b/runtime/queries/dart/indents.toml new file mode 100644 index 00000000..5c11e05d --- /dev/null +++ b/runtime/queries/dart/indents.toml @@ -0,0 +1,20 @@ +indent = [ + "class_body", + "function_body", + "function_expression_body", + "declaration", + "initializers", + "switch_block", + "if_statement", + "formal_parameter_list", + "formal_parameter", + "list_literal", + "return_statement", + "arguments" +] + +outdent = [ + "}", + "]", + ")" +] diff --git a/runtime/queries/dart/injections.scm b/runtime/queries/dart/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/dart/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/dart/locals.scm b/runtime/queries/dart/locals.scm new file mode 100644 index 00000000..629838e5 --- /dev/null +++ b/runtime/queries/dart/locals.scm @@ -0,0 +1,20 @@ +; Scopes +;------- + +[ + (block) + (try_statement) + (catch_clause) + (finally_clause) +] @local.scope + +; Definitions +;------------ + +(class_definition + body: (_) @local.definition) + +; References +;------------ + +(identifier) @local.reference diff --git a/runtime/queries/dockerfile/highlights.scm b/runtime/queries/dockerfile/highlights.scm new file mode 100644 index 00000000..5a945fb9 --- /dev/null +++ b/runtime/queries/dockerfile/highlights.scm @@ -0,0 +1,51 @@ +[ + "FROM" + "AS" + "RUN" + "CMD" + "LABEL" + "EXPOSE" + "ENV" + "ADD" + "COPY" + "ENTRYPOINT" + "VOLUME" + "USER" + "WORKDIR" + "ARG" + "ONBUILD" + "STOPSIGNAL" + "HEALTHCHECK" + "SHELL" + "MAINTAINER" + "CROSS_BUILD" +] @keyword + +[ + ":" + "@" +] @operator + +(comment) @comment + + +(image_spec + (image_tag + ":" @punctuation.special) + (image_digest + "@" @punctuation.special)) + +(double_quoted_string) @string + +(expansion + [ + "$" + "{" + "}" + ] @punctuation.special +) @none + +((variable) @constant + (#match? @constant "^[A-Z][A-Z_0-9]*$")) + + diff --git a/runtime/queries/dockerfile/injections.scm b/runtime/queries/dockerfile/injections.scm new file mode 100644 index 00000000..20396f1a --- /dev/null +++ b/runtime/queries/dockerfile/injections.scm @@ -0,0 +1,6 @@ +((comment) @injection.content + (#set! injection.language "comment")) + +([(shell_command) (shell_fragment)] @injection.content + (#set! injection.language "bash")) + diff --git a/runtime/queries/elixir/injections.scm b/runtime/queries/elixir/injections.scm new file mode 100644 index 00000000..8370a0d8 --- /dev/null +++ b/runtime/queries/elixir/injections.scm @@ -0,0 +1,9 @@ +((comment) @injection.content + (#set! injection.language "comment")) + +((sigil + (sigil_name) @_sigil_name + (quoted_content) @injection.content) + (#match? @_sigil_name "^(r|R)$") + (#set! injection.language "regex") + (#set! injection.combined)) diff --git a/runtime/queries/elm/highlights.scm b/runtime/queries/elm/highlights.scm new file mode 100644 index 00000000..3c8fd12d --- /dev/null +++ b/runtime/queries/elm/highlights.scm @@ -0,0 +1,83 @@ +; Keywords +[ + "if" + "then" + "else" + "let" + "in" + ] @keyword.control +(case) @keyword.control +(of) @keyword.control + +(colon) @keyword.operator +(backslash) @keyword +(as) @keyword +(port) @keyword +(exposing) @keyword +(alias) @keyword +(infix) @keyword + +(arrow) @keyword.operator +(dot) @keyword.operator + +(port) @keyword + +(type_annotation(lower_case_identifier) @function) +(port_annotation(lower_case_identifier) @function) +(file (value_declaration (function_declaration_left(lower_case_identifier) @function))) + +(field name: (lower_case_identifier) @attribute) +(field_access_expr(lower_case_identifier) @attribute) + +(operator_identifier) @keyword.operator +(eq) @keyword.operator.assignment + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +"|" @keyword +"," @punctuation.delimiter + +[ + "|>" +] @keyword + + +(import) @keyword.contol.import +(module) @keyword.other + +(number_constant_expr) @constant.numeric + +(type) @type + +(type_declaration(upper_case_identifier) @type) +(type_ref) @type +(type_alias_declaration name: (upper_case_identifier) @type) + +(union_pattern constructor: (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member)) +(union_pattern constructor: (upper_case_qid (upper_case_identifier) @variable.other.member)) + +(union_variant(upper_case_identifier) @variable.other.member) +(value_expr name: (value_qid (upper_case_identifier) @label)) +(value_expr (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member)) +(value_expr(upper_case_qid(upper_case_identifier)) @variable.other.member) + +; comments +(line_comment) @comment +(block_comment) @comment + +; strings +(string_escape) @constant.character.escape + +(open_quote) @string +(close_quote) @string +(regular_string_part) @string + +(open_char) @constant.character +(close_char) @constant.character diff --git a/runtime/queries/elm/injections.scm b/runtime/queries/elm/injections.scm new file mode 100644 index 00000000..83f8245c --- /dev/null +++ b/runtime/queries/elm/injections.scm @@ -0,0 +1,4 @@ +; Parse glsl where defined + +((glsl_content) @injection.content + (#set! injection.language "glsl")) diff --git a/runtime/queries/elm/locals.scm b/runtime/queries/elm/locals.scm new file mode 100644 index 00000000..ab103115 --- /dev/null +++ b/runtime/queries/elm/locals.scm @@ -0,0 +1,14 @@ +(value_declaration) @local.scope +(type_alias_declaration) @local.scope +(type_declaration) @local.scope +(type_annotation) @local.scope +(port_annotation) @local.scope +(infix_declaration) @local.scope +(let_in_expr) @local.scope + +(function_declaration_left (lower_pattern (lower_case_identifier)) @local.definition) +(function_declaration_left (lower_case_identifier) @local.definition) + +(value_expr(value_qid(upper_case_identifier)) @local.reference) +(value_expr(value_qid(lower_case_identifier)) @local.reference) +(type_ref (upper_case_qid) @local.reference) diff --git a/runtime/queries/elm/tags.scm b/runtime/queries/elm/tags.scm new file mode 100644 index 00000000..03999fb1 --- /dev/null +++ b/runtime/queries/elm/tags.scm @@ -0,0 +1,19 @@ +(value_declaration (function_declaration_left (lower_case_identifier) @name)) @definition.function + +(function_call_expr (value_expr (value_qid) @name)) @reference.function +(exposed_value (lower_case_identifier) @name) @reference.function +(type_annotation ((lower_case_identifier) @name) (colon)) @reference.function + +(type_declaration ((upper_case_identifier) @name) ) @definition.type + +(type_ref (upper_case_qid (upper_case_identifier) @name)) @reference.type +(exposed_type (upper_case_identifier) @name) @reference.type + +(type_declaration (union_variant (upper_case_identifier) @name)) @definition.union + +(value_expr (upper_case_qid (upper_case_identifier) @name)) @reference.union + + +(module_declaration + (upper_case_qid (upper_case_identifier)) @name +) @definition.module diff --git a/runtime/queries/fish/highlights.scm b/runtime/queries/fish/highlights.scm new file mode 100644 index 00000000..def53931 --- /dev/null +++ b/runtime/queries/fish/highlights.scm @@ -0,0 +1,156 @@ +;; Operators + +[ + "&&" + "||" + "|" + "&" + "=" + "!=" + ".." + "!" + (direction) + (stream_redirect) + (test_option) +] @operator + +[ + "not" + "and" + "or" +] @keyword.operator + +;; Conditionals + +(if_statement +[ + "if" + "end" +] @keyword.control.conditional) + +(switch_statement +[ + "switch" + "end" +] @keyword.control.conditional) + +(case_clause +[ + "case" +] @keyword.control.conditional) + +(else_clause +[ + "else" +] @keyword.control.conditional) + +(else_if_clause +[ + "else" + "if" +] @keyword.control.conditional) + +;; Loops/Blocks + +(while_statement +[ + "while" + "end" +] @keyword.control.repeat) + +(for_statement +[ + "for" + "end" +] @keyword.control.repeat) + +(begin_statement +[ + "begin" + "end" +] @keyword.control.repeat) + +;; Keywords + +[ + "in" + (break) + (continue) +] @keyword + +"return" @keyword.control.return + +;; Punctuation + +[ + "[" + "]" + "{" + "}" + "(" + ")" +] @punctuation.bracket + +"," @punctuation.delimiter + +;; Commands + +(command + argument: [ + (word) @variable.parameter (#match? @variable.parameter "^-") + ] +) + +; non-bultin command names +(command name: (word) @function) + +; derived from builtin -n (fish 3.2.2) +(command + name: [ + (word) @function.builtin + (#match? @function.builtin "^(\.|:|_|alias|argparse|bg|bind|block|breakpoint|builtin|cd|command|commandline|complete|contains|count|disown|echo|emit|eval|exec|exit|fg|functions|history|isatty|jobs|math|printf|pwd|random|read|realpath|set|set_color|source|status|string|test|time|type|ulimit|wait)$") + ] +) + +(test_command "test" @function.builtin) + +;; Functions + +(function_definition ["function" "end"] @keyword.function) + +(function_definition + name: [ + (word) (concatenation) + ] +@function) + +(function_definition + option: [ + (word) + (concatenation (word)) + ] @variable.parameter (#match? @variable.parameter "^-") +) + +;; Strings + +[(double_quote_string) (single_quote_string)] @string +(escape_sequence) @constant.character.escape + +;; Variables + +(variable_name) @variable +(variable_expansion) @constant + +;; Nodes + +(integer) @constant.numeric.integer +(float) @constant.numeric.float +(comment) @comment +(test_option) @string + +((word) @constant.builtin.boolean +(#match? @constant.builtin.boolean "^(true|false)$")) + +;; Error + +(ERROR) @error diff --git a/runtime/queries/fish/indents.toml b/runtime/queries/fish/indents.toml new file mode 100644 index 00000000..6f1e563a --- /dev/null +++ b/runtime/queries/fish/indents.toml @@ -0,0 +1,12 @@ +indent = [ + "function_definition", + "while_statement", + "for_statement", + "if_statement", + "begin_statement", + "switch_statement", +] + +outdent = [ + "end" +] diff --git a/runtime/queries/fish/injections.scm b/runtime/queries/fish/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/fish/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/fish/textobjects.scm b/runtime/queries/fish/textobjects.scm new file mode 100644 index 00000000..67fd6614 --- /dev/null +++ b/runtime/queries/fish/textobjects.scm @@ -0,0 +1 @@ +(function_definition) @function.around diff --git a/runtime/queries/git-commit/highlights.scm b/runtime/queries/git-commit/highlights.scm new file mode 100644 index 00000000..0b50d419 --- /dev/null +++ b/runtime/queries/git-commit/highlights.scm @@ -0,0 +1,14 @@ +(subject) @markup.heading +(path) @string.special.path +(branch) @string.special.symbol +(commit) @constant +(item) @markup.link.url +(header) @tag + +(change kind: "new file" @diff.plus) +(change kind: "deleted" @diff.minus) +(change kind: "modified" @diff.delta) +(change kind: "renamed" @diff.delta.moved) + +[":" "->"] @punctuation.delimeter +(comment) @comment diff --git a/runtime/queries/git-commit/injections.scm b/runtime/queries/git-commit/injections.scm new file mode 100644 index 00000000..cf0657f7 --- /dev/null +++ b/runtime/queries/git-commit/injections.scm @@ -0,0 +1,8 @@ +((comment (scissors)) + (message) @injection.content + (#set! injection.include-children) + (#set! injection.language "diff")) + +((rebase_command) @injection.content + (#set! injection.include-children) + (#set! injection.language "git-rebase")) diff --git a/runtime/queries/git-config/highlights.scm b/runtime/queries/git-config/highlights.scm new file mode 100644 index 00000000..84767edc --- /dev/null +++ b/runtime/queries/git-config/highlights.scm @@ -0,0 +1,27 @@ +((section_name) @keyword.directive + (#eq? @keyword.directive "include")) + +((section_header + (section_name) @keyword.directive + (subsection_name)) + (#eq? @keyword.directive "includeIf")) + +(section_name) @markup.heading +(variable (name) @variable.other.member) +[(true) (false)] @constant.builtin.boolean +(integer) @constant.numeric.integer + +((string) @string.special.path + (#match? @string.special.path "^(~|./|/)")) + +[(string) (subsection_name)] @string + +[ + "[" + "]" + "\"" +] @punctuation.bracket + +"=" @punctuation.delimiter + +(comment) @comment diff --git a/runtime/queries/git-diff/highlights.scm b/runtime/queries/git-diff/highlights.scm new file mode 100644 index 00000000..1c1a8829 --- /dev/null +++ b/runtime/queries/git-diff/highlights.scm @@ -0,0 +1,6 @@ +[(addition) (new_file)] @diff.plus +[(deletion) (old_file)] @diff.minus + +(commit) @constant +(location) @attribute +(command) @markup.bold diff --git a/runtime/queries/git-rebase/highlights.scm b/runtime/queries/git-rebase/highlights.scm new file mode 100644 index 00000000..4f007037 --- /dev/null +++ b/runtime/queries/git-rebase/highlights.scm @@ -0,0 +1,11 @@ +(operation operator: ["p" "pick" "r" "reword" "e" "edit" "s" "squash" "m" "merge" "d" "drop" "b" "break" "x" "exec"] @keyword) +(operation operator: ["l" "label" "t" "reset"] @function) +(operation operator: ["f" "fixup"] @function.special) + +(option) @operator +(label) @string.special.symbol +(commit) @constant +"#" @punctuation.delimiter +(comment) @comment + +(ERROR) @error diff --git a/runtime/queries/git-rebase/injections.scm b/runtime/queries/git-rebase/injections.scm new file mode 100644 index 00000000..070129b6 --- /dev/null +++ b/runtime/queries/git-rebase/injections.scm @@ -0,0 +1,4 @@ +((operation + operator: ["x" "exec"] + (command) @injection.content) + (#set! injection.language "bash")) diff --git a/runtime/queries/glsl/injections.scm b/runtime/queries/glsl/injections.scm index 7d3323b1..6330ea3e 100644 --- a/runtime/queries/glsl/injections.scm +++ b/runtime/queries/glsl/injections.scm @@ -1,3 +1,4 @@ -(preproc_arg) @glsl +; inherits: c -(comment) @comment +((preproc_arg) @injection.content + (#set! injection.language "glsl")) diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index 56384d4d..4ff8675b 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -69,6 +69,7 @@ "|" "|=" "||" + "~" ] @operator ; Keywords @@ -143,6 +144,9 @@ (false) ] @constant.builtin.boolean -(nil) @constant.builtin +[ + (nil) + (iota) +] @constant.builtin (comment) @comment diff --git a/runtime/queries/go/injections.scm b/runtime/queries/go/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/go/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/graphql/highlights.scm b/runtime/queries/graphql/highlights.scm new file mode 100644 index 00000000..9fab4051 --- /dev/null +++ b/runtime/queries/graphql/highlights.scm @@ -0,0 +1,163 @@ +; Types +;------ + +(scalar_type_definition + (name) @type) + +(object_type_definition + (name) @type) + +(interface_type_definition + (name) @type) + +(union_type_definition + (name) @type) + +(enum_type_definition + (name) @type) + +(input_object_type_definition + (name) @type) + +(directive_definition + (name) @type) + +(directive_definition + "@" @type) + +(scalar_type_extension + (name) @type) + +(object_type_extension + (name) @type) + +(interface_type_extension + (name) @type) + +(union_type_extension + (name) @type) + +(enum_type_extension + (name) @type) + +(input_object_type_extension + (name) @type) + +(named_type + (name) @type) + +(directive) @type + +; Properties +;----------- + +(field + (name) @variable.other.member) + +(field + (alias + (name) @variable.other.member)) + +(field_definition + (name) @variable.other.member) + +(object_value + (object_field + (name) @variable.other.member)) + +(enum_value + (name) @variable.other.member) + +; Variable Definitions and Arguments +;----------------------------------- + +(operation_definition + (name) @variable) + +(fragment_name + (name) @variable) + +(input_fields_definition + (input_value_definition + (name) @variable.parameter)) + +(argument + (name) @variable.parameter) + +(arguments_definition + (input_value_definition + (name) @variable.parameter)) + +(variable_definition + (variable) @variable.parameter) + +(argument + (value + (variable) @variable)) + +; Constants +;---------- + +(string_value) @string + +(int_value) @constants.numeric.integer + +(float_value) @constants.numeric.float + +(boolean_value) @constants.builtin.boolean + +; Literals +;--------- + +(description) @comment + +(comment) @comment + +(directive_location + (executable_directive_location) @type.builtin) + +(directive_location + (type_system_directive_location) @type.builtin) + +; Keywords +;---------- + +[ + "query" + "mutation" + "subscription" + "fragment" + "scalar" + "type" + "interface" + "union" + "enum" + "input" + "extend" + "directive" + "schema" + "on" + "repeatable" + "implements" +] @keyword + +; Punctuation +;------------ + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +"=" @operator + +"|" @punctuation.delimiter +"&" @punctuation.delimiter +":" @punctuation.delimiter + +"..." @punctuation.special +"!" @punctuation.special diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm index 72187876..5009f3b5 100644 --- a/runtime/queries/haskell/highlights.scm +++ b/runtime/queries/haskell/highlights.scm @@ -1,45 +1,125 @@ -(variable) @variable -(operator) @operator -(exp_name (constructor) @constructor) -(constructor_operator) @operator -(module) @namespace -(type) @type -(type) @class -(constructor) @constructor -(pragma) @pragma -(comment) @comment -(signature name: (variable) @fun_type_name) -(function name: (variable) @function) -(constraint class: (class_name (type)) @class) -(class (class_head class: (class_name (type)) @class)) -(instance (instance_head class: (class_name (type)) @class)) +;; ---------------------------------------------------------------------------- +;; Literals and comments + (integer) @constant.numeric.integer +(exp_negation) @constant.numeric.integer (exp_literal (float)) @constant.numeric.float (char) @constant.character -(con_unit) @literal -(con_list) @literal -(tycon_arrow) @operator -(where) @keyword -"module" @keyword -"let" @keyword -"in" @keyword -"class" @keyword -"instance" @keyword -"data" @keyword -"newtype" @keyword -"family" @keyword -"type" @keyword -"import" @keyword -"qualified" @keyword -"as" @keyword -"deriving" @keyword -"via" @keyword -"stock" @keyword -"anyclass" @keyword -"do" @keyword -"mdo" @keyword -"rec" @keyword +(string) @string + +(con_unit) @constant.builtin ; unit, as in () + +(comment) @comment + + +;; ---------------------------------------------------------------------------- +;; Punctuation + [ "(" ")" + "{" + "}" + "[" + "]" ] @punctuation.bracket + +[ + (comma) + ";" +] @punctuation.delimiter + + +;; ---------------------------------------------------------------------------- +;; Keywords, operators, includes + +(pragma) @constant.macro + +[ + "if" + "then" + "else" + "case" + "of" +] @keyword.control.conditional + +[ + "import" + "qualified" + "module" +] @keyword.control.import + +[ + (operator) + (constructor_operator) + (type_operator) + (tycon_arrow) + (qualified_module) ; grabs the `.` (dot), ex: import System.IO + (all_names) + (wildcard) + "=" + "|" + "::" + "=>" + "->" + "<-" + "\\" + "`" + "@" +] @operator + +(qualified_module (module) @constructor) +(qualified_type (module) @namespace) +(qualified_variable (module) @namespace) +(import (module) @namespace) + +[ + (where) + "let" + "in" + "class" + "instance" + "data" + "newtype" + "family" + "type" + "as" + "hiding" + "deriving" + "via" + "stock" + "anyclass" + "do" + "mdo" + "rec" + "forall" + "∀" + "infix" + "infixl" + "infixr" +] @keyword + + +;; ---------------------------------------------------------------------------- +;; Functions and variables + +(signature name: (variable) @type) +(function name: (variable) @function) + +(variable) @variable +"_" @variable.builtin + +(exp_infix (variable) @operator) ; consider infix functions as operators + +("@" @namespace) ; "as" pattern operator, e.g. x@Constructor + + +;; ---------------------------------------------------------------------------- +;; Types + +(type) @type + +(constructor) @constructor + +; True or False +((constructor) @_bool (#match? @_bool "(True|False)")) @constant.builtin.boolean diff --git a/runtime/queries/haskell/injections.scm b/runtime/queries/haskell/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/haskell/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/html/injections.scm b/runtime/queries/html/injections.scm index 71e7c3ae..ef58f415 100644 --- a/runtime/queries/html/injections.scm +++ b/runtime/queries/html/injections.scm @@ -1,3 +1,6 @@ +((comment) @injection.content + (#set! injection.language "comment")) + ((script_element (raw_text) @injection.content) (#set! injection.language "javascript")) diff --git a/runtime/queries/iex/highlights.scm b/runtime/queries/iex/highlights.scm new file mode 100644 index 00000000..2847fbff --- /dev/null +++ b/runtime/queries/iex/highlights.scm @@ -0,0 +1 @@ +(prompt) @comment diff --git a/runtime/queries/iex/injections.scm b/runtime/queries/iex/injections.scm new file mode 100644 index 00000000..48863d9d --- /dev/null +++ b/runtime/queries/iex/injections.scm @@ -0,0 +1,6 @@ +((evaluation_block (prompt_line (expression) @injection.content)) + (#set! injection.language "elixir") + (#set! injection.combined)) + +((result) @injection.content + (#set! injection.language "elixir")) diff --git a/runtime/queries/java/injections.scm b/runtime/queries/java/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/java/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/javascript/injections.scm b/runtime/queries/javascript/injections.scm index 5539241a..e8429111 100644 --- a/runtime/queries/javascript/injections.scm +++ b/runtime/queries/javascript/injections.scm @@ -9,6 +9,14 @@ ] arguments: (template_string) @injection.content) +; Parse the contents of gql template literals + +((call_expression + function: (identifier) @_template_function_name + arguments: (template_string) @injection.content) + (#eq? @_template_function_name "gql") + (#set! injection.language "graphql")) + ; Parse regex syntax within regex literals ((regex_pattern) @injection.content diff --git a/runtime/queries/julia/injections.scm b/runtime/queries/julia/injections.scm index be2412c0..1c1e804e 100644 --- a/runtime/queries/julia/injections.scm +++ b/runtime/queries/julia/injections.scm @@ -1,5 +1,5 @@ -; TODO: re-add when markdown is added. -; ((triple_string) @markdown -; (#offset! @markdown 0 3 0 -3)) +((triple_string) @injection.content + (#set! injection.language "markdown")) -(comment) @comment +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/julia/locals.scm b/runtime/queries/julia/locals.scm index f8b34f71..d5ac794e 100644 --- a/runtime/queries/julia/locals.scm +++ b/runtime/queries/julia/locals.scm @@ -2,24 +2,24 @@ (import_statement (identifier) @definition.import) (variable_declaration - (identifier) @definition.var) + (identifier) @local.definition) (variable_declaration (tuple_expression - (identifier) @definition.var)) + (identifier) @local.definition)) (for_binding - (identifier) @definition.var) + (identifier) @local.definition) (for_binding (tuple_expression - (identifier) @definition.var)) + (identifier) @local.definition)) (assignment_expression (tuple_expression - (identifier) @definition.var)) + (identifier) @local.definition)) (assignment_expression (bare_tuple_expression - (identifier) @definition.var)) + (identifier) @local.definition)) (assignment_expression - (identifier) @definition.var) + (identifier) @local.definition) (type_parameter_list (identifier) @definition.type) @@ -43,11 +43,11 @@ (identifier) @definition.parameter) (function_definition - name: (identifier) @definition.function) @scope + name: (identifier) @definition.function) @local.scope (macro_definition - name: (identifier) @definition.macro) @scope + name: (identifier) @definition.macro) @local.scope -(identifier) @reference +(identifier) @local.reference [ (try_statement) @@ -56,4 +56,4 @@ (let_statement) (compound_expression) (for_statement) -] @scope +] @local.scope diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm index f045c82d..0a030b31 100644 --- a/runtime/queries/latex/highlights.scm +++ b/runtime/queries/latex/highlights.scm @@ -278,7 +278,7 @@ "\\includeinkscape" "\\usepgflibrary" "\\usetikzlibrary" -] @include +] @keyword.control.import [ "\\part" @@ -318,60 +318,60 @@ ["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX (chapter - text: (brace_group) @text.title) + text: (brace_group) @markup.heading) (part - text: (brace_group) @text.title) + text: (brace_group) @markup.heading) (section - text: (brace_group) @text.title) + text: (brace_group) @markup.heading) (subsection - text: (brace_group) @text.title) + text: (brace_group) @markup.heading) (subsubsection - text: (brace_group) @text.title) + text: (brace_group) @markup.heading) (paragraph - text: (brace_group) @text.title) + text: (brace_group) @markup.heading) (subparagraph - text: (brace_group) @text.title) + text: (brace_group) @markup.heading) ((environment (begin name: (word) @_frame) (brace_group - child: (text) @text.title)) + child: (text) @markup.heading)) (#eq? @_frame "frame")) ((generic_command name:(generic_command_name) @_name arg: (brace_group - (text) @text.title)) + (text) @markup.heading)) (#eq? @_name "\\frametitle")) ;; Formatting ((generic_command name:(generic_command_name) @_name - arg: (_) @text.emphasis) + arg: (_) @markup.italic) (#eq? @_name "\\emph")) ((generic_command name:(generic_command_name) @_name - arg: (_) @text.emphasis) + arg: (_) @markup.italic) (#match? @_name "^(\\\\textit|\\\\mathit)$")) ((generic_command name:(generic_command_name) @_name - arg: (_) @text.strong) + arg: (_) @markup.bold) (#match? @_name "^(\\\\textbf|\\\\mathbf)$")) ((generic_command name:(generic_command_name) @_name . - arg: (_) @text.uri) + arg: (_) @markup.link.url) (#match? @_name "^(\\\\url|\\\\href)$")) (ERROR) @error diff --git a/runtime/queries/latex/injections.scm b/runtime/queries/latex/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/latex/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/lean/folds.scm b/runtime/queries/lean/folds.scm new file mode 100644 index 00000000..2c2bbb33 --- /dev/null +++ b/runtime/queries/lean/folds.scm @@ -0,0 +1,15 @@ +[ + (namespace) + (section) + + (instance) + (def) + (theorem) + (example) + + (product) + (array) + (list) + + (string) +] @fold diff --git a/runtime/queries/lean/highlights.scm b/runtime/queries/lean/highlights.scm new file mode 100644 index 00000000..a64feb1d --- /dev/null +++ b/runtime/queries/lean/highlights.scm @@ -0,0 +1,217 @@ +(open + namespace: (identifier) @namespace) +(namespace + name: (identifier) @namespace) +(section + name: (identifier) @namespace) + +;; Identifier naming conventions +((identifier) @type + (#match? @type "^[A-Z]")) + +(arrow) @type +(product) @type + +;; Declarations + +[ + "abbrev" + "def" + "theorem" + "constant" + "instance" + "axiom" + "example" + "inductive" + "structure" + "class" + + "deriving" + + "section" + "namespace" +] @keyword + +(attributes + (identifier) @function) + +(abbrev + name: (identifier) @type) +(def + name: (identifier) @function) +(theorem + name: (identifier) @function) +(constant + name: (identifier) @type) +(instance + name: (identifier) @function) +(instance + type: (identifier) @type) +(axiom + name: (identifier) @function) +(structure + name: (identifier) @type) +(structure + extends: (identifier) @type) + +(where_decl + type: (identifier) @type) + +(proj + name: (identifier) @field) + +(binders + type: (identifier) @type) + +["if" "then" "else"] @keyword.control.conditional + +["for" "in" "do"] @keyword.control.repeat + +(import) @include + +; Tokens + +[ + "!" + "$" + "%" + "&&" + "*" + "*>" + "+" + "++" + "-" + "/" + "::" + ":=" + "<" + "<$>" + "<*" + "<*>" + "<=" + "<|" + "<|>" + "=" + "==" + "=>" + ">" + ">" + ">=" + ">>" + ">>=" + "@" + "^" + "|>" + "|>." + "||" + "←" + "→" + "↔" + "∘" + "∧" + "∨" + "≠" + "≤" + "≥" +] @operator + +[ + "@&" +] @operator + +[ + "attribute" + "by" + "end" + "export" + "extends" + "fun" + "let" + "have" + "match" + "open" + "return" + "universe" + "variable" + "where" + "with" + "λ" + (hash_command) + (prelude) + (sorry) +] @keyword + +[ + "prefix" + "infix" + "infixl" + "infixr" + "postfix" + "notation" + "macro_rules" + "syntax" + "elab" + "builtin_initialize" +] @keyword + +[ + "noncomputable" + "partial" + "private" + "protected" + "unsafe" +] @keyword + +[ + "apply" + "exact" + "rewrite" + "rw" + "simp" + (trivial) +] @keyword + +[ + "catch" + "finally" + "try" +] @exception + +((apply + name: (identifier) @exception) + (#match? @exception "throw")) + +[ + "unless" + "mut" +] @keyword + +[(true) (false)] @boolean + +(number) @constant.numeric.integer +(float) @constant.numeric.float + +(comment) @comment +(char) @character +(string) @string +(interpolated_string) @string +; (escape_sequence) @string.escape + +; Reset highlighing in string interpolation +(interpolation) @none + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) + +["(" ")" "[" "]" "{" "}" "⟨" "⟩"] @punctuation.bracket + +["|" "," "." ":" ";"] @punctuation.delimiter + +(sorry) @error + +;; Error +(ERROR) @error + +; Variables +(identifier) @variable diff --git a/runtime/queries/lean/injections.scm b/runtime/queries/lean/injections.scm new file mode 100644 index 00000000..030714f1 --- /dev/null +++ b/runtime/queries/lean/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "markdown")) diff --git a/runtime/queries/lean/locals.scm b/runtime/queries/lean/locals.scm new file mode 100644 index 00000000..dd6c2036 --- /dev/null +++ b/runtime/queries/lean/locals.scm @@ -0,0 +1,5 @@ +[ + (module) + (namespace) + (section) +] @local.scope diff --git a/runtime/queries/ledger/highlights.scm b/runtime/queries/ledger/highlights.scm index bdf5f2db..02a9ea9a 100644 --- a/runtime/queries/ledger/highlights.scm +++ b/runtime/queries/ledger/highlights.scm @@ -12,7 +12,7 @@ ((account) @variable.other.member) ((commodity) @text.literal) -"include" @include +"include" @keyword.local.import [ "account" diff --git a/runtime/queries/ledger/injections.scm b/runtime/queries/ledger/injections.scm index 2d948141..c1714786 100644 --- a/runtime/queries/ledger/injections.scm +++ b/runtime/queries/ledger/injections.scm @@ -1,2 +1,2 @@ -(comment) @comment -(note) @comment +([(comment) (note)] @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/llvm-mir-yaml/highlights.scm b/runtime/queries/llvm-mir-yaml/highlights.scm new file mode 100644 index 00000000..4ba254e8 --- /dev/null +++ b/runtime/queries/llvm-mir-yaml/highlights.scm @@ -0,0 +1 @@ +; inherits: yaml diff --git a/runtime/queries/llvm-mir-yaml/indents.toml b/runtime/queries/llvm-mir-yaml/indents.toml new file mode 100644 index 00000000..ddc3578b --- /dev/null +++ b/runtime/queries/llvm-mir-yaml/indents.toml @@ -0,0 +1,3 @@ +indent = [ + "block_mapping_pair", +] diff --git a/runtime/queries/llvm-mir-yaml/injections.scm b/runtime/queries/llvm-mir-yaml/injections.scm new file mode 100644 index 00000000..b3243022 --- /dev/null +++ b/runtime/queries/llvm-mir-yaml/injections.scm @@ -0,0 +1,9 @@ +; inherits: yaml + +((document (block_node (block_scalar) @injection.content)) + (#set! injection.language "llvm")) + +((document (block_node (block_mapping (block_mapping_pair + key: (flow_node (plain_scalar (string_scalar))) ; "body" + value: (block_node (block_scalar) @injection.content))))) + (#set! injection.language "mir")) diff --git a/runtime/queries/llvm-mir/highlights.scm b/runtime/queries/llvm-mir/highlights.scm new file mode 100644 index 00000000..79234612 --- /dev/null +++ b/runtime/queries/llvm-mir/highlights.scm @@ -0,0 +1,136 @@ +[ + (label) + (bb_ref) +] @label + +[ + (comment) + (multiline_comment) +] @comment + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" +] @punctuation.bracket + +[ + "," + ":" + "|" + "*" +] @punctuation.delimiter + +[ + "=" + "x" +] @operator + +[ + "true" + "false" +] @constant.builtin.boolean + +[ + "null" + "_" + "unknown-address" +] @constant.builtin + +[ + (stack_object) + (constant_pool_index) + (jump_table_index) + (var) + (physical_register) + (ir_block) + (external_symbol) + (global_var) + (ir_local_var) + (metadata_ref) + (mnemonic) +] @variable + +(low_level_type) @type + +[ + (immediate_type) + (primitive_type) +] @type.builtin + +(number) @constant.numeric.integer +(float) @constant.numeric.float +(string) @string + +(instruction name: _ @keyword.operator) + +[ + "successors" + "liveins" + "pre-instr-symbol" + "post-instr-symbol" + "heap-alloc-marker" + "debug-instr-number" + "debug-location" + "mcsymbol" + "tied-def" + "target-flags" + "CustomRegMask" + "same_value" + "def_cfa_register" + "restore" + "undefined" + "offset" + "rel_offset" + "def_cfa" + "llvm_def_aspace_cfa" + "register" + "escape" + "remember_state" + "restore_state" + "window_save" + "negate_ra_sign_state" + "intpred" + "floatpred" + "shufflemask" + "liveout" + "target-index" + "blockaddress" + "intrinsic" + "load" + "store" + "unknown-size" + "on" + "from" + "into" + "align" + "basealign" + "addrspace" + "call-entry" + "custom" + "constant-pool" + "stack" + "got" + "jump-table" + "syncscope" + "address-taken" + "landing-pad" + "inlineasm-br-indirect-target" + "ehfunclet-entry" + "bbsections" + + (intpred) + (floatpred) + (memory_operand_flag) + (atomic_ordering) + (register_flag) + (instruction_flag) + (float_keyword) +] @keyword + +(ERROR) @error diff --git a/runtime/queries/llvm-mir/indents.toml b/runtime/queries/llvm-mir/indents.toml new file mode 100644 index 00000000..6a70e5ad --- /dev/null +++ b/runtime/queries/llvm-mir/indents.toml @@ -0,0 +1,7 @@ +indent = [ + "basic_block", +] + +outdent = [ + "label", +] diff --git a/runtime/queries/llvm-mir/injections.scm b/runtime/queries/llvm-mir/injections.scm new file mode 100644 index 00000000..0b476f86 --- /dev/null +++ b/runtime/queries/llvm-mir/injections.scm @@ -0,0 +1,2 @@ +([ (comment) (multiline_comment)] @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/llvm-mir/textobjects.scm b/runtime/queries/llvm-mir/textobjects.scm new file mode 100644 index 00000000..73f6f772 --- /dev/null +++ b/runtime/queries/llvm-mir/textobjects.scm @@ -0,0 +1,3 @@ +(basic_block) @function.around + +(argument) @parameter.inside diff --git a/runtime/queries/llvm/highlights.scm b/runtime/queries/llvm/highlights.scm index 73afe85e..cb705197 100644 --- a/runtime/queries/llvm/highlights.scm +++ b/runtime/queries/llvm/highlights.scm @@ -1,14 +1,158 @@ (type) @type -(statement) @keyword.operator +(type_keyword) @type.builtin + +(type [ + (local_var) + (global_var) + ] @type) + +(argument) @variable.parameter + +(_ inst_name: _ @keyword.operator) + +[ + "catch" + "filter" +] @keyword.operator + +[ + "to" + "nuw" + "nsw" + "exact" + "unwind" + "from" + "cleanup" + "swifterror" + "volatile" + "inbounds" + "inrange" + (icmp_cond) + (fcmp_cond) + (fast_math) +] @keyword.control + +(_ callee: _ @function) +(function_header name: _ @function) + +[ + "declare" + "define" + (calling_conv) +] @keyword.function + +[ + "target" + "triple" + "datalayout" + "source_filename" + "addrspace" + "blockaddress" + "align" + "syncscope" + "within" + "uselistorder" + "uselistorder_bb" + "module" + "asm" + "sideeffect" + "alignstack" + "inteldialect" + "unwind" + "type" + "global" + "constant" + "externally_initialized" + "alias" + "ifunc" + "section" + "comdat" + "thread_local" + "localdynamic" + "initialexec" + "localexec" + "any" + "exactmatch" + "largest" + "nodeduplicate" + "samesize" + "distinct" + "attributes" + "vscale" + "no_cfi" + (linkage_aux) + (dso_local) + (visibility) + (dll_storage_class) + (unnamed_addr) + (attribute_name) +] @keyword + + +(function_header [ + (linkage) + (calling_conv) + (unnamed_addr) + ] @keyword.function) + +[ + (string) + (cstring) +] @string + (number) @constant.numeric.integer (comment) @comment -(string) @string (label) @label -(keyword) @keyword -"ret" @keyword.control.return -(boolean) @constant.builtin.boolean +(_ inst_name: "ret" @keyword.control.return) (float) @constant.numeric.float -(constant) @constant -(identifier) @variable -(symbol) @punctuation.delimiter -(bracket) @punctuation.bracket + +[ + (local_var) + (global_var) +] @variable + +[ + (struct_value) + (array_value) + (vector_value) +] @constructor + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" + "<{" + "}>" +] @punctuation.bracket + +[ + "," + ":" +] @punctuation.delimiter + +[ + "=" + "|" + "x" + "..." +] @operator + +[ + "true" + "false" +] @constant.builtin.boolean + +[ + "undef" + "poison" + "null" + "none" + "zeroinitializer" +] @constant.builtin + +(ERROR) @error diff --git a/runtime/queries/llvm/indents.toml b/runtime/queries/llvm/indents.toml new file mode 100644 index 00000000..8cd603c8 --- /dev/null +++ b/runtime/queries/llvm/indents.toml @@ -0,0 +1,8 @@ +indent = [ + "function_body", + "instruction", +] + +outdent = [ + "}", +] diff --git a/runtime/queries/llvm/injections.scm b/runtime/queries/llvm/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/llvm/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/llvm/locals.scm b/runtime/queries/llvm/locals.scm new file mode 100644 index 00000000..1946c287 --- /dev/null +++ b/runtime/queries/llvm/locals.scm @@ -0,0 +1,14 @@ +; Scopes + +(function_body) @local.scope + +; Definitions + +(argument + (value (var (local_var) @local.definition))) + +(instruction + (local_var) @local.definition) + +; References +(local_var) @local.reference diff --git a/runtime/queries/llvm/textobjects.scm b/runtime/queries/llvm/textobjects.scm new file mode 100644 index 00000000..3738a3bb --- /dev/null +++ b/runtime/queries/llvm/textobjects.scm @@ -0,0 +1,16 @@ +(define + body: (_) @function.inside) @function.around + +(struct_type + (struct_body) @class.inside) @class.around + +(packed_struct_type + (struct_body) @class.inside) @class.around + +(array_type + (array_vector_body) @class.inside) @class.around + +(vector_type + (array_vector_body) @class.inside) @class.around + +(argument) @parameter.inside diff --git a/runtime/queries/lua/injections.scm b/runtime/queries/lua/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/lua/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/make/highlights.scm b/runtime/queries/make/highlights.scm new file mode 100644 index 00000000..50380baf --- /dev/null +++ b/runtime/queries/make/highlights.scm @@ -0,0 +1,170 @@ +[ + "(" + ")" + "{" + "}" +] @punctuation.bracket + +[ + ":" + "&:" + "::" + "|" + ";" + "\"" + "'" + "," +] @punctuation.delimiter + +[ + "$" + "$$" +] @punctuation.special + +(automatic_variable + [ "@" "%" "<" "?" "^" "+" "/" "*" "D" "F"] @punctuation.special) + +(automatic_variable + "/" @error . ["D" "F"]) + +[ + "=" + ":=" + "::=" + "?=" + "+=" + "!=" + "@" + "-" + "+" +] @operator + +[ + (text) + (string) + (raw_text) +] @string + +(variable_assignment (word) @string) + +[ + "ifeq" + "ifneq" + "ifdef" + "ifndef" + "else" + "endif" + "if" + "or" ; boolean functions are conditional in make grammar + "and" +] @keyword.control.conditional + +"foreach" @keyword.control.repeat + +[ + "define" + "endef" + "vpath" + "undefine" + "export" + "unexport" + "override" + "private" +; "load" +] @keyword + +[ + "include" + "sinclude" + "-include" +] @keyword.control.import + +[ + "subst" + "patsubst" + "strip" + "findstring" + "filter" + "filter-out" + "sort" + "word" + "words" + "wordlist" + "firstword" + "lastword" + "dir" + "notdir" + "suffix" + "basename" + "addsuffix" + "addprefix" + "join" + "wildcard" + "realpath" + "abspath" + "call" + "eval" + "file" + "value" + "shell" +] @keyword.function + +[ + "error" + "warning" + "info" +] @keyword.control.exception + +;; Variable +(variable_assignment + name: (word) @variable) + +(variable_reference + (word) @variable) + +(comment) @comment + +((word) @clean @string.regexp + (#match? @clean "[%\*\?]")) + +(function_call + function: "error" + (arguments (text) @error)) + +(function_call + function: "warning" + (arguments (text) @warning)) + +(function_call + function: "info" + (arguments (text) @info)) + +;; Install Command Categories +;; Others special variables +;; Variables Used by Implicit Rules +[ + "VPATH" + ".RECIPEPREFIX" +] @constant.builtin + +(variable_assignment + name: (word) @clean @constant.builtin + (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS)$")) + +(variable_reference + (word) @clean @constant.builtin + (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS\.VARIABLES|\.FEATURES|\.INCLUDE_DIRS|\.LOADED)$")) + +;; Standart targets +(targets + (word) @constant.macro + (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$")) + +(targets + (word) @constant.macro + (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$")) + +;; Builtin targets +(targets + (word) @constant.macro + (#match? @constant.macro "^\.(PHONY|SUFFIXES|DEFAULT|PRECIOUS|INTERMEDIATE|SECONDARY|SECONDEXPANSION|DELETE_ON_ERROR|IGNORE|LOW_RESOLUTION_TIME|SILENT|EXPORT_ALL_VARIABLES|NOTPARALLEL|ONESHELL|POSIX)$")) diff --git a/runtime/queries/make/injections.scm b/runtime/queries/make/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/make/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm new file mode 100644 index 00000000..f12254e9 --- /dev/null +++ b/runtime/queries/markdown/highlights.scm @@ -0,0 +1,41 @@ +[ + (atx_heading) + (setext_heading) +] @markup.heading + +(code_fence_content) @none + +[ + (indented_code_block) + (fenced_code_block) +] @markup.raw.block + +(block_quote) @markup.quote + +(code_span) @markup.raw.inline + +(emphasis) @markup.italic + +(strong_emphasis) @markup.bold + +(link_destination) @markup.link.url +(link_label) @markup.link.label + +[ + (link_text) + (image_description) +] @markup.link.text + +[ + (list_marker_plus) + (list_marker_minus) + (list_marker_star) + (list_marker_dot) + (list_marker_parenthesis) +] @punctuation.special + +[ + (backslash_escape) + (hard_line_break) +] @string.character.escape + diff --git a/runtime/queries/markdown/injections.scm b/runtime/queries/markdown/injections.scm new file mode 100644 index 00000000..10dcab0b --- /dev/null +++ b/runtime/queries/markdown/injections.scm @@ -0,0 +1,9 @@ +(fenced_code_block + (info_string) @injection.language + (code_fence_content) @injection.content + (#set! injection.include-children)) + +((html_block) @injection.content + (#set! injection.language "html")) +((html_tag) @injection.content + (#set! injection.language "html")) diff --git a/runtime/queries/nix/highlights.scm b/runtime/queries/nix/highlights.scm index 66719e87..f6682065 100644 --- a/runtime/queries/nix/highlights.scm +++ b/runtime/queries/nix/highlights.scm @@ -13,7 +13,7 @@ ] @keyword ((identifier) @variable.builtin - (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$") + (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins)$") (#is-not? local)) ((identifier) @function.builtin @@ -33,6 +33,11 @@ (uri) @string.special.uri +; boolean +((identifier) @constant.builtin.boolean (#match? @constant.builtin.boolean "^(true|false)$")) @constant.builtin.boolean +; null +((identifier) @constant.builtin (#eq? @constant.builtin "null")) @constant.builtin + (integer) @constant.numeric.integer (float) @constant.numeric.float diff --git a/runtime/queries/ocaml-interface/injections.scm b/runtime/queries/ocaml-interface/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/ocaml-interface/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm index 15f46cc1..a08b1267 100644 --- a/runtime/queries/ocaml/highlights.scm +++ b/runtime/queries/ocaml/highlights.scm @@ -90,7 +90,7 @@ ["exception" "try"] @keyword.control.exception -["include" "open"] @include +["include" "open"] @keyword.control.import ["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat diff --git a/runtime/queries/ocaml/indents.toml b/runtime/queries/ocaml/indents.toml index 9b6462d8..7586b83a 100644 --- a/runtime/queries/ocaml/indents.toml +++ b/runtime/queries/ocaml/indents.toml @@ -8,6 +8,6 @@ indent = [ "match_case", ] -oudent = [ +outdent = [ "}", ] diff --git a/runtime/queries/ocaml/injections.scm b/runtime/queries/ocaml/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/ocaml/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/perl/indents.toml b/runtime/queries/perl/indents.toml new file mode 100644 index 00000000..365e0663 --- /dev/null +++ b/runtime/queries/perl/indents.toml @@ -0,0 +1,17 @@ +indent = [ + "function", + "identifier", + "method_invocation", + "if_statement", + "unless_statement", + "if_simple_statement", + "unless_simple_statement", + "variable_declaration", + "block", + "list_item", + "word_list_qw" +] + +outdent = [ + "}" +] diff --git a/runtime/queries/perl/injections.scm b/runtime/queries/perl/injections.scm new file mode 100644 index 00000000..cab5f53d --- /dev/null +++ b/runtime/queries/perl/injections.scm @@ -0,0 +1,2 @@ +((comments) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/php/highlights.scm b/runtime/queries/php/highlights.scm index 46b5d26c..5379fa1e 100644 --- a/runtime/queries/php/highlights.scm +++ b/runtime/queries/php/highlights.scm @@ -5,7 +5,8 @@ (primitive_type) @type.builtin (cast_type) @type.builtin -(type_name (name) @type) +(named_type (name) @type) @type +(named_type (qualified_name) @type) @type ; Functions @@ -85,10 +86,12 @@ "endif" @keyword "endswitch" @keyword "endwhile" @keyword +"enum" @keyword "extends" @keyword "final" @keyword "finally" @keyword "foreach" @keyword +"fn" @keyword "function" @keyword "global" @keyword "if" @keyword @@ -97,6 +100,7 @@ "include" @keyword "insteadof" @keyword "interface" @keyword +"match" @keyword "namespace" @keyword "new" @keyword "private" @keyword diff --git a/runtime/queries/php/injections.scm b/runtime/queries/php/injections.scm index 16d5736b..614a3850 100644 --- a/runtime/queries/php/injections.scm +++ b/runtime/queries/php/injections.scm @@ -1,3 +1,6 @@ ((text) @injection.content (#set! injection.language "html") (#set! injection.combined)) + +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/php/textobjects.scm b/runtime/queries/php/textobjects.scm new file mode 100644 index 00000000..04ffefd2 --- /dev/null +++ b/runtime/queries/php/textobjects.scm @@ -0,0 +1,30 @@ +(class_declaration + body: (_) @class.inside) @class.around + +(interface_declaration + body: (_) @class.inside) @class.around + +(trait_declaration + body: (_) @class.inside) @class.around + +(enum_declaration + body: (_) @class.inside) @class.around + +(function_definition + body: (_) @function.inside) @function.around + +(method_declaration + body: (_) @function.inside) @function.around + +(arrow_function + body: (_) @function.inside) @function.around + +(anonymous_function_creation_expression + body: (_) @function.inside) @function.around + +(formal_parameters + [ + (simple_parameter) + (variadic_parameter) + (property_promotion_parameter) + ] @parameter.inside) diff --git a/runtime/queries/protobuf/injections.scm b/runtime/queries/protobuf/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/protobuf/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/python/injections.scm b/runtime/queries/python/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/python/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/regex/highlights.scm b/runtime/queries/regex/highlights.scm new file mode 100644 index 00000000..9376caa9 --- /dev/null +++ b/runtime/queries/regex/highlights.scm @@ -0,0 +1,53 @@ +; upstream: https://github.com/tree-sitter/tree-sitter-regex/blob/e1cfca3c79896ff79842f057ea13e529b66af636/queries/highlights.scm + +[ + "(" + ")" + "(?" + "(?:" + "(?<" + ">" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "*" + "+" + "|" + "=" + "<=" + "!" + "<!" + "?" +] @operator + +[ + (identity_escape) + (control_letter_escape) + (character_class_escape) + (control_escape) + (start_assertion) + (end_assertion) + (boundary_assertion) + (non_boundary_assertion) +] @constant.character.escape + +(group_name) @property + +(count_quantifier + [ + (decimal_digits) @constant.numeric + "," @punctuation.delimiter + ]) + +(character_class + [ + "^" @operator + (class_range "-" @operator) + ]) + +(class_character) @constant.character +(pattern_character) @string diff --git a/runtime/queries/rescript/highlights.scm b/runtime/queries/rescript/highlights.scm new file mode 100644 index 00000000..b9ab8ea6 --- /dev/null +++ b/runtime/queries/rescript/highlights.scm @@ -0,0 +1,179 @@ +(comment) @comment + +; Identifiers +;------------ + +; Escaped identifiers like \"+." +((value_identifier) @function.macro + (#match? @function.macro "^\\.*$")) + +[ + (type_identifier) + (unit_type) + "list" +] @type + +[ + (variant_identifier) + (polyvar_identifier) +] @constant + +(property_identifier) @variable.other.member +(module_identifier) @namespace + +(jsx_identifier) @tag +(jsx_attribute (property_identifier) @variable.parameter) + +; Parameters +;---------------- + +(list_pattern (value_identifier) @variable.parameter) +(spread_pattern (value_identifier) @variable.parameter) + +; String literals +;---------------- + +[ + (string) + (template_string) +] @string + +(template_substitution + "${" @punctuation.bracket + "}" @punctuation.bracket) @embedded + +(character) @constant.character +(escape_sequence) @constant.character.escape + +; Other literals +;--------------- + +[ + (true) + (false) +] @constant.builtin + +(number) @constant.numeric +(polyvar) @constant +(polyvar_string) @constant + +; Functions +;---------- + +[ + (formal_parameters (value_identifier)) + (positional_parameter (value_identifier)) + (labeled_parameter (value_identifier)) +] @variable.parameter + +(function parameter: (value_identifier) @variable.parameter) + +; Meta +;----- + +[ + "@" + "@@" + (decorator_identifier) +] @label + +(extension_identifier) @keyword +("%") @keyword + +; Misc +;----- + +(subscript_expression index: (string) @variable.other.member) +(polyvar_type_pattern "#" @constant) + +[ + ("include") + ("open") +] @keyword + +[ + "as" + "export" + "external" + "let" + "module" + "mutable" + "private" + "rec" + "type" + "and" +] @keyword + +[ + "if" + "else" + "switch" +] @keyword + +[ + "exception" + "try" + "catch" + "raise" +] @keyword + +[ + "." + "," + "|" +] @punctuation.delimiter + +[ + "++" + "+" + "+." + "-" + "-." + "*" + "*." + "/" + "/." + "<" + "<=" + "==" + "===" + "!" + "!=" + "!==" + ">" + ">=" + "&&" + "||" + "=" + ":=" + "->" + "|>" + ":>" + (uncurry) +] @operator + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +(polyvar_type + [ + "[" + "[>" + "[<" + "]" + ] @punctuation.bracket) + +[ + "~" + "?" + "=>" + "..." +] @punctuation + +(ternary_expression ["?" ":"] @operator) diff --git a/runtime/queries/rescript/injections.scm b/runtime/queries/rescript/injections.scm new file mode 100644 index 00000000..201cce75 --- /dev/null +++ b/runtime/queries/rescript/injections.scm @@ -0,0 +1,8 @@ +((comment) @injection.content + (#set! injection.language "comment")) + +((raw_js) @injection.content + (#set! injection.language "javascript")) + +((raw_gql) @injection.content + (#set! injection.language "graphql")) diff --git a/runtime/queries/rescript/textobjects.scm b/runtime/queries/rescript/textobjects.scm new file mode 100644 index 00000000..7ee8cd1a --- /dev/null +++ b/runtime/queries/rescript/textobjects.scm @@ -0,0 +1,9 @@ +; Classes (modules) +;------------------ + +(module_declaration definition: ((_) @class.inside)) @class.around + +; Functions +;---------- + +(function body: (_) @function.inside) @function.around diff --git a/runtime/queries/ruby/indents.toml b/runtime/queries/ruby/indents.toml new file mode 100644 index 00000000..b417751f --- /dev/null +++ b/runtime/queries/ruby/indents.toml @@ -0,0 +1,25 @@ +indent = [ + "argument_list", + "array", + "begin", + "block", + "call", + "class", + "case", + "do_block", + "elsif", + "if", + "hash", + "method", + "module", + "singleton_class", + "singleton_method", +] + +outdent = [ + ")", + "}", + "]", + "end", + "when", +] diff --git a/runtime/queries/ruby/injections.scm b/runtime/queries/ruby/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/ruby/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 539d9550..26496c66 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -127,11 +127,16 @@ "await" ] @keyword.control +"use" @keyword.control.import +(mod_item "mod" @keyword.control.import !body) +(use_as_clause "as" @keyword.control.import) + +(type_cast_expression "as" @keyword.operator) + [ (crate) (super) "as" - "use" "pub" "mod" "extern" @@ -242,10 +247,9 @@ ; --- ; Macros ; --- - (meta_item - (identifier) @attribute) -(attribute_item) @attribute + (identifier) @function.macro) + (inner_attribute_item) @attribute (macro_definition @@ -259,7 +263,7 @@ "!" @function.macro) (metavariable) @variable.parameter -(fragment_specifier) @variable.parameter +(fragment_specifier) @type diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml index 3900f0b9..51a0ceea 100644 --- a/runtime/queries/rust/indents.toml +++ b/runtime/queries/rust/indents.toml @@ -9,6 +9,7 @@ indent = [ "field_initializer_list", "struct_pattern", "tuple_pattern", + "unit_expression", "enum_variant_list", "call_expression", "binary_expression", diff --git a/runtime/queries/rust/injections.scm b/runtime/queries/rust/injections.scm index 6035d418..77c70805 100644 --- a/runtime/queries/rust/injections.scm +++ b/runtime/queries/rust/injections.scm @@ -1,3 +1,6 @@ +([(line_comment) (block_comment)] @injection.content + (#set! injection.language "comment")) + ((macro_invocation (token_tree) @injection.content) (#set! injection.language "rust") @@ -7,3 +10,17 @@ (token_tree) @injection.content) (#set! injection.language "rust") (#set! injection.include-children)) + +(call_expression + function: (scoped_identifier + path: (identifier) @_regex (#eq? @_regex "Regex") + name: (identifier) @_new (#eq? @_new "new")) + arguments: (arguments (raw_string_literal) @injection.content) + (#set! injection.language "regex")) + +(call_expression + function: (scoped_identifier + path: (scoped_identifier (identifier) @_regex (#eq? @_regex "Regex") .) + name: (identifier) @_new (#eq? @_new "new")) + arguments: (arguments (raw_string_literal) @injection.content) + (#set! injection.language "regex")) diff --git a/runtime/queries/scala/highlights.scm b/runtime/queries/scala/highlights.scm new file mode 100644 index 00000000..50a6e18a --- /dev/null +++ b/runtime/queries/scala/highlights.scm @@ -0,0 +1,203 @@ +; CREDITS @stumash (stuart.mashaal@gmail.com) + +;; variables + + +((identifier) @variable.builtin + (#match? @variable.builtin "^this$")) + +(interpolation) @none + +; Assume other uppercase names constants. +; NOTE: In order to distinguish constants we highlight +; all the identifiers that are uppercased. But this solution +; is not suitable for all occurrences e.g. it will highlight +; an uppercased method as a constant if used with no params. +; Introducing highlighting for those specific cases, is probably +; best way to resolve the issue. +((identifier) @constant (#match? @constant "^[A-Z]")) + +;; types + +(type_identifier) @type + +(class_definition + name: (identifier) @type) + +(object_definition + name: (identifier) @type) + +(trait_definition + name: (identifier) @type) + +(type_definition + name: (type_identifier) @type) + +; method definition + +(class_definition + body: (template_body + (function_definition + name: (identifier) @function.method))) +(object_definition + body: (template_body + (function_definition + name: (identifier) @function.method))) +(trait_definition + body: (template_body + (function_definition + name: (identifier) @function.method))) + +; imports + +(import_declaration + path: (identifier) @namespace) +((stable_identifier (identifier) @namespace)) + +((import_declaration + path: (identifier) @type) (#match? @type "^[A-Z]")) +((stable_identifier (identifier) @type) (#match? @type "^[A-Z]")) + +((import_selectors (identifier) @type) (#match? @type "^[A-Z]")) + +; method invocation + + +(call_expression + function: (identifier) @function) + +(call_expression + function: (field_expression + field: (identifier) @function.method)) + +((call_expression + function: (identifier) @variable.other.member) + (#match? @variable.other.member "^[A-Z]")) + +(generic_function + function: (identifier) @function) + +( + (identifier) @function.builtin + (#match? @function.builtin "^super$") +) + +; function definitions + +(function_definition + name: (identifier) @function) + +(parameter + name: (identifier) @variable.parameter) + +; expressions + + +(field_expression field: (identifier) @variable.other.member) +(field_expression value: (identifier) @type + (#match? @type "^[A-Z]")) + +(infix_expression operator: (identifier) @operator) +(infix_expression operator: (operator_identifier) @operator) +(infix_type operator: (operator_identifier) @operator) +(infix_type operator: (operator_identifier) @operator) + +; literals +(boolean_literal) @constant.builtin.boolean +(integer_literal) @constant.numeric.integer +(floating_point_literal) @constant.numeric.float + + +(symbol_literal) @string.special.symbol + +[ +(string) +(character_literal) +(interpolated_string_expression) +] @string + +(interpolation "$" @punctuation.special) + +;; keywords + +[ + "abstract" + "case" + "class" + "extends" + "final" + "finally" +;; `forSome` existential types not implemented yet + "implicit" + "lazy" +;; `macro` not implemented yet + "object" + "override" + "package" + "private" + "protected" + "sealed" + "trait" + "type" + "val" + "var" + "with" +] @keyword + +(null_literal) @constant.builtin +(wildcard) @keyword + +;; special keywords + +"new" @keyword.operator + +[ + "else" + "if" + "match" + "try" + "catch" + "throw" +] @keyword.control.conditional + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "." + "," +] @punctuation.delimiter + +[ + "do" + "for" + "while" + "yield" +] @keyword.control.repeat + +"def" @keyword.function + +[ + "=>" + "<-" + "@" +] @keyword.operator + +"import" @keyword.control.import + +"return" @keyword.control.return + +(comment) @comment + +;; `case` is a conditional keyword in case_block + +(case_block + (case_clause ("case") @keyword.control.conditional)) + +(identifier) @variable
\ No newline at end of file diff --git a/runtime/queries/scala/indents.toml b/runtime/queries/scala/indents.toml new file mode 100644 index 00000000..6de54844 --- /dev/null +++ b/runtime/queries/scala/indents.toml @@ -0,0 +1,23 @@ + +indent = [ + "block", + "arguments", + "parameter", + "class_definition", + "trait_definition", + "object_definition", + "function_definition", + "val_definition", + "import_declaration", + "while_expression", + "do_while_expression", + "for_expression", + "try_expression", + "match_expression" +] + +outdent = [ + "}", + "]", + ")" +] diff --git a/runtime/queries/scala/injections.scm b/runtime/queries/scala/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/scala/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm index 4fcdfd66..22b0c551 100644 --- a/runtime/queries/svelte/highlights.scm +++ b/runtime/queries/svelte/highlights.scm @@ -20,12 +20,12 @@ ((element (start_tag (tag_name) @_tag) (text) @markup.inline) (#match? @_tag "^(code|kbd)$")) -((element (start_tag (tag_name) @_tag) (text) @markup.underline.link) +((element (start_tag (tag_name) @_tag) (text) @markup.link.url) (#eq? @_tag "a")) ((attribute (attribute_name) @_attr - (quoted_attribute_value (attribute_value) @markup.undeline.link)) + (quoted_attribute_value (attribute_value) @markup.link.url)) (#match? @_attr "^(href|src)$")) (tag_name) @tag diff --git a/runtime/queries/svelte/injections.scm b/runtime/queries/svelte/injections.scm index 266f4701..04e860cf 100644 --- a/runtime/queries/svelte/injections.scm +++ b/runtime/queries/svelte/injections.scm @@ -26,5 +26,5 @@ (#set! injection.language "typescript") ) -(comment) @comment - +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/tablegen/highlights.scm b/runtime/queries/tablegen/highlights.scm new file mode 100644 index 00000000..8ade5ba9 --- /dev/null +++ b/runtime/queries/tablegen/highlights.scm @@ -0,0 +1,90 @@ +[ + (comment) + (multiline_comment) +] @comment + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" +] @punctuation.bracket + +[ + "," + ";" + "." +] @punctuation.delimiter + +[ + "#" + "-" + "..." + ":" +] @operator + +[ + "=" + "!cond" + (operator_keyword) +] @function + +[ + "true" + "false" +] @constant.builtin.boolean + +[ + "?" +] @constant.builtin + +(var) @variable + +(template_arg (identifier) @variable.parameter) + +(_ argument: (value (identifier) @variable.parameter)) + +(type) @type + +"code" @type.builtin + +(number) @constant.numeric.integer +[ + (string_string) + (code_string) +] @string + +(preprocessor) @keyword.directive + +[ + "class" + "field" + "let" + "defvar" + "def" + "defset" + "defvar" + "assert" +] @keyword + +[ + "let" + "in" + "foreach" + "if" + "then" + "else" +] @keyword.operator + +"include" @keyword.control.import + +[ + "multiclass" + "defm" +] @namespace + +(ERROR) @error diff --git a/runtime/queries/tablegen/indents.toml b/runtime/queries/tablegen/indents.toml new file mode 100644 index 00000000..43532f4d --- /dev/null +++ b/runtime/queries/tablegen/indents.toml @@ -0,0 +1,7 @@ +indent = [ + "statement", +] + +outdent = [ + "}", +] diff --git a/runtime/queries/tablegen/injections.scm b/runtime/queries/tablegen/injections.scm new file mode 100644 index 00000000..0b476f86 --- /dev/null +++ b/runtime/queries/tablegen/injections.scm @@ -0,0 +1,2 @@ +([ (comment) (multiline_comment)] @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/tablegen/textobjects.scm b/runtime/queries/tablegen/textobjects.scm new file mode 100644 index 00000000..2cb80268 --- /dev/null +++ b/runtime/queries/tablegen/textobjects.scm @@ -0,0 +1,7 @@ +(class + body: (_) @class.inside) @class.around + +(multiclass + body: (_) @class.inside) @class.around + +(_ argument: _ @parameter.inside) diff --git a/runtime/queries/toml/injections.scm b/runtime/queries/toml/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/toml/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/tsq/injections.scm b/runtime/queries/tsq/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/tsq/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/tsx/injections.scm b/runtime/queries/tsx/injections.scm new file mode 100644 index 00000000..1b61e36d --- /dev/null +++ b/runtime/queries/tsx/injections.scm @@ -0,0 +1 @@ +; inherits: typescript diff --git a/runtime/queries/twig/highlights.scm b/runtime/queries/twig/highlights.scm new file mode 100644 index 00000000..2c95ab63 --- /dev/null +++ b/runtime/queries/twig/highlights.scm @@ -0,0 +1,16 @@ +(comment_directive) @comment + +[ + "{%" + "{%-" + "{%~" + "%}" + "-%}" + "~%}" + "{{" + "{{-" + "{{~" + "}}" + "-}}" + "~}}" +] @keyword diff --git a/runtime/queries/twig/injections.scm b/runtime/queries/twig/injections.scm new file mode 100644 index 00000000..f0822734 --- /dev/null +++ b/runtime/queries/twig/injections.scm @@ -0,0 +1,3 @@ +((content) @injection.content + (#set! injection.language "html") + (#set! injection.combined)) diff --git a/runtime/queries/typescript/injections.scm b/runtime/queries/typescript/injections.scm new file mode 100644 index 00000000..ff0ddfac --- /dev/null +++ b/runtime/queries/typescript/injections.scm @@ -0,0 +1 @@ +; inherits: javascript diff --git a/runtime/queries/vue/injections.scm b/runtime/queries/vue/injections.scm index 8ee34ffb..73df868b 100644 --- a/runtime/queries/vue/injections.scm +++ b/runtime/queries/vue/injections.scm @@ -15,3 +15,6 @@ ((style_element (raw_text) @injection.content) (#set! injection.language "css")) + +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/wgsl/injections.scm b/runtime/queries/wgsl/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/wgsl/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm index a7efb5e7..e4fed27a 100644 --- a/runtime/queries/yaml/highlights.scm +++ b/runtime/queries/yaml/highlights.scm @@ -1,9 +1,19 @@ -(block_mapping_pair key: (_) @variable.other.member) -(flow_mapping (_ key: (_) @variable.other.member)) +(block_mapping_pair + key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member)) +(block_mapping_pair + key: (flow_node (plain_scalar (string_scalar) @variable.other.member))) + +(flow_mapping + (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member))) +(flow_mapping + (_ key: (flow_node (plain_scalar (string_scalar) @variable.other.member)))) + (boolean_scalar) @constant.builtin.boolean (null_scalar) @constant.builtin (double_quote_scalar) @string (single_quote_scalar) @string +(block_scalar) @string +(string_scalar) @string (escape_sequence) @constant.character.escape (integer_scalar) @constant.numeric.integer (float_scalar) @constant.numeric.float @@ -30,4 +40,4 @@ "}" ] @punctuation.bracket -["*" "&"] @punctuation.special +["*" "&" "---" "..."] @punctuation.special diff --git a/runtime/queries/yaml/injections.scm b/runtime/queries/yaml/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/yaml/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/zig/highlights.scm b/runtime/queries/zig/highlights.scm index 34dbeacd..62c99acc 100644 --- a/runtime/queries/zig/highlights.scm +++ b/runtime/queries/zig/highlights.scm @@ -144,7 +144,6 @@ field_constant: (IDENTIFIER) @constant ; VarDecl [ - "comptime" "threadlocal" "fn" ] @keyword.function @@ -178,6 +177,7 @@ field_constant: (IDENTIFIER) @constant ; PrecProc [ + "comptime" "inline" "noinline" "asm" @@ -195,15 +195,14 @@ field_constant: (IDENTIFIER) @constant (BitwiseOp) (BitShiftOp) (AdditionOp) + (AssignOp) (MultiplyOp) (PrefixOp) "*" "**" "->" - "=>" ".?" ".*" - "=" "?" ] @operator diff --git a/runtime/queries/zig/indents.toml b/runtime/queries/zig/indents.toml index 88f88e16..36ba8e55 100644 --- a/runtime/queries/zig/indents.toml +++ b/runtime/queries/zig/indents.toml @@ -3,6 +3,9 @@ indent = [ "BlockExpr", "ContainerDecl", "SwitchExpr", + "AssignExpr", + "ErrorUnionExpr", + "Statement", "InitList" ] diff --git a/runtime/queries/zig/injections.scm b/runtime/queries/zig/injections.scm new file mode 100644 index 00000000..3df95897 --- /dev/null +++ b/runtime/queries/zig/injections.scm @@ -0,0 +1,2 @@ +([(line_comment) (doc_comment)] @injection.content + (#set! injection.language "comment")) diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml index d65995c0..d19863e0 100644 --- a/runtime/themes/base16_default_dark.toml +++ b/runtime/themes/base16_default_dark.toml @@ -1,27 +1,28 @@ -# Author: RayGervais<raygervais@hotmail.ca> +# Author: RayGervais <raygervais@hotmail.ca> "ui.background" = { bg = "base00" } "ui.menu" = "base01" -"ui.menu.selected" = { fg = "base04", bg = "base01" } -"ui.linenr" = {fg = "base01" } +"ui.menu.selected" = { fg = "base01", bg = "base04" } +"ui.linenr" = { fg = "base03", bg = "base01" } "ui.popup" = { bg = "base01" } "ui.window" = { bg = "base01" } -"ui.liner.selected" = "base02" -"ui.selection" = "base02" -"comment" = "base03" -"ui.statusline" = {fg = "base04", bg = "base01" } +"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] } +"ui.selection" = { bg = "base02" } +"comment" = { fg = "base03", modifiers = ["italic"] } +"ui.statusline" = { fg = "base04", bg = "base01" } "ui.help" = { fg = "base04", bg = "base01" } -"ui.cursor" = { fg = "base05", modifiers = ["reversed"] } -"ui.text" = { fg = "base05" } +"ui.cursor" = { fg = "base04", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] } +"ui.text" = "base05" "operator" = "base05" -"ui.text.focus" = { fg = "base05" } +"ui.text.focus" = "base05" "variable" = "base08" "constant.numeric" = "base09" "constant" = "base09" -"attributes" = "base09" +"attributes" = "base09" "type" = "base0A" "ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] } -"strings" = "base0B" +"string" = "base0B" "variable.other.member" = "base0B" "constant.character.escape" = "base0C" "function" = "base0D" @@ -30,15 +31,28 @@ "keyword" = "base0E" "label" = "base0E" "namespace" = "base0E" -"ui.popup" = { bg = "base01" } -"ui.window" = { bg = "base00" } -"ui.help" = { bg = "base01", fg = "base06" } +"ui.help" = { fg = "base06", bg = "base01" } + +"markup.heading" = "base0D" +"markup.list" = "base08" +"markup.bold" = { fg = "base0A", modifiers = ["bold"] } +"markup.italic" = { fg = "base0E", modifiers = ["italic"] } +"markup.link.url" = { fg = "base09", modifiers = ["underlined"] } +"markup.link.text" = "base08" +"markup.quote" = "base0C" +"markup.raw" = "base0B" + +"diff.plus" = "base0B" +"diff.delta" = "base09" +"diff.minus" = "base08" -"info" = "base03" +"diagnostic" = { modifiers = ["underlined"] } +"ui.gutter" = { bg = "base01" } +"info" = "base0D" "hint" = "base03" "debug" = "base03" -"diagnostic" = "base03" -"error" = "base0E" +"warning" = "base09" +"error" = "base08" [palette] base00 = "#181818" # Default Background diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml new file mode 100644 index 00000000..483e87cc --- /dev/null +++ b/runtime/themes/base16_default_light.toml @@ -0,0 +1,73 @@ +# Author: NNB <nnbnh@protonmail.com> + +"ui.background" = { bg = "base00" } +"ui.menu" = "base01" +"ui.menu.selected" = { fg = "base01", bg = "base04" } +"ui.linenr" = { fg = "base03", bg = "base01" } +"ui.popup" = { bg = "base01" } +"ui.window" = { bg = "base01" } +"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] } +"ui.selection" = { bg = "base02" } +"comment" = { fg = "base03", modifiers = ["italic"] } +"ui.statusline" = { fg = "base04", bg = "base01" } +"ui.help" = { fg = "base04", bg = "base01" } +"ui.cursor" = { fg = "base04", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] } +"ui.text" = "base05" +"operator" = "base05" +"ui.text.focus" = "base05" +"variable" = "base08" +"constant.numeric" = "base09" +"constant" = "base09" +"attributes" = "base09" +"type" = "base0A" +"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] } +"string" = "base0B" +"variable.other.member" = "base0B" +"constant.character.escape" = "base0C" +"function" = "base0D" +"constructor" = "base0D" +"special" = "base0D" +"keyword" = "base0E" +"label" = "base0E" +"namespace" = "base0E" +"ui.help" = { fg = "base06", bg = "base01" } + +"markup.heading" = "base0D" +"markup.list" = "base08" +"markup.bold" = { fg = "base0A", modifiers = ["bold"] } +"markup.italic" = { fg = "base0E", modifiers = ["italic"] } +"markup.link.url" = { fg = "base09", modifiers = ["underlined"] } +"markup.link.text" = "base08" +"markup.quote" = "base0C" +"markup.raw" = "base0B" + +"diff.plus" = "base0B" +"diff.delta" = "base09" +"diff.minus" = "base08" + +"diagnostic" = { modifiers = ["underlined"] } +"ui.gutter" = { bg = "base01" } +"info" = "base0D" +"hint" = "base03" +"debug" = "base03" +"warning" = "base09" +"error" = "base08" + +[palette] +base00 = "#f8f8f8" # Default Background +base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks) +base02 = "#d8d8d8" # Selection Background +base03 = "#b8b8b8" # Comments, Invisibles, Line Highlighting +base04 = "#585858" # Dark Foreground (Used for status bars) +base05 = "#383838" # Default Foreground, Caret, Delimiters, Operators +base06 = "#282828" # Light Foreground (Not often used) +base07 = "#181818" # Light Background (Not often used) +base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted +base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url +base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background +base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted +base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes +base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings +base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed +base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?> diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml new file mode 100644 index 00000000..23240d8d --- /dev/null +++ b/runtime/themes/base16_terminal.toml @@ -0,0 +1,52 @@ +# Author: NNB <nnbnh@protonmail.com> + +"ui.menu" = "black" +"ui.menu.selected" = { modifiers = ["reversed"] } +"ui.linenr" = { fg = "light-gray", bg = "black" } +"ui.popup" = { bg = "black" } +"ui.window" = { bg = "black" } +"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] } +"ui.selection" = { fg = "gray", modifiers = ["reversed"] } +"comment" = { fg = "light-gray", modifiers = ["italic"] } +"ui.statusline" = { fg = "white", bg = "black" } +"ui.statusline.inactive" = { fg = "gray", bg = "black" } +"ui.help" = { fg = "white", bg = "black" } +"ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "light-white", modifiers = ["reversed"] } +"variable" = "light-red" +"constant.numeric" = "yellow" +"constant" = "yellow" +"attributes" = "yellow" +"type" = "light-yellow" +"ui.cursor.match" = { fg = "light-yellow", modifiers = ["underlined"] } +"string" = "light-green" +"variable.other.member" = "light-green" +"constant.character.escape" = "light-cyan" +"function" = "light-blue" +"constructor" = "light-blue" +"special" = "light-blue" +"keyword" = "light-magenta" +"label" = "light-magenta" +"namespace" = "light-magenta" +"ui.help" = { fg = "white", bg = "black" } + +"markup.heading" = "light-blue" +"markup.list" = "light-red" +"markup.bold" = { fg = "light-yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "light-magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "light-red" +"markup.quote" = "light-cyan" +"markup.raw" = "light-green" + +"diff.plus" = "light-green" +"diff.delta" = "yellow" +"diff.minus" = "light-red" + +"diagnostic" = { modifiers = ["underlined"] } +"ui.gutter" = { bg = "black" } +"info" = "light-blue" +"hint" = "gray" +"debug" = "gray" +"warning" = "yellow" +"error" = "light-red" diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml index 86a6c34b..32b58d0a 100644 --- a/runtime/themes/bogster.toml +++ b/runtime/themes/bogster.toml @@ -28,6 +28,20 @@ "module" = "#d32c5d" +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = "#59dcb7" +"diff.delta" = "#dc7759" +"diff.minus" = "#dc597f" + "ui.background" = { bg = "#161c23" } "ui.linenr" = { fg = "#415367" } "ui.linenr.selected" = { fg = "#e5ded6" } # TODO @@ -49,3 +63,6 @@ "error" = "#dc597f" "info" = "#59dcb7" "hint" = "#59c0dc" + +# make diagnostic underlined, to distinguish with selection text. +diagnostic = { modifiers = ["underlined"] } diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 0554f827..ab7c16ec 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -39,6 +39,20 @@ "constant.numeric" = { fg = "pale_green" } "constant.character.escape" = { fg = "gold" } +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = { fg = "pale_green" } +"diff.delta" = { fg = "gold" } +"diff.minus" = { fg = "red" } + "ui.background" = { fg = "light_gray", bg = "dark_gray2" } "ui.window" = { bg = "widget" } diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml new file mode 100644 index 00000000..1db25d8f --- /dev/null +++ b/runtime/themes/dracula.toml @@ -0,0 +1,63 @@ +# Author : Sebastian Zivota <loewenheim@mailbox.org> +"comment" = { fg = "comment" } +"constant" = { fg = "purple" } +"constant.character.escape" = { fg = "pink" } +"function" = { fg = "green" } +"keyword" = { fg = "pink" } +"operator" = { fg = "pink" } +"punctuation" = { fg = "foreground" } +"string" = { fg = "yellow" } +"string.regexp" = { fg = "red" } +"tag" = { fg = "pink" } +"type" = { fg = "cyan", modifiers = ["italic"] } +"type.enum.variant" = { fg = "foreground", modifiers = ["italic"] } +"variable" = { fg = "foreground" } +"variable.builtin" = { fg = "cyan", modifiers = ["italic"] } +"variable.parameter" = { fg ="orange", modifiers = ["italic"] } + +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + +"ui.background" = { fg = "foreground", bg = "background" } +"ui.cursor" = { fg = "background", bg = "orange", modifiers = ["dim"] } +"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] } +"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] } +"ui.help" = { fg = "foreground", bg = "background_dark" } +"ui.linenr" = { fg = "comment" } +"ui.linenr.selected" = { fg = "foreground" } +"ui.menu" = { fg = "foreground", bg = "background_dark" } +"ui.menu.selected" = { fg = "cyan", bg = "background_dark" } +"ui.popup" = { fg = "foreground", bg = "background_dark" } +"ui.selection" = { fg = "background", bg = "purple", modifiers = ["dim"] } +"ui.selection.primary" = { fg = "background", bg = "pink" } +"ui.statusline" = { fg = "foreground", bg = "background_dark" } +"ui.statusline.inactive" = { fg = "comment", bg = "background_dark" } +"ui.text" = { fg = "foreground" } +"ui.text.focus" = { fg = "cyan" } +"ui.window" = { fg = "foreground" } + +"error" = { fg = "red" } +"warning" = { fg = "cyan" } + +"markup.heading" = { fg = "purple", modifiers = ["bold"] } +"markup.list" = "cyan" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "yellow", modifiers = ["italic"] } +"markup.link.url" = "cyan" +"markup.link.text" = "pink" +"markup.quote" = { fg = "yellow", modifiers = ["italic"] } +"markup.raw" = { fg = "foreground" } + +[palette] +background = "#282a36" +background_dark = "#21222c" +foreground = "#f8f8f2" +comment = "#6272a4" +red = "#ff5555" +orange = "#ffb86c" +yellow = "#f1fa8c" +green = "#50fa7b" +purple = "#bd93f9" +cyan = "#8be9fd" +pink = "#ff79c6" diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml index bbd005e6..a6389da2 100644 --- a/runtime/themes/everforest_dark.toml +++ b/runtime/themes/everforest_dark.toml @@ -12,7 +12,7 @@ "type" = "yellow" "constant" = "purple" "constant.numeric" = "purple" -"string" = "grey2" +"string" = "green" "comment" = "grey0" "variable" = "fg" "variable.builtin" = "blue" @@ -34,6 +34,20 @@ "module" = "blue" "special" = "orange" +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = "green" +"diff.delta" = "orange" +"diff.minus" = "red" + "ui.background" = { bg = "bg0" } "ui.cursor" = { fg = "bg0", bg = "fg" } "ui.cursor.match" = { fg = "orange", bg = "bg_yellow" } diff --git a/runtime/themes/everforest_light.toml b/runtime/themes/everforest_light.toml new file mode 100644 index 00000000..5490adb3 --- /dev/null +++ b/runtime/themes/everforest_light.toml @@ -0,0 +1,100 @@ +# Everforest (Dark Hard) +# Author: CptPotato + +# Original Author: +# URL: https://github.com/sainnhe/everforest +# Filename: autoload/everforest.vim +# Author: sainnhe +# Email: sainnhe@gmail.com +# License: MIT License + +"constant.character.escape" = "orange" +"type" = "yellow" +"constant" = "purple" +"constant.numeric" = "purple" +"string" = "green" +"comment" = "grey0" +"variable" = "fg" +"variable.builtin" = "blue" +"variable.parameter" = "fg" +"variable.other.member" = "fg" +"label" = "aqua" +"punctuation" = "grey2" +"punctuation.delimiter" = "grey2" +"punctuation.bracket" = "fg" +"keyword" = "red" +"operator" = "orange" +"function" = "green" +"function.builtin" = "blue" +"function.macro" = "aqua" +"tag" = "yellow" +"namespace" = "aqua" +"attribute" = "aqua" +"constructor" = "yellow" +"module" = "blue" +"special" = "orange" + +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = "green" +"diff.delta" = "orange" +"diff.minus" = "red" + +"ui.background" = { bg = "bg0" } +"ui.cursor" = { fg = "bg0", bg = "fg" } +"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" } +"ui.cursor.insert" = { fg = "bg0", bg = "grey1" } +"ui.cursor.select" = { fg = "bg0", bg = "blue" } +"ui.linenr" = "grey0" +"ui.linenr.selected" = "fg" +"ui.statusline" = { fg = "grey2", bg = "bg2" } +"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" } +"ui.popup" = { fg = "grey2", bg = "bg1" } +"ui.window" = { fg = "grey2", bg = "bg1" } +"ui.help" = { fg = "fg", bg = "bg1" } +"ui.text" = "fg" +"ui.text.focus" = "fg" +"ui.menu" = { fg = "fg", bg = "bg2" } +"ui.menu.selected" = { fg = "bg0", bg = "green" } +"ui.selection" = { bg = "bg3" } + +"hint" = "blue" +"info" = "aqua" +"warning" = "yellow" +"error" = "red" +"diagnostic" = { modifiers = ["underlined"] } + + +[palette] + +bg0 = "#fff9e8" +bg1 = "#f7f4e0" +bg2 = "#f0eed9" +bg3 = "#e9e8d2" +bg4 = "#e1ddcb" +bg5 = "#bec5b2" +bg_visual = "#edf0cd" +bg_red = "#fce5dc" +bg_green = "#f1f3d4" +bg_blue = "#eaf2eb" +bg_yellow = "#fbefd0" + +fg = "#5c6a72" +red = "#f85552" +orange = "#f57d26" +yellow = "#dfa000" +green = "#8da101" +aqua = "#35a77c" +blue = "#3a94c5" +purple = "#df69ba" +grey0 = "#a6b0a0" +grey1 = "#939f91" +grey2 = "#829181" diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index 0ff039ea..a976a9bd 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -19,6 +19,7 @@ "function" = { fg = "green1", modifiers = ["bold"] } "function.macro" = "aqua1" "function.builtin" = "yellow1" +"tag" = "red1" "comment" = { fg = "gray1", modifiers = ["italic"] } "constant" = { fg = "purple1" } "constant.builtin" = { fg = "purple1", modifiers = ["bold"] } @@ -28,6 +29,10 @@ "label" = "aqua1" "module" = "aqua1" +"diff.plus" = "green1" +"diff.delta" = "orange1" +"diff.minus" = "red1" + "warning" = { fg = "orange1", bg = "bg1" } "error" = { fg = "red1", bg = "bg1" } "info" = { fg = "aqua1", bg = "bg1" } @@ -51,6 +56,13 @@ "diagnostic" = { modifiers = ["underlined"] } +"markup.heading" = "aqua1" +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.link.url" = { fg = "green1", modifiers = ["underlined"] } +"markup.link.text" = "red1" +"markup.raw" = "red1" + [palette] bg0 = "#282828" # main background bg1 = "#3c3836" diff --git a/runtime/themes/gruvbox_light.toml b/runtime/themes/gruvbox_light.toml new file mode 100644 index 00000000..81ea7fd1 --- /dev/null +++ b/runtime/themes/gruvbox_light.toml @@ -0,0 +1,96 @@ +# Author : Rohan Jain <crodjer@pm.me> +# Author : Jakub Bartodziej <kubabartodziej@gmail.com> +# The theme uses the gruvbox light palette with standard contrast: github.com/morhetz/gruvbox + +"attribute" = "aqua1" +"keyword" = { fg = "red1" } +"keyword.directive" = "red0" +"namespace" = "aqua1" +"punctuation" = "orange1" +"punctuation.delimiter" = "orange1" +"operator" = "purple1" +"special" = "purple0" +"variable.other.member" = "blue1" +"variable" = "fg1" +"variable.builtin" = "orange1" +"variable.parameter" = "fg2" +"type" = "yellow1" +"type.builtin" = "yellow1" +"constructor" = { fg = "purple1", modifiers = ["bold"] } +"function" = { fg = "green1", modifiers = ["bold"] } +"function.macro" = "aqua1" +"function.builtin" = "yellow1" +"tag" = "red1" +"comment" = { fg = "gray1", modifiers = ["italic"] } +"constant" = { fg = "purple1" } +"constant.builtin" = { fg = "purple1", modifiers = ["bold"] } +"string" = "green1" +"constant.numeric" = "purple1" +"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] } +"label" = "aqua1" +"module" = "aqua1" + +"diff.plus" = "green1" +"diff.delta" = "orange1" +"diff.minus" = "red1" + +"warning" = { fg = "orange1", bg = "bg1" } +"error" = { fg = "red1", bg = "bg1" } +"info" = { fg = "aqua1", bg = "bg1" } +"hint" = { fg = "blue1", bg = "bg1" } + +"ui.background" = { bg = "bg0" } +"ui.linenr" = { fg = "bg4" } +"ui.linenr.selected" = { fg = "yellow1" } +"ui.statusline" = { fg = "fg1", bg = "bg2" } +"ui.statusline.inactive" = { fg = "fg4", bg = "bg1" } +"ui.popup" = { bg = "bg1" } +"ui.window" = { bg = "bg1" } +"ui.help" = { bg = "bg1", fg = "fg1" } +"ui.text" = { fg = "fg1" } +"ui.text.focus" = { fg = "fg1" } +"ui.selection" = { bg = "bg3", modifiers = ["reversed"] } +"ui.cursor.primary" = { modifiers = ["reversed"] } +"ui.cursor.match" = { modifiers = ["reversed"] } +"ui.menu" = { fg = "fg1", bg = "bg2" } +"ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] } + +"diagnostic" = { modifiers = ["underlined"] } + +"markup.heading" = "aqua1" +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.link.url" = { fg = "green1", modifiers = ["underlined"] } +"markup.link.text" = "red1" +"markup.raw" = "red1" + +[palette] +bg0 = "#fbf1c7" # main background +bg1 = "#ebdbb2" +bg2 = "#d5c4a1" +bg3 = "#bdae93" +bg4 = "#a89984" + +fg0 = "#282828" # main foreground +fg1 = "#3c3836" +fg2 = "#504945" +fg3 = "#665c54" +fg4 = "#7c6f64" # gray0 + +gray0 = "#7c6f64" +gray1 = "#928374" + +red0 = "#cc241d" # neutral +red1 = "#9d0006" # bright +green0 = "#98971a" +green1 = "#79740e" +yellow0 = "#d79921" +yellow1 = "#b57614" +blue0 = "#458588" +blue1 = "#076678" +purple0 = "#b16286" +purple1 = "#8f3f71" +aqua0 = "#689d6a" +aqua1 = "#427b58" +orange0 = "#d65d0e" +orange1 = "#af3a03" diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml index 30829475..a7c33e2d 100644 --- a/runtime/themes/ingrid.toml +++ b/runtime/themes/ingrid.toml @@ -28,6 +28,20 @@ "module" = "#839A53" +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = "#839A53" +"diff.delta" = "#D4A520" +"diff.minus" = "#D74E50" + "ui.background" = { bg = "#FFFCFD" } "ui.linenr" = { fg = "#bbbbbb" } "ui.linenr.selected" = { fg = "#F3EAE9" } # TODO diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index 38f9f170..e6ff0a5e 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -39,6 +39,20 @@ "constant.numeric" = { fg = "#ae81ff" } "constant.character.escape" = { fg = "#ae81ff" } +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = { fg = "#a6e22e" } +"diff.delta" = { fg = "#fd971f" } +"diff.minus" = { fg = "#f92672" } + "ui.background" = { fg = "text", bg = "background" } "ui.window" = { bg = "widget" } @@ -65,7 +79,7 @@ "warning" = { fg = "#cca700" } "error" = { fg = "#f48771" } "info" = { fg = "#75beff" } -"hint" = { fg = "#eeeeeeb3" } +"hint" = { fg = "#eeeeeb3" } diagnostic = { modifiers = ["underlined"] } diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml new file mode 100644 index 00000000..8de9994c --- /dev/null +++ b/runtime/themes/monokai_pro.toml @@ -0,0 +1,115 @@ +# Author : WindSoilder<WindSoilder@outlook.com> +# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro +# Credit goes to the original creator: https://monokai.pro + +"ui.linenr.selected" = { bg = "base3" } +"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] } +"ui.menu.selected" = { fg = "base2", bg = "yellow" } + +"info" = "base8" +"hint" = "base8" + +# background color +"ui.background" = { bg = "base2" } +"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" } + +# status bars, panels, modals, autocompletion +"ui.statusline" = { bg = "base4" } +"ui.popup" = { bg = "base3" } +"ui.window" = { bg = "base3" } +"ui.help" = { bg = "base3" } + +# active line, highlighting +"ui.selection" = { bg = "base4" } +"ui.cursor.match" = { bg = "base4" } + +# comments, nord3 based lighter color +"comment" = { fg = "base5", modifiers = ["italic"] } +"ui.linenr" = { fg = "base5" } + +# cursor, variables, constants, attributes, fields +"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] } +"attribute" = "blue" +"variable" = "base8" +"constant" = "orange" +"variable.builtin" = "red" +"constant.builtin" = "red" +"namespace" = "base8" + +# base text, punctuation +"ui.text" = { fg = "base8" } +"punctuation" = "base6" + +# classes, types, primiatives +"type" = "green" +"type.builtin" = { fg = "red"} +"label" = "base8" + +# declaration, methods, routines +"constructor" = "blue" +"function" = "green" +"function.macro" = { fg = "blue" } +"function.builtin" = { fg = "cyan" } + +# operator, tags, units, punctuations +"operator" = "red" +"variable.other.member" = "base8" + +# keywords, special +"keyword" = { fg = "red" } +"keyword.directive" = "blue" +"variable.parameter" = "#f59762" + +# error +"error" = "red" + +# annotations, decorators +"special" = "#f59762" +"module" = "#f59762" + +# warnings, escape characters, regex +"warning" = "orange" +"constant.character.escape" = { fg = "base8" } + +# strings +"string" = "yellow" + +# integer, floating point +"constant.numeric" = "purple" + +# vcs +"diff.plus" = "green" +"diff.delta" = "orange" +"diff.minus" = "red" + +# make diagnostic underlined, to distinguish with selection text. +diagnostic = { modifiers = ["underlined"] } + +# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default +"markup.heading" = "green" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "orange", modifiers = ["italic"] } +"markup.link.url" = { fg = "orange", modifiers = ["underlined"] } +"markup.link.text" = "yellow" +"markup.quote" = "green" + +[palette] +# primary colors +"red" = "#ff6188" +"orange" = "#fc9867" +"yellow" = "#ffd866" +"green" = "#a9dc76" +"blue" = "#78dce8" +"purple" = "#ab9df2" +# base colors, sorted from darkest to lightest +"base0" = "#19181a" +"base1" = "#221f22" +"base2" = "#2d2a2e" +"base3" = "#403e41" +"base4" = "#5b595c" +"base5" = "#727072" +"base6" = "#939293" +"base7" = "#c1c0c0" +"base8" = "#fcfcfa" +# variants (for when transparency isn't supported) +"base8x0c" = "#363337" # using base2 as bg diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml new file mode 100644 index 00000000..c5890042 --- /dev/null +++ b/runtime/themes/monokai_pro_machine.toml @@ -0,0 +1,115 @@ +# Author : WindSoilder<WindSoilder@outlook.com> +# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro +# Credit goes to the original creator: https://monokai.pro + +"ui.linenr.selected" = { bg = "base3" } +"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] } +"ui.menu.selected" = { fg = "base2", bg = "yellow" } + +"info" = "base8" +"hint" = "base8" + +# background color +"ui.background" = { bg = "base2" } +"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" } + +# status bars, panels, modals, autocompletion +"ui.statusline" = { bg = "base4" } +"ui.popup" = { bg = "base3" } +"ui.window" = { bg = "base3" } +"ui.help" = { bg = "base3" } + +# active line, highlighting +"ui.selection" = { bg = "base4" } +"ui.cursor.match" = { bg = "base4" } + +# comments, nord3 based lighter color +"comment" = { fg = "base5", modifiers = ["italic"] } +"ui.linenr" = { fg = "base5" } + +# cursor, variables, constants, attributes, fields +"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] } +"attribute" = "blue" +"variable" = "base8" +"constant" = "orange" +"variable.builtin" = "red" +"constant.builtin" = "red" +"namespace" = "base8" + +# base text, punctuation +"ui.text" = { fg = "base8" } +"punctuation" = "base6" + +# classes, types, primiatives +"type" = "green" +"type.builtin" = { fg = "red"} +"label" = "base8" + +# declaration, methods, routines +"constructor" = "blue" +"function" = "green" +"function.macro" = { fg = "blue" } +"function.builtin" = { fg = "cyan" } + +# operator, tags, units, punctuations +"operator" = "red" +"variable.other.member" = "base8" + +# keywords, special +"keyword" = { fg = "red" } +"keyword.directive" = "blue" +"variable.parameter" = "#f59762" + +# error +"error" = "red" + +# annotations, decorators +"special" = "#f59762" +"module" = "#f59762" + +# warnings, escape characters, regex +"warning" = "orange" +"constant.character.escape" = { fg = "base8" } + +# strings +"string" = "yellow" + +# integer, floating point +"constant.numeric" = "purple" + +# vcs +"diff.plus" = "green" +"diff.delta" = "orange" +"diff.minus" = "red" + +# make diagnostic underlined, to distinguish with selection text. +diagnostic = { modifiers = ["underlined"] } + +# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default +"markup.heading" = "green" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "orange", modifiers = ["italic"] } +"markup.link.url" = { fg = "orange", modifiers = ["underlined"] } +"markup.link.text" = "yellow" +"markup.quote" = "green" + +[palette] +# primary colors +"red" = "#ff6d7e" +"orange" = "#ffb270" +"yellow" = "#ffed72" +"green" = "#a2e57b" +"blue" = "#7cd5f1" +"purple" = "#baa0f8" +# base colors +"base0" = "#161b1e" +"base1" = "#1d2528" +"base2" = "#273136" +"base3" = "#3a4449" +"base4" = "#545f62" +"base5" = "#6b7678" +"base6" = "#798384" +"base7" = "#b8c4c3" +"base8" = "#f2fffc" +# variants +"base8x0c" = "#303a3e" diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml new file mode 100644 index 00000000..d9badf3c --- /dev/null +++ b/runtime/themes/monokai_pro_octagon.toml @@ -0,0 +1,115 @@ +# Author : WindSoilder<WindSoilder@outlook.com> +# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro +# Credit goes to the original creator: https://monokai.pro + +"ui.linenr.selected" = { bg = "base3" } +"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] } +"ui.menu.selected" = { fg = "base2", bg = "yellow" } + +"info" = "base8" +"hint" = "base8" + +# background color +"ui.background" = { bg = "base2" } +"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" } + +# status bars, panels, modals, autocompletion +"ui.statusline" = { bg = "base4" } +"ui.popup" = { bg = "base3" } +"ui.window" = { bg = "base3" } +"ui.help" = { bg = "base3" } + +# active line, highlighting +"ui.selection" = { bg = "base4" } +"ui.cursor.match" = { bg = "base4" } + +# comments, nord3 based lighter color +"comment" = { fg = "base5", modifiers = ["italic"] } +"ui.linenr" = { fg = "base5" } + +# cursor, variables, constants, attributes, fields +"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] } +"attribute" = "blue" +"variable" = "base8" +"constant" = "orange" +"variable.builtin" = "red" +"constant.builtin" = "red" +"namespace" = "base8" + +# base text, punctuation +"ui.text" = { fg = "base8" } +"punctuation" = "base6" + +# classes, types, primiatives +"type" = "green" +"type.builtin" = { fg = "red"} +"label" = "base8" + +# declaration, methods, routines +"constructor" = "blue" +"function" = "green" +"function.macro" = { fg = "blue" } +"function.builtin" = { fg = "cyan" } + +# operator, tags, units, punctuations +"operator" = "red" +"variable.other.member" = "base8" + +# keywords, special +"keyword" = { fg = "red" } +"keyword.directive" = "blue" +"variable.parameter" = "#f59762" + +# error +"error" = "red" + +# annotations, decorators +"special" = "#f59762" +"module" = "#f59762" + +# warnings, escape characters, regex +"warning" = "orange" +"constant.character.escape" = { fg = "base8" } + +# strings +"string" = "yellow" + +# integer, floating point +"constant.numeric" = "purple" + +# vcs +"diff.plus" = "green" +"diff.delta" = "orange" +"diff.minus" = "red" + +# make diagnostic underlined, to distinguish with selection text. +diagnostic = { modifiers = ["underlined"] } + +# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default +"markup.heading" = "green" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "orange", modifiers = ["italic"] } +"markup.link.url" = { fg = "orange", modifiers = ["underlined"] } +"markup.link.text" = "yellow" +"markup.quote" = "green" + +[palette] +# primary colors +"red" = "#ff657a" +"orange" = "#ff9b5e" +"yellow" = "#ffd76d" +"green" = "#bad761" +"blue" = "#9cd1bb" +"purple" = "#c39ac9" +# base colors +"base0" = "#161821" +"base1" = "#1e1f2b" +"base2" = "#282a3a" +"base3" = "#3a3d4b" +"base4" = "#535763" +"base5" = "#696d77" +"base6" = "#767b81" +"base7" = "#b2b9bd" +"base8" = "#eaf2f1" +# variants +"base8x0c" = "#303342" diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml new file mode 100644 index 00000000..ed7ebeae --- /dev/null +++ b/runtime/themes/monokai_pro_ristretto.toml @@ -0,0 +1,115 @@ +# Author : WindSoilder<WindSoilder@outlook.com> +# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro +# Credit goes to the original creator: https://monokai.pro + +"ui.linenr.selected" = { bg = "base3" } +"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] } +"ui.menu.selected" = { fg = "base2", bg = "yellow" } + +"info" = "base8" +"hint" = "base8" + +# background color +"ui.background" = { bg = "base2" } +"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" } + +# status bars, panels, modals, autocompletion +"ui.statusline" = { bg = "base4" } +"ui.popup" = { bg = "base3" } +"ui.window" = { bg = "base3" } +"ui.help" = { bg = "base3" } + +# active line, highlighting +"ui.selection" = { bg = "base4" } +"ui.cursor.match" = { bg = "base4" } + +# comments, nord3 based lighter color +"comment" = { fg = "base5", modifiers = ["italic"] } +"ui.linenr" = { fg = "base5" } + +# cursor, variables, constants, attributes, fields +"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] } +"attribute" = "blue" +"variable" = "base8" +"constant" = "orange" +"variable.builtin" = "red" +"constant.builtin" = "red" +"namespace" = "base8" + +# base text, punctuation +"ui.text" = { fg = "base8" } +"punctuation" = "base6" + +# classes, types, primiatives +"type" = "green" +"type.builtin" = { fg = "red"} +"label" = "base8" + +# declaration, methods, routines +"constructor" = "blue" +"function" = "green" +"function.macro" = { fg = "blue" } +"function.builtin" = { fg = "cyan" } + +# operator, tags, units, punctuations +"operator" = "red" +"variable.other.member" = "base8" + +# keywords, special +"keyword" = { fg = "red" } +"keyword.directive" = "blue" +"variable.parameter" = "#f59762" + +# error +"error" = "red" + +# annotations, decorators +"special" = "#f59762" +"module" = "#f59762" + +# warnings, escape characters, regex +"warning" = "orange" +"constant.character.escape" = { fg = "base8" } + +# strings +"string" = "yellow" + +# integer, floating point +"constant.numeric" = "purple" + +# vcs +"diff.plus" = "green" +"diff.delta" = "orange" +"diff.minus" = "red" + +# make diagnostic underlined, to distinguish with selection text. +diagnostic = { modifiers = ["underlined"] } + +# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default +"markup.heading" = "green" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "orange", modifiers = ["italic"] } +"markup.link.url" = { fg = "orange", modifiers = ["underlined"] } +"markup.link.text" = "yellow" +"markup.quote" = "green" + +[palette] +# primary colors +"red" = "#fd6883" +"orange" = "#f38d70" +"yellow" = "#f9cc6c" +"green" = "#adda78" +"blue" = "#85dacc" +"purple" = "#a8a9eb" +# base colors +"base0" = "#191515" +"base1" = "#211c1c" +"base2" = "#2c2525" +"base3" = "#403838" +"base4" = "#5b5353" +"base5" = "#72696a" +"base6" = "#8c8384" +"base7" = "#c3b7b8" +"base8" = "#fff1f3" +# variants +"base8x0c" = "#352e2e" diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml new file mode 100644 index 00000000..da06e597 --- /dev/null +++ b/runtime/themes/monokai_pro_spectrum.toml @@ -0,0 +1,115 @@ +# Author : WindSoilder<WindSoilder@outlook.com> +# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro +# Credit goes to the original creator: https://monokai.pro + +"ui.linenr.selected" = { bg = "base3" } +"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] } +"ui.menu.selected" = { fg = "base2", bg = "yellow" } + +"info" = "base8" +"hint" = "base8" + +# background color +"ui.background" = { bg = "base2" } +"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" } + +# status bars, panels, modals, autocompletion +"ui.statusline" = { bg = "base4" } +"ui.popup" = { bg = "base3" } +"ui.window" = { bg = "base3" } +"ui.help" = { bg = "base3" } + +# active line, highlighting +"ui.selection" = { bg = "base4" } +"ui.cursor.match" = { bg = "base4" } + +# comments, nord3 based lighter color +"comment" = { fg = "base5", modifiers = ["italic"] } +"ui.linenr" = { fg = "base5" } + +# cursor, variables, constants, attributes, fields +"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] } +"attribute" = "blue" +"variable" = "base8" +"constant" = "orange" +"variable.builtin" = "red" +"constant.builtin" = "red" +"namespace" = "base8" + +# base text, punctuation +"ui.text" = { fg = "base8" } +"punctuation" = "base6" + +# classes, types, primiatives +"type" = "green" +"type.builtin" = { fg = "red"} +"label" = "base8" + +# declaration, methods, routines +"constructor" = "blue" +"function" = "green" +"function.macro" = { fg = "blue" } +"function.builtin" = { fg = "cyan" } + +# operator, tags, units, punctuations +"operator" = "red" +"variable.other.member" = "base8" + +# keywords, special +"keyword" = { fg = "red" } +"keyword.directive" = "blue" +"variable.parameter" = "#f59762" + +# error +"error" = "red" + +# annotations, decorators +"special" = "#f59762" +"module" = "#f59762" + +# warnings, escape characters, regex +"warning" = "orange" +"constant.character.escape" = { fg = "base8" } + +# strings +"string" = "yellow" + +# integer, floating point +"constant.numeric" = "purple" + +# vcs +"diff.plus" = "green" +"diff.delta" = "orange" +"diff.minus" = "red" + +# make diagnostic underlined, to distinguish with selection text. +diagnostic = { modifiers = ["underlined"] } + +# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default +"markup.heading" = "green" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "orange", modifiers = ["italic"] } +"markup.link.url" = { fg = "orange", modifiers = ["underlined"] } +"markup.link.text" = "yellow" +"markup.quote" = "green" + +[palette] +# primary colors +"red" = "#fc618d" +"orange" = "#fd9353" +"yellow" = "#fce566" +"green" = "#7bd88f" +"blue" = "#5ad4e6" +"purple" = "#948ae3" +# base colors +"base0" = "#131313" +"base1" = "#191919" +"base2" = "#222222" +"base3" = "#363537" +"base4" = "#525053" +"base5" = "#69676c" +"base6" = "#8b888f" +"base7" = "#bab6c0" +"base8" = "#f7f1ff" +# variants +"base8x0c" = "#2b2b2b" diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml index a619f902..deb90452 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -84,6 +84,21 @@ # nord15 - integer, floating point "constant.numeric" = "nord15" +# TODO markup +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +# vcs +"diff.plus" = "nord14" +"diff.delta" = "nord12" +"diff.minus" = "nord11" + [palette] nord0 = "#2e3440" nord1 = "#3b4252" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 40ed1abe..acdaf99c 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -3,27 +3,42 @@ "attribute" = { fg = "yellow" } "comment" = { fg = "light-gray", modifiers = ["italic"] } "constant" = { fg = "cyan" } -"constant.builtin" = { fg = "blue" } +"constant.numeric" = { fg = "gold" } +"constant.builtin" = { fg = "gold" } +"constant.character.escape" = { fg = "gold" } "constructor" = { fg = "blue" } -"escape" = { fg = "gold" } "function" = { fg = "blue" } "function.builtin" = { fg = "blue" } "function.macro" = { fg = "purple" } "keyword" = { fg = "red" } "keyword.control" = { fg = "purple" } +"keyword.control.import" = { fg = "red" } "keyword.directive" = { fg = "purple" } "label" = { fg = "purple" } "namespace" = { fg = "blue" } -"number" = { fg = "gold" } "operator" = { fg = "purple" } +"keyword.operator" = { fg = "purple" } "property" = { fg = "red" } "special" = { fg = "blue" } "string" = { fg = "green" } "type" = { fg = "yellow" } -"type.builtin" = { fg = "yellow" } # "variable" = { fg = "blue" } "variable.builtin" = { fg = "blue" } "variable.parameter" = { fg = "red" } +"variable.other.member" = { fg = "red" } + +"markup.heading" = { fg = "red" } +"markup.raw.inline" = { fg = "green" } +"markup.bold" = { fg = "gold", modifiers = ["bold"] } +"markup.italic" = { fg = "purple", modifiers = ["italic"] } +"markup.list" = { fg = "red" } +"markup.quote" = { fg = "yellow" } +"markup.link.url" = { fg = "cyan", modifiers = ["underlined"]} +"markup.link.text" = { fg = "purple" } + +"diff.plus" = "green" +"diff.delta" = "gold" +"diff.minus" = "red" diagnostic = { modifiers = ["underlined"] } "info" = { fg = "blue", modifiers = ["bold"] } diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index 53777008..66717bb2 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -1,15 +1,15 @@ # Author: RayGervais<raygervais@hotmail.ca> +# Author: ChrisHa<chunghha@users.noreply.github.com> "ui.background" = { bg = "base" } -"ui.menu" = "surface" +"ui.menu" = { fg = "text", bg = "overlay" } "ui.menu.selected" = { fg = "iris", bg = "surface" } "ui.linenr" = {fg = "subtle" } -"ui.popup" = { bg = "overlay" } -"ui.window" = { bg = "overlay" } "ui.liner.selected" = "highlightOverlay" "ui.selection" = "highlight" "comment" = "subtle" "ui.statusline" = {fg = "foam", bg = "surface" } +"ui.statusline.inactive" = { fg = "iris", bg = "surface" } "ui.help" = { fg = "foam", bg = "surface" } "ui.cursor" = { fg = "rose", modifiers = ["reversed"] } "ui.text" = { fg = "text" } @@ -32,10 +32,13 @@ "keyword" = "pine" "label" = "iris" "namespace" = "pine" -"ui.popup" = { bg = "overlay" } +"ui.popup" = { bg = "surface" } "ui.window" = { bg = "base" } "ui.help" = { bg = "overlay", fg = "foam" } "text" = "text" +"diff.plus" = "foam" +"diff.delta" = "rose" +"diff.minus" = "love" "info" = "gold" "hint" = "gold" @@ -43,6 +46,15 @@ "diagnostic" = "rose" "error" = "love" +"markup.heading" = { fg = "rose" } +"markup.raw.inline" = { fg = "foam" } +"markup.bold" = { fg = "gold", modifiers = ["bold"] } +"markup.italic" = { fg = "iris", modifiers = ["italic"] } +"markup.list" = { fg = "love" } +"markup.quote" = { fg = "rose" } +"markup.link.url" = { fg = "pine", modifiers = ["underlined"]} +"markup.link.text" = { fg = "foam" } + [palette] base = "#191724" surface = "#1f1d2e" diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml new file mode 100644 index 00000000..bec77506 --- /dev/null +++ b/runtime/themes/rose_pine_dawn.toml @@ -0,0 +1,73 @@ +# Author: RayGervais<raygervais@hotmail.ca> +# Author: ChrisHa<chunghha@users.noreply.github.com> + +"ui.background" = { bg = "surface" } +"ui.menu" = { fg = "text", bg = "overlay" } +"ui.menu.selected" = { fg = "iris", bg = "surface" } +"ui.linenr" = {fg = "subtle" } +"ui.liner.selected" = "highlightOverlay" +"ui.selection" = "highlight" +"comment" = "subtle" +"ui.statusline" = {fg = "foam", bg = "surface" } +"ui.statusline.inactive" = { fg = "iris", bg = "surface" } +"ui.help" = { fg = "foam", bg = "surface" } +"ui.cursor" = { fg = "rose", modifiers = ["reversed"] } +"ui.text" = { fg = "text" } +"operator" = "rose" +"ui.text.focus" = { fg = "base05" } +"variable" = "text" +"number" = "iris" +"constant" = "gold" +"attributes" = "gold" +"type" = "foam" +"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] } +"string" = "gold" +"property" = "foam" +"escape" = "subtle" +"function" = "rose" +"function.builtin" = "rose" +"function.method" = "foam" +"constructor" = "gold" +"special" = "gold" +"keyword" = "pine" +"label" = "iris" +"namespace" = "pine" +"ui.popup" = { bg = "surface" } +"ui.window" = { bg = "base" } +"ui.help" = { bg = "overlay", fg = "foam" } +"text" = "text" +"diff.plus" = "foam" +"diff.delta" = "rose" +"diff.minus" = "love" + +"info" = "gold" +"hint" = "gold" +"debug" = "rose" +"diagnostic" = "rose" +"error" = "love" + +"markup.heading" = { fg = "rose" } +"markup.raw.inline" = { fg = "foam" } +"markup.bold" = { fg = "gold", modifiers = ["bold"] } +"markup.italic" = { fg = "iris", modifiers = ["italic"] } +"markup.list" = { fg = "love" } +"markup.quote" = { fg = "rose" } +"markup.link.url" = { fg = "pine", modifiers = ["underlined"]} +"markup.link.test" = { fg = "foam" } + +[palette] +base = "#faf4ed" +surface = "#fffaf3" +overlay = "#f2e9de" +inactive = "#9893a5" +subtle = "#6e6a86" +text = "#575279" +love = "#b4637a" +gold = "#ea9d34" +rose = "#d7827e" +pine = "#286983" +foam = "#56949f" +iris = "#907aa9" +highlight = "#eee9e6" +highlightInactive = "#f2ede9" +highlightOverlay = "#e4dfde" diff --git a/runtime/themes/serika-dark.toml b/runtime/themes/serika-dark.toml new file mode 100644 index 00000000..da145780 --- /dev/null +++ b/runtime/themes/serika-dark.toml @@ -0,0 +1,99 @@ +# Serika (Dark) +# Author: VuiMuich + +# Original Author: +# URL: https://github.com/arturoalviar/serika-syntax +# Author: arturoalviar +# License: MIT License + +"escape" = "orange" +"type" = "yellow" +"constant" = "purple" +"number" = "purple" +"string" = "fg" +"comment" = "grey2" +"variable" = "yellow" +"variable.builtin" = "blue" +"variable.parameter" = "yellow" +"variable.property" = "yellow" +"label" = "aqua" +"punctuation" = "grey0" +"punctuation.delimiter" = "grey2" +"punctuation.bracket" = "fg" +"keyword" = "red" +"operator" = "grey0" +"function" = "green" +"function.builtin" = "blue" +"function.macro" = "aqua" +"tag" = "yellow" +"namespace" = "fg" +"attribute" = "aqua" +"constructor" = "yellow" +"module" = "blue" +"property" = "yellow" +"special" = "orange" + +"ui.background" = { bg = "bg0" } +"ui.cursor" = { fg = "bg0", bg = "fg" } +"ui.cursor.match" = { fg = "grey3", bg = "grey2" } +"ui.cursor.insert" = { fg = "bg0", bg = "bg_yellow" } +"ui.cursor.select" = { fg = "bg0", bg = "bg_yellow" } +"ui.linenr" = "yellow" +"ui.linenr.selected" = { fg = "fg", modifiers = ["bold", "underlined"] } +"ui.statusline" = { fg = "grey1", bg = "bg2" } +"ui.statusline.inactive" = { fg = "grey2", bg = "bg1" } +"ui.popup" = { fg = "grey2", bg = "bg1" } +"ui.window" = { fg = "grey2", bg = "bg1" } +"ui.help" = { fg = "fg", bg = "bg1" } +"ui.text" = "fg" +"ui.text.focus" = "yellow" +"ui.menu" = { fg = "fg", bg = "bg2" } +"ui.menu.selected" = { fg = "bg0", bg = "bg_yellow" } +"ui.selection" = { bg = "bg3" } + +"hint" = "blue" +"info" = "aqua" +"warning" = "yellow" +"error" = "nasty-red" +"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] } + +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + +"markup.heading" = { fg = "purple", modifiers = ["bold"] } +"markup.list" = "cyan" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "yellow", modifiers = ["italic"] } +"markup.link.url" = "cyan" +"markup.link.text" = "pink" +"markup.quote" = { fg = "yellow", modifiers = ["italic"] } +"markup.raw" = { fg = "foreground" } + +[palette] + +bg0 = "#323437" +bg1 = "#494c50" +bg2 = "#55585e" +bg3 = "#61656b" +bg4 = "#6d7278" +bg5 = "#797e86" +bg_visual = "#646669" +bg_red = "#7e2a33" +bg_green = "#86b365" +bg_blue = "#6a89af" +bg_yellow = "#e2b714" + +fg = "#d1d0c5" +red = "#f9ebed" +nasty-red = "#ca4754" +dark-red = "#7e2a33" +orange = "#dd8a3c" +yellow = "#e2b714" +green = "#e5eae1" +aqua = "#b9c2c6" +blue = "#bdcadb" +purple = "#d0c4d4" +grey0 = "#aaaeb3" +grey1 = "#e1e1e3" +grey2 = "#646669" diff --git a/runtime/themes/serika-light.toml b/runtime/themes/serika-light.toml new file mode 100644 index 00000000..edde9044 --- /dev/null +++ b/runtime/themes/serika-light.toml @@ -0,0 +1,100 @@ +# Serika (Light) +# Author: VuiMuich + +# Original Author: +# URL: https://github.com/arturoalviar/serika-syntax +# Author: arturoalviar +# License: MIT License + +"escape" = "orange" +"type" = "yellow" +"constant" = "purple" +"number" = "purple" +"string" = "fg" +"comment" = "grey2" +"variable" = "yellow" +"variable.builtin" = "blue" +"variable.parameter" = "yellow" +"variable.property" = "yellow" +"label" = "aqua" +"punctuation" = "grey0" +"punctuation.delimiter" = "grey2" +"punctuation.bracket" = "fg" +"keyword" = "red" +"operator" = "grey0" +"function" = "green" +"function.builtin" = "blue" +"function.macro" = "aqua" +"tag" = "yellow" +"namespace" = "fg" +"attribute" = "aqua" +"constructor" = "yellow" +"module" = "blue" +"property" = "yellow" +"special" = "orange" + +"ui.background" = { bg = "bg0" } +"ui.cursor" = { fg = "bg0", bg = "fg" } +"ui.cursor.match" = { fg = "grey1", bg = "grey2" } +"ui.cursor.insert" = { fg = "bg0", bg = "bg_yellow" } +"ui.cursor.select" = { fg = "bg0", bg = "bg_yellow" } +"ui.linenr" = "yellow" +"ui.linenr.selected" = { fg = "fg", modifiers = ["bold", "underlined"] } +"ui.statusline" = { fg = "grey1", bg = "bg5" } +"ui.statusline.inactive" = { fg = "grey2", bg = "bg1" } +"ui.popup" = { fg = "bg0", bg = "bg5" } +"ui.window" = { fg = "bg0", bg = "bg5" } +"ui.help" = { fg = "bg0", bg = "bg5" } +"ui.text" = "fg" +"ui.text.focus" = "yellow" +"ui.menu" = { fg = "bg0", bg = "bg3" } +"ui.menu.selected" = { fg = "bg0", bg = "bg_yellow" } +"ui.selection" = { fg = "bg0", bg = "bg3" } + +"hint" = "blue" +"info" = "aqua" +"warning" = "yellow" +"error" = "nasty-red" +"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] } + +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + +"markup.heading" = { fg = "purple", modifiers = ["bold"] } +"markup.list" = "cyan" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "yellow", modifiers = ["italic"] } +"markup.link.url" = "cyan" +"markup.link.text" = "pink" +"markup.quote" = { fg = "yellow", modifiers = ["italic"] } +"markup.raw" = { fg = "foreground" } + + +[palette] + +bg0 = "#e1e1e3" +bg1 = "#494c50" +bg2 = "#55585e" +bg3 = "#61656b" +bg4 = "#6d7278" +bg5 = "#797e86" +bg_visual = "#646669" +bg_red = "#7e2a33" +bg_green = "#86b365" +bg_blue = "#6a89af" +bg_yellow = "#e2b714" + +fg = "#323437" +red = "#621d28" +nasty-red = "#da3333" +dark-red = "#791717" +orange = "#57320f" +yellow = "#e2b714" +green = "#3f4b34" +aqua = "#455054" +blue = "#3f5673" +purple = "#534059" +grey0 = "#aaaeb3" +grey1 = "#e1e1e3" +grey2 = "#646669" diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml index 984c86ee..dfaa104a 100644 --- a/runtime/themes/solarized_dark.toml +++ b/runtime/themes/solarized_dark.toml @@ -22,6 +22,20 @@ "module" = { fg = "violet" } "tag" = { fg = "magenta" } +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + # 背景 "ui.background" = { bg = "base03" } @@ -58,13 +72,13 @@ "ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] } # 主光标/selectio -"ui.cursor.primary" = {fg = "base03", bg = "base1"} -"ui.selection.primary" = { fg = "base03", bg = "base01" } -"ui.cursor.select" = {fg = "base02", bg = "green"} -"ui.selection" = { fg = "base02", bg = "yellow" } +"ui.cursor.primary" = { fg = "base03", bg = "base1" } +"ui.cursor.select" = { fg = "base02", bg = "cyan" } +"ui.selection" = { bg = "base0175" } +"ui.selection.primary" = { bg = "base015" } # normal模式的光标 -"ui.cursor" = {fg = "base03", bg = "green"} +"ui.cursor" = {fg = "base02", bg = "cyan"} "ui.cursor.insert" = {fg = "base03", bg = "base3"} # 当前光标匹配的标点符号 "ui.cursor.match" = {modifiers = ["reversed"]} @@ -73,18 +87,20 @@ "error" = { fg = "red", modifiers= ["bold", "underlined"] } "info" = { fg = "blue", modifiers= ["bold", "underlined"] } "hint" = { fg = "base01", modifiers= ["bold", "underlined"] } -"diagnostic" = { mdifiers = ["underlined"] } +"diagnostic" = { modifiers = ["underlined"] } [palette] # 深色 越来越深 -base03 = "#002b36" -base02 = "#073642" -base01 = "#586e75" -base00 = "#657b83" -base0 = "#839496" -base1 = "#93a1a1" -base2 = "#eee8d5" -base3 = "#fdf6e3" +base03 = "#002b36" +base02 = "#073642" +base0175 = "#16404b" +base015 = "#2c4f59" +base01 = "#586e75" +base00 = "#657b83" +base0 = "#839496" +base1 = "#93a1a1" +base2 = "#eee8d5" +base3 = "#fdf6e3" # 浅色 越來越浅 yellow = "#b58900" diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml index 0ab1b962..c8a3dee1 100644 --- a/runtime/themes/solarized_light.toml +++ b/runtime/themes/solarized_light.toml @@ -22,6 +22,20 @@ "module" = { fg = "violet" } "tag" = { fg = "magenta" } +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + # 背景 "ui.background" = { bg = "base03" } @@ -58,13 +72,13 @@ "ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] } # 主光标/selectio -"ui.cursor.primary" = {fg = "base03", bg = "base1"} -"ui.selection.primary" = { fg = "base03", bg = "base01" } -"ui.cursor.select" = {fg = "base02", bg = "green"} -"ui.selection" = { fg = "base02", bg = "yellow" } +"ui.cursor.primary" = { fg = "base03", bg = "base1" } +"ui.cursor.select" = { fg = "base02", bg = "cyan" } +"ui.selection" = { bg = "base0175" } +"ui.selection.primary" = { bg = "base015" } # normal模式的光标 -"ui.cursor" = {fg = "base03", bg = "green"} +"ui.cursor" = {fg = "base02", bg = "cyan"} "ui.cursor.insert" = {fg = "base03", bg = "base3"} # 当前光标匹配的标点符号 "ui.cursor.match" = {modifiers = ["reversed"]} @@ -73,26 +87,28 @@ "error" = { fg = "red", modifiers= ["bold", "underlined"] } "info" = { fg = "blue", modifiers= ["bold", "underlined"] } "hint" = { fg = "base01", modifiers= ["bold", "underlined"] } -"diagnostic" = { mdifiers = ["underlined"] } +"diagnostic" = { modifiers = ["underlined"] } [palette] -red = '#dc322f' -green = '#859900' -yellow = '#b58900' -blue = '#268bd2' -magenta = '#d33682' -cyan = '#2aa198' -orange = '#cb4b16' -violet = '#6c71c4' +red = '#dc322f' +green = '#859900' +yellow = '#b58900' +blue = '#268bd2' +magenta = '#d33682' +cyan = '#2aa198' +orange = '#cb4b16' +violet = '#6c71c4' # 深色 越来越深 -base0 = '#657b83' -base1 = '#586e75' -base2 = '#073642' -base3 = '#002b36' +base0 = '#657b83' +base1 = '#586e75' +base2 = '#073642' +base3 = '#002b36' ## 浅色 越來越浅 -base00 = '#839496' -base01 = '#93a1a1' -base02 = '#eee8d5' -base03 = '#fdf6e3' +base00 = '#839496' +base01 = '#93a1a1' +base015 = '#c5c8bd' +base0175 = '#dddbcc' +base02 = '#eee8d5' +base03 = '#fdf6e3' diff --git a/runtime/themes/spacebones_light.toml b/runtime/themes/spacebones_light.toml index 92f116ab..5318dc2d 100644 --- a/runtime/themes/spacebones_light.toml +++ b/runtime/themes/spacebones_light.toml @@ -30,6 +30,20 @@ "label" = "#b1951d" "module" = "#b1951d" +# TODO +"markup.heading" = "blue" +"markup.list" = "red" +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "magenta", modifiers = ["italic"] } +"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] } +"markup.link.text" = "red" +"markup.quote" = "cyan" +"markup.raw" = "green" + +"diff.plus" = "#2d9574" +"diff.delta" = "#715ab1" +"diff.minus" = "#ba2f59" + "warning" = { fg = "#da8b55" } "error" = { fg = "#e0211d" } "info" = { fg = "#b1951d" } @@ -28,6 +28,17 @@ string = "silver" # used for lifetimes label = "honey" +"markup.heading" = "lilac" +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.link.url" = { fg = "silver", modifiers = ["underlined"] } +"markup.link.text" = "almond" +"markup.raw" = "almond" + +"diff.plus" = "#35bf86" +"diff.minus" = "#f22c86" +"diff.delta" = "#6f44f0" + # TODO: diferentiate doc comment # concat (ERROR) @error.syntax and "MISSING ;" selectors for errors diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..717530d0 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "xtask" +version = "0.6.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +helix-term = { version = "0.6", path = "../helix-term" } +helix-core = { version = "0.6", path = "../helix-core" } +toml = "0.5" diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..d24a29cc --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,277 @@ +use std::{env, error::Error}; + +type DynError = Box<dyn Error>; + +pub mod helpers { + use std::{ + fmt::Display, + path::{Path, PathBuf}, + }; + + use crate::path; + use helix_core::syntax::Configuration as LangConfig; + + #[derive(Copy, Clone)] + pub enum TsFeature { + Highlight, + TextObjects, + AutoIndent, + } + + impl TsFeature { + pub fn all() -> &'static [Self] { + &[Self::Highlight, Self::TextObjects, Self::AutoIndent] + } + + pub fn runtime_filename(&self) -> &'static str { + match *self { + Self::Highlight => "highlights.scm", + Self::TextObjects => "textobjects.scm", + Self::AutoIndent => "indents.toml", + } + } + } + + impl Display for TsFeature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match *self { + Self::Highlight => "Syntax Highlighting", + Self::TextObjects => "Treesitter Textobjects", + Self::AutoIndent => "Auto Indent", + } + ) + } + } + + /// Get the list of languages that support a particular tree-sitter + /// based feature. + pub fn ts_lang_support(feat: TsFeature) -> Vec<String> { + let queries_dir = path::ts_queries(); + + find_files(&queries_dir, feat.runtime_filename()) + .iter() + .map(|f| { + // .../helix/runtime/queries/python/highlights.scm + let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm + let lang = tail.components().next().unwrap(); // python + lang.as_os_str().to_string_lossy().to_string() + }) + .collect() + } + + /// Get the list of languages that have any form of tree-sitter + /// queries defined in the runtime directory. + pub fn langs_with_ts_queries() -> Vec<String> { + std::fs::read_dir(path::ts_queries()) + .unwrap() + .filter_map(|entry| { + let entry = entry.ok()?; + entry + .file_type() + .ok()? + .is_dir() + .then(|| entry.file_name().to_string_lossy().to_string()) + }) + .collect() + } + + // naive implementation, but suffices for our needs + pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> { + std::fs::read_dir(dir) + .unwrap() + .filter_map(|entry| { + let path = entry.ok()?.path(); + if path.is_dir() { + Some(find_files(&path, filename)) + } else { + (path.file_name()?.to_string_lossy() == filename).then(|| vec![path]) + } + }) + .flatten() + .collect() + } + + pub fn lang_config() -> LangConfig { + let bytes = std::fs::read(path::lang_config()).unwrap(); + toml::from_slice(&bytes).unwrap() + } +} + +pub mod md_gen { + use crate::DynError; + + use crate::helpers; + use crate::path; + use std::fs; + + use helix_term::commands::cmd::TYPABLE_COMMAND_LIST; + + pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md"; + pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md"; + + fn md_table_heading(cols: &[String]) -> String { + let mut header = String::new(); + header += &md_table_row(cols); + header += &md_table_row(&vec!["---".to_string(); cols.len()]); + header + } + + fn md_table_row(cols: &[String]) -> String { + format!("| {} |\n", cols.join(" | ")) + } + + fn md_mono(s: &str) -> String { + format!("`{}`", s) + } + + pub fn typable_commands() -> Result<String, DynError> { + let mut md = String::new(); + md.push_str(&md_table_heading(&[ + "Name".to_owned(), + "Description".to_owned(), + ])); + + let cmdify = |s: &str| format!("`:{}`", s); + + for cmd in TYPABLE_COMMAND_LIST { + let names = std::iter::once(&cmd.name) + .chain(cmd.aliases.iter()) + .map(|a| cmdify(a)) + .collect::<Vec<_>>() + .join(", "); + + md.push_str(&md_table_row(&[names.to_owned(), cmd.doc.to_owned()])); + } + + Ok(md) + } + + pub fn lang_features() -> Result<String, DynError> { + let mut md = String::new(); + let ts_features = helpers::TsFeature::all(); + + let mut cols = vec!["Language".to_owned()]; + cols.append( + &mut ts_features + .iter() + .map(|t| t.to_string()) + .collect::<Vec<_>>(), + ); + cols.push("Default LSP".to_owned()); + + md.push_str(&md_table_heading(&cols)); + let config = helpers::lang_config(); + + let mut langs = config + .language + .iter() + .map(|l| l.language_id.clone()) + .collect::<Vec<_>>(); + langs.sort_unstable(); + + let mut ts_features_to_langs = Vec::new(); + for &feat in ts_features { + ts_features_to_langs.push((feat, helpers::ts_lang_support(feat))); + } + + let mut row = Vec::new(); + for lang in langs { + let lc = config + .language + .iter() + .find(|l| l.language_id == lang) + .unwrap(); // lang comes from config + row.push(lc.language_id.clone()); + + for (_feat, support_list) in &ts_features_to_langs { + row.push( + if support_list.contains(&lang) { + "✓" + } else { + "" + } + .to_owned(), + ); + } + row.push( + lc.language_server + .as_ref() + .map(|s| s.command.clone()) + .map(|c| md_mono(&c)) + .unwrap_or_default(), + ); + + md.push_str(&md_table_row(&row)); + row.clear(); + } + + Ok(md) + } + + pub fn write(filename: &str, data: &str) { + let error = format!("Could not write to {}", filename); + let path = path::book_gen().join(filename); + fs::write(path, data).expect(&error); + } +} + +pub mod path { + use std::path::{Path, PathBuf}; + + pub fn project_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf() + } + + pub fn book_gen() -> PathBuf { + project_root().join("book/src/generated/") + } + + pub fn ts_queries() -> PathBuf { + project_root().join("runtime/queries") + } + + pub fn lang_config() -> PathBuf { + project_root().join("languages.toml") + } +} + +pub mod tasks { + use crate::md_gen; + use crate::DynError; + + pub fn docgen() -> Result<(), DynError> { + use md_gen::*; + write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?); + write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?); + Ok(()) + } + + pub fn print_help() { + println!( + " +Usage: Run with `cargo xtask <task>`, eg. `cargo xtask docgen`. + + Tasks: + docgen: Generate files to be included in the mdbook output. +" + ); + } +} + +fn main() -> Result<(), DynError> { + let task = env::args().nth(1); + match task { + None => tasks::print_help(), + Some(t) => match t.as_str() { + "docgen" => tasks::docgen()?, + invalid => return Err(format!("Invalid task name: {}", invalid).into()), + }, + }; + Ok(()) +} |