Merge branch 'master' into ilyakooo0:iko/init-hoon
@ -1,3 +1,17 @@
|
||||
# we use tokio_unstable to enable runtime::Handle::id so we can separate
|
||||
# globals from multiple parallel tests. If that function ever does get removed
|
||||
# its possible to replace (with some additional overhead and effort)
|
||||
# Annoyingly build.rustflags doesn't work here because it gets overwritten
|
||||
# if people have their own global target.<..> config (for example to enable mold)
|
||||
# specifying flags this way is more robust as they get merged
|
||||
# This still gets overwritten by RUST_FLAGS though, luckily it shouldn't be necessary
|
||||
# to set those most of the time. If downstream does overwrite this its not a huge
|
||||
# deal since it will only break tests anyway
|
||||
[target."cfg(all())"]
|
||||
rustflags = ["--cfg", "tokio_unstable", "-C", "target-feature=-crt-static"]
|
||||
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
integration-test = "test --features integration --profile integration --workspace --test integration"
|
||||
|
||||
|
2
.github/workflows/build.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
shared-key: "build"
|
||||
|
||||
- name: Cache test tree-sitter grammar
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: runtime/grammars
|
||||
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
|
||||
|
4
.github/workflows/cachix.yml
vendored
@ -14,10 +14,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install nix
|
||||
uses: cachix/install-nix-action@v24
|
||||
uses: cachix/install-nix-action@v25
|
||||
|
||||
- name: Authenticate with Cachix
|
||||
uses: cachix/cachix-action@v13
|
||||
uses: cachix/cachix-action@v14
|
||||
with:
|
||||
name: helix
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
2
.ignore
@ -1,2 +0,0 @@
|
||||
# Things that we don't want ripgrep to search that we do want in git
|
||||
# https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering
|
534
Cargo.lock
generated
@ -11,6 +11,7 @@ members = [
|
||||
"helix-loader",
|
||||
"helix-vcs",
|
||||
"helix-parsec",
|
||||
"helix-stdx",
|
||||
"xtask",
|
||||
]
|
||||
|
||||
@ -36,7 +37,7 @@ package.helix-tui.opt-level = 2
|
||||
package.helix-term.opt-level = 2
|
||||
|
||||
[workspace.dependencies]
|
||||
tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
|
||||
tree-sitter = { version = "0.20", git = "https://github.com/helix-editor/tree-sitter", rev = "660481dbf71413eba5a928b0b0ab8da50c1109e0" }
|
||||
nucleo = "0.2.0"
|
||||
|
||||
[workspace.package]
|
||||
|
@ -51,7 +51,8 @@ ### `[editor]` Section
|
||||
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
|
||||
| `auto-format` | Enable automatic formatting on save | `true` |
|
||||
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
|
||||
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
|
||||
| `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` |
|
||||
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
|
||||
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
|
||||
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
|
||||
|
@ -21,10 +21,10 @@
|
||||
| cpon | ✓ | | ✓ | |
|
||||
| cpp | ✓ | ✓ | ✓ | `clangd` |
|
||||
| crystal | ✓ | ✓ | | `crystalline` |
|
||||
| css | ✓ | | | `vscode-css-language-server` |
|
||||
| css | ✓ | | ✓ | `vscode-css-language-server` |
|
||||
| cue | ✓ | | | `cuelsp` |
|
||||
| d | ✓ | ✓ | ✓ | `serve-d` |
|
||||
| dart | ✓ | | ✓ | `dart` |
|
||||
| dart | ✓ | ✓ | ✓ | `dart` |
|
||||
| dbml | ✓ | | | |
|
||||
| devicetree | ✓ | | | |
|
||||
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
|
||||
@ -69,6 +69,7 @@
|
||||
| hcl | ✓ | | ✓ | `terraform-ls` |
|
||||
| heex | ✓ | ✓ | | `elixir-ls` |
|
||||
| hoon | ✓ | | | |
|
||||
| hocon | ✓ | | ✓ | |
|
||||
| hosts | ✓ | | | |
|
||||
| html | ✓ | | | `vscode-html-language-server` |
|
||||
| hurl | ✓ | | ✓ | |
|
||||
@ -97,7 +98,7 @@
|
||||
| log | ✓ | | | |
|
||||
| lpf | ✓ | | | |
|
||||
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
|
||||
| make | ✓ | | | |
|
||||
| make | ✓ | | ✓ | |
|
||||
| markdoc | ✓ | | | `markdoc-ls` |
|
||||
| markdown | ✓ | | | `marksman` |
|
||||
| markdown.inline | ✓ | | | |
|
||||
@ -123,12 +124,13 @@
|
||||
| pem | ✓ | | | |
|
||||
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
|
||||
| php | ✓ | ✓ | ✓ | `intelephense` |
|
||||
| pkl | ✓ | | ✓ | |
|
||||
| po | ✓ | ✓ | | |
|
||||
| pod | ✓ | | | |
|
||||
| ponylang | ✓ | ✓ | ✓ | |
|
||||
| prisma | ✓ | | | `prisma-language-server` |
|
||||
| prolog | | | | `swipl` |
|
||||
| protobuf | ✓ | | ✓ | `bufls`, `pb` |
|
||||
| protobuf | ✓ | ✓ | ✓ | `bufls`, `pb` |
|
||||
| prql | ✓ | | | |
|
||||
| purescript | ✓ | ✓ | | `purescript-language-server` |
|
||||
| python | ✓ | ✓ | ✓ | `pylsp` |
|
||||
@ -145,10 +147,10 @@
|
||||
| ruby | ✓ | ✓ | ✓ | `solargraph` |
|
||||
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
|
||||
| sage | ✓ | ✓ | | |
|
||||
| scala | ✓ | | ✓ | `metals` |
|
||||
| scala | ✓ | ✓ | ✓ | `metals` |
|
||||
| scheme | ✓ | | ✓ | |
|
||||
| scss | ✓ | | | `vscode-css-language-server` |
|
||||
| slint | ✓ | | ✓ | `slint-lsp` |
|
||||
| slint | ✓ | ✓ | ✓ | `slint-lsp` |
|
||||
| smali | ✓ | | ✓ | |
|
||||
| smithy | ✓ | | | `cs` |
|
||||
| sml | ✓ | | | |
|
||||
@ -162,6 +164,7 @@
|
||||
| swift | ✓ | | | `sourcekit-lsp` |
|
||||
| t32 | ✓ | | | |
|
||||
| tablegen | ✓ | ✓ | ✓ | |
|
||||
| tact | ✓ | ✓ | ✓ | |
|
||||
| task | ✓ | | | |
|
||||
| templ | ✓ | | | `templ` |
|
||||
| tfvars | ✓ | | ✓ | `terraform-ls` |
|
||||
@ -173,7 +176,7 @@
|
||||
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| typst | ✓ | | | `typst-lsp` |
|
||||
| ungrammar | ✓ | | | |
|
||||
| unison | ✓ | | | |
|
||||
| unison | ✓ | | ✓ | |
|
||||
| uxntal | ✓ | | | |
|
||||
| v | ✓ | ✓ | ✓ | `v-analyzer` |
|
||||
| vala | ✓ | | | `vala-language-server` |
|
||||
|
@ -315,6 +315,10 @@ ## Predicates
|
||||
The first argument (a capture) must/must not match the regex given in the
|
||||
second argument (a string).
|
||||
|
||||
- `#any-of?`/`#not-any-of?`:
|
||||
The first argument (a capture) must/must not be one of the other arguments
|
||||
(strings).
|
||||
|
||||
Additionally, we support some custom predicates for indent queries:
|
||||
|
||||
- `#not-kind-eq?`:
|
||||
@ -366,4 +370,4 @@ ### Scopes
|
||||
Then, on the closing brace, we encounter an outdent with a scope of "all", which
|
||||
means the first line is included, and the indent level is cancelled out on this
|
||||
line. (Note these scopes are the defaults for `@indent` and `@outdent`—they are
|
||||
written explicitly for demonstration.)
|
||||
written explicitly for demonstration.)
|
||||
|
@ -54,4 +54,7 @@ ## Predicates
|
||||
The first argument (a capture) must match the regex given in the
|
||||
second argument (a string).
|
||||
|
||||
- `#any-of?` (standard):
|
||||
The first argument (a capture) must be one of the other arguments (strings).
|
||||
|
||||
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
|
||||
|
@ -76,6 +76,15 @@ ### Arch Linux extra
|
||||
```sh
|
||||
sudo pacman -S helix
|
||||
```
|
||||
|
||||
> 💡 When installed from the `extra` repository, run Helix with `helix` instead of `hx`.
|
||||
>
|
||||
> For example:
|
||||
> ```sh
|
||||
> helix --health
|
||||
> ```
|
||||
> to check health
|
||||
|
||||
Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
|
||||
in the AUR, which builds the master branch.
|
||||
|
||||
@ -204,6 +213,8 @@ ## Building from source
|
||||
This command will create the `hx` executable and construct the tree-sitter
|
||||
grammars in the local `runtime` folder.
|
||||
|
||||
> 💡 If you do not want to fetch or build grammars, set an environment variable `HELIX_DISABLE_AUTO_GRAMMAR_BUILD`
|
||||
|
||||
> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
|
||||
> grammars with `hx --grammar fetch` and compile them with
|
||||
> `hx --grammar build`. This will install them in
|
||||
@ -214,12 +225,12 @@ ### Configuring Helix's runtime files
|
||||
|
||||
#### Linux and macOS
|
||||
|
||||
The **runtime** directory is one below the Helix source, so either set a
|
||||
The **runtime** directory is one below the Helix source, so either export a
|
||||
`HELIX_RUNTIME` environment variable to point to that directory and add it to
|
||||
your `~/.bashrc` or equivalent:
|
||||
|
||||
```sh
|
||||
HELIX_RUNTIME=~/src/helix/runtime
|
||||
export HELIX_RUNTIME=~/src/helix/runtime
|
||||
```
|
||||
|
||||
Or, create a symbolic link:
|
||||
|
@ -205,7 +205,7 @@ #### Goto mode
|
||||
| ----- | ----------- | ------- |
|
||||
| `g` | Go to line number `<n>` else start of file | `goto_file_start` |
|
||||
| `e` | Go to the end of the file | `goto_last_line` |
|
||||
| `f` | Go to files in the selection | `goto_file` |
|
||||
| `f` | Go to files in the selections | `goto_file` |
|
||||
| `h` | Go to the start of the line | `goto_line_start` |
|
||||
| `l` | Go to the end of the line | `goto_line_end` |
|
||||
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
|
||||
@ -253,8 +253,8 @@ #### Window mode
|
||||
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
|
||||
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
|
||||
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
|
||||
| `f` | Go to files in the selection in horizontal splits | `goto_file` |
|
||||
| `F` | Go to files in the selection in vertical splits | `goto_file` |
|
||||
| `f` | Go to files in the selections in horizontal splits | `goto_file` |
|
||||
| `F` | Go to files in the selections in vertical splits | `goto_file` |
|
||||
| `h`, `Ctrl-h`, `Left` | Move to left split | `jump_view_left` |
|
||||
| `j`, `Ctrl-j`, `Down` | Move to split below | `jump_view_down` |
|
||||
| `k`, `Ctrl-k`, `Up` | Move to split above | `jump_view_up` |
|
||||
|
BIN
contrib/helix-256p.ico
Normal file
After Width: | Height: | Size: 264 KiB |
12
grammars.nix
@ -28,7 +28,17 @@
|
||||
owner = builtins.elemAt match 0;
|
||||
repo = builtins.elemAt match 1;
|
||||
};
|
||||
gitGrammars = builtins.filter isGitGrammar languagesConfig.grammar;
|
||||
# If `use-grammars.only` is set, use only those grammars.
|
||||
# If `use-grammars.except` is set, use all other grammars.
|
||||
# Otherwise use all grammars.
|
||||
useGrammar = grammar:
|
||||
if languagesConfig?use-grammars.only then
|
||||
builtins.elem grammar.name languagesConfig.use-grammars.only
|
||||
else if languagesConfig?use-grammars.except then
|
||||
!(builtins.elem grammar.name languagesConfig.use-grammars.except)
|
||||
else true;
|
||||
grammarsToUse = builtins.filter useGrammar languagesConfig.grammar;
|
||||
gitGrammars = builtins.filter isGitGrammar grammarsToUse;
|
||||
buildGrammar = grammar: let
|
||||
gh = toGitHubFetcher grammar.source.git;
|
||||
sourceGit = builtins.fetchTree {
|
||||
|
@ -16,10 +16,11 @@ unicode-lines = ["ropey/unicode_lines"]
|
||||
integration = []
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
||||
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
|
||||
smallvec = "1.11"
|
||||
smallvec = "1.13"
|
||||
smartstring = "1.0.1"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
|
@ -551,7 +551,7 @@ fn query_indents<'a>(
|
||||
// The row/column position of the optional anchor in this query
|
||||
let mut anchor: Option<tree_sitter::Node> = None;
|
||||
for capture in m.captures {
|
||||
let capture_name = query.capture_names()[capture.index as usize].as_str();
|
||||
let capture_name = query.capture_names()[capture.index as usize];
|
||||
let capture_type = match capture_name {
|
||||
"indent" => IndentCaptureType::Indent,
|
||||
"indent.always" => IndentCaptureType::IndentAlways,
|
||||
|
@ -17,7 +17,6 @@
|
||||
pub mod match_brackets;
|
||||
pub mod movement;
|
||||
pub mod object;
|
||||
pub mod path;
|
||||
mod position;
|
||||
pub mod search;
|
||||
pub mod selection;
|
||||
|
@ -57,10 +57,10 @@ fn find_pair(
|
||||
pos_: usize,
|
||||
traverse_parents: bool,
|
||||
) -> Option<usize> {
|
||||
let tree = syntax.tree();
|
||||
let pos = doc.char_to_byte(pos_);
|
||||
|
||||
let mut node = tree.root_node().descendant_for_byte_range(pos, pos)?;
|
||||
let root = syntax.tree_for_byte_range(pos, pos + 1).root_node();
|
||||
let mut node = root.descendant_for_byte_range(pos, pos + 1)?;
|
||||
|
||||
loop {
|
||||
if node.is_named() {
|
||||
@ -118,7 +118,7 @@ fn find_pair(
|
||||
};
|
||||
node = parent;
|
||||
}
|
||||
let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
|
||||
let node = root.named_descendant_for_byte_range(pos, pos + 1)?;
|
||||
if node.child_count() != 0 {
|
||||
return None;
|
||||
}
|
||||
@ -141,7 +141,7 @@ fn find_pair(
|
||||
#[must_use]
|
||||
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
|
||||
// Don't do anything when the cursor is not on top of a bracket.
|
||||
let bracket = doc.char(cursor_pos);
|
||||
let bracket = doc.get_char(cursor_pos)?;
|
||||
if !is_valid_bracket(bracket) {
|
||||
return None;
|
||||
}
|
||||
@ -265,6 +265,12 @@ fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_matching_bracket_empty_file() {
|
||||
let actual = find_matching_bracket_plaintext("".into(), 0);
|
||||
assert_eq!(actual, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_matching_bracket_current_line_plaintext() {
|
||||
let assert = |input: &str, pos, expected| {
|
||||
|
@ -573,16 +573,11 @@ pub fn move_parent_node_end(
|
||||
dir: Direction,
|
||||
movement: Movement,
|
||||
) -> Selection {
|
||||
let tree = syntax.tree();
|
||||
|
||||
selection.transform(|range| {
|
||||
let start_from = text.char_to_byte(range.from());
|
||||
let start_to = text.char_to_byte(range.to());
|
||||
|
||||
let mut node = match tree
|
||||
.root_node()
|
||||
.named_descendant_for_byte_range(start_from, start_to)
|
||||
{
|
||||
let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) {
|
||||
Some(node) => node,
|
||||
None => {
|
||||
log::debug!(
|
||||
|
@ -263,7 +263,7 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
GotoDeclaration => "goto-declaration",
|
||||
GotoDefinition => "goto-definition",
|
||||
GotoTypeDefinition => "goto-type-definition",
|
||||
GotoReference => "goto-type-definition",
|
||||
GotoReference => "goto-reference",
|
||||
GotoImplementation => "goto-implementation",
|
||||
SignatureHelp => "signature-help",
|
||||
Hover => "hover",
|
||||
@ -1338,6 +1338,32 @@ pub fn highlight_iter<'a>(
|
||||
result
|
||||
}
|
||||
|
||||
pub fn tree_for_byte_range(&self, start: usize, end: usize) -> &Tree {
|
||||
let mut container_id = self.root;
|
||||
|
||||
for (layer_id, layer) in self.layers.iter() {
|
||||
if layer.depth > self.layers[container_id].depth
|
||||
&& layer.contains_byte_range(start, end)
|
||||
{
|
||||
container_id = layer_id;
|
||||
}
|
||||
}
|
||||
|
||||
self.layers[container_id].tree()
|
||||
}
|
||||
|
||||
pub fn named_descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
|
||||
self.tree_for_byte_range(start, end)
|
||||
.root_node()
|
||||
.named_descendant_for_byte_range(start, end)
|
||||
}
|
||||
|
||||
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
|
||||
self.tree_for_byte_range(start, end)
|
||||
.root_node()
|
||||
.descendant_for_byte_range(start, end)
|
||||
}
|
||||
|
||||
// Commenting
|
||||
// comment_strings_for_pos
|
||||
// is_commented
|
||||
@ -1434,6 +1460,32 @@ fn parse(&mut self, parser: &mut Parser, source: RopeSlice) -> Result<(), Error>
|
||||
self.tree = Some(tree);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the layer contains the given byte range.
|
||||
///
|
||||
/// If the layer has multiple ranges (i.e. combined injections), the
|
||||
/// given range is considered contained if it is within the start and
|
||||
/// end bytes of the first and last ranges **and** if the given range
|
||||
/// starts or ends within any of the layer's ranges.
|
||||
fn contains_byte_range(&self, start: usize, end: usize) -> bool {
|
||||
let layer_start = self
|
||||
.ranges
|
||||
.first()
|
||||
.expect("ranges should not be empty")
|
||||
.start_byte;
|
||||
let layer_end = self
|
||||
.ranges
|
||||
.last()
|
||||
.expect("ranges should not be empty")
|
||||
.end_byte;
|
||||
|
||||
layer_start <= start
|
||||
&& layer_end >= end
|
||||
&& self.ranges.iter().any(|range| {
|
||||
let byte_range = range.start_byte..range.end_byte;
|
||||
byte_range.contains(&start) || byte_range.contains(&end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_edits(
|
||||
@ -1727,7 +1779,7 @@ pub fn new(
|
||||
let mut local_scope_capture_index = None;
|
||||
for (i, name) in query.capture_names().iter().enumerate() {
|
||||
let i = Some(i as u32);
|
||||
match name.as_str() {
|
||||
match *name {
|
||||
"local.definition" => local_def_capture_index = i,
|
||||
"local.definition-value" => local_def_value_capture_index = i,
|
||||
"local.reference" => local_ref_capture_index = i,
|
||||
@ -1738,7 +1790,7 @@ pub fn new(
|
||||
|
||||
for (i, name) in injections_query.capture_names().iter().enumerate() {
|
||||
let i = Some(i as u32);
|
||||
match name.as_str() {
|
||||
match *name {
|
||||
"injection.content" => injection_content_capture_index = i,
|
||||
"injection.language" => injection_language_capture_index = i,
|
||||
"injection.filename" => injection_filename_capture_index = i,
|
||||
@ -1768,7 +1820,7 @@ pub fn new(
|
||||
}
|
||||
|
||||
/// Get a slice containing all of the highlight names used in the configuration.
|
||||
pub fn names(&self) -> &[String] {
|
||||
pub fn names(&self) -> &[&str] {
|
||||
self.query.capture_names()
|
||||
}
|
||||
|
||||
@ -1795,7 +1847,6 @@ pub fn configure(&self, recognized_names: &[String]) {
|
||||
let mut best_index = None;
|
||||
let mut best_match_len = 0;
|
||||
for (i, recognized_name) in recognized_names.iter().enumerate() {
|
||||
let recognized_name = recognized_name;
|
||||
let mut len = 0;
|
||||
let mut matches = true;
|
||||
for (i, part) in recognized_name.split('.').enumerate() {
|
||||
@ -2264,6 +2315,7 @@ fn next(&mut self) -> Option<Self::Item> {
|
||||
// highlighting patterns that are disabled for local variables.
|
||||
if definition_highlight.is_some() || reference_highlight.is_some() {
|
||||
while layer.config.non_local_variable_patterns[match_.pattern_index] {
|
||||
match_.remove();
|
||||
if let Some((next_match, next_capture_index)) = captures.peek() {
|
||||
let next_capture = next_match.captures[*next_capture_index];
|
||||
if next_capture.node == capture.node {
|
||||
|
@ -13,6 +13,7 @@ homepage.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
|
||||
anyhow = "1.0"
|
||||
@ -21,7 +22,6 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
|
||||
which = "5.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
fern = "0.6"
|
||||
|
@ -9,7 +9,6 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use anyhow::anyhow;
|
||||
pub use log::{error, info};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
@ -114,7 +113,7 @@ pub fn stdio(
|
||||
id: usize,
|
||||
) -> Result<(Self, UnboundedReceiver<Payload>)> {
|
||||
// Resolve path to the binary
|
||||
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
|
||||
let cmd = helix_stdx::env::which(cmd)?;
|
||||
|
||||
let process = Command::new(cmd)
|
||||
.args(args)
|
||||
|
@ -19,6 +19,8 @@ pub enum Error {
|
||||
#[error("server closed the stream")]
|
||||
StreamClosed,
|
||||
#[error(transparent)]
|
||||
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
@ -12,5 +12,18 @@ homepage.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
|
||||
parking_lot = { version = "0.12", features = ["send_guard"] }
|
||||
ahash = "0.8.3"
|
||||
hashbrown = "0.14.0"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
|
||||
# the event registry is essentially read only but must be an rwlock so we can
|
||||
# setup new events on initialization, hardware-lock-elision hugely benefits this case
|
||||
# as it essentially makes the lock entirely free as long as there is no writes
|
||||
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
|
||||
once_cell = "1.18"
|
||||
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
futures-executor = "0.3.28"
|
||||
|
||||
[features]
|
||||
integration_test = []
|
||||
|
19
helix-event/src/cancel.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use std::future::Future;
|
||||
|
||||
pub use oneshot::channel as cancelation;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
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> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel => {
|
||||
None
|
||||
}
|
||||
res = future => {
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
}
|
67
helix-event/src/debounce.rs
Normal file
@ -0,0 +1,67 @@
|
||||
//! Utilities for declaring an async (usually debounced) hook
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_executor::block_on;
|
||||
use tokio::sync::mpsc::{self, error::TrySendError, Sender};
|
||||
use tokio::time::Instant;
|
||||
|
||||
/// Async hooks provide a convenient framework for implementing (debounced)
|
||||
/// async event handlers. Most synchronous event hooks will likely need to
|
||||
/// debounce their events, coordinate multiple different hooks and potentially
|
||||
/// track some state. `AsyncHooks` facilitate these use cases by running as
|
||||
/// a background tokio task that waits for events (usually an enum) to be
|
||||
/// sent through a channel.
|
||||
pub trait AsyncHook: Sync + Send + 'static + Sized {
|
||||
type Event: Sync + Send + 'static;
|
||||
/// Called immediately whenever an event is received, this function can
|
||||
/// consume the event immediately or debounce it. In case of debouncing,
|
||||
/// it can either define a new debounce timeout or continue the current one
|
||||
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;
|
||||
|
||||
/// Called whenever the debounce timeline is reached
|
||||
fn finish_debounce(&mut self);
|
||||
|
||||
fn spawn(self) -> mpsc::Sender<Self::Event> {
|
||||
// the capacity doesn't matter too much here, unless the cpu is totally overwhelmed
|
||||
// the cap will never be reached since we always immediately drain the channel
|
||||
// so it should only be reached in case of total CPU overload.
|
||||
// However, a bounded channel is much more efficient so it's nice to use here
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
tokio::spawn(run(self, rx));
|
||||
tx
|
||||
}
|
||||
}
|
||||
|
||||
async fn run<Hook: AsyncHook>(mut hook: Hook, mut rx: mpsc::Receiver<Hook::Event>) {
|
||||
let mut deadline = None;
|
||||
loop {
|
||||
let event = match deadline {
|
||||
Some(deadline_) => {
|
||||
let res = tokio::time::timeout_at(deadline_, rx.recv()).await;
|
||||
match res {
|
||||
Ok(event) => event,
|
||||
Err(_) => {
|
||||
hook.finish_debounce();
|
||||
deadline = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => rx.recv().await,
|
||||
};
|
||||
let Some(event) = event else {
|
||||
break;
|
||||
};
|
||||
deadline = hook.handle_event(event, deadline);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_blocking<T>(tx: &Sender<T>, data: T) {
|
||||
// block_on has some overhead and in practice the channel should basically
|
||||
// never be full anyway so first try sending without blocking
|
||||
if let Err(TrySendError::Full(data)) = tx.try_send(data) {
|
||||
// set a timeout so that we just drop a message instead of freezing the editor in the worst case
|
||||
let _ = block_on(tx.send_timeout(data, Duration::from_millis(10)));
|
||||
}
|
||||
}
|
91
helix-event/src/hook.rs
Normal file
@ -0,0 +1,91 @@
|
||||
//! rust dynamic dispatch is extremely limited so we have to build our
|
||||
//! own vtable implementation. Otherwise implementing the event system would not be possible.
|
||||
//! A nice bonus of this approach is that we can optimize the vtable a bit more. Normally
|
||||
//! a dyn Trait fat pointer contains two pointers: A pointer to the data itself and a
|
||||
//! pointer to a global (static) vtable entry which itself contains multiple other pointers
|
||||
//! (the various functions of the trait, drop, size and align). That makes dynamic
|
||||
//! dispatch pretty slow (double pointer indirections). However, we only have a single function
|
||||
//! in the hook trait and don't need a drop implementation (event system is global anyway
|
||||
//! and never dropped) so we can just store the entire vtable inline.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::ptr::{self, NonNull};
|
||||
|
||||
use crate::Event;
|
||||
|
||||
/// Opaque handle type that represents an erased type parameter.
|
||||
///
|
||||
/// If extern types were stable, this could be implemented as `extern { pub type Opaque; }` but
|
||||
/// until then we can use this.
|
||||
///
|
||||
/// Care should be taken that we don't use a concrete instance of this. It should only be used
|
||||
/// through a reference, so we can maintain something else's lifetime.
|
||||
struct Opaque(());
|
||||
|
||||
pub(crate) struct ErasedHook {
|
||||
data: NonNull<Opaque>,
|
||||
call: unsafe fn(NonNull<Opaque>, NonNull<Opaque>, NonNull<Opaque>),
|
||||
}
|
||||
|
||||
impl ErasedHook {
|
||||
pub(crate) fn new_dynamic<H: Fn() -> Result<()> + 'static + Send + Sync>(
|
||||
hook: H,
|
||||
) -> ErasedHook {
|
||||
unsafe fn call<F: Fn() -> Result<()> + 'static + Send + Sync>(
|
||||
hook: NonNull<Opaque>,
|
||||
_event: NonNull<Opaque>,
|
||||
result: NonNull<Opaque>,
|
||||
) {
|
||||
let hook: NonNull<F> = hook.cast();
|
||||
let result: NonNull<Result<()>> = result.cast();
|
||||
let hook: &F = hook.as_ref();
|
||||
let res = hook();
|
||||
ptr::write(result.as_ptr(), res)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ErasedHook {
|
||||
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
|
||||
call: call::<H>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new<E: Event, F: Fn(&mut E) -> Result<()>>(hook: F) -> ErasedHook {
|
||||
unsafe fn call<E: Event, F: Fn(&mut E) -> Result<()>>(
|
||||
hook: NonNull<Opaque>,
|
||||
event: NonNull<Opaque>,
|
||||
result: NonNull<Opaque>,
|
||||
) {
|
||||
let hook: NonNull<F> = hook.cast();
|
||||
let mut event: NonNull<E> = event.cast();
|
||||
let result: NonNull<Result<()>> = result.cast();
|
||||
let hook: &F = hook.as_ref();
|
||||
let res = hook(event.as_mut());
|
||||
ptr::write(result.as_ptr(), res)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ErasedHook {
|
||||
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
|
||||
call: call::<E, F>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn call<E: Event>(&self, event: &mut E) -> Result<()> {
|
||||
let mut res = Ok(());
|
||||
|
||||
unsafe {
|
||||
(self.call)(
|
||||
self.data,
|
||||
NonNull::from(event).cast(),
|
||||
NonNull::from(&mut res).cast(),
|
||||
);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Sync for ErasedHook {}
|
||||
unsafe impl Send for ErasedHook {}
|
@ -1,8 +1,203 @@
|
||||
//! `helix-event` contains systems that allow (often async) communication between
|
||||
//! different editor components without strongly coupling them. Currently this
|
||||
//! crate only contains some smaller facilities but the intend is to add more
|
||||
//! functionality in the future ( like a generic hook system)
|
||||
//! different editor components without strongly coupling them. Specifically
|
||||
//! it allows defining synchronous hooks that run when certain editor events
|
||||
//! occur.
|
||||
//!
|
||||
//! The core of the event system are hook callbacks and the [`Event`] trait. A
|
||||
//! hook is essentially just a closure `Fn(event: &mut impl Event) -> Result<()>`
|
||||
//! that gets called every time an appropriate event is dispatched. The implementation
|
||||
//! details of the [`Event`] trait are considered private. The [`events`] macro is
|
||||
//! provided which automatically declares event types. Similarly the `register_hook`
|
||||
//! macro should be used to (safely) declare event hooks.
|
||||
//!
|
||||
//! Hooks run synchronously which can be advantageous since they can modify the
|
||||
//! current editor state right away (for example to immediately hide the completion
|
||||
//! popup). However, they can not contain their own state without locking since
|
||||
//! they only receive immutable references. For handler that want to track state, do
|
||||
//! expensive background computations or debouncing an [`AsyncHook`] is preferable.
|
||||
//! Async hooks are based around a channels that receive events specific to
|
||||
//! that `AsyncHook` (usually an enum). These events can be sent by synchronous
|
||||
//! hooks. Due to some limitations around tokio channels the [`send_blocking`]
|
||||
//! function exported in this crate should be used instead of the builtin
|
||||
//! `blocking_send`.
|
||||
//!
|
||||
//! In addition to the core event system, this crate contains some message queues
|
||||
//! that allow transfer of data back to the main event loop from async hooks and
|
||||
//! hooks that may not have access to all application data (for example in helix-view).
|
||||
//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and
|
||||
//! display status messages ([`status`]).
|
||||
//!
|
||||
//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the
|
||||
//! main loop (including access to the compositor). Ideally that queue will be moved
|
||||
//! 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 debounce::{send_blocking, AsyncHook};
|
||||
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
|
||||
pub use registry::Event;
|
||||
|
||||
mod cancel;
|
||||
mod debounce;
|
||||
mod hook;
|
||||
mod redraw;
|
||||
mod registry;
|
||||
#[doc(hidden)]
|
||||
pub mod runtime;
|
||||
pub mod status;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub fn register_event<E: Event + 'static>() {
|
||||
registry::with_mut(|registry| registry.register_event::<E>())
|
||||
}
|
||||
|
||||
/// Registers a hook that will be called when an event of type `E` is dispatched.
|
||||
/// This function should usually not be used directly, use the [`register_hook`]
|
||||
/// macro instead.
|
||||
///
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `hook` must be totally generic over all lifetime parameters of `E`. For
|
||||
/// example if `E` was a known type `Foo<'a, 'b>`, then the correct trait bound
|
||||
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)`, but there is no way to
|
||||
/// express that kind of constraint for a generic type with the Rust type system
|
||||
/// as of this writing.
|
||||
pub unsafe fn register_hook_raw<E: Event>(
|
||||
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
|
||||
) {
|
||||
registry::with_mut(|registry| registry.register_hook(hook))
|
||||
}
|
||||
|
||||
/// Register a hook solely by event name
|
||||
pub fn register_dynamic_hook(
|
||||
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
|
||||
id: &str,
|
||||
) -> Result<()> {
|
||||
registry::with_mut(|reg| reg.register_dynamic_hook(hook, id))
|
||||
}
|
||||
|
||||
pub fn dispatch(e: impl Event) {
|
||||
registry::with(|registry| registry.dispatch(e));
|
||||
}
|
||||
|
||||
/// Macro to declare events
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ``` no-compile
|
||||
/// events! {
|
||||
/// FileWrite(&Path)
|
||||
/// ViewScrolled{ view: View, new_pos: ViewOffset }
|
||||
/// DocumentChanged<'a> { old_doc: &'a Rope, doc: &'a mut Document, changes: &'a ChangeSet }
|
||||
/// }
|
||||
///
|
||||
/// fn init() {
|
||||
/// register_event::<FileWrite>();
|
||||
/// register_event::<ViewScrolled>();
|
||||
/// register_event::<DocumentChanged>();
|
||||
/// }
|
||||
///
|
||||
/// fn save(path: &Path, content: &str){
|
||||
/// std::fs::write(path, content);
|
||||
/// dispatch(FileWrite(path));
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! events {
|
||||
($name: ident<$($lt: lifetime),*> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
|
||||
pub struct $name<$($lt),*> { $(pub $data: $data_ty),* }
|
||||
unsafe impl<$($lt),*> $crate::Event for $name<$($lt),*> {
|
||||
const ID: &'static str = stringify!($name);
|
||||
const LIFETIMES: usize = $crate::events!(@sum $(1, $lt),*);
|
||||
type Static = $crate::events!(@replace_lt $name, $('static, $lt),*);
|
||||
}
|
||||
$crate::events!{ $($rem)* }
|
||||
};
|
||||
($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
|
||||
pub struct $name { $(pub $data: $data_ty),* }
|
||||
unsafe impl $crate::Event for $name {
|
||||
const ID: &'static str = stringify!($name);
|
||||
const LIFETIMES: usize = 0;
|
||||
type Static = Self;
|
||||
}
|
||||
$crate::events!{ $($rem)* }
|
||||
};
|
||||
() => {};
|
||||
(@replace_lt $name: ident, $($lt1: lifetime, $lt2: lifetime),* ) => {$name<$($lt1),*>};
|
||||
(@sum $($val: expr, $lt1: lifetime),* ) => {0 $(+ $val)*};
|
||||
}
|
||||
|
||||
/// Safely register statically typed event hooks
|
||||
#[macro_export]
|
||||
macro_rules! register_hook {
|
||||
// Safety: this is safe because we fully control the type of the event here and
|
||||
// ensure all lifetime arguments are fully generic and the correct number of lifetime arguments
|
||||
// is present
|
||||
(move |$event:ident: &mut $event_ty: ident<$($lt: lifetime),*>| $body: expr) => {
|
||||
let val = move |$event: &mut $event_ty<$($lt),*>| $body;
|
||||
unsafe {
|
||||
// Lifetimes are a bit of a pain. We want to allow events being
|
||||
// non-static. Lifetimes don't actually exist at runtime so its
|
||||
// fine to essentially transmute the lifetimes as long as we can
|
||||
// prove soundness. The hook must therefore accept any combination
|
||||
// of lifetimes. In other words fn(&'_ mut Event<'_, '_>) is ok
|
||||
// but examples like fn(&'_ mut Event<'_, 'static>) or fn<'a>(&'a
|
||||
// mut Event<'a, 'a>) are not. To make this safe we use a macro to
|
||||
// forbid the user from specifying lifetimes manually (all lifetimes
|
||||
// specified are always function generics and passed to the event so
|
||||
// lifetimes can't be used multiple times and using 'static causes a
|
||||
// syntax error).
|
||||
//
|
||||
// There is one soundness hole tough: Type Aliases allow
|
||||
// "accidentally" creating these problems. For example:
|
||||
//
|
||||
// type Event2 = Event<'static>.
|
||||
// type Event2<'a> = Event<'a, a>.
|
||||
//
|
||||
// These cases can be caught by counting the number of lifetimes
|
||||
// parameters at the parameter declaration site and then at the hook
|
||||
// declaration site. By asserting the number of lifetime parameters
|
||||
// are equal we can catch all bad type aliases under one assumption:
|
||||
// There are no unused lifetime parameters. Introducing a static
|
||||
// would reduce the number of arguments of the alias by one in the
|
||||
// above example Event2 has zero lifetime arguments while the original
|
||||
// event has one lifetime argument. Similar logic applies to using
|
||||
// a lifetime argument multiple times. The ASSERT below performs a
|
||||
// a compile time assertion to ensure exactly this property.
|
||||
//
|
||||
// With unused lifetime arguments it is still one way to cause unsound code:
|
||||
//
|
||||
// type Event2<'a, 'b> = Event<'a, 'a>;
|
||||
//
|
||||
// However, this case will always emit a compiler warning/cause CI
|
||||
// failures so a user would have to introduce #[allow(unused)] which
|
||||
// is easily caught in review (and a very theoretical case anyway).
|
||||
// If we want to be pedantic we can simply compile helix with
|
||||
// forbid(unused). All of this is just a safety net to prevent
|
||||
// very theoretical misuse. This won't come up in real code (and is
|
||||
// easily caught in review).
|
||||
#[allow(unused)]
|
||||
const ASSERT: () = {
|
||||
if <$event_ty as $crate::Event>::LIFETIMES != 0 + $crate::events!(@sum $(1, $lt),*){
|
||||
panic!("invalid type alias");
|
||||
}
|
||||
};
|
||||
$crate::register_hook_raw::<$crate::events!(@replace_lt $event_ty, $('static, $lt),*)>(val);
|
||||
}
|
||||
};
|
||||
(move |$event:ident: &mut $event_ty: ident| $body: expr) => {
|
||||
let val = move |$event: &mut $event_ty| $body;
|
||||
unsafe {
|
||||
#[allow(unused)]
|
||||
const ASSERT: () = {
|
||||
if <$event_ty as $crate::Event>::LIFETIMES != 0{
|
||||
panic!("invalid type alias");
|
||||
}
|
||||
};
|
||||
$crate::register_hook_raw::<$event_ty>(val);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -5,16 +5,20 @@
|
||||
use parking_lot::{RwLock, RwLockReadGuard};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
/// A `Notify` instance that can be used to (asynchronously) request
|
||||
/// the editor the render a new frame.
|
||||
static REDRAW_NOTIFY: Notify = Notify::const_new();
|
||||
use crate::runtime_local;
|
||||
|
||||
/// A `RwLock` that prevents the next frame from being
|
||||
/// drawn until an exclusive (write) lock can be acquired.
|
||||
/// This allows asynchsonous tasks to acquire `non-exclusive`
|
||||
/// locks (read) to prevent the next frame from being drawn
|
||||
/// until a certain computation has finished.
|
||||
static RENDER_LOCK: RwLock<()> = RwLock::new(());
|
||||
runtime_local! {
|
||||
/// A `Notify` instance that can be used to (asynchronously) request
|
||||
/// the editor to render a new frame.
|
||||
static REDRAW_NOTIFY: Notify = Notify::const_new();
|
||||
|
||||
/// A `RwLock` that prevents the next frame from being
|
||||
/// drawn until an exclusive (write) lock can be acquired.
|
||||
/// This allows asynchronous tasks to acquire `non-exclusive`
|
||||
/// locks (read) to prevent the next frame from being drawn
|
||||
/// until a certain computation has finished.
|
||||
static RENDER_LOCK: RwLock<()> = RwLock::new(());
|
||||
}
|
||||
|
||||
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
|
||||
|
||||
|
131
helix-event/src/registry.rs
Normal file
@ -0,0 +1,131 @@
|
||||
//! A global registry where events are registered and can be
|
||||
//! subscribed to by registering hooks. The registry identifies event
|
||||
//! types using their type name so multiple event with the same type name
|
||||
//! may not be registered (will cause a panic to ensure soundness)
|
||||
|
||||
use std::any::TypeId;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use hashbrown::hash_map::Entry;
|
||||
use hashbrown::HashMap;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::hook::ErasedHook;
|
||||
use crate::runtime_local;
|
||||
|
||||
pub struct Registry {
|
||||
events: HashMap<&'static str, TypeId, ahash::RandomState>,
|
||||
handlers: HashMap<&'static str, Vec<ErasedHook>, ahash::RandomState>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn register_event<E: Event + 'static>(&mut self) {
|
||||
let ty = TypeId::of::<E>();
|
||||
assert_eq!(ty, TypeId::of::<E::Static>());
|
||||
match self.events.entry(E::ID) {
|
||||
Entry::Occupied(entry) => {
|
||||
if entry.get() == &ty {
|
||||
// don't warn during tests to avoid log spam
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
panic!("Event {} was registered multiple times", E::ID);
|
||||
} else {
|
||||
panic!("Multiple events with ID {} were registered", E::ID);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(ent) => {
|
||||
ent.insert(ty);
|
||||
self.handlers.insert(E::ID, Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `hook` must be totally generic over all lifetime parameters of `E`. For
|
||||
/// example if `E` was a known type `Foo<'a, 'b> then the correct trait bound
|
||||
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)` but there is no way to
|
||||
/// express that kind of constraint for a generic type with the rust type system
|
||||
/// right now.
|
||||
pub unsafe fn register_hook<E: Event>(
|
||||
&mut self,
|
||||
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
|
||||
) {
|
||||
// ensure event type ids match so we can rely on them always matching
|
||||
let id = E::ID;
|
||||
let Some(&event_id) = self.events.get(id) else {
|
||||
panic!("Tried to register handler for unknown event {id}");
|
||||
};
|
||||
assert!(
|
||||
TypeId::of::<E::Static>() == event_id,
|
||||
"Tried to register invalid hook for event {id}"
|
||||
);
|
||||
let hook = ErasedHook::new(hook);
|
||||
self.handlers.get_mut(id).unwrap().push(hook);
|
||||
}
|
||||
|
||||
pub fn register_dynamic_hook(
|
||||
&mut self,
|
||||
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
|
||||
id: &str,
|
||||
) -> Result<()> {
|
||||
// ensure event type ids match so we can rely on them always matching
|
||||
if self.events.get(id).is_none() {
|
||||
bail!("Tried to register handler for unknown event {id}");
|
||||
};
|
||||
let hook = ErasedHook::new_dynamic(hook);
|
||||
self.handlers.get_mut(id).unwrap().push(hook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dispatch<E: Event>(&self, mut event: E) {
|
||||
let Some(hooks) = self.handlers.get(E::ID) else {
|
||||
log::error!("Dispatched unknown event {}", E::ID);
|
||||
return;
|
||||
};
|
||||
let event_id = self.events[E::ID];
|
||||
|
||||
assert_eq!(
|
||||
TypeId::of::<E::Static>(),
|
||||
event_id,
|
||||
"Tried to dispatch invalid event {}",
|
||||
E::ID
|
||||
);
|
||||
|
||||
for hook in hooks {
|
||||
// safety: event type is the same
|
||||
if let Err(err) = unsafe { hook.call(&mut event) } {
|
||||
log::error!("{} hook failed: {err:#?}", E::ID);
|
||||
crate::status::report_blocking(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime_local! {
|
||||
static REGISTRY: RwLock<Registry> = RwLock::new(Registry {
|
||||
// hardcoded random number is good enough here we don't care about DOS resistance
|
||||
// and avoids the additional complexity of `Option<Registry>`
|
||||
events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)),
|
||||
handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)),
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn with<T>(f: impl FnOnce(&Registry) -> T) -> T {
|
||||
f(®ISTRY.read())
|
||||
}
|
||||
|
||||
pub(crate) fn with_mut<T>(f: impl FnOnce(&mut Registry) -> T) -> T {
|
||||
f(&mut REGISTRY.write())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// The number of specified lifetimes and the static type *must* be correct.
|
||||
/// This is ensured automatically by the [`events`](crate::events)
|
||||
/// macro.
|
||||
pub unsafe trait Event: Sized {
|
||||
/// Globally unique (case sensitive) string that identifies this type.
|
||||
/// A good candidate is the events type name
|
||||
const ID: &'static str;
|
||||
const LIFETIMES: usize;
|
||||
type Static: Event + 'static;
|
||||
}
|
88
helix-event/src/runtime.rs
Normal file
@ -0,0 +1,88 @@
|
||||
//! The event system makes use of global to decouple different systems.
|
||||
//! However, this can cause problems for the integration test system because
|
||||
//! it runs multiple helix applications in parallel. Making the globals
|
||||
//! thread-local does not work because a applications can/does have multiple
|
||||
//! runtime threads. Instead this crate implements a similar notion to a thread
|
||||
//! local but instead of being local to a single thread, the statics are local to
|
||||
//! a single tokio-runtime. The implementation requires locking so it's not exactly efficient.
|
||||
//!
|
||||
//! Therefore this function is only enabled during integration tests and behaves like
|
||||
//! a normal static otherwise. I would prefer this module to be fully private and to only
|
||||
//! export the macro but the macro still need to construct these internals so it's marked
|
||||
//! `doc(hidden)` instead
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
pub struct RuntimeLocal<T: 'static> {
|
||||
/// inner API used in the macro, not part of public API
|
||||
#[doc(hidden)]
|
||||
pub __data: T,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
impl<T> Deref for RuntimeLocal<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.__data
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
#[macro_export]
|
||||
macro_rules! runtime_local {
|
||||
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
|
||||
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal {
|
||||
__data: $init
|
||||
};)*
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
pub struct RuntimeLocal<T: 'static> {
|
||||
data:
|
||||
parking_lot::RwLock<hashbrown::HashMap<tokio::runtime::Id, &'static T, ahash::RandomState>>,
|
||||
init: fn() -> T,
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
impl<T> RuntimeLocal<T> {
|
||||
/// inner API used in the macro, not part of public API
|
||||
#[doc(hidden)]
|
||||
pub const fn __new(init: fn() -> T) -> Self {
|
||||
Self {
|
||||
data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher(
|
||||
ahash::RandomState::with_seeds(423, 9978, 38322, 3280080),
|
||||
)),
|
||||
init,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
impl<T> Deref for RuntimeLocal<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
let id = tokio::runtime::Handle::current().id();
|
||||
let guard = self.data.read();
|
||||
match guard.get(&id) {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
drop(guard);
|
||||
let data = Box::leak(Box::new((self.init)()));
|
||||
let mut guard = self.data.write();
|
||||
guard.insert(id, data);
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
#[macro_export]
|
||||
macro_rules! runtime_local {
|
||||
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
|
||||
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal::__new(|| $init);)*
|
||||
};
|
||||
}
|
68
helix-event/src/status.rs
Normal file
@ -0,0 +1,68 @@
|
||||
//! A queue of async messages/errors that will be shown in the editor
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{runtime_local, send_blocking};
|
||||
use once_cell::sync::OnceCell;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
/// Describes the severity level of a [`StatusMessage`].
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub enum Severity {
|
||||
Hint,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct StatusMessage {
|
||||
pub severity: Severity,
|
||||
pub message: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for StatusMessage {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
StatusMessage {
|
||||
severity: Severity::Error,
|
||||
message: err.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for StatusMessage {
|
||||
fn from(msg: &'static str) -> Self {
|
||||
StatusMessage {
|
||||
severity: Severity::Info,
|
||||
message: msg.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime_local! {
|
||||
static MESSAGES: OnceCell<Sender<StatusMessage>> = OnceCell::new();
|
||||
}
|
||||
|
||||
pub async fn report(msg: impl Into<StatusMessage>) {
|
||||
// if the error channel overflows just ignore it
|
||||
let _ = MESSAGES
|
||||
.wait()
|
||||
.send_timeout(msg.into(), Duration::from_millis(10))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub fn report_blocking(msg: impl Into<StatusMessage>) {
|
||||
let messages = MESSAGES.wait();
|
||||
send_blocking(messages, msg.into())
|
||||
}
|
||||
|
||||
/// Must be called once during editor startup exactly once
|
||||
/// before any of the messages in this module can be used
|
||||
///
|
||||
/// # Panics
|
||||
/// If called multiple times
|
||||
pub fn setup() -> Receiver<StatusMessage> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(128);
|
||||
let _ = MESSAGES.set(tx);
|
||||
rx
|
||||
}
|
90
helix-event/src/test.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{dispatch, events, register_dynamic_hook, register_event, register_hook};
|
||||
#[test]
|
||||
fn smoke_test() {
|
||||
events! {
|
||||
Event1 { content: String }
|
||||
Event2 { content: usize }
|
||||
}
|
||||
register_event::<Event1>();
|
||||
register_event::<Event2>();
|
||||
|
||||
// setup hooks
|
||||
let res1: Arc<Mutex<String>> = Arc::default();
|
||||
let acc = Arc::clone(&res1);
|
||||
register_hook!(move |event: &mut Event1| {
|
||||
acc.lock().push_str(&event.content);
|
||||
Ok(())
|
||||
});
|
||||
let res2: Arc<AtomicUsize> = Arc::default();
|
||||
let acc = Arc::clone(&res2);
|
||||
register_hook!(move |event: &mut Event2| {
|
||||
acc.fetch_add(event.content, Ordering::Relaxed);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// triggers events
|
||||
let thread = std::thread::spawn(|| {
|
||||
for i in 0..1000 {
|
||||
dispatch(Event2 { content: i });
|
||||
}
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
dispatch(Event1 {
|
||||
content: "foo".to_owned(),
|
||||
});
|
||||
dispatch(Event2 { content: 42 });
|
||||
dispatch(Event1 {
|
||||
content: "bar".to_owned(),
|
||||
});
|
||||
dispatch(Event1 {
|
||||
content: "hello world".to_owned(),
|
||||
});
|
||||
thread.join().unwrap();
|
||||
|
||||
// check output
|
||||
assert_eq!(&**res1.lock(), "foobarhello world");
|
||||
assert_eq!(
|
||||
res2.load(Ordering::Relaxed),
|
||||
42 + (0..1000usize).sum::<usize>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic() {
|
||||
events! {
|
||||
Event3 {}
|
||||
Event4 { count: usize }
|
||||
};
|
||||
register_event::<Event3>();
|
||||
register_event::<Event4>();
|
||||
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let count1 = count.clone();
|
||||
let count2 = count.clone();
|
||||
register_dynamic_hook(
|
||||
move || {
|
||||
count1.fetch_add(2, Ordering::Relaxed);
|
||||
Ok(())
|
||||
},
|
||||
"Event3",
|
||||
)
|
||||
.unwrap();
|
||||
register_dynamic_hook(
|
||||
move || {
|
||||
count2.fetch_add(3, Ordering::Relaxed);
|
||||
Ok(())
|
||||
},
|
||||
"Event4",
|
||||
)
|
||||
.unwrap();
|
||||
dispatch(Event3 {});
|
||||
dispatch(Event4 { count: 0 });
|
||||
dispatch(Event3 {});
|
||||
assert_eq!(count.load(Ordering::Relaxed), 7)
|
||||
}
|
@ -15,6 +15,8 @@ name = "hx-loader"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
|
||||
anyhow = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.7"
|
||||
@ -22,14 +24,13 @@ etcetera = "0.8"
|
||||
tree-sitter.workspace = true
|
||||
once_cell = "1.19"
|
||||
log = "0.4"
|
||||
which = "5.0.0"
|
||||
|
||||
# TODO: these two should be on !wasm32 only
|
||||
|
||||
# cloning/compiling tree-sitter grammars
|
||||
cc = { version = "1" }
|
||||
threadpool = { version = "1.0" }
|
||||
tempfile = "3.8.1"
|
||||
tempfile = "3.9.0"
|
||||
dunce = "1.0.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
@ -86,10 +86,8 @@ pub fn get_language(name: &str) -> Result<Language> {
|
||||
}
|
||||
|
||||
fn ensure_git_is_available() -> Result<()> {
|
||||
match which::which("git") {
|
||||
Ok(_cmd) => Ok(()),
|
||||
Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")),
|
||||
}
|
||||
helix_stdx::env::which("git")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fetch_grammars() -> Result<()> {
|
||||
|
@ -1,14 +1,13 @@
|
||||
pub mod config;
|
||||
pub mod grammar;
|
||||
|
||||
use helix_stdx::{env::current_working_dir, path};
|
||||
|
||||
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
|
||||
|
||||
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
|
||||
static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
|
||||
once_cell::sync::Lazy::new(prioritize_runtime_dirs);
|
||||
|
||||
@ -16,31 +15,6 @@
|
||||
|
||||
static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
|
||||
|
||||
// Get the current working directory.
|
||||
// This information is managed internally as the call to std::env::current_dir
|
||||
// might fail if the cwd has been deleted.
|
||||
pub fn current_working_dir() -> PathBuf {
|
||||
if let Some(path) = &*CWD.read().unwrap() {
|
||||
return path.clone();
|
||||
}
|
||||
|
||||
let path = std::env::current_dir()
|
||||
.and_then(dunce::canonicalize)
|
||||
.expect("Couldn't determine current working directory");
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path.clone());
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
|
||||
let path = dunce::canonicalize(path)?;
|
||||
std::env::set_current_dir(&path)?;
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initialize_config_file(specified_file: Option<PathBuf>) {
|
||||
let config_file = specified_file.unwrap_or_else(default_config_file);
|
||||
ensure_parent_dir(&config_file);
|
||||
@ -79,7 +53,8 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
|
||||
rt_dirs.push(conf_rt_dir);
|
||||
|
||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||
rt_dirs.push(dir.into());
|
||||
let dir = path::expand_tilde(dir);
|
||||
rt_dirs.push(path::normalize(dir));
|
||||
}
|
||||
|
||||
// If this variable is set during build time, it will always be included
|
||||
@ -280,21 +255,9 @@ fn ensure_parent_dir(path: &Path) {
|
||||
mod merge_toml_tests {
|
||||
use std::str;
|
||||
|
||||
use super::{current_working_dir, merge_toml_values, set_current_working_dir};
|
||||
use super::merge_toml_values;
|
||||
use toml::Value;
|
||||
|
||||
#[test]
|
||||
fn current_dir_is_set() {
|
||||
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
|
||||
let cwd = current_working_dir();
|
||||
assert_ne!(cwd, new_path);
|
||||
|
||||
set_current_working_dir(&new_path).expect("Couldn't set new path");
|
||||
|
||||
let cwd = current_working_dir();
|
||||
assert_eq!(cwd, new_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_toml_map_merges() {
|
||||
const USER: &str = r#"
|
||||
|
@ -13,6 +13,7 @@ homepage.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-parsec = { path = "../helix-parsec" }
|
||||
@ -28,5 +29,4 @@ serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
which = "5.0.0"
|
||||
parking_lot = "0.12.1"
|
||||
|
@ -1,27 +1,29 @@
|
||||
use crate::{
|
||||
file_operations::FileOperationsInterest,
|
||||
find_lsp_workspace, jsonrpc,
|
||||
transport::{Payload, Transport},
|
||||
Call, Error, OffsetEncoding, Result,
|
||||
};
|
||||
|
||||
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
|
||||
use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope};
|
||||
use helix_loader::{self, VERSION_AND_GIT_HASH};
|
||||
use helix_stdx::path;
|
||||
use lsp::{
|
||||
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
|
||||
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
|
||||
WorkspaceFoldersChangeEvent,
|
||||
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url,
|
||||
WorkspaceFolder, WorkspaceFoldersChangeEvent,
|
||||
};
|
||||
use lsp_types as lsp;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::process::Stdio;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{future::Future, sync::OnceLock};
|
||||
use std::{path::Path, process::Stdio};
|
||||
use tokio::{
|
||||
io::{BufReader, BufWriter},
|
||||
process::{Child, Command},
|
||||
@ -50,6 +52,7 @@ pub struct Client {
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
request_counter: AtomicU64,
|
||||
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
|
||||
pub(crate) file_operation_interest: OnceLock<FileOperationsInterest>,
|
||||
config: Option<Value>,
|
||||
root_path: std::path::PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
@ -68,7 +71,7 @@ pub fn try_add_doc(
|
||||
may_support_workspace: bool,
|
||||
) -> bool {
|
||||
let (workspace, workspace_is_cwd) = find_workspace();
|
||||
let workspace = path::get_normalized_path(&workspace);
|
||||
let workspace = path::normalize(workspace);
|
||||
let root = find_lsp_workspace(
|
||||
doc_path
|
||||
.and_then(|x| x.parent().and_then(|x| x.to_str()))
|
||||
@ -182,7 +185,7 @@ pub fn start(
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
||||
// Resolve path to the binary
|
||||
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
|
||||
let cmd = helix_stdx::env::which(cmd)?;
|
||||
|
||||
let process = Command::new(cmd)
|
||||
.envs(server_environment)
|
||||
@ -204,7 +207,7 @@ pub fn start(
|
||||
let (server_rx, server_tx, initialize_notify) =
|
||||
Transport::start(reader, writer, stderr, id, name.clone());
|
||||
let (workspace, workspace_is_cwd) = find_workspace();
|
||||
let workspace = path::get_normalized_path(&workspace);
|
||||
let workspace = path::normalize(workspace);
|
||||
let root = find_lsp_workspace(
|
||||
doc_path
|
||||
.and_then(|x| x.parent().and_then(|x| x.to_str()))
|
||||
@ -232,6 +235,7 @@ pub fn start(
|
||||
server_tx,
|
||||
request_counter: AtomicU64::new(0),
|
||||
capabilities: OnceCell::new(),
|
||||
file_operation_interest: OnceLock::new(),
|
||||
config,
|
||||
req_timeout,
|
||||
root_path,
|
||||
@ -277,6 +281,11 @@ pub fn capabilities(&self) -> &lsp::ServerCapabilities {
|
||||
.expect("language server not yet initialized!")
|
||||
}
|
||||
|
||||
pub(crate) fn file_operations_intests(&self) -> &FileOperationsInterest {
|
||||
self.file_operation_interest
|
||||
.get_or_init(|| FileOperationsInterest::new(self.capabilities()))
|
||||
}
|
||||
|
||||
/// Client has to be initialized otherwise this function panics
|
||||
#[inline]
|
||||
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
|
||||
@ -716,27 +725,27 @@ pub fn did_change_workspace(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prepare_file_rename(
|
||||
pub fn will_rename(
|
||||
&self,
|
||||
old_uri: &lsp::Url,
|
||||
new_uri: &lsp::Url,
|
||||
old_path: &Path,
|
||||
new_path: &Path,
|
||||
is_dir: bool,
|
||||
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support willRename feature
|
||||
match &capabilities.workspace {
|
||||
Some(workspace) => match &workspace.file_operations {
|
||||
Some(op) => {
|
||||
op.will_rename.as_ref()?;
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
let capabilities = self.file_operations_intests();
|
||||
if !capabilities.will_rename.has_interest(old_path, is_dir) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let url_from_path = |path| {
|
||||
let url = if is_dir {
|
||||
Url::from_directory_path(path)
|
||||
} else {
|
||||
Url::from_file_path(path)
|
||||
};
|
||||
Some(url.ok()?.to_string())
|
||||
};
|
||||
let files = vec![lsp::FileRename {
|
||||
old_uri: old_uri.to_string(),
|
||||
new_uri: new_uri.to_string(),
|
||||
old_uri: url_from_path(old_path)?,
|
||||
new_uri: url_from_path(new_path)?,
|
||||
}];
|
||||
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
|
||||
lsp::RenameFilesParams { files },
|
||||
@ -750,27 +759,28 @@ pub fn prepare_file_rename(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn did_file_rename(
|
||||
pub fn did_rename(
|
||||
&self,
|
||||
old_uri: &lsp::Url,
|
||||
new_uri: &lsp::Url,
|
||||
old_path: &Path,
|
||||
new_path: &Path,
|
||||
is_dir: bool,
|
||||
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support DidRename feature
|
||||
match &capabilities.workspace {
|
||||
Some(workspace) => match &workspace.file_operations {
|
||||
Some(op) => {
|
||||
op.did_rename.as_ref()?;
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
let capabilities = self.file_operations_intests();
|
||||
if !capabilities.did_rename.has_interest(new_path, is_dir) {
|
||||
return None;
|
||||
}
|
||||
let url_from_path = |path| {
|
||||
let url = if is_dir {
|
||||
Url::from_directory_path(path)
|
||||
} else {
|
||||
Url::from_file_path(path)
|
||||
};
|
||||
Some(url.ok()?.to_string())
|
||||
};
|
||||
|
||||
let files = vec![lsp::FileRename {
|
||||
old_uri: old_uri.to_string(),
|
||||
new_uri: new_uri.to_string(),
|
||||
old_uri: url_from_path(old_path)?,
|
||||
new_uri: url_from_path(new_path)?,
|
||||
}];
|
||||
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
|
||||
}
|
||||
@ -998,6 +1008,7 @@ pub fn completion(
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
context: lsp::CompletionContext,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
@ -1009,13 +1020,12 @@ pub fn completion(
|
||||
text_document,
|
||||
position,
|
||||
},
|
||||
context: Some(context),
|
||||
// TODO: support these tokens by async receiving and updating the choice list
|
||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||
partial_result_params: lsp::PartialResultParams {
|
||||
partial_result_token: None,
|
||||
},
|
||||
context: None,
|
||||
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
|
||||
};
|
||||
|
||||
Some(self.call::<lsp::request::Completion>(params))
|
||||
@ -1062,7 +1072,7 @@ pub fn text_document_signature_help(
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
work_done_token: Option<lsp::ProgressToken>,
|
||||
) -> Option<impl Future<Output = Result<Value>>> {
|
||||
) -> Option<impl Future<Output = Result<Option<SignatureHelp>>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support signature help.
|
||||
@ -1078,7 +1088,8 @@ pub fn text_document_signature_help(
|
||||
// lsp::SignatureHelpContext
|
||||
};
|
||||
|
||||
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
|
||||
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
|
||||
Some(async move { Ok(serde_json::from_value(res.await?)?) })
|
||||
}
|
||||
|
||||
pub fn text_document_range_inlay_hints(
|
||||
|
105
helix-lsp/src/file_operations.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use std::path::Path;
|
||||
|
||||
use globset::{GlobBuilder, GlobSet};
|
||||
|
||||
use crate::lsp;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct FileOperationFilter {
|
||||
dir_globs: GlobSet,
|
||||
file_globs: GlobSet,
|
||||
}
|
||||
|
||||
impl FileOperationFilter {
|
||||
fn new(capability: Option<&lsp::FileOperationRegistrationOptions>) -> FileOperationFilter {
|
||||
let Some(cap) = capability else {
|
||||
return FileOperationFilter::default();
|
||||
};
|
||||
let mut dir_globs = GlobSet::builder();
|
||||
let mut file_globs = GlobSet::builder();
|
||||
for filter in &cap.filters {
|
||||
// TODO: support other url schemes
|
||||
let is_non_file_schema = filter
|
||||
.scheme
|
||||
.as_ref()
|
||||
.is_some_and(|schema| schema != "file");
|
||||
if is_non_file_schema {
|
||||
continue;
|
||||
}
|
||||
let ignore_case = filter
|
||||
.pattern
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|opts| opts.ignore_case)
|
||||
.unwrap_or(false);
|
||||
let mut glob_builder = GlobBuilder::new(&filter.pattern.glob);
|
||||
glob_builder.case_insensitive(!ignore_case);
|
||||
let glob = match glob_builder.build() {
|
||||
Ok(glob) => glob,
|
||||
Err(err) => {
|
||||
log::error!("invalid glob send by LS: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match filter.pattern.matches {
|
||||
Some(lsp::FileOperationPatternKind::File) => {
|
||||
file_globs.add(glob);
|
||||
}
|
||||
Some(lsp::FileOperationPatternKind::Folder) => {
|
||||
dir_globs.add(glob);
|
||||
}
|
||||
None => {
|
||||
file_globs.add(glob.clone());
|
||||
dir_globs.add(glob);
|
||||
}
|
||||
};
|
||||
}
|
||||
let file_globs = file_globs.build().unwrap_or_else(|err| {
|
||||
log::error!("invalid globs send by LS: {err}");
|
||||
GlobSet::empty()
|
||||
});
|
||||
let dir_globs = dir_globs.build().unwrap_or_else(|err| {
|
||||
log::error!("invalid globs send by LS: {err}");
|
||||
GlobSet::empty()
|
||||
});
|
||||
FileOperationFilter {
|
||||
dir_globs,
|
||||
file_globs,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_interest(&self, path: &Path, is_dir: bool) -> bool {
|
||||
if is_dir {
|
||||
self.dir_globs.is_match(path)
|
||||
} else {
|
||||
self.file_globs.is_match(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct FileOperationsInterest {
|
||||
// TODO: support other notifications
|
||||
// did_create: FileOperationFilter,
|
||||
// will_create: FileOperationFilter,
|
||||
pub did_rename: FileOperationFilter,
|
||||
pub will_rename: FileOperationFilter,
|
||||
// did_delete: FileOperationFilter,
|
||||
// will_delete: FileOperationFilter,
|
||||
}
|
||||
|
||||
impl FileOperationsInterest {
|
||||
pub fn new(capabilities: &lsp::ServerCapabilities) -> FileOperationsInterest {
|
||||
let capabilities = capabilities
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|capabilities| capabilities.file_operations.as_ref());
|
||||
let Some(capabilities) = capabilities else {
|
||||
return FileOperationsInterest::default();
|
||||
};
|
||||
FileOperationsInterest {
|
||||
did_rename: FileOperationFilter::new(capabilities.did_rename.as_ref()),
|
||||
will_rename: FileOperationFilter::new(capabilities.will_rename.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
mod client;
|
||||
pub mod file_event;
|
||||
mod file_operations;
|
||||
pub mod jsonrpc;
|
||||
pub mod snippet;
|
||||
mod transport;
|
||||
@ -11,10 +12,10 @@
|
||||
pub use lsp_types as lsp;
|
||||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use helix_core::{
|
||||
path,
|
||||
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
|
||||
use helix_core::syntax::{
|
||||
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
|
||||
};
|
||||
use helix_stdx::path;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
use std::{
|
||||
@ -44,6 +45,8 @@ pub enum Error {
|
||||
#[error("Unhandled")]
|
||||
Unhandled,
|
||||
#[error(transparent)]
|
||||
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
@ -549,6 +552,7 @@ pub enum MethodCall {
|
||||
WorkspaceConfiguration(lsp::ConfigurationParams),
|
||||
RegisterCapability(lsp::RegistrationParams),
|
||||
UnregisterCapability(lsp::UnregistrationParams),
|
||||
ShowDocument(lsp::ShowDocumentParams),
|
||||
}
|
||||
|
||||
impl MethodCall {
|
||||
@ -576,6 +580,10 @@ pub fn parse(method: &str, params: jsonrpc::Params) -> Result<MethodCall> {
|
||||
let params: lsp::UnregistrationParams = params.parse()?;
|
||||
Self::UnregisterCapability(params)
|
||||
}
|
||||
lsp::request::ShowDocument::METHOD => {
|
||||
let params: lsp::ShowDocumentParams = params.parse()?;
|
||||
Self::ShowDocument(params)
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::Unhandled);
|
||||
}
|
||||
@ -915,10 +923,17 @@ fn start_client(
|
||||
}
|
||||
|
||||
// next up, notify<initialized>
|
||||
_client
|
||||
let notification_result = _client
|
||||
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
|
||||
if let Err(e) = notification_result {
|
||||
log::error!(
|
||||
"failed to notify language server of its initialization: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
initialize_notify.notify_one();
|
||||
});
|
||||
@ -946,10 +961,10 @@ pub fn find_lsp_workspace(
|
||||
let mut file = if file.is_absolute() {
|
||||
file.to_path_buf()
|
||||
} else {
|
||||
let current_dir = helix_loader::current_working_dir();
|
||||
let current_dir = helix_stdx::env::current_working_dir();
|
||||
current_dir.join(file)
|
||||
};
|
||||
file = path::get_normalized_path(&file);
|
||||
file = path::normalize(&file);
|
||||
|
||||
if !file.starts_with(workspace) {
|
||||
return None;
|
||||
@ -966,7 +981,7 @@ pub fn find_lsp_workspace(
|
||||
|
||||
if root_dirs
|
||||
.iter()
|
||||
.any(|root_dir| path::get_normalized_path(&workspace.join(root_dir)) == ancestor)
|
||||
.any(|root_dir| path::normalize(workspace.join(root_dir)) == ancestor)
|
||||
{
|
||||
// if the worskapce is the cwd do not search any higher for workspaces
|
||||
// but specify
|
||||
|
@ -270,7 +270,14 @@ async fn recv(
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(Error::StreamClosed) => {
|
||||
Err(err) => {
|
||||
if !matches!(err, Error::StreamClosed) {
|
||||
error!(
|
||||
"Exiting {} after unexpected error: {err:?}",
|
||||
&transport.name
|
||||
);
|
||||
}
|
||||
|
||||
// Close any outstanding requests.
|
||||
for (id, tx) in transport.pending_requests.lock().await.drain() {
|
||||
match tx.send(Err(Error::StreamClosed)).await {
|
||||
@ -300,10 +307,6 @@ async fn recv(
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{} err: <- {err:?}", transport.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
helix-stdx/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "helix-stdx"
|
||||
description = "Standard library extensions"
|
||||
include = ["src/**/*", "README.md"]
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
categories.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dunce = "1.0"
|
||||
etcetera = "0.8"
|
||||
ropey = { version = "1.6.1", default-features = false }
|
||||
which = "6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.9"
|
80
helix-stdx/src/env.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::RwLock,
|
||||
};
|
||||
|
||||
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
|
||||
// Get the current working directory.
|
||||
// This information is managed internally as the call to std::env::current_dir
|
||||
// might fail if the cwd has been deleted.
|
||||
pub fn current_working_dir() -> PathBuf {
|
||||
if let Some(path) = &*CWD.read().unwrap() {
|
||||
return path.clone();
|
||||
}
|
||||
|
||||
let path = std::env::current_dir()
|
||||
.map(crate::path::normalize)
|
||||
.expect("Couldn't determine current working directory");
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path.clone());
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
|
||||
let path = crate::path::canonicalize(path);
|
||||
std::env::set_current_dir(&path)?;
|
||||
let mut cwd = CWD.write().unwrap();
|
||||
*cwd = Some(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn env_var_is_set(env_var_name: &str) -> bool {
|
||||
std::env::var_os(env_var_name).is_some()
|
||||
}
|
||||
|
||||
pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
|
||||
which::which(binary_name).is_ok()
|
||||
}
|
||||
|
||||
pub fn which<T: AsRef<OsStr>>(
|
||||
binary_name: T,
|
||||
) -> Result<std::path::PathBuf, ExecutableNotFoundError> {
|
||||
which::which(binary_name.as_ref()).map_err(|err| ExecutableNotFoundError {
|
||||
command: binary_name.as_ref().to_string_lossy().into_owned(),
|
||||
inner: err,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecutableNotFoundError {
|
||||
command: String,
|
||||
inner: which::Error,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExecutableNotFoundError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "command '{}' not found: {}", self.command, self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ExecutableNotFoundError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{current_working_dir, set_current_working_dir};
|
||||
|
||||
#[test]
|
||||
fn current_dir_is_set() {
|
||||
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
|
||||
let cwd = current_working_dir();
|
||||
assert_ne!(cwd, new_path);
|
||||
|
||||
set_current_working_dir(&new_path).expect("Couldn't set new path");
|
||||
|
||||
let cwd = current_working_dir();
|
||||
assert_eq!(cwd, new_path);
|
||||
}
|
||||
}
|
3
helix-stdx/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod env;
|
||||
pub mod path;
|
||||
pub mod rope;
|
@ -1,6 +1,9 @@
|
||||
use etcetera::home_dir;
|
||||
pub use etcetera::home_dir;
|
||||
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use crate::env::current_working_dir;
|
||||
|
||||
/// Replaces users home directory from `path` with tilde `~` if the directory
|
||||
/// is available, otherwise returns the path unchanged.
|
||||
pub fn fold_home_dir(path: &Path) -> PathBuf {
|
||||
@ -16,7 +19,8 @@ pub fn fold_home_dir(path: &Path) -> PathBuf {
|
||||
/// Expands tilde `~` into users home directory if available, otherwise returns the path
|
||||
/// unchanged. The tilde will only be expanded when present as the first component of the path
|
||||
/// and only slash follows it.
|
||||
pub fn expand_tilde(path: &Path) -> PathBuf {
|
||||
pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = path.as_ref();
|
||||
let mut components = path.components().peekable();
|
||||
if let Some(Component::Normal(c)) = components.peek() {
|
||||
if c == &"~" {
|
||||
@ -30,32 +34,11 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Normalize a path, removing things like `.` and `..`.
|
||||
///
|
||||
/// CAUTION: This does not resolve symlinks (unlike
|
||||
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
|
||||
/// behavior at times. This should be used carefully. Unfortunately,
|
||||
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
|
||||
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
|
||||
/// needs to improve on.
|
||||
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
|
||||
pub fn get_normalized_path(path: &Path) -> PathBuf {
|
||||
// normalization strategy is to canonicalize first ancestor path that exists (i.e., canonicalize as much as possible),
|
||||
// then run handrolled normalization on the non-existent remainder
|
||||
let (base, path) = path
|
||||
.ancestors()
|
||||
.find_map(|base| {
|
||||
let canonicalized_base = dunce::canonicalize(base).ok()?;
|
||||
let remainder = path.strip_prefix(base).ok()?.into();
|
||||
Some((canonicalized_base, remainder))
|
||||
})
|
||||
.unwrap_or_else(|| (PathBuf::new(), PathBuf::from(path)));
|
||||
|
||||
if path.as_os_str().is_empty() {
|
||||
return base;
|
||||
}
|
||||
|
||||
let mut components = path.components().peekable();
|
||||
/// 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.
|
||||
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() {
|
||||
components.next();
|
||||
PathBuf::from(c.as_os_str())
|
||||
@ -70,37 +53,77 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
|
||||
ret.push(component.as_os_str());
|
||||
}
|
||||
Component::CurDir => {}
|
||||
#[cfg(not(windows))]
|
||||
Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Component::ParentDir => {
|
||||
if let Some(head) = ret.components().next_back() {
|
||||
match head {
|
||||
Component::Prefix(_) | Component::RootDir => {}
|
||||
Component::CurDir => unreachable!(),
|
||||
// If we left previous component as ".." it means we met a symlink before and we can't pop path.
|
||||
Component::ParentDir => {
|
||||
ret.push("..");
|
||||
}
|
||||
Component::Normal(_) => {
|
||||
if ret.is_symlink() {
|
||||
ret.push("..");
|
||||
} else {
|
||||
ret.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Component::Normal(c) => 'normal: {
|
||||
use std::fs::canonicalize;
|
||||
|
||||
let new_path = ret.join(c);
|
||||
if new_path.is_symlink() {
|
||||
ret = new_path;
|
||||
break 'normal;
|
||||
}
|
||||
let (can_new, can_old) = (canonicalize(&new_path), canonicalize(&ret));
|
||||
match (can_new, can_old) {
|
||||
(Ok(can_new), Ok(can_old)) => {
|
||||
let striped = can_new.strip_prefix(can_old);
|
||||
ret.push(striped.unwrap_or_else(|_| c.as_ref()));
|
||||
}
|
||||
_ => ret.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
base.join(ret)
|
||||
dunce::simplified(&ret).to_path_buf()
|
||||
}
|
||||
|
||||
/// Returns the canonical, absolute form of a path with all intermediate components normalized.
|
||||
///
|
||||
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
|
||||
/// This function is used instead of [`std::fs::canonicalize`] because we don't want to verify
|
||||
/// here if the path exists, just normalize it's components.
|
||||
pub fn get_canonicalized_path(path: &Path) -> PathBuf {
|
||||
pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = expand_tilde(path);
|
||||
let path = if path.is_relative() {
|
||||
helix_loader::current_working_dir().join(path)
|
||||
current_working_dir().join(path)
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
get_normalized_path(path.as_path())
|
||||
normalize(path)
|
||||
}
|
||||
|
||||
pub fn get_relative_path(path: &Path) -> PathBuf {
|
||||
let path = PathBuf::from(path);
|
||||
pub fn get_relative_path(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = PathBuf::from(path.as_ref());
|
||||
let path = if path.is_absolute() {
|
||||
let cwdir = get_normalized_path(&helix_loader::current_working_dir());
|
||||
get_normalized_path(&path)
|
||||
let cwdir = normalize(current_working_dir());
|
||||
normalize(&path)
|
||||
.strip_prefix(cwdir)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(path)
|
||||
@ -116,8 +139,8 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
|
||||
/// Also strip the current working directory from the beginning of the path.
|
||||
/// Note that this function does not check if the truncated path is unambiguous.
|
||||
///
|
||||
/// ```
|
||||
/// use helix_core::path::get_truncated_path;
|
||||
/// ```
|
||||
/// use helix_stdx::path::get_truncated_path;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// assert_eq!(
|
||||
@ -139,8 +162,8 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
|
||||
/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
|
||||
/// ```
|
||||
///
|
||||
pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
|
||||
let cwd = helix_loader::current_working_dir();
|
||||
pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
|
||||
let cwd = current_working_dir();
|
||||
let path = path
|
||||
.as_ref()
|
||||
.strip_prefix(cwd)
|
26
helix-stdx/src/rope.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use ropey::RopeSlice;
|
||||
|
||||
pub trait RopeSliceExt: Sized {
|
||||
fn ends_with(self, text: &str) -> bool;
|
||||
fn starts_with(self, text: &str) -> bool;
|
||||
}
|
||||
|
||||
impl RopeSliceExt for RopeSlice<'_> {
|
||||
fn ends_with(self, text: &str) -> bool {
|
||||
let len = self.len_bytes();
|
||||
if len < text.len() {
|
||||
return false;
|
||||
}
|
||||
self.get_byte_slice(len - text.len()..)
|
||||
.map_or(false, |end| end == text)
|
||||
}
|
||||
|
||||
fn starts_with(self, text: &str) -> bool {
|
||||
let len = self.len_bytes();
|
||||
if len < text.len() {
|
||||
return false;
|
||||
}
|
||||
self.get_byte_slice(..len - text.len())
|
||||
.map_or(false, |start| start == text)
|
||||
}
|
||||
}
|
124
helix-stdx/tests/path.rs
Normal file
@ -0,0 +1,124 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
error::Error,
|
||||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
|
||||
use helix_stdx::path;
|
||||
use tempfile::Builder;
|
||||
|
||||
// Paths on Windows are almost always case-insensitive.
|
||||
// Normalization should return the original path.
|
||||
// E.g. mkdir `CaSe`, normalize(`case`) = `CaSe`.
|
||||
#[test]
|
||||
fn test_case_folding_windows() -> Result<(), Box<dyn Error>> {
|
||||
// tmp/root/case
|
||||
let tmp_prefix = std::env::temp_dir();
|
||||
set_current_dir(&tmp_prefix)?;
|
||||
|
||||
let root = Builder::new().prefix("root-").tempdir()?;
|
||||
let case = Builder::new().prefix("CaSe-").tempdir_in(&root)?;
|
||||
|
||||
let root_without_prefix = root.path().strip_prefix(&tmp_prefix)?;
|
||||
|
||||
let lowercase_case = format!(
|
||||
"case-{}",
|
||||
case.path()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.split_at(5)
|
||||
.1
|
||||
);
|
||||
let test_path = root_without_prefix.join(lowercase_case);
|
||||
assert_eq!(
|
||||
path::normalize(&test_path),
|
||||
case.path().strip_prefix(&tmp_prefix)?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path() -> Result<(), Box<dyn Error>> {
|
||||
/*
|
||||
tmp/root/
|
||||
├── link -> dir1/orig_file
|
||||
├── dir1/
|
||||
│ └── orig_file
|
||||
└── dir2/
|
||||
└── dir_link -> ../dir1/
|
||||
*/
|
||||
|
||||
let tmp_prefix = std::env::temp_dir();
|
||||
set_current_dir(&tmp_prefix)?;
|
||||
|
||||
// Create a tree structure as shown above
|
||||
let root = Builder::new().prefix("root-").tempdir()?;
|
||||
let dir1 = Builder::new().prefix("dir1-").tempdir_in(&root)?;
|
||||
let orig_file = Builder::new().prefix("orig_file-").tempfile_in(&dir1)?;
|
||||
let dir2 = Builder::new().prefix("dir2-").tempdir_in(&root)?;
|
||||
|
||||
// Create path and delete existing file
|
||||
let dir_link = Builder::new()
|
||||
.prefix("dir_link-")
|
||||
.tempfile_in(&dir2)?
|
||||
.path()
|
||||
.to_owned();
|
||||
let link = Builder::new()
|
||||
.prefix("link-")
|
||||
.tempfile_in(&root)?
|
||||
.path()
|
||||
.to_owned();
|
||||
|
||||
use std::os::windows;
|
||||
windows::fs::symlink_dir(&dir1, &dir_link)?;
|
||||
windows::fs::symlink_file(&orig_file, &link)?;
|
||||
|
||||
// root/link
|
||||
let path = link.strip_prefix(&tmp_prefix)?;
|
||||
assert_eq!(
|
||||
path::normalize(path),
|
||||
path,
|
||||
"input {:?} and symlink last component shouldn't be resolved",
|
||||
path
|
||||
);
|
||||
|
||||
// root/dir2/dir_link/orig_file/../..
|
||||
let path = dir_link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(orig_file.path().file_name().unwrap())
|
||||
.join(Component::ParentDir)
|
||||
.join(Component::ParentDir);
|
||||
let expected = dir_link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(Component::ParentDir);
|
||||
assert_eq!(
|
||||
path::normalize(&path),
|
||||
expected,
|
||||
"input {:?} and \"..\" should not erase the simlink that goes ahead",
|
||||
&path
|
||||
);
|
||||
|
||||
// root/link/.././../dir2/../
|
||||
let path = link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(Component::ParentDir)
|
||||
.join(Component::CurDir)
|
||||
.join(Component::ParentDir)
|
||||
.join(dir2.path().file_name().unwrap())
|
||||
.join(Component::ParentDir);
|
||||
let expected = link
|
||||
.strip_prefix(&tmp_prefix)
|
||||
.unwrap()
|
||||
.join(Component::ParentDir)
|
||||
.join(Component::ParentDir);
|
||||
assert_eq!(path::normalize(&path), expected, "input {:?}", &path);
|
||||
|
||||
Ok(())
|
||||
}
|
@ -15,7 +15,7 @@ homepage.workspace = true
|
||||
[features]
|
||||
default = ["git"]
|
||||
unicode-lines = ["helix-core/unicode-lines"]
|
||||
integration = []
|
||||
integration = ["helix-event/integration_test"]
|
||||
git = ["helix-vcs/git"]
|
||||
|
||||
[[bin]]
|
||||
@ -23,6 +23,7 @@ name = "hx"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-event = { path = "../helix-event" }
|
||||
helix-view = { path = "../helix-view" }
|
||||
@ -34,8 +35,6 @@ helix-loader = { path = "../helix-loader" }
|
||||
anyhow = "1"
|
||||
once_cell = "1.19"
|
||||
|
||||
which = "5.0.0"
|
||||
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
|
||||
crossterm = { version = "0.27", features = ["event-stream"] }
|
||||
@ -53,7 +52,7 @@ log = "0.4"
|
||||
nucleo.workspace = true
|
||||
ignore = "0.4"
|
||||
# markdown doc rendering
|
||||
pulldown-cmark = { version = "0.9", default-features = false }
|
||||
pulldown-cmark = { version = "0.10", default-features = false }
|
||||
# file type detection
|
||||
content_inspector = "0.2.4"
|
||||
|
||||
@ -73,7 +72,7 @@ grep-searcher = "0.1.13"
|
||||
|
||||
[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.151"
|
||||
libc = "0.2.153"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
|
||||
@ -82,6 +81,6 @@ crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
||||
[dev-dependencies]
|
||||
smallvec = "1.11"
|
||||
smallvec = "1.13"
|
||||
indoc = "2.0.4"
|
||||
tempfile = "3.8.1"
|
||||
tempfile = "3.9.0"
|
||||
|
@ -6,4 +6,150 @@ fn main() {
|
||||
build_grammars(Some(std::env::var("TARGET").unwrap()))
|
||||
.expect("Failed to compile tree-sitter grammars");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_rc {
|
||||
use std::io::prelude::Write;
|
||||
use std::{env, io, path::Path, path::PathBuf, process};
|
||||
|
||||
pub(crate) fn link_icon_in_windows_exe(icon_path: &str) {
|
||||
let rc_exe = find_rc_exe().expect("Windows SDK is to be installed along with MSVC");
|
||||
|
||||
let output = env::var("OUT_DIR").expect("Env var OUT_DIR should have been set by compiler");
|
||||
let output_dir = PathBuf::from(output);
|
||||
|
||||
let rc_path = output_dir.join("resource.rc");
|
||||
write_resource_file(&rc_path, icon_path).unwrap();
|
||||
|
||||
let resource_file = PathBuf::from(&output_dir).join("resource.lib");
|
||||
compile_with_toolkit_msvc(rc_exe, resource_file, rc_path);
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", output_dir.display());
|
||||
println!("cargo:rustc-link-lib=dylib=resource");
|
||||
}
|
||||
|
||||
fn compile_with_toolkit_msvc(rc_exe: PathBuf, output: PathBuf, input: PathBuf) {
|
||||
let mut command = process::Command::new(rc_exe);
|
||||
let command = command.arg(format!(
|
||||
"/I{}",
|
||||
env::var("CARGO_MANIFEST_DIR")
|
||||
.expect("CARGO_MANIFEST_DIR should have been set by Cargo")
|
||||
));
|
||||
|
||||
let status = command
|
||||
.arg(format!("/fo{}", output.display()))
|
||||
.arg(format!("{}", input.display()))
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
println!(
|
||||
"RC Output:\n{}\n------",
|
||||
String::from_utf8_lossy(&status.stdout)
|
||||
);
|
||||
println!(
|
||||
"RC Error:\n{}\n------",
|
||||
String::from_utf8_lossy(&status.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn find_rc_exe() -> io::Result<PathBuf> {
|
||||
let find_reg_key = process::Command::new("reg")
|
||||
.arg("query")
|
||||
.arg(r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots")
|
||||
.arg("/reg:32")
|
||||
.arg("/v")
|
||||
.arg("KitsRoot10")
|
||||
.output();
|
||||
|
||||
match find_reg_key {
|
||||
Err(find_reg_key) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to run registry query: {}", find_reg_key),
|
||||
))
|
||||
}
|
||||
Ok(find_reg_key) => {
|
||||
if find_reg_key.status.code().unwrap() != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Can not find Windows SDK",
|
||||
));
|
||||
} else {
|
||||
let lines = String::from_utf8(find_reg_key.stdout)
|
||||
.expect("Should be able to parse the output");
|
||||
let mut lines: Vec<&str> = lines.lines().collect();
|
||||
let mut rc_exe_paths: Vec<PathBuf> = Vec::new();
|
||||
lines.reverse();
|
||||
for line in lines {
|
||||
if line.trim().starts_with("KitsRoot") {
|
||||
let kit: String = line
|
||||
.chars()
|
||||
.skip(line.find("REG_SZ").unwrap() + 6)
|
||||
.skip_while(|c| c.is_whitespace())
|
||||
.collect();
|
||||
|
||||
let p = PathBuf::from(&kit);
|
||||
let rc = if cfg!(target_arch = "x86_64") {
|
||||
p.join(r"bin\x64\rc.exe")
|
||||
} else {
|
||||
p.join(r"bin\x86\rc.exe")
|
||||
};
|
||||
|
||||
if rc.exists() {
|
||||
println!("{:?}", rc);
|
||||
rc_exe_paths.push(rc.to_owned());
|
||||
}
|
||||
|
||||
if let Ok(bin) = p.join("bin").read_dir() {
|
||||
for e in bin.filter_map(|e| e.ok()) {
|
||||
let p = if cfg!(target_arch = "x86_64") {
|
||||
e.path().join(r"x64\rc.exe")
|
||||
} else {
|
||||
e.path().join(r"x86\rc.exe")
|
||||
};
|
||||
if p.exists() {
|
||||
println!("{:?}", p);
|
||||
rc_exe_paths.push(p.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if rc_exe_paths.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Can not find Windows SDK",
|
||||
));
|
||||
}
|
||||
|
||||
println!("{:?}", rc_exe_paths);
|
||||
let rc_path = rc_exe_paths.pop().unwrap();
|
||||
|
||||
let rc_exe = if !rc_path.exists() {
|
||||
if cfg!(target_arch = "x86_64") {
|
||||
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x64\rc.exe")
|
||||
} else {
|
||||
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x86\rc.exe")
|
||||
}
|
||||
} else {
|
||||
rc_path
|
||||
};
|
||||
|
||||
println!("Selected RC path: '{}'", rc_exe.display());
|
||||
Ok(rc_exe)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_resource_file(rc_path: &Path, icon_path: &str) -> io::Result<()> {
|
||||
let mut f = std::fs::File::create(rc_path)?;
|
||||
writeln!(f, "{} ICON \"{}\"", 1, icon_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,12 @@
|
||||
use arc_swap::{access::Map, ArcSwap};
|
||||
use futures_util::Stream;
|
||||
use helix_core::{
|
||||
chars::char_is_word,
|
||||
diagnostic::{DiagnosticTag, NumberOrString},
|
||||
path::get_relative_path,
|
||||
pos_at_coords, syntax, Selection,
|
||||
};
|
||||
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
|
||||
use helix_lsp::{
|
||||
lsp::{self, notification::Notification},
|
||||
util::lsp_pos_to_pos,
|
||||
util::lsp_range_to_range,
|
||||
LspProgressMap,
|
||||
};
|
||||
use helix_stdx::path::get_relative_path;
|
||||
use helix_view::{
|
||||
align_view,
|
||||
document::DocumentSavedEventResult,
|
||||
@ -25,15 +21,15 @@
|
||||
|
||||
use crate::{
|
||||
args::Args,
|
||||
commands::apply_workspace_edit,
|
||||
compositor::{Compositor, Event},
|
||||
config::Config,
|
||||
handlers,
|
||||
job::Jobs,
|
||||
keymap::Keymaps,
|
||||
ui::{self, overlay::overlaid},
|
||||
};
|
||||
|
||||
use log::{debug, error, warn};
|
||||
use log::{debug, error, info, warn};
|
||||
#[cfg(not(feature = "integration"))]
|
||||
use std::io::stdout;
|
||||
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
|
||||
@ -142,6 +138,7 @@ pub fn new(
|
||||
let area = terminal.size().expect("couldn't get terminal size");
|
||||
let mut compositor = Compositor::new(area);
|
||||
let config = Arc::new(ArcSwap::from_pointee(config));
|
||||
let handlers = handlers::setup(config.clone());
|
||||
let mut editor = Editor::new(
|
||||
area,
|
||||
theme_loader.clone(),
|
||||
@ -149,6 +146,7 @@ pub fn new(
|
||||
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.editor
|
||||
})),
|
||||
handlers,
|
||||
);
|
||||
|
||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
@ -325,10 +323,21 @@ pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
|
||||
Some(event) = input_stream.next() => {
|
||||
self.handle_terminal_events(event).await;
|
||||
}
|
||||
Some(callback) = self.jobs.futures.next() => {
|
||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
||||
Some(callback) = self.jobs.callbacks.recv() => {
|
||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
|
||||
self.render().await;
|
||||
}
|
||||
Some(msg) = self.jobs.status_messages.recv() => {
|
||||
let severity = match msg.severity{
|
||||
helix_event::status::Severity::Hint => Severity::Hint,
|
||||
helix_event::status::Severity::Info => Severity::Info,
|
||||
helix_event::status::Severity::Warning => Severity::Warning,
|
||||
helix_event::status::Severity::Error => Severity::Error,
|
||||
};
|
||||
// TODO: show multiple status messages at once to avoid clobbering
|
||||
self.editor.status_msg = Some((msg.message, severity));
|
||||
helix_event::request_redraw();
|
||||
}
|
||||
Some(callback) = self.jobs.wait_futures.next() => {
|
||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
||||
self.render().await;
|
||||
@ -392,6 +401,12 @@ fn refresh_language_config(&mut self) -> Result<(), Error> {
|
||||
self.editor.syn_loader = self.syn_loader.clone();
|
||||
for document in self.editor.documents.values_mut() {
|
||||
document.detect_language(self.syn_loader.clone());
|
||||
let diagnostics = Editor::doc_diagnostics(
|
||||
&self.editor.language_servers,
|
||||
&self.editor.diagnostics,
|
||||
document,
|
||||
);
|
||||
document.replace_diagnostics(diagnostics, &[], None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -557,18 +572,8 @@ pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult
|
||||
let lines = doc_save_event.text.len_lines();
|
||||
let bytes = doc_save_event.text.len_bytes();
|
||||
|
||||
if doc.path() != Some(&doc_save_event.path) {
|
||||
doc.set_path(Some(&doc_save_event.path));
|
||||
|
||||
let loader = self.editor.syn_loader.clone();
|
||||
|
||||
// borrowing the same doc again to get around the borrow checker
|
||||
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
|
||||
let id = doc.id();
|
||||
doc.detect_language(loader);
|
||||
self.editor.refresh_language_servers(id);
|
||||
}
|
||||
|
||||
self.editor
|
||||
.set_doc_path(doc_save_event.doc_id, &doc_save_event.path);
|
||||
// TODO: fix being overwritten by lsp
|
||||
self.editor.set_status(format!(
|
||||
"'{}' written, {}L {}B",
|
||||
@ -675,9 +680,13 @@ macro_rules! language_server {
|
||||
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
|
||||
let notification = match Notification::parse(&method, params) {
|
||||
Ok(notification) => notification,
|
||||
Err(helix_lsp::Error::Unhandled) => {
|
||||
info!("Ignoring Unhandled notification from Language Server");
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"received malformed notification from Language Server: {}",
|
||||
error!(
|
||||
"Ignoring unknown notification from Language Server: {}",
|
||||
err
|
||||
);
|
||||
return;
|
||||
@ -731,7 +740,6 @@ macro_rules! language_server {
|
||||
log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name());
|
||||
return;
|
||||
}
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
// have to inline the function because of borrow checking...
|
||||
let doc = self.editor.documents.values_mut()
|
||||
.find(|doc| doc.path().map(|p| p == &path).unwrap_or(false))
|
||||
@ -745,11 +753,10 @@ macro_rules! language_server {
|
||||
true
|
||||
});
|
||||
|
||||
if let Some(doc) = doc {
|
||||
let mut unchanged_diag_sources = Vec::new();
|
||||
if let Some(doc) = &doc {
|
||||
let lang_conf = doc.language.clone();
|
||||
let text = doc.text().clone();
|
||||
|
||||
let mut unchaged_diag_sources_ = Vec::new();
|
||||
if let Some(lang_conf) = &lang_conf {
|
||||
if let Some(old_diagnostics) =
|
||||
self.editor.diagnostics.get(¶ms.uri)
|
||||
@ -774,118 +781,11 @@ macro_rules! language_server {
|
||||
})
|
||||
.map(|(d, _)| d);
|
||||
if new_diagnostics.eq(old_diagnostics) {
|
||||
unchaged_diag_sources_.push(source.clone())
|
||||
unchanged_diag_sources.push(source.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let unchaged_diag_sources = &unchaged_diag_sources_;
|
||||
let diagnostics =
|
||||
params.diagnostics.iter().filter_map(move |diagnostic| {
|
||||
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
|
||||
use lsp::DiagnosticSeverity;
|
||||
|
||||
if diagnostic.source.as_ref().map_or(false, |source| {
|
||||
unchaged_diag_sources.contains(source)
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TODO: convert inside server
|
||||
let start = if let Some(start) = lsp_pos_to_pos(
|
||||
&text,
|
||||
diagnostic.range.start,
|
||||
offset_encoding,
|
||||
) {
|
||||
start
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
|
||||
let end = if let Some(end) =
|
||||
lsp_pos_to_pos(&text, diagnostic.range.end, offset_encoding)
|
||||
{
|
||||
end
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
let severity =
|
||||
diagnostic.severity.map(|severity| match severity {
|
||||
DiagnosticSeverity::ERROR => Error,
|
||||
DiagnosticSeverity::WARNING => Warning,
|
||||
DiagnosticSeverity::INFORMATION => Info,
|
||||
DiagnosticSeverity::HINT => Hint,
|
||||
severity => unreachable!(
|
||||
"unrecognized diagnostic severity: {:?}",
|
||||
severity
|
||||
),
|
||||
});
|
||||
|
||||
if let Some(lang_conf) = &lang_conf {
|
||||
if let Some(severity) = severity {
|
||||
if severity < lang_conf.diagnostic_severity {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let code = match diagnostic.code.clone() {
|
||||
Some(x) => match x {
|
||||
lsp::NumberOrString::Number(x) => {
|
||||
Some(NumberOrString::Number(x))
|
||||
}
|
||||
lsp::NumberOrString::String(x) => {
|
||||
Some(NumberOrString::String(x))
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let tags = if let Some(tags) = &diagnostic.tags {
|
||||
let new_tags = tags
|
||||
.iter()
|
||||
.filter_map(|tag| match *tag {
|
||||
lsp::DiagnosticTag::DEPRECATED => {
|
||||
Some(DiagnosticTag::Deprecated)
|
||||
}
|
||||
lsp::DiagnosticTag::UNNECESSARY => {
|
||||
Some(DiagnosticTag::Unnecessary)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
new_tags
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let ends_at_word = start != end
|
||||
&& end != 0
|
||||
&& text.get_char(end - 1).map_or(false, char_is_word);
|
||||
let starts_at_word = start != end
|
||||
&& text.get_char(start).map_or(false, char_is_word);
|
||||
|
||||
Some(Diagnostic {
|
||||
range: Range { start, end },
|
||||
ends_at_word,
|
||||
starts_at_word,
|
||||
zero_width: start == end,
|
||||
line: diagnostic.range.start.line as usize,
|
||||
message: diagnostic.message.clone(),
|
||||
severity,
|
||||
code,
|
||||
tags,
|
||||
source: diagnostic.source.clone(),
|
||||
data: diagnostic.data.clone(),
|
||||
language_server_id: server_id,
|
||||
})
|
||||
});
|
||||
|
||||
doc.replace_diagnostics(diagnostics, unchaged_diag_sources, server_id);
|
||||
}
|
||||
|
||||
let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id));
|
||||
@ -910,6 +810,27 @@ macro_rules! language_server {
|
||||
diagnostics.sort_unstable_by_key(|(d, server_id)| {
|
||||
(d.severity, d.range.start, *server_id)
|
||||
});
|
||||
|
||||
if let Some(doc) = doc {
|
||||
let diagnostic_of_language_server_and_not_in_unchanged_sources =
|
||||
|diagnostic: &lsp::Diagnostic, ls_id| {
|
||||
ls_id == server_id
|
||||
&& diagnostic.source.as_ref().map_or(true, |source| {
|
||||
!unchanged_diag_sources.contains(source)
|
||||
})
|
||||
};
|
||||
let diagnostics = Editor::doc_diagnostics_with_filter(
|
||||
&self.editor.language_servers,
|
||||
&self.editor.diagnostics,
|
||||
doc,
|
||||
diagnostic_of_language_server_and_not_in_unchanged_sources,
|
||||
);
|
||||
doc.replace_diagnostics(
|
||||
diagnostics,
|
||||
&unchanged_diag_sources,
|
||||
Some(server_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
Notification::ShowMessage(params) => {
|
||||
log::warn!("unhandled window/showMessage: {:?}", params);
|
||||
@ -1017,7 +938,7 @@ macro_rules! language_server {
|
||||
|
||||
// Clear any diagnostics for documents with this server open.
|
||||
for doc in self.editor.documents_mut() {
|
||||
doc.clear_diagnostics(server_id);
|
||||
doc.clear_diagnostics(Some(server_id));
|
||||
}
|
||||
|
||||
// Remove the language server from the registry.
|
||||
@ -1071,11 +992,9 @@ macro_rules! language_server {
|
||||
let language_server = language_server!();
|
||||
if language_server.is_initialized() {
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let res = apply_workspace_edit(
|
||||
&mut self.editor,
|
||||
offset_encoding,
|
||||
¶ms.edit,
|
||||
);
|
||||
let res = self
|
||||
.editor
|
||||
.apply_workspace_edit(offset_encoding, ¶ms.edit);
|
||||
|
||||
Ok(json!(lsp::ApplyWorkspaceEditResponse {
|
||||
applied: res.is_ok(),
|
||||
@ -1176,6 +1095,13 @@ macro_rules! language_server {
|
||||
}
|
||||
Ok(serde_json::Value::Null)
|
||||
}
|
||||
Ok(MethodCall::ShowDocument(params)) => {
|
||||
let language_server = language_server!();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
|
||||
let result = self.handle_show_document(params, offset_encoding);
|
||||
Ok(json!(result))
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(language_server!().reply(id, reply));
|
||||
@ -1184,6 +1110,68 @@ macro_rules! language_server {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_show_document(
|
||||
&mut self,
|
||||
params: lsp::ShowDocumentParams,
|
||||
offset_encoding: helix_lsp::OffsetEncoding,
|
||||
) -> lsp::ShowDocumentResult {
|
||||
if let lsp::ShowDocumentParams {
|
||||
external: Some(true),
|
||||
uri,
|
||||
..
|
||||
} = params
|
||||
{
|
||||
self.jobs.callback(crate::open_external_url_callback(uri));
|
||||
return lsp::ShowDocumentResult { success: true };
|
||||
};
|
||||
|
||||
let lsp::ShowDocumentParams {
|
||||
uri,
|
||||
selection,
|
||||
take_focus,
|
||||
..
|
||||
} = params;
|
||||
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
log::error!("unsupported file URI: {}: {:?}", uri, err);
|
||||
return lsp::ShowDocumentResult { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
let action = match take_focus {
|
||||
Some(true) => helix_view::editor::Action::Replace,
|
||||
_ => helix_view::editor::Action::VerticalSplit,
|
||||
};
|
||||
|
||||
let doc_id = match self.editor.open(&path, action) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
log::error!("failed to open path: {:?}: {:?}", uri, err);
|
||||
return lsp::ShowDocumentResult { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
let doc = doc_mut!(self.editor, &doc_id);
|
||||
if let Some(range) = selection {
|
||||
// TODO: convert inside server
|
||||
if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding) {
|
||||
let view = view_mut!(self.editor);
|
||||
|
||||
// we flip the range so that the cursor sits on the start of the symbol
|
||||
// (for example start of the function).
|
||||
doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
|
||||
if action.align_view(view, doc.id()) {
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", range);
|
||||
};
|
||||
};
|
||||
lsp::ShowDocumentResult { success: true }
|
||||
}
|
||||
|
||||
async fn claim_term(&mut self) -> std::io::Result<()> {
|
||||
let terminal_config = self.config.load().editor.clone().into();
|
||||
self.terminal.claim(terminal_config)
|
||||
|
@ -90,10 +90,9 @@ pub fn parse_args() -> Result<Args> {
|
||||
}
|
||||
}
|
||||
arg if arg.starts_with('+') => {
|
||||
let arg = &arg[1..];
|
||||
line_number = match arg.parse::<usize>() {
|
||||
Ok(n) => n.saturating_sub(1),
|
||||
_ => anyhow::bail!("bad line number after +"),
|
||||
match arg[1..].parse::<usize>() {
|
||||
Ok(n) => line_number = n.saturating_sub(1),
|
||||
_ => args.files.push(parse_file(arg)),
|
||||
};
|
||||
}
|
||||
arg => args.files.push(parse_file(arg)),
|
||||
|
@ -5,7 +5,6 @@
|
||||
pub use dap::*;
|
||||
use helix_vcs::Hunk;
|
||||
pub use lsp::*;
|
||||
use tokio::sync::oneshot;
|
||||
use tui::widgets::Row;
|
||||
pub use typed::*;
|
||||
|
||||
@ -33,7 +32,7 @@
|
||||
};
|
||||
use helix_view::{
|
||||
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
|
||||
editor::{Action, CompleteAction},
|
||||
editor::Action,
|
||||
info::Info,
|
||||
input::KeyEvent,
|
||||
keyboard::KeyCode,
|
||||
@ -52,14 +51,10 @@
|
||||
filter_picker_entry,
|
||||
job::Callback,
|
||||
keymap::ReverseKeymap,
|
||||
ui::{
|
||||
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker,
|
||||
Popup, Prompt, PromptEvent,
|
||||
},
|
||||
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
||||
};
|
||||
|
||||
use crate::job::{self, Jobs};
|
||||
use futures_util::{stream::FuturesUnordered, TryStreamExt};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
@ -88,7 +83,7 @@ pub struct Context<'a> {
|
||||
pub count: Option<NonZeroUsize>,
|
||||
pub editor: &'a mut Editor,
|
||||
|
||||
pub callback: Option<crate::compositor::Callback>,
|
||||
pub callback: Vec<crate::compositor::Callback>,
|
||||
pub on_next_key_callback: Option<OnKeyCallback>,
|
||||
pub jobs: &'a mut Jobs,
|
||||
}
|
||||
@ -96,16 +91,18 @@ pub struct Context<'a> {
|
||||
impl<'a> Context<'a> {
|
||||
/// Push a new component onto the compositor.
|
||||
pub fn push_layer(&mut self, component: Box<dyn Component>) {
|
||||
self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
|
||||
compositor.push(component)
|
||||
}));
|
||||
self.callback
|
||||
.push(Box::new(|compositor: &mut Compositor, _| {
|
||||
compositor.push(component)
|
||||
}));
|
||||
}
|
||||
|
||||
/// Call `replace_or_push` on the Compositor
|
||||
pub fn replace_or_push_layer<T: Component>(&mut self, id: &'static str, component: T) {
|
||||
self.callback = Some(Box::new(move |compositor: &mut Compositor, _| {
|
||||
compositor.replace_or_push(id, component);
|
||||
}));
|
||||
self.callback
|
||||
.push(Box::new(move |compositor: &mut Compositor, _| {
|
||||
compositor.replace_or_push(id, component);
|
||||
}));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -337,9 +334,9 @@ pub fn doc(&self) -> &str {
|
||||
goto_implementation, "Goto implementation",
|
||||
goto_file_start, "Goto line number <n> else file start",
|
||||
goto_file_end, "Goto file end",
|
||||
goto_file, "Goto files/URLs in selection",
|
||||
goto_file_hsplit, "Goto files in selection (hsplit)",
|
||||
goto_file_vsplit, "Goto files in selection (vsplit)",
|
||||
goto_file, "Goto files/URLs in selections",
|
||||
goto_file_hsplit, "Goto files in selections (hsplit)",
|
||||
goto_file_vsplit, "Goto files in selections (vsplit)",
|
||||
goto_reference, "Goto references",
|
||||
goto_window_top, "Goto window top",
|
||||
goto_window_center, "Goto window center",
|
||||
@ -795,7 +792,7 @@ fn goto_buffer(editor: &mut Editor, direction: Direction) {
|
||||
let iter = editor.documents.keys();
|
||||
let mut iter = iter.rev().skip_while(|id| *id != ¤t);
|
||||
iter.next(); // skip current item
|
||||
iter.next().or_else(|| editor.documents.keys().rev().next())
|
||||
iter.next().or_else(|| editor.documents.keys().next_back())
|
||||
}
|
||||
}
|
||||
.unwrap();
|
||||
@ -1227,7 +1224,7 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
|
||||
.unwrap_or_default();
|
||||
|
||||
if url.scheme() != "file" {
|
||||
return open_external_url(cx, url);
|
||||
return cx.jobs.callback(crate::open_external_url_callback(url));
|
||||
}
|
||||
|
||||
let content_type = std::fs::File::open(url.path()).and_then(|file| {
|
||||
@ -1240,7 +1237,9 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
|
||||
// we attempt to open binary files - files that can't be open in helix - using external
|
||||
// program as well, e.g. pdf files or images
|
||||
match content_type {
|
||||
Ok(content_inspector::ContentType::BINARY) => open_external_url(cx, url),
|
||||
Ok(content_inspector::ContentType::BINARY) => {
|
||||
cx.jobs.callback(crate::open_external_url_callback(url))
|
||||
}
|
||||
Ok(_) | Err(_) => {
|
||||
let path = &rel_path.join(url.path());
|
||||
if path.is_dir() {
|
||||
@ -1253,23 +1252,6 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens URL in external program.
|
||||
fn open_external_url(cx: &mut Context, url: Url) {
|
||||
let commands = open::commands(url.as_str());
|
||||
cx.jobs.callback(async {
|
||||
for cmd in commands {
|
||||
let mut command = tokio::process::Command::new(cmd.get_program());
|
||||
command.args(cmd.get_args());
|
||||
if command.output().await.is_ok() {
|
||||
return Ok(job::Callback::Editor(Box::new(|_| {})));
|
||||
}
|
||||
}
|
||||
Ok(job::Callback::Editor(Box::new(move |editor| {
|
||||
editor.set_error("Opening URL in external program failed")
|
||||
})))
|
||||
});
|
||||
}
|
||||
|
||||
fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
|
||||
where
|
||||
F: Fn(RopeSlice, Range, usize) -> Range,
|
||||
@ -2184,7 +2166,7 @@ impl ui::menu::Item for FileResult {
|
||||
type Data = Option<PathBuf>;
|
||||
|
||||
fn format(&self, current_path: &Self::Data) -> Row {
|
||||
let relative_path = helix_core::path::get_relative_path(&self.path)
|
||||
let relative_path = helix_stdx::path::get_relative_path(&self.path)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if current_path
|
||||
@ -2233,7 +2215,7 @@ fn format(&self, current_path: &Self::Data) -> Row {
|
||||
.case_smart(smart_case)
|
||||
.build(regex.as_str())
|
||||
{
|
||||
let search_root = helix_loader::current_working_dir();
|
||||
let search_root = helix_stdx::env::current_working_dir();
|
||||
if !search_root.exists() {
|
||||
cx.editor
|
||||
.set_error("Current working directory does not exist");
|
||||
@ -2606,7 +2588,6 @@ fn delete_by_selection_insert_mode(
|
||||
);
|
||||
}
|
||||
doc.apply(&transaction, view.id);
|
||||
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||
}
|
||||
|
||||
fn delete_selection(cx: &mut Context) {
|
||||
@ -2680,10 +2661,6 @@ fn insert_mode(cx: &mut Context) {
|
||||
.transform(|range| Range::new(range.to(), range.from()));
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
|
||||
// [TODO] temporary workaround until we're not using the idle timer to
|
||||
// trigger auto completions any more
|
||||
cx.editor.clear_idle_timer();
|
||||
}
|
||||
|
||||
// inserts at the end of each selection
|
||||
@ -2746,7 +2723,7 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
|
||||
}
|
||||
|
||||
fn file_picker_in_current_directory(cx: &mut Context) {
|
||||
let cwd = helix_loader::current_working_dir();
|
||||
let cwd = helix_stdx::env::current_working_dir();
|
||||
if !cwd.exists() {
|
||||
cx.editor
|
||||
.set_error("Current working directory does not exist");
|
||||
@ -2774,7 +2751,7 @@ fn format(&self, _data: &Self::Data) -> Row {
|
||||
let path = self
|
||||
.path
|
||||
.as_deref()
|
||||
.map(helix_core::path::get_relative_path);
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
let path = match path.as_deref().and_then(Path::to_str) {
|
||||
Some(path) => path,
|
||||
None => SCRATCH_BUFFER_NAME,
|
||||
@ -2804,7 +2781,7 @@ fn format(&self, _data: &Self::Data) -> Row {
|
||||
.editor
|
||||
.documents
|
||||
.values()
|
||||
.map(|doc| new_meta(doc))
|
||||
.map(new_meta)
|
||||
.collect::<Vec<BufferMeta>>();
|
||||
|
||||
// mru
|
||||
@ -2841,7 +2818,7 @@ fn format(&self, _data: &Self::Data) -> Row {
|
||||
let path = self
|
||||
.path
|
||||
.as_deref()
|
||||
.map(helix_core::path::get_relative_path);
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
let path = match path.as_deref().and_then(Path::to_str) {
|
||||
Some(path) => path,
|
||||
None => SCRATCH_BUFFER_NAME,
|
||||
@ -2949,7 +2926,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||
let register = cx.register;
|
||||
let count = cx.count;
|
||||
|
||||
cx.callback = Some(Box::new(
|
||||
cx.callback.push(Box::new(
|
||||
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
||||
let keymap = compositor.find::<ui::EditorView>().unwrap().keymaps.map()
|
||||
[&cx.editor.mode]
|
||||
@ -2969,7 +2946,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||
register,
|
||||
count,
|
||||
editor: cx.editor,
|
||||
callback: None,
|
||||
callback: Vec::new(),
|
||||
on_next_key_callback: None,
|
||||
jobs: cx.jobs,
|
||||
};
|
||||
@ -2997,7 +2974,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||
|
||||
fn last_picker(cx: &mut Context) {
|
||||
// TODO: last picker does not seem to work well with buffer_picker
|
||||
cx.callback = Some(Box::new(|compositor, cx| {
|
||||
cx.callback.push(Box::new(|compositor, cx| {
|
||||
if let Some(picker) = compositor.last_picker.take() {
|
||||
compositor.push(picker);
|
||||
} else {
|
||||
@ -3350,7 +3327,7 @@ fn exit_select_mode(cx: &mut Context) {
|
||||
|
||||
fn goto_first_diag(cx: &mut Context) {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let selection = match doc.shown_diagnostics().next() {
|
||||
let selection = match doc.diagnostics().first() {
|
||||
Some(diag) => Selection::single(diag.range.start, diag.range.end),
|
||||
None => return,
|
||||
};
|
||||
@ -3359,7 +3336,7 @@ fn goto_first_diag(cx: &mut Context) {
|
||||
|
||||
fn goto_last_diag(cx: &mut Context) {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let selection = match doc.shown_diagnostics().last() {
|
||||
let selection = match doc.diagnostics().last() {
|
||||
Some(diag) => Selection::single(diag.range.start, diag.range.end),
|
||||
None => return,
|
||||
};
|
||||
@ -3375,9 +3352,10 @@ fn goto_next_diag(cx: &mut Context) {
|
||||
.cursor(doc.text().slice(..));
|
||||
|
||||
let diag = doc
|
||||
.shown_diagnostics()
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.find(|diag| diag.range.start > cursor_pos)
|
||||
.or_else(|| doc.shown_diagnostics().next());
|
||||
.or_else(|| doc.diagnostics().first());
|
||||
|
||||
let selection = match diag {
|
||||
Some(diag) => Selection::single(diag.range.start, diag.range.end),
|
||||
@ -3395,10 +3373,11 @@ fn goto_prev_diag(cx: &mut Context) {
|
||||
.cursor(doc.text().slice(..));
|
||||
|
||||
let diag = doc
|
||||
.shown_diagnostics()
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|diag| diag.range.start < cursor_pos)
|
||||
.or_else(|| doc.shown_diagnostics().last());
|
||||
.or_else(|| doc.diagnostics().last());
|
||||
|
||||
let selection = match diag {
|
||||
// NOTE: the selection is reversed because we're jumping to the
|
||||
@ -3507,9 +3486,10 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
|
||||
}
|
||||
|
||||
pub mod insert {
|
||||
use crate::events::PostInsertChar;
|
||||
|
||||
use super::*;
|
||||
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
||||
pub type PostHook = fn(&mut Context, char);
|
||||
|
||||
/// Exclude the cursor in range.
|
||||
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
|
||||
@ -3523,88 +3503,6 @@ fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
|
||||
}
|
||||
}
|
||||
|
||||
// It trigger completion when idle timer reaches deadline
|
||||
// Only trigger completion if the word under cursor is longer than n characters
|
||||
pub fn idle_completion(cx: &mut Context) {
|
||||
let config = cx.editor.config();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
|
||||
use helix_core::chars::char_is_word;
|
||||
let mut iter = text.chars_at(cursor);
|
||||
iter.reverse();
|
||||
for _ in 0..config.completion_trigger_len {
|
||||
match iter.next() {
|
||||
Some(c) if char_is_word(c) => {}
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
super::completion(cx);
|
||||
}
|
||||
|
||||
fn language_server_completion(cx: &mut Context, ch: char) {
|
||||
let config = cx.editor.config();
|
||||
if !config.auto_completion {
|
||||
return;
|
||||
}
|
||||
|
||||
use helix_lsp::lsp;
|
||||
// if ch matches completion char, trigger completion
|
||||
let doc = doc_mut!(cx.editor);
|
||||
let trigger_completion = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.any(|ls| {
|
||||
// TODO: what if trigger is multiple chars long
|
||||
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(triggers),
|
||||
..
|
||||
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
|
||||
});
|
||||
|
||||
if trigger_completion {
|
||||
cx.editor.clear_idle_timer();
|
||||
super::completion(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn signature_help(cx: &mut Context, ch: char) {
|
||||
use helix_lsp::lsp;
|
||||
// if ch matches signature_help char, trigger
|
||||
let doc = doc_mut!(cx.editor);
|
||||
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
|
||||
let Some(language_server) = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||
.next()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let capabilities = language_server.capabilities();
|
||||
|
||||
if let lsp::ServerCapabilities {
|
||||
signature_help_provider:
|
||||
Some(lsp::SignatureHelpOptions {
|
||||
trigger_characters: Some(triggers),
|
||||
// TODO: retrigger_characters
|
||||
..
|
||||
}),
|
||||
..
|
||||
} = capabilities
|
||||
{
|
||||
// TODO: what if trigger is multiple chars long
|
||||
let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
|
||||
// lsp doesn't tell us when to close the signature help, so we request
|
||||
// the help information again after common close triggers which should
|
||||
// return None, which in turn closes the popup.
|
||||
let close_triggers = &[')', ';', '.'];
|
||||
|
||||
if is_trigger || close_triggers.contains(&ch) {
|
||||
super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The default insert hook: simply insert the character
|
||||
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
|
||||
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
|
||||
@ -3634,12 +3532,7 @@ pub fn insert_char(cx: &mut Context, c: char) {
|
||||
doc.apply(&t, view.id);
|
||||
}
|
||||
|
||||
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
|
||||
// this could also generically look at Transaction, but it's a bit annoying to look at
|
||||
// Operation instead of Change.
|
||||
for hook in &[language_server_completion, signature_help] {
|
||||
hook(cx, c);
|
||||
}
|
||||
helix_event::dispatch(PostInsertChar { c, cx });
|
||||
}
|
||||
|
||||
pub fn smart_tab(cx: &mut Context) {
|
||||
@ -3758,7 +3651,7 @@ pub fn insert_newline(cx: &mut Context) {
|
||||
(pos, pos, local_offs)
|
||||
};
|
||||
|
||||
let new_range = if doc.restore_cursor {
|
||||
let new_range = if range.cursor(text) > range.anchor {
|
||||
// when appending, extend the range by local_offs
|
||||
Range::new(
|
||||
range.anchor + global_offs,
|
||||
@ -3864,8 +3757,6 @@ pub fn delete_char_backward(cx: &mut Context) {
|
||||
});
|
||||
let (view, doc) = current!(cx.editor);
|
||||
doc.apply(&transaction, view.id);
|
||||
|
||||
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||
}
|
||||
|
||||
pub fn delete_char_forward(cx: &mut Context) {
|
||||
@ -4185,9 +4076,13 @@ fn replace_with_yanked(cx: &mut Context) {
|
||||
}
|
||||
|
||||
fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) {
|
||||
let Some(values) = editor.registers
|
||||
let Some(values) = editor
|
||||
.registers
|
||||
.read(register, editor)
|
||||
.filter(|values| values.len() > 0) else { return };
|
||||
.filter(|values| values.len() > 0)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let values: Vec<_> = values.map(|value| value.to_string()).collect();
|
||||
|
||||
let (view, doc) = current!(editor);
|
||||
@ -4224,7 +4119,9 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) {
|
||||
}
|
||||
|
||||
fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) {
|
||||
let Some(values) = editor.registers.read(register, editor) else { return };
|
||||
let Some(values) = editor.registers.read(register, editor) else {
|
||||
return;
|
||||
};
|
||||
let values: Vec<_> = values.map(|value| value.to_string()).collect();
|
||||
|
||||
let (view, doc) = current!(editor);
|
||||
@ -4513,151 +4410,14 @@ fn remove_primary_selection(cx: &mut Context) {
|
||||
}
|
||||
|
||||
pub fn completion(cx: &mut Context) {
|
||||
use helix_lsp::{lsp, util::pos_to_lsp_pos};
|
||||
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let range = doc.selection(view.id).primary();
|
||||
let text = doc.text().slice(..);
|
||||
let cursor = range.cursor(text);
|
||||
|
||||
let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion
|
||||
{
|
||||
savepoint.clone()
|
||||
} else {
|
||||
doc.savepoint(view)
|
||||
};
|
||||
|
||||
let text = savepoint.text.clone();
|
||||
let cursor = savepoint.cursor();
|
||||
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
|
||||
let mut futures: FuturesUnordered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|language_server| {
|
||||
let language_server_id = language_server.id();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
|
||||
let doc_id = doc.identifier();
|
||||
let completion_request = language_server.completion(doc_id, pos, None).unwrap();
|
||||
|
||||
async move {
|
||||
let json = completion_request.await?;
|
||||
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
||||
|
||||
let items = match response {
|
||||
Some(lsp::CompletionResponse::Array(items)) => items,
|
||||
// TODO: do something with is_incomplete
|
||||
Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: _is_incomplete,
|
||||
items,
|
||||
})) => items,
|
||||
None => Vec::new(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|item| CompletionItem {
|
||||
item,
|
||||
language_server_id,
|
||||
resolved: false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
anyhow::Ok(items)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// setup a channel that allows the request to be canceled
|
||||
let (tx, rx) = oneshot::channel();
|
||||
// set completion_request so that this request can be canceled
|
||||
// by setting completion_request, the old channel stored there is dropped
|
||||
// and the associated request is automatically dropped
|
||||
cx.editor.completion_request_handle = Some(tx);
|
||||
let future = async move {
|
||||
let items_future = async move {
|
||||
let mut items = Vec::new();
|
||||
// TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
|
||||
while let Some(mut lsp_items) = futures.try_next().await? {
|
||||
items.append(&mut lsp_items);
|
||||
}
|
||||
anyhow::Ok(items)
|
||||
};
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = rx => {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
res = items_future => {
|
||||
res
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let trigger_offset = cursor;
|
||||
|
||||
// TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
|
||||
// completion filtering. For example logger.te| should filter the initial suggestion list with "te".
|
||||
|
||||
use helix_core::chars;
|
||||
let mut iter = text.chars_at(cursor);
|
||||
iter.reverse();
|
||||
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
|
||||
let start_offset = cursor.saturating_sub(offset);
|
||||
|
||||
let trigger_doc = doc.id();
|
||||
let trigger_view = view.id;
|
||||
|
||||
// FIXME: The commands Context can only have a single callback
|
||||
// which means it gets overwritten when executing keybindings
|
||||
// with multiple commands or macros. This would mean that completion
|
||||
// might be incorrectly applied when repeating the insertmode action
|
||||
//
|
||||
// TODO: to solve this either make cx.callback a Vec of callbacks or
|
||||
// alternatively move `last_insert` to `helix_view::Editor`
|
||||
cx.callback = Some(Box::new(
|
||||
move |compositor: &mut Compositor, _cx: &mut compositor::Context| {
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
||||
},
|
||||
));
|
||||
|
||||
cx.jobs.callback(async move {
|
||||
let items = future.await?;
|
||||
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let (view, doc) = current_ref!(editor);
|
||||
// check if the completion request is stale.
|
||||
//
|
||||
// Completions are completed asynchronously and therefore the user could
|
||||
//switch document/view or leave insert mode. In all of thoise cases the
|
||||
// completion should be discarded
|
||||
if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc {
|
||||
return;
|
||||
}
|
||||
|
||||
if items.is_empty() {
|
||||
// editor.set_error("No completion available");
|
||||
return;
|
||||
}
|
||||
let size = compositor.size();
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
let completion_area = ui.set_completion(
|
||||
editor,
|
||||
savepoint,
|
||||
items,
|
||||
start_offset,
|
||||
trigger_offset,
|
||||
size,
|
||||
);
|
||||
let size = compositor.size();
|
||||
let signature_help_area = compositor
|
||||
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
|
||||
.map(|signature_help| signature_help.area(size, editor));
|
||||
// Delete the signature help popup if they intersect.
|
||||
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b))
|
||||
{
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
}
|
||||
};
|
||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
||||
});
|
||||
cx.editor
|
||||
.handlers
|
||||
.trigger_completions(cursor, doc.id(), view.id);
|
||||
}
|
||||
|
||||
// comments
|
||||
@ -4836,10 +4596,6 @@ fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
|
||||
);
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
|
||||
// [TODO] temporary workaround until we're not using the idle timer to
|
||||
// trigger auto completions any more
|
||||
editor.clear_idle_timer();
|
||||
}
|
||||
};
|
||||
|
||||
@ -5827,7 +5583,7 @@ fn replay_macro(cx: &mut Context) {
|
||||
cx.editor.macro_replaying.push(reg);
|
||||
|
||||
let count = cx.count();
|
||||
cx.callback = Some(Box::new(move |compositor, cx| {
|
||||
cx.callback.push(Box::new(move |compositor, cx| {
|
||||
for _ in 0..count {
|
||||
for &key in keys.iter() {
|
||||
compositor.handle_event(&compositor::Event::Key(key), cx);
|
||||
|
@ -78,7 +78,7 @@ fn thread_picker(
|
||||
})
|
||||
.with_preview(move |editor, thread| {
|
||||
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||
let frame = frames.get(0)?;
|
||||
let frame = frames.first()?;
|
||||
let path = frame.source.as_ref()?.path.clone()?;
|
||||
let pos = Some((
|
||||
frame.line.saturating_sub(1),
|
||||
@ -166,7 +166,7 @@ pub fn dap_start_impl(
|
||||
// TODO: avoid refetching all of this... pass a config in
|
||||
let template = match name {
|
||||
Some(name) => config.templates.iter().find(|t| t.name == name),
|
||||
None => config.templates.get(0),
|
||||
None => config.templates.first(),
|
||||
}
|
||||
.ok_or_else(|| anyhow!("No debug config with given name"))?;
|
||||
|
||||
@ -217,7 +217,7 @@ pub fn dap_start_impl(
|
||||
}
|
||||
}
|
||||
|
||||
args.insert("cwd", to_value(helix_loader::current_working_dir())?);
|
||||
args.insert("cwd", to_value(helix_stdx::env::current_working_dir())?);
|
||||
|
||||
let args = to_value(args).unwrap();
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
|
||||
use futures_util::{stream::FuturesUnordered, FutureExt};
|
||||
use helix_lsp::{
|
||||
block_on,
|
||||
lsp::{
|
||||
@ -8,22 +8,21 @@
|
||||
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
|
||||
Client, OffsetEncoding,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio_stream::StreamExt;
|
||||
use tui::{
|
||||
text::{Span, Spans},
|
||||
widgets::Row,
|
||||
};
|
||||
|
||||
use super::{align_view, push_jump, Align, Context, Editor, Open};
|
||||
use super::{align_view, push_jump, Align, Context, Editor};
|
||||
|
||||
use helix_core::{
|
||||
path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
|
||||
};
|
||||
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
|
||||
use helix_stdx::path;
|
||||
use helix_view::{
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||
editor::Action,
|
||||
graphics::Margin,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
theme::Style,
|
||||
Document, View,
|
||||
};
|
||||
@ -31,10 +30,7 @@
|
||||
use crate::{
|
||||
compositor::{self, Compositor},
|
||||
job::Callback,
|
||||
ui::{
|
||||
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
|
||||
PromptEvent,
|
||||
},
|
||||
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
|
||||
};
|
||||
|
||||
use std::{
|
||||
@ -43,7 +39,6 @@
|
||||
fmt::Write,
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Gets the first language server that is attached to a document which supports a specific feature.
|
||||
@ -731,8 +726,7 @@ pub fn code_action(cx: &mut Context) {
|
||||
resolved_code_action.as_ref().unwrap_or(code_action);
|
||||
|
||||
if let Some(ref workspace_edit) = resolved_code_action.edit {
|
||||
log::debug!("edit: {:?}", workspace_edit);
|
||||
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
|
||||
let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit);
|
||||
}
|
||||
|
||||
// if code action provides both edit and command first the edit
|
||||
@ -792,63 +786,6 @@ pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd:
|
||||
});
|
||||
}
|
||||
|
||||
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
|
||||
use lsp::ResourceOp;
|
||||
use std::fs;
|
||||
match op {
|
||||
ResourceOp::Create(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
if ignore_if_exists && path.exists() {
|
||||
Ok(())
|
||||
} else {
|
||||
// Create directory if it does not exist
|
||||
if let Some(dir) = path.parent() {
|
||||
if !dir.is_dir() {
|
||||
fs::create_dir_all(dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(&path, [])
|
||||
}
|
||||
}
|
||||
ResourceOp::Delete(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
if path.is_dir() {
|
||||
let recursive = op
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|options| options.recursive)
|
||||
.unwrap_or(false);
|
||||
|
||||
if recursive {
|
||||
fs::remove_dir_all(&path)
|
||||
} else {
|
||||
fs::remove_dir(&path)
|
||||
}
|
||||
} else if path.is_file() {
|
||||
fs::remove_file(&path)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ResourceOp::Rename(op) => {
|
||||
let from = op.old_uri.to_file_path().unwrap();
|
||||
let to = op.new_uri.to_file_path().unwrap();
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
if ignore_if_exists && to.exists() {
|
||||
Ok(())
|
||||
} else {
|
||||
fs::rename(from, &to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyEditError {
|
||||
pub kind: ApplyEditErrorKind,
|
||||
@ -876,168 +813,20 @@ fn to_string(&self) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
///TODO make this transactional (and set failureMode to transactional)
|
||||
pub fn apply_workspace_edit(
|
||||
editor: &mut Editor,
|
||||
offset_encoding: OffsetEncoding,
|
||||
workspace_edit: &lsp::WorkspaceEdit,
|
||||
) -> Result<(), ApplyEditError> {
|
||||
let mut apply_edits = |uri: &helix_lsp::Url,
|
||||
version: Option<i32>,
|
||||
text_edits: Vec<lsp::TextEdit>|
|
||||
-> Result<(), ApplyEditErrorKind> {
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
let err = format!("unable to convert URI to filepath: {}", uri);
|
||||
log::error!("{}", err);
|
||||
editor.set_error(err);
|
||||
return Err(ApplyEditErrorKind::UnknownURISchema);
|
||||
}
|
||||
};
|
||||
|
||||
let current_view_id = view!(editor).id;
|
||||
let doc_id = match editor.open(&path, Action::Load) {
|
||||
Ok(doc_id) => doc_id,
|
||||
Err(err) => {
|
||||
let err = format!("failed to open document: {}: {}", uri, err);
|
||||
log::error!("{}", err);
|
||||
editor.set_error(err);
|
||||
return Err(ApplyEditErrorKind::FileNotFound);
|
||||
}
|
||||
};
|
||||
|
||||
let doc = doc_mut!(editor, &doc_id);
|
||||
if let Some(version) = version {
|
||||
if version != doc.version() {
|
||||
let err = format!("outdated workspace edit for {path:?}");
|
||||
log::error!("{err}, expected {} but got {version}", doc.version());
|
||||
editor.set_error(err);
|
||||
return Err(ApplyEditErrorKind::DocumentChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// Need to determine a view for apply/append_changes_to_history
|
||||
let selections = doc.selections();
|
||||
let view_id = if selections.contains_key(¤t_view_id) {
|
||||
// use current if possible
|
||||
current_view_id
|
||||
} else {
|
||||
// Hack: we take the first available view_id
|
||||
selections
|
||||
.keys()
|
||||
.next()
|
||||
.copied()
|
||||
.expect("No view_id available")
|
||||
};
|
||||
|
||||
let transaction = helix_lsp::util::generate_transaction_from_edits(
|
||||
doc.text(),
|
||||
text_edits,
|
||||
offset_encoding,
|
||||
);
|
||||
let view = view_mut!(editor, view_id);
|
||||
doc.apply(&transaction, view.id);
|
||||
doc.append_changes_to_history(view);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Some(ref document_changes) = workspace_edit.document_changes {
|
||||
match document_changes {
|
||||
lsp::DocumentChanges::Edits(document_edits) => {
|
||||
for (i, document_edit) in document_edits.iter().enumerate() {
|
||||
let edits = document_edit
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| match edit {
|
||||
lsp::OneOf::Left(text_edit) => text_edit,
|
||||
lsp::OneOf::Right(annotated_text_edit) => {
|
||||
&annotated_text_edit.text_edit
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
apply_edits(
|
||||
&document_edit.text_document.uri,
|
||||
document_edit.text_document.version,
|
||||
edits,
|
||||
)
|
||||
.map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
lsp::DocumentChanges::Operations(operations) => {
|
||||
log::debug!("document changes - operations: {:?}", operations);
|
||||
for (i, operation) in operations.iter().enumerate() {
|
||||
match operation {
|
||||
lsp::DocumentChangeOperation::Op(op) => {
|
||||
apply_document_resource_op(op).map_err(|io| ApplyEditError {
|
||||
kind: ApplyEditErrorKind::IoError(io),
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
|
||||
lsp::DocumentChangeOperation::Edit(document_edit) => {
|
||||
let edits = document_edit
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| match edit {
|
||||
lsp::OneOf::Left(text_edit) => text_edit,
|
||||
lsp::OneOf::Right(annotated_text_edit) => {
|
||||
&annotated_text_edit.text_edit
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
apply_edits(
|
||||
&document_edit.text_document.uri,
|
||||
document_edit.text_document.version,
|
||||
edits,
|
||||
)
|
||||
.map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(ref changes) = workspace_edit.changes {
|
||||
log::debug!("workspace changes: {:?}", changes);
|
||||
for (i, (uri, text_edits)) in changes.iter().enumerate() {
|
||||
let text_edits = text_edits.to_vec();
|
||||
apply_edits(uri, None, text_edits).map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Precondition: `locations` should be non-empty.
|
||||
fn goto_impl(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
locations: Vec<lsp::Location>,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) {
|
||||
let cwdir = helix_loader::current_working_dir();
|
||||
let cwdir = helix_stdx::env::current_working_dir();
|
||||
|
||||
match locations.as_slice() {
|
||||
[location] => {
|
||||
jump_to_location(editor, location, offset_encoding, Action::Replace);
|
||||
}
|
||||
[] => {
|
||||
editor.set_error("No definition found.");
|
||||
}
|
||||
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
||||
_locations => {
|
||||
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
|
||||
jump_to_location(cx.editor, location, offset_encoding, action)
|
||||
@ -1079,7 +868,11 @@ fn goto_single_impl<P, F>(cx: &mut Context, feature: LanguageServerFeature, requ
|
||||
future,
|
||||
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
|
||||
let items = to_locations(response);
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
if items.is_empty() {
|
||||
editor.set_error("No definition found.");
|
||||
} else {
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1139,151 +932,19 @@ pub fn goto_reference(cx: &mut Context) {
|
||||
future,
|
||||
move |editor, compositor, response: Option<Vec<lsp::Location>>| {
|
||||
let items = response.unwrap_or_default();
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
if items.is_empty() {
|
||||
editor.set_error("No references found.");
|
||||
} else {
|
||||
goto_impl(editor, compositor, items, offset_encoding);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum SignatureHelpInvoked {
|
||||
Manual,
|
||||
Automatic,
|
||||
}
|
||||
|
||||
pub fn signature_help(cx: &mut Context) {
|
||||
signature_help_impl(cx, SignatureHelpInvoked::Manual)
|
||||
}
|
||||
|
||||
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
|
||||
let future = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||
.find_map(|language_server| {
|
||||
let pos = doc.position(view.id, language_server.offset_encoding());
|
||||
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
||||
});
|
||||
|
||||
let Some(future) = future else {
|
||||
// Do not show the message if signature help was invoked
|
||||
// automatically on backspace, trigger characters, etc.
|
||||
if invoked == SignatureHelpInvoked::Manual {
|
||||
cx.editor
|
||||
.set_error("No configured language server supports signature-help");
|
||||
}
|
||||
return;
|
||||
};
|
||||
signature_help_impl_with_future(cx, future.boxed(), invoked);
|
||||
}
|
||||
|
||||
pub fn signature_help_impl_with_future(
|
||||
cx: &mut Context,
|
||||
future: BoxFuture<'static, helix_lsp::Result<Value>>,
|
||||
invoked: SignatureHelpInvoked,
|
||||
) {
|
||||
cx.callback(
|
||||
future,
|
||||
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
|
||||
let config = &editor.config();
|
||||
|
||||
if !(config.lsp.auto_signature_help
|
||||
|| SignatureHelp::visible_popup(compositor).is_some()
|
||||
|| invoked == SignatureHelpInvoked::Manual)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
|
||||
// it very probably means the server was a little slow to respond and the user has
|
||||
// already moved on to something else, making a signature help popup will just be an
|
||||
// annoyance, see https://github.com/helix-editor/helix/issues/3112
|
||||
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match response {
|
||||
// According to the spec the response should be None if there
|
||||
// are no signatures, but some servers don't follow this.
|
||||
Some(s) if !s.signatures.is_empty() => s,
|
||||
_ => {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let doc = doc!(editor);
|
||||
let language = doc.language_name().unwrap_or("");
|
||||
|
||||
let signature = match response
|
||||
.signatures
|
||||
.get(response.active_signature.unwrap_or(0) as usize)
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
let mut contents = SignatureHelp::new(
|
||||
signature.label.clone(),
|
||||
language.to_string(),
|
||||
Arc::clone(&editor.syn_loader),
|
||||
);
|
||||
|
||||
let signature_doc = if config.lsp.display_signature_help_docs {
|
||||
signature.documentation.as_ref().map(|doc| match doc {
|
||||
lsp::Documentation::String(s) => s.clone(),
|
||||
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
contents.set_signature_doc(signature_doc);
|
||||
|
||||
let active_param_range = || -> Option<(usize, usize)> {
|
||||
let param_idx = signature
|
||||
.active_parameter
|
||||
.or(response.active_parameter)
|
||||
.unwrap_or(0) as usize;
|
||||
let param = signature.parameters.as_ref()?.get(param_idx)?;
|
||||
match ¶m.label {
|
||||
lsp::ParameterLabel::Simple(string) => {
|
||||
let start = signature.label.find(string.as_str())?;
|
||||
Some((start, start + string.len()))
|
||||
}
|
||||
lsp::ParameterLabel::LabelOffsets([start, end]) => {
|
||||
// LS sends offsets based on utf-16 based string representation
|
||||
// but highlighting in helix is done using byte offset.
|
||||
use helix_core::str_utils::char_to_byte_idx;
|
||||
let from = char_to_byte_idx(&signature.label, *start as usize);
|
||||
let to = char_to_byte_idx(&signature.label, *end as usize);
|
||||
Some((from, to))
|
||||
}
|
||||
}
|
||||
};
|
||||
contents.set_active_param_range(active_param_range());
|
||||
|
||||
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
|
||||
let mut popup = Popup::new(SignatureHelp::ID, contents)
|
||||
.position(old_popup.and_then(|p| p.get_position()))
|
||||
.position_bias(Open::Above)
|
||||
.ignore_escape_key(true);
|
||||
|
||||
// Don't create a popup if it intersects the auto-complete menu.
|
||||
let size = compositor.size();
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.as_mut()
|
||||
.map(|completion| completion.area(size, editor))
|
||||
.filter(|area| area.intersects(popup.area(size, editor)))
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
compositor.replace_or_push(SignatureHelp::ID, popup);
|
||||
},
|
||||
);
|
||||
cx.editor
|
||||
.handlers
|
||||
.trigger_signature_help(SignatureHelpInvoked::Manual, cx.editor)
|
||||
}
|
||||
|
||||
pub fn hover(cx: &mut Context) {
|
||||
@ -1408,7 +1069,7 @@ fn create_rename_prompt(
|
||||
|
||||
match block_on(future) {
|
||||
Ok(edits) => {
|
||||
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
|
||||
let _ = cx.editor.apply_workspace_edit(offset_encoding, &edits);
|
||||
}
|
||||
Err(err) => cx.editor.set_error(err.to_string()),
|
||||
}
|
||||
@ -1421,6 +1082,16 @@ fn create_rename_prompt(
|
||||
|
||||
let (view, doc) = current_ref!(cx.editor);
|
||||
|
||||
if doc
|
||||
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
cx.editor
|
||||
.set_error("No configured language server supports symbol renaming");
|
||||
return;
|
||||
}
|
||||
|
||||
let language_server_with_prepare_rename_support = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
|
||||
.find(|ls| {
|
||||
|
@ -7,8 +7,7 @@
|
||||
|
||||
use helix_core::fuzzy::fuzzy_match;
|
||||
use helix_core::indent::MAX_INDENT;
|
||||
use helix_core::{encoding, line_ending, path::get_canonicalized_path, shellwords::Shellwords};
|
||||
use helix_lsp::{OffsetEncoding, Url};
|
||||
use helix_core::{encoding, line_ending, shellwords::Shellwords};
|
||||
use helix_view::document::DEFAULT_LANGUAGE_NAME;
|
||||
use helix_view::editor::{Action, CloseError, ConfigEvent};
|
||||
use serde_json::Value;
|
||||
@ -111,7 +110,7 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
|
||||
ensure!(!args.is_empty(), "wrong argument count");
|
||||
for arg in args {
|
||||
let (path, pos) = args::parse_file(arg);
|
||||
let path = helix_core::path::expand_tilde(&path);
|
||||
let path = helix_stdx::path::expand_tilde(&path);
|
||||
// If the path is a directory, open a file picker on that directory and update the status
|
||||
// message
|
||||
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
|
||||
@ -483,7 +482,7 @@ fn set_indent_style(
|
||||
}
|
||||
|
||||
// Attempt to parse argument as an indent style.
|
||||
let style = match args.get(0) {
|
||||
let style = match args.first() {
|
||||
Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
|
||||
Some(Cow::Borrowed("0")) => Some(Tabs),
|
||||
Some(arg) => arg
|
||||
@ -535,7 +534,7 @@ fn set_line_ending(
|
||||
}
|
||||
|
||||
let arg = args
|
||||
.get(0)
|
||||
.first()
|
||||
.context("argument missing")?
|
||||
.to_ascii_lowercase();
|
||||
|
||||
@ -674,13 +673,15 @@ pub fn write_all_impl(
|
||||
let mut errors: Vec<&'static str> = Vec::new();
|
||||
let config = cx.editor.config();
|
||||
let jobs = &mut cx.jobs;
|
||||
let current_view = view!(cx.editor);
|
||||
|
||||
let saves: Vec<_> = cx
|
||||
.editor
|
||||
.documents
|
||||
.values_mut()
|
||||
.filter_map(|doc| {
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|id| {
|
||||
let doc = doc!(cx.editor, &id);
|
||||
if !doc.is_modified() {
|
||||
return None;
|
||||
}
|
||||
@ -691,22 +692,9 @@ pub fn write_all_impl(
|
||||
return None;
|
||||
}
|
||||
|
||||
// Look for a view to apply the formatting change to. If the document
|
||||
// is in the current view, just use that. Otherwise, since we don't
|
||||
// have any other metric available for better selection, just pick
|
||||
// the first view arbitrarily so that we still commit the document
|
||||
// state for undos. If somehow we have a document that has not been
|
||||
// initialized with any view, initialize it with the current view.
|
||||
let target_view = if doc.selections().contains_key(¤t_view.id) {
|
||||
current_view.id
|
||||
} else if let Some(view) = doc.selections().keys().next() {
|
||||
*view
|
||||
} else {
|
||||
doc.ensure_view_init(current_view.id);
|
||||
current_view.id
|
||||
};
|
||||
|
||||
Some((doc.id(), target_view))
|
||||
// Look for a view to apply the formatting change to.
|
||||
let target_view = cx.editor.get_synced_view_id(doc.id());
|
||||
Some((id, target_view))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -1090,18 +1078,17 @@ fn change_current_directory(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let dir = helix_core::path::expand_tilde(
|
||||
let dir = helix_stdx::path::expand_tilde(
|
||||
args.first()
|
||||
.context("target directory not provided")?
|
||||
.as_ref()
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
helix_loader::set_current_working_dir(dir)?;
|
||||
helix_stdx::env::set_current_working_dir(dir)?;
|
||||
|
||||
cx.editor.set_status(format!(
|
||||
"Current working directory is now {}",
|
||||
helix_loader::current_working_dir().display()
|
||||
helix_stdx::env::current_working_dir().display()
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
@ -1115,7 +1102,7 @@ fn show_current_directory(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let cwd = helix_loader::current_working_dir();
|
||||
let cwd = helix_stdx::env::current_working_dir();
|
||||
let message = format!("Current working directory is {}", cwd.display());
|
||||
|
||||
if cwd.exists() {
|
||||
@ -1502,7 +1489,7 @@ fn lsp_stop(
|
||||
|
||||
for doc in cx.editor.documents_mut() {
|
||||
if let Some(client) = doc.remove_language_server_by_name(ls_name) {
|
||||
doc.clear_diagnostics(client.id());
|
||||
doc.clear_diagnostics(Some(client.id()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1558,10 +1545,7 @@ fn find_highlight_at_cursor(
|
||||
let text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
let byte = text.char_to_byte(cursor);
|
||||
let node = syntax
|
||||
.tree()
|
||||
.root_node()
|
||||
.descendant_for_byte_range(byte, byte)?;
|
||||
let node = syntax.descendant_for_byte_range(byte, byte)?;
|
||||
// Query the same range as the one used in syntax highlighting.
|
||||
let range = {
|
||||
// Calculate viewport byte ranges:
|
||||
@ -2008,6 +1992,10 @@ fn language(
|
||||
|
||||
let id = doc.id();
|
||||
cx.editor.refresh_language_servers(id);
|
||||
let doc = doc_mut!(cx.editor);
|
||||
let diagnostics =
|
||||
Editor::doc_diagnostics(&cx.editor.language_servers, &cx.editor.diagnostics, doc);
|
||||
doc.replace_diagnostics(diagnostics, &[], None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -2085,7 +2073,7 @@ fn reflow(
|
||||
// - The configured text-width for this language in languages.toml
|
||||
// - The configured text-width in the config.toml
|
||||
let text_width: usize = args
|
||||
.get(0)
|
||||
.first()
|
||||
.map(|num| num.parse::<usize>())
|
||||
.transpose()?
|
||||
.or_else(|| doc.language_config().and_then(|config| config.text_width))
|
||||
@ -2124,11 +2112,7 @@ fn tree_sitter_subtree(
|
||||
let text = doc.text();
|
||||
let from = text.char_to_byte(primary_selection.from());
|
||||
let to = text.char_to_byte(primary_selection.to());
|
||||
if let Some(selected_node) = syntax
|
||||
.tree()
|
||||
.root_node()
|
||||
.descendant_for_byte_range(from, to)
|
||||
{
|
||||
if let Some(selected_node) = syntax.descendant_for_byte_range(from, to) {
|
||||
let mut contents = String::from("```tsq\n");
|
||||
helix_core::syntax::pretty_print_tree(&mut contents, selected_node)?;
|
||||
contents.push_str("\n```");
|
||||
@ -2419,66 +2403,14 @@ fn move_buffer(
|
||||
|
||||
ensure!(args.len() == 1, format!(":move takes one argument"));
|
||||
let doc = doc!(cx.editor);
|
||||
|
||||
let new_path = get_canonicalized_path(&PathBuf::from(args.first().unwrap().to_string()));
|
||||
let old_path = doc
|
||||
.path()
|
||||
.ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
|
||||
.context("Scratch buffer cannot be moved. Use :write instead")?
|
||||
.clone();
|
||||
let old_path_as_url = doc.url().unwrap();
|
||||
let new_path_as_url = Url::from_file_path(&new_path).unwrap();
|
||||
|
||||
let edits: Vec<(
|
||||
helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
|
||||
OffsetEncoding,
|
||||
String,
|
||||
)> = doc
|
||||
.language_servers()
|
||||
.map(|lsp| {
|
||||
(
|
||||
lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
|
||||
lsp.offset_encoding(),
|
||||
lsp.name().to_owned(),
|
||||
)
|
||||
})
|
||||
.filter(|(f, _, _)| f.is_some())
|
||||
.map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
|
||||
.collect();
|
||||
|
||||
for (lsp_reply, encoding, name) in edits {
|
||||
match lsp_reply {
|
||||
Ok(edit) => {
|
||||
if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
|
||||
log::error!(
|
||||
":move command failed to apply edits from lsp {}: {:?}",
|
||||
name,
|
||||
e
|
||||
);
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
|
||||
}
|
||||
};
|
||||
let new_path = args.first().unwrap().to_string();
|
||||
if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) {
|
||||
bail!("Could not move file: {err}");
|
||||
}
|
||||
|
||||
let doc = doc_mut!(cx.editor);
|
||||
|
||||
doc.set_path(Some(new_path.as_path()));
|
||||
if let Err(e) = std::fs::rename(&old_path, &new_path) {
|
||||
doc.set_path(Some(old_path.as_path()));
|
||||
bail!("Could not move file: {}", e);
|
||||
};
|
||||
|
||||
doc.language_servers().for_each(|lsp| {
|
||||
lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
|
||||
});
|
||||
|
||||
cx.editor
|
||||
.language_servers
|
||||
.file_event_handler
|
||||
.file_changed(new_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
20
helix-term/src/events.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use helix_event::{events, register_event};
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::{DocumentDidChange, SelectionDidChange};
|
||||
|
||||
use crate::commands;
|
||||
use crate::keymap::MappableCommand;
|
||||
|
||||
events! {
|
||||
OnModeSwitch<'a, 'cx> { old_mode: Mode, new_mode: Mode, cx: &'a mut commands::Context<'cx> }
|
||||
PostInsertChar<'a, 'cx> { c: char, cx: &'a mut commands::Context<'cx> }
|
||||
PostCommand<'a, 'cx> { command: & 'a MappableCommand, cx: &'a mut commands::Context<'cx> }
|
||||
}
|
||||
|
||||
pub fn register() {
|
||||
register_event::<OnModeSwitch>();
|
||||
register_event::<PostInsertChar>();
|
||||
register_event::<PostCommand>();
|
||||
register_event::<DocumentDidChange>();
|
||||
register_event::<SelectionDidChange>();
|
||||
}
|
30
helix-term/src/handlers.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use helix_event::AsyncHook;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::events;
|
||||
use crate::handlers::completion::CompletionHandler;
|
||||
use crate::handlers::signature_help::SignatureHelpHandler;
|
||||
|
||||
pub use completion::trigger_auto_completion;
|
||||
pub use helix_view::handlers::lsp::SignatureHelpInvoked;
|
||||
pub use helix_view::handlers::Handlers;
|
||||
|
||||
mod completion;
|
||||
mod signature_help;
|
||||
|
||||
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
events::register();
|
||||
|
||||
let completions = CompletionHandler::new(config).spawn();
|
||||
let signature_hints = SignatureHelpHandler::new().spawn();
|
||||
let handlers = Handlers {
|
||||
completions,
|
||||
signature_hints,
|
||||
};
|
||||
completion::register_hooks(&handlers);
|
||||
signature_help::register_hooks(&handlers);
|
||||
handlers
|
||||
}
|
465
helix-term/src/handlers/completion.rs
Normal file
@ -0,0 +1,465 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
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_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 tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::commands;
|
||||
use crate::compositor::Compositor;
|
||||
use crate::config::Config;
|
||||
use crate::events::{OnModeSwitch, PostCommand, PostInsertChar};
|
||||
use crate::job::{dispatch, dispatch_blocking};
|
||||
use crate::keymap::MappableCommand;
|
||||
use crate::ui::editor::InsertEvent;
|
||||
use crate::ui::lsp::SignatureHelp;
|
||||
use crate::ui::{self, CompletionItem, Popup};
|
||||
|
||||
use super::Handlers;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum TriggerKind {
|
||||
Auto,
|
||||
TriggerChar,
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Trigger {
|
||||
pos: usize,
|
||||
view: ViewId,
|
||||
doc: DocumentId,
|
||||
kind: TriggerKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
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>,
|
||||
config: Arc<ArcSwap<Config>>,
|
||||
}
|
||||
|
||||
impl CompletionHandler {
|
||||
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
|
||||
Self {
|
||||
config,
|
||||
request: None,
|
||||
trigger: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for CompletionHandler {
|
||||
type Event = CompletionEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
_old_timeout: Option<Instant>,
|
||||
) -> Option<Instant> {
|
||||
match event {
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor: trigger_pos,
|
||||
doc,
|
||||
view,
|
||||
} => {
|
||||
// techically it shouldn't be possible to switch views/documents in insert mode
|
||||
// but people may create weird keymaps/use the mouse so lets be extra careful
|
||||
if self
|
||||
.trigger
|
||||
.as_ref()
|
||||
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
|
||||
{
|
||||
self.trigger = Some(Trigger {
|
||||
pos: trigger_pos,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Auto,
|
||||
});
|
||||
}
|
||||
}
|
||||
CompletionEvent::TriggerChar { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.request = None;
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::TriggerChar,
|
||||
});
|
||||
}
|
||||
CompletionEvent::ManualTrigger { cursor, doc, view } => {
|
||||
// immediately request completions and drop all auto completion requests
|
||||
self.request = None;
|
||||
self.trigger = Some(Trigger {
|
||||
pos: cursor,
|
||||
view,
|
||||
doc,
|
||||
kind: TriggerKind::Manual,
|
||||
});
|
||||
// stop debouncing immediately and request the completion
|
||||
self.finish_debounce();
|
||||
return None;
|
||||
}
|
||||
CompletionEvent::Cancel => {
|
||||
self.trigger = None;
|
||||
self.request = None;
|
||||
}
|
||||
CompletionEvent::DeleteText { cursor } => {
|
||||
// if we deleted the original trigger, abort the completion
|
||||
if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) {
|
||||
self.trigger = None;
|
||||
self.request = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
self.config.load().editor.completion_timeout
|
||||
} else {
|
||||
// we want almost instant completions for trigger chars
|
||||
// and restarting completion requests. The small timeout here mainly
|
||||
// serves to better handle cases where the completion handler
|
||||
// may fall behind (so multiple events in the channel) and macros
|
||||
Duration::from_millis(5)
|
||||
};
|
||||
Instant::now() + timeout
|
||||
})
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let trigger = self.trigger.take().expect("debounce always has a trigger");
|
||||
let (tx, rx) = cancelation();
|
||||
self.request = Some(tx);
|
||||
dispatch_blocking(move |editor, compositor| {
|
||||
request_completion(trigger, rx, editor, compositor)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_completion(
|
||||
mut trigger: Trigger,
|
||||
cancel: CancelRx,
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
) {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.is_some()
|
||||
|| editor.mode != Mode::Insert
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let text = doc.text();
|
||||
let cursor = doc.selection(view.id).primary().cursor(text.slice(..));
|
||||
if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos {
|
||||
return;
|
||||
}
|
||||
// this looks odd... Why are we not using the trigger position from
|
||||
// the `trigger` here? Won't that mean that the trigger char doesn't get
|
||||
// send to the LS if we type fast enougn? Yes that is true but it's
|
||||
// not actually a problem. The LSP will resolve the completion to the identifier
|
||||
// anyway (in fact sending the later position is necessary to get the right results
|
||||
// from LSPs that provide incomplete completion list). We rely on trigger offset
|
||||
// and primary cursor matching for multi-cursor completions so this is definitely
|
||||
// necessary from our side too.
|
||||
trigger.pos = cursor;
|
||||
let trigger_text = text.slice(..cursor);
|
||||
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let mut futures: FuturesUnordered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|ls| {
|
||||
let language_server_id = ls.id();
|
||||
let offset_encoding = ls.offset_encoding();
|
||||
let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
|
||||
let doc_id = doc.identifier();
|
||||
let context = if trigger.kind == TriggerKind::Manual {
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||
trigger_character: None,
|
||||
}
|
||||
} else {
|
||||
let trigger_char =
|
||||
ls.capabilities()
|
||||
.completion_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| {
|
||||
provider
|
||||
.trigger_characters
|
||||
.as_deref()?
|
||||
.iter()
|
||||
.find(|&trigger| trigger_text.ends_with(trigger))
|
||||
});
|
||||
lsp::CompletionContext {
|
||||
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
trigger_character: trigger_char.cloned(),
|
||||
}
|
||||
};
|
||||
|
||||
let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
|
||||
async move {
|
||||
let json = completion_response.await?;
|
||||
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
||||
let items = match response {
|
||||
Some(lsp::CompletionResponse::Array(items)) => items,
|
||||
// TODO: do something with is_incomplete
|
||||
Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: _is_incomplete,
|
||||
items,
|
||||
})) => items,
|
||||
None => Vec::new(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|item| CompletionItem {
|
||||
item,
|
||||
language_server_id,
|
||||
resolved: false,
|
||||
})
|
||||
.collect();
|
||||
anyhow::Ok(items)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let future = async move {
|
||||
let mut items = Vec::new();
|
||||
while let Some(lsp_items) = futures.next().await {
|
||||
match lsp_items {
|
||||
Ok(mut lsp_items) => items.append(&mut lsp_items),
|
||||
Err(err) => {
|
||||
log::debug!("completion request failed: {err:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
items
|
||||
};
|
||||
|
||||
let savepoint = doc.savepoint(view);
|
||||
|
||||
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() {
|
||||
return;
|
||||
}
|
||||
dispatch(move |editor, compositor| {
|
||||
show_completion(editor, compositor, items, trigger, savepoint)
|
||||
})
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
fn show_completion(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
items: Vec<CompletionItem>,
|
||||
trigger: Trigger,
|
||||
savepoint: Arc<SavePoint>,
|
||||
) {
|
||||
let (view, doc) = current_ref!(editor);
|
||||
// check if the completion request is stale.
|
||||
//
|
||||
// Completions are completed asynchronously and therefore the user could
|
||||
//switch document/view or leave insert mode. In all of thoise cases the
|
||||
// completion should be discarded
|
||||
if editor.mode != Mode::Insert || view.id != trigger.view || doc.id() != trigger.doc {
|
||||
return;
|
||||
}
|
||||
|
||||
let size = compositor.size();
|
||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||
if ui.completion.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
|
||||
let signature_help_area = compositor
|
||||
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
|
||||
.map(|signature_help| signature_help.area(size, editor));
|
||||
// Delete the signature help popup if they intersect.
|
||||
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_auto_completion(
|
||||
tx: &Sender<CompletionEvent>,
|
||||
editor: &Editor,
|
||||
trigger_char_only: bool,
|
||||
) {
|
||||
let config = editor.config.load();
|
||||
if !config.auto_completion {
|
||||
return;
|
||||
}
|
||||
let (view, doc): (&helix_view::View, &helix_view::Document) = current_ref!(editor);
|
||||
let mut text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
text = doc.text().slice(..cursor);
|
||||
|
||||
let is_trigger_char = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||
.any(|ls| {
|
||||
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(triggers),
|
||||
..
|
||||
}) if triggers.iter().any(|trigger| text.ends_with(trigger)))
|
||||
});
|
||||
if is_trigger_char {
|
||||
send_blocking(
|
||||
tx,
|
||||
CompletionEvent::TriggerChar {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let is_auto_trigger = !trigger_char_only
|
||||
&& doc
|
||||
.text()
|
||||
.chars_at(cursor)
|
||||
.reversed()
|
||||
.take(config.completion_trigger_len as usize)
|
||||
.all(char_is_word);
|
||||
|
||||
if is_auto_trigger {
|
||||
send_blocking(
|
||||
tx,
|
||||
CompletionEvent::AutoTrigger {
|
||||
cursor,
|
||||
doc: doc.id(),
|
||||
view: view.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_completions(cx: &mut commands::Context, c: Option<char>) {
|
||||
cx.callback.push(Box::new(move |compositor, cx| {
|
||||
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||
if let Some(completion) = &mut editor_view.completion {
|
||||
completion.update_filter(c);
|
||||
if completion.is_empty() {
|
||||
editor_view.clear_completion(cx.editor);
|
||||
// clearing completions might mean we want to immediately rerequest them (usually
|
||||
// this occurs if typing a trigger char)
|
||||
if c.is_some() {
|
||||
trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn clear_completions(cx: &mut commands::Context) {
|
||||
cx.callback.push(Box::new(|compositor, cx| {
|
||||
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||
editor_view.clear_completion(cx.editor);
|
||||
}))
|
||||
}
|
||||
|
||||
fn completion_post_command_hook(
|
||||
tx: &Sender<CompletionEvent>,
|
||||
PostCommand { command, cx }: &mut PostCommand<'_, '_>,
|
||||
) -> anyhow::Result<()> {
|
||||
if cx.editor.mode == Mode::Insert {
|
||||
if cx.editor.last_completion.is_some() {
|
||||
match command {
|
||||
MappableCommand::Static {
|
||||
name: "delete_word_forward" | "delete_char_forward" | "completion",
|
||||
..
|
||||
} => (),
|
||||
MappableCommand::Static {
|
||||
name: "delete_char_backward",
|
||||
..
|
||||
} => update_completions(cx, None),
|
||||
_ => clear_completions(cx),
|
||||
}
|
||||
} else {
|
||||
let event = match command {
|
||||
MappableCommand::Static {
|
||||
name: "delete_char_backward" | "delete_word_forward" | "delete_char_forward",
|
||||
..
|
||||
} => {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let primary_cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
CompletionEvent::DeleteText {
|
||||
cursor: primary_cursor,
|
||||
}
|
||||
}
|
||||
// hacks: some commands are handeled elsewhere and we don't want to
|
||||
// cancel in that case
|
||||
MappableCommand::Static {
|
||||
name: "completion" | "insert_mode" | "append_mode",
|
||||
..
|
||||
} => return Ok(()),
|
||||
_ => CompletionEvent::Cancel,
|
||||
};
|
||||
send_blocking(tx, event);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event));
|
||||
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||
if event.old_mode == Mode::Insert {
|
||||
send_blocking(&tx, CompletionEvent::Cancel);
|
||||
clear_completions(event.cx);
|
||||
} else if event.new_mode == Mode::Insert {
|
||||
trigger_auto_completion(&tx, event.cx.editor, false)
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.completions.clone();
|
||||
register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
|
||||
if event.cx.editor.last_completion.is_some() {
|
||||
update_completions(event.cx, Some(event.c))
|
||||
} else {
|
||||
trigger_auto_completion(&tx, event.cx.editor, false);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
335
helix-term/src/handlers/signature_help.rs
Normal file
@ -0,0 +1,335 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use helix_event::{
|
||||
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||
};
|
||||
use helix_lsp::lsp;
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::{DocumentDidChange, SelectionDidChange};
|
||||
use helix_view::handlers::lsp::{SignatureHelpEvent, SignatureHelpInvoked};
|
||||
use helix_view::Editor;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::commands::Open;
|
||||
use crate::compositor::Compositor;
|
||||
use crate::events::{OnModeSwitch, PostInsertChar};
|
||||
use crate::handlers::Handlers;
|
||||
use crate::ui::lsp::SignatureHelp;
|
||||
use crate::ui::Popup;
|
||||
use crate::{job, ui};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Open,
|
||||
Closed,
|
||||
Pending { request: CancelTx },
|
||||
}
|
||||
|
||||
/// debounce timeout in ms, value taken from VSCode
|
||||
/// TODO: make this configurable?
|
||||
const TIMEOUT: u64 = 120;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct SignatureHelpHandler {
|
||||
trigger: Option<SignatureHelpInvoked>,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl SignatureHelpHandler {
|
||||
pub fn new() -> SignatureHelpHandler {
|
||||
SignatureHelpHandler {
|
||||
trigger: None,
|
||||
state: State::Closed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_event::AsyncHook for SignatureHelpHandler {
|
||||
type Event = SignatureHelpEvent;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<Instant> {
|
||||
match event {
|
||||
SignatureHelpEvent::Invoked => {
|
||||
self.trigger = Some(SignatureHelpInvoked::Manual);
|
||||
self.state = State::Closed;
|
||||
self.finish_debounce();
|
||||
return None;
|
||||
}
|
||||
SignatureHelpEvent::Trigger => {}
|
||||
SignatureHelpEvent::ReTrigger => {
|
||||
// don't retrigger if we aren't open/pending yet
|
||||
if matches!(self.state, State::Closed) {
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
SignatureHelpEvent::Cancel => {
|
||||
self.state = State::Closed;
|
||||
return None;
|
||||
}
|
||||
SignatureHelpEvent::RequestComplete { open } => {
|
||||
// don't cancel rerequest that was already triggered
|
||||
if let State::Pending { request } = &self.state {
|
||||
if !request.is_closed() {
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
self.state = if open { State::Open } else { State::Closed };
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
if self.trigger.is_none() {
|
||||
self.trigger = Some(SignatureHelpInvoked::Automatic)
|
||||
}
|
||||
Some(Instant::now() + Duration::from_millis(TIMEOUT))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_signature_help(
|
||||
editor: &mut Editor,
|
||||
invoked: SignatureHelpInvoked,
|
||||
cancel: CancelRx,
|
||||
) {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
|
||||
let future = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||
.find_map(|language_server| {
|
||||
let pos = doc.position(view.id, language_server.offset_encoding());
|
||||
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
||||
});
|
||||
|
||||
let Some(future) = future else {
|
||||
// Do not show the message if signature help was invoked
|
||||
// automatically on backspace, trigger characters, etc.
|
||||
if invoked == SignatureHelpInvoked::Manual {
|
||||
editor
|
||||
.set_error("No configured language server supports signature-help");
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
match cancelable_future(future, cancel).await {
|
||||
Some(Ok(res)) => {
|
||||
job::dispatch(move |editor, compositor| {
|
||||
show_signature_help(editor, compositor, invoked, res)
|
||||
})
|
||||
.await
|
||||
}
|
||||
Some(Err(err)) => log::error!("signature help request failed: {err}"),
|
||||
None => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn show_signature_help(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
invoked: SignatureHelpInvoked,
|
||||
response: Option<lsp::SignatureHelp>,
|
||||
) {
|
||||
let config = &editor.config();
|
||||
|
||||
if !(config.lsp.auto_signature_help
|
||||
|| SignatureHelp::visible_popup(compositor).is_some()
|
||||
|| invoked == SignatureHelpInvoked::Manual)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
|
||||
// it very probably means the server was a little slow to respond and the user has
|
||||
// already moved on to something else, making a signature help popup will just be an
|
||||
// annoyance, see https://github.com/helix-editor/helix/issues/3112
|
||||
// For the most part this should not be needed as the request gets canceled automatically now
|
||||
// but it's technically possible for the mode change to just preempt this callback so better safe than sorry
|
||||
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match response {
|
||||
// According to the spec the response should be None if there
|
||||
// are no signatures, but some servers don't follow this.
|
||||
Some(s) if !s.signatures.is_empty() => s,
|
||||
_ => {
|
||||
send_blocking(
|
||||
&editor.handlers.signature_hints,
|
||||
SignatureHelpEvent::RequestComplete { open: false },
|
||||
);
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
return;
|
||||
}
|
||||
};
|
||||
send_blocking(
|
||||
&editor.handlers.signature_hints,
|
||||
SignatureHelpEvent::RequestComplete { open: true },
|
||||
);
|
||||
|
||||
let doc = doc!(editor);
|
||||
let language = doc.language_name().unwrap_or("");
|
||||
|
||||
let signature = match response
|
||||
.signatures
|
||||
.get(response.active_signature.unwrap_or(0) as usize)
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
let mut contents = SignatureHelp::new(
|
||||
signature.label.clone(),
|
||||
language.to_string(),
|
||||
Arc::clone(&editor.syn_loader),
|
||||
);
|
||||
|
||||
let signature_doc = if config.lsp.display_signature_help_docs {
|
||||
signature.documentation.as_ref().map(|doc| match doc {
|
||||
lsp::Documentation::String(s) => s.clone(),
|
||||
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
contents.set_signature_doc(signature_doc);
|
||||
|
||||
let active_param_range = || -> Option<(usize, usize)> {
|
||||
let param_idx = signature
|
||||
.active_parameter
|
||||
.or(response.active_parameter)
|
||||
.unwrap_or(0) as usize;
|
||||
let param = signature.parameters.as_ref()?.get(param_idx)?;
|
||||
match ¶m.label {
|
||||
lsp::ParameterLabel::Simple(string) => {
|
||||
let start = signature.label.find(string.as_str())?;
|
||||
Some((start, start + string.len()))
|
||||
}
|
||||
lsp::ParameterLabel::LabelOffsets([start, end]) => {
|
||||
// LS sends offsets based on utf-16 based string representation
|
||||
// but highlighting in helix is done using byte offset.
|
||||
use helix_core::str_utils::char_to_byte_idx;
|
||||
let from = char_to_byte_idx(&signature.label, *start as usize);
|
||||
let to = char_to_byte_idx(&signature.label, *end as usize);
|
||||
Some((from, to))
|
||||
}
|
||||
}
|
||||
};
|
||||
contents.set_active_param_range(active_param_range());
|
||||
|
||||
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
|
||||
let mut popup = Popup::new(SignatureHelp::ID, contents)
|
||||
.position(old_popup.and_then(|p| p.get_position()))
|
||||
.position_bias(Open::Above)
|
||||
.ignore_escape_key(true);
|
||||
|
||||
// Don't create a popup if it intersects the auto-complete menu.
|
||||
let size = compositor.size();
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.as_mut()
|
||||
.map(|completion| completion.area(size, editor))
|
||||
.filter(|area| area.intersects(popup.area(size, editor)))
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
compositor.replace_or_push(SignatureHelp::ID, popup);
|
||||
}
|
||||
|
||||
fn signature_help_post_insert_char_hook(
|
||||
tx: &Sender<SignatureHelpEvent>,
|
||||
PostInsertChar { cx, .. }: &mut PostInsertChar<'_, '_>,
|
||||
) -> anyhow::Result<()> {
|
||||
if !cx.editor.config().lsp.auto_signature_help {
|
||||
return Ok(());
|
||||
}
|
||||
let (view, doc) = current!(cx.editor);
|
||||
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
|
||||
let Some(language_server) = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||
.next()
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let capabilities = language_server.capabilities();
|
||||
|
||||
if let lsp::ServerCapabilities {
|
||||
signature_help_provider:
|
||||
Some(lsp::SignatureHelpOptions {
|
||||
trigger_characters: Some(triggers),
|
||||
// TODO: retrigger_characters
|
||||
..
|
||||
}),
|
||||
..
|
||||
} = capabilities
|
||||
{
|
||||
let mut text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
text = text.slice(..cursor);
|
||||
if triggers.iter().any(|trigger| text.ends_with(trigger)) {
|
||||
send_blocking(tx, SignatureHelpEvent::Trigger)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||
match (event.old_mode, event.new_mode) {
|
||||
(Mode::Insert, _) => {
|
||||
send_blocking(&tx, SignatureHelpEvent::Cancel);
|
||||
event.cx.callback.push(Box::new(|compositor, _| {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
}));
|
||||
}
|
||||
(_, Mode::Insert) => {
|
||||
if event.cx.editor.config().lsp.auto_signature_help {
|
||||
send_blocking(&tx, SignatureHelpEvent::Trigger);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(
|
||||
move |event: &mut PostInsertChar<'_, '_>| signature_help_post_insert_char_hook(&tx, event)
|
||||
);
|
||||
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||
if event.doc.config.load().lsp.auto_signature_help {
|
||||
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let tx = handlers.signature_hints.clone();
|
||||
register_hook!(move |event: &mut SelectionDidChange<'_>| {
|
||||
if event.doc.config.load().lsp.auto_signature_help {
|
||||
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
@ -145,7 +145,7 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let mut headings = vec!["Language", "LSP", "DAP"];
|
||||
let mut headings = vec!["Language", "LSP", "DAP", "Formatter"];
|
||||
|
||||
for feat in TsFeature::all() {
|
||||
headings.push(feat.short_title())
|
||||
@ -182,7 +182,7 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
.sort_unstable_by_key(|l| l.language_id.clone());
|
||||
|
||||
let check_binary = |cmd: Option<&str>| match cmd {
|
||||
Some(cmd) => match which::which(cmd) {
|
||||
Some(cmd) => match helix_stdx::env::which(cmd) {
|
||||
Ok(_) => column(&format!("✓ {}", cmd), Color::Green),
|
||||
Err(_) => column(&format!("✘ {}", cmd), Color::Red),
|
||||
},
|
||||
@ -203,6 +203,12 @@ pub fn languages_all() -> std::io::Result<()> {
|
||||
let dap = lang.debugger.as_ref().map(|dap| dap.command.as_str());
|
||||
check_binary(dap);
|
||||
|
||||
let formatter = lang
|
||||
.formatter
|
||||
.as_ref()
|
||||
.map(|formatter| formatter.command.as_str());
|
||||
check_binary(formatter);
|
||||
|
||||
for ts_feat in TsFeature::all() {
|
||||
match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() {
|
||||
true => column("✓", Color::Green),
|
||||
@ -285,6 +291,13 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
|
||||
lang.debugger.as_ref().map(|dap| dap.command.to_string()),
|
||||
)?;
|
||||
|
||||
probe_protocol(
|
||||
"formatter",
|
||||
lang.formatter
|
||||
.as_ref()
|
||||
.map(|formatter| formatter.command.to_string()),
|
||||
)?;
|
||||
|
||||
for ts_feat in TsFeature::all() {
|
||||
probe_treesitter_feature(&lang_str, *ts_feat)?
|
||||
}
|
||||
@ -309,7 +322,7 @@ fn probe_protocols<'a, I: Iterator<Item = &'a str> + 'a>(
|
||||
writeln!(stdout)?;
|
||||
|
||||
for cmd in server_cmds {
|
||||
let (path, icon) = match which::which(cmd) {
|
||||
let (path, icon) = match helix_stdx::env::which(cmd) {
|
||||
Ok(path) => (path.display().to_string().green(), "✓".green()),
|
||||
Err(_) => (format!("'{}' not found in $PATH", cmd).red(), "✘".red()),
|
||||
};
|
||||
@ -331,7 +344,7 @@ fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::R
|
||||
writeln!(stdout, "Configured {}: {}", protocol_name, cmd_name)?;
|
||||
|
||||
if let Some(cmd) = server_cmd {
|
||||
let path = match which::which(&cmd) {
|
||||
let path = match helix_stdx::env::which(&cmd) {
|
||||
Ok(path) => path.display().to_string().green(),
|
||||
Err(_) => format!("'{}' not found in $PATH", cmd).red(),
|
||||
};
|
||||
|
@ -1,13 +1,37 @@
|
||||
use helix_event::status::StatusMessage;
|
||||
use helix_event::{runtime_local, send_blocking};
|
||||
use helix_view::Editor;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::compositor::Compositor;
|
||||
|
||||
use futures_util::future::{BoxFuture, Future, FutureExt};
|
||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||
|
||||
pub type EditorCompositorCallback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
|
||||
pub type EditorCallback = Box<dyn FnOnce(&mut Editor) + Send>;
|
||||
|
||||
runtime_local! {
|
||||
static JOB_QUEUE: OnceCell<Sender<Callback>> = OnceCell::new();
|
||||
}
|
||||
|
||||
pub async fn dispatch_callback(job: Callback) {
|
||||
let _ = JOB_QUEUE.wait().send(job).await;
|
||||
}
|
||||
|
||||
pub async fn dispatch(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
|
||||
let _ = JOB_QUEUE
|
||||
.wait()
|
||||
.send(Callback::EditorCompositor(Box::new(job)))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub fn dispatch_blocking(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
|
||||
let jobs = JOB_QUEUE.wait();
|
||||
send_blocking(jobs, Callback::EditorCompositor(Box::new(job)))
|
||||
}
|
||||
|
||||
pub enum Callback {
|
||||
EditorCompositor(EditorCompositorCallback),
|
||||
Editor(EditorCallback),
|
||||
@ -21,11 +45,11 @@ pub struct Job {
|
||||
pub wait: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Jobs {
|
||||
pub futures: FuturesUnordered<JobFuture>,
|
||||
/// These are the ones that need to complete before we exit.
|
||||
/// jobs that need to complete before we exit.
|
||||
pub wait_futures: FuturesUnordered<JobFuture>,
|
||||
pub callbacks: Receiver<Callback>,
|
||||
pub status_messages: Receiver<StatusMessage>,
|
||||
}
|
||||
|
||||
impl Job {
|
||||
@ -52,8 +76,16 @@ pub fn wait_before_exiting(mut self) -> Self {
|
||||
}
|
||||
|
||||
impl Jobs {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
let (tx, rx) = channel(1024);
|
||||
let _ = JOB_QUEUE.set(tx);
|
||||
let status_messages = helix_event::status::setup();
|
||||
Self {
|
||||
wait_futures: FuturesUnordered::new(),
|
||||
callbacks: rx,
|
||||
status_messages,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
|
||||
@ -85,18 +117,17 @@ pub fn handle_callback(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
|
||||
tokio::select! {
|
||||
event = self.futures.next() => { event }
|
||||
event = self.wait_futures.next() => { event }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&self, j: Job) {
|
||||
if j.wait {
|
||||
self.wait_futures.push(j.future);
|
||||
} else {
|
||||
self.futures.push(j.future);
|
||||
tokio::spawn(async move {
|
||||
match j.future.await {
|
||||
Ok(Some(cb)) => dispatch_callback(cb).await,
|
||||
Ok(None) => (),
|
||||
Err(err) => helix_event::status::report(err).await,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,7 +319,7 @@ pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
|
||||
self.sticky = None;
|
||||
}
|
||||
|
||||
let first = self.state.get(0).unwrap_or(&key);
|
||||
let first = self.state.first().unwrap_or(&key);
|
||||
let trie_node = match self.sticky {
|
||||
Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())),
|
||||
None => Cow::Borrowed(keymap),
|
||||
|
@ -6,13 +6,20 @@
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
pub mod health;
|
||||
pub mod job;
|
||||
pub mod keymap;
|
||||
pub mod ui;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use futures_util::Future;
|
||||
mod handlers;
|
||||
|
||||
use ignore::DirEntry;
|
||||
use url::Url;
|
||||
|
||||
pub use keymap::macros::*;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
@ -47,3 +54,22 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Opens URL in external program.
|
||||
fn open_external_url_callback(
|
||||
url: Url,
|
||||
) -> impl Future<Output = Result<job::Callback, anyhow::Error>> + Send + 'static {
|
||||
let commands = open::commands(url.as_str());
|
||||
async {
|
||||
for cmd in commands {
|
||||
let mut command = tokio::process::Command::new(cmd.get_program());
|
||||
command.args(cmd.get_args());
|
||||
if command.output().await.is_ok() {
|
||||
return Ok(job::Callback::Editor(Box::new(|_| {})));
|
||||
}
|
||||
}
|
||||
Ok(job::Callback::Editor(Box::new(move |editor| {
|
||||
editor.set_error("Opening URL in external program failed")
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
@ -118,16 +118,16 @@ async fn main_impl() -> Result<i32> {
|
||||
|
||||
// Before setting the working directory, resolve all the paths in args.files
|
||||
for (path, _) in args.files.iter_mut() {
|
||||
*path = helix_core::path::get_canonicalized_path(path);
|
||||
*path = helix_stdx::path::canonicalize(&path);
|
||||
}
|
||||
|
||||
// NOTE: Set the working directory early so the correct configuration is loaded. Be aware that
|
||||
// Application::new() depends on this logic so it must be updated if this changes.
|
||||
if let Some(path) = &args.working_directory {
|
||||
helix_loader::set_current_working_dir(path)?;
|
||||
helix_stdx::env::set_current_working_dir(path)?;
|
||||
} else if let Some((path, _)) = args.files.first().filter(|p| p.0.is_dir()) {
|
||||
// If the first file is a directory, it will be the working directory unless -w was specified
|
||||
helix_loader::set_current_working_dir(path)?;
|
||||
helix_stdx::env::set_current_working_dir(path)?;
|
||||
}
|
||||
|
||||
let config = match Config::load_default() {
|
||||
|
@ -1,8 +1,12 @@
|
||||
use crate::compositor::{Component, Context, Event, EventResult};
|
||||
use crate::{
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
handlers::trigger_auto_completion,
|
||||
};
|
||||
use helix_view::{
|
||||
document::SavePoint,
|
||||
editor::CompleteAction,
|
||||
graphics::Margin,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
theme::{Modifier, Style},
|
||||
ViewId,
|
||||
};
|
||||
@ -10,7 +14,7 @@
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use helix_core::{Change, Transaction};
|
||||
use helix_core::{chars, Change, Transaction};
|
||||
use helix_view::{graphics::Rect, Document, Editor};
|
||||
|
||||
use crate::commands;
|
||||
@ -95,10 +99,9 @@ pub struct CompletionItem {
|
||||
/// Wraps a Menu.
|
||||
pub struct Completion {
|
||||
popup: Popup<Menu<CompletionItem>>,
|
||||
start_offset: usize,
|
||||
#[allow(dead_code)]
|
||||
trigger_offset: usize,
|
||||
// TODO: maintain a completioncontext with trigger kind & trigger char
|
||||
filter: String,
|
||||
}
|
||||
|
||||
impl Completion {
|
||||
@ -108,7 +111,6 @@ pub fn new(
|
||||
editor: &Editor,
|
||||
savepoint: Arc<SavePoint>,
|
||||
mut items: Vec<CompletionItem>,
|
||||
start_offset: usize,
|
||||
trigger_offset: usize,
|
||||
) -> Self {
|
||||
let preview_completion_insert = editor.config().preview_completion_insert;
|
||||
@ -246,7 +248,7 @@ macro_rules! language_server {
|
||||
// (also without sending the transaction to the LS) *before any further transaction is applied*.
|
||||
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
|
||||
// is applied to).
|
||||
if editor.last_completion.is_none() {
|
||||
if matches!(editor.last_completion, Some(CompleteAction::Triggered)) {
|
||||
editor.last_completion = Some(CompleteAction::Selected {
|
||||
savepoint: doc.savepoint(view),
|
||||
})
|
||||
@ -324,8 +326,18 @@ macro_rules! language_server {
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
}
|
||||
// we could have just inserted a trigger char (like a `crate::` completion for rust
|
||||
// so we want to retrigger immediately when accepting a completion.
|
||||
trigger_auto_completion(&editor.handlers.completions, editor, true);
|
||||
}
|
||||
};
|
||||
|
||||
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
|
||||
if event != PromptEvent::Update {
|
||||
editor
|
||||
.handlers
|
||||
.trigger_signature_help(SignatureHelpInvoked::Automatic, editor);
|
||||
}
|
||||
});
|
||||
|
||||
let margin = if editor.menu_border() {
|
||||
@ -339,14 +351,30 @@ macro_rules! language_server {
|
||||
.ignore_escape_key(true)
|
||||
.margin(margin);
|
||||
|
||||
let (view, doc) = current_ref!(editor);
|
||||
let text = doc.text().slice(..);
|
||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||
let offset = text
|
||||
.chars_at(cursor)
|
||||
.reversed()
|
||||
.take_while(|ch| chars::char_is_word(*ch))
|
||||
.count();
|
||||
let start_offset = cursor.saturating_sub(offset);
|
||||
|
||||
let fragment = doc.text().slice(start_offset..cursor);
|
||||
let mut completion = Self {
|
||||
popup,
|
||||
start_offset,
|
||||
trigger_offset,
|
||||
// TODO: expand nucleo api to allow moving straight to a Utf32String here
|
||||
// and avoid allocation during matching
|
||||
filter: String::from(fragment),
|
||||
};
|
||||
|
||||
// need to recompute immediately in case start_offset != trigger_offset
|
||||
completion.recompute_filter(editor);
|
||||
completion
|
||||
.popup
|
||||
.contents_mut()
|
||||
.score(&completion.filter, false);
|
||||
|
||||
completion
|
||||
}
|
||||
@ -366,39 +394,22 @@ fn resolve_completion_item(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recompute_filter(&mut self, editor: &Editor) {
|
||||
/// Appends (`c: Some(c)`) or removes (`c: None`) a character to/from the filter
|
||||
/// this should be called whenever the user types or deletes a character in insert mode.
|
||||
pub fn update_filter(&mut self, c: Option<char>) {
|
||||
// recompute menu based on matches
|
||||
let menu = self.popup.contents_mut();
|
||||
let (view, doc) = current_ref!(editor);
|
||||
|
||||
// cx.hooks()
|
||||
// cx.add_hook(enum type, ||)
|
||||
// cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
|
||||
// callback with editor & compositor
|
||||
//
|
||||
// trigger_hook sends event into channel, that's consumed in the global loop and
|
||||
// triggers all registered callbacks
|
||||
// TODO: hooks should get processed immediately so maybe do it after select!(), before
|
||||
// looping?
|
||||
|
||||
let cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
if self.trigger_offset <= cursor {
|
||||
let fragment = doc.text().slice(self.start_offset..cursor);
|
||||
let text = Cow::from(fragment);
|
||||
// TODO: logic is same as ui/picker
|
||||
menu.score(&text);
|
||||
} else {
|
||||
// we backspaced before the start offset, clear the menu
|
||||
// this will cause the editor to remove the completion popup
|
||||
menu.clear();
|
||||
match c {
|
||||
Some(c) => self.filter.push(c),
|
||||
None => {
|
||||
self.filter.pop();
|
||||
if self.filter.is_empty() {
|
||||
menu.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, cx: &mut commands::Context) {
|
||||
self.recompute_filter(cx.editor)
|
||||
menu.score(&self.filter, c.is_some());
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
commands::{self, OnKeyCallback},
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
job::{self, Callback},
|
||||
events::{OnModeSwitch, PostCommand},
|
||||
key,
|
||||
keymap::{KeymapResult, Keymaps},
|
||||
ui::{
|
||||
@ -33,8 +33,8 @@
|
||||
|
||||
use tui::{buffer::Buffer as Surface, text::Span};
|
||||
|
||||
use super::document::LineDecoration;
|
||||
use super::{completion::CompletionItem, statusline};
|
||||
use super::{document::LineDecoration, lsp::SignatureHelp};
|
||||
|
||||
pub struct EditorView {
|
||||
pub keymaps: Keymaps,
|
||||
@ -386,7 +386,7 @@ pub fn doc_diagnostics_highlights(
|
||||
let mut warning_vec = Vec::new();
|
||||
let mut error_vec = Vec::new();
|
||||
|
||||
for diagnostic in doc.shown_diagnostics() {
|
||||
for diagnostic in doc.diagnostics() {
|
||||
// Separate diagnostics into different Vecs by severity.
|
||||
let (vec, scope) = match diagnostic.severity {
|
||||
Some(Severity::Info) => (&mut info_vec, info),
|
||||
@ -684,7 +684,7 @@ pub fn render_diagnostics(
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
|
||||
let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
|
||||
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
|
||||
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
|
||||
});
|
||||
|
||||
@ -835,35 +835,26 @@ fn handle_keymap_event(
|
||||
|
||||
let mut execute_command = |command: &commands::MappableCommand| {
|
||||
command.execute(cxt);
|
||||
let current_mode = cxt.editor.mode();
|
||||
match (last_mode, current_mode) {
|
||||
(Mode::Normal, Mode::Insert) => {
|
||||
// HAXX: if we just entered insert mode from normal, clear key buf
|
||||
// and record the command that got us into this mode.
|
||||
helix_event::dispatch(PostCommand { command, cx: cxt });
|
||||
|
||||
let current_mode = cxt.editor.mode();
|
||||
if current_mode != last_mode {
|
||||
helix_event::dispatch(OnModeSwitch {
|
||||
old_mode: last_mode,
|
||||
new_mode: current_mode,
|
||||
cx: cxt,
|
||||
});
|
||||
|
||||
// HAXX: if we just entered insert mode from normal, clear key buf
|
||||
// and record the command that got us into this mode.
|
||||
if current_mode == Mode::Insert {
|
||||
// how we entered insert mode is important, and we should track that so
|
||||
// we can repeat the side effect.
|
||||
self.last_insert.0 = command.clone();
|
||||
self.last_insert.1.clear();
|
||||
|
||||
commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic);
|
||||
}
|
||||
(Mode::Insert, Mode::Normal) => {
|
||||
// if exiting insert mode, remove completion
|
||||
self.clear_completion(cxt.editor);
|
||||
cxt.editor.completion_request_handle = None;
|
||||
|
||||
// TODO: Use an on_mode_change hook to remove signature help
|
||||
cxt.jobs.callback(async {
|
||||
let call: job::Callback =
|
||||
Callback::EditorCompositor(Box::new(|_editor, compositor| {
|
||||
compositor.remove(SignatureHelp::ID);
|
||||
}));
|
||||
Ok(call)
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
last_mode = current_mode;
|
||||
};
|
||||
|
||||
@ -991,12 +982,10 @@ pub fn set_completion(
|
||||
editor: &mut Editor,
|
||||
savepoint: Arc<SavePoint>,
|
||||
items: Vec<CompletionItem>,
|
||||
start_offset: usize,
|
||||
trigger_offset: usize,
|
||||
size: Rect,
|
||||
) -> Option<Rect> {
|
||||
let mut completion =
|
||||
Completion::new(editor, savepoint, items, start_offset, trigger_offset);
|
||||
let mut completion = Completion::new(editor, savepoint, items, trigger_offset);
|
||||
|
||||
if completion.is_empty() {
|
||||
// skip if we got no completion results
|
||||
@ -1004,7 +993,7 @@ pub fn set_completion(
|
||||
}
|
||||
|
||||
let area = completion.area(size, editor);
|
||||
editor.last_completion = None;
|
||||
editor.last_completion = Some(CompleteAction::Triggered);
|
||||
self.last_insert.1.push(InsertEvent::TriggerCompletion);
|
||||
|
||||
// TODO : propagate required size on resize to completion too
|
||||
@ -1017,6 +1006,7 @@ pub fn clear_completion(&mut self, editor: &mut Editor) {
|
||||
self.completion = None;
|
||||
if let Some(last_completion) = editor.last_completion.take() {
|
||||
match last_completion {
|
||||
CompleteAction::Triggered => (),
|
||||
CompleteAction::Applied {
|
||||
trigger_offset,
|
||||
changes,
|
||||
@ -1030,9 +1020,6 @@ pub fn clear_completion(&mut self, editor: &mut Editor) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any savepoints
|
||||
editor.clear_idle_timer(); // don't retrigger
|
||||
}
|
||||
|
||||
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
||||
@ -1046,13 +1033,7 @@ pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult
|
||||
};
|
||||
}
|
||||
|
||||
if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion {
|
||||
return EventResult::Ignored(None);
|
||||
}
|
||||
|
||||
crate::commands::insert::idle_completion(cx);
|
||||
|
||||
EventResult::Consumed(None)
|
||||
EventResult::Ignored(None)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1265,7 +1246,7 @@ fn handle_event(
|
||||
editor: context.editor,
|
||||
count: None,
|
||||
register: None,
|
||||
callback: None,
|
||||
callback: Vec::new(),
|
||||
on_next_key_callback: None,
|
||||
jobs: context.jobs,
|
||||
};
|
||||
@ -1302,8 +1283,6 @@ fn handle_event(
|
||||
cx.editor.status_msg = None;
|
||||
|
||||
let mode = cx.editor.mode();
|
||||
let (view, _) = current!(cx.editor);
|
||||
let focus = view.id;
|
||||
|
||||
if let Some(on_next_key) = self.on_next_key.take() {
|
||||
// if there's a command waiting input, do that first
|
||||
@ -1340,12 +1319,6 @@ fn handle_event(
|
||||
if callback.is_some() {
|
||||
// assume close_fn
|
||||
self.clear_completion(cx.editor);
|
||||
|
||||
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
|
||||
commands::signature_help_impl(
|
||||
&mut cx,
|
||||
commands::SignatureHelpInvoked::Automatic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1356,14 +1329,6 @@ fn handle_event(
|
||||
|
||||
// record last_insert key
|
||||
self.last_insert.1.push(InsertEvent::Key(key));
|
||||
|
||||
// lastly we recalculate completion
|
||||
if let Some(completion) = &mut self.completion {
|
||||
completion.update(&mut cx);
|
||||
if completion.is_empty() {
|
||||
self.clear_completion(cx.editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mode => self.command_mode(mode, &mut cx, key),
|
||||
@ -1377,7 +1342,7 @@ fn handle_event(
|
||||
}
|
||||
|
||||
// appease borrowck
|
||||
let callback = cx.callback.take();
|
||||
let callbacks = take(&mut cx.callback);
|
||||
|
||||
// if the command consumed the last view, skip the render.
|
||||
// on the next loop cycle the Application will then terminate.
|
||||
@ -1385,21 +1350,27 @@ fn handle_event(
|
||||
return EventResult::Ignored(None);
|
||||
}
|
||||
|
||||
// if the focused view still exists and wasn't closed
|
||||
if cx.editor.tree.contains(focus) {
|
||||
let config = cx.editor.config();
|
||||
let mode = cx.editor.mode();
|
||||
let view = view_mut!(cx.editor, focus);
|
||||
let doc = doc_mut!(cx.editor, &view.doc);
|
||||
let config = cx.editor.config();
|
||||
let mode = cx.editor.mode();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
view.ensure_cursor_in_view(doc, config.scrolloff);
|
||||
view.ensure_cursor_in_view(doc, config.scrolloff);
|
||||
|
||||
// Store a history state if not in insert mode. This also takes care of
|
||||
// committing changes when leaving insert mode.
|
||||
if mode != Mode::Insert {
|
||||
doc.append_changes_to_history(view);
|
||||
}
|
||||
// Store a history state if not in insert mode. This also takes care of
|
||||
// committing changes when leaving insert mode.
|
||||
if mode != Mode::Insert {
|
||||
doc.append_changes_to_history(view);
|
||||
}
|
||||
let callback = if callbacks.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let callback: crate::compositor::Callback = Box::new(move |compositor, cx| {
|
||||
for callback in callbacks {
|
||||
callback(compositor, cx)
|
||||
}
|
||||
});
|
||||
Some(callback)
|
||||
};
|
||||
|
||||
EventResult::Consumed(callback)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||
|
||||
use helix_core::{
|
||||
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
|
||||
@ -209,7 +209,7 @@ fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
|
||||
|
||||
list_stack.push(list);
|
||||
}
|
||||
Event::End(Tag::List(_)) => {
|
||||
Event::End(TagEnd::List(_)) => {
|
||||
list_stack.pop();
|
||||
|
||||
// whenever top-level list closes, empty line
|
||||
@ -249,7 +249,10 @@ fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
|
||||
Event::End(tag) => {
|
||||
tags.pop();
|
||||
match tag {
|
||||
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) | Tag::Item => {
|
||||
TagEnd::Heading(_)
|
||||
| TagEnd::Paragraph
|
||||
| TagEnd::CodeBlock
|
||||
| TagEnd::Item => {
|
||||
push_line(&mut spans, &mut lines);
|
||||
}
|
||||
_ => (),
|
||||
@ -257,7 +260,7 @@ fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
|
||||
|
||||
// whenever heading, code block or paragraph closes, empty line
|
||||
match tag {
|
||||
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) => {
|
||||
TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::CodeBlock => {
|
||||
lines.push(Spans::default());
|
||||
}
|
||||
_ => (),
|
||||
@ -279,7 +282,7 @@ fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
|
||||
lines.extend(tui_text.lines.into_iter());
|
||||
} else {
|
||||
let style = match tags.last() {
|
||||
Some(Tag::Heading(level, ..)) => match level {
|
||||
Some(Tag::Heading { level, .. }) => match level {
|
||||
HeadingLevel::H1 => heading_styles[0],
|
||||
HeadingLevel::H2 => heading_styles[1],
|
||||
HeadingLevel::H3 => heading_styles[2],
|
||||
|
@ -96,20 +96,34 @@ pub fn new(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score(&mut self, pattern: &str) {
|
||||
// reuse the matches allocation
|
||||
self.matches.clear();
|
||||
pub fn score(&mut self, pattern: &str, incremental: bool) {
|
||||
let mut matcher = MATCHER.lock();
|
||||
matcher.config = Config::DEFAULT;
|
||||
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
|
||||
let mut buf = Vec::new();
|
||||
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
|
||||
let text = option.filter_text(&self.editor_data);
|
||||
pattern
|
||||
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
|
||||
.map(|score| (i as u32, score as u32))
|
||||
});
|
||||
self.matches.extend(matches);
|
||||
if incremental {
|
||||
self.matches.retain_mut(|(index, score)| {
|
||||
let option = &self.options[*index as usize];
|
||||
let text = option.filter_text(&self.editor_data);
|
||||
let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher);
|
||||
match new_score {
|
||||
Some(new_score) => {
|
||||
*score = new_score as u32;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.matches.clear();
|
||||
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
|
||||
let text = option.filter_text(&self.editor_data);
|
||||
pattern
|
||||
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
|
||||
.map(|score| (i as u32, score as u32))
|
||||
});
|
||||
self.matches.extend(matches);
|
||||
}
|
||||
self.matches
|
||||
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
|
||||
|
||||
@ -413,6 +427,7 @@ const fn div_ceil(a: usize, b: usize) -> usize {
|
||||
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
} else if !render_borders {
|
||||
// Draw scroll track
|
||||
cell.set_symbol(half_block);
|
||||
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
}
|
||||
}
|
||||
|
@ -409,7 +409,7 @@ fn filename_impl<F>(
|
||||
use std::path::Path;
|
||||
|
||||
let is_tilde = input == "~";
|
||||
let path = helix_core::path::expand_tilde(Path::new(input));
|
||||
let path = helix_stdx::path::expand_tilde(Path::new(input));
|
||||
|
||||
let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
|
||||
(path, None)
|
||||
@ -430,7 +430,7 @@ fn filename_impl<F>(
|
||||
match path.parent() {
|
||||
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
|
||||
// Path::new("h")'s parent is Some("")...
|
||||
_ => helix_loader::current_working_dir(),
|
||||
_ => helix_stdx::env::current_working_dir(),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -63,7 +63,7 @@ impl PathOrId {
|
||||
fn get_canonicalized(self) -> Self {
|
||||
use PathOrId::*;
|
||||
match self {
|
||||
Path(path) => Path(helix_core::path::get_canonicalized_path(&path)),
|
||||
Path(path) => Path(helix_stdx::path::canonicalize(path)),
|
||||
Id(id) => Id(id),
|
||||
}
|
||||
}
|
||||
@ -480,8 +480,7 @@ fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
|
||||
.find::<Overlay<DynamicPicker<T>>>()
|
||||
.map(|overlay| &mut overlay.content.file_picker),
|
||||
};
|
||||
let Some(picker) = picker
|
||||
else {
|
||||
let Some(picker) = picker else {
|
||||
log::info!("picker closed before syntax highlighting finished");
|
||||
return;
|
||||
};
|
||||
@ -489,7 +488,15 @@ fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
|
||||
let doc = match current_file {
|
||||
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
|
||||
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
|
||||
Some(CachedPreview::Document(ref mut doc)) => doc,
|
||||
Some(CachedPreview::Document(ref mut doc)) => {
|
||||
let diagnostics = Editor::doc_diagnostics(
|
||||
&editor.language_servers,
|
||||
&editor.diagnostics,
|
||||
doc,
|
||||
);
|
||||
doc.replace_diagnostics(diagnostics, &[], None);
|
||||
doc
|
||||
}
|
||||
_ => return,
|
||||
},
|
||||
};
|
||||
|
@ -303,6 +303,7 @@ const fn div_ceil(a: usize, b: usize) -> usize {
|
||||
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
} else if !render_borders {
|
||||
// Draw scroll track
|
||||
cell.set_symbol(half_block);
|
||||
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
|
||||
}
|
||||
}
|
||||
|
@ -393,7 +393,7 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context)
|
||||
height,
|
||||
);
|
||||
|
||||
if !self.completion.is_empty() {
|
||||
if completion_area.height > 0 && !self.completion.is_empty() {
|
||||
let area = completion_area;
|
||||
let background = theme.get("ui.menu");
|
||||
|
||||
|
@ -11,7 +11,7 @@ pub fn get(&self, id: usize) -> Option<&Spinner> {
|
||||
}
|
||||
|
||||
pub fn get_or_create(&mut self, id: usize) -> &mut Spinner {
|
||||
self.inner.entry(id).or_insert_with(Spinner::default)
|
||||
self.inner.entry(id).or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +227,8 @@ fn render_diagnostics<F>(context: &mut RenderContext, write: F)
|
||||
{
|
||||
let (warnings, errors) = context
|
||||
.doc
|
||||
.shown_diagnostics()
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.fold((0, 0), |mut counts, diag| {
|
||||
use helix_core::diagnostic::Severity;
|
||||
match diag.severity {
|
||||
|
@ -3,7 +3,8 @@
|
||||
ops::RangeInclusive,
|
||||
};
|
||||
|
||||
use helix_core::{diagnostic::Severity, path::get_normalized_path};
|
||||
use helix_core::diagnostic::Severity;
|
||||
use helix_stdx::path;
|
||||
use helix_view::doc;
|
||||
|
||||
use super::*;
|
||||
@ -23,7 +24,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
|
||||
assert_eq!(1, docs.len());
|
||||
|
||||
let doc = docs.pop().unwrap();
|
||||
assert_eq!(Some(&get_normalized_path(file.path())), doc.path());
|
||||
assert_eq!(Some(&path::normalize(file.path())), doc.path());
|
||||
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
|
||||
}),
|
||||
false,
|
||||
@ -269,7 +270,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
|
||||
assert_eq!(1, docs.len());
|
||||
|
||||
let doc = docs.pop().unwrap();
|
||||
assert_eq!(Some(&get_normalized_path(file.path())), doc.path());
|
||||
assert_eq!(Some(&path::normalize(file.path())), doc.path());
|
||||
}),
|
||||
false,
|
||||
)
|
||||
@ -341,7 +342,7 @@ async fn test_write_new_path() -> anyhow::Result<()> {
|
||||
Some(&|app| {
|
||||
let doc = doc!(app.editor);
|
||||
assert!(!app.editor.is_err());
|
||||
assert_eq!(&get_normalized_path(file1.path()), doc.path().unwrap());
|
||||
assert_eq!(&path::normalize(file1.path()), doc.path().unwrap());
|
||||
}),
|
||||
),
|
||||
(
|
||||
@ -349,7 +350,7 @@ async fn test_write_new_path() -> anyhow::Result<()> {
|
||||
Some(&|app| {
|
||||
let doc = doc!(app.editor);
|
||||
assert!(!app.editor.is_err());
|
||||
assert_eq!(&get_normalized_path(file2.path()), doc.path().unwrap());
|
||||
assert_eq!(&path::normalize(file2.path()), doc.path().unwrap());
|
||||
assert!(app.editor.document_by_path(file1.path()).is_none());
|
||||
}),
|
||||
),
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
use helix_core::path::get_normalized_path;
|
||||
use helix_stdx::path;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_split_write_quit_all() -> anyhow::Result<()> {
|
||||
@ -27,21 +27,21 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> {
|
||||
|
||||
let doc1 = docs
|
||||
.iter()
|
||||
.find(|doc| doc.path().unwrap() == &get_normalized_path(file1.path()))
|
||||
.find(|doc| doc.path().unwrap() == &path::normalize(file1.path()))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("hello1", doc1.text().to_string());
|
||||
|
||||
let doc2 = docs
|
||||
.iter()
|
||||
.find(|doc| doc.path().unwrap() == &get_normalized_path(file2.path()))
|
||||
.find(|doc| doc.path().unwrap() == &path::normalize(file2.path()))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("hello2", doc2.text().to_string());
|
||||
|
||||
let doc3 = docs
|
||||
.iter()
|
||||
.find(|doc| doc.path().unwrap() == &get_normalized_path(file3.path()))
|
||||
.find(|doc| doc.path().unwrap() == &path::normalize(file3.path()))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("hello3", doc3.text().to_string());
|
||||
|
@ -79,6 +79,7 @@ pub struct CrosstermBackend<W: Write> {
|
||||
capabilities: Capabilities,
|
||||
supports_keyboard_enhancement_protocol: OnceCell<bool>,
|
||||
mouse_capture_enabled: bool,
|
||||
supports_bracketed_paste: bool,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
@ -91,6 +92,7 @@ pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend<W> {
|
||||
capabilities: Capabilities::from_env_or_default(config),
|
||||
supports_keyboard_enhancement_protocol: OnceCell::new(),
|
||||
mouse_capture_enabled: false,
|
||||
supports_bracketed_paste: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,9 +136,16 @@ fn claim(&mut self, config: Config) -> io::Result<()> {
|
||||
execute!(
|
||||
self.buffer,
|
||||
terminal::EnterAlternateScreen,
|
||||
EnableBracketedPaste,
|
||||
EnableFocusChange
|
||||
)?;
|
||||
match execute!(self.buffer, EnableBracketedPaste,) {
|
||||
Err(err) if err.kind() == io::ErrorKind::Unsupported => {
|
||||
log::warn!("Bracketed paste is not supported on this terminal.");
|
||||
self.supports_bracketed_paste = false;
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
Ok(_) => (),
|
||||
};
|
||||
execute!(self.buffer, terminal::Clear(terminal::ClearType::All))?;
|
||||
if config.enable_mouse_capture {
|
||||
execute!(self.buffer, EnableMouseCapture)?;
|
||||
@ -177,9 +186,11 @@ fn restore(&mut self, config: Config) -> io::Result<()> {
|
||||
if self.supports_keyboard_enhancement_protocol() {
|
||||
execute!(self.buffer, PopKeyboardEnhancementFlags)?;
|
||||
}
|
||||
if self.supports_bracketed_paste {
|
||||
execute!(self.buffer, DisableBracketedPaste,)?;
|
||||
}
|
||||
execute!(
|
||||
self.buffer,
|
||||
DisableBracketedPaste,
|
||||
DisableFocusChange,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
@ -195,12 +206,8 @@ fn force_restore() -> io::Result<()> {
|
||||
// disable without calling enable previously
|
||||
let _ = execute!(stdout, DisableMouseCapture);
|
||||
let _ = execute!(stdout, PopKeyboardEnhancementFlags);
|
||||
execute!(
|
||||
stdout,
|
||||
DisableBracketedPaste,
|
||||
DisableFocusChange,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
let _ = execute!(stdout, DisableBracketedPaste);
|
||||
execute!(stdout, DisableFocusChange, terminal::LeaveAlternateScreen)?;
|
||||
terminal::disable_raw_mode()
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
|
||||
parking_lot = "0.12"
|
||||
arc-swap = { version = "1.6.0" }
|
||||
|
||||
gix = { version = "0.57.0", default-features = false , optional = true }
|
||||
gix = { version = "0.58.0", features = ["attributes"], default-features = false, optional = true }
|
||||
imara-diff = "0.1.5"
|
||||
anyhow = "1"
|
||||
|
||||
@ -29,4 +29,4 @@ log = "0.4"
|
||||
git = ["gix"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.8"
|
||||
tempfile = "3.9"
|
||||
|
@ -1,5 +1,7 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use gix::filter::plumbing::driver::apply::Delay;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -76,29 +78,21 @@ fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
|
||||
let file_oid = find_file_in_commit(&repo, &head, file)?;
|
||||
|
||||
let file_object = repo.find_object(file_oid)?;
|
||||
let mut data = file_object.detach().data;
|
||||
// convert LF to CRLF if configured to avoid showing every line as changed
|
||||
if repo
|
||||
.config_snapshot()
|
||||
.boolean("core.autocrlf")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let mut normalized_file = Vec::with_capacity(data.len());
|
||||
let mut at_cr = false;
|
||||
for &byte in &data {
|
||||
if byte == b'\n' {
|
||||
// if this is a LF instead of a CRLF (last byte was not a CR)
|
||||
// insert a new CR to generate a CRLF
|
||||
if !at_cr {
|
||||
normalized_file.push(b'\r');
|
||||
}
|
||||
}
|
||||
at_cr = byte == b'\r';
|
||||
normalized_file.push(byte)
|
||||
}
|
||||
data = normalized_file
|
||||
let data = file_object.detach().data;
|
||||
// Get the actual data that git would make out of the git object.
|
||||
// This will apply the user's git config or attributes like crlf conversions.
|
||||
if let Some(work_dir) = repo.work_dir() {
|
||||
let rela_path = file.strip_prefix(work_dir)?;
|
||||
let rela_path = gix::path::try_into_bstr(rela_path)?;
|
||||
let (mut pipeline, _) = repo.filter_pipeline(None)?;
|
||||
let mut worktree_outcome =
|
||||
pipeline.convert_to_worktree(&data, rela_path.as_ref(), Delay::Forbid)?;
|
||||
let mut buf = Vec::with_capacity(data.len());
|
||||
worktree_outcome.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
} else {
|
||||
Ok(data)
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||
|
@ -15,6 +15,7 @@ default = []
|
||||
term = ["crossterm"]
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-event = { path = "../helix-event" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
@ -45,7 +46,6 @@ serde_json = "1.0"
|
||||
toml = "0.7"
|
||||
log = "~0.4"
|
||||
|
||||
which = "5.0.0"
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
|
||||
|
@ -73,7 +73,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
use crate::env::{binary_exists, env_var_is_set};
|
||||
use helix_stdx::env::{binary_exists, env_var_is_set};
|
||||
|
||||
if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
command_provider! {
|
||||
@ -98,7 +98,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
|
||||
#[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))]
|
||||
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
use crate::env::{binary_exists, env_var_is_set};
|
||||
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?
|
||||
|
@ -4,10 +4,12 @@
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
use helix_core::auto_pairs::AutoPairs;
|
||||
use helix_core::chars::char_is_word;
|
||||
use helix_core::doc_formatter::TextFormat;
|
||||
use helix_core::encoding::Encoding;
|
||||
use helix_core::syntax::{Highlight, LanguageServerFeature};
|
||||
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
|
||||
use helix_lsp::util::lsp_pos_to_pos;
|
||||
use helix_vcs::{DiffHandle, DiffProviderRegistry};
|
||||
|
||||
use ::parking_lot::Mutex;
|
||||
@ -34,6 +36,7 @@
|
||||
};
|
||||
|
||||
use crate::editor::Config;
|
||||
use crate::events::{DocumentDidChange, SelectionDidChange};
|
||||
use crate::{DocumentId, Editor, Theme, View, ViewId};
|
||||
|
||||
/// 8kB of buffer space for encoding and decoding `Rope`s.
|
||||
@ -112,19 +115,6 @@ pub struct SavePoint {
|
||||
/// The view this savepoint is associated with
|
||||
pub view: ViewId,
|
||||
revert: Mutex<Transaction>,
|
||||
pub text: Rope,
|
||||
}
|
||||
|
||||
impl SavePoint {
|
||||
pub fn cursor(&self) -> usize {
|
||||
// we always create transactions with selections
|
||||
self.revert
|
||||
.lock()
|
||||
.selection()
|
||||
.unwrap()
|
||||
.primary()
|
||||
.cursor(self.text.slice(..))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
@ -736,7 +726,12 @@ pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterE
|
||||
if let Some((fmt_cmd, fmt_args)) = self
|
||||
.language_config()
|
||||
.and_then(|c| c.formatter.as_ref())
|
||||
.and_then(|formatter| Some((which::which(&formatter.command).ok()?, &formatter.args)))
|
||||
.and_then(|formatter| {
|
||||
Some((
|
||||
helix_stdx::env::which(&formatter.command).ok()?,
|
||||
&formatter.args,
|
||||
))
|
||||
})
|
||||
{
|
||||
use std::process::Stdio;
|
||||
let text = self.text().clone();
|
||||
@ -853,7 +848,7 @@ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send
|
||||
let text = self.text().clone();
|
||||
|
||||
let path = match path {
|
||||
Some(path) => helix_core::path::get_canonicalized_path(&path),
|
||||
Some(path) => helix_stdx::path::canonicalize(path),
|
||||
None => {
|
||||
if self.path.is_none() {
|
||||
bail!("Can't save with no path set!");
|
||||
@ -1046,8 +1041,11 @@ pub fn encoding(&self) -> &'static Encoding {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
/// sets the document path without sending events to various
|
||||
/// observers (like LSP), in most cases `Editor::set_doc_path`
|
||||
/// should be used instead
|
||||
pub fn set_path(&mut self, path: Option<&Path>) {
|
||||
let path = path.map(helix_core::path::get_canonicalized_path);
|
||||
let path = path.map(helix_stdx::path::canonicalize);
|
||||
|
||||
// if parent doesn't exist we still want to open the document
|
||||
// and error out when document is saved
|
||||
@ -1075,14 +1073,6 @@ pub fn set_language(
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the programming language for the file if you know the name (scope) but don't have the
|
||||
/// [`syntax::LanguageConfiguration`] for it.
|
||||
pub fn set_language2(&mut self, scope: &str, config_loader: Arc<syntax::Loader>) {
|
||||
let language_config = config_loader.language_config_for_scope(scope);
|
||||
|
||||
self.set_language(language_config, Some(config_loader));
|
||||
}
|
||||
|
||||
/// Set the programming language for the file if you know the language but don't have the
|
||||
/// [`syntax::LanguageConfiguration`] for it.
|
||||
pub fn set_language_by_language_id(
|
||||
@ -1102,6 +1092,10 @@ pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
|
||||
// TODO: use a transaction?
|
||||
self.selections
|
||||
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
|
||||
helix_event::dispatch(SelectionDidChange {
|
||||
doc: self,
|
||||
view: view_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the origin selection of the text in a document, i.e. where
|
||||
@ -1155,6 +1149,14 @@ fn apply_impl(
|
||||
let success = transaction.changes().apply(&mut self.text);
|
||||
|
||||
if success {
|
||||
if emit_lsp_notification {
|
||||
helix_event::dispatch(DocumentDidChange {
|
||||
doc: self,
|
||||
view: view_id,
|
||||
old_text: &old_doc,
|
||||
});
|
||||
}
|
||||
|
||||
for selection in self.selections.values_mut() {
|
||||
*selection = selection
|
||||
.clone()
|
||||
@ -1170,6 +1172,10 @@ fn apply_impl(
|
||||
view_id,
|
||||
selection.clone().ensure_invariants(self.text.slice(..)),
|
||||
);
|
||||
helix_event::dispatch(SelectionDidChange {
|
||||
doc: self,
|
||||
view: view_id,
|
||||
});
|
||||
}
|
||||
|
||||
self.modified_since_accessed = true;
|
||||
@ -1222,18 +1228,23 @@ fn apply_impl(
|
||||
};
|
||||
(&mut diagnostic.range.start, assoc)
|
||||
}));
|
||||
changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| {
|
||||
changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| {
|
||||
if diagnostic.zero_width {
|
||||
// for zero width diagnostics treat the diagnostic as a point
|
||||
// rather than a range
|
||||
return None;
|
||||
}
|
||||
let assoc = if diagnostic.ends_at_word {
|
||||
Assoc::AfterWord
|
||||
} else {
|
||||
Assoc::Before
|
||||
};
|
||||
(&mut diagnostic.range.end, assoc)
|
||||
Some((&mut diagnostic.range.end, assoc))
|
||||
}));
|
||||
self.diagnostics.retain_mut(|diagnostic| {
|
||||
if diagnostic.range.start > diagnostic.range.end
|
||||
|| (!diagnostic.zero_width && diagnostic.range.start == diagnostic.range.end)
|
||||
{
|
||||
if diagnostic.zero_width {
|
||||
diagnostic.range.end = diagnostic.range.start
|
||||
} else if diagnostic.range.start >= diagnostic.range.end {
|
||||
return false;
|
||||
}
|
||||
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
|
||||
@ -1277,6 +1288,7 @@ fn apply_impl(
|
||||
}
|
||||
|
||||
if emit_lsp_notification {
|
||||
// TODO: move to hook
|
||||
// emit lsp notification
|
||||
for language_server in self.language_servers() {
|
||||
let notify = language_server.text_document_did_change(
|
||||
@ -1387,7 +1399,6 @@ pub fn savepoint(&mut self, view: &View) -> Arc<SavePoint> {
|
||||
let savepoint = Arc::new(SavePoint {
|
||||
view: view.id,
|
||||
revert: Mutex::new(revert),
|
||||
text: self.text.clone(),
|
||||
});
|
||||
self.savepoints.push(Arc::downgrade(&savepoint));
|
||||
savepoint
|
||||
@ -1673,7 +1684,7 @@ pub fn selections(&self) -> &HashMap<ViewId, Selection> {
|
||||
pub fn relative_path(&self) -> Option<PathBuf> {
|
||||
self.path
|
||||
.as_deref()
|
||||
.map(helix_core::path::get_relative_path)
|
||||
.map(helix_stdx::path::get_relative_path)
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Cow<'static, str> {
|
||||
@ -1709,29 +1720,107 @@ pub fn position(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn lsp_diagnostic_to_diagnostic(
|
||||
text: &Rope,
|
||||
language_config: Option<&LanguageConfiguration>,
|
||||
diagnostic: &helix_lsp::lsp::Diagnostic,
|
||||
language_server_id: usize,
|
||||
offset_encoding: helix_lsp::OffsetEncoding,
|
||||
) -> Option<Diagnostic> {
|
||||
use helix_core::diagnostic::{Range, Severity::*};
|
||||
|
||||
// TODO: convert inside server
|
||||
let start =
|
||||
if let Some(start) = lsp_pos_to_pos(text, diagnostic.range.start, offset_encoding) {
|
||||
start
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
|
||||
let end = if let Some(end) = lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding) {
|
||||
end
|
||||
} else {
|
||||
log::warn!("lsp position out of bounds - {:?}", diagnostic);
|
||||
return None;
|
||||
};
|
||||
|
||||
let severity = diagnostic.severity.map(|severity| match severity {
|
||||
lsp::DiagnosticSeverity::ERROR => Error,
|
||||
lsp::DiagnosticSeverity::WARNING => Warning,
|
||||
lsp::DiagnosticSeverity::INFORMATION => Info,
|
||||
lsp::DiagnosticSeverity::HINT => Hint,
|
||||
severity => unreachable!("unrecognized diagnostic severity: {:?}", severity),
|
||||
});
|
||||
|
||||
if let Some(lang_conf) = language_config {
|
||||
if let Some(severity) = severity {
|
||||
if severity < lang_conf.diagnostic_severity {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
use helix_core::diagnostic::{DiagnosticTag, NumberOrString};
|
||||
|
||||
let code = match diagnostic.code.clone() {
|
||||
Some(x) => match x {
|
||||
lsp::NumberOrString::Number(x) => Some(NumberOrString::Number(x)),
|
||||
lsp::NumberOrString::String(x) => Some(NumberOrString::String(x)),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let tags = if let Some(tags) = &diagnostic.tags {
|
||||
let new_tags = tags
|
||||
.iter()
|
||||
.filter_map(|tag| match *tag {
|
||||
lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
|
||||
lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
new_tags
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let ends_at_word =
|
||||
start != end && end != 0 && text.get_char(end - 1).map_or(false, char_is_word);
|
||||
let starts_at_word = start != end && text.get_char(start).map_or(false, char_is_word);
|
||||
|
||||
Some(Diagnostic {
|
||||
range: Range { start, end },
|
||||
ends_at_word,
|
||||
starts_at_word,
|
||||
zero_width: start == end,
|
||||
line: diagnostic.range.start.line as usize,
|
||||
message: diagnostic.message.clone(),
|
||||
severity,
|
||||
code,
|
||||
tags,
|
||||
source: diagnostic.source.clone(),
|
||||
data: diagnostic.data.clone(),
|
||||
language_server_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn diagnostics(&self) -> &[Diagnostic] {
|
||||
&self.diagnostics
|
||||
}
|
||||
|
||||
pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator {
|
||||
self.diagnostics.iter().filter(|d| {
|
||||
self.language_servers_with_feature(LanguageServerFeature::Diagnostics)
|
||||
.any(|ls| ls.id() == d.language_server_id)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_diagnostics(
|
||||
&mut self,
|
||||
diagnostics: impl IntoIterator<Item = Diagnostic>,
|
||||
unchanged_sources: &[String],
|
||||
language_server_id: usize,
|
||||
language_server_id: Option<usize>,
|
||||
) {
|
||||
if unchanged_sources.is_empty() {
|
||||
self.clear_diagnostics(language_server_id);
|
||||
} else {
|
||||
self.diagnostics.retain(|d| {
|
||||
if d.language_server_id != language_server_id {
|
||||
if language_server_id.map_or(false, |id| id != d.language_server_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1752,9 +1841,13 @@ pub fn replace_diagnostics(
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clear_diagnostics(&mut self, language_server_id: usize) {
|
||||
self.diagnostics
|
||||
.retain(|d| d.language_server_id != language_server_id);
|
||||
/// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared
|
||||
pub fn clear_diagnostics(&mut self, language_server_id: Option<usize>) {
|
||||
if let Some(id) = language_server_id {
|
||||
self.diagnostics.retain(|d| d.language_server_id != id);
|
||||
} else {
|
||||
self.diagnostics.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the document's auto pairs. If the document has a recognized
|
||||
|
@ -2,6 +2,7 @@
|
||||
align_view,
|
||||
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
|
||||
graphics::{CursorKind, Rect},
|
||||
handlers::Handlers,
|
||||
info::Info,
|
||||
input::KeyEvent,
|
||||
register::Registers,
|
||||
@ -22,7 +23,8 @@
|
||||
borrow::Cow,
|
||||
cell::Cell,
|
||||
collections::{BTreeMap, HashMap},
|
||||
io::stdin,
|
||||
fs,
|
||||
io::{self, stdin},
|
||||
num::NonZeroUsize,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
@ -30,10 +32,7 @@
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
oneshot,
|
||||
},
|
||||
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
time::{sleep, Duration, Instant, Sleep},
|
||||
};
|
||||
|
||||
@ -42,11 +41,12 @@
|
||||
pub use helix_core::diagnostic::Severity;
|
||||
use helix_core::{
|
||||
auto_pairs::AutoPairs,
|
||||
syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap},
|
||||
syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
|
||||
Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
|
||||
};
|
||||
use helix_dap as dap;
|
||||
use helix_lsp::lsp;
|
||||
use helix_stdx::path::canonicalize;
|
||||
|
||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
@ -243,12 +243,19 @@ pub struct Config {
|
||||
/// Set a global text_width
|
||||
pub text_width: usize,
|
||||
/// Time in milliseconds since last keypress before idle timers trigger.
|
||||
/// Used for autocompletion, set to 0 for instant. Defaults to 250ms.
|
||||
/// Used for various UI timeouts. Defaults to 250ms.
|
||||
#[serde(
|
||||
serialize_with = "serialize_duration_millis",
|
||||
deserialize_with = "deserialize_duration_millis"
|
||||
)]
|
||||
pub idle_timeout: Duration,
|
||||
/// Time in milliseconds after typing a word character before auto completions
|
||||
/// are shown, set to 5 for instant. Defaults to 250ms.
|
||||
#[serde(
|
||||
serialize_with = "serialize_duration_millis",
|
||||
deserialize_with = "deserialize_duration_millis"
|
||||
)]
|
||||
pub completion_timeout: Duration,
|
||||
/// Whether to insert the completion suggestion on hover. Defaults to true.
|
||||
pub preview_completion_insert: bool,
|
||||
pub completion_trigger_len: u8,
|
||||
@ -324,7 +331,7 @@ pub struct TerminalConfig {
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
use crate::env::binary_exists;
|
||||
use helix_stdx::env::binary_exists;
|
||||
|
||||
if binary_exists("wt") {
|
||||
return Some(TerminalConfig {
|
||||
@ -347,7 +354,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
|
||||
#[cfg(not(any(windows, target_os = "wasm32")))]
|
||||
pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
use crate::env::{binary_exists, env_var_is_set};
|
||||
use helix_stdx::env::{binary_exists, env_var_is_set};
|
||||
|
||||
if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
return Some(TerminalConfig {
|
||||
@ -828,6 +835,7 @@ fn default() -> Self {
|
||||
auto_format: true,
|
||||
auto_save: false,
|
||||
idle_timeout: Duration::from_millis(250),
|
||||
completion_timeout: Duration::from_millis(250),
|
||||
preview_completion_insert: true,
|
||||
completion_trigger_len: 2,
|
||||
auto_info: true,
|
||||
@ -952,14 +960,7 @@ pub struct Editor {
|
||||
/// avoid calculating the cursor position multiple
|
||||
/// times during rendering and should not be set by other functions.
|
||||
pub cursor_cache: Cell<Option<Option<Position>>>,
|
||||
/// When a new completion request is sent to the server old
|
||||
/// unfinished request must be dropped. Each completion
|
||||
/// request is associated with a channel that cancels
|
||||
/// when the channel is dropped. That channel is stored
|
||||
/// here. When a new completion request is sent this
|
||||
/// field is set and any old requests are automatically
|
||||
/// canceled as a result
|
||||
pub completion_request_handle: Option<oneshot::Sender<()>>,
|
||||
pub handlers: Handlers,
|
||||
}
|
||||
|
||||
pub type Motion = Box<dyn Fn(&mut Editor)>;
|
||||
@ -987,13 +988,16 @@ enum ThemeAction {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompleteAction {
|
||||
Triggered,
|
||||
/// A savepoint of the currently selected completion. The savepoint
|
||||
/// MUST be restored before sending any event to the LSP
|
||||
Selected {
|
||||
savepoint: Arc<SavePoint>,
|
||||
},
|
||||
Applied {
|
||||
trigger_offset: usize,
|
||||
changes: Vec<Change>,
|
||||
},
|
||||
/// A savepoint of the currently selected completion. The savepoint
|
||||
/// MUST be restored before sending any event to the LSP
|
||||
Selected { savepoint: Arc<SavePoint> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
@ -1027,6 +1031,7 @@ pub fn new(
|
||||
theme_loader: Arc<theme::Loader>,
|
||||
syn_loader: Arc<syntax::Loader>,
|
||||
config: Arc<dyn DynAccess<Config>>,
|
||||
handlers: Handlers,
|
||||
) -> Self {
|
||||
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
|
||||
let conf = config.load();
|
||||
@ -1071,7 +1076,7 @@ pub fn new(
|
||||
config_events: unbounded_channel(),
|
||||
needs_redraw: false,
|
||||
cursor_cache: Cell::new(None),
|
||||
completion_request_handle: None,
|
||||
handlers,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1144,7 +1149,7 @@ pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
|
||||
#[inline]
|
||||
pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
|
||||
let error = error.into();
|
||||
log::error!("editor error: {}", error);
|
||||
log::debug!("editor error: {}", error);
|
||||
self.status_msg = Some((error, Severity::Error));
|
||||
}
|
||||
|
||||
@ -1212,6 +1217,90 @@ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) {
|
||||
self.launch_language_servers(doc_id)
|
||||
}
|
||||
|
||||
/// moves/renames a path, invoking any event handlers (currently only lsp)
|
||||
/// and calling `set_doc_path` if the file is open in the editor
|
||||
pub fn move_path(&mut self, old_path: &Path, new_path: &Path) -> io::Result<()> {
|
||||
let new_path = canonicalize(new_path);
|
||||
// sanity check
|
||||
if old_path == new_path {
|
||||
return Ok(());
|
||||
}
|
||||
let is_dir = old_path.is_dir();
|
||||
let language_servers: Vec<_> = self
|
||||
.language_servers
|
||||
.iter_clients()
|
||||
.filter(|client| client.is_initialized())
|
||||
.cloned()
|
||||
.collect();
|
||||
for language_server in language_servers {
|
||||
let Some(request) = language_server.will_rename(old_path, &new_path, is_dir) else {
|
||||
continue;
|
||||
};
|
||||
let edit = match helix_lsp::block_on(request) {
|
||||
Ok(edit) => edit,
|
||||
Err(err) => {
|
||||
log::error!("invalid willRename response: {err:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(err) = self.apply_workspace_edit(language_server.offset_encoding(), &edit) {
|
||||
log::error!("failed to apply workspace edit: {err:?}")
|
||||
}
|
||||
}
|
||||
fs::rename(old_path, &new_path)?;
|
||||
if let Some(doc) = self.document_by_path(old_path) {
|
||||
self.set_doc_path(doc.id(), &new_path);
|
||||
}
|
||||
let is_dir = new_path.is_dir();
|
||||
for ls in self.language_servers.iter_clients() {
|
||||
if let Some(notification) = ls.did_rename(old_path, &new_path, is_dir) {
|
||||
tokio::spawn(notification);
|
||||
};
|
||||
}
|
||||
self.language_servers
|
||||
.file_event_handler
|
||||
.file_changed(old_path.to_owned());
|
||||
self.language_servers
|
||||
.file_event_handler
|
||||
.file_changed(new_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_doc_path(&mut self, doc_id: DocumentId, path: &Path) {
|
||||
let doc = doc_mut!(self, &doc_id);
|
||||
let old_path = doc.path();
|
||||
|
||||
if let Some(old_path) = old_path {
|
||||
// sanity check, should not occur but some callers (like an LSP) may
|
||||
// create bogus calls
|
||||
if old_path == path {
|
||||
return;
|
||||
}
|
||||
// if we are open in LSPs send did_close notification
|
||||
for language_server in doc.language_servers() {
|
||||
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
|
||||
}
|
||||
}
|
||||
// we need to clear the list of language servers here so that
|
||||
// refresh_doc_language/refresh_language_servers doesn't resend
|
||||
// text_document_did_close. Since we called `text_document_did_close`
|
||||
// we have fully unregistered this document from its LS
|
||||
doc.language_servers.clear();
|
||||
doc.set_path(Some(path));
|
||||
self.refresh_doc_language(doc_id)
|
||||
}
|
||||
|
||||
pub fn refresh_doc_language(&mut self, doc_id: DocumentId) {
|
||||
let loader = self.syn_loader.clone();
|
||||
let doc = doc_mut!(self, &doc_id);
|
||||
doc.detect_language(loader);
|
||||
doc.detect_indent_and_line_ending();
|
||||
self.refresh_language_servers(doc_id);
|
||||
let doc = doc_mut!(self, &doc_id);
|
||||
let diagnostics = Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, doc);
|
||||
doc.replace_diagnostics(diagnostics, &[], None);
|
||||
}
|
||||
|
||||
/// Launch a language server for a given document
|
||||
fn launch_language_servers(&mut self, doc_id: DocumentId) {
|
||||
if !self.config().lsp.enable {
|
||||
@ -1235,19 +1324,26 @@ fn launch_language_servers(&mut self, doc_id: DocumentId) {
|
||||
.filter_map(|(lang, client)| match client {
|
||||
Ok(client) => Some((lang, client)),
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
|
||||
language.scope(),
|
||||
lang,
|
||||
err
|
||||
);
|
||||
if let helix_lsp::Error::ExecutableNotFound(err) = err {
|
||||
// Silence by default since some language servers might just not be installed
|
||||
log::debug!(
|
||||
"Language server not found for `{}` {} {}", language.scope(), lang, err,
|
||||
);
|
||||
} else {
|
||||
log::error!(
|
||||
"Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
|
||||
language.scope(),
|
||||
lang,
|
||||
err
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
|
||||
if language_servers.is_empty() {
|
||||
if language_servers.is_empty() && doc.language_servers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1464,7 +1560,7 @@ pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Erro
|
||||
|
||||
// ??? possible use for integration tests
|
||||
pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
|
||||
let path = helix_core::path::get_canonicalized_path(path);
|
||||
let path = helix_stdx::path::canonicalize(path);
|
||||
let id = self.document_by_path(&path).map(|doc| doc.id);
|
||||
|
||||
let id = if let Some(id) = id {
|
||||
@ -1477,6 +1573,10 @@ pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error>
|
||||
self.config.clone(),
|
||||
)?;
|
||||
|
||||
let diagnostics =
|
||||
Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, &doc);
|
||||
doc.replace_diagnostics(diagnostics, &[], None);
|
||||
|
||||
if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
|
||||
doc.set_diff_base(diff_base);
|
||||
}
|
||||
@ -1706,6 +1806,60 @@ pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut D
|
||||
.find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Returns all supported diagnostics for the document
|
||||
pub fn doc_diagnostics<'a>(
|
||||
language_servers: &'a helix_lsp::Registry,
|
||||
diagnostics: &'a BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
|
||||
document: &Document,
|
||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
|
||||
}
|
||||
|
||||
/// Returns all supported diagnostics for the document
|
||||
/// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from
|
||||
pub fn doc_diagnostics_with_filter<'a>(
|
||||
language_servers: &'a helix_lsp::Registry,
|
||||
diagnostics: &'a BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
|
||||
|
||||
document: &Document,
|
||||
filter: impl Fn(&lsp::Diagnostic, usize) -> bool + 'a,
|
||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||
let text = document.text().clone();
|
||||
let language_config = document.language.clone();
|
||||
document
|
||||
.path()
|
||||
.and_then(|path| url::Url::from_file_path(path).ok()) // TODO log error?
|
||||
.and_then(|uri| diagnostics.get(&uri))
|
||||
.map(|diags| {
|
||||
diags.iter().filter_map(move |(diagnostic, lsp_id)| {
|
||||
let ls = language_servers.get_by_id(*lsp_id)?;
|
||||
language_config
|
||||
.as_ref()
|
||||
.and_then(|c| {
|
||||
c.language_servers.iter().find(|features| {
|
||||
features.name == ls.name()
|
||||
&& features.has_feature(LanguageServerFeature::Diagnostics)
|
||||
})
|
||||
})
|
||||
.and_then(|_| {
|
||||
if filter(diagnostic, *lsp_id) {
|
||||
Document::lsp_diagnostic_to_diagnostic(
|
||||
&text,
|
||||
language_config.as_deref(),
|
||||
diagnostic,
|
||||
*lsp_id,
|
||||
ls.offset_encoding(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Gets the primary cursor position in screen coordinates,
|
||||
/// or `None` if the primary cursor is not visible on screen.
|
||||
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
||||
@ -1836,10 +1990,12 @@ pub fn enter_normal_mode(&mut self) {
|
||||
if doc.restore_cursor {
|
||||
let text = doc.text().slice(..);
|
||||
let selection = doc.selection(view.id).clone().transform(|range| {
|
||||
Range::new(
|
||||
range.from(),
|
||||
graphemes::prev_grapheme_boundary(text, range.to()),
|
||||
)
|
||||
let mut head = range.to();
|
||||
if range.head > range.anchor {
|
||||
head = graphemes::prev_grapheme_boundary(text, head);
|
||||
}
|
||||
|
||||
Range::new(range.from(), head)
|
||||
});
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
@ -1852,6 +2008,30 @@ pub fn current_stack_frame(&self) -> Option<&StackFrame> {
|
||||
.as_ref()
|
||||
.and_then(|debugger| debugger.current_stack_frame())
|
||||
}
|
||||
|
||||
/// Returns the id of a view that this doc contains a selection for,
|
||||
/// making sure it is synced with the current changes
|
||||
/// if possible or there are no selections returns current_view
|
||||
/// otherwise uses an arbitrary view
|
||||
pub fn get_synced_view_id(&mut self, id: DocumentId) -> ViewId {
|
||||
let current_view = view_mut!(self);
|
||||
let doc = self.documents.get_mut(&id).unwrap();
|
||||
if doc.selections().contains_key(¤t_view.id) {
|
||||
// only need to sync current view if this is not the current doc
|
||||
if current_view.doc != id {
|
||||
current_view.sync_changes(doc);
|
||||
}
|
||||
current_view.id
|
||||
} else if let Some(view_id) = doc.selections().keys().next() {
|
||||
let view_id = *view_id;
|
||||
let view = self.tree.get_mut(view_id);
|
||||
view.sync_changes(doc);
|
||||
view_id
|
||||
} else {
|
||||
doc.ensure_view_init(current_view.id);
|
||||
current_view.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_restore_indent(doc: &mut Document, view: &mut View) {
|
||||
|
@ -1,8 +0,0 @@
|
||||
pub fn binary_exists(binary_name: &str) -> bool {
|
||||
which::which(binary_name).is_ok()
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn env_var_is_set(env_var_name: &str) -> bool {
|
||||
std::env::var_os(env_var_name).is_some()
|
||||
}
|
9
helix-view/src/events.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use helix_core::Rope;
|
||||
use helix_event::events;
|
||||
|
||||
use crate::{Document, ViewId};
|
||||
|
||||
events! {
|
||||
DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope }
|
||||
SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId }
|
||||
}
|
41
helix-view/src/handlers.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use helix_event::send_blocking;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use crate::handlers::lsp::SignatureHelpInvoked;
|
||||
use crate::{DocumentId, Editor, ViewId};
|
||||
|
||||
pub mod dap;
|
||||
pub mod lsp;
|
||||
|
||||
pub struct Handlers {
|
||||
// only public because most of the actual implementation is in helix-term right now :/
|
||||
pub completions: Sender<lsp::CompletionEvent>,
|
||||
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
|
||||
}
|
||||
|
||||
impl Handlers {
|
||||
/// Manually trigger completion (c-x)
|
||||
pub fn trigger_completions(&self, trigger_pos: usize, doc: DocumentId, view: ViewId) {
|
||||
send_blocking(
|
||||
&self.completions,
|
||||
lsp::CompletionEvent::ManualTrigger {
|
||||
cursor: trigger_pos,
|
||||
doc,
|
||||
view,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn trigger_signature_help(&self, invocation: SignatureHelpInvoked, editor: &Editor) {
|
||||
let event = match invocation {
|
||||
SignatureHelpInvoked::Automatic => {
|
||||
if !editor.config().lsp.auto_signature_help {
|
||||
return;
|
||||
}
|
||||
lsp::SignatureHelpEvent::Trigger
|
||||
}
|
||||
SignatureHelpInvoked::Manual => lsp::SignatureHelpEvent::Invoked,
|
||||
};
|
||||
send_blocking(&self.signature_hints, event)
|
||||
}
|
||||
}
|
@ -1 +1,270 @@
|
||||
use crate::editor::Action;
|
||||
use crate::Editor;
|
||||
use crate::{DocumentId, ViewId};
|
||||
use helix_lsp::util::generate_transaction_from_edits;
|
||||
use helix_lsp::{lsp, OffsetEncoding};
|
||||
|
||||
pub enum CompletionEvent {
|
||||
/// Auto completion was triggered by typing a word char
|
||||
AutoTrigger {
|
||||
cursor: usize,
|
||||
doc: DocumentId,
|
||||
view: ViewId,
|
||||
},
|
||||
/// Auto completion was triggered by typing a trigger char
|
||||
/// specified by the LSP
|
||||
TriggerChar {
|
||||
cursor: usize,
|
||||
doc: DocumentId,
|
||||
view: ViewId,
|
||||
},
|
||||
/// A completion was manually requested (c-x)
|
||||
ManualTrigger {
|
||||
cursor: usize,
|
||||
doc: DocumentId,
|
||||
view: ViewId,
|
||||
},
|
||||
/// Some text was deleted and the cursor is now at `pos`
|
||||
DeleteText { cursor: usize },
|
||||
/// Invalidate the current auto completion trigger
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum SignatureHelpInvoked {
|
||||
Automatic,
|
||||
Manual,
|
||||
}
|
||||
|
||||
pub enum SignatureHelpEvent {
|
||||
Invoked,
|
||||
Trigger,
|
||||
ReTrigger,
|
||||
Cancel,
|
||||
RequestComplete { open: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyEditError {
|
||||
pub kind: ApplyEditErrorKind,
|
||||
pub failed_change_idx: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ApplyEditErrorKind {
|
||||
DocumentChanged,
|
||||
FileNotFound,
|
||||
UnknownURISchema,
|
||||
IoError(std::io::Error),
|
||||
// TODO: check edits before applying and propagate failure
|
||||
// InvalidEdit,
|
||||
}
|
||||
|
||||
impl ToString for ApplyEditErrorKind {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
|
||||
ApplyEditErrorKind::FileNotFound => "file not found".to_string(),
|
||||
ApplyEditErrorKind::UnknownURISchema => "URI schema not supported".to_string(),
|
||||
ApplyEditErrorKind::IoError(err) => err.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
fn apply_text_edits(
|
||||
&mut self,
|
||||
uri: &helix_lsp::Url,
|
||||
version: Option<i32>,
|
||||
text_edits: Vec<lsp::TextEdit>,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) -> Result<(), ApplyEditErrorKind> {
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
let err = format!("unable to convert URI to filepath: {}", uri);
|
||||
log::error!("{}", err);
|
||||
self.set_error(err);
|
||||
return Err(ApplyEditErrorKind::UnknownURISchema);
|
||||
}
|
||||
};
|
||||
|
||||
let doc_id = match self.open(&path, Action::Load) {
|
||||
Ok(doc_id) => doc_id,
|
||||
Err(err) => {
|
||||
let err = format!("failed to open document: {}: {}", uri, err);
|
||||
log::error!("{}", err);
|
||||
self.set_error(err);
|
||||
return Err(ApplyEditErrorKind::FileNotFound);
|
||||
}
|
||||
};
|
||||
|
||||
let doc = doc_mut!(self, &doc_id);
|
||||
if let Some(version) = version {
|
||||
if version != doc.version() {
|
||||
let err = format!("outdated workspace edit for {path:?}");
|
||||
log::error!("{err}, expected {} but got {version}", doc.version());
|
||||
self.set_error(err);
|
||||
return Err(ApplyEditErrorKind::DocumentChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// Need to determine a view for apply/append_changes_to_history
|
||||
let view_id = self.get_synced_view_id(doc_id);
|
||||
let doc = doc_mut!(self, &doc_id);
|
||||
|
||||
let transaction = generate_transaction_from_edits(doc.text(), text_edits, offset_encoding);
|
||||
let view = view_mut!(self, view_id);
|
||||
doc.apply(&transaction, view.id);
|
||||
doc.append_changes_to_history(view);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO make this transactional (and set failureMode to transactional)
|
||||
pub fn apply_workspace_edit(
|
||||
&mut self,
|
||||
offset_encoding: OffsetEncoding,
|
||||
workspace_edit: &lsp::WorkspaceEdit,
|
||||
) -> Result<(), ApplyEditError> {
|
||||
if let Some(ref document_changes) = workspace_edit.document_changes {
|
||||
match document_changes {
|
||||
lsp::DocumentChanges::Edits(document_edits) => {
|
||||
for (i, document_edit) in document_edits.iter().enumerate() {
|
||||
let edits = document_edit
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| match edit {
|
||||
lsp::OneOf::Left(text_edit) => text_edit,
|
||||
lsp::OneOf::Right(annotated_text_edit) => {
|
||||
&annotated_text_edit.text_edit
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
self.apply_text_edits(
|
||||
&document_edit.text_document.uri,
|
||||
document_edit.text_document.version,
|
||||
edits,
|
||||
offset_encoding,
|
||||
)
|
||||
.map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
lsp::DocumentChanges::Operations(operations) => {
|
||||
log::debug!("document changes - operations: {:?}", operations);
|
||||
for (i, operation) in operations.iter().enumerate() {
|
||||
match operation {
|
||||
lsp::DocumentChangeOperation::Op(op) => {
|
||||
self.apply_document_resource_op(op).map_err(|io| {
|
||||
ApplyEditError {
|
||||
kind: ApplyEditErrorKind::IoError(io),
|
||||
failed_change_idx: i,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
lsp::DocumentChangeOperation::Edit(document_edit) => {
|
||||
let edits = document_edit
|
||||
.edits
|
||||
.iter()
|
||||
.map(|edit| match edit {
|
||||
lsp::OneOf::Left(text_edit) => text_edit,
|
||||
lsp::OneOf::Right(annotated_text_edit) => {
|
||||
&annotated_text_edit.text_edit
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
self.apply_text_edits(
|
||||
&document_edit.text_document.uri,
|
||||
document_edit.text_document.version,
|
||||
edits,
|
||||
offset_encoding,
|
||||
)
|
||||
.map_err(|kind| {
|
||||
ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(ref changes) = workspace_edit.changes {
|
||||
log::debug!("workspace changes: {:?}", changes);
|
||||
for (i, (uri, text_edits)) in changes.iter().enumerate() {
|
||||
let text_edits = text_edits.to_vec();
|
||||
self.apply_text_edits(uri, None, text_edits, offset_encoding)
|
||||
.map_err(|kind| ApplyEditError {
|
||||
kind,
|
||||
failed_change_idx: i,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Result<()> {
|
||||
use lsp::ResourceOp;
|
||||
use std::fs;
|
||||
match op {
|
||||
ResourceOp::Create(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
if !ignore_if_exists || !path.exists() {
|
||||
// Create directory if it does not exist
|
||||
if let Some(dir) = path.parent() {
|
||||
if !dir.is_dir() {
|
||||
fs::create_dir_all(dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(&path, [])?;
|
||||
self.language_servers.file_event_handler.file_changed(path);
|
||||
}
|
||||
}
|
||||
ResourceOp::Delete(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
if path.is_dir() {
|
||||
let recursive = op
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|options| options.recursive)
|
||||
.unwrap_or(false);
|
||||
|
||||
if recursive {
|
||||
fs::remove_dir_all(&path)?
|
||||
} else {
|
||||
fs::remove_dir(&path)?
|
||||
}
|
||||
self.language_servers.file_event_handler.file_changed(path);
|
||||
} else if path.is_file() {
|
||||
fs::remove_file(&path)?;
|
||||
}
|
||||
}
|
||||
ResourceOp::Rename(op) => {
|
||||
let from = op.old_uri.to_file_path().unwrap();
|
||||
let to = op.new_uri.to_file_path().unwrap();
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
if !ignore_if_exists || !to.exists() {
|
||||
self.move_path(&from, &to)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -325,7 +325,7 @@ impl std::str::FromStr for KeyEvent {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut tokens: Vec<_> = s.split('-').collect();
|
||||
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
|
||||
let mut code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
|
||||
keys::BACKSPACE => KeyCode::Backspace,
|
||||
keys::ENTER => KeyCode::Enter,
|
||||
keys::LEFT => KeyCode::Left,
|
||||
@ -405,6 +405,18 @@ fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
modifiers.insert(flag);
|
||||
}
|
||||
|
||||
// Normalize character keys so that characters like C-S-r and C-R
|
||||
// are represented by equal KeyEvents.
|
||||
match code {
|
||||
KeyCode::Char(ch)
|
||||
if ch.is_ascii_lowercase() && modifiers.contains(KeyModifiers::SHIFT) =>
|
||||
{
|
||||
code = KeyCode::Char(ch.to_ascii_uppercase());
|
||||
modifiers.remove(KeyModifiers::SHIFT);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(KeyEvent { code, modifiers })
|
||||
}
|
||||
}
|
||||
@ -684,6 +696,19 @@ fn parsing_modified_keys() {
|
||||
modifiers: KeyModifiers::ALT | KeyModifiers::CONTROL
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("C-S-r").unwrap(),
|
||||
str::parse::<KeyEvent>("C-R").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
str::parse::<KeyEvent>("S-w").unwrap(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('W'),
|
||||
modifiers: KeyModifiers::NONE
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1,17 +1,14 @@
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
|
||||
pub mod base64;
|
||||
pub mod clipboard;
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
pub mod env;
|
||||
pub mod events;
|
||||
pub mod graphics;
|
||||
pub mod gutter;
|
||||
pub mod handlers {
|
||||
pub mod dap;
|
||||
pub mod lsp;
|
||||
}
|
||||
pub mod base64;
|
||||
pub mod handlers;
|
||||
pub mod info;
|
||||
pub mod input;
|
||||
pub mod keyboard;
|
||||
|
110
languages.toml
@ -1,6 +1,8 @@
|
||||
# Language support configuration.
|
||||
# See the languages documentation: https://docs.helix-editor.com/master/languages.html
|
||||
|
||||
use-grammars = { except = [ "hare", "wren", "gemini" ] }
|
||||
|
||||
[language-server]
|
||||
|
||||
als = { command = "als" }
|
||||
@ -324,6 +326,29 @@ comment-token = "//"
|
||||
language-servers = [ "mint" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "janet"
|
||||
scope = "source.janet"
|
||||
injection-regex = "janet"
|
||||
file-types = ["cgen", "janet", "jdn"]
|
||||
shebangs = ["janet"]
|
||||
roots = ["project.janet"]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
formatter = { command = "janet-format" }
|
||||
grammar = "janet-simple"
|
||||
|
||||
[language.auto-pairs]
|
||||
'"' = '"'
|
||||
'(' = ')'
|
||||
'[' = ']'
|
||||
'{' = '}'
|
||||
"`" = "`"
|
||||
|
||||
[[grammar]]
|
||||
name = "janet-simple"
|
||||
source = { git = "https://github.com/sogaiu/tree-sitter-janet-simple", rev = "51271e260346878e1a1aa6c506ce6a797b7c25e2" }
|
||||
|
||||
[[language]]
|
||||
name = "json"
|
||||
scope = "source.json"
|
||||
@ -348,7 +373,8 @@ file-types = [
|
||||
".vuerc",
|
||||
"composer.lock",
|
||||
".watchmanconfig",
|
||||
"avsc"
|
||||
"avsc",
|
||||
".prettierrc"
|
||||
]
|
||||
language-servers = [ "vscode-json-language-server" ]
|
||||
auto-format = true
|
||||
@ -793,6 +819,7 @@ file-types = [
|
||||
"sh",
|
||||
"bash",
|
||||
"zsh",
|
||||
".bash_history",
|
||||
".bash_login",
|
||||
".bash_logout",
|
||||
".bash_profile",
|
||||
@ -1116,7 +1143,7 @@ name = "purescript"
|
||||
scope = "source.purescript"
|
||||
injection-regex = "purescript"
|
||||
file-types = ["purs"]
|
||||
roots = ["spago.dhall", "bower.json"]
|
||||
roots = ["spago.yaml", "spago.dhall", "bower.json"]
|
||||
comment-token = "--"
|
||||
language-servers = [ "purescript-language-server" ]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
@ -1245,7 +1272,7 @@ file-types = ["pod"]
|
||||
|
||||
[[grammar]]
|
||||
name = "pod"
|
||||
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "d466b84009a63986834498073ec05d58d727d55f" }
|
||||
source = { git = "https://github.com/tree-sitter-perl/tree-sitter-pod", rev = "39da859947b94abdee43e431368e1ae975c0a424" }
|
||||
|
||||
[[language]]
|
||||
name = "racket"
|
||||
@ -1281,7 +1308,7 @@ injection-regex = "comment"
|
||||
|
||||
[[grammar]]
|
||||
name = "comment"
|
||||
source = { git = "https://github.com/stsewd/tree-sitter-comment", rev = "a37ca370310ac6f89b6e0ebf2b86b2219780494e" }
|
||||
source = { git = "https://github.com/stsewd/tree-sitter-comment", rev = "aefcc2813392eb6ffe509aa0fc8b4e9b57413ee1" }
|
||||
|
||||
[[language]]
|
||||
name = "wgsl"
|
||||
@ -1390,7 +1417,7 @@ language-servers = [ "metals" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "scala"
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "23d21310fe4ab4b3273e7a6810e781224a3e7fe1" }
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "7891815f42dca9ed6aeb464c2edc39d479ab965c" }
|
||||
|
||||
[[language]]
|
||||
name = "dockerfile"
|
||||
@ -1480,7 +1507,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev =
|
||||
[[language]]
|
||||
name = "git-ignore"
|
||||
scope = "source.gitignore"
|
||||
file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS"]
|
||||
file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS", { suffix = ".config/helix/ignore" }, { suffix = ".helix/ignore" }]
|
||||
injection-regex = "git-ignore"
|
||||
comment-token = "#"
|
||||
grammar = "gitignore"
|
||||
@ -1805,7 +1832,7 @@ language-servers = [ "nu-lsp" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "nu"
|
||||
source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "98c11c491e3405c75affa1cf004097692da3dda2" }
|
||||
source = { git = "https://github.com/nushell/tree-sitter-nu", rev = "358c4f509eb97f0148bbd25ad36acc729819b9c1" }
|
||||
|
||||
[[language]]
|
||||
name = "vala"
|
||||
@ -1913,6 +1940,12 @@ shebangs = ["scheme", "guile", "chicken"]
|
||||
comment-token = ";"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[language.auto-pairs]
|
||||
'(' = ')'
|
||||
'{' = '}'
|
||||
'[' = ']'
|
||||
'"' = '"'
|
||||
|
||||
[[grammar]]
|
||||
name = "scheme"
|
||||
source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af3af6c9356b936f8a515a1e449c32e804c2b1a8" }
|
||||
@ -2106,7 +2139,7 @@ language-servers = [ "slint-lsp" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "slint"
|
||||
source = { git = "https://github.com/jrmoulton/tree-sitter-slint", rev = "00c8a2d3645766f68c0d0460086c0a994e5b0d85" }
|
||||
source = { git = "https://github.com/slint-ui/tree-sitter-slint", rev = "15618215b79b9db08f824a5c97a12d073dcc1c00" }
|
||||
|
||||
[[language]]
|
||||
name = "task"
|
||||
@ -2249,7 +2282,7 @@ grammar = "vhs"
|
||||
|
||||
[[grammar]]
|
||||
name = "vhs"
|
||||
source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "c6d81f34c011c29ee86dd73b45a8ecc9f2e2bdaf" }
|
||||
source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "9534865e614c95eb9418e5e73f061c32fa4d9540" }
|
||||
|
||||
[[language]]
|
||||
name = "kdl"
|
||||
@ -2656,7 +2689,7 @@ language-servers = [ "cs" ]
|
||||
|
||||
[[grammar]]
|
||||
name = "smithy"
|
||||
source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "cf8c7eb9faf7c7049839585eac19c94af231e6a0" }
|
||||
source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "8327eb84d55639ffbe08c9dc82da7fff72a1ad07" }
|
||||
|
||||
[[language]]
|
||||
name = "vhdl"
|
||||
@ -2855,7 +2888,7 @@ indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "typst"
|
||||
source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "e35aa22395fdde82bbc4b5700c324ce346dfc9e5" }
|
||||
source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "ecf8596336857adfcd5f7cbb3b2aa11a67badc37" }
|
||||
|
||||
[[language]]
|
||||
name = "nunjucks"
|
||||
@ -2879,7 +2912,7 @@ source = { git = "https://github.com/varpeti/tree-sitter-jinja2", rev = "a533cd3
|
||||
|
||||
[[grammar]]
|
||||
name = "wren"
|
||||
source = { git = "https://git.sr.ht/~jummit/tree-sitter-wren", rev = "793d58266924e6efcc40e411663393e9d72bec87"}
|
||||
source = { git = "https://git.sr.ht/~jummit/tree-sitter-wren", rev = "6748694be32f11e7ec6b5faeb1b48ca6156d4e06" }
|
||||
|
||||
[[language]]
|
||||
name = "wren"
|
||||
@ -2907,7 +2940,7 @@ indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "unison"
|
||||
source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "98c4e8bc5c9f5989814a720457cf36963cf4043d" }
|
||||
source = { git = "https://github.com/kylegoetz/tree-sitter-unison", rev = "1f505e2447fa876a87aee47ff3d70b9e141c744f" }
|
||||
|
||||
[[language]]
|
||||
name = "todotxt"
|
||||
@ -2998,15 +3031,6 @@ file-types = ["log"]
|
||||
name = "log"
|
||||
source = { git = "https://github.com/Tudyx/tree-sitter-log", rev = "62cfe307e942af3417171243b599cc7deac5eab9" }
|
||||
|
||||
[[language]]
|
||||
name = "janet"
|
||||
scope = "source.janet"
|
||||
injection-regex = "janet"
|
||||
file-types = ["janet"]
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
grammar = "clojure"
|
||||
|
||||
[[language]]
|
||||
name = "hoon"
|
||||
scope = "source.hoon"
|
||||
@ -3018,3 +3042,45 @@ indent = {tab-width = 2, unit = " "}
|
||||
[[grammar]]
|
||||
name = "hoon"
|
||||
source = { git = "https://github.com/urbit-pilled/tree-sitter-hoon", rev = "1d5df35af3e0afe592832a67b9fb3feeeba1f7b6" }
|
||||
|
||||
[[language]]
|
||||
name = "hocon"
|
||||
scope = "source.conf"
|
||||
file-types = ["conf"]
|
||||
comment-token = "#"
|
||||
auto-format = true
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "hocon"
|
||||
source = { git = "https://github.com/antosha417/tree-sitter-hocon", rev = "c390f10519ae69fdb03b3e5764f5592fb6924bcc" }
|
||||
|
||||
[[language]]
|
||||
name = "tact"
|
||||
scope = "source.tact"
|
||||
injection-regex = "tact"
|
||||
file-types = ["tact"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[language.auto-pairs]
|
||||
'"' = '"'
|
||||
'{' = '}'
|
||||
'(' = ')'
|
||||
'<' = '>'
|
||||
|
||||
[[grammar]]
|
||||
name = "tact"
|
||||
source = { git = "https://github.com/tact-lang/tree-sitter-tact", rev = "ec57ab29c86d632639726631fb2bb178d23e1c91" }
|
||||
|
||||
[[language]]
|
||||
name = "pkl"
|
||||
scope = "source.pkl"
|
||||
injection-regex = "pkl"
|
||||
file-types = ["pkl", "pcf"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[grammar]]
|
||||
name = "pkl"
|
||||
source = { git = "https://github.com/apple/tree-sitter-pkl", rev = "c03f04a313b712f8ab00a2d862c10b37318699ae" }
|
||||
|
2
logo.svg
@ -1 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"><g transform="matrix(1,0,0,1,-31352.7,-1817.25)"><g transform="matrix(1,0,0,1,31062.7,-20.8972)"><g transform="matrix(1,0,0,1,-130.173,0.00185558)"><path style="fill:#706bc8;" d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z"></path></g><g transform="matrix(1,0,0,1,-130.173,0.00185558)"><path style="fill:#55c5e4;" d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z"></path></g><g transform="matrix(1,0,0,1,216.062,984.098)"><path style="fill:#84ddea;" d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z"></path></g><g transform="matrix(1,0,0,1,216.062,984.098)"><path style="fill:#997bc8;" d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z"></path></g></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="663.38 37.57 575.35 903.75"><path d="m1083.58 1875.72 551.48 318.4a47.66 47.66 0 0 1 23.82 41.27v105.94c0 8.51-2.27 16.7-6.38 23.83 0 0-437.8-252.76-545.3-314.83a47.245 47.245 0 0 1-23.62-40.92z" style="fill:#706bc8" transform="translate(-420.173 -1838.145)"/><path d="M1635.26 2604.84a47.228 47.228 0 0 1 23.62 40.91v133.69l-551.47-318.39a47.66 47.66 0 0 1-23.83-41.27v-105.94c0-8.52 2.27-16.71 6.38-23.83 0 0 437.8 252.76 545.3 314.83" style="fill:#55c5e4" transform="translate(-420.173 -1838.145)"/><path d="M790.407 1432.56a35.033 35.033 0 0 0-12.898 12.9c-9.647 16.7-4.036 38.3 12.495 48.13h-.006l-28.825-16.64a47.644 47.644 0 0 1-23.829-41.27v-105.94c0-17.03 9.083-32.76 23.829-41.27l498.417-287.73.24-.14a34.962 34.962 0 0 0 12.65-12.756c9.65-16.708 4.04-38.3-12.49-48.137h.01l28.82 16.642a47.648 47.648 0 0 1 23.83 41.273v105.938c0 17.03-9.08 32.76-23.83 41.27l-29.63 17.11.4-.26z" style="fill:#84ddea" transform="translate(-73.938 -854.05)"/><path d="M790.407 1686.24a35.08 35.08 0 0 0-12.898 12.89c-9.647 16.71-4.036 38.3 12.495 48.14h-.006l-28.825-16.64a47.656 47.656 0 0 1-23.829-41.27v-105.94c0-17.03 9.083-32.76 23.829-41.27l498.417-287.73.24-.14c5.09-2.99 9.5-7.29 12.65-12.76 9.65-16.71 4.04-38.3-12.49-48.14h.01l28.82 16.65a47.636 47.636 0 0 1 23.83 41.27v105.94c0 17.02-9.08 32.76-23.83 41.27l-29.63 17.1.4-.25z" style="fill:#997bc8" transform="translate(-73.938 -854.05)"/></svg>
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.5 KiB |
116
logo_dark.svg
@ -1,115 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
|
||||
viewBox="663.38 37.57 2087.006 903.71997"
|
||||
id="svg22"
|
||||
sodipodi:docname="logo_dark.svg"
|
||||
width="2087.0059"
|
||||
height="903.71997"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs26"><rect
|
||||
x="713.02588"
|
||||
y="-304.32538"
|
||||
width="3615.2336"
|
||||
height="1864.7544"
|
||||
id="rect14663" /><rect
|
||||
x="972.073"
|
||||
y="151.15895"
|
||||
width="2140.9646"
|
||||
height="684.86273"
|
||||
id="rect447" /><rect
|
||||
x="897.0401"
|
||||
y="217.45384"
|
||||
width="837.72321"
|
||||
height="631.59924"
|
||||
id="rect435" /><rect
|
||||
x="825.67834"
|
||||
y="157.61452"
|
||||
width="1496.2448"
|
||||
height="861.45544"
|
||||
id="rect429" /><rect
|
||||
x="798.3819"
|
||||
y="-42.157242"
|
||||
width="2236.0837"
|
||||
height="945.90723"
|
||||
id="rect315" /><rect
|
||||
x="661.30237"
|
||||
y="48.087799"
|
||||
width="769.15619"
|
||||
height="828.46844"
|
||||
id="rect309" /></defs><sodipodi:namedview
|
||||
id="namedview24"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.28409405"
|
||||
inkscape:cx="1904.2989"
|
||||
inkscape:cy="633.59299"
|
||||
inkscape:window-width="1908"
|
||||
inkscape:window-height="2075"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg22" /> <g
|
||||
transform="translate(-31352.726,-1817.2547)"
|
||||
id="g20"> <g
|
||||
transform="translate(31062.7,-20.8972)"
|
||||
id="g18"> <g
|
||||
transform="translate(-130.173,0.00185558)"
|
||||
id="g4"> <path
|
||||
d="m 1083.58,1875.72 551.48,318.4 c 14.74,8.51 23.82,24.25 23.82,41.27 0,29.59 0,76.35 0,105.94 0,8.51 -2.27,16.7 -6.38,23.83 0,0 -437.8,-252.76 -545.3,-314.83 -14.62,-8.44 -23.62,-24.04 -23.62,-40.92 0,-45.91 0,-133.69 0,-133.69 z"
|
||||
style="fill:#706bc8"
|
||||
id="path2" /> </g> <g
|
||||
transform="translate(-130.173,0.00185558)"
|
||||
id="g8"> <path
|
||||
d="m 1635.26,2604.84 c 14.62,8.44 23.62,24.03 23.62,40.91 0,45.92 0,133.69 0,133.69 l -551.47,-318.39 c -14.75,-8.52 -23.83,-24.25 -23.83,-41.27 0,-29.59 0,-76.36 0,-105.94 0,-8.52 2.27,-16.71 6.38,-23.83 0,0 437.8,252.76 545.3,314.83 z"
|
||||
style="fill:#55c5e4"
|
||||
id="path6" /> </g> <g
|
||||
transform="translate(216.062,984.098)"
|
||||
id="g12"> <path
|
||||
d="m 790.407,1432.56 c -5.193,2.99 -9.69,7.34 -12.898,12.9 -9.647,16.7 -4.036,38.3 12.495,48.13 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.24 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.983 9.5,-7.286 12.65,-12.756 9.65,-16.708 4.04,-38.3 -12.49,-48.137 h 0.01 l 28.82,16.642 c 14.75,8.513 23.83,24.246 23.83,41.273 0,29.588 0,76.348 0,105.938 0,17.03 -9.08,32.76 -23.83,41.27 l -29.63,17.11 0.4,-0.26 z"
|
||||
style="fill:#84ddea"
|
||||
id="path10" /> </g> <g
|
||||
transform="translate(216.062,984.098)"
|
||||
id="g16"> <path
|
||||
d="m 790.407,1686.24 c -5.193,2.99 -9.69,7.34 -12.898,12.89 -9.647,16.71 -4.036,38.3 12.495,48.14 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.25 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.99 9.5,-7.29 12.65,-12.76 9.65,-16.71 4.04,-38.3 -12.49,-48.14 h 0.01 l 28.82,16.65 c 14.75,8.51 23.83,24.24 23.83,41.27 0,29.59 0,76.35 0,105.94 0,17.02 -9.08,32.76 -23.83,41.27 l -29.63,17.1 0.4,-0.25 z"
|
||||
style="fill:#997bc8"
|
||||
id="path14" /></g></g></g> <text
|
||||
xml:space="preserve"
|
||||
transform="translate(663.354,37.565425)"
|
||||
id="text307"
|
||||
style="font-size:4px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect309);display:inline;fill:#006400;stroke:#006400;stroke-width:2.66667" /><g
|
||||
aria-label="Helix"
|
||||
transform="matrix(1.3113898,0,0,1.3113898,142.0244,48.21073)"
|
||||
id="text445"
|
||||
style="font-size:4px;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect447);display:inline;fill:#f0f6fc;stroke:#f0f6fc;stroke-width:2.66687;stroke-opacity:1;fill-opacity:1"><path
|
||||
d="m 1242.0723,515.10828 h -60.4 v -123.2 h -113.2 v 123.2 h -60.4 v -285.6 h 60.4 v 112 h 113.2 v -112 h 60.4 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
|
||||
id="path14794" /><path
|
||||
d="m 1399.272,292.70828 q 30.4,0 52,11.6 22,11.6 34,33.6 12,22 12,54 v 28.8 h -140.8 q 0.8,25.2 14.8,39.6 14.4,14.4 39.6,14.4 21.2,0 38.4,-4 17.2,-4.4 35.6,-13.2 v 46 q -16,8 -34,11.6 -17.6,4 -42.8,4 -32.8,0 -58,-12 -25.2,-12.4 -39.6,-37.2 -14.4,-24.8 -14.4,-62.4 0,-38.4 12.8,-63.6 13.2,-25.6 36.4,-38.4 23.2,-12.8 54,-12.8 z m 0.4,42.4 q -17.2,0 -28.8,11.2 -11.2,11.2 -13.2,34.8 h 83.6 q 0,-13.2 -4.8,-23.6 -4.4,-10.4 -13.6,-16.4 -9.2,-6 -23.2,-6 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
|
||||
id="path14796" /><path
|
||||
d="m 1605.2719,515.10828 h -59.6 v -304 h 59.6 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
|
||||
id="path14798" /><path
|
||||
d="m 1727.272,296.70828 v 218.4 h -59.6 v -218.4 z m -29.6,-85.6 q 13.2,0 22.8,6.4 9.6,6 9.6,22.8 0,16.4 -9.6,22.8 -9.6,6.4 -22.8,6.4 -13.6,0 -23.2,-6.4 -9.2,-6.4 -9.2,-22.8 0,-16.8 9.2,-22.8 9.6,-6.4 23.2,-6.4 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
|
||||
id="path14800" /><path
|
||||
d="m 1834.4721,403.50828 -70.4,-106.8 h 67.6 l 42.4,69.6 42.8,-69.6 h 67.6 l -71.2,106.8 74.4,111.6 h -67.6 l -46,-74.8 -46,74.8 h -67.6 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
|
||||
id="path14802" /></g><text
|
||||
xml:space="preserve"
|
||||
transform="translate(663.38,37.570044)"
|
||||
id="text14661"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="2087.006" height="903.72" style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="663.38 37.57 2087.006 903.72"><defs><path id="c" d="M713.026-304.325H4328.26v1864.754H713.026z"/><path id="b" d="M972.073 151.159h2140.965v684.863H972.073z"/><path id="a" d="M661.302 48.088h769.156v828.468H661.302z"/></defs><path d="m1083.58 1875.72 551.48 318.4a47.66 47.66 0 0 1 23.82 41.27v105.94c0 8.51-2.27 16.7-6.38 23.83 0 0-437.8-252.76-545.3-314.83a47.245 47.245 0 0 1-23.62-40.92v-133.69z" style="fill:#706bc8" transform="translate(-420.199 -1838.15)"/><path d="M1635.26 2604.84a47.228 47.228 0 0 1 23.62 40.91v133.69l-551.47-318.39a47.66 47.66 0 0 1-23.83-41.27v-105.94c0-8.52 2.27-16.71 6.38-23.83 0 0 437.8 252.76 545.3 314.83" style="fill:#55c5e4" transform="translate(-420.199 -1838.15)"/><path d="M790.407 1432.56a35.033 35.033 0 0 0-12.898 12.9c-9.647 16.7-4.036 38.3 12.495 48.13h-.006l-28.825-16.64a47.644 47.644 0 0 1-23.829-41.27v-105.94c0-17.03 9.083-32.76 23.829-41.27l498.417-287.73.24-.14a34.962 34.962 0 0 0 12.65-12.756c9.65-16.708 4.04-38.3-12.49-48.137h.01l28.82 16.642a47.648 47.648 0 0 1 23.83 41.273v105.938c0 17.03-9.08 32.76-23.83 41.27l-29.63 17.11.4-.26z" style="fill:#84ddea" transform="translate(-73.964 -854.054)"/><path d="M790.407 1686.24a35.08 35.08 0 0 0-12.898 12.89c-9.647 16.71-4.036 38.3 12.495 48.14h-.006l-28.825-16.64a47.656 47.656 0 0 1-23.829-41.27v-105.94c0-17.03 9.083-32.76 23.829-41.27l498.417-287.73.24-.14c5.09-2.99 9.5-7.29 12.65-12.76 9.65-16.71 4.04-38.3-12.49-48.14h.01l28.82 16.65a47.636 47.636 0 0 1 23.83 41.27v105.94c0 17.02-9.08 32.76-23.83 41.27l-29.63 17.1.4-.25z" style="fill:#997bc8" transform="translate(-73.964 -854.054)"/><g aria-label="Helix" style="font-size:4px;-inkscape-font-specification:"sans-serif, Normal";white-space:pre;shape-inside:url(#b);display:inline;fill:#f0f6fc;stroke:#f0f6fc;stroke-width:2.66687;stroke-opacity:1;fill-opacity:1" transform="matrix(1.31139 0 0 1.31139 142.024 48.21)"><path d="M1242.072 515.108h-60.4v-123.2h-113.2v123.2h-60.4v-285.6h60.4v112h113.2v-112h60.4zM1399.272 292.708q30.4 0 52 11.6 22 11.6 34 33.6t12 54v28.8h-140.8q.8 25.2 14.8 39.6 14.4 14.4 39.6 14.4 21.2 0 38.4-4 17.2-4.4 35.6-13.2v46q-16 8-34 11.6-17.6 4-42.8 4-32.8 0-58-12-25.2-12.4-39.6-37.2-14.4-24.8-14.4-62.4 0-38.4 12.8-63.6 13.2-25.6 36.4-38.4 23.2-12.8 54-12.8zm.4 42.4q-17.2 0-28.8 11.2-11.2 11.2-13.2 34.8h83.6q0-13.2-4.8-23.6-4.4-10.4-13.6-16.4-9.2-6-23.2-6zM1605.272 515.108h-59.6v-304h59.6zM1727.272 296.708v218.4h-59.6v-218.4zm-29.6-85.6q13.2 0 22.8 6.4 9.6 6 9.6 22.8 0 16.4-9.6 22.8-9.6 6.4-22.8 6.4-13.6 0-23.2-6.4-9.2-6.4-9.2-22.8 0-16.8 9.2-22.8 9.6-6.4 23.2-6.4zM1834.472 403.508l-70.4-106.8h67.6l42.4 69.6 42.8-69.6h67.6l-71.2 106.8 74.4 111.6h-67.6l-46-74.8-46 74.8h-67.6z" style="font-size:400px;-inkscape-font-specification:"sans-serif, @wght=700";font-variation-settings:"wght"700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"/></g></svg>
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 3.0 KiB |
116
logo_light.svg
@ -1,115 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
|
||||
viewBox="663.38 37.57 2087.006 903.71997"
|
||||
id="svg22"
|
||||
sodipodi:docname="logo_light.svg"
|
||||
width="2087.0059"
|
||||
height="903.71997"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs26"><rect
|
||||
x="713.02588"
|
||||
y="-304.32538"
|
||||
width="3615.2336"
|
||||
height="1864.7544"
|
||||
id="rect14663" /><rect
|
||||
x="972.073"
|
||||
y="151.15895"
|
||||
width="2140.9646"
|
||||
height="684.86273"
|
||||
id="rect447" /><rect
|
||||
x="897.0401"
|
||||
y="217.45384"
|
||||
width="837.72321"
|
||||
height="631.59924"
|
||||
id="rect435" /><rect
|
||||
x="825.67834"
|
||||
y="157.61452"
|
||||
width="1496.2448"
|
||||
height="861.45544"
|
||||
id="rect429" /><rect
|
||||
x="798.3819"
|
||||
y="-42.157242"
|
||||
width="2236.0837"
|
||||
height="945.90723"
|
||||
id="rect315" /><rect
|
||||
x="661.30237"
|
||||
y="48.087799"
|
||||
width="769.15619"
|
||||
height="828.46844"
|
||||
id="rect309" /></defs><sodipodi:namedview
|
||||
id="namedview24"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.28409405"
|
||||
inkscape:cx="1904.2989"
|
||||
inkscape:cy="633.59299"
|
||||
inkscape:window-width="1908"
|
||||
inkscape:window-height="2075"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg22" /> <g
|
||||
transform="translate(-31352.726,-1817.2547)"
|
||||
id="g20"> <g
|
||||
transform="translate(31062.7,-20.8972)"
|
||||
id="g18"> <g
|
||||
transform="translate(-130.173,0.00185558)"
|
||||
id="g4"> <path
|
||||
d="m 1083.58,1875.72 551.48,318.4 c 14.74,8.51 23.82,24.25 23.82,41.27 0,29.59 0,76.35 0,105.94 0,8.51 -2.27,16.7 -6.38,23.83 0,0 -437.8,-252.76 -545.3,-314.83 -14.62,-8.44 -23.62,-24.04 -23.62,-40.92 0,-45.91 0,-133.69 0,-133.69 z"
|
||||
style="fill:#706bc8"
|
||||
id="path2" /> </g> <g
|
||||
transform="translate(-130.173,0.00185558)"
|
||||
id="g8"> <path
|
||||
d="m 1635.26,2604.84 c 14.62,8.44 23.62,24.03 23.62,40.91 0,45.92 0,133.69 0,133.69 l -551.47,-318.39 c -14.75,-8.52 -23.83,-24.25 -23.83,-41.27 0,-29.59 0,-76.36 0,-105.94 0,-8.52 2.27,-16.71 6.38,-23.83 0,0 437.8,252.76 545.3,314.83 z"
|
||||
style="fill:#55c5e4"
|
||||
id="path6" /> </g> <g
|
||||
transform="translate(216.062,984.098)"
|
||||
id="g12"> <path
|
||||
d="m 790.407,1432.56 c -5.193,2.99 -9.69,7.34 -12.898,12.9 -9.647,16.7 -4.036,38.3 12.495,48.13 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.24 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.983 9.5,-7.286 12.65,-12.756 9.65,-16.708 4.04,-38.3 -12.49,-48.137 h 0.01 l 28.82,16.642 c 14.75,8.513 23.83,24.246 23.83,41.273 0,29.588 0,76.348 0,105.938 0,17.03 -9.08,32.76 -23.83,41.27 l -29.63,17.11 0.4,-0.26 z"
|
||||
style="fill:#84ddea"
|
||||
id="path10" /> </g> <g
|
||||
transform="translate(216.062,984.098)"
|
||||
id="g16"> <path
|
||||
d="m 790.407,1686.24 c -5.193,2.99 -9.69,7.34 -12.898,12.89 -9.647,16.71 -4.036,38.3 12.495,48.14 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.25 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.99 9.5,-7.29 12.65,-12.76 9.65,-16.71 4.04,-38.3 -12.49,-48.14 h 0.01 l 28.82,16.65 c 14.75,8.51 23.83,24.24 23.83,41.27 0,29.59 0,76.35 0,105.94 0,17.02 -9.08,32.76 -23.83,41.27 l -29.63,17.1 0.4,-0.25 z"
|
||||
style="fill:#997bc8"
|
||||
id="path14" /></g></g></g> <text
|
||||
xml:space="preserve"
|
||||
transform="translate(663.354,37.565425)"
|
||||
id="text307"
|
||||
style="font-size:4px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect309);display:inline;fill:#006400;stroke:#006400;stroke-width:2.66667" /><g
|
||||
aria-label="Helix"
|
||||
transform="matrix(1.3113898,0,0,1.3113898,142.0244,48.21073)"
|
||||
id="text445"
|
||||
style="font-size:4px;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect447);display:inline;fill:#2a292f;stroke:#2a292f;stroke-width:2.66687"><path
|
||||
d="m 1242.0723,515.10828 h -60.4 v -123.2 h -113.2 v 123.2 h -60.4 v -285.6 h 60.4 v 112 h 113.2 v -112 h 60.4 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
|
||||
id="path14794" /><path
|
||||
d="m 1399.272,292.70828 q 30.4,0 52,11.6 22,11.6 34,33.6 12,22 12,54 v 28.8 h -140.8 q 0.8,25.2 14.8,39.6 14.4,14.4 39.6,14.4 21.2,0 38.4,-4 17.2,-4.4 35.6,-13.2 v 46 q -16,8 -34,11.6 -17.6,4 -42.8,4 -32.8,0 -58,-12 -25.2,-12.4 -39.6,-37.2 -14.4,-24.8 -14.4,-62.4 0,-38.4 12.8,-63.6 13.2,-25.6 36.4,-38.4 23.2,-12.8 54,-12.8 z m 0.4,42.4 q -17.2,0 -28.8,11.2 -11.2,11.2 -13.2,34.8 h 83.6 q 0,-13.2 -4.8,-23.6 -4.4,-10.4 -13.6,-16.4 -9.2,-6 -23.2,-6 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
|
||||
id="path14796" /><path
|
||||
d="m 1605.2719,515.10828 h -59.6 v -304 h 59.6 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
|
||||
id="path14798" /><path
|
||||
d="m 1727.272,296.70828 v 218.4 h -59.6 v -218.4 z m -29.6,-85.6 q 13.2,0 22.8,6.4 9.6,6 9.6,22.8 0,16.4 -9.6,22.8 -9.6,6.4 -22.8,6.4 -13.6,0 -23.2,-6.4 -9.2,-6.4 -9.2,-22.8 0,-16.8 9.2,-22.8 9.6,-6.4 23.2,-6.4 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
|
||||
id="path14800" /><path
|
||||
d="m 1834.4721,403.50828 -70.4,-106.8 h 67.6 l 42.4,69.6 42.8,-69.6 h 67.6 l -71.2,106.8 74.4,111.6 h -67.6 l -46,-74.8 -46,74.8 h -67.6 z"
|
||||
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
|
||||
id="path14802" /></g><text
|
||||
xml:space="preserve"
|
||||
transform="translate(663.38,37.570044)"
|
||||
id="text14661"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="2087.006" height="903.72" style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="663.38 37.57 2087.006 903.72"><defs><path id="c" d="M713.026-304.325H4328.26v1864.754H713.026z"/><path id="b" d="M972.073 151.159h2140.965v684.863H972.073z"/><path id="a" d="M661.302 48.088h769.156v828.468H661.302z"/></defs><path d="m1083.58 1875.72 551.48 318.4a47.66 47.66 0 0 1 23.82 41.27v105.94c0 8.51-2.27 16.7-6.38 23.83 0 0-437.8-252.76-545.3-314.83a47.245 47.245 0 0 1-23.62-40.92v-133.69z" style="fill:#706bc8" transform="translate(-420.199 -1838.15)"/><path d="M1635.26 2604.84a47.228 47.228 0 0 1 23.62 40.91v133.69l-551.47-318.39a47.66 47.66 0 0 1-23.83-41.27v-105.94c0-8.52 2.27-16.71 6.38-23.83 0 0 437.8 252.76 545.3 314.83" style="fill:#55c5e4" transform="translate(-420.199 -1838.15)"/><path d="M790.407 1432.56a35.033 35.033 0 0 0-12.898 12.9c-9.647 16.7-4.036 38.3 12.495 48.13h-.006l-28.825-16.64a47.644 47.644 0 0 1-23.829-41.27v-105.94c0-17.03 9.083-32.76 23.829-41.27l498.417-287.73.24-.14a34.962 34.962 0 0 0 12.65-12.756c9.65-16.708 4.04-38.3-12.49-48.137h.01l28.82 16.642a47.648 47.648 0 0 1 23.83 41.273v105.938c0 17.03-9.08 32.76-23.83 41.27l-29.63 17.11.4-.26z" style="fill:#84ddea" transform="translate(-73.964 -854.054)"/><path d="M790.407 1686.24a35.08 35.08 0 0 0-12.898 12.89c-9.647 16.71-4.036 38.3 12.495 48.14h-.006l-28.825-16.64a47.656 47.656 0 0 1-23.829-41.27v-105.94c0-17.03 9.083-32.76 23.829-41.27l498.417-287.73.24-.14c5.09-2.99 9.5-7.29 12.65-12.76 9.65-16.71 4.04-38.3-12.49-48.14h.01l28.82 16.65a47.636 47.636 0 0 1 23.83 41.27v105.94c0 17.02-9.08 32.76-23.83 41.27l-29.63 17.1.4-.25z" style="fill:#997bc8" transform="translate(-73.964 -854.054)"/><g aria-label="Helix" style="font-size:4px;-inkscape-font-specification:"sans-serif, Normal";white-space:pre;shape-inside:url(#b);display:inline;fill:#2a292f;stroke:#2a292f;stroke-width:2.66687" transform="matrix(1.31139 0 0 1.31139 142.024 48.21)"><path d="M1242.072 515.108h-60.4v-123.2h-113.2v123.2h-60.4v-285.6h60.4v112h113.2v-112h60.4zM1399.272 292.708q30.4 0 52 11.6 22 11.6 34 33.6t12 54v28.8h-140.8q.8 25.2 14.8 39.6 14.4 14.4 39.6 14.4 21.2 0 38.4-4 17.2-4.4 35.6-13.2v46q-16 8-34 11.6-17.6 4-42.8 4-32.8 0-58-12-25.2-12.4-39.6-37.2-14.4-24.8-14.4-62.4 0-38.4 12.8-63.6 13.2-25.6 36.4-38.4 23.2-12.8 54-12.8zm.4 42.4q-17.2 0-28.8 11.2-11.2 11.2-13.2 34.8h83.6q0-13.2-4.8-23.6-4.4-10.4-13.6-16.4-9.2-6-23.2-6zM1605.272 515.108h-59.6v-304h59.6zM1727.272 296.708v218.4h-59.6v-218.4zm-29.6-85.6q13.2 0 22.8 6.4 9.6 6 9.6 22.8 0 16.4-9.6 22.8-9.6 6.4-22.8 6.4-13.6 0-23.2-6.4-9.2-6.4-9.2-22.8 0-16.8 9.2-22.8 9.6-6.4 23.2-6.4zM1834.472 403.508l-70.4-106.8h67.6l42.4 69.6 42.8-69.6h67.6l-71.2 106.8 74.4 111.6h-67.6l-46-74.8-46 74.8h-67.6z" style="font-size:400px;-inkscape-font-specification:"sans-serif, @wght=700";font-variation-settings:"wght"700"/></g></svg>
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 2.9 KiB |
@ -10,23 +10,37 @@
|
||||
(variable_name) @variable.other.member
|
||||
|
||||
[
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"elif"
|
||||
"fi"
|
||||
"case"
|
||||
"in"
|
||||
"esac"
|
||||
] @keyword.control.conditional
|
||||
|
||||
[
|
||||
"for"
|
||||
"do"
|
||||
"done"
|
||||
"elif"
|
||||
"else"
|
||||
"esac"
|
||||
"export"
|
||||
"fi"
|
||||
"for"
|
||||
"function"
|
||||
"if"
|
||||
"in"
|
||||
"unset"
|
||||
"select"
|
||||
"until"
|
||||
"while"
|
||||
"then"
|
||||
] @keyword.control.repeat
|
||||
|
||||
[
|
||||
"declare"
|
||||
"typeset"
|
||||
"export"
|
||||
"readonly"
|
||||
"local"
|
||||
"unset"
|
||||
"unsetenv"
|
||||
] @keyword
|
||||
|
||||
"function" @keyword.function
|
||||
|
||||
(comment) @comment
|
||||
|
||||
(function_definition name: (word) @function)
|
||||
|
@ -44,3 +44,5 @@
|
||||
; User mention (@user)
|
||||
("text" @tag
|
||||
(#match? @tag "^[@][a-zA-Z0-9_-]+$"))
|
||||
|
||||
(uri) @markup.link.url
|
||||
|
7
runtime/queries/css/indents.scm
Normal file
@ -0,0 +1,7 @@
|
||||
[
|
||||
(block)
|
||||
] @indent
|
||||
|
||||
[
|
||||
"}"
|
||||
] @outdent
|
68
runtime/queries/dart/textobjects.scm
Normal file
@ -0,0 +1,68 @@
|
||||
(class_definition
|
||||
body: (_) @class.inside) @class.around
|
||||
|
||||
(mixin_declaration
|
||||
(class_body) @class.inside) @class.around
|
||||
|
||||
(extension_declaration
|
||||
(extension_body) @class.inside) @class.around
|
||||
|
||||
(enum_declaration
|
||||
body: (_) @class.inside) @class.around
|
||||
|
||||
(type_alias) @class.around
|
||||
|
||||
(_
|
||||
(
|
||||
[
|
||||
(getter_signature)
|
||||
(setter_signature)
|
||||
(function_signature)
|
||||
(method_signature)
|
||||
(constructor_signature)
|
||||
]
|
||||
.
|
||||
(function_body) @function.inside @function.around
|
||||
) @function.around
|
||||
)
|
||||
|
||||
(declaration
|
||||
[
|
||||
(constant_constructor_signature)
|
||||
(constructor_signature)
|
||||
(factory_constructor_signature)
|
||||
(redirecting_factory_constructor_signature)
|
||||
(getter_signature)
|
||||
(setter_signature)
|
||||
(operator_signature)
|
||||
(function_signature)
|
||||
]
|
||||
) @function.around
|
||||
|
||||
(lambda_expression
|
||||
body: (_) @function.inside
|
||||
) @function.around
|
||||
|
||||
(function_expression
|
||||
body: (_) @function.inside
|
||||
) @function.around
|
||||
|
||||
[
|
||||
(comment)
|
||||
(documentation_comment)
|
||||
] @comment.inside
|
||||
|
||||
(comment)+ @comment.around
|
||||
|
||||
(documentation_comment)+ @comment.around
|
||||
|
||||
(formal_parameter) @parameter.inside
|
||||
|
||||
(formal_parameter_list) @parameter.around
|
||||
|
||||
(expression_statement
|
||||
((identifier) @_name (#any-of? @_name "test" "testWidgets"))
|
||||
.
|
||||
(selector (argument_part (arguments . (_) . (argument) @test.inside)))
|
||||
) @test.around
|
||||
|
@ -13,5 +13,7 @@
|
||||
(typed_default_parameter)
|
||||
] @parameter.inside @parameter.around)
|
||||
|
||||
(arguments (_expression) @parameter.inside @parameter.around)
|
||||
|
||||
(comment) @comment.inside
|
||||
(comment)+ @comment.around
|
||||
|
@ -183,9 +183,12 @@
|
||||
|
||||
[
|
||||
(int_literal)
|
||||
] @constant.numeric.integer
|
||||
|
||||
[
|
||||
(float_literal)
|
||||
(imaginary_literal)
|
||||
] @constant.numeric.integer
|
||||
] @constant.numeric.float
|
||||
|
||||
[
|
||||
(true)
|
||||
@ -197,4 +200,31 @@
|
||||
(iota)
|
||||
] @constant.builtin
|
||||
|
||||
; Comments
|
||||
|
||||
(comment) @comment
|
||||
|
||||
; Doc Comments
|
||||
(source_file
|
||||
.
|
||||
(comment)+ @comment.block.documentation)
|
||||
|
||||
(source_file
|
||||
(comment)+ @comment.block.documentation
|
||||
.
|
||||
(const_declaration))
|
||||
|
||||
(source_file
|
||||
(comment)+ @comment.block.documentation
|
||||
.
|
||||
(function_declaration))
|
||||
|
||||
(source_file
|
||||
(comment)+ @comment.block.documentation
|
||||
.
|
||||
(type_declaration))
|
||||
|
||||
(source_file
|
||||
(comment)+ @comment.block.documentation
|
||||
.
|
||||
(var_declaration))
|
||||
|
@ -1,2 +1,14 @@
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
||||
|
||||
|
||||
(call_expression
|
||||
(selector_expression) @_function
|
||||
(#any-of? @_function "regexp.Match" "regexp.MatchReader" "regexp.MatchString" "regexp.Compile" "regexp.CompilePOSIX" "regexp.MustCompile" "regexp.MustCompilePOSIX")
|
||||
(argument_list
|
||||
.
|
||||
[
|
||||
(raw_string_literal)
|
||||
(interpreted_string_literal)
|
||||
] @injection.content
|
||||
(#set! injection.language "regex")))
|
||||
|