mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-24 18:36:18 +04:00
Compare commits
66 Commits
e708e64261
...
c5469a4964
Author | SHA1 | Date | |
---|---|---|---|
|
c5469a4964 | ||
|
dc941d6d24 | ||
|
f305c7299d | ||
|
9e0d2d0a19 | ||
|
b8313da5a8 | ||
|
32ff0fce4a | ||
|
9e171e7d1d | ||
|
b92e8abfb3 | ||
|
8807dbfc40 | ||
|
15b478d433 | ||
|
467fad51b1 | ||
|
b855cd0cda | ||
|
6101b3a7a3 | ||
|
887bbbc375 | ||
|
7ee66c0658 | ||
|
843c058f0b | ||
|
ed7e5bd8dc | ||
|
b501a300e9 | ||
|
310bc04f23 | ||
|
2f6a113fbe | ||
|
8c6ca3c0fc | ||
|
b97c745631 | ||
|
d8e2aab201 | ||
|
188f701f50 | ||
|
83f1b98e80 | ||
|
4dc46f9472 | ||
|
4d0b7e57b1 | ||
|
548fd57489 | ||
|
8ed8d52e9d | ||
|
eeb5b7bbdd | ||
|
d95b21ddd3 | ||
|
56bb366f7e | ||
|
06d5b88dee | ||
|
e2d79c1891 | ||
|
5b3e0b64f0 | ||
|
07262f5170 | ||
|
6ec510d58f | ||
|
4d3612125b | ||
|
f9ac1f1ff1 | ||
|
2dbecd3c80 | ||
|
aa10b1fd11 | ||
|
07968880e6 | ||
|
4e2faa0be9 | ||
|
0fca0d057e | ||
|
68ee87695b | ||
|
b6e555a2ed | ||
|
48e15f77f7 | ||
|
59b020ec91 | ||
|
239262e094 | ||
|
23600e3ecb | ||
|
287e412780 | ||
|
bc18dc2c0c | ||
|
3fd7ca334e | ||
|
6373027c9e | ||
|
f06f481ad9 | ||
|
a219d5aabb | ||
|
d489c03c4f | ||
|
f621423e7d | ||
|
35802cb025 | ||
|
6cca98264f | ||
|
9806ca08b1 | ||
|
b5d56e57a6 | ||
|
6336ab43b3 | ||
|
6c2b4ce26e | ||
|
2ebefd73cf | ||
|
76c8a74cb4 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ target
|
||||
helix-term/rustfmt.toml
|
||||
result
|
||||
runtime/grammars
|
||||
.DS_Store
|
||||
|
642
Cargo.lock
generated
642
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,7 @@ package.helix-term.opt-level = 2
|
||||
tree-sitter = { version = "0.22" }
|
||||
nucleo = "0.5.0"
|
||||
slotmap = "1.0.7"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
[workspace.package]
|
||||
version = "24.7.0"
|
||||
|
@ -117,7 +117,7 @@ #### Note to packagers
|
||||
script could follow are:
|
||||
|
||||
1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime`
|
||||
1. `cargo build --profile opt --locked --path helix-term`
|
||||
1. `cargo build --profile opt --locked`
|
||||
1. `cp -r runtime $BUILD_DIR/usr/lib/helix/`
|
||||
1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx`
|
||||
|
||||
|
@ -27,8 +27,8 @@ # Configuration
|
||||
|
||||
You can use a custom configuration file by specifying it with the `-c` or
|
||||
`--config` command line argument, for example `hx -c path/to/custom-config.toml`.
|
||||
Additionally, you can reload the configuration file by sending the USR1
|
||||
signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
|
||||
You can reload the config file by issuing the `:config-reload` command. Alternatively, on Unix operating systems, you can reload it by sending the USR1
|
||||
signal to the Helix process, such as by using the command `pkill -USR1 hx`.
|
||||
|
||||
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
|
||||
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
|
||||
|
@ -24,6 +24,7 @@ ### `[editor]` Section
|
||||
|--|--|---------|
|
||||
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling | `5` |
|
||||
| `mouse` | Enable mouse mode | `true` |
|
||||
| `default-yank-register` | Default register used for yank/paste | `"` |
|
||||
| `middle-click-paste` | Middle click paste support | `true` |
|
||||
| `scroll-lines` | Number of lines to scroll per scroll wheel step | `3` |
|
||||
| `shell` | Shell to use when running external commands | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
|
||||
@ -32,6 +33,7 @@ ### `[editor]` Section
|
||||
| `cursorcolumn` | Highlight all columns with a cursor | `false` |
|
||||
| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
|
||||
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
|
||||
| `path-completion` | Enable filepath completion. Show files and directories if an existing path at the cursor was recognized, either absolute or relative to the current opened document or current working directory (if the buffer is not yet saved). Defaults to true. | `true` |
|
||||
| `auto-format` | Enable automatic formatting on save | `true` |
|
||||
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `250` |
|
||||
| `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |
|
||||
@ -52,6 +54,30 @@ ### `[editor]` Section
|
||||
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
|
||||
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
|
||||
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
|
||||
| `clipboard-provider` | Which API to use for clipboard interaction. One of `pasteboard` (MacOS), `wayland`, `x-clip`, `x-sel`, `win-32-yank`, `termux`, `tmux`, `windows`, `termcode`, `none`, or a custom command set. | Platform and environment specific. |
|
||||
|
||||
### `[editor.clipboard-provider]` Section
|
||||
|
||||
Helix can be configured wither to use a builtin clipboard configuration or to use
|
||||
a provided command.
|
||||
|
||||
For instance, setting it to use OSC 52 termcodes, the configuration would be:
|
||||
```toml
|
||||
[editor]
|
||||
clipboard-provider = "termcode"
|
||||
```
|
||||
|
||||
Alternatively, Helix can be configured to use arbitary commands for clipboard integration:
|
||||
|
||||
```toml
|
||||
[editor.clipboard-provider.custom]
|
||||
yank = { command = "cat", args = ["test.txt"] }
|
||||
paste = { command = "tee", args = ["test.txt"] }
|
||||
primary-yank = { command = "cat", args = ["test-primary.txt"] } # optional
|
||||
primary-paste = { command = "tee", args = ["test-primary.txt"] } # optional
|
||||
```
|
||||
|
||||
For custom commands the contents of the yank/paste is communicated over stdin/stdout.
|
||||
|
||||
### `[editor.statusline]` Section
|
||||
|
||||
@ -428,7 +454,8 @@ ### `[editor.inline-diagnostics]` Section
|
||||
|
||||
The new diagnostic rendering is not yet enabled by default. As soon as end of line or inline diagnostics are enabled the old diagnostics rendering is automatically disabled. The recommended default setting are:
|
||||
|
||||
```
|
||||
```toml
|
||||
[editor]
|
||||
end-of-line-diagnostics = "hint"
|
||||
[editor.inline-diagnostics]
|
||||
cursor-line = "warning" # show warnings and errors on the cursorline inline
|
||||
|
@ -3,6 +3,7 @@
|
||||
| ada | ✓ | ✓ | | `ada_language_server` |
|
||||
| adl | ✓ | ✓ | ✓ | |
|
||||
| agda | ✓ | | | |
|
||||
| amber | ✓ | | | |
|
||||
| astro | ✓ | | | |
|
||||
| awk | ✓ | ✓ | | `awk-language-server` |
|
||||
| bash | ✓ | ✓ | ✓ | `bash-language-server` |
|
||||
@ -91,7 +92,7 @@
|
||||
| hosts | ✓ | | | |
|
||||
| html | ✓ | | | `vscode-html-language-server`, `superhtml` |
|
||||
| hurl | ✓ | ✓ | ✓ | |
|
||||
| hyprlang | ✓ | | ✓ | |
|
||||
| hyprlang | ✓ | | ✓ | `hyprls` |
|
||||
| idris | | | | `idris2-lsp` |
|
||||
| iex | ✓ | | | |
|
||||
| ini | ✓ | | | |
|
||||
@ -136,6 +137,7 @@
|
||||
| move | ✓ | | | |
|
||||
| msbuild | ✓ | | ✓ | |
|
||||
| nasm | ✓ | ✓ | | |
|
||||
| nestedtext | ✓ | ✓ | ✓ | |
|
||||
| nickel | ✓ | | ✓ | `nls` |
|
||||
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
|
||||
| nix | ✓ | ✓ | | `nil`, `nixd` |
|
||||
@ -168,6 +170,7 @@
|
||||
| purescript | ✓ | ✓ | | `purescript-language-server` |
|
||||
| python | ✓ | ✓ | ✓ | `ruff`, `jedi-language-server`, `pylsp` |
|
||||
| qml | ✓ | | ✓ | `qmlls` |
|
||||
| quint | ✓ | | | `quint-language-server` |
|
||||
| r | ✓ | | | `R` |
|
||||
| racket | ✓ | | ✓ | `racket` |
|
||||
| regex | ✓ | | | |
|
||||
@ -189,6 +192,7 @@
|
||||
| sml | ✓ | | | |
|
||||
| snakemake | ✓ | | ✓ | `pylsp` |
|
||||
| solidity | ✓ | ✓ | | `solc` |
|
||||
| spade | ✓ | | ✓ | `spade-language-server` |
|
||||
| spicedb | ✓ | | | |
|
||||
| sql | ✓ | ✓ | | |
|
||||
| sshclientconfig | ✓ | | | |
|
||||
@ -204,6 +208,7 @@
|
||||
| task | ✓ | | | |
|
||||
| tcl | ✓ | | ✓ | |
|
||||
| templ | ✓ | | | `templ` |
|
||||
| textproto | ✓ | ✓ | ✓ | |
|
||||
| tfvars | ✓ | | ✓ | `terraform-ls` |
|
||||
| thrift | ✓ | | | |
|
||||
| todotxt | ✓ | | | |
|
||||
@ -215,7 +220,7 @@
|
||||
| typespec | ✓ | ✓ | ✓ | `tsp-server` |
|
||||
| typst | ✓ | | | `tinymist`, `typst-lsp` |
|
||||
| ungrammar | ✓ | | | |
|
||||
| unison | ✓ | | ✓ | |
|
||||
| unison | ✓ | ✓ | ✓ | |
|
||||
| uxntal | ✓ | | | |
|
||||
| v | ✓ | ✓ | ✓ | `v-analyzer` |
|
||||
| vala | ✓ | ✓ | | `vala-language-server` |
|
||||
|
@ -112,42 +112,43 @@ #### Shell
|
||||
|
||||
### Selection manipulation
|
||||
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `s` | Select all regex matches inside selections | `select_regex` |
|
||||
| `S` | Split selection into sub selections on regex matches | `split_selection` |
|
||||
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
|
||||
| `Alt-minus` | Merge selections | `merge_selections` |
|
||||
| `Alt-_` | Merge consecutive selections | `merge_consecutive_selections` |
|
||||
| `&` | Align selection in columns | `align_selections` |
|
||||
| `_` | Trim whitespace from the selection | `trim_selections` |
|
||||
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
|
||||
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
|
||||
| `Alt-:` | Ensures the selection is in forward direction | `ensure_selections_forward` |
|
||||
| `,` | Keep only the primary selection | `keep_primary_selection` |
|
||||
| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
|
||||
| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
|
||||
| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` |
|
||||
| `(` | Rotate main selection backward | `rotate_selections_backward` |
|
||||
| `)` | Rotate main selection forward | `rotate_selections_forward` |
|
||||
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
|
||||
| `Alt-)` | Rotate selection contents forward | `rotate_selection_contents_forward` |
|
||||
| `%` | Select entire file | `select_all` |
|
||||
| `x` | Select current line, if already selected, extend to next line | `extend_line_below` |
|
||||
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
|
||||
| `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` |
|
||||
| `J` | Join lines inside selection | `join_selections` |
|
||||
| `Alt-J` | Join lines inside selection and select the inserted space | `join_selections_space` |
|
||||
| `K` | Keep selections matching the regex | `keep_selections` |
|
||||
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
|
||||
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
|
||||
| `Alt-o`, `Alt-up` | Expand selection to parent syntax node (**TS**) | `expand_selection` |
|
||||
| `Alt-i`, `Alt-down` | Shrink syntax tree object selection (**TS**) | `shrink_selection` |
|
||||
| `Alt-p`, `Alt-left` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` |
|
||||
| `Alt-n`, `Alt-right` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` |
|
||||
| `Alt-a` | Select all sibling nodes in syntax tree (**TS**) | `select_all_siblings` |
|
||||
| `Alt-e` | Move to end of parent node in syntax tree (**TS**) | `move_parent_node_end` |
|
||||
| `Alt-b` | Move to start of parent node in syntax tree (**TS**) | `move_parent_node_start` |
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `s` | Select all regex matches inside selections | `select_regex` |
|
||||
| `S` | Split selection into sub selections on regex matches | `split_selection` |
|
||||
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
|
||||
| `Alt-minus` | Merge selections | `merge_selections` |
|
||||
| `Alt-_` | Merge consecutive selections | `merge_consecutive_selections` |
|
||||
| `&` | Align selection in columns | `align_selections` |
|
||||
| `_` | Trim whitespace from the selection | `trim_selections` |
|
||||
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
|
||||
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
|
||||
| `Alt-:` | Ensures the selection is in forward direction | `ensure_selections_forward` |
|
||||
| `,` | Keep only the primary selection | `keep_primary_selection` |
|
||||
| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
|
||||
| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
|
||||
| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` |
|
||||
| `(` | Rotate main selection backward | `rotate_selections_backward` |
|
||||
| `)` | Rotate main selection forward | `rotate_selections_forward` |
|
||||
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
|
||||
| `Alt-)` | Rotate selection contents forward | `rotate_selection_contents_forward` |
|
||||
| `%` | Select entire file | `select_all` |
|
||||
| `x` | Select current line, if already selected, extend to next line | `extend_line_below` |
|
||||
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
|
||||
| `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` |
|
||||
| `J` | Join lines inside selection | `join_selections` |
|
||||
| `Alt-J` | Join lines inside selection and select the inserted space | `join_selections_space` |
|
||||
| `K` | Keep selections matching the regex | `keep_selections` |
|
||||
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
|
||||
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
|
||||
| `Alt-o`, `Alt-up` | Expand selection to parent syntax node (**TS**) | `expand_selection` |
|
||||
| `Alt-i`, `Alt-down` | Shrink syntax tree object selection (**TS**) | `shrink_selection` |
|
||||
| `Alt-p`, `Alt-left` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` |
|
||||
| `Alt-n`, `Alt-right` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` |
|
||||
| `Alt-a` | Select all sibling nodes in syntax tree (**TS**) | `select_all_siblings` |
|
||||
| `Alt-I`, `Alt-Shift-down`| Select all children nodes in syntax tree (**TS**) | `select_all_children` |
|
||||
| `Alt-e` | Move to end of parent node in syntax tree (**TS**) | `move_parent_node_end` |
|
||||
| `Alt-b` | Move to start of parent node in syntax tree (**TS**) | `move_parent_node_start` |
|
||||
|
||||
### Search
|
||||
|
||||
@ -281,7 +282,7 @@ #### Space mode
|
||||
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `f` | Open file picker | `file_picker` |
|
||||
| `f` | Open file picker at LSP workspace root | `file_picker` |
|
||||
| `F` | Open file picker at current working directory | `file_picker_in_current_directory` |
|
||||
| `b` | Open buffer picker | `buffer_picker` |
|
||||
| `j` | Open jumplist picker | `jumplist_picker` |
|
||||
|
@ -69,6 +69,7 @@ ## Language configuration
|
||||
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
|
||||
| `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section)
|
||||
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
|
||||
| `path-completion` | Overrides the `editor.path-completion` config key for the language. |
|
||||
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
|
||||
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.
|
||||
|
||||
|
@ -101,7 +101,15 @@ ### AppImage
|
||||
chmod +x helix-*.AppImage # change permission for executable mode
|
||||
./helix-*.AppImage # run helix
|
||||
```
|
||||
|
||||
|
||||
You can optionally [add the `.desktop` file](./building-from-source.md#configure-the-desktop-shortcut). Helix must be installed in `PATH` with the name `hx`. For example:
|
||||
```sh
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
mv helix-*.AppImage "$HOME/.local/bin/hx"
|
||||
```
|
||||
|
||||
and make sure `~/.local/bin` is in your `PATH`.
|
||||
|
||||
## macOS
|
||||
|
||||
### Homebrew Core
|
||||
|
@ -4,10 +4,31 @@ ## Key remapping
|
||||
file. (More powerful solutions such as rebinding via commands will be
|
||||
available in the future).
|
||||
|
||||
There are three kinds of commands that can be used in keymaps:
|
||||
|
||||
* Static commands: commands like `move_char_right` which are usually bound to
|
||||
keys and used for movement and editing. A list of static commands is
|
||||
available in the [Keymap](./keymap.html) documentation and in the source code
|
||||
in [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
|
||||
at the invocation of `static_commands!` macro.
|
||||
* Typable commands: commands that can be executed from command mode (`:`), for
|
||||
example `:write!`. See the [Commands](./commands.html) documentation for a
|
||||
list of available typeable commands or the `TypableCommandList` declaration in
|
||||
the source code at [`helix-term/src/commands/typed.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands/typed.rs).
|
||||
* Macros: sequences of keys that are executed in order. These keybindings
|
||||
start with `@` and then list any number of keys to be executed. For example
|
||||
`@miw` can be used to select the surrounding word. For now, macro keybindings
|
||||
are not allowed in keybinding sequences due to limitations in the way that
|
||||
command sequences are executed. Modifier keys (e.g. Alt+o) can be used
|
||||
like `"<A-o>"`, e.g. `"@miw<A-o>"`
|
||||
|
||||
To remap keys, create a `config.toml` file in your `helix` configuration
|
||||
directory (default `~/.config/helix` on Linux systems) with a structure like
|
||||
this:
|
||||
|
||||
> 💡 To set a modifier + key as a keymap, type `A-X = ...` or `C-X = ...` for Alt + X or Ctrl + X. Combine with Shift using a dash, e.g. `C-S-esc`.
|
||||
> Within macros, wrap them in `<>`, e.g. `<A-X>` and `<C-X>` to distinguish from the `A` or `C` keys.
|
||||
|
||||
```toml
|
||||
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
|
||||
[keys.normal]
|
||||
@ -18,6 +39,7 @@ # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
|
||||
"C-S-esc" = "extend_line" # Maps Ctrl-Shift-Escape to extend_line
|
||||
g = { a = "code_action" } # Maps `ga` to show possible code actions
|
||||
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
|
||||
"A-x" = "@x<A-d>" # Maps Alt-x to a macro selecting the whole line and deleting it without yanking it
|
||||
|
||||
[keys.insert]
|
||||
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
|
||||
@ -74,21 +96,3 @@ ## Special keys and modifiers
|
||||
| Escape | `"esc"` |
|
||||
|
||||
Keys can be disabled by binding them to the `no_op` command.
|
||||
|
||||
## Commands
|
||||
|
||||
There are three kinds of commands that can be used in keymaps:
|
||||
|
||||
* Static commands: commands like `move_char_right` which are usually bound to
|
||||
keys and used for movement and editing. A list of static commands is
|
||||
available in the [Keymap](./keymap.html) documentation and in the source code
|
||||
in [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
|
||||
at the invocation of `static_commands!` macro and the `TypableCommandList`.
|
||||
* Typable commands: commands that can be executed from command mode (`:`), for
|
||||
example `:write!`. See the [Commands](./commands.html) documentation for a
|
||||
list of available typeable commands.
|
||||
* Macros: sequences of keys that are executed in order. These keybindings
|
||||
start with `@` and then list any number of keys to be executed. For example
|
||||
`@miw` can be used to select the surrounding word. For now, macro keybindings
|
||||
are not allowed in keybinding sequences due to limitations in the way that
|
||||
command sequences are executed.
|
||||
|
@ -29,7 +29,7 @@ unicode-segmentation = "1.12"
|
||||
# For now lets lock the version to avoid rendering glitches
|
||||
# when installing without `--locked`
|
||||
unicode-width = "=0.1.12"
|
||||
unicode-general-category = "0.6"
|
||||
unicode-general-category = "1.0"
|
||||
slotmap.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
once_cell = "1.20"
|
||||
@ -39,7 +39,7 @@ bitflags = "2.6"
|
||||
ahash = "0.8.11"
|
||||
hashbrown = { version = "0.14.5", features = ["raw"] }
|
||||
dunce = "1.0"
|
||||
url = "2.5.0"
|
||||
url = "2.5.3"
|
||||
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
12
helix-core/src/completion.rs
Normal file
12
helix-core/src/completion.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::Transaction;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct CompletionItem {
|
||||
pub transaction: Transaction,
|
||||
pub label: Cow<'static, str>,
|
||||
pub kind: Cow<'static, str>,
|
||||
/// Containing Markdown
|
||||
pub documentation: String,
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
pub mod auto_pairs;
|
||||
pub mod chars;
|
||||
pub mod comment;
|
||||
pub mod completion;
|
||||
pub mod config;
|
||||
pub mod diagnostic;
|
||||
pub mod diff;
|
||||
@ -63,6 +64,7 @@ pub mod unicode {
|
||||
pub use smallvec::{smallvec, SmallVec};
|
||||
pub use syntax::Syntax;
|
||||
|
||||
pub use completion::CompletionItem;
|
||||
pub use diagnostic::Diagnostic;
|
||||
|
||||
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
|
||||
|
@ -125,6 +125,9 @@ pub struct LanguageConfiguration {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub formatter: Option<FormatterConfiguration>,
|
||||
|
||||
/// If set, overrides `editor.path-completion`.
|
||||
pub path_completion: Option<bool>,
|
||||
|
||||
#[serde(default)]
|
||||
pub diagnostic_severity: Severity,
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
/// Given a slice of text, return the text re-wrapped to fit it
|
||||
/// within the given width.
|
||||
pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString<LazyCompact> {
|
||||
let options = Options::new(text_width).word_splitter(NoHyphenation);
|
||||
let options = Options::new(text_width)
|
||||
.word_splitter(NoHyphenation)
|
||||
.word_separator(textwrap::WordSeparator::AsciiSpace);
|
||||
textwrap::refill(text, options).into()
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::future::Future;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use oneshot::channel as cancelation;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
pub type CancelTx = oneshot::Sender<()>;
|
||||
pub type CancelRx = oneshot::Receiver<()>;
|
||||
|
||||
pub async fn cancelable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> {
|
||||
pub async fn cancelable_future<T>(
|
||||
future: impl Future<Output = T>,
|
||||
cancel: impl Borrow<TaskHandle>,
|
||||
) -> Option<T> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel => {
|
||||
_ = cancel.borrow().canceled() => {
|
||||
None
|
||||
}
|
||||
res = future => {
|
||||
@ -17,3 +20,268 @@ pub async fn cancelable_future<T>(future: impl Future<Output = T>, cancel: Cance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Shared {
|
||||
state: AtomicU64,
|
||||
// `Notify` has some features that we don't really need here because it
|
||||
// supports waking single tasks (`notify_one`) and does its own (more
|
||||
// complicated) state tracking, we could reimplement the waiter linked list
|
||||
// with modest effort and reduce memory consumption by one word/8 bytes and
|
||||
// reduce code complexity/number of atomic operations.
|
||||
//
|
||||
// I don't think that's worth the complexity (unsafe code).
|
||||
//
|
||||
// if we only cared about async code then we could also only use a notify
|
||||
// (without the generation count), this would be equivalent (or maybe more
|
||||
// correct if we want to allow cloning the TX) but it would be extremly slow
|
||||
// to frequently check for cancelation from sync code
|
||||
notify: Notify,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
fn generation(&self) -> u32 {
|
||||
self.state.load(Relaxed) as u32
|
||||
}
|
||||
|
||||
fn num_running(&self) -> u32 {
|
||||
(self.state.load(Relaxed) >> 32) as u32
|
||||
}
|
||||
|
||||
/// Increments the generation count and sets `num_running`
|
||||
/// to the provided value, this operation is not with
|
||||
/// regard to the generation counter (doesn't use `fetch_add`)
|
||||
/// so the calling code must ensure it cannot execute concurrently
|
||||
/// to maintain correctness (but not safety)
|
||||
fn inc_generation(&self, num_running: u32) -> (u32, u32) {
|
||||
let state = self.state.load(Relaxed);
|
||||
let generation = state as u32;
|
||||
let prev_running = (state >> 32) as u32;
|
||||
// no need to create a new generation if the refcount is zero (fastpath)
|
||||
if prev_running == 0 && num_running == 0 {
|
||||
return (generation, 0);
|
||||
}
|
||||
let new_generation = generation.saturating_add(1);
|
||||
self.state.store(
|
||||
new_generation as u64 | ((num_running as u64) << 32),
|
||||
Relaxed,
|
||||
);
|
||||
self.notify.notify_waiters();
|
||||
(new_generation, prev_running)
|
||||
}
|
||||
|
||||
fn inc_running(&self, generation: u32) {
|
||||
let mut state = self.state.load(Relaxed);
|
||||
loop {
|
||||
let current_generation = state as u32;
|
||||
if current_generation != generation {
|
||||
break;
|
||||
}
|
||||
let off = 1 << 32;
|
||||
let res = self.state.compare_exchange_weak(
|
||||
state,
|
||||
state.saturating_add(off),
|
||||
Relaxed,
|
||||
Relaxed,
|
||||
);
|
||||
match res {
|
||||
Ok(_) => break,
|
||||
Err(new_state) => state = new_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dec_running(&self, generation: u32) {
|
||||
let mut state = self.state.load(Relaxed);
|
||||
loop {
|
||||
let current_generation = state as u32;
|
||||
if current_generation != generation {
|
||||
break;
|
||||
}
|
||||
let num_running = (state >> 32) as u32;
|
||||
// running can't be zero here, that would mean we miscounted somewhere
|
||||
assert_ne!(num_running, 0);
|
||||
let off = 1 << 32;
|
||||
let res = self
|
||||
.state
|
||||
.compare_exchange_weak(state, state - off, Relaxed, Relaxed);
|
||||
match res {
|
||||
Ok(_) => break,
|
||||
Err(new_state) => state = new_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This intentionally doesn't implement `Clone` and requires a mutable reference
|
||||
// for cancelation to avoid races (in inc_generation).
|
||||
|
||||
/// A task controller allows managing a single subtask enabling the controller
|
||||
/// to cancel the subtask and to check whether it is still running.
|
||||
///
|
||||
/// For efficiency reasons the controller can be reused/restarted,
|
||||
/// in that case the previous task is automatically canceled.
|
||||
///
|
||||
/// If the controller is dropped, the subtasks are automatically canceled.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TaskController {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl TaskController {
|
||||
pub fn new() -> Self {
|
||||
TaskController::default()
|
||||
}
|
||||
/// Cancels the active task (handle).
|
||||
///
|
||||
/// Returns whether any tasks were still running before the cancelation.
|
||||
pub fn cancel(&mut self) -> bool {
|
||||
self.shared.inc_generation(0).1 != 0
|
||||
}
|
||||
|
||||
/// Checks whether there are any task handles
|
||||
/// that haven't been dropped (or canceled) yet.
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.shared.num_running() != 0
|
||||
}
|
||||
|
||||
/// Starts a new task and cancels the previous task (handles).
|
||||
pub fn restart(&mut self) -> TaskHandle {
|
||||
TaskHandle {
|
||||
generation: self.shared.inc_generation(1).0,
|
||||
shared: self.shared.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TaskController {
|
||||
fn drop(&mut self) {
|
||||
self.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle that is used to link a task with a task controller.
|
||||
///
|
||||
/// It can be used to cancel async futures very efficiently but can also be checked for
|
||||
/// cancelation very quickly (single atomic read) in blocking code.
|
||||
/// The handle can be cheaply cloned (reference counted).
|
||||
///
|
||||
/// The TaskController can check whether a task is "running" by inspecting the
|
||||
/// refcount of the (current) tasks handles. Therefore, if that information
|
||||
/// is important, ensure that the handle is not dropped until the task fully
|
||||
/// completes.
|
||||
pub struct TaskHandle {
|
||||
shared: Arc<Shared>,
|
||||
generation: u32,
|
||||
}
|
||||
|
||||
impl Clone for TaskHandle {
|
||||
fn clone(&self) -> Self {
|
||||
self.shared.inc_running(self.generation);
|
||||
TaskHandle {
|
||||
shared: self.shared.clone(),
|
||||
generation: self.generation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TaskHandle {
|
||||
fn drop(&mut self) {
|
||||
self.shared.dec_running(self.generation);
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskHandle {
|
||||
/// Waits until [`TaskController::cancel`] is called for the corresponding
|
||||
/// [`TaskController`]. Immediately returns if `cancel` was already called since
|
||||
pub async fn canceled(&self) {
|
||||
let notified = self.shared.notify.notified();
|
||||
if !self.is_canceled() {
|
||||
notified.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.generation != self.shared.generation()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::future::poll_fn;
|
||||
|
||||
use futures_executor::block_on;
|
||||
use tokio::task::yield_now;
|
||||
|
||||
use crate::{cancelable_future, TaskController};
|
||||
|
||||
#[test]
|
||||
fn immediate_cancel() {
|
||||
let mut controller = TaskController::new();
|
||||
let handle = controller.restart();
|
||||
controller.cancel();
|
||||
assert!(handle.is_canceled());
|
||||
controller.restart();
|
||||
assert!(handle.is_canceled());
|
||||
|
||||
let res = block_on(cancelable_future(
|
||||
poll_fn(|_cx| std::task::Poll::Ready(())),
|
||||
handle,
|
||||
));
|
||||
assert!(res.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_count() {
|
||||
let mut controller = TaskController::new();
|
||||
let handle = controller.restart();
|
||||
assert!(controller.is_running());
|
||||
assert!(!handle.is_canceled());
|
||||
drop(handle);
|
||||
assert!(!controller.is_running());
|
||||
assert!(!controller.cancel());
|
||||
let handle = controller.restart();
|
||||
assert!(!handle.is_canceled());
|
||||
assert!(controller.is_running());
|
||||
let handle2 = handle.clone();
|
||||
assert!(!handle.is_canceled());
|
||||
assert!(controller.is_running());
|
||||
drop(handle2);
|
||||
assert!(!handle.is_canceled());
|
||||
assert!(controller.is_running());
|
||||
assert!(controller.cancel());
|
||||
assert!(handle.is_canceled());
|
||||
assert!(!controller.is_running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_cancel() {
|
||||
let mut controller = TaskController::new();
|
||||
let handle = controller.restart();
|
||||
assert!(!handle.is_canceled());
|
||||
|
||||
let res = block_on(cancelable_future(
|
||||
poll_fn(|_cx| std::task::Poll::Ready(())),
|
||||
handle,
|
||||
));
|
||||
assert!(res.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delayed_cancel() {
|
||||
let mut controller = TaskController::new();
|
||||
let handle = controller.restart();
|
||||
|
||||
let mut hit = false;
|
||||
let res = block_on(cancelable_future(
|
||||
async {
|
||||
controller.cancel();
|
||||
hit = true;
|
||||
yield_now().await;
|
||||
},
|
||||
handle,
|
||||
));
|
||||
assert!(res.is_none());
|
||||
assert!(hit);
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@
|
||||
//! to helix-view in the future if we manage to detach the compositor from its rendering backend.
|
||||
|
||||
use anyhow::Result;
|
||||
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
|
||||
pub use cancel::{cancelable_future, TaskController, TaskHandle};
|
||||
pub use debounce::{send_blocking, AsyncHook};
|
||||
pub use redraw::{
|
||||
lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard, RequestRedrawOnDrop,
|
||||
|
@ -30,7 +30,7 @@ log = "0.4"
|
||||
# cloning/compiling tree-sitter grammars
|
||||
cc = { version = "1" }
|
||||
threadpool = { version = "1.0" }
|
||||
tempfile = "3.13.0"
|
||||
tempfile = "3.14.0"
|
||||
dunce = "1.0.5"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
@ -22,10 +22,10 @@ license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.6.0"
|
||||
serde = { version = "1.0.209", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
serde_repr = "0.1"
|
||||
url = {version = "2.0.0", features = ["serde"]}
|
||||
url = {version = "2.5.3", features = ["serde"]}
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
@ -26,7 +26,7 @@ globset = "0.4.15"
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.40", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio = { version = "1.41", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream = "0.1.15"
|
||||
parking_lot = "0.12.3"
|
||||
arc-swap = "1"
|
||||
|
@ -15,9 +15,11 @@ homepage.workspace = true
|
||||
dunce = "1.0"
|
||||
etcetera = "0.8"
|
||||
ropey = { version = "1.6.1", default-features = false }
|
||||
which = "6.0"
|
||||
which = "7.0"
|
||||
regex-cursor = "0.1.4"
|
||||
bitflags = "2.6"
|
||||
once_cell = "1.19"
|
||||
regex-automata = "0.4.8"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
|
||||
@ -26,4 +28,4 @@ windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Securit
|
||||
rustix = { version = "0.38", features = ["fs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.13"
|
||||
tempfile = "3.14"
|
||||
|
@ -1,9 +1,12 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
borrow::Cow,
|
||||
ffi::{OsStr, OsString},
|
||||
path::{Path, PathBuf},
|
||||
sync::RwLock,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
|
||||
// Get the current working directory.
|
||||
@ -59,6 +62,93 @@ pub fn which<T: AsRef<OsStr>>(
|
||||
})
|
||||
}
|
||||
|
||||
fn find_brace_end(src: &[u8]) -> Option<usize> {
|
||||
use regex_automata::meta::Regex;
|
||||
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::builder().build("[{}]").unwrap());
|
||||
let mut depth = 0;
|
||||
for mat in REGEX.find_iter(src) {
|
||||
let pos = mat.start();
|
||||
match src[pos] {
|
||||
b'{' => depth += 1,
|
||||
b'}' if depth == 0 => return Some(pos),
|
||||
b'}' => depth -= 1,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>) -> Cow<OsStr> {
|
||||
use regex_automata::meta::Regex;
|
||||
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::builder()
|
||||
.build_many(&[
|
||||
r"\$\{([^\}:]+):-",
|
||||
r"\$\{([^\}:]+):=",
|
||||
r"\$\{([^\}-]+)-",
|
||||
r"\$\{([^\}=]+)=",
|
||||
r"\$\{([^\}]+)",
|
||||
r"\$(\w+)",
|
||||
])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let bytes = src.as_encoded_bytes();
|
||||
let mut res = Vec::with_capacity(bytes.len());
|
||||
let mut pos = 0;
|
||||
for captures in REGEX.captures_iter(bytes) {
|
||||
let mat = captures.get_match().unwrap();
|
||||
let pattern_id = mat.pattern().as_usize();
|
||||
let mut range = mat.range();
|
||||
let var = &bytes[captures.get_group(1).unwrap().range()];
|
||||
let default = if pattern_id != 5 {
|
||||
let Some(bracket_pos) = find_brace_end(&bytes[range.end..]) else {
|
||||
break;
|
||||
};
|
||||
let default = &bytes[range.end..range.end + bracket_pos];
|
||||
range.end += bracket_pos + 1;
|
||||
default
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
// safety: this is a codepoint aligned substring of an osstr (always valid)
|
||||
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
|
||||
let expansion = resolve(var);
|
||||
let expansion = match &expansion {
|
||||
Some(val) => {
|
||||
if val.is_empty() && pattern_id < 2 {
|
||||
default
|
||||
} else {
|
||||
val.as_encoded_bytes()
|
||||
}
|
||||
}
|
||||
None => default,
|
||||
};
|
||||
res.extend_from_slice(&bytes[pos..range.start]);
|
||||
pos = range.end;
|
||||
res.extend_from_slice(expansion);
|
||||
}
|
||||
if pos == 0 {
|
||||
src.into()
|
||||
} else {
|
||||
res.extend_from_slice(&bytes[pos..]);
|
||||
// safety: this is a composition of valid osstr (and codepoint aligned slices which are also valid)
|
||||
unsafe { OsString::from_encoded_bytes_unchecked(res) }.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// performs substitution of enviorment variables. Supports the following (POSIX) syntax:
|
||||
///
|
||||
/// * `$<var>`, `${<var>}`
|
||||
/// * `${<var>:-<default>}`, `${<var>-<default>}`
|
||||
/// * `${<var>:=<default>}`, `${<var>=default}`
|
||||
///
|
||||
pub fn expand<S: AsRef<OsStr> + ?Sized>(src: &S) -> Cow<OsStr> {
|
||||
expand_impl(src.as_ref(), |var| std::env::var_os(var))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecutableNotFoundError {
|
||||
command: String,
|
||||
@ -75,7 +165,9 @@ impl std::error::Error for ExecutableNotFoundError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{current_working_dir, set_current_working_dir};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
use super::{current_working_dir, expand_impl, set_current_working_dir};
|
||||
|
||||
#[test]
|
||||
fn current_dir_is_set() {
|
||||
@ -88,4 +180,34 @@ fn current_dir_is_set() {
|
||||
let cwd = current_working_dir();
|
||||
assert_eq!(cwd, new_path);
|
||||
}
|
||||
|
||||
macro_rules! assert_env_expand {
|
||||
($env: expr, $lhs: expr, $rhs: expr) => {
|
||||
assert_eq!(&*expand_impl($lhs.as_ref(), $env), OsStr::new($rhs));
|
||||
};
|
||||
}
|
||||
|
||||
/// paths that should work on all platforms
|
||||
#[test]
|
||||
fn test_env_expand() {
|
||||
let env = |var: &OsStr| -> Option<OsString> {
|
||||
match var.to_str().unwrap() {
|
||||
"FOO" => Some("foo".into()),
|
||||
"EMPTY" => Some("".into()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
assert_env_expand!(env, "pass_trough", "pass_trough");
|
||||
assert_env_expand!(env, "$FOO", "foo");
|
||||
assert_env_expand!(env, "bar/$FOO/baz", "bar/foo/baz");
|
||||
assert_env_expand!(env, "bar/${FOO}/baz", "bar/foo/baz");
|
||||
assert_env_expand!(env, "baz/${BAR:-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR:=bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${BAR=bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY:-bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY:=bar}/foo", "baz/bar/foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY-bar}/foo", "baz//foo");
|
||||
assert_env_expand!(env, "baz/${EMPTY=bar}/foo", "baz//foo");
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
pub use etcetera::home_dir;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex_cursor::{engines::meta::Regex, Input};
|
||||
use ropey::RopeSlice;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
ops::Range,
|
||||
path::{Component, Path, PathBuf, MAIN_SEPARATOR_STR},
|
||||
};
|
||||
|
||||
@ -51,7 +55,7 @@ pub fn expand_tilde<'a, P>(path: P) -> Cow<'a, Path>
|
||||
|
||||
/// Normalize a path without resolving symlinks.
|
||||
// Strategy: start from the first component and move up. Cannonicalize previous path,
|
||||
// join component, cannonicalize new path, strip prefix and join to the final result.
|
||||
// join component, canonicalize new path, strip prefix and join to the final result.
|
||||
pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
|
||||
let mut components = path.as_ref().components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
@ -201,6 +205,96 @@ pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
|
||||
ret
|
||||
}
|
||||
|
||||
fn path_component_regex(windows: bool) -> String {
|
||||
// TODO: support backslash path escape on windows (when using git bash for example)
|
||||
let space_escape = if windows { r"[\^`]\s" } else { r"[\\]\s" };
|
||||
// partially baesd on what's allowed in an url but with some care to avoid
|
||||
// false positivies (like any kind of brackets or quotes)
|
||||
r"[\w@.\-+#$%?!,;~&]|".to_owned() + space_escape
|
||||
}
|
||||
|
||||
/// Regex for delimited environment captures like `${HOME}`.
|
||||
fn braced_env_regex(windows: bool) -> String {
|
||||
r"\$\{(?:".to_owned() + &path_component_regex(windows) + r"|[/:=])+\}"
|
||||
}
|
||||
|
||||
fn compile_path_regex(
|
||||
prefix: &str,
|
||||
postfix: &str,
|
||||
match_single_file: bool,
|
||||
windows: bool,
|
||||
) -> Regex {
|
||||
let first_component = format!(
|
||||
"(?:{}|(?:{}))",
|
||||
braced_env_regex(windows),
|
||||
path_component_regex(windows)
|
||||
);
|
||||
// For all components except the first we allow an equals so that `foo=/
|
||||
// bar/baz` does not include foo. This is primarily intended for url queries
|
||||
// (where an equals is never in the first component)
|
||||
let component = format!("(?:{first_component}|=)");
|
||||
let sep = if windows { r"[/\\]" } else { "/" };
|
||||
let url_prefix = r"[\w+\-.]+://??";
|
||||
let path_prefix = if windows {
|
||||
// single slash handles most windows prefixes (like\\server\...) but `\
|
||||
// \?\C:\..` (and C:\) needs special handling, since we don't allow : in path
|
||||
// components (so that colon separated paths and <path>:<line> work)
|
||||
r"\\\\\?\\\w:|\w:|\\|"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let path_start = format!("(?:{first_component}+|~|{path_prefix}{url_prefix})");
|
||||
let optional = if match_single_file {
|
||||
format!("|{path_start}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let path_regex = format!(
|
||||
"{prefix}(?:{path_start}?(?:(?:{sep}{component}+)+{sep}?|{sep}){optional}){postfix}"
|
||||
);
|
||||
Regex::new(&path_regex).unwrap()
|
||||
}
|
||||
|
||||
/// If `src` ends with a path then this function returns the part of the slice.
|
||||
pub fn get_path_suffix(src: RopeSlice<'_>, match_single_file: bool) -> Option<RopeSlice<'_>> {
|
||||
let regex = if match_single_file {
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "$", true, cfg!(windows)));
|
||||
&*REGEX
|
||||
} else {
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "$", false, cfg!(windows)));
|
||||
&*REGEX
|
||||
};
|
||||
|
||||
regex
|
||||
.find(Input::new(src))
|
||||
.map(|mat| src.byte_slice(mat.range()))
|
||||
}
|
||||
|
||||
/// Returns an iterator of the **byte** ranges in src that contain a path.
|
||||
pub fn find_paths(
|
||||
src: RopeSlice<'_>,
|
||||
match_single_file: bool,
|
||||
) -> impl Iterator<Item = Range<usize>> + '_ {
|
||||
let regex = if match_single_file {
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "", true, cfg!(windows)));
|
||||
&*REGEX
|
||||
} else {
|
||||
static REGEX: Lazy<Regex> = Lazy::new(|| compile_path_regex("", "", false, cfg!(windows)));
|
||||
&*REGEX
|
||||
};
|
||||
regex.find_iter(Input::new(src)).map(|mat| mat.range())
|
||||
}
|
||||
|
||||
/// Performs substitution of `~` and environment variables, see [`env::expand`](crate::env::expand) and [`expand_tilde`]
|
||||
pub fn expand<T: AsRef<Path> + ?Sized>(path: &T) -> Cow<'_, Path> {
|
||||
let path = path.as_ref();
|
||||
let path = expand_tilde(path);
|
||||
match crate::env::expand(&*path) {
|
||||
Cow::Borrowed(_) => path,
|
||||
Cow::Owned(path) => PathBuf::from(path).into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
@ -208,7 +302,10 @@ mod tests {
|
||||
path::{Component, Path},
|
||||
};
|
||||
|
||||
use crate::path;
|
||||
use regex_cursor::Input;
|
||||
use ropey::RopeSlice;
|
||||
|
||||
use crate::path::{self, compile_path_regex};
|
||||
|
||||
#[test]
|
||||
fn expand_tilde() {
|
||||
@ -228,4 +325,127 @@ fn expand_tilde() {
|
||||
assert_ne!(component_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! assert_match {
|
||||
($regex: expr, $haystack: expr) => {
|
||||
let haystack = Input::new(RopeSlice::from($haystack));
|
||||
assert!(
|
||||
$regex.is_match(haystack),
|
||||
"regex should match {}",
|
||||
$haystack
|
||||
);
|
||||
};
|
||||
}
|
||||
macro_rules! assert_no_match {
|
||||
($regex: expr, $haystack: expr) => {
|
||||
let haystack = Input::new(RopeSlice::from($haystack));
|
||||
assert!(
|
||||
!$regex.is_match(haystack),
|
||||
"regex should not match {}",
|
||||
$haystack
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_matches {
|
||||
($regex: expr, $haystack: expr, [$($matches: expr),*]) => {
|
||||
let src = $haystack;
|
||||
let matches: Vec<_> = $regex
|
||||
.find_iter(Input::new(RopeSlice::from(src)))
|
||||
.map(|it| &src[it.range()])
|
||||
.collect();
|
||||
assert_eq!(matches, vec![$($matches),*]);
|
||||
};
|
||||
}
|
||||
|
||||
/// Linux-only path
|
||||
#[test]
|
||||
fn path_regex_unix() {
|
||||
// due to ambiguity with the `\` path separator we can't support space escapes `\ ` on windows
|
||||
let regex = compile_path_regex("^", "$", false, false);
|
||||
assert_match!(regex, "${FOO}/hello\\ world");
|
||||
assert_match!(regex, "${FOO}/\\ ");
|
||||
}
|
||||
|
||||
/// Windows-only paths
|
||||
#[test]
|
||||
fn path_regex_windows() {
|
||||
let regex = compile_path_regex("^", "$", false, true);
|
||||
assert_match!(regex, "${FOO}/hello^ world");
|
||||
assert_match!(regex, "${FOO}/hello` world");
|
||||
assert_match!(regex, "${FOO}/^ ");
|
||||
assert_match!(regex, "${FOO}/` ");
|
||||
assert_match!(regex, r"foo\bar");
|
||||
assert_match!(regex, r"foo\bar");
|
||||
assert_match!(regex, r"..\bar");
|
||||
assert_match!(regex, r"..\");
|
||||
assert_match!(regex, r"C:\");
|
||||
assert_match!(regex, r"\\?\C:\foo");
|
||||
assert_match!(regex, r"\\server\foo");
|
||||
}
|
||||
|
||||
/// Paths that should work on all platforms
|
||||
#[test]
|
||||
fn path_regex() {
|
||||
for windows in [false, true] {
|
||||
let regex = compile_path_regex("^", "$", false, windows);
|
||||
assert_no_match!(regex, "foo");
|
||||
assert_no_match!(regex, "");
|
||||
assert_match!(regex, "https://github.com/notifications/query=foo");
|
||||
assert_match!(regex, "file:///foo/bar");
|
||||
assert_match!(regex, "foo/bar");
|
||||
assert_match!(regex, "$HOME/foo");
|
||||
assert_match!(regex, "${FOO:-bar}/baz");
|
||||
assert_match!(regex, "foo/bar_");
|
||||
assert_match!(regex, "/home/bar");
|
||||
assert_match!(regex, "foo/");
|
||||
assert_match!(regex, "./");
|
||||
assert_match!(regex, "../");
|
||||
assert_match!(regex, "../..");
|
||||
assert_match!(regex, "./foo");
|
||||
assert_match!(regex, "./foo.rs");
|
||||
assert_match!(regex, "/");
|
||||
assert_match!(regex, "~/");
|
||||
assert_match!(regex, "~/foo");
|
||||
assert_match!(regex, "~/foo");
|
||||
assert_match!(regex, "~/foo/../baz");
|
||||
assert_match!(regex, "${HOME}/foo");
|
||||
assert_match!(regex, "$HOME/foo");
|
||||
assert_match!(regex, "/$FOO");
|
||||
assert_match!(regex, "/${FOO}");
|
||||
assert_match!(regex, "/${FOO}/${BAR}");
|
||||
assert_match!(regex, "/${FOO}/${BAR}/foo");
|
||||
assert_match!(regex, "/${FOO}/${BAR}");
|
||||
assert_match!(regex, "${FOO}/hello_$WORLD");
|
||||
assert_match!(regex, "${FOO}/hello_${WORLD}");
|
||||
let regex = compile_path_regex("", "", false, windows);
|
||||
assert_no_match!(regex, "");
|
||||
assert_matches!(
|
||||
regex,
|
||||
r#"${FOO}/hello_${WORLD} ${FOO}/hello_${WORLD} foo("./bar", "/home/foo")""#,
|
||||
[
|
||||
"${FOO}/hello_${WORLD}",
|
||||
"${FOO}/hello_${WORLD}",
|
||||
"./bar",
|
||||
"/home/foo"
|
||||
]
|
||||
);
|
||||
assert_matches!(
|
||||
regex,
|
||||
r#"--> helix-stdx/src/path.rs:427:13"#,
|
||||
["helix-stdx/src/path.rs"]
|
||||
);
|
||||
assert_matches!(
|
||||
regex,
|
||||
r#"PATH=/foo/bar:/bar/baz:${foo:-/foo}/bar:${PATH}"#,
|
||||
["/foo/bar", "/bar/baz", "${foo:-/foo}/bar"]
|
||||
);
|
||||
let regex = compile_path_regex("^", "$", true, windows);
|
||||
assert_no_match!(regex, "");
|
||||
assert_match!(regex, "foo");
|
||||
assert_match!(regex, "foo/");
|
||||
assert_match!(regex, "$FOO");
|
||||
assert_match!(regex, "${BAR}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,11 +56,11 @@ ignore = "0.4"
|
||||
pulldown-cmark = { version = "0.12", default-features = false }
|
||||
# file type detection
|
||||
content_inspector = "0.2.4"
|
||||
thiserror = "1.0"
|
||||
thiserror.workspace = true
|
||||
|
||||
# opening URLs
|
||||
open = "5.3.0"
|
||||
url = "2.5.2"
|
||||
open = "5.3.1"
|
||||
url = "2.5.3"
|
||||
|
||||
# config
|
||||
toml = "0.8"
|
||||
@ -74,7 +74,7 @@ grep-searcher = "0.1.14"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
|
||||
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
|
||||
libc = "0.2.161"
|
||||
libc = "0.2.164"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
|
||||
@ -85,5 +85,5 @@ helix-loader = { path = "../helix-loader" }
|
||||
[dev-dependencies]
|
||||
smallvec = "1.13"
|
||||
indoc = "2.0.5"
|
||||
tempfile = "3.13.0"
|
||||
tempfile = "3.14.0"
|
||||
same-file = "1.0.1"
|
||||
|
@ -6,7 +6,7 @@
|
||||
use futures_util::FutureExt;
|
||||
use helix_event::status;
|
||||
use helix_stdx::{
|
||||
path::expand_tilde,
|
||||
path::{self, find_paths},
|
||||
rope::{self, RopeSliceExt},
|
||||
};
|
||||
use helix_vcs::{FileChange, Hunk};
|
||||
@ -1272,53 +1272,31 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
|
||||
.unwrap_or_default();
|
||||
|
||||
let paths: Vec<_> = if selections.len() == 1 && primary.len() == 1 {
|
||||
// Secial case: if there is only one one-width selection, try to detect the
|
||||
// path under the cursor.
|
||||
let is_valid_path_char = |c: &char| {
|
||||
#[cfg(target_os = "windows")]
|
||||
let valid_chars = &[
|
||||
'@', '/', '\\', '.', '-', '_', '+', '#', '$', '%', '{', '}', '[', ']', ':', '!',
|
||||
'~', '=',
|
||||
];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let valid_chars = &['@', '/', '.', '-', '_', '+', '#', '$', '%', '~', '=', ':'];
|
||||
|
||||
valid_chars.contains(c) || c.is_alphabetic() || c.is_numeric()
|
||||
};
|
||||
|
||||
let cursor_pos = primary.cursor(text.slice(..));
|
||||
let pre_cursor_pos = cursor_pos.saturating_sub(1);
|
||||
let post_cursor_pos = cursor_pos + 1;
|
||||
let start_pos = if is_valid_path_char(&text.char(cursor_pos)) {
|
||||
cursor_pos
|
||||
} else if is_valid_path_char(&text.char(pre_cursor_pos)) {
|
||||
pre_cursor_pos
|
||||
} else {
|
||||
post_cursor_pos
|
||||
};
|
||||
|
||||
let prefix_len = text
|
||||
.chars_at(start_pos)
|
||||
.reversed()
|
||||
.take_while(is_valid_path_char)
|
||||
.count();
|
||||
|
||||
let postfix_len = text
|
||||
.chars_at(start_pos)
|
||||
.take_while(is_valid_path_char)
|
||||
.count();
|
||||
|
||||
let path: String = text
|
||||
.slice((start_pos - prefix_len)..(start_pos + postfix_len))
|
||||
.into();
|
||||
log::debug!("goto_file auto-detected path: {}", path);
|
||||
|
||||
vec![path]
|
||||
let mut pos = primary.cursor(text.slice(..));
|
||||
pos = text.char_to_byte(pos);
|
||||
let search_start = text
|
||||
.line_to_byte(text.byte_to_line(pos))
|
||||
.max(pos.saturating_sub(1000));
|
||||
let search_end = text
|
||||
.line_to_byte(text.byte_to_line(pos) + 1)
|
||||
.min(pos + 1000);
|
||||
let search_range = text.slice(search_start..search_end);
|
||||
// we also allow paths that are next to the cursor (can be ambigous but
|
||||
// rarely so in practice) so that gf on quoted/braced path works (not sure about this
|
||||
// but apparently that is how gf has worked historically in helix)
|
||||
let path = find_paths(search_range, true)
|
||||
.inspect(|mat| println!("{mat:?} {:?}", pos - search_start))
|
||||
.take_while(|range| search_start + range.start <= pos + 1)
|
||||
.find(|range| pos <= search_start + range.end)
|
||||
.map(|range| Cow::from(search_range.byte_slice(range)));
|
||||
log::debug!("goto_file auto-detected path: {path:?}");
|
||||
let path = path.unwrap_or_else(|| primary.fragment(text.slice(..)));
|
||||
vec![path.into_owned()]
|
||||
} else {
|
||||
// Otherwise use each selection, trimmed.
|
||||
selections
|
||||
.fragments(text.slice(..))
|
||||
.map(|sel| sel.trim().to_string())
|
||||
.map(|sel| sel.trim().to_owned())
|
||||
.filter(|sel| !sel.is_empty())
|
||||
.collect()
|
||||
};
|
||||
@ -1329,7 +1307,7 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = expand_tilde(Cow::from(PathBuf::from(sel)));
|
||||
let path = path::expand(&sel);
|
||||
let path = &rel_path.join(path);
|
||||
if path.is_dir() {
|
||||
let picker = ui::file_picker(path.into(), &cx.editor.config());
|
||||
@ -2735,7 +2713,9 @@ fn delete_selection_impl(cx: &mut Context, op: Operation, yank: YankAction) {
|
||||
// yank the selection
|
||||
let text = doc.text().slice(..);
|
||||
let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
|
||||
let reg_name = cx.register.unwrap_or('"');
|
||||
let reg_name = cx
|
||||
.register
|
||||
.unwrap_or_else(|| cx.editor.config.load().default_yank_register);
|
||||
if let Err(err) = cx.editor.registers.write(reg_name, values) {
|
||||
cx.editor.set_error(err.to_string());
|
||||
return;
|
||||
@ -4221,7 +4201,11 @@ fn commit_undo_checkpoint(cx: &mut Context) {
|
||||
// Yank / Paste
|
||||
|
||||
fn yank(cx: &mut Context) {
|
||||
yank_impl(cx.editor, cx.register.unwrap_or('"'));
|
||||
yank_impl(
|
||||
cx.editor,
|
||||
cx.register
|
||||
.unwrap_or(cx.editor.config().default_yank_register),
|
||||
);
|
||||
exit_select_mode(cx);
|
||||
}
|
||||
|
||||
@ -4282,7 +4266,12 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
|
||||
|
||||
fn yank_joined(cx: &mut Context) {
|
||||
let separator = doc!(cx.editor).line_ending.as_str();
|
||||
yank_joined_impl(cx.editor, separator, cx.register.unwrap_or('"'));
|
||||
yank_joined_impl(
|
||||
cx.editor,
|
||||
separator,
|
||||
cx.register
|
||||
.unwrap_or(cx.editor.config().default_yank_register),
|
||||
);
|
||||
exit_select_mode(cx);
|
||||
}
|
||||
|
||||
@ -4339,6 +4328,10 @@ fn paste_impl(
|
||||
return;
|
||||
}
|
||||
|
||||
if mode == Mode::Insert {
|
||||
doc.append_changes_to_history(view);
|
||||
}
|
||||
|
||||
let repeat = std::iter::repeat(
|
||||
// `values` is asserted to have at least one entry above.
|
||||
values
|
||||
@ -4438,7 +4431,12 @@ fn paste_primary_clipboard_before(cx: &mut Context) {
|
||||
}
|
||||
|
||||
fn replace_with_yanked(cx: &mut Context) {
|
||||
replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count());
|
||||
replace_with_yanked_impl(
|
||||
cx.editor,
|
||||
cx.register
|
||||
.unwrap_or(cx.editor.config().default_yank_register),
|
||||
cx.count(),
|
||||
);
|
||||
exit_select_mode(cx);
|
||||
}
|
||||
|
||||
@ -4501,7 +4499,8 @@ fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) {
|
||||
fn paste_after(cx: &mut Context) {
|
||||
paste(
|
||||
cx.editor,
|
||||
cx.register.unwrap_or('"'),
|
||||
cx.register
|
||||
.unwrap_or(cx.editor.config().default_yank_register),
|
||||
Paste::After,
|
||||
cx.count(),
|
||||
);
|
||||
@ -4511,7 +4510,8 @@ fn paste_after(cx: &mut Context) {
|
||||
fn paste_before(cx: &mut Context) {
|
||||
paste(
|
||||
cx.editor,
|
||||
cx.register.unwrap_or('"'),
|
||||
cx.register
|
||||
.unwrap_or(cx.editor.config().default_yank_register),
|
||||
Paste::Before,
|
||||
cx.count(),
|
||||
);
|
||||
@ -5365,7 +5365,8 @@ fn insert_register(cx: &mut Context) {
|
||||
cx.register = Some(ch);
|
||||
paste(
|
||||
cx.editor,
|
||||
cx.register.unwrap_or('"'),
|
||||
cx.register
|
||||
.unwrap_or(cx.editor.config().default_yank_register),
|
||||
Paste::Cursor,
|
||||
cx.count(),
|
||||
);
|
||||
|
@ -1074,7 +1074,7 @@ fn show_clipboard_provider(
|
||||
}
|
||||
|
||||
cx.editor
|
||||
.set_status(cx.editor.registers.clipboard_provider_name().to_string());
|
||||
.set_status(cx.editor.registers.clipboard_provider_name());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -4,20 +4,20 @@
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use futures_util::FutureExt;
|
||||
use helix_core::chars::char_is_word;
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_event::{
|
||||
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||
};
|
||||
use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
|
||||
use helix_lsp::lsp;
|
||||
use helix_lsp::util::pos_to_lsp_pos;
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::{Mode, SavePoint};
|
||||
use helix_view::handlers::lsp::CompletionEvent;
|
||||
use helix_view::{DocumentId, Editor, ViewId};
|
||||
use path::path_completion;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_stream::StreamExt as _;
|
||||
|
||||
use crate::commands;
|
||||
use crate::compositor::Compositor;
|
||||
@ -27,10 +27,13 @@
|
||||
use crate::keymap::MappableCommand;
|
||||
use crate::ui::editor::InsertEvent;
|
||||
use crate::ui::lsp::SignatureHelp;
|
||||
use crate::ui::{self, CompletionItem, Popup};
|
||||
use crate::ui::{self, Popup};
|
||||
|
||||
use super::Handlers;
|
||||
pub use item::{CompletionItem, LspCompletionItem};
|
||||
pub use resolve::ResolveHandler;
|
||||
mod item;
|
||||
mod path;
|
||||
mod resolve;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@ -53,12 +56,8 @@ pub(super) struct CompletionHandler {
|
||||
/// currently active trigger which will cause a
|
||||
/// completion request after the timeout
|
||||
trigger: Option<Trigger>,
|
||||
/// A handle for currently active completion request.
|
||||
/// This can be used to determine whether the current
|
||||
/// request is still active (and new triggers should be
|
||||
/// ignored) and can also be used to abort the current
|
||||
/// request (by dropping the handle)
|
||||
request: Option<CancelTx>,
|
||||
in_flight: Option<Trigger>,
|
||||
task_controller: TaskController,
|
||||
config: Arc<ArcSwap<Config>>,
|
||||
}
|
||||
|
||||
@ -66,8 +65,9 @@ impl CompletionHandler {
|
||||
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
|
||||
Self {
|
||||
config,
|
||||
request: None,
|
||||
task_controller: TaskController::new(),
|
||||
trigger: None,
|
||||
in_flight: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,6 +80,9 @@ fn handle_event(
|
||||
event: Self::Event,
|
||||
_old_timeout: Option<Instant>,
|
||||
) -> Option<Instant> {
|
||||
if self.in_flight.is_some() && !self.task_controller.is_running() {
|
||||
self.in_flight = None;
|
||||
}
|
||||
match event {
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor: trigger_pos,
|
||||
@ -90,7 +93,7 @@ fn handle_event(
|
||||
// but people may create weird keymaps/use the mouse so lets be extra careful
|
||||
if self
|
||||
.trigger
|
||||
.as_ref()
|
||||
.or(self.in_flight)
|
||||
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
|
||||
{
|
||||
self.trigger = Some(Trigger {
|
||||
@ -103,7 +106,7 @@ fn handle_event(
|
||||
}
|
||||
CompletionEvent::TriggerChar { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.request = None;
|
||||
self.task_controller.cancel();
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
@ -113,7 +116,6 @@ fn handle_event(
|
||||
}
|
||||
CompletionEvent::ManualTrigger { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.request = None;
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
@ -126,21 +128,21 @@ fn handle_event(
|
||||
}
|
||||
CompletionEvent::Cancel => {
|
||||
self.trigger = None;
|
||||
self.request = None;
|
||||
self.task_controller.cancel();
|
||||
}
|
||||
CompletionEvent::DeleteText { cursor } => {
|
||||
// if we deleted the original trigger, abort the completion
|
||||
if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) {
|
||||
if matches!(self.trigger.or(self.in_flight), Some(Trigger{ pos, .. }) if cursor < pos)
|
||||
{
|
||||
self.trigger = None;
|
||||
self.request = None;
|
||||
self.task_controller.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.trigger.map(|trigger| {
|
||||
// if the current request was closed forget about it
|
||||
// otherwise immediately restart the completion request
|
||||
let cancel = self.request.take().map_or(false, |req| !req.is_closed());
|
||||
let timeout = if trigger.kind == TriggerKind::Auto && !cancel {
|
||||
let timeout = if trigger.kind == TriggerKind::Auto {
|
||||
self.config.load().editor.completion_timeout
|
||||
} else {
|
||||
// we want almost instant completions for trigger chars
|
||||
@ -155,17 +157,17 @@ fn handle_event(
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let trigger = self.trigger.take().expect("debounce always has a trigger");
|
||||
let (tx, rx) = cancelation();
|
||||
self.request = Some(tx);
|
||||
self.in_flight = Some(trigger);
|
||||
let handle = self.task_controller.restart();
|
||||
dispatch_blocking(move |editor, compositor| {
|
||||
request_completion(trigger, rx, editor, compositor)
|
||||
request_completion(trigger, handle, editor, compositor)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_completion(
|
||||
mut trigger: Trigger,
|
||||
cancel: CancelRx,
|
||||
handle: TaskHandle,
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
) {
|
||||
@ -251,15 +253,19 @@ fn request_completion(
|
||||
None => Vec::new(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|item| CompletionItem {
|
||||
item,
|
||||
provider: language_server_id,
|
||||
resolved: false,
|
||||
.map(|item| {
|
||||
CompletionItem::Lsp(LspCompletionItem {
|
||||
item,
|
||||
provider: language_server_id,
|
||||
resolved: false,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
anyhow::Ok(items)
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.chain(path_completion(cursor, text.clone(), doc, handle.clone()))
|
||||
.collect();
|
||||
|
||||
let future = async move {
|
||||
@ -280,12 +286,13 @@ fn request_completion(
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
||||
tokio::spawn(async move {
|
||||
let items = cancelable_future(future, cancel).await.unwrap_or_default();
|
||||
if items.is_empty() {
|
||||
let items = cancelable_future(future, &handle).await;
|
||||
let Some(items) = items.filter(|items| !items.is_empty()) else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
dispatch(move |editor, compositor| {
|
||||
show_completion(editor, compositor, items, trigger, savepoint)
|
||||
show_completion(editor, compositor, items, trigger, savepoint);
|
||||
drop(handle)
|
||||
})
|
||||
.await
|
||||
});
|
||||
@ -346,7 +353,17 @@ pub fn trigger_auto_completion(
|
||||
..
|
||||
}) if triggers.iter().any(|trigger| text.ends_with(trigger)))
|
||||
});
|
||||
if is_trigger_char {
|
||||
|
||||
let cursor_char = text
|
||||
.get_bytes_at(text.len_bytes())
|
||||
.and_then(|t| t.reversed().next());
|
||||
|
||||
#[cfg(windows)]
|
||||
let is_path_completion_trigger = matches!(cursor_char, Some(b'/' | b'\\'));
|
||||
#[cfg(not(windows))]
|
||||
let is_path_completion_trigger = matches!(cursor_char, Some(b'/'));
|
||||
|
||||
if is_trigger_char || (is_path_completion_trigger && doc.path_completion_enabled()) {
|
||||
send_blocking(
|
||||
tx,
|
||||
CompletionEvent::TriggerChar {
|
||||
|
41
helix-term/src/handlers/completion/item.rs
Normal file
41
helix-term/src/handlers/completion/item.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use helix_lsp::{lsp, LanguageServerId};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct LspCompletionItem {
|
||||
pub item: lsp::CompletionItem,
|
||||
pub provider: LanguageServerId,
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum CompletionItem {
|
||||
Lsp(LspCompletionItem),
|
||||
Other(helix_core::CompletionItem),
|
||||
}
|
||||
|
||||
impl PartialEq<CompletionItem> for LspCompletionItem {
|
||||
fn eq(&self, other: &CompletionItem) -> bool {
|
||||
match other {
|
||||
CompletionItem::Lsp(other) => self == other,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<CompletionItem> for helix_core::CompletionItem {
|
||||
fn eq(&self, other: &CompletionItem) -> bool {
|
||||
match other {
|
||||
CompletionItem::Other(other) => self == other,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionItem {
|
||||
pub fn preselect(&self) -> bool {
|
||||
match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.preselect.unwrap_or(false),
|
||||
CompletionItem::Other(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
189
helix-term/src/handlers/completion/path.rs
Normal file
189
helix-term/src/handlers/completion/path.rs
Normal file
@ -0,0 +1,189 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr as _,
|
||||
};
|
||||
|
||||
use futures_util::{future::BoxFuture, FutureExt as _};
|
||||
use helix_core as core;
|
||||
use helix_core::Transaction;
|
||||
use helix_event::TaskHandle;
|
||||
use helix_stdx::path::{self, canonicalize, fold_home_dir, get_path_suffix};
|
||||
use helix_view::Document;
|
||||
use url::Url;
|
||||
|
||||
use super::item::CompletionItem;
|
||||
|
||||
pub(crate) fn path_completion(
|
||||
cursor: usize,
|
||||
text: core::Rope,
|
||||
doc: &Document,
|
||||
handle: TaskHandle,
|
||||
) -> Option<BoxFuture<'static, anyhow::Result<Vec<CompletionItem>>>> {
|
||||
if !doc.path_completion_enabled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cur_line = text.char_to_line(cursor);
|
||||
let start = text.line_to_char(cur_line).max(cursor.saturating_sub(1000));
|
||||
let line_until_cursor = text.slice(start..cursor);
|
||||
|
||||
let (dir_path, typed_file_name) =
|
||||
get_path_suffix(line_until_cursor, false).and_then(|matched_path| {
|
||||
let matched_path = Cow::from(matched_path);
|
||||
let path: Cow<_> = if matched_path.starts_with("file://") {
|
||||
Url::from_str(&matched_path)
|
||||
.ok()
|
||||
.and_then(|url| url.to_file_path().ok())?
|
||||
.into()
|
||||
} else {
|
||||
Path::new(&*matched_path).into()
|
||||
};
|
||||
let path = path::expand(&path);
|
||||
let parent_dir = doc.path().and_then(|dp| dp.parent());
|
||||
let path = match parent_dir {
|
||||
Some(parent_dir) if path.is_relative() => parent_dir.join(&path),
|
||||
_ => path.into_owned(),
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let ends_with_slash = matches!(matched_path.as_bytes().last(), Some(b'/' | b'\\'));
|
||||
#[cfg(not(windows))]
|
||||
let ends_with_slash = matches!(matched_path.as_bytes().last(), Some(b'/'));
|
||||
|
||||
if ends_with_slash {
|
||||
Some((PathBuf::from(path.as_path()), None))
|
||||
} else {
|
||||
path.parent().map(|parent_path| {
|
||||
(
|
||||
PathBuf::from(parent_path),
|
||||
path.file_name().and_then(|f| f.to_str().map(String::from)),
|
||||
)
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
if handle.is_canceled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let future = tokio::task::spawn_blocking(move || {
|
||||
let Ok(read_dir) = std::fs::read_dir(&dir_path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
read_dir
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|dir_entry| {
|
||||
dir_entry
|
||||
.metadata()
|
||||
.ok()
|
||||
.and_then(|md| Some((dir_entry.file_name().into_string().ok()?, md)))
|
||||
})
|
||||
.map_while(|(file_name, md)| {
|
||||
if handle.is_canceled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let kind = path_kind(&md);
|
||||
let documentation = path_documentation(&md, &dir_path.join(&file_name), kind);
|
||||
|
||||
let edit_diff = typed_file_name
|
||||
.as_ref()
|
||||
.map(|f| f.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
let transaction = Transaction::change(
|
||||
&text,
|
||||
std::iter::once((cursor - edit_diff, cursor, Some((&file_name).into()))),
|
||||
);
|
||||
|
||||
Some(CompletionItem::Other(core::CompletionItem {
|
||||
kind: Cow::Borrowed(kind),
|
||||
label: file_name.into(),
|
||||
transaction,
|
||||
documentation,
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
Some(async move { Ok(future.await?) }.boxed())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn path_documentation(md: &fs::Metadata, full_path: &Path, kind: &str) -> String {
|
||||
let full_path = fold_home_dir(canonicalize(full_path));
|
||||
let full_path_name = full_path.to_string_lossy();
|
||||
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
let mode = md.permissions().mode();
|
||||
|
||||
let perms = [
|
||||
(libc::S_IRUSR, 'r'),
|
||||
(libc::S_IWUSR, 'w'),
|
||||
(libc::S_IXUSR, 'x'),
|
||||
(libc::S_IRGRP, 'r'),
|
||||
(libc::S_IWGRP, 'w'),
|
||||
(libc::S_IXGRP, 'x'),
|
||||
(libc::S_IROTH, 'r'),
|
||||
(libc::S_IWOTH, 'w'),
|
||||
(libc::S_IXOTH, 'x'),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(String::with_capacity(9), |mut acc, (p, s)| {
|
||||
// This cast is necessary on some platforms such as macos as `mode_t` is u16 there
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
acc.push(if mode & (p as u32) > 0 { s } else { '-' });
|
||||
acc
|
||||
});
|
||||
|
||||
// TODO it would be great to be able to individually color the documentation,
|
||||
// but this will likely require a custom doc implementation (i.e. not `lsp::Documentation`)
|
||||
// and/or different rendering in completion.rs
|
||||
format!(
|
||||
"type: `{kind}`\n\
|
||||
permissions: `[{perms}]`\n\
|
||||
full path: `{full_path_name}`",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn path_documentation(md: &fs::Metadata, full_path: &Path, kind: &str) -> String {
|
||||
let full_path = fold_home_dir(canonicalize(full_path));
|
||||
let full_path_name = full_path.to_string_lossy();
|
||||
format!("type: `{kind}`\nfull path: `{full_path_name}`",)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn path_kind(md: &fs::Metadata) -> &'static str {
|
||||
if md.is_symlink() {
|
||||
"link"
|
||||
} else if md.is_dir() {
|
||||
"folder"
|
||||
} else {
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
if md.file_type().is_block_device() {
|
||||
"block"
|
||||
} else if md.file_type().is_socket() {
|
||||
"socket"
|
||||
} else if md.file_type().is_char_device() {
|
||||
"char_device"
|
||||
} else if md.file_type().is_fifo() {
|
||||
"fifo"
|
||||
} else {
|
||||
"file"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn path_kind(md: &fs::Metadata) -> &'static str {
|
||||
if md.is_symlink() {
|
||||
"link"
|
||||
} else if md.is_dir() {
|
||||
"folder"
|
||||
} else {
|
||||
"file"
|
||||
}
|
||||
}
|
@ -4,9 +4,10 @@
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::{Duration, Instant};
|
||||
|
||||
use helix_event::{send_blocking, AsyncHook, CancelRx};
|
||||
use helix_event::{send_blocking, AsyncHook, TaskController, TaskHandle};
|
||||
use helix_view::Editor;
|
||||
|
||||
use super::LspCompletionItem;
|
||||
use crate::handlers::completion::CompletionItem;
|
||||
use crate::job;
|
||||
|
||||
@ -22,7 +23,7 @@
|
||||
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
||||
/// > The returned completion item should have the documentation property filled in.
|
||||
pub struct ResolveHandler {
|
||||
last_request: Option<Arc<CompletionItem>>,
|
||||
last_request: Option<Arc<LspCompletionItem>>,
|
||||
resolver: Sender<ResolveRequest>,
|
||||
}
|
||||
|
||||
@ -30,15 +31,11 @@ impl ResolveHandler {
|
||||
pub fn new() -> ResolveHandler {
|
||||
ResolveHandler {
|
||||
last_request: None,
|
||||
resolver: ResolveTimeout {
|
||||
next_request: None,
|
||||
in_flight: None,
|
||||
}
|
||||
.spawn(),
|
||||
resolver: ResolveTimeout::default().spawn(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut CompletionItem) {
|
||||
pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut LspCompletionItem) {
|
||||
if item.resolved {
|
||||
return;
|
||||
}
|
||||
@ -93,14 +90,15 @@ pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut Completio
|
||||
}
|
||||
|
||||
struct ResolveRequest {
|
||||
item: Arc<CompletionItem>,
|
||||
item: Arc<LspCompletionItem>,
|
||||
ls: Arc<helix_lsp::Client>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ResolveTimeout {
|
||||
next_request: Option<ResolveRequest>,
|
||||
in_flight: Option<(helix_event::CancelTx, Arc<CompletionItem>)>,
|
||||
in_flight: Option<Arc<LspCompletionItem>>,
|
||||
task_controller: TaskController,
|
||||
}
|
||||
|
||||
impl AsyncHook for ResolveTimeout {
|
||||
@ -120,7 +118,7 @@ fn handle_event(
|
||||
} else if self
|
||||
.in_flight
|
||||
.as_ref()
|
||||
.is_some_and(|(_, old_request)| old_request.item == request.item.item)
|
||||
.is_some_and(|old_request| old_request.item == request.item.item)
|
||||
{
|
||||
self.next_request = None;
|
||||
None
|
||||
@ -134,14 +132,14 @@ fn finish_debounce(&mut self) {
|
||||
let Some(request) = self.next_request.take() else {
|
||||
return;
|
||||
};
|
||||
let (tx, rx) = helix_event::cancelation();
|
||||
self.in_flight = Some((tx, request.item.clone()));
|
||||
tokio::spawn(request.execute(rx));
|
||||
let token = self.task_controller.restart();
|
||||
self.in_flight = Some(request.item.clone());
|
||||
tokio::spawn(request.execute(token));
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveRequest {
|
||||
async fn execute(self, cancel: CancelRx) {
|
||||
async fn execute(self, cancel: TaskHandle) {
|
||||
let future = self.ls.resolve_completion_item(&self.item.item);
|
||||
let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else {
|
||||
return;
|
||||
@ -152,8 +150,8 @@ async fn execute(self, cancel: CancelRx) {
|
||||
.unwrap()
|
||||
.completion
|
||||
{
|
||||
let resolved_item = match resolved_item {
|
||||
Ok(item) => CompletionItem {
|
||||
let resolved_item = CompletionItem::Lsp(match resolved_item {
|
||||
Ok(item) => LspCompletionItem {
|
||||
item,
|
||||
resolved: true,
|
||||
..*self.item
|
||||
@ -166,8 +164,8 @@ async fn execute(self, cancel: CancelRx) {
|
||||
item.resolved = true;
|
||||
item
|
||||
}
|
||||
};
|
||||
completion.replace_item(&self.item, resolved_item);
|
||||
});
|
||||
completion.replace_item(&*self.item, resolved_item);
|
||||
};
|
||||
})
|
||||
.await
|
||||
|
@ -2,9 +2,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_event::{
|
||||
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||
};
|
||||
use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
|
||||
use helix_lsp::lsp::{self, SignatureInformation};
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::Mode;
|
||||
@ -22,11 +20,11 @@
|
||||
use crate::ui::Popup;
|
||||
use crate::{job, ui};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum State {
|
||||
Open,
|
||||
Closed,
|
||||
Pending { request: CancelTx },
|
||||
Pending,
|
||||
}
|
||||
|
||||
/// debounce timeout in ms, value taken from VSCode
|
||||
@ -37,6 +35,7 @@ enum State {
|
||||
pub(super) struct SignatureHelpHandler {
|
||||
trigger: Option<SignatureHelpInvoked>,
|
||||
state: State,
|
||||
task_controller: TaskController,
|
||||
}
|
||||
|
||||
impl SignatureHelpHandler {
|
||||
@ -44,6 +43,7 @@ pub fn new() -> SignatureHelpHandler {
|
||||
SignatureHelpHandler {
|
||||
trigger: None,
|
||||
state: State::Closed,
|
||||
task_controller: TaskController::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,12 +76,11 @@ fn handle_event(
|
||||
}
|
||||
SignatureHelpEvent::RequestComplete { open } => {
|
||||
// don't cancel rerequest that was already triggered
|
||||
if let State::Pending { request } = &self.state {
|
||||
if !request.is_closed() {
|
||||
return timeout;
|
||||
}
|
||||
if self.state == State::Pending && self.task_controller.is_running() {
|
||||
return timeout;
|
||||
}
|
||||
self.state = if open { State::Open } else { State::Closed };
|
||||
self.task_controller.cancel();
|
||||
|
||||
return timeout;
|
||||
}
|
||||
@ -94,16 +93,16 @@ fn handle_event(
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let invocation = self.trigger.take().unwrap();
|
||||
let (tx, rx) = cancelation();
|
||||
self.state = State::Pending { request: tx };
|
||||
job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, rx))
|
||||
self.state = State::Pending;
|
||||
let handle = self.task_controller.restart();
|
||||
job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, handle))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_signature_help(
|
||||
editor: &mut Editor,
|
||||
invoked: SignatureHelpInvoked,
|
||||
cancel: CancelRx,
|
||||
cancel: TaskHandle,
|
||||
) {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
use crate::config::{Config, ConfigLoadError};
|
||||
use crossterm::{
|
||||
style::{Color, Print, Stylize},
|
||||
tty::IsTty,
|
||||
};
|
||||
use helix_core::config::{default_lang_config, user_lang_config};
|
||||
use helix_loader::grammar::load_runtime_file;
|
||||
use helix_view::clipboard::get_clipboard_provider;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@ -53,7 +53,6 @@ pub fn general() -> std::io::Result<()> {
|
||||
let lang_file = helix_loader::lang_config_file();
|
||||
let log_file = helix_loader::log_file();
|
||||
let rt_dirs = helix_loader::runtime_dirs();
|
||||
let clipboard_provider = get_clipboard_provider();
|
||||
|
||||
if config_file.exists() {
|
||||
writeln!(stdout, "Config file: {}", config_file.display())?;
|
||||
@ -92,7 +91,6 @@ pub fn general() -> std::io::Result<()> {
|
||||
writeln!(stdout, "{}", msg.yellow())?;
|
||||
}
|
||||
}
|
||||
writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -101,8 +99,19 @@ pub fn clipboard() -> std::io::Result<()> {
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
let board = get_clipboard_provider();
|
||||
match board.name().as_ref() {
|
||||
let config = match Config::load_default() {
|
||||
Ok(config) => config,
|
||||
Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
Config::default()
|
||||
}
|
||||
Err(err) => {
|
||||
writeln!(stdout, "{}", "Configuration file malformed".red())?;
|
||||
writeln!(stdout, "{}", err)?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
match config.editor.clipboard_provider.name().as_ref() {
|
||||
"none" => {
|
||||
writeln!(
|
||||
stdout,
|
||||
|
@ -1,6 +1,9 @@
|
||||
use crate::{
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
handlers::{completion::ResolveHandler, trigger_auto_completion},
|
||||
handlers::{
|
||||
completion::{CompletionItem, LspCompletionItem, ResolveHandler},
|
||||
trigger_auto_completion,
|
||||
},
|
||||
};
|
||||
use helix_view::{
|
||||
document::SavePoint,
|
||||
@ -13,12 +16,12 @@
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use helix_core::{chars, Change, Transaction};
|
||||
use helix_core::{self as core, chars, Change, Transaction};
|
||||
use helix_view::{graphics::Rect, Document, Editor};
|
||||
|
||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
|
||||
use helix_lsp::{lsp, util, LanguageServerId, OffsetEncoding};
|
||||
use helix_lsp::{lsp, util, OffsetEncoding};
|
||||
|
||||
impl menu::Item for CompletionItem {
|
||||
type Data = ();
|
||||
@ -28,30 +31,35 @@ fn sort_text(&self, data: &Self::Data) -> Cow<str> {
|
||||
|
||||
#[inline]
|
||||
fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
|
||||
self.item
|
||||
.filter_text
|
||||
.as_ref()
|
||||
.unwrap_or(&self.item.label)
|
||||
.as_str()
|
||||
.into()
|
||||
match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item
|
||||
.filter_text
|
||||
.as_ref()
|
||||
.unwrap_or(&item.label)
|
||||
.as_str()
|
||||
.into(),
|
||||
CompletionItem::Other(core::CompletionItem { label, .. }) => label.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format(&self, _data: &Self::Data) -> menu::Row {
|
||||
let deprecated = self.item.deprecated.unwrap_or_default()
|
||||
|| self.item.tags.as_ref().map_or(false, |tags| {
|
||||
tags.contains(&lsp::CompletionItemTag::DEPRECATED)
|
||||
});
|
||||
let deprecated = match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => {
|
||||
item.deprecated.unwrap_or_default()
|
||||
|| item.tags.as_ref().map_or(false, |tags| {
|
||||
tags.contains(&lsp::CompletionItemTag::DEPRECATED)
|
||||
})
|
||||
}
|
||||
CompletionItem::Other(_) => false,
|
||||
};
|
||||
|
||||
menu::Row::new(vec![
|
||||
menu::Cell::from(Span::styled(
|
||||
self.item.label.as_str(),
|
||||
if deprecated {
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT)
|
||||
} else {
|
||||
Style::default()
|
||||
},
|
||||
)),
|
||||
menu::Cell::from(match self.item.kind {
|
||||
let label = match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.label.as_str(),
|
||||
CompletionItem::Other(core::CompletionItem { label, .. }) => label,
|
||||
};
|
||||
|
||||
let kind = match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => match item.kind {
|
||||
Some(lsp::CompletionItemKind::TEXT) => "text",
|
||||
Some(lsp::CompletionItemKind::METHOD) => "method",
|
||||
Some(lsp::CompletionItemKind::FUNCTION) => "function",
|
||||
@ -82,18 +90,24 @@ fn format(&self, _data: &Self::Data) -> menu::Row {
|
||||
""
|
||||
}
|
||||
None => "",
|
||||
}),
|
||||
},
|
||||
CompletionItem::Other(core::CompletionItem { kind, .. }) => kind,
|
||||
};
|
||||
|
||||
menu::Row::new([
|
||||
menu::Cell::from(Span::styled(
|
||||
label,
|
||||
if deprecated {
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT)
|
||||
} else {
|
||||
Style::default()
|
||||
},
|
||||
)),
|
||||
menu::Cell::from(kind),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default, Clone)]
|
||||
pub struct CompletionItem {
|
||||
pub item: lsp::CompletionItem,
|
||||
pub provider: LanguageServerId,
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
/// Wraps a Menu.
|
||||
pub struct Completion {
|
||||
popup: Popup<Menu<CompletionItem>>,
|
||||
@ -115,11 +129,11 @@ pub fn new(
|
||||
let preview_completion_insert = editor.config().preview_completion_insert;
|
||||
let replace_mode = editor.config().completion_replace;
|
||||
// Sort completion items according to their preselect status (given by the LSP server)
|
||||
items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
|
||||
items.sort_by_key(|item| !item.preselect());
|
||||
|
||||
// Then create the menu
|
||||
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
|
||||
fn item_to_transaction(
|
||||
fn lsp_item_to_transaction(
|
||||
doc: &Document,
|
||||
view_id: ViewId,
|
||||
item: &lsp::CompletionItem,
|
||||
@ -257,16 +271,23 @@ macro_rules! language_server {
|
||||
// always present here
|
||||
let item = item.unwrap();
|
||||
|
||||
let transaction = item_to_transaction(
|
||||
doc,
|
||||
view.id,
|
||||
&item.item,
|
||||
language_server!(item).offset_encoding(),
|
||||
trigger_offset,
|
||||
true,
|
||||
replace_mode,
|
||||
);
|
||||
doc.apply_temporary(&transaction, view.id);
|
||||
match item {
|
||||
CompletionItem::Lsp(item) => doc.apply_temporary(
|
||||
&lsp_item_to_transaction(
|
||||
doc,
|
||||
view.id,
|
||||
&item.item,
|
||||
language_server!(item).offset_encoding(),
|
||||
trigger_offset,
|
||||
true,
|
||||
replace_mode,
|
||||
),
|
||||
view.id,
|
||||
),
|
||||
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
|
||||
doc.apply_temporary(transaction, view.id)
|
||||
}
|
||||
};
|
||||
}
|
||||
PromptEvent::Update => {}
|
||||
PromptEvent::Validate => {
|
||||
@ -275,32 +296,46 @@ macro_rules! language_server {
|
||||
{
|
||||
doc.restore(view, &savepoint, false);
|
||||
}
|
||||
// always present here
|
||||
let mut item = item.unwrap().clone();
|
||||
|
||||
let language_server = language_server!(item);
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
|
||||
if !item.resolved {
|
||||
if let Some(resolved) =
|
||||
Self::resolve_completion_item(language_server, item.item.clone())
|
||||
{
|
||||
item.item = resolved;
|
||||
}
|
||||
};
|
||||
// if more text was entered, remove it
|
||||
doc.restore(view, &savepoint, true);
|
||||
// save an undo checkpoint before the completion
|
||||
doc.append_changes_to_history(view);
|
||||
let transaction = item_to_transaction(
|
||||
doc,
|
||||
view.id,
|
||||
&item.item,
|
||||
offset_encoding,
|
||||
trigger_offset,
|
||||
false,
|
||||
replace_mode,
|
||||
);
|
||||
|
||||
// item always present here
|
||||
let (transaction, additional_edits) = match item.unwrap().clone() {
|
||||
CompletionItem::Lsp(mut item) => {
|
||||
let language_server = language_server!(item);
|
||||
|
||||
// resolve item if not yet resolved
|
||||
if !item.resolved {
|
||||
if let Some(resolved_item) = Self::resolve_completion_item(
|
||||
language_server,
|
||||
item.item.clone(),
|
||||
) {
|
||||
item.item = resolved_item;
|
||||
}
|
||||
};
|
||||
|
||||
let encoding = language_server.offset_encoding();
|
||||
let transaction = lsp_item_to_transaction(
|
||||
doc,
|
||||
view.id,
|
||||
&item.item,
|
||||
encoding,
|
||||
trigger_offset,
|
||||
false,
|
||||
replace_mode,
|
||||
);
|
||||
let add_edits = item.item.additional_text_edits;
|
||||
|
||||
(transaction, add_edits.map(|edits| (edits, encoding)))
|
||||
}
|
||||
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
|
||||
(transaction, None)
|
||||
}
|
||||
};
|
||||
|
||||
doc.apply(&transaction, view.id);
|
||||
|
||||
editor.last_completion = Some(CompleteAction::Applied {
|
||||
@ -309,7 +344,7 @@ macro_rules! language_server {
|
||||
});
|
||||
|
||||
// TODO: add additional _edits to completion_changes?
|
||||
if let Some(additional_edits) = item.item.additional_text_edits {
|
||||
if let Some((additional_edits, offset_encoding)) = additional_edits {
|
||||
if !additional_edits.is_empty() {
|
||||
let transaction = util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
@ -414,7 +449,11 @@ pub fn is_empty(&self) -> bool {
|
||||
self.popup.contents().is_empty()
|
||||
}
|
||||
|
||||
pub fn replace_item(&mut self, old_item: &CompletionItem, new_item: CompletionItem) {
|
||||
pub fn replace_item(
|
||||
&mut self,
|
||||
old_item: &impl PartialEq<CompletionItem>,
|
||||
new_item: CompletionItem,
|
||||
) {
|
||||
self.popup.contents_mut().replace_option(old_item, new_item);
|
||||
}
|
||||
|
||||
@ -440,7 +479,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
Some(option) => option,
|
||||
None => return,
|
||||
};
|
||||
if !option.resolved {
|
||||
if let CompletionItem::Lsp(option) = option {
|
||||
self.resolve_handler.ensure_item_resolved(cx.editor, option);
|
||||
}
|
||||
// need to render:
|
||||
@ -465,27 +504,32 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
Markdown::new(md, cx.editor.syn_loader.clone())
|
||||
};
|
||||
|
||||
let mut markdown_doc = match &option.item.documentation {
|
||||
Some(lsp::Documentation::String(contents))
|
||||
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::PlainText,
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: convert to wrapped text
|
||||
markdowned(language, option.item.detail.as_deref(), Some(contents))
|
||||
let mut markdown_doc = match option {
|
||||
CompletionItem::Lsp(option) => match &option.item.documentation {
|
||||
Some(lsp::Documentation::String(contents))
|
||||
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::PlainText,
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: convert to wrapped text
|
||||
markdowned(language, option.item.detail.as_deref(), Some(contents))
|
||||
}
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: set language based on doc scope
|
||||
markdowned(language, option.item.detail.as_deref(), Some(contents))
|
||||
}
|
||||
None if option.item.detail.is_some() => {
|
||||
// TODO: set language based on doc scope
|
||||
markdowned(language, option.item.detail.as_deref(), None)
|
||||
}
|
||||
None => return,
|
||||
},
|
||||
CompletionItem::Other(option) => {
|
||||
markdowned(language, None, Some(&option.documentation))
|
||||
}
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: set language based on doc scope
|
||||
markdowned(language, option.item.detail.as_deref(), Some(contents))
|
||||
}
|
||||
None if option.item.detail.is_some() => {
|
||||
// TODO: set language based on doc scope
|
||||
markdowned(language, option.item.detail.as_deref(), None)
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
let popup_area = self.popup.area(area, cx.editor);
|
||||
|
@ -2,13 +2,14 @@
|
||||
commands::{self, OnKeyCallback},
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
events::{OnModeSwitch, PostCommand},
|
||||
handlers::completion::CompletionItem,
|
||||
key,
|
||||
keymap::{KeymapResult, Keymaps},
|
||||
ui::{
|
||||
document::{render_document, LinePos, TextRenderer},
|
||||
statusline,
|
||||
text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics},
|
||||
Completion, CompletionItem, ProgressSpinners,
|
||||
Completion, ProgressSpinners,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -228,7 +228,7 @@ pub fn len(&self) -> usize {
|
||||
}
|
||||
|
||||
impl<T: Item + PartialEq> Menu<T> {
|
||||
pub fn replace_option(&mut self, old_option: &T, new_option: T) {
|
||||
pub fn replace_option(&mut self, old_option: &impl PartialEq<T>, new_option: T) {
|
||||
for option in &mut self.options {
|
||||
if old_option == option {
|
||||
*option = new_option;
|
||||
|
@ -17,7 +17,7 @@
|
||||
use crate::compositor::Compositor;
|
||||
use crate::filter_picker_entry;
|
||||
use crate::job::{self, Callback};
|
||||
pub use completion::{Completion, CompletionItem};
|
||||
pub use completion::Completion;
|
||||
pub use editor::EditorView;
|
||||
use helix_stdx::rope;
|
||||
pub use markdown::Markdown;
|
||||
|
@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
|
||||
parking_lot = "0.12"
|
||||
arc-swap = { version = "1.7.1" }
|
||||
|
||||
gix = { version = "0.66.0", features = ["attributes", "status"], default-features = false, optional = true }
|
||||
gix = { version = "0.67.0", features = ["attributes", "status"], default-features = false, optional = true }
|
||||
imara-diff = "0.1.7"
|
||||
anyhow = "1"
|
||||
|
||||
@ -29,4 +29,4 @@ log = "0.4"
|
||||
git = ["gix"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.13"
|
||||
tempfile = "3.14"
|
||||
|
@ -198,7 +198,7 @@ fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Resul
|
||||
let rel_path = file.strip_prefix(repo_dir)?;
|
||||
let tree = commit.tree()?;
|
||||
let tree_entry = tree
|
||||
.lookup_entry_by_path(rel_path, &mut Vec::new())?
|
||||
.lookup_entry_by_path(rel_path)?
|
||||
.context("file is untracked")?;
|
||||
match tree_entry.mode().kind() {
|
||||
// not a file, everything is new, do not show diff
|
||||
|
@ -28,11 +28,11 @@ bitflags = "2.6"
|
||||
anyhow = "1"
|
||||
crossterm = { version = "0.28", optional = true }
|
||||
|
||||
tempfile = "3.13"
|
||||
tempfile = "3.14"
|
||||
|
||||
# Conversion traits
|
||||
once_cell = "1.20"
|
||||
url = "2.5.2"
|
||||
url = "2.5.3"
|
||||
|
||||
arc-swap = { version = "1.7.1" }
|
||||
|
||||
|
@ -1,164 +1,408 @@
|
||||
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ClipboardType {
|
||||
Clipboard,
|
||||
Selection,
|
||||
}
|
||||
|
||||
pub trait ClipboardProvider: std::fmt::Debug {
|
||||
fn name(&self) -> Cow<str>;
|
||||
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String>;
|
||||
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ClipboardError {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("could not convert terminal output to UTF-8: {0}")]
|
||||
FromUtf8Error(#[from] std::string::FromUtf8Error),
|
||||
#[cfg(windows)]
|
||||
#[error("Windows API error: {0}")]
|
||||
WinAPI(#[from] clipboard_win::ErrorCode),
|
||||
#[error("clipboard provider command failed")]
|
||||
CommandFailed,
|
||||
#[error("failed to write to clipboard provider's stdin")]
|
||||
StdinWriteFailed,
|
||||
#[error("clipboard provider did not return any contents")]
|
||||
MissingStdout,
|
||||
#[error("This clipboard provider does not support reading")]
|
||||
ReadingNotSupported,
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
macro_rules! command_provider {
|
||||
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
|
||||
log::debug!(
|
||||
"Using {} to interact with the system clipboard",
|
||||
if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() }
|
||||
);
|
||||
Box::new(provider::command::Provider {
|
||||
get_cmd: provider::command::Config {
|
||||
prg: $get_prg,
|
||||
args: &[ $( $get_arg ),* ],
|
||||
},
|
||||
set_cmd: provider::command::Config {
|
||||
prg: $set_prg,
|
||||
args: &[ $( $set_arg ),* ],
|
||||
},
|
||||
get_primary_cmd: None,
|
||||
set_primary_cmd: None,
|
||||
})
|
||||
}};
|
||||
|
||||
(paste => $get_prg:literal $( , $get_arg:literal )* ;
|
||||
copy => $set_prg:literal $( , $set_arg:literal )* ;
|
||||
primary_paste => $pr_get_prg:literal $( , $pr_get_arg:literal )* ;
|
||||
primary_copy => $pr_set_prg:literal $( , $pr_set_arg:literal )* ;
|
||||
) => {{
|
||||
log::debug!(
|
||||
"Using {} to interact with the system and selection (primary) clipboard",
|
||||
if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() }
|
||||
);
|
||||
Box::new(provider::command::Provider {
|
||||
get_cmd: provider::command::Config {
|
||||
prg: $get_prg,
|
||||
args: &[ $( $get_arg ),* ],
|
||||
},
|
||||
set_cmd: provider::command::Config {
|
||||
prg: $set_prg,
|
||||
args: &[ $( $set_arg ),* ],
|
||||
},
|
||||
get_primary_cmd: Some(provider::command::Config {
|
||||
prg: $pr_get_prg,
|
||||
args: &[ $( $pr_get_arg ),* ],
|
||||
}),
|
||||
set_primary_cmd: Some(provider::command::Config {
|
||||
prg: $pr_set_prg,
|
||||
args: &[ $( $pr_set_arg ),* ],
|
||||
}),
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
Box::<provider::WindowsProvider>::default()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
use helix_stdx::env::{binary_exists, env_var_is_set};
|
||||
|
||||
if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
command_provider! {
|
||||
paste => "tmux", "save-buffer", "-";
|
||||
copy => "tmux", "load-buffer", "-w", "-";
|
||||
}
|
||||
} else if binary_exists("pbcopy") && binary_exists("pbpaste") {
|
||||
command_provider! {
|
||||
paste => "pbpaste";
|
||||
copy => "pbcopy";
|
||||
}
|
||||
} else {
|
||||
Box::new(provider::FallbackProvider::new())
|
||||
}
|
||||
}
|
||||
type Result<T> = std::result::Result<T, ClipboardError>;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use external::ClipboardProvider;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
// TODO:
|
||||
Box::new(provider::FallbackProvider::new())
|
||||
}
|
||||
pub use noop::ClipboardProvider;
|
||||
|
||||
#[cfg(not(any(windows, target_arch = "wasm32", target_os = "macos")))]
|
||||
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
use helix_stdx::env::{binary_exists, env_var_is_set};
|
||||
use provider::command::is_exit_success;
|
||||
// TODO: support for user-defined provider, probably when we have plugin support by setting a
|
||||
// variable?
|
||||
// Clipboard not supported for wasm
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod noop {
|
||||
use super::*;
|
||||
|
||||
if env_var_is_set("WAYLAND_DISPLAY") && binary_exists("wl-copy") && binary_exists("wl-paste") {
|
||||
command_provider! {
|
||||
paste => "wl-paste", "--no-newline";
|
||||
copy => "wl-copy", "--type", "text/plain";
|
||||
primary_paste => "wl-paste", "-p", "--no-newline";
|
||||
primary_copy => "wl-copy", "-p", "--type", "text/plain";
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClipboardProvider {}
|
||||
|
||||
impl ClipboardProvider {
|
||||
pub fn detect() -> Self {
|
||||
Self
|
||||
}
|
||||
} else if env_var_is_set("DISPLAY") && binary_exists("xclip") {
|
||||
command_provider! {
|
||||
paste => "xclip", "-o", "-selection", "clipboard";
|
||||
copy => "xclip", "-i", "-selection", "clipboard";
|
||||
primary_paste => "xclip", "-o";
|
||||
primary_copy => "xclip", "-i";
|
||||
|
||||
pub fn name(&self) -> Cow<str> {
|
||||
"none".into()
|
||||
}
|
||||
} else if env_var_is_set("DISPLAY")
|
||||
&& binary_exists("xsel")
|
||||
&& is_exit_success("xsel", &["-o", "-b"])
|
||||
{
|
||||
// FIXME: check performance of is_exit_success
|
||||
command_provider! {
|
||||
paste => "xsel", "-o", "-b";
|
||||
copy => "xsel", "-i", "-b";
|
||||
primary_paste => "xsel", "-o";
|
||||
primary_copy => "xsel", "-i";
|
||||
|
||||
pub fn get_contents(&self, _clipboard_type: ClipboardType) -> Result<String> {
|
||||
Err(ClipboardError::ReadingNotSupported)
|
||||
}
|
||||
} else if binary_exists("win32yank.exe") {
|
||||
command_provider! {
|
||||
paste => "win32yank.exe", "-o", "--lf";
|
||||
copy => "win32yank.exe", "-i", "--crlf";
|
||||
|
||||
pub fn set_contents(&self, _content: &str, _clipboard_type: ClipboardType) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
} else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get") {
|
||||
command_provider! {
|
||||
paste => "termux-clipboard-get";
|
||||
copy => "termux-clipboard-set";
|
||||
}
|
||||
} else if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
command_provider! {
|
||||
paste => "tmux", "save-buffer", "-";
|
||||
copy => "tmux", "load-buffer", "-w", "-";
|
||||
}
|
||||
} else {
|
||||
Box::new(provider::FallbackProvider::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub mod provider {
|
||||
use super::{ClipboardProvider, ClipboardType};
|
||||
use anyhow::Result;
|
||||
use std::borrow::Cow;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod external {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Command {
|
||||
command: Cow<'static, str>,
|
||||
#[serde(default)]
|
||||
args: Cow<'static, [Cow<'static, str>]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct CommandProvider {
|
||||
yank: Command,
|
||||
paste: Command,
|
||||
yank_primary: Option<Command>,
|
||||
paste_primary: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ClipboardProvider {
|
||||
Pasteboard,
|
||||
Wayland,
|
||||
XClip,
|
||||
XSel,
|
||||
Win32Yank,
|
||||
Tmux,
|
||||
#[cfg(windows)]
|
||||
Windows,
|
||||
Termux,
|
||||
#[cfg(feature = "term")]
|
||||
Termcode,
|
||||
Custom(CommandProvider),
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for ClipboardProvider {
|
||||
#[cfg(windows)]
|
||||
fn default() -> Self {
|
||||
use helix_stdx::env::binary_exists;
|
||||
|
||||
if binary_exists("win32yank.exe") {
|
||||
Self::Win32Yank
|
||||
} else {
|
||||
Self::Windows
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn default() -> Self {
|
||||
use helix_stdx::env::{binary_exists, env_var_is_set};
|
||||
|
||||
if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
Self::Tmux
|
||||
} else if binary_exists("pbcopy") && binary_exists("pbpaste") {
|
||||
Self::Pasteboard
|
||||
} else if cfg!(feature = "term") {
|
||||
Self::Termcode
|
||||
} else {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "macos")))]
|
||||
fn default() -> Self {
|
||||
use helix_stdx::env::{binary_exists, env_var_is_set};
|
||||
|
||||
fn is_exit_success(program: &str, args: &[&str]) -> bool {
|
||||
std::process::Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|out| out.status.success().then_some(()))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
if env_var_is_set("WAYLAND_DISPLAY")
|
||||
&& binary_exists("wl-copy")
|
||||
&& binary_exists("wl-paste")
|
||||
{
|
||||
Self::Wayland
|
||||
} else if env_var_is_set("DISPLAY") && binary_exists("xclip") {
|
||||
Self::XClip
|
||||
} else if env_var_is_set("DISPLAY")
|
||||
&& binary_exists("xsel")
|
||||
// FIXME: check performance of is_exit_success
|
||||
&& is_exit_success("xsel", &["-o", "-b"])
|
||||
{
|
||||
Self::XSel
|
||||
} else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get")
|
||||
{
|
||||
Self::Termux
|
||||
} else if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
Self::Tmux
|
||||
} else if binary_exists("win32yank.exe") {
|
||||
Self::Win32Yank
|
||||
} else if cfg!(feature = "term") {
|
||||
Self::Termcode
|
||||
} else {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipboardProvider {
|
||||
pub fn name(&self) -> Cow<'_, str> {
|
||||
fn builtin_name<'a>(
|
||||
name: &'static str,
|
||||
provider: &'static CommandProvider,
|
||||
) -> Cow<'a, str> {
|
||||
if provider.yank.command != provider.paste.command {
|
||||
Cow::Owned(format!(
|
||||
"{} ({}+{})",
|
||||
name, provider.yank.command, provider.paste.command
|
||||
))
|
||||
} else {
|
||||
Cow::Owned(format!("{} ({})", name, provider.yank.command))
|
||||
}
|
||||
}
|
||||
|
||||
match self {
|
||||
// These names should match the config option names from Serde
|
||||
Self::Pasteboard => builtin_name("pasteboard", &PASTEBOARD),
|
||||
Self::Wayland => builtin_name("wayland", &WL_CLIPBOARD),
|
||||
Self::XClip => builtin_name("x-clip", &XCLIP),
|
||||
Self::XSel => builtin_name("x-sel", &XSEL),
|
||||
Self::Win32Yank => builtin_name("win-32-yank", &WIN32),
|
||||
Self::Tmux => builtin_name("tmux", &TMUX),
|
||||
Self::Termux => builtin_name("termux", &TERMUX),
|
||||
#[cfg(windows)]
|
||||
Self::Windows => "windows".into(),
|
||||
#[cfg(feature = "term")]
|
||||
Self::Termcode => "termcode".into(),
|
||||
Self::Custom(command_provider) => Cow::Owned(format!(
|
||||
"custom ({}+{})",
|
||||
command_provider.yank.command, command_provider.paste.command
|
||||
)),
|
||||
Self::None => "none".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_contents(&self, clipboard_type: &ClipboardType) -> Result<String> {
|
||||
fn yank_from_builtin(
|
||||
provider: CommandProvider,
|
||||
clipboard_type: &ClipboardType,
|
||||
) -> Result<String> {
|
||||
match clipboard_type {
|
||||
ClipboardType::Clipboard => execute_command(&provider.yank, None, true)?
|
||||
.ok_or(ClipboardError::MissingStdout),
|
||||
ClipboardType::Selection => {
|
||||
if let Some(cmd) = provider.yank_primary.as_ref() {
|
||||
return execute_command(cmd, None, true)?
|
||||
.ok_or(ClipboardError::MissingStdout);
|
||||
}
|
||||
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Pasteboard => yank_from_builtin(PASTEBOARD, clipboard_type),
|
||||
Self::Wayland => yank_from_builtin(WL_CLIPBOARD, clipboard_type),
|
||||
Self::XClip => yank_from_builtin(XCLIP, clipboard_type),
|
||||
Self::XSel => yank_from_builtin(XSEL, clipboard_type),
|
||||
Self::Win32Yank => yank_from_builtin(WIN32, clipboard_type),
|
||||
Self::Tmux => yank_from_builtin(TMUX, clipboard_type),
|
||||
Self::Termux => yank_from_builtin(TERMUX, clipboard_type),
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::Windows => match clipboard_type {
|
||||
ClipboardType::Clipboard => {
|
||||
let contents =
|
||||
clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
|
||||
Ok(contents)
|
||||
}
|
||||
ClipboardType::Selection => Ok(String::new()),
|
||||
},
|
||||
#[cfg(feature = "term")]
|
||||
Self::Termcode => Err(ClipboardError::ReadingNotSupported),
|
||||
Self::Custom(command_provider) => {
|
||||
execute_command(&command_provider.yank, None, true)?
|
||||
.ok_or(ClipboardError::MissingStdout)
|
||||
}
|
||||
Self::None => Err(ClipboardError::ReadingNotSupported),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_contents(&self, content: &str, clipboard_type: ClipboardType) -> Result<()> {
|
||||
fn paste_to_builtin(
|
||||
provider: CommandProvider,
|
||||
content: &str,
|
||||
clipboard_type: ClipboardType,
|
||||
) -> Result<()> {
|
||||
let cmd = match clipboard_type {
|
||||
ClipboardType::Clipboard => &provider.paste,
|
||||
ClipboardType::Selection => {
|
||||
if let Some(cmd) = provider.paste_primary.as_ref() {
|
||||
cmd
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
execute_command(cmd, Some(content), false).map(|_| ())
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Pasteboard => paste_to_builtin(PASTEBOARD, content, clipboard_type),
|
||||
Self::Wayland => paste_to_builtin(WL_CLIPBOARD, content, clipboard_type),
|
||||
Self::XClip => paste_to_builtin(XCLIP, content, clipboard_type),
|
||||
Self::XSel => paste_to_builtin(XSEL, content, clipboard_type),
|
||||
Self::Win32Yank => paste_to_builtin(WIN32, content, clipboard_type),
|
||||
Self::Tmux => paste_to_builtin(TMUX, content, clipboard_type),
|
||||
Self::Termux => paste_to_builtin(TERMUX, content, clipboard_type),
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::Windows => match clipboard_type {
|
||||
ClipboardType::Clipboard => {
|
||||
clipboard_win::set_clipboard(clipboard_win::formats::Unicode, content)?;
|
||||
Ok(())
|
||||
}
|
||||
ClipboardType::Selection => Ok(()),
|
||||
},
|
||||
#[cfg(feature = "term")]
|
||||
Self::Termcode => {
|
||||
crossterm::queue!(
|
||||
std::io::stdout(),
|
||||
osc52::SetClipboardCommand::new(content, clipboard_type)
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
Self::Custom(command_provider) => match clipboard_type {
|
||||
ClipboardType::Clipboard => {
|
||||
execute_command(&command_provider.paste, Some(content), false).map(|_| ())
|
||||
}
|
||||
ClipboardType::Selection => {
|
||||
if let Some(cmd) = &command_provider.paste_primary {
|
||||
execute_command(cmd, Some(content), false).map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! command_provider {
|
||||
($name:ident,
|
||||
yank => $yank_cmd:literal $( , $yank_arg:literal )* ;
|
||||
paste => $paste_cmd:literal $( , $paste_arg:literal )* ; ) => {
|
||||
const $name: CommandProvider = CommandProvider {
|
||||
yank: Command {
|
||||
command: Cow::Borrowed($yank_cmd),
|
||||
args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_arg) ),* ])
|
||||
},
|
||||
paste: Command {
|
||||
command: Cow::Borrowed($paste_cmd),
|
||||
args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_arg) ),* ])
|
||||
},
|
||||
yank_primary: None,
|
||||
paste_primary: None,
|
||||
};
|
||||
};
|
||||
($name:ident,
|
||||
yank => $yank_cmd:literal $( , $yank_arg:literal )* ;
|
||||
paste => $paste_cmd:literal $( , $paste_arg:literal )* ;
|
||||
yank_primary => $yank_primary_cmd:literal $( , $yank_primary_arg:literal )* ;
|
||||
paste_primary => $paste_primary_cmd:literal $( , $paste_primary_arg:literal )* ; ) => {
|
||||
const $name: CommandProvider = CommandProvider {
|
||||
yank: Command {
|
||||
command: Cow::Borrowed($yank_cmd),
|
||||
args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_arg) ),* ])
|
||||
},
|
||||
paste: Command {
|
||||
command: Cow::Borrowed($paste_cmd),
|
||||
args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_arg) ),* ])
|
||||
},
|
||||
yank_primary: Some(Command {
|
||||
command: Cow::Borrowed($yank_primary_cmd),
|
||||
args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_primary_arg) ),* ])
|
||||
}),
|
||||
paste_primary: Some(Command {
|
||||
command: Cow::Borrowed($paste_primary_cmd),
|
||||
args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_primary_arg) ),* ])
|
||||
}),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
command_provider! {
|
||||
TMUX,
|
||||
yank => "tmux", "save-buffer", "-";
|
||||
paste => "tmux", "load-buffer", "-w", "-";
|
||||
}
|
||||
command_provider! {
|
||||
PASTEBOARD,
|
||||
yank => "pbpaste";
|
||||
paste => "pbcopy";
|
||||
}
|
||||
command_provider! {
|
||||
WL_CLIPBOARD,
|
||||
yank => "wl-paste", "--no-newline";
|
||||
paste => "wl-copy", "--type", "text/plain";
|
||||
yank_primary => "wl-paste", "-p", "--no-newline";
|
||||
paste_primary => "wl-copy", "-p", "--type", "text/plain";
|
||||
}
|
||||
command_provider! {
|
||||
XCLIP,
|
||||
yank => "xclip", "-o", "-selection", "clipboard";
|
||||
paste => "xclip", "-i", "-selection", "clipboard";
|
||||
yank_primary => "xclip", "-o";
|
||||
paste_primary => "xclip", "-i";
|
||||
}
|
||||
command_provider! {
|
||||
XSEL,
|
||||
yank => "xsel", "-o", "-b";
|
||||
paste => "xsel", "-i", "-b";
|
||||
yank_primary => "xsel", "-o";
|
||||
paste_primary => "xsel", "-i";
|
||||
}
|
||||
command_provider! {
|
||||
WIN32,
|
||||
yank => "win32yank.exe", "-o", "--lf";
|
||||
paste => "win32yank.exe", "-i", "--crlf";
|
||||
}
|
||||
command_provider! {
|
||||
TERMUX,
|
||||
yank => "termux-clipboard-get";
|
||||
paste => "termux-clipboard-set";
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
mod osc52 {
|
||||
use {super::ClipboardType, crate::base64};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SetClipboardCommand {
|
||||
encoded_content: String,
|
||||
clipboard_type: ClipboardType,
|
||||
@ -182,232 +426,74 @@ fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
// Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/
|
||||
write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::result::Result<(), std::io::Error> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"OSC clipboard codes not supported by winapi.",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FallbackProvider {
|
||||
buf: String,
|
||||
primary_buf: String,
|
||||
}
|
||||
fn execute_command(
|
||||
cmd: &Command,
|
||||
input: Option<&str>,
|
||||
pipe_output: bool,
|
||||
) -> Result<Option<String>> {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
impl FallbackProvider {
|
||||
pub fn new() -> Self {
|
||||
#[cfg(feature = "term")]
|
||||
log::debug!(
|
||||
"No native clipboard provider found. Yanking by OSC 52 and pasting will be internal to Helix"
|
||||
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
|
||||
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
|
||||
|
||||
let mut command: Command = Command::new(cmd.command.as_ref());
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut command_mut: &mut Command = command
|
||||
.args(cmd.args.iter().map(AsRef::as_ref))
|
||||
.stdin(stdin)
|
||||
.stdout(stdout)
|
||||
.stderr(Stdio::null());
|
||||
|
||||
// Fix for https://github.com/helix-editor/helix/issues/5424
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
unsafe {
|
||||
command_mut = command_mut.pre_exec(|| match libc::setsid() {
|
||||
-1 => Err(std::io::Error::last_os_error()),
|
||||
_ => Ok(()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut child = command_mut.spawn()?;
|
||||
|
||||
if let Some(input) = input {
|
||||
let mut stdin = child.stdin.take().ok_or(ClipboardError::StdinWriteFailed)?;
|
||||
stdin
|
||||
.write_all(input.as_bytes())
|
||||
.map_err(|_| ClipboardError::StdinWriteFailed)?;
|
||||
}
|
||||
|
||||
// TODO: add timer?
|
||||
let output = child.wait_with_output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
log::error!(
|
||||
"clipboard provider {} failed with stderr: \"{}\"",
|
||||
cmd.command,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
#[cfg(not(feature = "term"))]
|
||||
log::warn!(
|
||||
"No native clipboard provider found! Yanking and pasting will be internal to Helix"
|
||||
);
|
||||
Self {
|
||||
buf: String::new(),
|
||||
primary_buf: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FallbackProvider {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipboardProvider for FallbackProvider {
|
||||
#[cfg(feature = "term")]
|
||||
fn name(&self) -> Cow<str> {
|
||||
Cow::Borrowed("termcode")
|
||||
return Err(ClipboardError::CommandFailed);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "term"))]
|
||||
fn name(&self) -> Cow<str> {
|
||||
Cow::Borrowed("none")
|
||||
}
|
||||
|
||||
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
|
||||
// This is the same noop if term is enabled or not.
|
||||
// We don't use the get side of OSC 52 as it isn't often enabled, it's a security hole,
|
||||
// and it would require this to be async to listen for the response
|
||||
let value = match clipboard_type {
|
||||
ClipboardType::Clipboard => self.buf.clone(),
|
||||
ClipboardType::Selection => self.primary_buf.clone(),
|
||||
};
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> {
|
||||
#[cfg(feature = "term")]
|
||||
crossterm::execute!(
|
||||
std::io::stdout(),
|
||||
osc52::SetClipboardCommand::new(&content, clipboard_type)
|
||||
)?;
|
||||
// Set our internal variables to use in get_content regardless of using OSC 52
|
||||
match clipboard_type {
|
||||
ClipboardType::Clipboard => self.buf = content,
|
||||
ClipboardType::Selection => self.primary_buf = content,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod command {
|
||||
use super::*;
|
||||
use anyhow::{bail, Context as _};
|
||||
|
||||
#[cfg(not(any(windows, target_os = "macos")))]
|
||||
pub fn is_exit_success(program: &str, args: &[&str]) -> bool {
|
||||
std::process::Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|out| out.status.success().then_some(()))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub prg: &'static str,
|
||||
pub args: &'static [&'static str],
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
|
||||
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
|
||||
|
||||
let mut command: Command = Command::new(self.prg);
|
||||
|
||||
let mut command_mut: &mut Command = command
|
||||
.args(self.args)
|
||||
.stdin(stdin)
|
||||
.stdout(stdout)
|
||||
.stderr(Stdio::null());
|
||||
|
||||
// Fix for https://github.com/helix-editor/helix/issues/5424
|
||||
if cfg!(unix) {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
unsafe {
|
||||
command_mut = command_mut.pre_exec(|| match libc::setsid() {
|
||||
-1 => Err(std::io::Error::last_os_error()),
|
||||
_ => Ok(()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut child = command_mut.spawn()?;
|
||||
|
||||
if let Some(input) = input {
|
||||
let mut stdin = child.stdin.take().context("stdin is missing")?;
|
||||
stdin
|
||||
.write_all(input.as_bytes())
|
||||
.context("couldn't write in stdin")?;
|
||||
}
|
||||
|
||||
// TODO: add timer?
|
||||
let output = child.wait_with_output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("clipboard provider {} failed", self.prg);
|
||||
}
|
||||
|
||||
if pipe_output {
|
||||
Ok(Some(String::from_utf8(output.stdout)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Provider {
|
||||
pub get_cmd: Config,
|
||||
pub set_cmd: Config,
|
||||
pub get_primary_cmd: Option<Config>,
|
||||
pub set_primary_cmd: Option<Config>,
|
||||
}
|
||||
|
||||
impl ClipboardProvider for Provider {
|
||||
fn name(&self) -> Cow<str> {
|
||||
if self.get_cmd.prg != self.set_cmd.prg {
|
||||
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
|
||||
} else {
|
||||
Cow::Borrowed(self.get_cmd.prg)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
|
||||
match clipboard_type {
|
||||
ClipboardType::Clipboard => Ok(self
|
||||
.get_cmd
|
||||
.execute(None, true)?
|
||||
.context("output is missing")?),
|
||||
ClipboardType::Selection => {
|
||||
if let Some(cmd) = &self.get_primary_cmd {
|
||||
return cmd.execute(None, true)?.context("output is missing");
|
||||
}
|
||||
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_contents(&mut self, value: String, clipboard_type: ClipboardType) -> Result<()> {
|
||||
let cmd = match clipboard_type {
|
||||
ClipboardType::Clipboard => &self.set_cmd,
|
||||
ClipboardType::Selection => {
|
||||
if let Some(cmd) = &self.set_primary_cmd {
|
||||
cmd
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
cmd.execute(Some(&value), false).map(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod provider {
|
||||
use super::{ClipboardProvider, ClipboardType};
|
||||
use anyhow::Result;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WindowsProvider;
|
||||
|
||||
impl ClipboardProvider for WindowsProvider {
|
||||
fn name(&self) -> Cow<str> {
|
||||
log::debug!("Using clipboard-win to interact with the system clipboard");
|
||||
Cow::Borrowed("clipboard-win")
|
||||
}
|
||||
|
||||
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
|
||||
match clipboard_type {
|
||||
ClipboardType::Clipboard => {
|
||||
let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
|
||||
Ok(contents)
|
||||
}
|
||||
ClipboardType::Selection => Ok(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> {
|
||||
match clipboard_type {
|
||||
ClipboardType::Clipboard => {
|
||||
clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?;
|
||||
}
|
||||
ClipboardType::Selection => {}
|
||||
};
|
||||
Ok(())
|
||||
if pipe_output {
|
||||
Ok(Some(String::from_utf8(output.stdout)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1713,6 +1713,12 @@ pub fn version(&self) -> i32 {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn path_completion_enabled(&self) -> bool {
|
||||
self.language_config()
|
||||
.and_then(|lang_config| lang_config.path_completion)
|
||||
.unwrap_or_else(|| self.config.load().path_completion)
|
||||
}
|
||||
|
||||
/// maintains the order as configured in the language_servers TOML array
|
||||
pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> {
|
||||
self.language_config().into_iter().flat_map(move |config| {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
|
||||
clipboard::ClipboardProvider,
|
||||
document::{
|
||||
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
|
||||
},
|
||||
@ -267,8 +268,15 @@ pub struct Config {
|
||||
pub auto_pairs: AutoPairConfig,
|
||||
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
|
||||
pub auto_completion: bool,
|
||||
/// Enable filepath completion.
|
||||
/// Show files and directories if an existing path at the cursor was recognized,
|
||||
/// either absolute or relative to the current opened document or current working directory (if the buffer is not yet saved).
|
||||
/// Defaults to true.
|
||||
pub path_completion: bool,
|
||||
/// Automatic formatting on save. Defaults to true.
|
||||
pub auto_format: bool,
|
||||
/// Default register used for yank/paste. Defaults to '"'
|
||||
pub default_yank_register: char,
|
||||
/// Automatic save on focus lost and/or after delay.
|
||||
/// Time delay in milliseconds since last edit after which auto save timer triggers.
|
||||
/// Time delay defaults to false with 3000ms delay. Focus lost defaults to false.
|
||||
@ -345,6 +353,8 @@ pub struct Config {
|
||||
/// Display diagnostic below the line they occur.
|
||||
pub inline_diagnostics: InlineDiagnosticsConfig,
|
||||
pub end_of_line_diagnostics: DiagnosticFilter,
|
||||
// Set to override the default clipboard provider
|
||||
pub clipboard_provider: ClipboardProvider,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
|
||||
@ -370,6 +380,8 @@ pub struct TerminalConfig {
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub args: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub join_args: bool,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@ -403,6 +415,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
return Some(TerminalConfig {
|
||||
command: "tmux".to_string(),
|
||||
args: vec!["split-window".to_string()],
|
||||
join_args: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -410,6 +423,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
return Some(TerminalConfig {
|
||||
command: "wezterm".to_string(),
|
||||
args: vec!["cli".to_string(), "split-pane".to_string()],
|
||||
join_args: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -947,7 +961,9 @@ fn default() -> Self {
|
||||
middle_click_paste: true,
|
||||
auto_pairs: AutoPairConfig::default(),
|
||||
auto_completion: true,
|
||||
path_completion: true,
|
||||
auto_format: true,
|
||||
default_yank_register: '"',
|
||||
auto_save: AutoSave::default(),
|
||||
idle_timeout: Duration::from_millis(250),
|
||||
completion_timeout: Duration::from_millis(250),
|
||||
@ -982,6 +998,7 @@ fn default() -> Self {
|
||||
jump_label_alphabet: ('a'..='z').collect(),
|
||||
inline_diagnostics: InlineDiagnosticsConfig::default(),
|
||||
end_of_line_diagnostics: DiagnosticFilter::Disable,
|
||||
clipboard_provider: ClipboardProvider::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1183,7 +1200,10 @@ pub fn new(
|
||||
theme_loader,
|
||||
last_theme: None,
|
||||
last_selection: None,
|
||||
registers: Registers::default(),
|
||||
registers: Registers::new(Box::new(arc_swap::access::Map::new(
|
||||
Arc::clone(&config),
|
||||
|config: &Config| &config.clipboard_provider,
|
||||
))),
|
||||
status_msg: None,
|
||||
autoinfo: None,
|
||||
idle_timer: Box::pin(sleep(conf.idle_timeout)),
|
||||
|
@ -287,7 +287,7 @@ fn execution_pause_indicator<'doc>(
|
||||
) -> GutterFn<'doc> {
|
||||
let style = theme.get("ui.debug.active");
|
||||
let current_stack_frame = editor.current_stack_frame();
|
||||
let frame_line = current_stack_frame.map(|frame| frame.line - 1);
|
||||
let frame_line = current_stack_frame.and_then(|frame| frame.line.checked_sub(1));
|
||||
let frame_source_path = current_stack_frame.map(|frame| {
|
||||
frame
|
||||
.source
|
||||
|
@ -366,12 +366,33 @@ pub async fn handle_debugger_message(&mut self, payload: helix_dap::Payload) ->
|
||||
Some(debugger) => debugger,
|
||||
None => return false,
|
||||
};
|
||||
let mut process = std::process::Command::new(config.command);
|
||||
|
||||
let process = match std::process::Command::new(config.command)
|
||||
.args(config.args)
|
||||
.arg(arguments.args.join(" "))
|
||||
.spawn()
|
||||
{
|
||||
process
|
||||
.args(
|
||||
config
|
||||
.args
|
||||
.into_iter()
|
||||
.map(|s| s.replace("%{cwd}", &arguments.cwd)), // temporary until #11164 is merged
|
||||
)
|
||||
.current_dir(arguments.cwd);
|
||||
|
||||
if config.join_args {
|
||||
process.arg(arguments.args.join(" "));
|
||||
} else {
|
||||
process.args(arguments.args);
|
||||
}
|
||||
|
||||
if let Some(env) = arguments.env {
|
||||
for (k, v) in env {
|
||||
match v {
|
||||
Some(v) => process.env(k, v),
|
||||
None => process.env_remove(k),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let process = match process.spawn() {
|
||||
Ok(process) => process,
|
||||
Err(err) => {
|
||||
self.set_error(format!("Error starting external terminal: {}", err));
|
||||
|
@ -1,10 +1,11 @@
|
||||
use std::{borrow::Cow, collections::HashMap, iter};
|
||||
|
||||
use anyhow::Result;
|
||||
use arc_swap::access::DynAccess;
|
||||
use helix_core::NATIVE_LINE_ENDING;
|
||||
|
||||
use crate::{
|
||||
clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType},
|
||||
clipboard::{ClipboardProvider, ClipboardType},
|
||||
Editor,
|
||||
};
|
||||
|
||||
@ -20,28 +21,25 @@
|
||||
/// * Document path (`%`): filename of the current buffer
|
||||
/// * System clipboard (`*`)
|
||||
/// * Primary clipboard (`+`)
|
||||
#[derive(Debug)]
|
||||
pub struct Registers {
|
||||
/// The mapping of register to values.
|
||||
/// Values are stored in reverse order when inserted with `Registers::write`.
|
||||
/// The order is reversed again in `Registers::read`. This allows us to
|
||||
/// efficiently prepend new values in `Registers::push`.
|
||||
inner: HashMap<char, Vec<String>>,
|
||||
clipboard_provider: Box<dyn ClipboardProvider>,
|
||||
clipboard_provider: Box<dyn DynAccess<ClipboardProvider>>,
|
||||
pub last_search_register: char,
|
||||
}
|
||||
|
||||
impl Default for Registers {
|
||||
fn default() -> Self {
|
||||
impl Registers {
|
||||
pub fn new(clipboard_provider: Box<dyn DynAccess<ClipboardProvider>>) -> Self {
|
||||
Self {
|
||||
inner: Default::default(),
|
||||
clipboard_provider: get_clipboard_provider(),
|
||||
clipboard_provider,
|
||||
last_search_register: '/',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registers {
|
||||
pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValues<'a>> {
|
||||
match name {
|
||||
'_' => Some(RegisterValues::new(iter::empty())),
|
||||
@ -64,7 +62,7 @@ pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValu
|
||||
Some(RegisterValues::new(iter::once(path)))
|
||||
}
|
||||
'*' | '+' => Some(read_from_clipboard(
|
||||
self.clipboard_provider.as_ref(),
|
||||
&self.clipboard_provider.load(),
|
||||
self.inner.get(&name),
|
||||
match name {
|
||||
'+' => ClipboardType::Clipboard,
|
||||
@ -84,8 +82,8 @@ pub fn write(&mut self, name: char, mut values: Vec<String>) -> Result<()> {
|
||||
'_' => Ok(()),
|
||||
'#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")),
|
||||
'*' | '+' => {
|
||||
self.clipboard_provider.set_contents(
|
||||
values.join(NATIVE_LINE_ENDING.as_str()),
|
||||
self.clipboard_provider.load().set_contents(
|
||||
&values.join(NATIVE_LINE_ENDING.as_str()),
|
||||
match name {
|
||||
'+' => ClipboardType::Clipboard,
|
||||
'*' => ClipboardType::Selection,
|
||||
@ -114,7 +112,10 @@ pub fn push(&mut self, name: char, mut value: String) -> Result<()> {
|
||||
'*' => ClipboardType::Selection,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let contents = self.clipboard_provider.get_contents(clipboard_type)?;
|
||||
let contents = self
|
||||
.clipboard_provider
|
||||
.load()
|
||||
.get_contents(&clipboard_type)?;
|
||||
let saved_values = self.inner.entry(name).or_default();
|
||||
|
||||
if !contents_are_saved(saved_values, &contents) {
|
||||
@ -127,7 +128,8 @@ pub fn push(&mut self, name: char, mut value: String) -> Result<()> {
|
||||
}
|
||||
value.push_str(&contents);
|
||||
self.clipboard_provider
|
||||
.set_contents(value, clipboard_type)?;
|
||||
.load()
|
||||
.set_contents(&value, clipboard_type)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -198,7 +200,8 @@ pub fn remove(&mut self, name: char) -> bool {
|
||||
fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
|
||||
if let Err(err) = self
|
||||
.clipboard_provider
|
||||
.set_contents("".into(), clipboard_type)
|
||||
.load()
|
||||
.set_contents("", clipboard_type)
|
||||
{
|
||||
log::error!(
|
||||
"Failed to clear {} clipboard: {err}",
|
||||
@ -210,17 +213,17 @@ fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clipboard_provider_name(&self) -> Cow<str> {
|
||||
self.clipboard_provider.name()
|
||||
pub fn clipboard_provider_name(&self) -> String {
|
||||
self.clipboard_provider.load().name().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_clipboard<'a>(
|
||||
provider: &dyn ClipboardProvider,
|
||||
provider: &ClipboardProvider,
|
||||
saved_values: Option<&'a Vec<String>>,
|
||||
clipboard_type: ClipboardType,
|
||||
) -> RegisterValues<'a> {
|
||||
match provider.get_contents(clipboard_type) {
|
||||
match provider.get_contents(&clipboard_type) {
|
||||
Ok(contents) => {
|
||||
// If we're pasting the same values that we just yanked, re-use
|
||||
// the saved values. This allows pasting multiple selections
|
||||
|
26
helix-view/tests/encoding/LICENSE-WHATWG
Normal file
26
helix-view/tests/encoding/LICENSE-WHATWG
Normal file
@ -0,0 +1,26 @@
|
||||
Copyright © WHATWG (Apple, Google, Mozilla, Microsoft).
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
@ -7185,13 +7184,13 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
χ
|
||||
ψ
|
||||
ω
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
︐
|
||||
︒
|
||||
︑
|
||||
︓
|
||||
︔
|
||||
︕
|
||||
︖
|
||||
︵
|
||||
︶
|
||||
︹
|
||||
@ -7204,14 +7203,14 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
﹂
|
||||
﹃
|
||||
﹄
|
||||
|
||||
|
||||
︗
|
||||
︘
|
||||
︻
|
||||
︼
|
||||
︷
|
||||
︸
|
||||
︱
|
||||
|
||||
︙
|
||||
︳
|
||||
︴
|
||||
|
||||
@ -23778,7 +23777,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
㑇
|
||||
⺈
|
||||
⺋
|
||||
|
||||
龴
|
||||
㖞
|
||||
㘚
|
||||
㘎
|
||||
@ -23786,19 +23785,19 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
⺗
|
||||
㥮
|
||||
㤘
|
||||
|
||||
龵
|
||||
㧏
|
||||
㧟
|
||||
㩳
|
||||
㧐
|
||||
|
||||
|
||||
龶
|
||||
龷
|
||||
㭎
|
||||
㱮
|
||||
㳠
|
||||
⺧
|
||||
|
||||
|
||||
龸
|
||||
⺪
|
||||
䁖
|
||||
䅟
|
||||
@ -23815,7 +23814,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
䓖
|
||||
䙡
|
||||
䙌
|
||||
|
||||
龹
|
||||
䜣
|
||||
䜩
|
||||
䝼
|
||||
@ -23832,7 +23831,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
䦛
|
||||
䦷
|
||||
䦶
|
||||
|
||||
龺
|
||||
|
||||
䲣
|
||||
䲟
|
||||
@ -23848,7 +23847,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
䴘
|
||||
䴙
|
||||
䶮
|
||||
|
||||
龻
|
||||
|
||||
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
@ -7184,13 +7183,13 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
χ
|
||||
ψ
|
||||
ω
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
︐
|
||||
︒
|
||||
︑
|
||||
︓
|
||||
︔
|
||||
︕
|
||||
︖
|
||||
︵
|
||||
︶
|
||||
︹
|
||||
@ -7203,14 +7202,14 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
﹂
|
||||
﹃
|
||||
﹄
|
||||
|
||||
|
||||
︗
|
||||
︘
|
||||
︻
|
||||
︼
|
||||
︷
|
||||
︸
|
||||
︱
|
||||
|
||||
︙
|
||||
︳
|
||||
︴
|
||||
|
||||
@ -23777,7 +23776,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
㑇
|
||||
⺈
|
||||
⺋
|
||||
|
||||
龴
|
||||
㖞
|
||||
㘚
|
||||
㘎
|
||||
@ -23785,19 +23784,19 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
⺗
|
||||
㥮
|
||||
㤘
|
||||
|
||||
龵
|
||||
㧏
|
||||
㧟
|
||||
㩳
|
||||
㧐
|
||||
|
||||
|
||||
龶
|
||||
龷
|
||||
㭎
|
||||
㱮
|
||||
㳠
|
||||
⺧
|
||||
|
||||
|
||||
龸
|
||||
⺪
|
||||
䁖
|
||||
䅟
|
||||
@ -23814,7 +23813,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
䓖
|
||||
䙡
|
||||
䙌
|
||||
|
||||
龹
|
||||
䜣
|
||||
䜩
|
||||
䝼
|
||||
@ -23831,7 +23830,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
䦛
|
||||
䦷
|
||||
䦶
|
||||
|
||||
龺
|
||||
|
||||
䲣
|
||||
䲟
|
||||
@ -23847,7 +23846,7 @@ Instead, please regenerate using generate-encoding-data.py
|
||||
䴘
|
||||
䴙
|
||||
䶮
|
||||
|
||||
龻
|
||||
|
||||
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
@ -1,5 +1,4 @@
|
||||
Any copyright to the test code below this comment is dedicated to the
|
||||
Public Domain. http://creativecommons.org/publicdomain/zero/1.0/
|
||||
Generated from WHATWG indexes.json; see LICENSE-WHATWG.
|
||||
|
||||
This is a generated file. Please do not edit.
|
||||
Instead, please regenerate using generate-encoding-data.py
|
||||
|
117
languages.toml
117
languages.toml
@ -44,6 +44,7 @@ gleam = { command = "gleam", args = ["lsp"] }
|
||||
glsl_analyzer = { command = "glsl_analyzer" }
|
||||
graphql-language-service = { command = "graphql-lsp", args = ["server", "-m", "stream"] }
|
||||
haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
|
||||
hyprls = { command = "hyprls" }
|
||||
idris2-lsp = { command = "idris2-lsp" }
|
||||
intelephense = { command = "intelephense", args = ["--stdio"] }
|
||||
jdtls = { command = "jdtls" }
|
||||
@ -83,6 +84,7 @@ pyright = { command = "pyright-langserver", args = ["--stdio"], config = {} }
|
||||
basedpyright = { command = "basedpyright-langserver", args = ["--stdio"], config = {} }
|
||||
pylyzer = { command = "pylyzer", args = ["--server"] }
|
||||
qmlls = { command = "qmlls" }
|
||||
quint-language-server = { command = "quint-language-server", args = ["--stdio"] }
|
||||
r = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
|
||||
racket = { command = "racket", args = ["-l", "racket-langserver"] }
|
||||
regols = { command = "regols" }
|
||||
@ -94,6 +96,7 @@ slint-lsp = { command = "slint-lsp", args = [] }
|
||||
solargraph = { command = "solargraph", args = ["stdio"] }
|
||||
solc = { command = "solc", args = ["--lsp"] }
|
||||
sourcekit-lsp = { command = "sourcekit-lsp" }
|
||||
spade-language-server = {command = "spade-language-server"}
|
||||
svlangserver = { command = "svlangserver", args = [] }
|
||||
swipl = { command = "swipl", args = [ "-g", "use_module(library(lsp_server))", "-g", "lsp_server:main", "-t", "halt", "--", "stdio" ] }
|
||||
superhtml = { command = "superhtml", args = ["lsp"]}
|
||||
@ -344,6 +347,19 @@ indent = { tab-width = 2, unit = " " }
|
||||
name = "protobuf"
|
||||
source = { git = "https://github.com/yusdacra/tree-sitter-protobuf", rev = "19c211a01434d9f03efff99f85e19f967591b175"}
|
||||
|
||||
[[language]]
|
||||
name = "textproto"
|
||||
file-types = ["txtpb", "textpb", "textproto"]
|
||||
comment-token = "#"
|
||||
scope = "source.textproto"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
formatter = { command = "txtpbfmt" }
|
||||
auto-format = true
|
||||
|
||||
[[grammar]]
|
||||
name = "textproto"
|
||||
source = { git = "https://github.com/PorterAtGoogle/tree-sitter-textproto", rev = "568471b80fd8793d37ed01865d8c2208a9fefd1b"}
|
||||
|
||||
[[language]]
|
||||
name = "elixir"
|
||||
scope = "source.elixir"
|
||||
@ -451,6 +467,7 @@ file-types = [
|
||||
"avsc",
|
||||
"ldtk",
|
||||
"ldtkl",
|
||||
{ glob = ".swift-format" },
|
||||
]
|
||||
language-servers = [ "vscode-json-language-server" ]
|
||||
auto-format = true
|
||||
@ -464,7 +481,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "73076
|
||||
name = "jsonc"
|
||||
scope = "source.json"
|
||||
injection-regex = "jsonc"
|
||||
file-types = ["jsonc"]
|
||||
file-types = ["jsonc", { glob = "tsconfig.json" }]
|
||||
grammar = "json"
|
||||
language-servers = [ "vscode-json-language-server" ]
|
||||
auto-format = true
|
||||
@ -729,6 +746,7 @@ injection-regex = "(js|javascript)"
|
||||
language-id = "javascript"
|
||||
file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", { glob = ".node_repl_history" }, { glob = "jakefile" }]
|
||||
shebangs = ["node"]
|
||||
roots = [ "package.json" ]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
language-servers = [ "typescript-language-server" ]
|
||||
@ -756,6 +774,7 @@ scope = "source.jsx"
|
||||
injection-regex = "jsx"
|
||||
language-id = "javascriptreact"
|
||||
file-types = ["jsx"]
|
||||
roots = [ "package.json" ]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
language-servers = [ "typescript-language-server" ]
|
||||
@ -769,6 +788,7 @@ injection-regex = "(ts|typescript)"
|
||||
language-id = "typescript"
|
||||
file-types = ["ts", "mts", "cts"]
|
||||
shebangs = ["deno", "bun", "ts-node"]
|
||||
roots = [ "package.json", "tsconfig.json" ]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
language-servers = [ "typescript-language-server" ]
|
||||
@ -801,6 +821,7 @@ scope = "source.tsx"
|
||||
injection-regex = "(tsx)" # |typescript
|
||||
language-id = "typescriptreact"
|
||||
file-types = ["tsx"]
|
||||
roots = [ "package.json", "tsconfig.json" ]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
language-servers = [ "typescript-language-server" ]
|
||||
@ -1305,7 +1326,7 @@ source = { git = "https://github.com/ikatyang/tree-sitter-vue", rev = "91fe27547
|
||||
[[language]]
|
||||
name = "yaml"
|
||||
scope = "source.yaml"
|
||||
file-types = ["yml", "yaml", { glob = ".prettierrc" }]
|
||||
file-types = ["yml", "yaml", { glob = ".prettierrc" }, { glob = ".clangd" }, { glob = ".clang-format" }]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = [ "yaml-language-server", "ansible-language-server" ]
|
||||
@ -1315,6 +1336,15 @@ injection-regex = "yml|yaml"
|
||||
name = "yaml"
|
||||
source = { git = "https://github.com/ikatyang/tree-sitter-yaml", rev = "0e36bed171768908f331ff7dff9d956bae016efb" }
|
||||
|
||||
[[language]]
|
||||
name = "nestedtext"
|
||||
scope = "text.nested"
|
||||
injection-regex = "nestedtext"
|
||||
file-types = ["nt"]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
grammar = "yaml"
|
||||
|
||||
[[language]]
|
||||
name = "haskell"
|
||||
scope = "source.haskell"
|
||||
@ -1365,7 +1395,7 @@ injection-regex = "zig"
|
||||
file-types = ["zig", "zon"]
|
||||
roots = ["build.zig"]
|
||||
auto-format = true
|
||||
comment-token = "//"
|
||||
comment-tokens = ["//", "///", "//!"]
|
||||
language-servers = [ "zls" ]
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
formatter = { command = "zig" , args = ["fmt", "--stdin"] }
|
||||
@ -1470,7 +1500,7 @@ indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "perl"
|
||||
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "e99bb5283805db4cb86c964722d709df21b0ac16" }
|
||||
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "72a08a496a23212f23802490ef6f4700d68cfd0e" }
|
||||
|
||||
[[language]]
|
||||
name = "pod"
|
||||
@ -1480,7 +1510,7 @@ file-types = ["pod"]
|
||||
|
||||
[[grammar]]
|
||||
name = "pod"
|
||||
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "39da859947b94abdee43e431368e1ae975c0a424" }
|
||||
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "0bf8387987c21bf2f8ed41d2575a8f22b139687f" }
|
||||
|
||||
[[language]]
|
||||
name = "racket"
|
||||
@ -1542,7 +1572,7 @@ injection-regex = "llvm"
|
||||
|
||||
[[grammar]]
|
||||
name = "llvm"
|
||||
source = { git = "https://github.com/benwilliamgraham/tree-sitter-llvm", rev = "e9948edc41e9e5869af99dddb2b5ff5cc5581af6" }
|
||||
source = { git = "https://github.com/benwilliamgraham/tree-sitter-llvm", rev = "c14cb839003348692158b845db9edda201374548" }
|
||||
|
||||
[[language]]
|
||||
name = "llvm-mir"
|
||||
@ -1554,7 +1584,7 @@ injection-regex = "mir"
|
||||
|
||||
[[grammar]]
|
||||
name = "llvm-mir"
|
||||
source = { git = "https://github.com/Flakebi/tree-sitter-llvm-mir", rev = "06fabca19454b2dc00c1b211a7cb7ad0bc2585f1" }
|
||||
source = { git = "https://github.com/Flakebi/tree-sitter-llvm-mir", rev = "d166ff8c5950f80b0a476956e7a0ad2f27c12505" }
|
||||
|
||||
[[language]]
|
||||
name = "llvm-mir-yaml"
|
||||
@ -1577,13 +1607,13 @@ injection-regex = "tablegen"
|
||||
|
||||
[[grammar]]
|
||||
name = "tablegen"
|
||||
source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "568dd8a937347175fd58db83d4c4cdaeb6069bd2" }
|
||||
source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "3e9c4822ab5cdcccf4f8aa9dcd42117f736d51d9" }
|
||||
|
||||
[[language]]
|
||||
name = "markdown"
|
||||
scope = "source.md"
|
||||
injection-regex = "md|markdown"
|
||||
file-types = ["md", "markdown", "mdx", "mkd", "mkdn", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }]
|
||||
file-types = ["md", "livemd", "markdown", "mdx", "mkd", "mkdn", "mdwn", "mdown", "markdn", "mdtxt", "mdtext", "workbook", { glob = "PULLREQ_EDITMSG" }]
|
||||
roots = [".marksman.toml"]
|
||||
language-servers = [ "marksman", "markdown-oxide" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
@ -1808,7 +1838,7 @@ indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "rescript"
|
||||
source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "467dcf99f68c47823d7b378779a6b282d7ef9782" }
|
||||
source = { git = "https://github.com/rescript-lang/tree-sitter-rescript", rev = "5e2a44a9d886b0a509f5bfd0437d33b4871fbac5" }
|
||||
|
||||
[[language]]
|
||||
name = "erlang"
|
||||
@ -1967,7 +1997,6 @@ roots = [ "Package.swift" ]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
formatter = { command = "swift-format" }
|
||||
auto-format = true
|
||||
language-servers = [ "sourcekit-lsp" ]
|
||||
|
||||
[[grammar]]
|
||||
@ -2202,7 +2231,7 @@ source = { git = "https://github.com/staysail/tree-sitter-meson", rev = "32a83e8
|
||||
[[language]]
|
||||
name = "sshclientconfig"
|
||||
scope = "source.sshclientconfig"
|
||||
file-types = [{ glob = ".ssh/config" }, { glob = "/etc/ssh/ssh_config" }]
|
||||
file-types = [{ glob = ".ssh/config" }, { glob = "/etc/ssh/ssh_config" }, { glob = "ssh_config.d/*.conf" } ]
|
||||
comment-token = "#"
|
||||
|
||||
[[grammar]]
|
||||
@ -2793,7 +2822,7 @@ source = { git = "https://github.com/inko-lang/tree-sitter-inko", rev = "7860637
|
||||
[[language]]
|
||||
name = "bicep"
|
||||
scope = "source.bicep"
|
||||
file-types = ["bicep"]
|
||||
file-types = ["bicep","bicepparam"]
|
||||
auto-format = true
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
@ -2802,7 +2831,7 @@ language-servers = [ "bicep-langserver" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "bicep"
|
||||
source = { git = "https://github.com/the-mikedavis/tree-sitter-bicep", rev = "d8e097fcfa143854861ef737161163a09cc2916b" }
|
||||
source = { git = "https://github.com/tree-sitter-grammars/tree-sitter-bicep", rev = "0092c7d1bd6bb22ce0a6f78497d50ea2b87f19c0" }
|
||||
|
||||
[[language]]
|
||||
name = "qml"
|
||||
@ -2983,7 +3012,7 @@ indent = { tab-width = 8, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "nasm"
|
||||
source = { git = "https://github.com/naclsn/tree-sitter-nasm", rev = "a0db15db6fcfb1bf2cc8702500e55e558825c48b" }
|
||||
source = { git = "https://github.com/naclsn/tree-sitter-nasm", rev = "570f3d7be01fffc751237f4cfcf52d04e20532d1" }
|
||||
|
||||
[[language]]
|
||||
name = "gas"
|
||||
@ -3316,7 +3345,7 @@ indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "unison"
|
||||
source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "1f505e2447fa876a87aee47ff3d70b9e141c744f" }
|
||||
source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "3c97db76d3cdbd002dfba493620c2d5df2fd6fa9" }
|
||||
|
||||
[[language]]
|
||||
name = "todotxt"
|
||||
@ -3550,6 +3579,7 @@ roots = ["hyprland.conf"]
|
||||
file-types = [ { glob = "hyprland.conf" }, { glob = "hyprpaper.conf" }, { glob = "hypridle.conf" }, { glob = "hyprlock.conf" } ]
|
||||
comment-token = "#"
|
||||
grammar = "hyprlang"
|
||||
language-servers = ["hyprls"]
|
||||
|
||||
[[grammar]]
|
||||
name = "hyprlang"
|
||||
@ -3839,11 +3869,15 @@ source = { git = "https://github.com/Decurity/tree-sitter-circom", rev = "021505
|
||||
name = "snakemake"
|
||||
scope = "source.snakemake"
|
||||
roots = ["Snakefile", "config.yaml", "environment.yaml", "workflow/"]
|
||||
file-types = ["smk", "Snakefile"]
|
||||
file-types = ["smk", { glob = "Snakefile" } ]
|
||||
comment-tokens = ["#", "##"]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-servers = ["pylsp" ]
|
||||
|
||||
[language.formatter]
|
||||
command = "snakefmt"
|
||||
args = ["-"]
|
||||
|
||||
[[grammar]]
|
||||
name = "snakemake"
|
||||
source = { git = "https://github.com/osthomas/tree-sitter-snakemake", rev = "e909815acdbe37e69440261ebb1091ed52e1dec6" }
|
||||
@ -3859,3 +3893,52 @@ indent = { tab-width = 4, unit = " " }
|
||||
[[grammar]]
|
||||
name = "cylc"
|
||||
source = { git = "https://github.com/elliotfontaine/tree-sitter-cylc", rev = "30dd40d9bf23912e4aefa93eeb4c7090bda3d0f6" }
|
||||
|
||||
[[language]]
|
||||
name = "quint"
|
||||
scope = "source.quint"
|
||||
file-types = ["qnt"]
|
||||
language-servers = ["quint-language-server"]
|
||||
comment-token = "//"
|
||||
block-comment-tokens = { start = "/*", end = "*/" }
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "quint"
|
||||
source = { git = "https://github.com/gruhn/tree-sitter-quint", rev = "eebbd01edfeff6404778c92efe5554e42e506a18" }
|
||||
|
||||
[[language]]
|
||||
name = "spade"
|
||||
scope = "source.spade"
|
||||
roots = ["swim.toml"]
|
||||
file-types = ['spade']
|
||||
injection-regex = "spade"
|
||||
comment-tokens = ["//", "///"]
|
||||
block-comment-tokens = [
|
||||
{ start = "/*", end = "*/" },
|
||||
{ start = "/**", end = "*/" },
|
||||
]
|
||||
language-servers = [ "spade-language-server" ]
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[language.auto-pairs]
|
||||
'(' = ')'
|
||||
'{' = '}'
|
||||
'[' = ']'
|
||||
'"' = '"'
|
||||
'<' = '>'
|
||||
|
||||
[[grammar]]
|
||||
name = "spade"
|
||||
source = { git = "https://gitlab.com/spade-lang/tree-sitter-spade/", rev = "4d5b141017c61fe7e168e0a5c5721ee62b0d9572" }
|
||||
|
||||
[[language]]
|
||||
name = "amber"
|
||||
scope = "source.ab"
|
||||
file-types = ["ab"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "amber"
|
||||
source = { git = "https://github.com/amber-lang/tree-sitter-amber", rev = "c6df3ec2ec243ed76550c525e7ac3d9a10c6c814" }
|
||||
|
24
pr.md
Normal file
24
pr.md
Normal file
@ -0,0 +1,24 @@
|
||||
Syntax symbol pickers
|
||||
==
|
||||
|
||||
This adds two new symbol picker commands that use tree-sitter rather than LSP. We run a new `symbols.scm` query across the file and extract tagged things like function definitions, types, classes, etc. For languages with unambiguous syntax this behaves roughly the same as the LSP symbol picker (`<space>s`). It's less precise though since we don't have semantic info about the language. For example it can easily produce false positives for C/C++ because of preprocessor magic.
|
||||
|
||||
The hope is to start introducing LSP-like features for navigation that can work without installing or running a language server. I made these two pickers in particular because I don't like LSP equivalents in ErlangLS - the document symbol picker can take a long time to show up during boot and the workspace symbol picker only searches for module names. The other motivation is to have some navigation features in cases when running a language server is too cumbersome - either to set up or because of resource constraints. For example `clangd` needs a fair amount of setup (`compile_commands.json`) that you might not want to do when quickly reading through a codebase.
|
||||
|
||||
GitHub already uses tree-sitter like this to provide [imprecise code navigation](https://docs.github.com/en/repositories/working-with-files/using-files/navigating-code-on-github#about-navigating-code-on-github). It should be possible to find definitions and references as well like `gd` and `gr` - this is left as a follow-up.
|
||||
|
||||
This PR also adds commands that either open the LSP symbol picker or the syntax one if a language server is not available. This way you can customize a language to not use the LSP symbol pickers, for example:
|
||||
|
||||
```toml
|
||||
[[language]]
|
||||
name = "erlang"
|
||||
language-servers = [{ name = "erlang-ls", except-features = ["document-symbols", "workspace-symbols"] }]
|
||||
```
|
||||
|
||||
and `<space>s` will use the syntax symbol picker, while `<space>s` on a Rust file will still prefer the language server.
|
||||
|
||||
---
|
||||
|
||||
Outstanding question - how closely should we try to match LSP symbol kind? Not at all? Should we have markup specific symbol kinds? (For example see markdown's `symbols.scm`).
|
||||
|
||||
Also this PR needs docs on writing `symbols.scm` queries.
|
60
runtime/queries/amber/highlights.scm
Normal file
60
runtime/queries/amber/highlights.scm
Normal file
@ -0,0 +1,60 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
"if"
|
||||
"loop"
|
||||
"for"
|
||||
"return"
|
||||
"fun"
|
||||
"else"
|
||||
"then"
|
||||
"break"
|
||||
"continue"
|
||||
"and"
|
||||
"or"
|
||||
"not"
|
||||
"let"
|
||||
"pub"
|
||||
"main"
|
||||
"echo"
|
||||
"exit"
|
||||
"fun"
|
||||
"import"
|
||||
"from"
|
||||
"as"
|
||||
"in"
|
||||
"fail"
|
||||
"failed"
|
||||
"silent"
|
||||
"nameof"
|
||||
"is"
|
||||
"unsafe"
|
||||
"trust"
|
||||
] @keyword
|
||||
|
||||
; Literals
|
||||
(boolean) @constant.builtin.boolean
|
||||
(number) @constant.numeric
|
||||
(null) @constant.numeric
|
||||
(string) @string
|
||||
(status) @keyword
|
||||
(command) @string
|
||||
(handler) @keyword
|
||||
(block) @punctuation.delimiter
|
||||
(variable_init) @keyword
|
||||
(variable_assignment) @punctuation.delimiter
|
||||
(variable) @variable
|
||||
(escape_sequence) @constant.character.escape
|
||||
(type_name_symbol) @type
|
||||
(interpolation) @punctuation.delimiter
|
||||
(reference) @keyword
|
||||
(preprocessor_directive) @comment
|
||||
(shebang) @comment
|
||||
(function_definition
|
||||
name: (variable) @function.method)
|
||||
(function_call
|
||||
name: (variable) @function.method)
|
||||
(import_statement
|
||||
"pub" @keyword
|
||||
"import" @keyword
|
||||
"from" @keyword)
|
@ -1,73 +1,232 @@
|
||||
; Keywords
|
||||
|
||||
; Includes
|
||||
[
|
||||
"module"
|
||||
"var"
|
||||
"param"
|
||||
"import"
|
||||
"resource"
|
||||
"existing"
|
||||
"if"
|
||||
"targetScope"
|
||||
"output"
|
||||
] @keyword
|
||||
"provider"
|
||||
"with"
|
||||
"as"
|
||||
"from"
|
||||
] @keyword.control.import
|
||||
|
||||
; Namespaces
|
||||
(module_declaration
|
||||
(identifier) @namespace)
|
||||
|
||||
; Builtins
|
||||
(primitive_type) @type.builtin
|
||||
|
||||
((member_expression
|
||||
object: (identifier) @type.builtin)
|
||||
(#eq? @type.builtin "sys"))
|
||||
|
||||
; Functions
|
||||
(call_expression
|
||||
function: (identifier) @function)
|
||||
|
||||
(decorator) @function.builtin
|
||||
(user_defined_function
|
||||
name: (identifier) @function)
|
||||
|
||||
(functionCall) @function
|
||||
; Properties
|
||||
(object_property
|
||||
(identifier) @function.method
|
||||
":" @punctuation.delimiter
|
||||
(_))
|
||||
|
||||
(functionCall
|
||||
(functionArgument
|
||||
(variableAccess) @variable))
|
||||
(object_property
|
||||
(compatible_identifier) @function.method
|
||||
":" @punctuation.delimiter
|
||||
(_))
|
||||
|
||||
; Literals/Types
|
||||
(property_identifier) @function.method
|
||||
|
||||
; Attributes
|
||||
(decorator
|
||||
"@" @attribute)
|
||||
|
||||
(decorator
|
||||
(call_expression
|
||||
(identifier) @attribute))
|
||||
|
||||
(decorator
|
||||
(call_expression
|
||||
(member_expression
|
||||
object: (identifier) @attribute
|
||||
property: (property_identifier) @attribute)))
|
||||
|
||||
; Types
|
||||
(type_declaration
|
||||
(identifier) @type)
|
||||
|
||||
(type_declaration
|
||||
(identifier)
|
||||
"="
|
||||
(identifier) @type)
|
||||
|
||||
(type
|
||||
(identifier) @type)
|
||||
|
||||
(resource_declaration
|
||||
(identifier) @type)
|
||||
|
||||
(resource_expression
|
||||
(identifier) @type)
|
||||
|
||||
; Parameters
|
||||
(parameter_declaration
|
||||
(identifier) @variable.parameter
|
||||
(_))
|
||||
|
||||
(call_expression
|
||||
function: (_)
|
||||
(arguments
|
||||
(identifier) @variable.parameter))
|
||||
|
||||
(call_expression
|
||||
function: (_)
|
||||
(arguments
|
||||
(member_expression
|
||||
object: (identifier) @variable.parameter)))
|
||||
|
||||
(parameter
|
||||
.
|
||||
(identifier) @variable.parameter)
|
||||
|
||||
; Variables
|
||||
(variable_declaration
|
||||
(identifier) @variable
|
||||
(_))
|
||||
|
||||
(metadata_declaration
|
||||
(identifier) @variable
|
||||
(_))
|
||||
|
||||
(output_declaration
|
||||
(identifier) @variable
|
||||
(_))
|
||||
|
||||
(object_property
|
||||
(_)
|
||||
":"
|
||||
(identifier) @variable)
|
||||
|
||||
(for_statement
|
||||
"for"
|
||||
(for_loop_parameters
|
||||
(loop_variable) @variable
|
||||
(loop_enumerator) @variable))
|
||||
|
||||
; Conditionals
|
||||
"if" @keyword.conditional
|
||||
|
||||
(ternary_expression
|
||||
"?" @keyword.control.conditional
|
||||
":" @keyword.control.conditional)
|
||||
|
||||
; Loops
|
||||
(for_statement
|
||||
"for" @keyword.control.repeat
|
||||
"in"
|
||||
":" @punctuation.delimiter)
|
||||
|
||||
; Keywords
|
||||
[
|
||||
"module"
|
||||
"metadata"
|
||||
"output"
|
||||
"param"
|
||||
"resource"
|
||||
"existing"
|
||||
"targetScope"
|
||||
"type"
|
||||
"var"
|
||||
"using"
|
||||
"test"
|
||||
] @keyword
|
||||
|
||||
"func" @keyword.function
|
||||
|
||||
"assert" @keyword.control.exception
|
||||
|
||||
; Operators
|
||||
[
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
"%"
|
||||
"||"
|
||||
"&&"
|
||||
"|"
|
||||
"=="
|
||||
"!="
|
||||
"=~"
|
||||
"!~"
|
||||
">"
|
||||
">="
|
||||
"<="
|
||||
"<"
|
||||
"??"
|
||||
"="
|
||||
"!"
|
||||
".?"
|
||||
] @operator
|
||||
|
||||
(subscript_expression
|
||||
"?" @operator)
|
||||
|
||||
(nullable_type
|
||||
"?" @operator)
|
||||
|
||||
"in" @keyword.operator
|
||||
|
||||
; Literals
|
||||
(string) @string
|
||||
|
||||
(escape_sequence) @constant.character
|
||||
|
||||
(number) @constant.number
|
||||
|
||||
(boolean) @constant.builtin.boolean
|
||||
|
||||
(null) @constant.builtin
|
||||
|
||||
; Misc
|
||||
(compatible_identifier
|
||||
"?" @punctuation.special)
|
||||
|
||||
(nullable_return_type) @punctuation.special
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
(resourceDeclaration
|
||||
(string
|
||||
(stringLiteral) @string.special))
|
||||
|
||||
(moduleDeclaration
|
||||
(string
|
||||
(stringLiteral) @string.special))
|
||||
[
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(string)
|
||||
(stringLiteral)
|
||||
] @string
|
||||
"("
|
||||
")"
|
||||
] @punctuation.bracket
|
||||
|
||||
(nullLiteral) @keyword
|
||||
(booleanLiteral) @constant.builtin.boolean
|
||||
(integerLiteral) @constant.numeric.integer
|
||||
(comment) @comment
|
||||
[
|
||||
"."
|
||||
":"
|
||||
"::"
|
||||
"=>"
|
||||
] @punctuation.delimiter
|
||||
|
||||
(string
|
||||
(variableAccess
|
||||
(identifier) @variable))
|
||||
; Interpolation
|
||||
(interpolation
|
||||
"${" @punctuation.special
|
||||
"}" @punctuation.special)
|
||||
|
||||
(type) @type
|
||||
(interpolation
|
||||
(identifier) @variable)
|
||||
|
||||
; Variables
|
||||
|
||||
(localVariable) @variable
|
||||
|
||||
; Statements
|
||||
|
||||
(object
|
||||
(objectProperty
|
||||
(identifier) @identifier))
|
||||
|
||||
(propertyAccess
|
||||
(identifier) @identifier)
|
||||
|
||||
(ifCondition) @keyword.control.conditional
|
||||
; Comments
|
||||
[
|
||||
(comment)
|
||||
(diagnostic_comment)
|
||||
] @comment
|
||||
|
@ -21,10 +21,10 @@
|
||||
|
||||
; Error level tags
|
||||
((tag (name) @error)
|
||||
(#match? @error "^(BUG|FIXME|ISSUE|XXX|FIX|SAFETY|FIXIT|FAILED|DEBUG)$"))
|
||||
(#match? @error "^(BUG|FIXME|ISSUE|XXX|FIX|SAFETY|FIXIT|FAILED|DEBUG|INVARIANT)$"))
|
||||
|
||||
("text" @error
|
||||
(#match? @error "^(BUG|FIXME|ISSUE|XXX|FIX|SAFETY|FIXIT|FAILED|DEBUG)$"))
|
||||
(#match? @error "^(BUG|FIXME|ISSUE|XXX|FIX|SAFETY|FIXIT|FAILED|DEBUG|INVARIANT)$"))
|
||||
|
||||
(tag
|
||||
(name) @ui.text
|
||||
|
@ -75,11 +75,16 @@
|
||||
"pre-instr-symbol"
|
||||
"post-instr-symbol"
|
||||
"heap-alloc-marker"
|
||||
"pcsections"
|
||||
"mmra"
|
||||
"cfi-type"
|
||||
"debug-instr-number"
|
||||
"debug-location"
|
||||
"dbg-instr-ref"
|
||||
"mcsymbol"
|
||||
"tied-def"
|
||||
"target-flags"
|
||||
"vscale"
|
||||
"CustomRegMask"
|
||||
"same_value"
|
||||
"def_cfa_register"
|
||||
@ -118,11 +123,16 @@
|
||||
"got"
|
||||
"jump-table"
|
||||
"syncscope"
|
||||
"address-taken"
|
||||
"machine-block-address-taken"
|
||||
"ir-block-address-taken"
|
||||
"landing-pad"
|
||||
"inlineasm-br-indirect-target"
|
||||
"ehfunclet-entry"
|
||||
"bb_id"
|
||||
"call-frame-size"
|
||||
"bbsections"
|
||||
"Exception"
|
||||
"Cold"
|
||||
|
||||
(intpred)
|
||||
(floatpred)
|
||||
|
@ -17,9 +17,11 @@
|
||||
|
||||
[
|
||||
"to"
|
||||
"nneg"
|
||||
"nuw"
|
||||
"nsw"
|
||||
"exact"
|
||||
"disjoint"
|
||||
"unwind"
|
||||
"from"
|
||||
"cleanup"
|
||||
|
1
runtime/queries/nestedtext/highlights.scm
Normal file
1
runtime/queries/nestedtext/highlights.scm
Normal file
@ -0,0 +1 @@
|
||||
; inherits: yaml
|
1
runtime/queries/nestedtext/indents.scm
Normal file
1
runtime/queries/nestedtext/indents.scm
Normal file
@ -0,0 +1 @@
|
||||
; inherits: yaml
|
1
runtime/queries/nestedtext/injections.scm
Normal file
1
runtime/queries/nestedtext/injections.scm
Normal file
@ -0,0 +1 @@
|
||||
; inherits: yaml
|
1
runtime/queries/nestedtext/textobjects.scm
Normal file
1
runtime/queries/nestedtext/textobjects.scm
Normal file
@ -0,0 +1 @@
|
||||
; inherits: yaml
|
@ -127,6 +127,16 @@
|
||||
(#set! injection.language "haskell")
|
||||
(#set! injection.combined))
|
||||
|
||||
; pkgs.writers.writeNim[Bin] name attrs content
|
||||
(apply_expression
|
||||
(apply_expression
|
||||
function: (apply_expression
|
||||
function: ((_) @_func)))
|
||||
argument: (indented_string_expression (string_fragment) @injection.content)
|
||||
(#match? @_func "(^|\\.)writeNim(Bin)?$")
|
||||
(#set! injection.language "nim")
|
||||
(#set! injection.combined))
|
||||
|
||||
; pkgs.writers.writeJS[Bin] name attrs content
|
||||
(apply_expression
|
||||
(apply_expression
|
||||
|
@ -1,9 +1,9 @@
|
||||
[
|
||||
"use" "no" "require" "package"
|
||||
"use" "no" "require" "package" "class" "role"
|
||||
] @keyword.control.import
|
||||
|
||||
[
|
||||
"sub"
|
||||
"sub" "method" "async" "extended"
|
||||
] @keyword.function
|
||||
|
||||
[
|
||||
@ -17,7 +17,7 @@
|
||||
] @keyword.control.repeat
|
||||
|
||||
[
|
||||
"my" "our" "local"
|
||||
"my" "our" "local" "state"
|
||||
] @keyword.storage.modifier
|
||||
|
||||
[
|
||||
@ -29,9 +29,10 @@
|
||||
] @constant.builtin
|
||||
|
||||
(phaser_statement phase: _ @keyword.directive)
|
||||
(class_phaser_statement phase: _ @keyword.directive)
|
||||
|
||||
[
|
||||
"or" "and"
|
||||
"or" "xor" "and"
|
||||
"eq" "ne" "cmp" "lt" "le" "ge" "gt"
|
||||
"isa"
|
||||
] @keyword.operator
|
||||
@ -55,7 +56,7 @@
|
||||
|
||||
[(quoted_regexp) (match_regexp)] @string.regexp
|
||||
|
||||
(autoquoted_bareword _?) @string.special
|
||||
(autoquoted_bareword) @string.special
|
||||
|
||||
[(scalar) (arraylen)] @variable
|
||||
(scalar_deref_expression ["->" "$" "*"] @variable)
|
||||
|
94
runtime/queries/quint/highlights.scm
Normal file
94
runtime/queries/quint/highlights.scm
Normal file
@ -0,0 +1,94 @@
|
||||
[
|
||||
"module"
|
||||
"type"
|
||||
"assume"
|
||||
"const"
|
||||
"var"
|
||||
"val"
|
||||
"nondet"
|
||||
"def"
|
||||
"pure"
|
||||
"action"
|
||||
"temporal"
|
||||
"run"
|
||||
] @keyword
|
||||
|
||||
(match_expr "match" @keyword.control.conditional)
|
||||
|
||||
(if_else_condition
|
||||
"if" @keyword.control.conditional
|
||||
"else" @keyword.control.conditional)
|
||||
|
||||
(import "import" @keyword.control.import)
|
||||
(import "as" @keyword.control.import)
|
||||
(import "from" @keyword.control.import)
|
||||
(export "export" @keyword.control.import)
|
||||
(export "as" @keyword.control.import)
|
||||
|
||||
[
|
||||
"true"
|
||||
"false"
|
||||
"Int"
|
||||
"Nat"
|
||||
"Bool"
|
||||
] @constant.builtin
|
||||
|
||||
[
|
||||
";"
|
||||
"."
|
||||
","
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
"-"
|
||||
"+"
|
||||
"*"
|
||||
"/"
|
||||
"%"
|
||||
"<"
|
||||
"<="
|
||||
"="
|
||||
"=="
|
||||
"!="
|
||||
"=>"
|
||||
">"
|
||||
">="
|
||||
"^"
|
||||
"->"
|
||||
] @operator
|
||||
|
||||
(infix_and "and" @operator)
|
||||
(infix_or "or" @operator)
|
||||
(infix_iff "iff" @operator)
|
||||
(infix_implies "implies" @operator)
|
||||
|
||||
(braced_and "and" @keyword)
|
||||
(braced_or "or" @keyword)
|
||||
(braced_all "all" @keyword)
|
||||
(braced_any "any" @keyword)
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
(polymorphic_type
|
||||
(type) @type.parameter)
|
||||
|
||||
(variant_constructor) @type.enum.variant
|
||||
|
||||
(type) @type
|
||||
(int_literal) @constant.numeric.integer
|
||||
(comment) @comment
|
||||
(string) @string
|
||||
|
||||
(operator_application
|
||||
operator: (qualified_identifier) @function)
|
||||
|
||||
(operator_definition
|
||||
name: (qualified_identifier) @function
|
||||
arguments: (typed_argument_list))
|
@ -10,11 +10,10 @@
|
||||
[
|
||||
(type_identifier)
|
||||
(unit_type)
|
||||
(list)
|
||||
(list_pattern)
|
||||
] @type
|
||||
|
||||
(list ["list{" "}"] @type)
|
||||
(list_pattern ["list{" "}"] @type)
|
||||
|
||||
[
|
||||
(variant_identifier)
|
||||
(polyvar_identifier)
|
||||
@ -72,14 +71,16 @@
|
||||
; single parameter with no parens
|
||||
(function parameter: (value_identifier) @variable.parameter)
|
||||
|
||||
; first-level descructuring (required for nvim-tree-sitter as it only matches direct
|
||||
; children and the above patterns do not match destructuring patterns in NeoVim)
|
||||
(parameter (tuple_pattern (tuple_item_pattern (value_identifier) @variable.parameter)))
|
||||
(parameter (array_pattern (value_identifier) @variable.parameter))
|
||||
(parameter (record_pattern (value_identifier) @variable.parameter))
|
||||
|
||||
; Meta
|
||||
;-----
|
||||
|
||||
[
|
||||
"@"
|
||||
"@@"
|
||||
(decorator_identifier)
|
||||
] @keyword.directive
|
||||
(decorator_identifier) @keyword.directive
|
||||
|
||||
(extension_identifier) @keyword
|
||||
("%") @keyword
|
||||
@ -87,7 +88,7 @@
|
||||
; Misc
|
||||
;-----
|
||||
|
||||
; (subscript_expression index: (string) @attribute)
|
||||
(subscript_expression index: (string) @attribute)
|
||||
(polyvar_type_pattern "#" @constant)
|
||||
|
||||
[
|
||||
@ -101,18 +102,21 @@
|
||||
"external"
|
||||
"let"
|
||||
"module"
|
||||
"mutable"
|
||||
"private"
|
||||
"rec"
|
||||
"type"
|
||||
"and"
|
||||
"assert"
|
||||
"async"
|
||||
"await"
|
||||
"with"
|
||||
"unpack"
|
||||
] @keyword.storage.type
|
||||
"lazy"
|
||||
"constraint"
|
||||
] @keyword
|
||||
|
||||
"mutable" @keyword.storage.modifier
|
||||
((function "async" @keyword.storage))
|
||||
|
||||
(module_unpack "unpack" @keyword)
|
||||
|
||||
[
|
||||
"if"
|
||||
@ -169,6 +173,7 @@
|
||||
"->"
|
||||
"|>"
|
||||
":>"
|
||||
"+="
|
||||
(uncurry)
|
||||
] @operator
|
||||
|
||||
|
@ -1,8 +1,29 @@
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
||||
((comment) @injection.content (#set! injection.language "comment"))
|
||||
|
||||
((raw_js) @injection.content
|
||||
(#set! injection.language "javascript"))
|
||||
; %re
|
||||
(extension_expression
|
||||
(extension_identifier) @_name
|
||||
(#eq? @_name "re")
|
||||
(expression_statement (_) @injection.content (#set! injection.language "regex")))
|
||||
|
||||
; %raw
|
||||
(extension_expression
|
||||
(extension_identifier) @_name
|
||||
(#eq? @_name "raw")
|
||||
(expression_statement
|
||||
(_ (_) @injection.content (#set! injection.language "javascript"))))
|
||||
|
||||
; %graphql
|
||||
(extension_expression
|
||||
(extension_identifier) @_name
|
||||
(#eq? @_name "graphql")
|
||||
(expression_statement
|
||||
(_ (_) @injection.content (#set! injection.language "graphql"))))
|
||||
|
||||
; %relay
|
||||
(extension_expression
|
||||
(extension_identifier) @_name
|
||||
(#eq? @_name "relay")
|
||||
(expression_statement
|
||||
(_ (_) @injection.content (#set! injection.language "graphql") )))
|
||||
|
||||
((raw_gql) @injection.content
|
||||
(#set! injection.language "graphql"))
|
@ -1,7 +1,7 @@
|
||||
(switch_expression) @local.scope
|
||||
(if_expression) @local.scope
|
||||
|
||||
; Definitions
|
||||
;------------
|
||||
(type_declaration) @local.defintion
|
||||
(let_binding) @local.defintion
|
||||
(type_declaration) @local.definition
|
||||
(let_binding) @local.definition
|
||||
(module_declaration) @local.definition
|
||||
|
@ -1,7 +1,7 @@
|
||||
; Classes (modules)
|
||||
;------------------
|
||||
|
||||
(module_declaration definition: ((_) @class.inside)) @class.around
|
||||
(module_binding definition: ((_) @class.inside)) @class.around
|
||||
|
||||
; Blocks
|
||||
;-------
|
||||
|
@ -12,6 +12,8 @@
|
||||
(unicode_string_literal)
|
||||
(yul_string_literal)
|
||||
] @string
|
||||
(hex_string_literal "hex" @string.special.symbol)
|
||||
(unicode_string_literal "unicode" @string.special.symbol)
|
||||
[
|
||||
(number_literal)
|
||||
(yul_decimal_number)
|
||||
@ -20,6 +22,7 @@
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
(yul_boolean)
|
||||
] @constant.builtin.boolean
|
||||
|
||||
(comment) @comment
|
||||
@ -44,18 +47,18 @@
|
||||
(type_name "(" @punctuation.bracket "=>" @punctuation.delimiter ")" @punctuation.bracket)
|
||||
|
||||
; Definitions
|
||||
(struct_declaration
|
||||
(struct_declaration
|
||||
name: (identifier) @type)
|
||||
(enum_declaration
|
||||
(enum_declaration
|
||||
name: (identifier) @type)
|
||||
(contract_declaration
|
||||
name: (identifier) @type)
|
||||
name: (identifier) @type)
|
||||
(library_declaration
|
||||
name: (identifier) @type)
|
||||
name: (identifier) @type)
|
||||
(interface_declaration
|
||||
name: (identifier) @type)
|
||||
(event_definition
|
||||
name: (identifier) @type)
|
||||
(event_definition
|
||||
name: (identifier) @type)
|
||||
|
||||
(function_definition
|
||||
name: (identifier) @function)
|
||||
|
130
runtime/queries/spade/highlights.scm
Normal file
130
runtime/queries/spade/highlights.scm
Normal file
@ -0,0 +1,130 @@
|
||||
(self) @variable.builtin
|
||||
|
||||
(unit_definition (identifier) @function)
|
||||
|
||||
(parameter (identifier) @variable.parameter)
|
||||
|
||||
((pipeline_reg_marker) @keyword)
|
||||
|
||||
(scoped_identifier
|
||||
path: (identifier) @namespace)
|
||||
(scoped_identifier
|
||||
(scoped_identifier
|
||||
name: (identifier) @namespace))
|
||||
|
||||
((builtin_type) @type.builtin)
|
||||
|
||||
((identifier) @type.builtin
|
||||
(#any-of?
|
||||
@type.builtin
|
||||
"uint"
|
||||
"Option"
|
||||
"Memory"))
|
||||
|
||||
((identifier) @type.enum.variant.builtin
|
||||
(#any-of? @type.enum.variant.builtin "Some" "None"))
|
||||
|
||||
((pipeline_stage_name) @label)
|
||||
|
||||
((stage_reference
|
||||
stage: (identifier) @label))
|
||||
|
||||
[
|
||||
"pipeline"
|
||||
"let"
|
||||
"set"
|
||||
"entity"
|
||||
"fn"
|
||||
"reg"
|
||||
"reset"
|
||||
"initial"
|
||||
"inst"
|
||||
"assert"
|
||||
"struct"
|
||||
"enum"
|
||||
"stage"
|
||||
"impl"
|
||||
"port"
|
||||
"decl"
|
||||
"mod"
|
||||
"where"
|
||||
"trait"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
"use"
|
||||
] @keyword.import
|
||||
|
||||
[
|
||||
"$if"
|
||||
"$else"
|
||||
"$config"
|
||||
] @keyword.directive
|
||||
|
||||
((comptime_if ["{" "}"] @keyword.directive))
|
||||
((comptime_else ["{" "}"] @keyword.directive))
|
||||
|
||||
((attribute) ["#" "[" "]"] @punctuation.delimiter)
|
||||
|
||||
[
|
||||
"else"
|
||||
"if"
|
||||
"match"
|
||||
] @keyword.control.conditional
|
||||
|
||||
(bool_literal) @constant.builtin.boolean
|
||||
(int_literal) @constant.numeric.integer
|
||||
|
||||
[
|
||||
"&"
|
||||
"inv"
|
||||
"-"
|
||||
"=>"
|
||||
">"
|
||||
"<"
|
||||
"::<"
|
||||
"::$<"
|
||||
"="
|
||||
"->"
|
||||
"~"
|
||||
"!"
|
||||
] @operator
|
||||
|
||||
|
||||
((op_add) @operator)
|
||||
((op_sub) @operator)
|
||||
((op_mul) @operator)
|
||||
((op_equals) @operator)
|
||||
((op_lt) @operator)
|
||||
((op_gt) @operator)
|
||||
((op_le) @operator)
|
||||
((op_ge) @operator)
|
||||
((op_lshift) @operator)
|
||||
((op_rshift) @operator)
|
||||
((op_bitwise_and) @operator)
|
||||
((op_bitwise_xor) @operator)
|
||||
((op_bitwise_or) @operator)
|
||||
((op_logical_and) @operator)
|
||||
((op_logical_or) @operator)
|
||||
|
||||
|
||||
[
|
||||
(line_comment)
|
||||
(block_comment)
|
||||
] @comment
|
||||
|
||||
[
|
||||
(doc_comment)
|
||||
] @comment.block.documentation
|
||||
|
||||
|
||||
((identifier) @type
|
||||
(#match? @type "[A-Z]"))
|
||||
|
||||
((scoped_identifier
|
||||
name: (identifier) @type)
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
|
||||
|
27
runtime/queries/spade/indents.scm
Normal file
27
runtime/queries/spade/indents.scm
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
[
|
||||
(unit_definition)
|
||||
(struct_definition)
|
||||
(enum_definition)
|
||||
(enum_member)
|
||||
(impl)
|
||||
(mod)
|
||||
(argument_list)
|
||||
(let_binding)
|
||||
(block)
|
||||
(tuple_literal)
|
||||
(array_literal)
|
||||
(paren_expression)
|
||||
(turbofish)
|
||||
(generic_parameters)
|
||||
(named_unpack)
|
||||
(positional_unpack)
|
||||
(tuple_pattern)
|
||||
] @indent
|
||||
|
||||
[
|
||||
"}"
|
||||
"]"
|
||||
")"
|
||||
] @outdent
|
||||
|
@ -68,7 +68,9 @@
|
||||
"def"
|
||||
"defset"
|
||||
"defvar"
|
||||
"deftype"
|
||||
"assert"
|
||||
"dump"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
|
22
runtime/queries/textproto/highlights.scm
Normal file
22
runtime/queries/textproto/highlights.scm
Normal file
@ -0,0 +1,22 @@
|
||||
(string) @string
|
||||
|
||||
(field_name) @variable.other.member
|
||||
|
||||
(comment) @comment
|
||||
|
||||
(number) @constant.numeric
|
||||
; covers e.g. booleans and "inf"
|
||||
(scalar_value (identifier)) @constant
|
||||
; Covers "-inf"
|
||||
(scalar_value (signed_identifier)) @constant.numeric
|
||||
|
||||
[
|
||||
(open_squiggly)
|
||||
(close_squiggly)
|
||||
(open_square)
|
||||
(close_square)
|
||||
(open_arrow)
|
||||
(close_arrow)
|
||||
] @punctuation.bracket
|
||||
|
||||
"," @punctuation.delimiter
|
11
runtime/queries/textproto/indents.scm
Normal file
11
runtime/queries/textproto/indents.scm
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
(message_value)
|
||||
(message_list)
|
||||
(scalar_list)
|
||||
] @indent
|
||||
|
||||
[
|
||||
(close_arrow)
|
||||
(close_square)
|
||||
(close_squiggly)
|
||||
] @outdent
|
12
runtime/queries/textproto/textobjects.scm
Normal file
12
runtime/queries/textproto/textobjects.scm
Normal file
@ -0,0 +1,12 @@
|
||||
(message_field
|
||||
(_) @entry.inside) @entry.around
|
||||
|
||||
(scalar_field
|
||||
(_) @entry.inside) @entry.around
|
||||
|
||||
(message_list
|
||||
(_) @entry.around)
|
||||
|
||||
(scalar_list
|
||||
(_) @entry.around)
|
||||
|
@ -9,32 +9,25 @@
|
||||
;; Keywords
|
||||
[
|
||||
(kw_forall)
|
||||
(type_kw)
|
||||
(kw_equals)
|
||||
(do)
|
||||
(kw_let)
|
||||
(ability)
|
||||
(where)
|
||||
] @keyword
|
||||
|
||||
(kw_let) @keyword.function
|
||||
(type_kw) @keyword.storage.type
|
||||
(unique) @keyword.storage.modifier
|
||||
(type_kw) @keyword.storage.modifier
|
||||
(structural) @keyword.storage.modifier
|
||||
("use") @keyword.control.import
|
||||
|
||||
|
||||
[
|
||||
(type_constructor)
|
||||
] @constructor
|
||||
(unique) @keyword.storage.modifier
|
||||
|
||||
[
|
||||
(operator)
|
||||
(pipe)
|
||||
(arrow_symbol)
|
||||
(">")
|
||||
(or)
|
||||
(and)
|
||||
(bang)
|
||||
] @operator
|
||||
|
||||
[
|
||||
@ -48,24 +41,62 @@
|
||||
|
||||
(blank_pattern) @variable.builtin
|
||||
|
||||
(pattern) @variable
|
||||
|
||||
(use_clause) @keyword.import
|
||||
|
||||
;; Types
|
||||
(record_field name: (wordy_id) @variable.other.member type: (_) @type)
|
||||
(type_constructor (type_name (wordy_id) @constructor))
|
||||
(ability_declaration type_name: (wordy_id) @type type_arg: (wordy_id) @variable.parameter)
|
||||
(effect (wordy_id) @special) ;; NOTE: an effect is just like a type, but in signature we special case it
|
||||
(record_field
|
||||
(field_name) @variable.other.member
|
||||
type: (regular_identifier) @type)
|
||||
|
||||
;; Namespaces
|
||||
(path) @namespace
|
||||
(namespace) @namespace
|
||||
(type_name) @type
|
||||
|
||||
;; Terms
|
||||
(type_signature term_name: (path)? @variable term_name: (wordy_id) @variable)
|
||||
(type_signature (wordy_id) @type)
|
||||
(type_signature (term_type(delayed(wordy_id))) @type)
|
||||
(type_declaration
|
||||
(regular_identifier) @type.enum.variant)
|
||||
|
||||
(term_definition param: (wordy_id) @variable.parameter)
|
||||
(ability_name
|
||||
(path)? @namespace
|
||||
(regular_identifier) @type)
|
||||
|
||||
(function_application function_name: (path)? function_name: (wordy_id) @function)
|
||||
(ability_declaration
|
||||
(ability_name) @type
|
||||
(type_argument) @variable.parameter)
|
||||
|
||||
(type_constructor) @constructor
|
||||
|
||||
(constructor
|
||||
(constructor_name) @constructor)
|
||||
|
||||
(constructor
|
||||
type: (regular_identifier) @type)
|
||||
|
||||
(effect
|
||||
(regular_identifier) @special) ; NOTE: an effect is a special type
|
||||
|
||||
; Namespaces
|
||||
(path) @module
|
||||
|
||||
(namespace) @module
|
||||
|
||||
; Terms
|
||||
(type_signature
|
||||
term_name: (path) @module
|
||||
term_name: (regular_identifier) @variable)
|
||||
|
||||
(type_signature
|
||||
term_name: (regular_identifier) @variable)
|
||||
|
||||
(term_type) @type
|
||||
|
||||
(term_definition
|
||||
name: (path) @namespace)
|
||||
|
||||
(term_definition
|
||||
name: (regular_identifier) @variable)
|
||||
|
||||
(term_definition
|
||||
param: (regular_identifier) @variable.parameter)
|
||||
|
||||
;; Punctuation
|
||||
[
|
||||
@ -82,4 +113,6 @@
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
(test_watch_expression (wordy_id) @keyword.directive)
|
||||
(watch_expression) @keyword.directive
|
||||
|
||||
(test_watch_expression) @keyword.directive
|
||||
|
@ -1,11 +1,6 @@
|
||||
[
|
||||
(term_definition)
|
||||
(type_declaration)
|
||||
(pattern)
|
||||
(tuple_or_parenthesized)
|
||||
(literal_list)
|
||||
(tuple_pattern)
|
||||
(function_application)
|
||||
(exp_if)
|
||||
(constructor)
|
||||
(delay_block)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user