diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ce3e092..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.3.0 + - uses: actions/upload-artifact@v2.3.1 with: name: bins-${{ matrix.build }} path: dist diff --git a/.gitmodules b/.gitmodules index 9c10846d..9297708a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -142,11 +142,15 @@ 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 @@ -154,3 +158,35 @@ 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 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 : and :goto 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 , , , 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. diff --git a/Cargo.lock b/Cargo.lock index cd6c0496..1a01da82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" [[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", @@ -258,15 +258,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" [[package]] name = "futures-executor" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" dependencies = [ "futures-core", "futures-task", @@ -275,15 +275,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" [[package]] name = "futures-util" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" dependencies = [ "futures-core", "futures-task", @@ -366,10 +366,11 @@ dependencies = [ [[package]] name = "helix-core" -version = "0.5.0" +version = "0.6.0" dependencies = [ "arc-swap", "chrono", + "encoding_rs", "etcetera", "helix-syntax", "log", @@ -391,7 +392,7 @@ dependencies = [ [[package]] name = "helix-lsp" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "futures-executor", @@ -409,7 +410,7 @@ dependencies = [ [[package]] name = "helix-syntax" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "cc", @@ -420,7 +421,7 @@ dependencies = [ [[package]] name = "helix-term" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "chrono", @@ -451,7 +452,7 @@ dependencies = [ [[package]] name = "helix-tui" -version = "0.5.0" +version = "0.6.0" dependencies = [ "bitflags", "cassowary", @@ -464,14 +465,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-lsp", @@ -690,9 +690,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", @@ -700,9 +700,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" @@ -847,9 +847,9 @@ 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", ] @@ -877,18 +877,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.131" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.131" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -897,9 +897,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ "itoa", "ryu", @@ -919,9 +919,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" dependencies = [ "libc", "signal-hook-registry", @@ -1069,11 +1069,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -1089,9 +1088,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", @@ -1120,9 +1119,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1" +checksum = "c36be3222512d85a112491ae0cc280a38076022414f00b64582da1b7565ffd82" dependencies = [ "cc", "regex", @@ -1262,7 +1261,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "xtask" -version = "0.5.0" +version = "0.6.0" dependencies = [ "helix-core", "helix-term", diff --git a/base16_theme.toml b/base16_theme.toml index 5ec74bcc..bb60a3ea 100644 --- a/base16_theme.toml +++ b/base16_theme.toml @@ -29,6 +29,10 @@ "namespace" = "magenta" "ui.help" = { fg = "white", bg = "black" } +"diff.plus" = "green" +"diff.delta" = "yellow" +"diff.minus" = "red" + "diagnostic" = { modifiers = ["underlined"] } "ui.gutter" = { bg = "black" } "info" = "blue" diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 80989e63..a1fbf172 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -1,12 +1,19 @@ | Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP | | --- | --- | --- | --- | --- | | bash | ✓ | | | `bash-language-server` | -| c | ✓ | | | `clangd` | +| c | ✓ | ✓ | ✓ | `clangd` | | c-sharp | ✓ | | | | -| cmake | ✓ | | | `cmake-language-server` | -| cpp | ✓ | | | `clangd` | +| cmake | ✓ | ✓ | ✓ | `cmake-language-server` | +| comment | ✓ | | | | +| cpp | ✓ | ✓ | ✓ | `clangd` | | css | ✓ | | | | +| dart | ✓ | | ✓ | `dart` | +| dockerfile | ✓ | | | `docker-langserver` | | elixir | ✓ | | | `elixir-ls` | +| fish | ✓ | ✓ | ✓ | | +| git-commit | ✓ | | | | +| git-diff | ✓ | | | | +| git-rebase | ✓ | | | | | glsl | ✓ | | ✓ | | | go | ✓ | ✓ | ✓ | `gopls` | | html | ✓ | | | | @@ -16,7 +23,9 @@ | julia | ✓ | | | `julia` | | latex | ✓ | | | | | ledger | ✓ | | | | -| llvm | ✓ | | | | +| llvm | ✓ | ✓ | ✓ | | +| llvm-mir | ✓ | ✓ | ✓ | | +| llvm-mir-yaml | ✓ | | ✓ | | | lua | ✓ | | ✓ | | | markdown | ✓ | | | | | mint | | | | `mint` | @@ -29,9 +38,11 @@ | protobuf | ✓ | | ✓ | | | python | ✓ | ✓ | ✓ | `pylsp` | | racket | | | | `racket` | -| ruby | ✓ | | | `solargraph` | +| ruby | ✓ | | ✓ | `solargraph` | | rust | ✓ | ✓ | ✓ | `rust-analyzer` | +| scala | ✓ | | ✓ | `metals` | | svelte | ✓ | | ✓ | `svelteserver` | +| tablegen | ✓ | ✓ | ✓ | | | toml | ✓ | | | | | tsq | ✓ | | | | | tsx | ✓ | | | `typescript-language-server` | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index bb21fd6b..65b2dc5f 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -20,6 +20,7 @@ | `: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. | @@ -41,3 +42,6 @@ | `: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. | diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 9ad2c285..5844a48e 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -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.` or `text.` 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.` or `text.` 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//`. 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//`. 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/keymap.md b/book/src/keymap.md index f0a2cb30..70ec13b3 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -46,39 +46,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` | -| `"` `` | 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 | `record_macro` | -| `Q` | Play back a recorded macro from the selected register | `play_macro` | +| 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` | +| `"` `` | 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 @@ -161,7 +161,7 @@ Jumps to various locations. | Key | Description | Command | | ----- | ----------- | ------- | -| `g` | Go to the start of the file | `goto_file_start` | +| `g` | Go to line number `` 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` | @@ -261,6 +261,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire | `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` | | `[space` | Add newline above | `add_newline_above` | | `]space` | Add newline below | `add_newline_below` | +| `]o` | Expand syntax tree object selection. | `expand_selection` | +| `[o` | Shrink syntax tree object selection. | `shrink_selection` | ## Insert Mode 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/themes.md b/book/src/themes.md index b6de7002..8eee334b 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -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) @@ -169,13 +170,20 @@ We use a similar set of scopes as - `numbered` - `bold` - `italic` - - `underline` - - `link` + - `link` + - `url` + - `label` - `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. diff --git a/flake.lock b/flake.lock index 2c59f993..acd10f76 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1637575296, - "narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=", + "lastModified": 1639692811, + "narHash": "sha256-wOOBH0fVsfNqw/5ZWRoKspyesoXBgiwEOUBH4c7JKEo=", "owner": "numtide", "repo": "devshell", - "rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618", + "rev": "d3a1f5bec3632b33346865b1c165bf2420bb2f52", "type": "github" }, "original": { @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1638425401, - "narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=", + "lastModified": 1639807801, + "narHash": "sha256-y32tMq1LTRVbMW3QN5i98iOQjQt2QSsif3ayUkD1o3g=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37", + "rev": "b5bbaa4f5239e6f0619846f9a5380f07baa853d3", "type": "github" }, "original": { @@ -56,11 +56,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1638376152, - "narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=", + "lastModified": 1639699734, + "narHash": "sha256-tlX6WebGmiHb2Hmniff+ltYp+7dRfdsBxw9YczLsP60=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f", + "rev": "03ec468b14067729a285c2c7cfa7b9434a04816c", "type": "github" }, "original": { @@ -99,11 +99,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1638497756, - "narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=", + "lastModified": 1639880499, + "narHash": "sha256-/BibDmFwgWuuTUkNVO6YlvuTSWM9dpBvlZoTAPs7ORI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a", + "rev": "c6c83589ae048af20d93d01eb07a4176012093d0", "type": "github" }, "original": { diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 0a2a56d9..3c11ec76 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 "] edition = "2021" license = "MPL-2.0" @@ -13,7 +13,7 @@ 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" @@ -23,7 +23,7 @@ unicode-width = "0.1" unicode-general-category = "0.4" # slab = "0.4.2" tree-sitter = "0.20" -once_cell = "1.8" +once_cell = "1.9" arc-swap = "1" regex = "1" @@ -35,6 +35,7 @@ toml = "0.5" similar = "2.1" etcetera = "0.3" +encoding_rs = "0.8" chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index c037afef..1b3de6ea 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -1,7 +1,7 @@ //! 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::{movement::Direction, Range, Rope, Selection, Tendril, Transaction}; use log::debug; use smallvec::SmallVec; @@ -30,7 +30,6 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20 // [TODO] // * delete implementation where it erases the whole bracket (|) -> | -// * do not reduce to cursors; use whole selections, and surround with pair // * change to multi character pairs to handle cases like placing the cursor in the // middle of triple quotes, and more exotic pairs like Jinja's {% %} @@ -38,20 +37,18 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20 pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option { debug!("autopairs hook selection: {:#?}", selection); - let cursors = selection.clone().cursors(doc.slice(..)); - for &(open, close) in PAIRS { if open == ch { if open == close { - return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE)); + return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE)); } else { - return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE)); + return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE)); } } if close == ch { // && char_at pos == close - return Some(handle_close(doc, &cursors, open, close)); + return Some(handle_close(doc, selection, open, close)); } } @@ -66,6 +63,36 @@ fn prev_char(doc: &Rope, pos: usize) -> Option { doc.get_char(pos - 1) } +/// calculate what the resulting range should be for an auto pair insertion +fn get_next_range( + start_range: &Range, + offset: usize, + typed_char: char, + len_inserted: usize, +) -> Range { + let end_head = start_range.head + offset + typed_char.len_utf8(); + + 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 + (1, Direction::Forward) => end_head - 1, + (1, Direction::Backward) => end_head + 1, + + // if we are appending, the anchor stays where it is; only offset + // for multiple range insertions + (_, Direction::Forward) => start_range.anchor + offset, + + // when we are inserting in front of a selection, we need to move + // the anchor over by however many characters were inserted overall + (_, Direction::Backward) => start_range.anchor + offset + len_inserted, + }; + + Range::new(end_anchor, end_head) +} + fn handle_open( doc: &Rope, selection: &Selection, @@ -74,36 +101,32 @@ fn handle_open( close_before: &str, ) -> Transaction { let mut end_ranges = SmallVec::with_capacity(selection.len()); - let mut offs = 0; let transaction = Transaction::change_by_selection(doc, selection, |start_range| { - let start_head = start_range.head; - - let next = doc.get_char(start_head); - let end_head = start_head + offs + open.len_utf8(); - - let end_anchor = if start_range.is_empty() { - end_head - } else { - start_range.anchor + offs - }; - - end_ranges.push(Range::new(end_anchor, end_head)); + let 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 += open.len_utf8(); - (start_head, start_head, Some(Tendril::from_char(open))) + len_inserted = open.len_utf8(); + (cursor, cursor, Some(Tendril::from_char(open))) } // None | Some(ch) if close_before.contains(ch) => {} _ => { // insert open & close let pair = Tendril::from_iter([open, close]); - offs += open.len_utf8() + close.len_utf8(); - (start_head, start_head, Some(pair)) + len_inserted = open.len_utf8() + close.len_utf8(); + (cursor, cursor, Some(pair)) } - } + }; + + let next_range = get_next_range(start_range, offs, open, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; + + change }); let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); @@ -117,28 +140,28 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> let mut offs = 0; let transaction = Transaction::change_by_selection(doc, selection, |start_range| { - let start_head = start_range.head; - let next = doc.get_char(start_head); - let end_head = start_head + offs + close.len_utf8(); + let cursor = start_range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor); + let mut len_inserted = 0; - let end_anchor = if start_range.is_empty() { - end_head + let change = if next_char == Some(close) { + // return transaction that moves past close + (cursor, cursor, None) // no-op } else { - start_range.anchor + offs + len_inserted += close.len_utf8(); + (cursor, cursor, Some(Tendril::from_char(close))) }; - end_ranges.push(Range::new(end_anchor, end_head)); + let next_range = get_next_range(start_range, offs, close, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; - if next == Some(close) { - // return transaction that moves past close - (start_head, start_head, None) // no-op - } else { - offs += close.len_utf8(); - (start_head, start_head, Some(Tendril::from_char(close))) - } + change }); - transaction.with_selection(Selection::new(end_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""") @@ -154,42 +177,41 @@ fn handle_same( let mut offs = 0; let transaction = Transaction::change_by_selection(doc, selection, |start_range| { - let start_head = start_range.head; - let end_head = start_head + offs + token.len_utf8(); + let cursor = start_range.cursor(doc.slice(..)); + let mut len_inserted = 0; - // if selection, retain anchor, if cursor, move over - let end_anchor = if start_range.is_empty() { - end_head - } else { - start_range.anchor + offs - }; + let next_char = doc.get_char(cursor); + let prev_char = prev_char(doc, cursor); - end_ranges.push(Range::new(end_anchor, end_head)); - - let next = doc.get_char(start_head); - let prev = prev_char(doc, start_head); - - if next == Some(token) { + let change = if next_char == Some(token) { // return transaction that moves past close - (start_head, start_head, None) // no-op + (cursor, cursor, None) // no-op } else { let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32); pair.push_char(token); // for equal pairs, don't insert both open and close if either // side has a non-pair char - if (next.is_none() || close_before.contains(next.unwrap())) - && (prev.is_none() || open_before.contains(prev.unwrap())) + if (next_char.is_none() || close_before.contains(next_char.unwrap())) + && (prev_char.is_none() || open_before.contains(prev_char.unwrap())) { pair.push_char(token); } - offs += pair.len(); - (start_head, start_head, Some(pair)) - } + len_inserted += pair.len(); + (cursor, cursor, Some(pair)) + }; + + let next_range = get_next_range(start_range, offs, token, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; + + change }); - transaction.with_selection(Selection::new(end_ranges, selection.primary_index())) + let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); + debug!("auto pair transaction: {:#?}", t); + t } #[cfg(test)] @@ -252,7 +274,20 @@ mod test { &Selection::single(1, 0), PAIRS, |open, close| format!("{}{}", open, close), - &Selection::single(1, 1), + &Selection::single(2, 1), + ); + } + + /// [] -> 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("\n\n"), + &Selection::single(0, 2), + PAIRS, + |open, close| format!("\n{}{}\n", open, close), + &Selection::single(0, 3), ); } @@ -276,26 +311,50 @@ mod test { ) }, &Selection::new( - smallvec!(Range::point(1), Range::point(4), Range::point(7),), + smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),), 0, ), ); } - // [TODO] broken until it works with selections /// fo[o] -> append ( -> fo[o(]) - #[ignore] #[test] fn test_append() { test_hooks_with_pairs( - &Rope::from("foo"), + &Rope::from("foo\n"), &Selection::single(2, 4), - PAIRS, - |open, close| format!("foo{}{}", open, close), + differing_pairs(), + |open, close| format!("foo{}{}\n", open, close), &Selection::single(2, 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() { @@ -307,7 +366,23 @@ mod test { &Selection::single(2, 1), *close, &doc, - &Selection::point(2), + &Selection::single(3, 2), + ); + } + } + + /// [(]) -> append ) -> [()] + #[test] + fn test_append_close_inside_pair() { + for (open, close) in PAIRS { + let doc = Rope::from(format!("{}{}\n", open, close)); + + test_hooks( + &doc, + &Selection::single(0, 2), + *close, + &doc, + &Selection::single(0, 3), ); } } @@ -323,8 +398,33 @@ mod test { ); let expected_sel = Selection::new( - // smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),), - smallvec!(Range::point(2), Range::point(5), Range::point(8),), + 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, ); @@ -343,7 +443,7 @@ mod test { #[test] fn test_insert_open_inside_pair() { let sel = Selection::single(2, 1); - let expected_sel = Selection::point(2); + let expected_sel = Selection::single(3, 2); for (open, close) in differing_pairs() { let doc = Rope::from(format!("{}{}", open, close)); @@ -357,11 +457,49 @@ mod test { } } + /// [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::point(2); + 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,)); @@ -385,21 +523,44 @@ mod test { &Selection::single(1, 0), PAIRS, |open, _| format!("{}word", open), - &Selection::point(1), + &Selection::single(2, 1), ) } - // [TODO] broken until it works with selections /// [wor]d -> insert ( -> ([wor]d #[test] - #[ignore] fn test_insert_open_with_selection() { test_hooks_with_pairs( &Rope::from("word"), - &Selection::single(0, 4), + &Selection::single(3, 0), PAIRS, |open, _| format!("{}word", open), - &Selection::single(1, 5), + &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), ) } @@ -413,7 +574,7 @@ mod test { fn test_insert_open_after_non_pair() { let doc = Rope::from("word"); let sel = Selection::single(5, 4); - let expected_sel = Selection::point(5); + let expected_sel = Selection::single(6, 5); test_hooks_with_pairs( &doc, @@ -431,4 +592,18 @@ mod test { &expected_sel, ); } + + /// appending with only a cursor should stay a cursor + /// + /// [] -> append to end "foo -> "foo[]" + #[test] + fn test_append_single_cursor() { + test_hooks_with_pairs( + &Rope::from("\n"), + &Selection::single(0, 1), + PAIRS, + |open, close| format!("{}{}\n", open, close), + &Selection::single(1, 2), + ); + } } 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/indent.rs b/helix-core/src/indent.rs index b6f5081a..1fc2b8a5 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 { /// 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 { @@ -210,10 +208,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option, newline: bool) -> usize { - // NOTE: can't use contains() on query because of comparing Vec 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, + line: usize, + newline: bool, +) -> usize { let mut increment: isize = 0; let mut node = match node { @@ -221,70 +224,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option, 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 @@ -293,23 +271,20 @@ pub fn suggested_indent_for_pos( syntax: Option<&Syntax>, text: RopeSlice, pos: usize, + line: usize, new_line: bool, -) -> usize { +) -> Option { 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 } } @@ -442,6 +417,7 @@ where ); let doc = Rope::from(doc); + use crate::diagnostic::Severity; use crate::syntax::{ Configuration, IndentationConfiguration, LanguageConfiguration, Loader, }; @@ -459,6 +435,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, @@ -482,14 +460,23 @@ where 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 92a59f31..7fd23b97 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; @@ -37,8 +39,14 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option { line.chars().position(|ch| !ch.is_whitespace()) } -/// Find `.git` root. -pub fn find_root(root: Option<&str>) -> Option { +/// 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 { let current_dir = std::env::current_dir().expect("unable to determine current directory"); let root = match root { @@ -50,16 +58,30 @@ pub fn find_root(root: Option<&str>) -> Option { 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 { 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..21fa24fb 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,7 +1,5 @@ use crate::{Range, RopeSlice, Selection, Syntax}; -// 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 { let tree = syntax.tree(); @@ -34,3 +32,30 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) } }) } + +pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { + let tree = syntax.tree(); + + selection.clone().transform(|range| { + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + + let descendant = match tree.root_node().descendant_for_byte_range(from, to) { + // find first child, if not possible, fallback to the node that contains selection + Some(descendant) => match descendant.child(0) { + Some(child) => child, + None => descendant, + }, + None => return range, + }; + + let from = text.byte_to_char(descendant.start_byte()); + let to = text.byte_to_char(descendant.end_byte()); + + if range.head < range.anchor { + Range::new(to, from) + } else { + Range::new(from, to) + } + }) +} diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 116a1c7c..1515c4fc 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 { @@ -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/syntax.rs b/helix-core/src/syntax.rs index ef35fc75..5d37c219 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, @@ -63,6 +64,10 @@ pub struct LanguageConfiguration { #[serde(default)] pub auto_format: bool, + #[serde(default)] + pub diagnostic_severity: Severity, + + pub tree_sitter_library: Option, // tree-sitter library name, defaults to language_id // content_regex #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")] @@ -189,9 +194,14 @@ 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, diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 5e4619ef..ee485f57 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 "] edition = "2021" 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" futures-executor = "0.3" @@ -23,5 +23,5 @@ lsp-types = { version = "0.91", 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"] } +tokio = { version = "1.15", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio-stream = "0.1.8" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 271fd9d5..c80f70b5 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -31,6 +31,7 @@ pub struct Client { pub(crate) capabilities: OnceCell, offset_encoding: OffsetEncoding, config: Option, + root_markers: Vec, } impl Client { @@ -39,6 +40,7 @@ impl Client { cmd: &str, args: &[String], config: Option, + root_markers: Vec, id: usize, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { 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 { // 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()); @@ -556,6 +560,14 @@ impl Client { self.call::(params) } + pub async fn resolve_completion_item( + &self, + completion_item: lsp::CompletionItem, + ) -> Result { + self.request::(completion_item) + .await + } + pub fn text_document_signature_help( &self, text_document: lsp::TextDocumentIdentifier, @@ -800,4 +812,16 @@ impl Client { let response = self.request::(params).await?; Ok(response.unwrap_or_default()) } + + pub fn command(&self, command: lsp::Command) -> impl Future> { + 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::(params) + } } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 15cae582..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 { - 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)); 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 "] 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 +. + +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/languages/tree-sitter-comment b/helix-syntax/languages/tree-sitter-comment new file mode 160000 index 00000000..5dd3c62f --- /dev/null +++ b/helix-syntax/languages/tree-sitter-comment @@ -0,0 +1 @@ +Subproject commit 5dd3c62f1bbe378b220fe16b317b85247898639e diff --git a/helix-syntax/languages/tree-sitter-dart b/helix-syntax/languages/tree-sitter-dart new file mode 160000 index 00000000..6a253766 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-dart @@ -0,0 +1 @@ +Subproject commit 6a25376685d1d47968c2cef06d4db8d84a70025e diff --git a/helix-syntax/languages/tree-sitter-dockerfile b/helix-syntax/languages/tree-sitter-dockerfile new file mode 160000 index 00000000..7af32bc0 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-dockerfile @@ -0,0 +1 @@ +Subproject commit 7af32bc04a66ab196f5b9f92ac471f29372ae2ce diff --git a/helix-syntax/languages/tree-sitter-fish b/helix-syntax/languages/tree-sitter-fish new file mode 160000 index 00000000..04e54ab6 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-fish @@ -0,0 +1 @@ +Subproject commit 04e54ab6585dfd4fee6ddfe5849af56f101b6d4f diff --git a/helix-syntax/languages/tree-sitter-git-commit b/helix-syntax/languages/tree-sitter-git-commit new file mode 160000 index 00000000..066e395e --- /dev/null +++ b/helix-syntax/languages/tree-sitter-git-commit @@ -0,0 +1 @@ +Subproject commit 066e395e1107df17183cf3ae4230f1a1406cc972 diff --git a/helix-syntax/languages/tree-sitter-git-diff b/helix-syntax/languages/tree-sitter-git-diff new file mode 160000 index 00000000..c12e6ecb --- /dev/null +++ b/helix-syntax/languages/tree-sitter-git-diff @@ -0,0 +1 @@ +Subproject commit c12e6ecb54485f764250556ffd7ccb18f8e2942b diff --git a/helix-syntax/languages/tree-sitter-git-rebase b/helix-syntax/languages/tree-sitter-git-rebase new file mode 160000 index 00000000..332dc528 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-git-rebase @@ -0,0 +1 @@ +Subproject commit 332dc528f27044bc4427024dbb33e6941fc131f2 diff --git a/helix-syntax/languages/tree-sitter-llvm b/helix-syntax/languages/tree-sitter-llvm new file mode 160000 index 00000000..3b213925 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-llvm @@ -0,0 +1 @@ +Subproject commit 3b213925b9c4f42c1acfe2e10bfbb438d9c6834d diff --git a/helix-syntax/languages/tree-sitter-llvm-mir b/helix-syntax/languages/tree-sitter-llvm-mir new file mode 160000 index 00000000..06fabca1 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-llvm-mir @@ -0,0 +1 @@ +Subproject commit 06fabca19454b2dc00c1b211a7cb7ad0bc2585f1 diff --git a/helix-syntax/languages/tree-sitter-tablegen b/helix-syntax/languages/tree-sitter-tablegen new file mode 160000 index 00000000..568dd8a9 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-tablegen @@ -0,0 +1 @@ +Subproject commit 568dd8a937347175fd58db83d4c4cdaeb6069bd2 diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 623c5bb9..28b4fe2a 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 "] edition = "2021" @@ -22,12 +22,12 @@ 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-core = { version = "0.6", path = "../helix-core" } +helix-view = { version = "0.6", path = "../helix-view" } +helix-lsp = { version = "0.6", path = "../helix-lsp" } 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" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3e0b6d59..c7202feb 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,8 +1,12 @@ use helix_core::{merge_toml_values, syntax}; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{theme, Editor}; +use serde_json::json; -use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; +use crate::{ + args::Args, commands::apply_workspace_edit, compositor::Compositor, config::Config, job::Jobs, + ui, +}; use log::{error, warn}; @@ -374,6 +378,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 @@ -411,19 +416,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 }) @@ -530,14 +547,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 => { @@ -567,8 +576,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), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cd566720..5c26a5b2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -26,6 +26,7 @@ use helix_view::{ }; 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}, @@ -266,6 +267,7 @@ impl MappableCommand { 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", @@ -287,7 +289,7 @@ impl MappableCommand { 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 else file start", goto_file_end, "Goto file end", goto_file, "Goto files in selection", goto_file_hsplit, "Goto files in selection (hsplit)", @@ -360,6 +362,7 @@ impl MappableCommand { 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", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save the current selection to the jumplist", @@ -396,7 +399,7 @@ impl MappableCommand { increment, "Increment", decrement, "Decrement", record_macro, "Record macro", - play_macro, "Play macro", + replay_macro, "Replay macro", ); } @@ -1280,16 +1283,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) { @@ -1543,7 +1553,7 @@ fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); let scrolloff = cx.editor.config.scrolloff; - let (_, doc) = current!(cx.editor); + let doc = doc!(cx.editor); // TODO: could probably share with select_on_matches? @@ -1630,7 +1640,7 @@ 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); } @@ -1904,7 +1914,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); } @@ -1938,7 +1962,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); } @@ -2030,7 +2054,7 @@ pub mod cmd { fn write_impl(cx: &mut compositor::Context, path: Option<&Cow>) -> 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().as_ref())) @@ -2083,8 +2107,7 @@ pub mod cmd { _args: &[Cow], _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); @@ -2307,12 +2330,7 @@ pub mod cmd { write_all_impl(cx, args, event, true, true) } - fn quit_all_impl( - editor: &mut Editor, - _args: &[Cow], - _event: PromptEvent, - force: bool, - ) -> anyhow::Result<()> { + fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> { if !force { buffers_remaining_impl(editor)?; } @@ -2328,18 +2346,18 @@ pub mod cmd { fn quit_all( cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, + _args: &[Cow], + _event: PromptEvent, ) -> anyhow::Result<()> { - quit_all_impl(cx.editor, args, event, false) + quit_all_impl(cx.editor, false) } fn force_quit_all( cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, + _args: &[Cow], + _event: PromptEvent, ) -> anyhow::Result<()> { - quit_all_impl(cx.editor, args, event, true) + quit_all_impl(cx.editor, true) } fn cquit( @@ -2353,12 +2371,21 @@ pub 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], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let exit_code = args + .first() + .and_then(|code| code.parse::().ok()) + .unwrap_or(1); + cx.editor.exit_code = exit_code; + + quit_all_impl(cx.editor, true) } fn theme( @@ -2393,7 +2420,7 @@ pub mod cmd { args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); + 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) @@ -2412,7 +2439,7 @@ pub mod cmd { args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); + 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) @@ -2539,7 +2566,7 @@ pub mod cmd { args: &[Cow], _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 { @@ -2637,6 +2664,86 @@ pub mod cmd { let (view, doc) = current!(cx.editor); view.ensure_cursor_in_view(doc, line); + Ok(()) + } + + fn setting( + cx: &mut compositor::Context, + args: &[Cow], + _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()?, + "smart-case" => runtime_config.smart_case = 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()?, + _ => anyhow::bail!("Unknown key `{}`.", args[0]), + } + + Ok(()) + } + + fn sort( + cx: &mut compositor::Context, + args: &[Cow], + _event: PromptEvent, + ) -> anyhow::Result<()> { + sort_impl(cx, args, false) + } + + fn sort_reverse( + cx: &mut compositor::Context, + args: &[Cow], + _event: PromptEvent, + ) -> anyhow::Result<()> { + sort_impl(cx, args, true) + } + + fn sort_impl( + cx: &mut compositor::Context, + _args: &[Cow], + 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_slice(&fragment)) + .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(()) } @@ -2664,18 +2771,18 @@ pub 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", @@ -2782,6 +2889,13 @@ pub mod cmd { fun: cquit, 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: &[], @@ -2928,7 +3042,28 @@ pub 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, + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = @@ -2948,17 +3083,28 @@ fn command_mode(cx: &mut Context) { ":".into(), Some(':'), |input: &str| { + static FUZZY_MATCHER: Lazy = + Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); + // we use .this over split_whitespace() because we care about empty segments let parts = input.split(' ').collect::>(); // 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(); @@ -3002,7 +3148,16 @@ fn command_mode(cx: &mut Context) { // Handle typable commands if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { - let args = shellwords::shellwords(input); + 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::>() + }; + if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); } @@ -3026,7 +3181,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)); } @@ -3118,7 +3274,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, @@ -3139,7 +3295,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) @@ -3181,17 +3337,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, @@ -3277,12 +3431,19 @@ pub fn code_action(cx: &mut Context) { move |editor, code_action, _action| match code_action { lsp::CodeActionOrCommand::Command(command) => { log::debug!("code action command: {:?}", command); - editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + 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 { - apply_workspace_edit(editor, offset_encoding, workspace_edit) + log::debug!("edit: {:?}", workspace_edit); + apply_workspace_edit(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()); } } }, @@ -3293,6 +3454,25 @@ pub fn code_action(cx: &mut Context) { ) } +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; @@ -3346,7 +3526,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, @@ -3537,22 +3717,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(), ) }; @@ -3563,8 +3743,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); @@ -3610,6 +3792,7 @@ fn normal_mode(cx: &mut Context) { doc.mode = Mode::Normal; + try_restore_indent(doc, view.id); doc.append_changes_to_history(view.id); // if leaving append mode, move cursor back by 1 @@ -3627,6 +3810,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.len32() as usize == 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); @@ -3994,27 +4211,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) { @@ -4270,48 +4481,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); } @@ -5079,7 +5290,7 @@ pub fn completion(cx: &mut Context) { move |editor: &mut Editor, compositor: &mut Compositor, response: Option| { - let (_, doc) = current!(editor); + let doc = doc!(editor); if doc.mode() != Mode::Insert { // we're not in insert mode anymore return; @@ -5257,6 +5468,7 @@ 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) } @@ -5272,7 +5484,39 @@ 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); + 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); doc.set_selection(view.id, selection); } }; @@ -5920,42 +6164,42 @@ fn record_macro(cx: &mut Context) { keys.pop(); let s = keys .into_iter() - .map(|key| format!("{}", key)) - .collect::>() - .join(" "); + .map(|key| { + let s = key.to_string(); + if s.chars().count() == 1 { + s + } else { + format!("<{}>", s) + } + }) + .collect::(); cx.editor.registers.get_mut(reg).write(vec![s]); cx.editor - .set_status(format!("Recorded to register {}", reg)); + .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)); + .set_status(format!("Recording to register [{}]", reg)); } } -fn play_macro(cx: &mut Context) { +fn replay_macro(cx: &mut Context) { let reg = cx.register.unwrap_or('@'); - let keys = match cx - .editor - .registers - .get(reg) - .and_then(|reg| reg.read().get(0)) - .context("Register empty") - .and_then(|s| { - s.split_whitespace() - .map(str::parse::) - .collect::, _>>() - .context("Failed to parse macro") - }) { - Ok(keys) => keys, - Err(e) => { - cx.editor.set_error(format!("{}", e)); - return; + let keys: Vec = 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(); + let count = cx.count(); cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { for _ in 0..count { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 257d5f29..49f8469a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -569,11 +569,13 @@ impl Default for Keymaps { "d" => goto_prev_diag, "D" => goto_first_diag, "space" => add_newline_above, + "o" => shrink_selection, }, "]" => { "Right bracket" "d" => goto_next_diag, "D" => goto_last_diag, "space" => add_newline_below, + "o" => expand_selection, }, "/" => search, @@ -593,8 +595,8 @@ impl Default for Keymaps { // paste_all "P" => paste_before, - "q" => record_macro, - "Q" => play_macro, + "Q" => record_macro, + "q" => replay_macro, ">" => indent, "<" => unindent, @@ -617,6 +619,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, diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index a55201ff..274330c0 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 { + Completion::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(), @@ -181,6 +192,31 @@ impl Completion { completion } + fn resolve_completion_item( + doc: &Document, + completion_item: lsp::CompletionItem, + ) -> Option { + 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(); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 6b015171..5b7e9075 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -7,7 +7,7 @@ use crate::{ }; use helix_core::{ - coords_at_pos, + coords_at_pos, encoding, graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, syntax::{self, HighlightEvent}, @@ -566,21 +566,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. //------------------------------- @@ -654,6 +639,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 @@ -664,6 +656,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 @@ -782,8 +799,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 } @@ -941,14 +959,18 @@ impl EditorView { } impl Component for EditorView { - fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { - let mut cxt = commands::Context { - editor: 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 { @@ -958,18 +980,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 => { @@ -981,8 +1004,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); @@ -992,40 +1015,40 @@ 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); // mode transitions match (mode, doc.mode()) { @@ -1054,7 +1077,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), } } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f57e2e2b..9ff9118f 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -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; @@ -208,6 +210,31 @@ pub mod completers { names } + pub fn setting(input: &str) -> Vec { + static KEYS: Lazy> = 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 { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); @@ -256,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/prompt.rs b/helix-term/src/ui/prompt.rs index 07e1b33c..0202de23 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 @@ -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 { diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 6df65d36..4b8d7acb 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 "] description = """ A library to build rich terminal user interfaces or dashboards @@ -21,5 +21,5 @@ cassowary = "0.3" unicode-segmentation = "1.8" crossterm = { version = "0.22", 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-view/Cargo.toml b/helix-view/Cargo.toml index 34f55eb6..3c8866fc 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 "] edition = "2021" license = "MPL-2.0" @@ -16,12 +16,12 @@ 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-core = { version = "0.6", path = "../helix-core" } +helix-lsp = { version = "0.6", path = "../helix-lsp"} crossterm = { version = "0.22", 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"] } @@ -29,7 +29,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"] } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index a0315bed..9185e483 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Context, Error}; use serde::de::{self, Deserialize, Deserializer}; +use serde::Serialize; use std::cell::Cell; use std::collections::HashMap; use std::fmt::Display; @@ -9,6 +10,7 @@ use std::str::FromStr; use std::sync::Arc; use helix_core::{ + encoding, history::History, indent::{auto_detect_indent_style, IndentStyle}, line_ending::auto_detect_line_ending, @@ -68,13 +70,22 @@ impl<'de> Deserialize<'de> for Mode { } } +impl Serialize for Mode { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + pub struct Document { pub(crate) id: DocumentId, text: Rope, pub(crate) selections: HashMap, path: Option, - encoding: &'static encoding_rs::Encoding, + encoding: &'static encoding::Encoding, /// Current editing mode. pub mode: Mode, @@ -143,8 +154,8 @@ impl fmt::Debug for Document { /// be used to override encoding auto-detection. pub fn from_reader( 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 @@ -212,11 +223,11 @@ pub fn from_reader( 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; @@ -251,7 +262,7 @@ pub fn from_reader( /// 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 @@ -286,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; @@ -322,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; @@ -356,7 +367,7 @@ impl Document { /// overwritten with the `encoding` parameter. pub fn open( path: &Path, - encoding: Option<&'static encoding_rs::Encoding>, + encoding: Option<&'static encoding::Encoding>, theme: Option<&Theme>, config_loader: Option<&syntax::Loader>, ) -> Result { @@ -366,7 +377,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) }; @@ -548,7 +559,7 @@ 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()) { + match encoding::Encoding::for_label(label.as_bytes()) { Some(encoding) => self.encoding = encoding, None => return Err(anyhow::anyhow!("unknown encoding")), } @@ -556,7 +567,7 @@ impl Document { } /// Returns the [`Document`]'s current encoding. - pub fn encoding(&self) -> &'static encoding_rs::Encoding { + pub fn encoding(&self) -> &'static encoding::Encoding { self.encoding } @@ -889,6 +900,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> { @@ -1119,7 +1134,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)); @@ -1138,7 +1153,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 d65c1fb2..eef7520e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -27,7 +27,7 @@ pub use helix_core::register::Registers; use helix_core::syntax; use helix_core::{Position, Selection}; -use serde::{Deserialize, Deserializer}; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize}; fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result where @@ -37,7 +37,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 @@ -77,7 +77,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. @@ -137,6 +137,20 @@ impl<'de> Deserialize<'de> for CursorShapeConfig { } } +impl Serialize for CursorShapeConfig { + fn serialize(&self, serializer: S) -> Result + 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]; @@ -151,7 +165,7 @@ impl Default for CursorShapeConfig { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum LineNumber { /// Show absolute line number @@ -160,6 +174,18 @@ pub enum LineNumber { Relative, } +impl std::str::FromStr for LineNumber { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + 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 { diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 892aa646..d75cde82 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -1,11 +1,11 @@ use bitflags::bitflags; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::{ cmp::{max, min}, str::FromStr, }; -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] /// UNSTABLE pub enum CursorKind { diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 92caa517..14dadc3b 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -254,6 +254,43 @@ impl From for crossterm::event::KeyEvent { } } +pub fn parse_macro(keys_str: &str) -> anyhow::Result> { + 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)] mod test { use super::*; @@ -339,4 +376,120 @@ mod test { assert!(str::parse::("123").is_err()); assert!(str::parse::("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("vhxx").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").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("abc123").is_err()); + assert!(parse_macro("wd").is_err()); + } } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 94d67acd..89a6c196 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -80,6 +80,8 @@ pub struct View { // 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; 2], + /// used to store previous selections of tree-sitter objecs + pub object_selections: Vec, } impl View { @@ -92,6 +94,7 @@ impl View { jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel last_accessed_doc: None, last_modified_docs: [None, None], + object_selections: Vec::new(), } } diff --git a/languages.toml b/languages.toml index abb94acf..e8329fe7 100644 --- a/languages.toml +++ b/languages.toml @@ -3,15 +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 } -diagnostics = { disabled = ["unresolved-proc-macro"] } [[language]] name = "toml" @@ -45,6 +41,17 @@ comment-token = "#" 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" @@ -327,6 +334,7 @@ file-types = ["yml", "yaml"] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } +injection-regex = "yml|yaml" # [[language]] # name = "haskell" @@ -379,6 +387,7 @@ roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } language-server = { command = "cmake-language-server" } +injection-regex = "cmake" [[language]] name = "glsl" @@ -387,6 +396,7 @@ file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ] roots = [] comment-token = "//" indent = { tab-width = 4, unit = " " } +injection-regex = "glsl" [[language]] name = "perl" @@ -406,6 +416,13 @@ shebangs = ["racket"] 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" @@ -421,6 +438,34 @@ 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" @@ -430,3 +475,58 @@ 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"] +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 = " " } 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/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/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..321c90ad --- /dev/null +++ b/runtime/queries/elixir/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) 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-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/julia/injections.scm b/runtime/queries/julia/injections.scm index be2412c0..3cf7339f 100644 --- a/runtime/queries/julia/injections.scm +++ b/runtime/queries/julia/injections.scm @@ -1,5 +1,7 @@ ; TODO: re-add when markdown is added. -; ((triple_string) @markdown -; (#offset! @markdown 0 3 0 -3)) +; ((triple_string) @injection.content +; (#offset! @injection.content 0 3 0 -3) +; (#set! injection.language "markdown")) -(comment) @comment +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm index 2e308f77..0a030b31 100644 --- a/runtime/queries/latex/highlights.scm +++ b/runtime/queries/latex/highlights.scm @@ -371,7 +371,7 @@ ((generic_command name:(generic_command_name) @_name . - arg: (_) @markup.underline.link) + arg: (_) @markup.link.url) (#match? @_name "^(\\\\url|\\\\href)$")) (ERROR) @error 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/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm index a102719b..a0bd3462 100644 --- a/runtime/queries/markdown/highlights.scm +++ b/runtime/queries/markdown/highlights.scm @@ -10,15 +10,16 @@ (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.underline.link - -; (link_label) @markup.label ; TODO: rename +(link_destination) @markup.link.url +(link_label) @markup.link.label [ (list_marker_plus) diff --git a/runtime/queries/markdown/injections.scm b/runtime/queries/markdown/injections.scm index ff3c5fe6..10dcab0b 100644 --- a/runtime/queries/markdown/injections.scm +++ b/runtime/queries/markdown/injections.scm @@ -1,6 +1,7 @@ (fenced_code_block (info_string) @injection.language - (code_fence_content) @injection.content) + (code_fence_content) @injection.content + (#set! injection.include-children)) ((html_block) @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/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/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/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/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..d8382e49 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") 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..4bb7d675 --- /dev/null +++ b/runtime/queries/scala/injections.scm @@ -0,0 +1 @@ +(comment) @comment diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm index f9eef6b5..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.underline.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/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/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..39bda293 --- /dev/null +++ b/runtime/queries/yaml/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml index fd4b0fea..33ff87fa 100644 --- a/runtime/themes/base16_default_dark.toml +++ b/runtime/themes/base16_default_dark.toml @@ -33,6 +33,10 @@ "namespace" = "base0E" "ui.help" = { fg = "base06", bg = "base01" } +"diff.plus" = "base0B" +"diff.delta" = "base09" +"diff.minus" = "base08" + "diagnostic" = { modifiers = ["underlined"] } "ui.gutter" = { bg = "base01" } "info" = "base0D" diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml index 596990da..3f4de265 100644 --- a/runtime/themes/base16_default_light.toml +++ b/runtime/themes/base16_default_light.toml @@ -33,6 +33,10 @@ "namespace" = "base0E" "ui.help" = { fg = "base06", bg = "base01" } +"diff.plus" = "base0B" +"diff.delta" = "base09" +"diff.minus" = "base08" + "diagnostic" = { modifiers = ["underlined"] } "ui.gutter" = { bg = "base01" } "info" = "base0D" diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml index 123ceaea..cbbfbf24 100644 --- a/runtime/themes/base16_terminal.toml +++ b/runtime/themes/base16_terminal.toml @@ -30,6 +30,10 @@ "namespace" = "light-magenta" "ui.help" = { fg = "white", bg = "black" } +"diff.plus" = "light-green" +"diff.delta" = "yellow" +"diff.minus" = "light-red" + "diagnostic" = { modifiers = ["underlined"] } "ui.gutter" = { bg = "black" } "info" = "light-blue" diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml index 86a6c34b..493e5ace 100644 --- a/runtime/themes/bogster.toml +++ b/runtime/themes/bogster.toml @@ -28,6 +28,10 @@ "module" = "#d32c5d" +"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 +53,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..78437649 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -39,6 +39,10 @@ "constant.numeric" = { fg = "pale_green" } "constant.character.escape" = { fg = "gold" } +"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..f01c7323 --- /dev/null +++ b/runtime/themes/dracula.toml @@ -0,0 +1,54 @@ +# Author : Sebastian Zivota +"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" } +"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" } + +[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..f5a0088c 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,10 @@ "module" = "blue" "special" = "orange" +"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..3038ef9c --- /dev/null +++ b/runtime/themes/everforest_light.toml @@ -0,0 +1,90 @@ +# 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" + +"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..9eae0915 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -28,6 +28,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" } diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml index 30829475..df8a922f 100644 --- a/runtime/themes/ingrid.toml +++ b/runtime/themes/ingrid.toml @@ -28,6 +28,10 @@ "module" = "#839A53" +"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..c8bf8ebd 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -39,6 +39,10 @@ "constant.numeric" = { fg = "#ae81ff" } "constant.character.escape" = { fg = "#ae81ff" } +"diff.plus" = { fg = "#a6e22e" } +"diff.delta" = { fg = "#fd971f" } +"diff.minus" = { fg = "#f92672" } + "ui.background" = { fg = "text", bg = "background" } "ui.window" = { bg = "widget" } @@ -65,7 +69,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 index bf8a4a84..5daeaf6c 100644 --- a/runtime/themes/monokai_pro.toml +++ b/runtime/themes/monokai_pro.toml @@ -77,6 +77,11 @@ # 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"] } diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml index d8a701f1..0763a5fb 100644 --- a/runtime/themes/monokai_pro_machine.toml +++ b/runtime/themes/monokai_pro_machine.toml @@ -77,6 +77,11 @@ # 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"] } diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml index 74459472..6a74a8d0 100644 --- a/runtime/themes/monokai_pro_octagon.toml +++ b/runtime/themes/monokai_pro_octagon.toml @@ -77,6 +77,11 @@ # 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"] } diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml index a9cf4b34..1a1a32ff 100644 --- a/runtime/themes/monokai_pro_ristretto.toml +++ b/runtime/themes/monokai_pro_ristretto.toml @@ -77,6 +77,11 @@ # 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"] } diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml index 232adfbd..36630493 100644 --- a/runtime/themes/monokai_pro_spectrum.toml +++ b/runtime/themes/monokai_pro_spectrum.toml @@ -77,6 +77,11 @@ # 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"] } diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml index a619f902..ae1ea29b 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -84,6 +84,11 @@ # nord15 - integer, floating point "constant.numeric" = "nord15" +# 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..dfeadbb2 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.label" = { 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 256b33c8..bf3a040b 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -1,4 +1,5 @@ # Author: RayGervais +# Author: ChrisHa "ui.background" = { bg = "base" } "ui.menu" = "surface" @@ -37,6 +38,9 @@ "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" @@ -44,6 +48,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.label" = { fg = "foam" } + [palette] base = "#191724" surface = "#1f1d2e" diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml index 43ba24ed..cc6d287e 100644 --- a/runtime/themes/rose_pine_dawn.toml +++ b/runtime/themes/rose_pine_dawn.toml @@ -1,8 +1,8 @@ -# Author: ChrisHa # Author: RayGervais +# Author: ChrisHa -"ui.background" = { bg = "base" } -"ui.menu" = "surface" +"ui.background" = { bg = "surface" } +"ui.menu" = "base" "ui.menu.selected" = { fg = "iris", bg = "surface" } "ui.linenr" = {fg = "subtle" } "ui.popup" = { bg = "overlay" } @@ -38,6 +38,9 @@ "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" @@ -45,6 +48,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.label" = { fg = "foam" } + [palette] base = "#faf4ed" surface = "#fffaf3" diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml index 979fdaac..ab345c8d 100644 --- a/runtime/themes/solarized_dark.toml +++ b/runtime/themes/solarized_dark.toml @@ -22,6 +22,10 @@ "module" = { fg = "violet" } "tag" = { fg = "magenta" } +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + # 背景 "ui.background" = { bg = "base03" } diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml index ded90cc4..6b0137fb 100644 --- a/runtime/themes/solarized_light.toml +++ b/runtime/themes/solarized_light.toml @@ -22,6 +22,10 @@ "module" = { fg = "violet" } "tag" = { fg = "magenta" } +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + # 背景 "ui.background" = { bg = "base03" } diff --git a/runtime/themes/spacebones_light.toml b/runtime/themes/spacebones_light.toml index 92f116ab..fcbe7b37 100644 --- a/runtime/themes/spacebones_light.toml +++ b/runtime/themes/spacebones_light.toml @@ -30,6 +30,10 @@ "label" = "#b1951d" "module" = "#b1951d" +"diff.plus" = "#2d9574" +"diff.delta" = "#715ab1" +"diff.minus" = "#ba2f59" + "warning" = { fg = "#da8b55" } "error" = { fg = "#e0211d" } "info" = { fg = "#b1951d" } diff --git a/theme.toml b/theme.toml index 0a79861e..ca0b2805 100644 --- a/theme.toml +++ b/theme.toml @@ -31,9 +31,13 @@ label = "honey" "markup.heading" = "lilac" "markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } -"markup.underline.link" = { fg = "silver", modifiers = ["underlined"] } +"markup.link.url" = { fg = "silver", modifiers = ["underlined"] } "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 index fe5d55d4..717530d0 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "xtask" -version = "0.5.0" +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.5", path = "../helix-term" } -helix-core = { version = "0.5", path = "../helix-core" } +helix-term = { version = "0.6", path = "../helix-term" } +helix-core = { version = "0.6", path = "../helix-core" } toml = "0.5"