From 5f44890176b30784c6b165c0a40026ed0913bffa Mon Sep 17 00:00:00 2001 From: Aito Stukas Date: Thu, 4 Apr 2024 20:12:19 -0400 Subject: [PATCH 001/126] Remove duplicate keyword highlight for Python 'and' (#10160) --- runtime/queries/python/highlights.scm | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/queries/python/highlights.scm b/runtime/queries/python/highlights.scm index 2666402b8..1bded9529 100644 --- a/runtime/queries/python/highlights.scm +++ b/runtime/queries/python/highlights.scm @@ -204,7 +204,6 @@ (for_in_clause "in" @keyword.control) [ - "and" "async" "class" "exec" From d053886fe3e0489c56e41477572bff8c310ed519 Mon Sep 17 00:00:00 2001 From: Karem Abdul-Samad Date: Thu, 4 Apr 2024 20:22:41 -0400 Subject: [PATCH 002/126] Add new keyword as per PEP 695 (#10165) --- runtime/queries/python/highlights.scm | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/queries/python/highlights.scm b/runtime/queries/python/highlights.scm index 1bded9529..0a082f2fd 100644 --- a/runtime/queries/python/highlights.scm +++ b/runtime/queries/python/highlights.scm @@ -210,6 +210,7 @@ "global" "nonlocal" "print" + "type" ] @keyword [ "and" From f240d896a484b56df4d1e7dd29d9f62040bf76cc Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 5 Apr 2024 01:50:41 -0400 Subject: [PATCH 003/126] Merge unnecessary/deprecated diagnostic highlights separately (#10084) Previously unnecessary/deprecated diagnostic tags replaced the highlight for the severity of a diagnostic. This could cause either the severity or unnecessary/deprecated scopes to disappear when diagnostic ranges overlapped though. Plus the severity highlight can be interesting in addition to the unnecessary/deprecated highlight. So this change separates the unnecessary and deprecated highlights from the severity highlights, so each is merged separately and when they overlap, the highlights are combined. --- helix-term/src/ui/editor.rs | 76 ++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ad7aa5c5a..97a08beb7 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -359,8 +359,8 @@ pub fn overlay_syntax_highlights( pub fn doc_diagnostics_highlights( doc: &Document, theme: &Theme, - ) -> [Vec<(usize, std::ops::Range)>; 5] { - use helix_core::diagnostic::{DiagnosticTag, Severity}; + ) -> [Vec<(usize, std::ops::Range)>; 7] { + use helix_core::diagnostic::{DiagnosticTag, Range, Severity}; let get_scope_of = |scope| { theme .find_scope_index_exact(scope) @@ -389,6 +389,25 @@ pub fn doc_diagnostics_highlights( let mut hint_vec = Vec::new(); let mut warning_vec = Vec::new(); let mut error_vec = Vec::new(); + let mut unnecessary_vec = Vec::new(); + let mut deprecated_vec = Vec::new(); + + let push_diagnostic = + |vec: &mut Vec<(usize, std::ops::Range)>, scope, range: Range| { + // If any diagnostic overlaps ranges with the prior diagnostic, + // merge the two together. Otherwise push a new span. + match vec.last_mut() { + Some((_, existing_range)) if range.start <= existing_range.end => { + // This branch merges overlapping diagnostics, assuming that the current + // diagnostic starts on range.start or later. If this assertion fails, + // we will discard some part of `diagnostic`. This implies that + // `doc.diagnostics()` is not sorted by `diagnostic.range`. + debug_assert!(existing_range.start <= range.start); + existing_range.end = range.end.max(existing_range.end) + } + _ => vec.push((scope, range.start..range.end)), + } + }; for diagnostic in doc.diagnostics() { // Separate diagnostics into different Vecs by severity. @@ -400,31 +419,44 @@ pub fn doc_diagnostics_highlights( _ => (&mut default_vec, r#default), }; - let scope = diagnostic - .tags - .first() - .and_then(|tag| match tag { - DiagnosticTag::Unnecessary => unnecessary, - DiagnosticTag::Deprecated => deprecated, - }) - .unwrap_or(scope); + // If the diagnostic has tags and a non-warning/error severity, skip rendering + // the diagnostic as info/hint/default and only render it as unnecessary/deprecated + // instead. For warning/error diagnostics, render both the severity highlight and + // the tag highlight. + if diagnostic.tags.is_empty() + || matches!( + diagnostic.severity, + Some(Severity::Warning | Severity::Error) + ) + { + push_diagnostic(vec, scope, diagnostic.range); + } - // If any diagnostic overlaps ranges with the prior diagnostic, - // merge the two together. Otherwise push a new span. - match vec.last_mut() { - Some((_, range)) if diagnostic.range.start <= range.end => { - // This branch merges overlapping diagnostics, assuming that the current - // diagnostic starts on range.start or later. If this assertion fails, - // we will discard some part of `diagnostic`. This implies that - // `doc.diagnostics()` is not sorted by `diagnostic.range`. - debug_assert!(range.start <= diagnostic.range.start); - range.end = diagnostic.range.end.max(range.end) + for tag in &diagnostic.tags { + match tag { + DiagnosticTag::Unnecessary => { + if let Some(scope) = unnecessary { + push_diagnostic(&mut unnecessary_vec, scope, diagnostic.range) + } + } + DiagnosticTag::Deprecated => { + if let Some(scope) = deprecated { + push_diagnostic(&mut deprecated_vec, scope, diagnostic.range) + } + } } - _ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)), } } - [default_vec, info_vec, hint_vec, warning_vec, error_vec] + [ + default_vec, + unnecessary_vec, + deprecated_vec, + info_vec, + hint_vec, + warning_vec, + error_vec, + ] } /// Get highlight spans for selections in a document view. From a2ee2e66f2725fbd2d595d43c58ebf43ef9e9383 Mon Sep 17 00:00:00 2001 From: Matthew Toohey Date: Fri, 5 Apr 2024 05:53:07 -0400 Subject: [PATCH 004/126] Add koka language server and update grammar (#10119) --- book/src/generated/lang-support.md | 2 +- languages.toml | 4 +++- runtime/queries/koka/highlights.scm | 34 ----------------------------- runtime/queries/koka/indents.scm | 1 - 4 files changed, 4 insertions(+), 37 deletions(-) diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index efdc8b634..156898d45 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -100,7 +100,7 @@ | julia | ✓ | ✓ | ✓ | `julia` | | just | ✓ | ✓ | ✓ | | | kdl | ✓ | ✓ | ✓ | | -| koka | ✓ | | ✓ | | +| koka | ✓ | | ✓ | `koka` | | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | | ld | ✓ | | ✓ | | diff --git a/languages.toml b/languages.toml index 95c467659..49185cc50 100644 --- a/languages.toml +++ b/languages.toml @@ -45,6 +45,7 @@ intelephense = { command = "intelephense", args = ["--stdio"] } jdtls = { command = "jdtls" } jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] } julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] } +koka = { command = "koka", args = ["--language-server", "--lsstdio"] } kotlin-language-server = { command = "kotlin-language-server" } lean = { command = "lean", args = [ "--server" ] } ltex-ls = { command = "ltex-ls" } @@ -3264,10 +3265,11 @@ injection-regex = "koka" file-types = ["kk"] comment-token = "//" indent = { tab-width = 8, unit = " " } +language-servers = ["koka"] [[grammar]] name = "koka" -source = { git = "https://github.com/mtoohey31/tree-sitter-koka", rev = "2527e152d4b6a79fd50aebd8d0b4b4336c94a034" } +source = { git = "https://github.com/mtoohey31/tree-sitter-koka", rev = "96d070c3700692858035f3524cc0ad944cef2594" } [[language]] name = "tact" diff --git a/runtime/queries/koka/highlights.scm b/runtime/queries/koka/highlights.scm index fead98384..1ef409278 100644 --- a/runtime/queries/koka/highlights.scm +++ b/runtime/queries/koka/highlights.scm @@ -12,18 +12,6 @@ ]))) ["(" (block) (fnexpr)]) -(ntlappexpr - function: (ntlappexpr - (atom - (qidentifier - [ - (qvarid) @function - (qidop) @function - (identifier - [(varid) (idop)] @function) - ]))) - ["(" (block) (fnexpr)]) - (appexpr field: (atom (qidentifier @@ -46,28 +34,6 @@ ]))) "[") -(ntlappexpr - field: (atom - (qidentifier - [ - (qvarid) @function - (qidop) @function - (identifier - [(varid) (idop)] @function) - ]))) - -(ntlappexpr - (ntlappexpr - field: (atom - (qidentifier - [ - (qvarid) @variable - (qidop) @variable - (identifier - [(varid) (idop)] @variable) - ]))) - "[") - [ "initially" "finally" diff --git a/runtime/queries/koka/indents.scm b/runtime/queries/koka/indents.scm index 0a47bcbbd..6045373db 100644 --- a/runtime/queries/koka/indents.scm +++ b/runtime/queries/koka/indents.scm @@ -1,6 +1,5 @@ [ (appexpr ["[" "("]) ; Applications. - (ntlappexpr ["[" "("]) (atom ["[" "("]) ; Lists and tuples. (program (moduledecl "{")) ; Braced module declarations. (funbody) From 1ba5763a0cee586fc1c56eeb02fb508081c6e9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Fern=C3=A1ndez=20Serrata?= <76864299+Rudxain@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:44:39 -0400 Subject: [PATCH 005/126] recognize more files and shebangs (#10120) * recognize `.node_repl_history` as JS * recognize `bun` shebang also add comments explaining Deno & Bun history files * recognize `.python_history` file and `python3` shebang * recognize more shells * rm Py3 shebang Co-authored-by: ath3 <45574139+ath3@users.noreply.github.com> * rm non-standard deno/bun hist recognition --------- Co-authored-by: ath3 <45574139+ath3@users.noreply.github.com> --- languages.toml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/languages.toml b/languages.toml index 49185cc50..15e42e9d9 100644 --- a/languages.toml +++ b/languages.toml @@ -670,7 +670,7 @@ name = "javascript" scope = "source.js" injection-regex = "(js|javascript)" language-id = "javascript" -file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", { glob = "jakefile" }] +file-types = ["js", "mjs", "cjs", "rules", "es6", "pac", { glob = ".node_repl_history" }, { glob = "jakefile" }] shebangs = ["node"] comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } @@ -709,9 +709,9 @@ grammar = "javascript" name = "typescript" scope = "source.ts" injection-regex = "(ts|typescript)" -file-types = ["ts", "mts", "cts"] language-id = "typescript" -shebangs = ["deno", "ts-node"] +file-types = ["ts", "mts", "cts"] +shebangs = ["deno", "bun", "ts-node"] comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } language-servers = [ "typescript-language-server" ] @@ -782,7 +782,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53 name = "python" scope = "source.python" injection-regex = "python" -file-types = ["py", "pyi", "py3", "pyw", "ptl", "rpy", "cpy", "ipy", "pyt", { glob = ".pythonstartup" }, { glob = ".pythonrc" }, { glob = "SConstruct" }, { glob = "SConscript" }] +file-types = ["py", "pyi", "py3", "pyw", "ptl", "rpy", "cpy", "ipy", "pyt", { glob = ".python_history" }, { glob = ".pythonstartup" }, { glob = ".pythonrc" }, { glob = "SConstruct" }, { glob = "SConscript" }] shebangs = ["python"] roots = ["pyproject.toml", "setup.py", "poetry.lock", "pyrightconfig.json"] comment-token = "#" @@ -884,6 +884,10 @@ injection-regex = "(shell|bash|zsh|sh)" file-types = [ "sh", "bash", + "ash", + "dash", + "ksh", + "mksh", "zsh", "zshenv", "zlogin", @@ -895,7 +899,6 @@ file-types = [ "bazelrc", "Renviron", "zsh-theme", - "ksh", "cshrc", "tcshrc", "bashrc_Apple_Terminal", From d3bfa3e063b7862e691bff099c61a7fe5cb48181 Mon Sep 17 00:00:00 2001 From: Eduardo Farinati Date: Fri, 5 Apr 2024 23:17:22 -0300 Subject: [PATCH 006/126] Fix crash on lsp text edits with invalid ranges (#9649) --- helix-lsp/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index c58d967b6..f1ffcdf88 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -539,6 +539,16 @@ pub fn generate_transaction_from_edits( } else { return (0, 0, None); }; + + if start > end { + log::error!( + "Invalid LSP text edit start {:?} > end {:?}, discarding", + start, + end + ); + return (0, 0, None); + } + (start, end, replacement) }), ) From 3f2de213422573ecdf686505ea3464bdf28a4f66 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 6 Apr 2024 00:38:51 -0400 Subject: [PATCH 007/126] Handle partial failure when sending textDocument/didSave (#10168) --- helix-view/src/document.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0c3a75f1b..f26ba8b97 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -961,13 +961,14 @@ impl Future> + 'static + Send for (_, language_server) in language_servers { if !language_server.is_initialized() { - return Ok(event); + continue; } - if let Some(identifier) = &identifier { - if let Some(notification) = - language_server.text_document_did_save(identifier.clone(), &text) - { - notification.await?; + if let Some(notification) = identifier + .clone() + .and_then(|id| language_server.text_document_did_save(id, &text)) + { + if let Err(err) = notification.await { + log::error!("Failed to send textDocument/didSave: {err}"); } } } From 97afd67fca0b505600e5fbba7ca59f06a4eec3ff Mon Sep 17 00:00:00 2001 From: Slug <9619abgoni@gmail.com> Date: Sat, 6 Apr 2024 16:49:14 +0900 Subject: [PATCH 008/126] Add jump-label style for dark high contrast (#10102) --- runtime/themes/dark_high_contrast.toml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/runtime/themes/dark_high_contrast.toml b/runtime/themes/dark_high_contrast.toml index 6da2389a6..9e2328450 100644 --- a/runtime/themes/dark_high_contrast.toml +++ b/runtime/themes/dark_high_contrast.toml @@ -3,11 +3,14 @@ # Interface "ui.background" = { bg = "black" } "ui.window" = { fg = "aqua" } -"special" = "orange" # file picker fuzzy match +"special" = "orange" # file picker fuzzy match "ui.background.separator" = { fg = "white" } "ui.text" = "white" "ui.text.focus" = { modifiers = ["reversed"] } # file picker selected +"ui.virtual.jump-label" = { fg = "deep_red", modifiers = [ + "bold", +], underline = { color = "deep_red", style = "line" } } "ui.virtual" = "gray" "ui.virtual.whitespace" = "gray" "ui.virtual.ruler" = { fg = "white", bg = "gray" } @@ -56,14 +59,14 @@ "diagnostic.error" = { underline = { color = "red", style = "curl" } } "diagnostic.unnecessary" = { modifiers = ["dim"] } "diagnostic.deprecated" = { modifiers = ["crossed_out"] } -"info" = "white" +"info" = "white" "hint" = "yellow" "warning" = "orange" "error" = "red" "debug" = "red" "diff.plus" = "green" -"diff.delta" ="blue" +"diff.delta" = "blue" "diff.minus" = "pink" # Syntax high light @@ -87,7 +90,9 @@ "label" = "blue" # Markup -"markup.heading" = { fg = "yellow", modifiers = ["bold"], underline = { color = "yellow", style = "double_line"} } +"markup.heading" = { fg = "yellow", modifiers = [ + "bold", +], underline = { color = "yellow", style = "double_line" } } "markup.list" = "pink" "markup.bold" = { fg = "emerald_green", modifiers = ["bold"] } "markup.italic" = { fg = "blue", modifiers = ["italic"] } @@ -107,6 +112,7 @@ dark_blue = "#0d1a2d" aqua = "#6fc3df" purple = "#c586c0" red = "#b65f5f" +deep_red = "#fd0004" pink = "#ff5c8d" orange = "#f38518" brown = "#ce9178" From e69292e5eb7b0f727fefa19cffec910718af31b3 Mon Sep 17 00:00:00 2001 From: TornaxO7 Date: Sat, 6 Apr 2024 23:27:29 +0200 Subject: [PATCH 009/126] Improve `goto_file_impl` (#9065) --- helix-term/src/commands.rs | 67 ++++++++++++++++++++++--------- helix-term/tests/test/commands.rs | 22 ++++++++++ 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7f43fa9ce..99e7608fc 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4,7 +4,10 @@ pub use dap::*; use helix_event::status; -use helix_stdx::rope::{self, RopeSliceExt}; +use helix_stdx::{ + path::expand_tilde, + rope::{self, RopeSliceExt}, +}; use helix_vcs::{FileChange, Hunk}; pub use lsp::*; use tui::{ @@ -1196,25 +1199,51 @@ fn goto_file_impl(cx: &mut Context, action: Action) { let primary = selections.primary(); // Checks whether there is only one selection with a width of 1 if selections.len() == 1 && primary.len() == 1 { - let count = cx.count(); - let text_slice = text.slice(..); - // In this case it selects the WORD under the cursor - let current_word = textobject::textobject_word( - text_slice, - primary, - textobject::TextObject::Inside, - count, - true, - ); - // Trims some surrounding chars so that the actual file is opened. - let surrounding_chars: &[_] = &['\'', '"', '(', ')']; paths.clear(); - paths.push( - current_word - .fragment(text_slice) - .trim_matches(surrounding_chars) - .to_string(), - ); + + let is_valid_path_char = |c: &char| { + #[cfg(target_os = "windows")] + let valid_chars = &[ + '@', '/', '\\', '.', '-', '_', '+', '#', '$', '%', '{', '}', '[', ']', ':', '!', + '~', '=', + ]; + #[cfg(not(target_os = "windows"))] + let valid_chars = &['@', '/', '.', '-', '_', '+', '#', '$', '%', '~', '=', ':']; + + valid_chars.contains(c) || c.is_alphabetic() || c.is_numeric() + }; + + let cursor_pos = primary.cursor(text.slice(..)); + let pre_cursor_pos = cursor_pos.saturating_sub(1); + let post_cursor_pos = cursor_pos + 1; + let start_pos = if is_valid_path_char(&text.char(cursor_pos)) { + cursor_pos + } else if is_valid_path_char(&text.char(pre_cursor_pos)) { + pre_cursor_pos + } else { + post_cursor_pos + }; + + let prefix_len = text + .chars_at(start_pos) + .reversed() + .take_while(is_valid_path_char) + .count(); + + let postfix_len = text + .chars_at(start_pos) + .take_while(is_valid_path_char) + .count(); + + let path: Cow = text + .slice((start_pos - prefix_len)..(start_pos + postfix_len)) + .into(); + log::debug!("Goto file path: {}", path); + + match expand_tilde(PathBuf::from(path.as_ref())).to_str() { + Some(path) => paths.push(path.to_string()), + None => cx.editor.set_error("Couldn't get string out of path."), + }; } for sel in paths { diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index aaa442f36..351f07fe9 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -153,6 +153,28 @@ fn match_paths(app: &Application, matches: Vec<&str>) -> usize { ) .await?; + // ';' is behind the path + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one.js';B;gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + // allow numeric values in path + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one123.js'B;gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one123.js"])); + }), + false, + ) + .await?; + Ok(()) } From e663dafcd835ca360c11b78a44744b27044b1cfe Mon Sep 17 00:00:00 2001 From: Pebrianz Date: Mon, 8 Apr 2024 09:32:29 +0700 Subject: [PATCH 010/126] Add angular language server (#10166) --- languages.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/languages.toml b/languages.toml index 15e42e9d9..87778c996 100644 --- a/languages.toml +++ b/languages.toml @@ -8,6 +8,7 @@ use-grammars = { except = [ "hare", "wren", "gemini" ] } als = { command = "als" } ada-language-server = { command = "ada_language_server" } ada-gpr-language-server = {command = "ada_language_server", args = ["--language-gpr"]} +angular = {command = "ngserver", args = ["--stdio", "--tsProbeLocations", ".", "--ngProbeLocations", ".",]} awk-language-server = { command = "awk-language-server" } bash-language-server = { command = "bash-language-server", args = ["start"] } bass = { command = "bass", args = ["--lsp"] } From 0da809c981d2c0ce77fe828b4d66c995cf2ad56b Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Mon, 8 Apr 2024 12:45:20 +1000 Subject: [PATCH 011/126] feat: Add ADL language support (#10029) * feat: Add ADL language support * removed error match & change captures to match https://docs.helix-editor.com/master/themes.html\#syntax-highlighting * fixes to grammar, highlight changes based on PR and grammar fixes --- book/src/generated/lang-support.md | 1 + languages.toml | 18 ++++++++++++++ runtime/queries/adl/highlights.scm | 37 +++++++++++++++++++++++++++++ runtime/queries/adl/indents.scm | 12 ++++++++++ runtime/queries/adl/textobjects.scm | 1 + 5 files changed, 69 insertions(+) create mode 100644 runtime/queries/adl/highlights.scm create mode 100644 runtime/queries/adl/indents.scm create mode 100644 runtime/queries/adl/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 156898d45..4cd67bc5c 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -1,6 +1,7 @@ | Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP | | --- | --- | --- | --- | --- | | ada | ✓ | ✓ | | `ada_language_server`, `ada_language_server` | +| adl | ✓ | ✓ | ✓ | | | agda | ✓ | | | | | astro | ✓ | | | | | awk | ✓ | ✓ | | `awk-language-server` | diff --git a/languages.toml b/languages.toml index 87778c996..b700c3265 100644 --- a/languages.toml +++ b/languages.toml @@ -3479,3 +3479,21 @@ language-servers = ["earthlyls"] [[grammar]] name = "earthfile" source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "2a6ab191f5f962562e495a818aa4e7f45f8a556a" } + +[[language]] +name = "adl" +scope = "source.adl" +injection-regex = "adl" +file-types = ["adl"] +roots = [] +comment-token = "//" +indent = { tab-width = 2, unit = " " } + +[language.auto-pairs] +'"' = '"' +'{' = '}' +'<' = '>' + +[[grammar]] +name = "adl" +source = { git = "https://github.com/adl-lang/tree-sitter-adl", rev = "2787d04beadfbe154d3f2da6e98dc45a1b134bbf" } diff --git a/runtime/queries/adl/highlights.scm b/runtime/queries/adl/highlights.scm new file mode 100644 index 000000000..88e00ad7f --- /dev/null +++ b/runtime/queries/adl/highlights.scm @@ -0,0 +1,37 @@ +; adl + +[ +"module" +"struct" +"union" +"type" +"newtype" +"annotation" +] @keyword + +(adl (scoped_name)) @namespace +(comment) @comment +(doc_comment) @comment.block.documentation +(name) @type + +(fname) @variable.other.member + +(type_expr (scoped_name) @type) + +(type_expr_params (param (scoped_name) @type.parameter)) + +; json +(key) @string.special + +(string) @string + +(number) @constant.numeric + +[ + (null) + (true) + (false) +] @constant.builtin + +(escape_sequence) @constant.character.escape + diff --git a/runtime/queries/adl/indents.scm b/runtime/queries/adl/indents.scm new file mode 100644 index 000000000..1200d4d42 --- /dev/null +++ b/runtime/queries/adl/indents.scm @@ -0,0 +1,12 @@ +[ + (struct) + (union) + + (array) + (object) +] @indent + +; [ +; "}" +; "]" +; ] @outdent diff --git a/runtime/queries/adl/textobjects.scm b/runtime/queries/adl/textobjects.scm new file mode 100644 index 000000000..36e39a2d4 --- /dev/null +++ b/runtime/queries/adl/textobjects.scm @@ -0,0 +1 @@ +(struct (_) @function.inside) @funtion.around From 6f5ea6be587c6a20eff17d015ea0cc88957f1175 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 8 Apr 2024 05:31:23 +0200 Subject: [PATCH 012/126] fix off by one error for completion-replace option (#10279) --- helix-lsp/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index f1ffcdf88..262bc1b94 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -284,7 +284,8 @@ fn find_completion_range(text: RopeSlice, replace_mode: bool, cursor: usize) -> .chars_at(cursor) .skip(1) .take_while(|ch| chars::char_is_word(*ch)) - .count(); + .count() + + 1; } (start, end) } From 1e7c01d75be649c307636c8ad98e5c7de8a02925 Mon Sep 17 00:00:00 2001 From: Yomain <40139584+yo-main@users.noreply.github.com> Date: Mon, 8 Apr 2024 07:47:44 +0200 Subject: [PATCH 013/126] material theme: add diagnostics and other missing stuff (#10290) --- runtime/themes/material_deep_ocean.toml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/runtime/themes/material_deep_ocean.toml b/runtime/themes/material_deep_ocean.toml index 874731769..e6c31e1f8 100644 --- a/runtime/themes/material_deep_ocean.toml +++ b/runtime/themes/material_deep_ocean.toml @@ -85,18 +85,27 @@ "ui.highlight" = { bg = "highlight" } "ui.menu" = { bg = "highlight", fg = "text" } +"ui.menu.selected" = { bg = "blue", fg = "bg" } "ui.help" = { bg = "highlight", fg = "text" } "ui.popup" = { bg = "highlight", fg = "text" } +"ui.virtual.jump-label" = { fg = "purple", modifiers = ["bold"] } + warning = "yellow" error = "error" info = "blue" hint = "purple" -"diagnostic.unnecessary" = { modifiers = ["dim"] } -"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"diagnostic" = { underline = { color = "error", style = "curl" } } +"diagnostic.info" = { underline = { color = "blue", style = "curl" } } +"diagnostic.hint" = { underline = { color = "purple", style = "curl" } } +"diagnostic.error" = { underline = { color = "error", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"]} +"diagnostic.deprecated" = { modifiers = ["crossed_out"]} + [palette] bg = "#0f111a" From d1803954610003499edd7813e79f3845a793d151 Mon Sep 17 00:00:00 2001 From: Bertrand Bousquet Date: Mon, 8 Apr 2024 13:53:10 +0000 Subject: [PATCH 014/126] Varua theme jump label support (#10299) --- runtime/themes/varua.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/varua.toml b/runtime/themes/varua.toml index 1c6b5e863..330503506 100644 --- a/runtime/themes/varua.toml +++ b/runtime/themes/varua.toml @@ -78,6 +78,7 @@ "diagnostic.error" = { underline = { style = "curl", color = "red" } } "diagnostic.unnecessary" = { modifiers = ["dim"] } "diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"ui.virtual.jump-label" = { fg = "red", modifiers = ["bold"] } [palette] bg0 = "#282828" From 92338bc207778c83e27e80e35b6cfc03f7bdad13 Mon Sep 17 00:00:00 2001 From: Diego Date: Mon, 8 Apr 2024 17:20:53 +0200 Subject: [PATCH 015/126] fix mouse right click selection (#10067) --- helix-term/src/ui/editor.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 97a08beb7..dc767e22e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1263,24 +1263,28 @@ fn handle_mouse_event( } MouseEventKind::Up(MouseButton::Right) => { - if let Some((coords, view_id)) = gutter_coords_and_view(cxt.editor, row, column) { + if let Some((pos, view_id)) = gutter_coords_and_view(cxt.editor, row, column) { cxt.editor.focus(view_id); - let (view, doc) = current!(cxt.editor); - if let Some(pos) = - view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true) - { - doc.set_selection(view_id, Selection::point(pos)); - if modifiers == KeyModifiers::ALT { - commands::MappableCommand::dap_edit_log.execute(cxt); - } else { - commands::MappableCommand::dap_edit_condition.execute(cxt); + if let Some((pos, _)) = pos_and_view(cxt.editor, row, column, true) { + doc_mut!(cxt.editor).set_selection(view_id, Selection::point(pos)); + } else { + let (view, doc) = current!(cxt.editor); + + if let Some(pos) = view.pos_at_visual_coords(doc, pos.row as u16, 0, true) { + doc.set_selection(view_id, Selection::point(pos)); + match modifiers { + KeyModifiers::ALT => { + commands::MappableCommand::dap_edit_log.execute(cxt) + } + _ => commands::MappableCommand::dap_edit_condition.execute(cxt), + }; } - - return EventResult::Consumed(None); } - } + cxt.editor.ensure_cursor_in_view(view_id); + return EventResult::Consumed(None); + } EventResult::Ignored(None) } From ea2a4858b7130d4c833eab00bd62b9b83d57d64c Mon Sep 17 00:00:00 2001 From: Hichem Date: Mon, 8 Apr 2024 17:41:03 +0200 Subject: [PATCH 016/126] Fix scrolling to the end within a popup (#10181) when the available height for the popup is low/small, then it is not possible to scroll until the end Signed-off-by: Ben Fekih, Hichem --- helix-term/src/ui/popup.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 4f379b4a1..124b4402f 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -283,16 +283,20 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { (height + self.margin.height()).min(max_height), ); - // re-clamp scroll offset - let max_offset = self.child_size.1.saturating_sub(self.size.1); - self.scroll = self.scroll.min(max_offset as usize); - Some(self.size) } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { let area = self.area(viewport, cx.editor); self.area = area; + + // required_size() calculates the popup size without taking account of self.position + // so we need to correct the popup height to correctly calculate the scroll + self.size.1 = area.height; + + // re-clamp scroll offset + let max_offset = self.child_size.1.saturating_sub(self.size.1); + self.scroll = self.scroll.min(max_offset as usize); cx.scroll = Some(self.scroll); // clear area From be8afe1bfe746c2105b3c5f81909aa8262eed08d Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Mon, 8 Apr 2024 18:15:31 +0200 Subject: [PATCH 017/126] chore: remove unused xtask themelint (#10294) * chore: remove unused xtask themelint * chore(clippy): remove unused themes --- book/src/themes.md | 7 -- xtask/src/main.rs | 11 --- xtask/src/path.rs | 4 - xtask/src/themelint.rs | 200 ----------------------------------------- 4 files changed, 222 deletions(-) delete mode 100644 xtask/src/themelint.rs diff --git a/book/src/themes.md b/book/src/themes.md index 0a49053f5..d982dee9a 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -36,13 +36,6 @@ ### Overview user-submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes). -### Using the linter - -Use the supplied linting tool to check for errors and missing scopes: - -```sh -cargo xtask themelint onedark # replace onedark with -``` ## The details of theme creation diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1421fd1a1..aba5c4499 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -2,7 +2,6 @@ mod helpers; mod path; mod querycheck; -mod themelint; use std::{env, error::Error}; @@ -12,7 +11,6 @@ pub mod tasks { use crate::docgen::{lang_features, typable_commands, write}; use crate::docgen::{LANG_SUPPORT_MD_OUTPUT, TYPABLE_COMMANDS_MD_OUTPUT}; use crate::querycheck::query_check; - use crate::themelint::{lint, lint_all}; use crate::DynError; pub fn docgen() -> Result<(), DynError> { @@ -21,13 +19,6 @@ pub fn docgen() -> Result<(), DynError> { Ok(()) } - pub fn themelint(file: Option) -> Result<(), DynError> { - match file { - Some(file) => lint(file), - None => lint_all(), - } - } - pub fn querycheck() -> Result<(), DynError> { query_check() } @@ -39,7 +30,6 @@ pub fn print_help() { Tasks: docgen: Generate files to be included in the mdbook output. - themelint : Report errors for , or all themes if no theme is specified. query-check: Check that tree-sitter queries are valid. " ); @@ -52,7 +42,6 @@ fn main() -> Result<(), DynError> { None => tasks::print_help(), Some(t) => match t.as_str() { "docgen" => tasks::docgen()?, - "themelint" => tasks::themelint(env::args().nth(2))?, "query-check" => tasks::querycheck()?, invalid => return Err(format!("Invalid task name: {}", invalid).into()), }, diff --git a/xtask/src/path.rs b/xtask/src/path.rs index 6f4545c27..62d884f2e 100644 --- a/xtask/src/path.rs +++ b/xtask/src/path.rs @@ -18,7 +18,3 @@ pub fn ts_queries() -> PathBuf { pub fn lang_config() -> PathBuf { project_root().join("languages.toml") } - -pub fn themes() -> PathBuf { - project_root().join("runtime/themes") -} diff --git a/xtask/src/themelint.rs b/xtask/src/themelint.rs deleted file mode 100644 index f7efb7d9a..000000000 --- a/xtask/src/themelint.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::path; -use crate::DynError; -use helix_view::theme::Loader; -use helix_view::theme::Modifier; -use helix_view::Theme; - -struct Rule { - fg: Option<&'static str>, - bg: Option<&'static str>, - check_both: bool, -} - -enum Require { - Existence(Rule), - Difference(&'static str, &'static str), -} - -// Placed in an fn here, so it's the first thing you see -fn get_rules() -> Vec { - vec![ - // Check for ui.selection, which is required - Require::Existence(Rule::has_either("ui.selection")), - Require::Existence(Rule::has_either("ui.selection.primary")), - Require::Difference("ui.selection", "ui.selection.primary"), - // Check for planned readable text - Require::Existence(Rule::has_fg("ui.text")), - Require::Existence(Rule::has_bg("ui.background")), - // Check for complete editor.statusline bare minimum - Require::Existence(Rule::has_both("ui.statusline")), - Require::Existence(Rule::has_both("ui.statusline.inactive")), - // Check for editor.color-modes - Require::Existence(Rule::has_either("ui.statusline.normal")), - Require::Existence(Rule::has_either("ui.statusline.insert")), - Require::Existence(Rule::has_either("ui.statusline.select")), - Require::Difference("ui.statusline.normal", "ui.statusline.insert"), - Require::Difference("ui.statusline.normal", "ui.statusline.select"), - // Check for editor.cursorline - Require::Existence(Rule::has_bg("ui.cursorline.primary")), - // Check for general ui.virtual (such as inlay-hint) - Require::Existence(Rule::has_fg("ui.virtual")), - // Check for editor.whitespace - Require::Existence(Rule::has_fg("ui.virtual.whitespace")), - // Check fir rulers - Require::Existence(Rule::has_either("ui.virtual.indent-guide")), - // Check for editor.rulers - Require::Existence(Rule::has_either("ui.virtual.ruler")), - // Check for menus and prompts - Require::Existence(Rule::has_both("ui.menu")), - Require::Existence(Rule::has_both("ui.help")), - Require::Existence(Rule::has_bg("ui.popup")), - Require::Existence(Rule::has_either("ui.window")), - // Check for visible cursor - Require::Existence(Rule::has_bg("ui.cursor.primary")), - Require::Existence(Rule::has_either("ui.cursor.match")), - ] -} - -impl Rule { - fn has_bg(bg: &'static str) -> Rule { - Rule { - fg: None, - bg: Some(bg), - check_both: true, - } - } - fn has_fg(fg: &'static str) -> Rule { - Rule { - fg: Some(fg), - bg: None, - check_both: true, - } - } - fn has_either(item: &'static str) -> Rule { - Rule { - fg: Some(item), - bg: Some(item), - check_both: false, - } - } - fn has_both(item: &'static str) -> Rule { - Rule { - fg: Some(item), - bg: Some(item), - check_both: true, - } - } - fn found_fg(&self, theme: &Theme) -> bool { - if let Some(fg) = &self.fg { - if theme.get(fg).fg.is_none() && theme.get(fg).add_modifier == Modifier::empty() { - return false; - } - } - true - } - fn found_bg(&self, theme: &Theme) -> bool { - if let Some(bg) = &self.bg { - if theme.get(bg).bg.is_none() && theme.get(bg).add_modifier == Modifier::empty() { - return false; - } - } - true - } - fn rule_name(&self) -> &'static str { - if self.fg.is_some() { - self.fg.unwrap() - } else if self.bg.is_some() { - self.bg.unwrap() - } else { - "LINTER_ERROR_NO_RULE" - } - } - - fn check_difference( - theme: &Theme, - a: &'static str, - b: &'static str, - messages: &mut Vec, - ) { - let theme_a = theme.get(a); - let theme_b = theme.get(b); - if theme_a == theme_b { - messages.push(format!("$THEME: `{}` and `{}` cannot be equal", a, b)); - } - } - - fn check_existence(rule: &Rule, theme: &Theme, messages: &mut Vec) { - let found_fg = rule.found_fg(theme); - let found_bg = rule.found_bg(theme); - - if !rule.check_both && (found_fg || found_bg) { - return; - } - if !found_fg || !found_bg { - let mut missing = vec![]; - if !found_fg { - missing.push("`fg`"); - } - if !found_bg { - missing.push("`bg`"); - } - let entry = if !rule.check_both && !found_fg && !found_bg { - missing.join(" or ") - } else { - missing.join(" and ") - }; - messages.push(format!( - "$THEME: missing {} for `{}`", - entry, - rule.rule_name() - )) - } - } -} - -pub fn lint(file: String) -> Result<(), DynError> { - if file.contains("base16") { - println!("Skipping base16: {}", file); - return Ok(()); - } - let path = path::themes().join(file.clone() + ".toml"); - let theme = std::fs::read_to_string(path).unwrap(); - let theme: Theme = toml::from_str(&theme).expect("Failed to parse theme"); - - let mut messages: Vec = vec![]; - get_rules().iter().for_each(|lint| match lint { - Require::Existence(rule) => Rule::check_existence(rule, &theme, &mut messages), - Require::Difference(a, b) => Rule::check_difference(&theme, a, b, &mut messages), - }); - - if !messages.is_empty() { - messages.iter().for_each(|m| { - let theme = file.clone(); - let message = m.replace("$THEME", theme.as_str()); - println!("{}", message); - }); - Err(format!("{} has issues", file).into()) - } else { - Ok(()) - } -} - -pub fn lint_all() -> Result<(), DynError> { - let files = Loader::read_names(path::themes().as_path()); - let files_count = files.len(); - let ok_files_count = files - .into_iter() - .filter_map(|path| lint(path.replace(".toml", "")).ok()) - .count(); - - if files_count != ok_files_count { - Err(format!( - "{} of {} themes had issues", - files_count - ok_files_count, - files_count - ) - .into()) - } else { - Ok(()) - } -} From c499f9aa5507e2ba721a7203a581ad4f614467bf Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Mon, 8 Apr 2024 11:01:40 -0700 Subject: [PATCH 018/126] feat(themes): add ruler for `adwaita-dark` (#10260) --- runtime/themes/adwaita-dark.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/adwaita-dark.toml b/runtime/themes/adwaita-dark.toml index e8f1a5887..44ae0063b 100644 --- a/runtime/themes/adwaita-dark.toml +++ b/runtime/themes/adwaita-dark.toml @@ -82,6 +82,7 @@ "ui.help" = { bg = "libadwaita_dark_alt" } "ui.text" = "light_4" "ui.virtual" = "dark_1" +"ui.virtual.ruler" = { bg = "libadwaita_popup"} "ui.menu" = { fg = "light_4", bg = "libadwaita_popup" } "ui.menu.selected" = { fg = "light_4", bg = "blue_5" } "ui.menu.scroll" = { fg = "light_7", bg = "dark_3" } From ae85f5ff712db6729032f302494eba0ca5655510 Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Mon, 8 Apr 2024 11:02:02 -0700 Subject: [PATCH 019/126] refactor(themes): removed `ui.highlight` effects from `solarized_dark` (#10261) --- runtime/themes/solarized_dark.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml index 9e23aff25..df8b7bbaf 100644 --- a/runtime/themes/solarized_dark.toml +++ b/runtime/themes/solarized_dark.toml @@ -79,8 +79,6 @@ "ui.text" = { fg = "base1" } # 影响 picker列表选中, 快捷键帮助窗口文本 "ui.text.focus" = { fg = "blue", modifiers = ["bold"]} -# file picker中, 预览的当前选中项 -"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] } # 主光标/selectio "ui.cursor.primary" = { fg = "base03", bg = "base1" } From cf259e37c82f7d8a0c0771bfbf5adee6ab5b3a79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:41:11 +0900 Subject: [PATCH 020/126] build(deps): bump peaceiris/actions-gh-pages from 3 to 4 (#10313) Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4. - [Release notes](https://github.com/peaceiris/actions-gh-pages/releases) - [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md) - [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4) --- updated-dependencies: - dependency-name: peaceiris/actions-gh-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/gh-pages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index ae2e5835e..efad7f913 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -27,14 +27,14 @@ jobs: echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV - name: Deploy stable - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 if: startswith(github.ref, 'refs/tags/') with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./book/book - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./book/book From 55346f32b5af8fe4d7de0966e991b2755436ce1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:41:41 +0900 Subject: [PATCH 021/126] build(deps): bump peaceiris/actions-mdbook from 1 to 2 (#10314) Bumps [peaceiris/actions-mdbook](https://github.com/peaceiris/actions-mdbook) from 1 to 2. - [Release notes](https://github.com/peaceiris/actions-mdbook/releases) - [Changelog](https://github.com/peaceiris/actions-mdbook/blob/main/CHANGELOG.md) - [Commits](https://github.com/peaceiris/actions-mdbook/compare/v1...v2) --- updated-dependencies: - dependency-name: peaceiris/actions-mdbook dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index efad7f913..39dc08982 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 + uses: peaceiris/actions-mdbook@v2 with: mdbook-version: 'latest' # mdbook-version: '0.4.8' From cf99615b433fa4708604460e0b2298cf92833960 Mon Sep 17 00:00:00 2001 From: Yomain <40139584+yo-main@users.noreply.github.com> Date: Tue, 9 Apr 2024 02:44:49 +0200 Subject: [PATCH 022/126] material theme: fix statusline color (#10308) --- runtime/themes/material_deep_ocean.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/themes/material_deep_ocean.toml b/runtime/themes/material_deep_ocean.toml index e6c31e1f8..8b1e3c861 100644 --- a/runtime/themes/material_deep_ocean.toml +++ b/runtime/themes/material_deep_ocean.toml @@ -63,9 +63,9 @@ "ui.statusline" = { bg = "bg", fg = "text" } "ui.statusline.inactive" = { bg = "bg", fg = "disabled" } -"ui.statusline.normal" = { bg = "accent", fg = "text" } -"ui.statusline.insert" = { bg = "green", fg = "text" } -"ui.statusline.select" = { bg = "purple", fg = "text" } +"ui.statusline.normal" = { bg = "blue", fg = "bg" } +"ui.statusline.insert" = { bg = "green", fg = "bg" } +"ui.statusline.select" = { bg = "purple", fg = "bg" } "ui.selection" = { bg = "selection" } From 07cb24abddb210bf0e6a8948aac38a9af6914f53 Mon Sep 17 00:00:00 2001 From: Evgeniy Tatarkin Date: Tue, 9 Apr 2024 17:28:54 +0300 Subject: [PATCH 023/126] Respect lsp definition order for code actions (#9590) --- book/src/languages.md | 2 ++ helix-term/src/commands/lsp.rs | 8 ++++---- helix-term/src/ui/completion.rs | 5 ----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/book/src/languages.md b/book/src/languages.md index dd93fec53..33ecbb92f 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -150,6 +150,8 @@ ### Configuring Language Servers for a language Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default. +The definition order of language servers affects the order in the results list of code action menu. + In case multiple language servers are specified in the `language-servers` attribute of a `language`, it's often useful to only enable/disable certain language-server features for these language servers. diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 63d1608f9..6a5ceae6f 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,4 +1,4 @@ -use futures_util::{stream::FuturesUnordered, FutureExt}; +use futures_util::{stream::FuturesOrdered, FutureExt}; use helix_lsp::{ block_on, lsp::{ @@ -341,7 +341,7 @@ fn nested_to_flat( let mut seen_language_servers = HashSet::new(); - let mut futures: FuturesUnordered<_> = doc + let mut futures: FuturesOrdered<_> = doc .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) .map(|language_server| { @@ -416,7 +416,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { let get_symbols = move |pattern: String, editor: &mut Editor| { let doc = doc!(editor); let mut seen_language_servers = HashSet::new(); - let mut futures: FuturesUnordered<_> = doc + let mut futures: FuturesOrdered<_> = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) .map(|language_server| { @@ -574,7 +574,7 @@ pub fn code_action(cx: &mut Context) { let mut seen_language_servers = HashSet::new(); - let mut futures: FuturesUnordered<_> = doc + let mut futures: FuturesOrdered<_> = doc .language_servers_with_feature(LanguageServerFeature::CodeAction) .filter(|ls| seen_language_servers.insert(ls.id())) // TODO this should probably already been filtered in something like "language_servers_with_feature" diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 6cbb5b109..735bc956a 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -285,11 +285,6 @@ macro_rules! language_server { let language_server = language_server!(item); let offset_encoding = language_server.offset_encoding(); - let language_server = editor - .language_servers - .get_by_id(item.language_server_id) - .unwrap(); - // resolve item if not yet resolved if !item.resolved { if let Some(resolved) = From cf9b88f9bda6d9ab3a32754b86be705c62662021 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 7 Apr 2024 14:15:29 -0400 Subject: [PATCH 024/126] add Range::{from_node,into_byte_range} --- helix-core/src/selection.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 579499de5..652612872 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -14,6 +14,7 @@ use helix_stdx::rope::{self, RopeSliceExt}; use smallvec::{smallvec, SmallVec}; use std::borrow::Cow; +use tree_sitter::Node; /// A single selection range. /// @@ -73,6 +74,12 @@ pub fn point(head: usize) -> Self { Self::new(head, head) } + pub fn from_node(node: Node, text: RopeSlice, direction: Direction) -> Self { + let from = text.byte_to_char(node.start_byte()); + let to = text.byte_to_char(node.end_byte()); + Range::new(from, to).with_direction(direction) + } + /// Start of the range. #[inline] #[must_use] @@ -376,6 +383,12 @@ pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool { let second = graphemes.next(); first.is_some() && second.is_none() } + + /// Converts this char range into an in order byte range, discarding + /// direction. + pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) { + (text.char_to_byte(self.from()), text.char_to_byte(self.to())) + } } impl From<(usize, usize)> for Range { @@ -783,7 +796,9 @@ pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection { let mut start = sel_start; for line in sel.slice(text).lines() { - let Some(line_ending) = get_line_ending(&line) else { break }; + let Some(line_ending) = get_line_ending(&line) else { + break; + }; let line_end = start + line.len_chars(); // TODO: retain range direction result.push(Range::new(start, line_end - line_ending.len_chars())); From 87c41617322896d5b19d13f019dcbb4ed89414b5 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Thu, 22 Jun 2023 00:21:02 -0400 Subject: [PATCH 025/126] feat(command): select_all_siblings --- helix-core/src/object.rs | 62 ++++++++- helix-term/src/commands.rs | 21 ++- helix-term/src/keymap/default.rs | 1 + helix-term/tests/test/commands/movement.rs | 151 +++++++++++++++++++++ 4 files changed, 232 insertions(+), 3 deletions(-) diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 0df105f1a..9593b882f 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,4 +1,5 @@ -use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; +use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; +use tree_sitter::{Node, Tree}; pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { let cursor = &mut syntax.walk(); @@ -40,6 +41,65 @@ pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selectio }) } +fn find_parent_with_more_children(mut node: Node) -> Option { + while let Some(parent) = node.parent() { + if parent.child_count() > 1 { + return Some(parent); + } + + node = parent; + } + + None +} + +pub fn select_all_siblings(tree: &Tree, text: RopeSlice, selection: Selection) -> Selection { + let root_node = &tree.root_node(); + + selection.transform_iter(|range| { + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + + root_node + .descendant_for_byte_range(from, to) + .and_then(find_parent_with_more_children) + .map(|parent| select_children(parent, text, range.direction())) + .unwrap_or_else(|| vec![range].into_iter()) + }) +} + +fn select_children( + node: Node, + text: RopeSlice, + direction: Direction, +) -> as std::iter::IntoIterator>::IntoIter { + let mut cursor = node.walk(); + + node.named_children(&mut cursor) + .map(|child| { + let from = text.byte_to_char(child.start_byte()); + let to = text.byte_to_char(child.end_byte()); + + if direction == Direction::Backward { + Range::new(to, from) + } else { + Range::new(from, to) + } + }) + .collect::>() + .into_iter() +} + +fn find_sibling_recursive(node: Node, sibling_fn: F) -> Option +where + F: Fn(Node) -> Option, +{ + sibling_fn(node).or_else(|| { + node.parent() + .and_then(|node| find_sibling_recursive(node, sibling_fn)) + }) +} + pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { select_node_impl(syntax, text, selection, |cursor| { while !cursor.goto_prev_sibling() { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 99e7608fc..7618fd0ab 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -438,8 +438,9 @@ pub fn doc(&self) -> &str { reverse_selection_contents, "Reverse selections contents", expand_selection, "Expand selection to parent syntax node", shrink_selection, "Shrink selection to previously expanded syntax node", - select_next_sibling, "Select next sibling in syntax tree", - select_prev_sibling, "Select previous sibling in syntax tree", + select_next_sibling, "Select next sibling in the syntax tree", + select_prev_sibling, "Select previous sibling the in syntax tree", + select_all_siblings, "Select all siblings in the syntax tree", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save current selection to jumplist", @@ -4974,6 +4975,22 @@ pub fn extend_parent_node_start(cx: &mut Context) { move_node_bound_impl(cx, Direction::Backward, Movement::Extend) } +fn select_all_siblings(cx: &mut Context) { + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); + + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let current_selection = doc.selection(view.id); + let selection = + object::select_all_siblings(syntax.tree(), text, current_selection.clone()); + doc.set_selection(view.id, selection); + } + }; + + cx.editor.apply_motion(motion); +} + fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); let is_select = cx.editor.mode == Mode::Select; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 498a9a3e7..90088e991 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -91,6 +91,7 @@ pub fn default() -> HashMap { "A-n" | "A-right" => select_next_sibling, "A-e" => move_parent_node_end, "A-b" => move_parent_node_start, + "A-a" => select_all_siblings, "%" => select_all, "x" => extend_line_below, diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 34c9d23b2..f263fbac4 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -450,3 +450,154 @@ fn foo() { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn select_all_siblings() -> anyhow::Result<()> { + let tests = vec![ + // basic tests + ( + indoc! {r##" + let foo = bar(#[a|]#, b, c); + "##}, + "", + indoc! {r##" + let foo = bar(#[a|]#, #(b|)#, #(c|)#); + "##}, + ), + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + 3, + 4, + 5, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // direction is preserved + ( + indoc! {r##" + let a = [ + #[|1]#, + 2, + 3, + 4, + 5, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + "##}, + ), + // can't pick any more siblings - selection stays the same + ( + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}, + ), + // each cursor does the sibling select independently + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + 3, + 4, + 5, + ]; + + let b = [ + #("one"|)#, + "two", + "three", + "four", + "five", + ]; + "##}, + "", + indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + + let b = [ + #("one"|)#, + #("two"|)#, + #("three"|)#, + #("four"|)#, + #("five"|)#, + ]; + "##}, + ), + // conflicting sibling selections get normalized. Here, the primary + // selection would choose every list item, but because the secondary + // range covers more than one item, the descendent is the entire list, + // which means the sibling is the assignment. The list item ranges just + // get normalized out since the list itself becomes selected. + ( + indoc! {r##" + let a = [ + #[1|]#, + 2, + #(3, + 4|)#, + 5, + ]; + "##}, + "", + indoc! {r##" + let #(a|)# = #[[ + 1, + 2, + 3, + 4, + 5, + ]|]#; + "##}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} From fa67c5c474e270664c652b87dcab4af7c79aeb59 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Thu, 22 Jun 2023 21:33:40 -0400 Subject: [PATCH 026/126] feat(command): select_all_children --- helix-core/src/object.rs | 30 ++++- helix-term/src/commands.rs | 20 +++- helix-term/src/keymap/default.rs | 1 + helix-term/tests/test/commands/movement.rs | 125 +++++++++++++++++++++ 4 files changed, 170 insertions(+), 6 deletions(-) diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 9593b882f..ff810489d 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -63,7 +63,21 @@ pub fn select_all_siblings(tree: &Tree, text: RopeSlice, selection: Selection) - root_node .descendant_for_byte_range(from, to) .and_then(find_parent_with_more_children) - .map(|parent| select_children(parent, text, range.direction())) + .and_then(|parent| select_children(parent, text, range.direction())) + .unwrap_or_else(|| vec![range].into_iter()) + }) +} + +pub fn select_all_children(tree: &Tree, text: RopeSlice, selection: Selection) -> Selection { + let root_node = &tree.root_node(); + + selection.transform_iter(|range| { + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + + root_node + .descendant_for_byte_range(from, to) + .and_then(|parent| select_children(parent, text, range.direction())) .unwrap_or_else(|| vec![range].into_iter()) }) } @@ -72,10 +86,11 @@ fn select_children( node: Node, text: RopeSlice, direction: Direction, -) -> as std::iter::IntoIterator>::IntoIter { +) -> Option< as std::iter::IntoIterator>::IntoIter> { let mut cursor = node.walk(); - node.named_children(&mut cursor) + let children = node + .named_children(&mut cursor) .map(|child| { let from = text.byte_to_char(child.start_byte()); let to = text.byte_to_char(child.end_byte()); @@ -86,8 +101,13 @@ fn select_children( Range::new(from, to) } }) - .collect::>() - .into_iter() + .collect::>(); + + if !children.is_empty() { + Some(children.into_iter()) + } else { + None + } } fn find_sibling_recursive(node: Node, sibling_fn: F) -> Option diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7618fd0ab..9b203092c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -440,7 +440,8 @@ pub fn doc(&self) -> &str { shrink_selection, "Shrink selection to previously expanded syntax node", select_next_sibling, "Select next sibling in the syntax tree", select_prev_sibling, "Select previous sibling the in syntax tree", - select_all_siblings, "Select all siblings in the syntax tree", + select_all_siblings, "Select all siblings of the current node", + select_all_children, "Select all children of the current node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save current selection to jumplist", @@ -4991,6 +4992,23 @@ fn select_all_siblings(cx: &mut Context) { cx.editor.apply_motion(motion); } +fn select_all_children(cx: &mut Context) { + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); + + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let current_selection = doc.selection(view.id); + let selection = + object::select_all_children(syntax.tree(), text, current_selection.clone()); + doc.set_selection(view.id, selection); + } + }; + + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); let is_select = cx.editor.mode == Mode::Select; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 90088e991..5a3e8eed4 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -87,6 +87,7 @@ pub fn default() -> HashMap { "A-;" => flip_selections, "A-o" | "A-up" => expand_selection, "A-i" | "A-down" => shrink_selection, + "A-I" | "A-S-down" => select_all_children, "A-p" | "A-left" => select_prev_sibling, "A-n" | "A-right" => select_next_sibling, "A-e" => move_parent_node_end, diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index f263fbac4..84806d5f8 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -601,3 +601,128 @@ async fn select_all_siblings() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn select_all_children() -> anyhow::Result<()> { + let tests = vec![ + // basic tests + ( + helpers::platform_line(indoc! {r##" + let foo = bar#[(a, b, c)|]#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let foo = bar(#[a|]#, #(b|)#, #(c|)#); + "##}), + ), + ( + helpers::platform_line(indoc! {r##" + let a = #[[ + 1, + 2, + 3, + 4, + 5, + ]|]#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}), + ), + // direction is preserved + ( + helpers::platform_line(indoc! {r##" + let a = #[|[ + 1, + 2, + 3, + 4, + 5, + ]]#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + "##}), + ), + // can't pick any more children - selection stays the same + ( + helpers::platform_line(indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[1|]#, + #(2|)#, + #(3|)#, + #(4|)#, + #(5|)#, + ]; + "##}), + ), + // each cursor does the sibling select independently + ( + helpers::platform_line(indoc! {r##" + let a = #[|[ + 1, + 2, + 3, + 4, + 5, + ]]#; + + let b = #([ + "one", + "two", + "three", + "four", + "five", + ]|)#; + "##}), + "", + helpers::platform_line(indoc! {r##" + let a = [ + #[|1]#, + #(|2)#, + #(|3)#, + #(|4)#, + #(|5)#, + ]; + + let b = [ + #("one"|)#, + #("two"|)#, + #("three"|)#, + #("four"|)#, + #("five"|)#, + ]; + "##}), + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} From c99c333337cf4c1269749bf29aff48c199e27124 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 25 Jun 2023 13:50:37 -0400 Subject: [PATCH 027/126] Use new in-crate TreeCursor --- helix-core/src/object.rs | 88 ++++----------- helix-core/src/syntax/tree_cursor.rs | 124 +++++++++++++++++++-- helix-term/src/commands.rs | 53 ++++----- helix-term/tests/test/commands/movement.rs | 40 +++---- 4 files changed, 186 insertions(+), 119 deletions(-) diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index ff810489d..28629235f 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,5 +1,4 @@ -use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; -use tree_sitter::{Node, Tree}; +use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { let cursor = &mut syntax.walk(); @@ -41,85 +40,46 @@ pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selectio }) } -fn find_parent_with_more_children(mut node: Node) -> Option { - while let Some(parent) = node.parent() { - if parent.child_count() > 1 { - return Some(parent); +pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + selection.transform_iter(|range| { + let mut cursor = syntax.walk(); + let (from, to) = range.into_byte_range(text); + cursor.reset_to_byte_range(from, to); + + if !cursor.goto_parent_with(|parent| parent.child_count() > 1) { + return vec![range].into_iter(); } - node = parent; - } - - None -} - -pub fn select_all_siblings(tree: &Tree, text: RopeSlice, selection: Selection) -> Selection { - let root_node = &tree.root_node(); - - selection.transform_iter(|range| { - let from = text.char_to_byte(range.from()); - let to = text.char_to_byte(range.to()); - - root_node - .descendant_for_byte_range(from, to) - .and_then(find_parent_with_more_children) - .and_then(|parent| select_children(parent, text, range.direction())) - .unwrap_or_else(|| vec![range].into_iter()) + select_children(&mut cursor, text, range).into_iter() }) } -pub fn select_all_children(tree: &Tree, text: RopeSlice, selection: Selection) -> Selection { - let root_node = &tree.root_node(); - +pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { selection.transform_iter(|range| { - let from = text.char_to_byte(range.from()); - let to = text.char_to_byte(range.to()); - - root_node - .descendant_for_byte_range(from, to) - .and_then(|parent| select_children(parent, text, range.direction())) - .unwrap_or_else(|| vec![range].into_iter()) + let mut cursor = syntax.walk(); + let (from, to) = range.into_byte_range(text); + cursor.reset_to_byte_range(from, to); + select_children(&mut cursor, text, range).into_iter() }) } -fn select_children( - node: Node, +fn select_children<'n>( + cursor: &'n mut TreeCursor<'n>, text: RopeSlice, - direction: Direction, -) -> Option< as std::iter::IntoIterator>::IntoIter> { - let mut cursor = node.walk(); - - let children = node - .named_children(&mut cursor) - .map(|child| { - let from = text.byte_to_char(child.start_byte()); - let to = text.byte_to_char(child.end_byte()); - - if direction == Direction::Backward { - Range::new(to, from) - } else { - Range::new(from, to) - } - }) + range: Range, +) -> Vec { + let children = cursor + .named_children() + .map(|child| Range::from_node(child, text, range.direction())) .collect::>(); if !children.is_empty() { - Some(children.into_iter()) + children } else { - None + vec![range] } } -fn find_sibling_recursive(node: Node, sibling_fn: F) -> Option -where - F: Fn(Node) -> Option, -{ - sibling_fn(node).or_else(|| { - node.parent() - .and_then(|node| find_sibling_recursive(node, sibling_fn)) - }) -} - pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { select_node_impl(syntax, text, selection, |cursor| { while !cursor.goto_prev_sibling() { diff --git a/helix-core/src/syntax/tree_cursor.rs b/helix-core/src/syntax/tree_cursor.rs index d9d140c9f..692d5890a 100644 --- a/helix-core/src/syntax/tree_cursor.rs +++ b/helix-core/src/syntax/tree_cursor.rs @@ -90,6 +90,19 @@ pub fn goto_parent(&mut self) -> bool { true } + pub fn goto_parent_with

(&mut self, predicate: P) -> bool + where + P: Fn(&Node) -> bool, + { + while self.goto_parent() { + if predicate(&self.node()) { + return true; + } + } + + false + } + /// Finds the injection layer that has exactly the same range as the given `range`. fn layer_id_of_byte_range(&self, search_range: Range) -> Option { let start_idx = self @@ -102,7 +115,7 @@ fn layer_id_of_byte_range(&self, search_range: Range) -> Option .find_map(|range| (range.start == search_range.start).then_some(range.layer_id)) } - pub fn goto_first_child(&mut self) -> bool { + fn goto_first_child_impl(&mut self, named: bool) -> bool { // Check if the current node's range is an exact injection layer range. if let Some(layer_id) = self .layer_id_of_byte_range(self.node().byte_range()) @@ -111,8 +124,16 @@ pub fn goto_first_child(&mut self) -> bool { // Switch to the child layer. self.current = layer_id; self.cursor = self.layers[self.current].tree().root_node(); - true - } else if let Some(child) = self.cursor.child(0) { + return true; + } + + let child = if named { + self.cursor.named_child(0) + } else { + self.cursor.child(0) + }; + + if let Some(child) = child { // Otherwise descend in the current tree. self.cursor = child; true @@ -121,8 +142,45 @@ pub fn goto_first_child(&mut self) -> bool { } } + pub fn goto_first_child(&mut self) -> bool { + self.goto_first_child_impl(false) + } + + pub fn goto_first_named_child(&mut self) -> bool { + self.goto_first_child_impl(true) + } + + fn goto_next_sibling_impl(&mut self, named: bool) -> bool { + let sibling = if named { + self.cursor.next_named_sibling() + } else { + self.cursor.next_sibling() + }; + + if let Some(sibling) = sibling { + self.cursor = sibling; + true + } else { + false + } + } + pub fn goto_next_sibling(&mut self) -> bool { - if let Some(sibling) = self.cursor.next_sibling() { + self.goto_next_sibling_impl(false) + } + + pub fn goto_next_named_sibling(&mut self) -> bool { + self.goto_next_sibling_impl(true) + } + + fn goto_prev_sibling_impl(&mut self, named: bool) -> bool { + let sibling = if named { + self.cursor.prev_named_sibling() + } else { + self.cursor.prev_sibling() + }; + + if let Some(sibling) = sibling { self.cursor = sibling; true } else { @@ -131,12 +189,11 @@ pub fn goto_next_sibling(&mut self) -> bool { } pub fn goto_prev_sibling(&mut self) -> bool { - if let Some(sibling) = self.cursor.prev_sibling() { - self.cursor = sibling; - true - } else { - false - } + self.goto_prev_sibling_impl(false) + } + + pub fn goto_prev_named_sibling(&mut self) -> bool { + self.goto_prev_sibling_impl(true) } /// Finds the injection layer that contains the given start-end range. @@ -157,4 +214,51 @@ pub fn reset_to_byte_range(&mut self, start: usize, end: usize) { let root = self.layers[self.current].tree().root_node(); self.cursor = root.descendant_for_byte_range(start, end).unwrap_or(root); } + + /// Returns an iterator over the children of the node the TreeCursor is on + /// at the time this is called. + pub fn children(&'a mut self) -> ChildIter { + let parent = self.node(); + + ChildIter { + cursor: self, + parent, + named: false, + } + } + + /// Returns an iterator over the named children of the node the TreeCursor is on + /// at the time this is called. + pub fn named_children(&'a mut self) -> ChildIter { + let parent = self.node(); + + ChildIter { + cursor: self, + parent, + named: true, + } + } +} + +pub struct ChildIter<'n> { + cursor: &'n mut TreeCursor<'n>, + parent: Node<'n>, + named: bool, +} + +impl<'n> Iterator for ChildIter<'n> { + type Item = Node<'n>; + + fn next(&mut self) -> Option { + // first iteration, just visit the first child + if self.cursor.node() == self.parent { + self.cursor + .goto_first_child_impl(self.named) + .then(|| self.cursor.node()) + } else { + self.cursor + .goto_next_sibling_impl(self.named) + .then(|| self.cursor.node()) + } + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9b203092c..6cda93dcb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -38,7 +38,7 @@ textobject, unicode::width::UnicodeWidthChar, visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes, - RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction, + RopeReader, RopeSlice, Selection, SmallVec, Syntax, Tendril, Transaction, }; use helix_view::{ document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, @@ -4976,17 +4976,23 @@ pub fn extend_parent_node_start(cx: &mut Context) { move_node_bound_impl(cx, Direction::Backward, Movement::Extend) } +fn select_all_impl(editor: &mut Editor, select_fn: F) +where + F: Fn(&Syntax, RopeSlice, Selection) -> Selection, +{ + let (view, doc) = current!(editor); + + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let current_selection = doc.selection(view.id); + let selection = select_fn(syntax, text, current_selection.clone()); + doc.set_selection(view.id, selection); + } +} + fn select_all_siblings(cx: &mut Context) { let motion = |editor: &mut Editor| { - let (view, doc) = current!(editor); - - if let Some(syntax) = doc.syntax() { - let text = doc.text().slice(..); - let current_selection = doc.selection(view.id); - let selection = - object::select_all_siblings(syntax.tree(), text, current_selection.clone()); - doc.set_selection(view.id, selection); - } + select_all_impl(editor, object::select_all_siblings); }; cx.editor.apply_motion(motion); @@ -4994,19 +5000,10 @@ fn select_all_siblings(cx: &mut Context) { fn select_all_children(cx: &mut Context) { let motion = |editor: &mut Editor| { - let (view, doc) = current!(editor); - - if let Some(syntax) = doc.syntax() { - let text = doc.text().slice(..); - let current_selection = doc.selection(view.id); - let selection = - object::select_all_children(syntax.tree(), text, current_selection.clone()); - doc.set_selection(view.id, selection); - } + select_all_impl(editor, object::select_all_children); }; - motion(cx.editor); - cx.editor.last_motion = Some(Motion(Box::new(motion))); + cx.editor.apply_motion(motion); } fn match_brackets(cx: &mut Context) { @@ -6040,7 +6037,10 @@ fn jump_to_label(cx: &mut Context, labels: Vec, behaviour: Movement) { let doc = doc.id(); cx.on_next_key(move |cx, event| { let alphabet = &cx.editor.config().jump_label_alphabet; - let Some(i ) = event.char().and_then(|ch| alphabet.iter().position(|&it| it == ch)) else { + let Some(i) = event + .char() + .and_then(|ch| alphabet.iter().position(|&it| it == ch)) + else { doc_mut!(cx.editor, &doc).remove_jump_labels(view); return; }; @@ -6053,7 +6053,10 @@ fn jump_to_label(cx: &mut Context, labels: Vec, behaviour: Movement) { cx.on_next_key(move |cx, event| { doc_mut!(cx.editor, &doc).remove_jump_labels(view); let alphabet = &cx.editor.config().jump_label_alphabet; - let Some(inner ) = event.char().and_then(|ch| alphabet.iter().position(|&it| it == ch)) else { + let Some(inner) = event + .char() + .and_then(|ch| alphabet.iter().position(|&it| it == ch)) + else { return; }; if let Some(mut range) = labels.get(outer + inner).copied() { @@ -6073,8 +6076,8 @@ fn jump_to_label(cx: &mut Context, labels: Vec, behaviour: Movement) { to } }; - Range::new(anchor, range.head) - }else{ + Range::new(anchor, range.head) + } else { range.with_direction(Direction::Forward) }; doc_mut!(cx.editor, &doc).set_selection(view, range.into()); diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 84806d5f8..1f33b3944 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -607,16 +607,16 @@ async fn select_all_children() -> anyhow::Result<()> { let tests = vec![ // basic tests ( - helpers::platform_line(indoc! {r##" + indoc! {r##" let foo = bar#[(a, b, c)|]#; - "##}), + "##}, "", - helpers::platform_line(indoc! {r##" + indoc! {r##" let foo = bar(#[a|]#, #(b|)#, #(c|)#); - "##}), + "##}, ), ( - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = #[[ 1, 2, @@ -624,9 +624,9 @@ async fn select_all_children() -> anyhow::Result<()> { 4, 5, ]|]#; - "##}), + "##}, "", - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = [ #[1|]#, #(2|)#, @@ -634,11 +634,11 @@ async fn select_all_children() -> anyhow::Result<()> { #(4|)#, #(5|)#, ]; - "##}), + "##}, ), // direction is preserved ( - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = #[|[ 1, 2, @@ -646,9 +646,9 @@ async fn select_all_children() -> anyhow::Result<()> { 4, 5, ]]#; - "##}), + "##}, "", - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = [ #[|1]#, #(|2)#, @@ -656,11 +656,11 @@ async fn select_all_children() -> anyhow::Result<()> { #(|4)#, #(|5)#, ]; - "##}), + "##}, ), // can't pick any more children - selection stays the same ( - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = [ #[1|]#, #(2|)#, @@ -668,9 +668,9 @@ async fn select_all_children() -> anyhow::Result<()> { #(4|)#, #(5|)#, ]; - "##}), + "##}, "", - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = [ #[1|]#, #(2|)#, @@ -678,11 +678,11 @@ async fn select_all_children() -> anyhow::Result<()> { #(4|)#, #(5|)#, ]; - "##}), + "##}, ), // each cursor does the sibling select independently ( - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = #[|[ 1, 2, @@ -698,9 +698,9 @@ async fn select_all_children() -> anyhow::Result<()> { "four", "five", ]|)#; - "##}), + "##}, "", - helpers::platform_line(indoc! {r##" + indoc! {r##" let a = [ #[|1]#, #(|2)#, @@ -716,7 +716,7 @@ async fn select_all_children() -> anyhow::Result<()> { #("four"|)#, #("five"|)#, ]; - "##}), + "##}, ), ]; From b8ddb2f114a06a1b10eb72984352ca5b72a5dc85 Mon Sep 17 00:00:00 2001 From: Valentin B <703631+beeb@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:42:51 +0200 Subject: [PATCH 028/126] feat(solidity): add textobject queries for solidity (#10318) * feat: add textobject queries for solidity * feat(solidity): add parameter textobject query for call expressions * feat(solidity): add more textobject queries for parameters * feat(solidity): add yul function textobject query * feat(solidity): add textobject query for emit statement arguments * feat(solidity): add textobject query for revert call arguments * feat(solidity): update tree-sitter grammar and fix typo * docs: update auto-generated docs * fix(solidity): fix identifiers highlight query priority * feat(solidity): add "abstract" to keywords list * feat(solidity): add highlight query for type alias * feat(solidity): add variable builtin highlight queries --- book/src/generated/lang-support.md | 2 +- languages.toml | 2 +- runtime/queries/solidity/highlights.scm | 18 +++++--- runtime/queries/solidity/textobjects.scm | 54 ++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 runtime/queries/solidity/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 4cd67bc5c..fb1c7bd5a 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -173,7 +173,7 @@ | smali | ✓ | | ✓ | | | smithy | ✓ | | | `cs` | | sml | ✓ | | | | -| solidity | ✓ | | | `solc` | +| solidity | ✓ | ✓ | | `solc` | | spicedb | ✓ | | | | | sql | ✓ | | | | | sshclientconfig | ✓ | | | | diff --git a/languages.toml b/languages.toml index b700c3265..0f70a7330 100644 --- a/languages.toml +++ b/languages.toml @@ -1787,7 +1787,7 @@ language-servers = [ "solc" ] [[grammar]] name = "solidity" -source = { git = "https://github.com/JoranHonig/tree-sitter-solidity", rev = "9004b86531cb424bd379424cf7266a4585f2af7d" } +source = { git = "https://github.com/JoranHonig/tree-sitter-solidity", rev = "08338dcee32603383fcef08f36321900bb7a354b" } [[language]] name = "gleam" diff --git a/runtime/queries/solidity/highlights.scm b/runtime/queries/solidity/highlights.scm index 08178c362..ca08d0159 100644 --- a/runtime/queries/solidity/highlights.scm +++ b/runtime/queries/solidity/highlights.scm @@ -1,8 +1,3 @@ -; identifiers -; ----------- -(identifier) @variable -(yul_identifier) @variable - ; Pragma (pragma_directive) @tag (solidity_version_comparison_operator _ @tag) @@ -36,6 +31,7 @@ (type_name) @type (primitive_type) @type (user_defined_type (identifier) @type) +(type_alias (identifier) @type) ; Color payable in payable address conversion as type and not as keyword (payable_conversion_expression "payable" @type) @@ -80,7 +76,7 @@ ; Function parameters (call_struct_argument name: (identifier) @field) -(event_paramater name: (identifier) @variable.parameter) +(event_parameter name: (identifier) @variable.parameter) (parameter name: (identifier) @variable.parameter) ; Yul functions @@ -99,6 +95,7 @@ ; Keywords (meta_type_expression "type" @keyword) [ + "abstract" "pragma" "contract" "interface" @@ -159,7 +156,7 @@ "import" @keyword.control.import (import_directive "as" @keyword.control.import) (import_directive "from" @keyword.control.import) -(event_paramater "indexed" @keyword) ; TODO fix spelling once fixed upstream +(event_parameter "indexed" @keyword) ; Punctuation @@ -217,3 +214,10 @@ "delete" "new" ] @keyword.operator + +; identifiers +; ----------- +((identifier) @variable.builtin + (#match? @variable.builtin "^(this|msg|block|tx)$")) +(identifier) @variable +(yul_identifier) @variable diff --git a/runtime/queries/solidity/textobjects.scm b/runtime/queries/solidity/textobjects.scm new file mode 100644 index 000000000..4e5ffdd12 --- /dev/null +++ b/runtime/queries/solidity/textobjects.scm @@ -0,0 +1,54 @@ +(function_definition + body: (_) @function.inside) @function.around + +(constructor_definition + body: (_) @function.inside) @function.around + +(fallback_receive_definition + body: (_) @function.inside) @function.around + +(yul_function_definition + (yul_block) @function.inside) @function.around + +(function_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(constructor_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(return_type_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(modifier_definition + ((parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(event_definition + ((event_parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(error_declaration + ((error_parameter) @parameter.inside . ","? @parameter.around) @parameter.around) + +(call_argument + ((call_struct_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(call_expression + ((call_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(variable_declaration_tuple + ((variable_declaration) @parameter.inside . ","? @parameter.around) @parameter.around) + +(emit_statement + ((call_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(revert_arguments + ((call_argument) @parameter.inside . ","? @parameter.around) @parameter.around) + +(struct_declaration + body: (_) @class.inside) @class.around + +(enum_declaration + body: (_) @class.inside) @class.around + +(comment) @comment.inside + +(comment)+ @comment.around From c5e257b81cd0fbb700beeabba9c217a63f62a756 Mon Sep 17 00:00:00 2001 From: VKondakoff Date: Tue, 9 Apr 2024 17:43:21 +0300 Subject: [PATCH 029/126] Update nord.toml (#10307) When "nord2" color is used in ui.selection it is almost invisible if cursorline highlighting is enabled. Changing the color to "nord3" fixes the issue. --- runtime/themes/nord.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml index 4916c3e84..b70f67a13 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -114,7 +114,7 @@ "ui.cursorcolumn.primary" = { bg = "nord1" } "ui.cursorline.primary" = { bg = "nord1" } -"ui.selection" = { bg = "nord2" } +"ui.selection" = { bg = "nord3" } "ui.highlight" = { fg = "nord8", bg = "nord2" } # Statusline From f601b7c27872c1de7b3f86908337642bc122a234 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 9 Apr 2024 18:49:30 +0200 Subject: [PATCH 030/126] fix char/byte index mixup in overlay rendering (#10317) --- helix-term/src/ui/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index dc767e22e..2aac3d335 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -350,7 +350,8 @@ pub fn overlay_syntax_highlights( let text = doc.text().slice(..); let row = text.char_to_line(anchor.min(text.len_chars())); - let range = Self::viewport_byte_range(text, row, height); + let mut range = Self::viewport_byte_range(text, row, height); + range = text.byte_to_char(range.start)..text.byte_to_char(range.end); text_annotations.collect_overlay_highlights(range) } From b974716b92baf645a70029551c42c88807174e51 Mon Sep 17 00:00:00 2001 From: blt-r <63462729+blt-r@users.noreply.github.com> Date: Tue, 9 Apr 2024 22:17:52 +0400 Subject: [PATCH 031/126] Hightlight meson.options as meson file (#10323) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 0f70a7330..6e0ad97bd 100644 --- a/languages.toml +++ b/languages.toml @@ -2060,7 +2060,7 @@ source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e4 name = "meson" scope = "source.meson" injection-regex = "meson" -file-types = [{ glob = "meson.build" }, { glob = "meson_options.txt" }] +file-types = [{ glob = "meson.build" }, { glob = "meson.options" }, { glob = "meson_options.txt" }] comment-token = "#" indent = { tab-width = 2, unit = " " } From 73d26d0d97bfc684266b5c8180d00783d6d84eba Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 10 Apr 2024 17:14:08 +0200 Subject: [PATCH 032/126] don't manually grapheme align ts highlights (#10310) --- helix-core/src/graphemes.rs | 17 ---------- helix-stdx/src/rope.rs | 62 +++++++++++++++++++++++++++++++++++ helix-term/src/ui/document.rs | 23 ++++++++++++- helix-term/src/ui/editor.rs | 22 +++---------- 4 files changed, 88 insertions(+), 36 deletions(-) diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index 7cb5cd062..a02d6e4dd 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -278,23 +278,6 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize } } -/// Returns the passed byte index if it's already a grapheme boundary, -/// or the next grapheme boundary byte index if not. -#[must_use] -#[inline] -pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize { - if byte_idx == 0 { - byte_idx - } else { - // TODO: optimize so we're not constructing grapheme cursor twice - if is_grapheme_boundary_byte(slice, byte_idx) { - byte_idx - } else { - next_grapheme_boundary_byte(slice, byte_idx) - } - } -} - /// Returns whether the given char position is a grapheme boundary. #[must_use] pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool { diff --git a/helix-stdx/src/rope.rs b/helix-stdx/src/rope.rs index 7e2549f5a..2695555e3 100644 --- a/helix-stdx/src/rope.rs +++ b/helix-stdx/src/rope.rs @@ -3,6 +3,7 @@ pub use regex_cursor::engines::meta::{Builder as RegexBuilder, Regex}; pub use regex_cursor::regex_automata::util::syntax::Config; use regex_cursor::{Input as RegexInput, RopeyCursor}; +use ropey::str_utils::byte_to_char_idx; use ropey::RopeSlice; pub trait RopeSliceExt<'a>: Sized { @@ -16,6 +17,23 @@ fn regex_input_at_bytes>( fn regex_input_at>(self, char_range: R) -> RegexInput>; fn first_non_whitespace_char(self) -> Option; fn last_non_whitespace_char(self) -> Option; + /// returns the char idx of `byte_idx`, if `byte_idx` is a char boundary + /// this function behaves the same as `byte_to_char` but if `byte_idx` is + /// not a valid char boundary (so within a char) this will return the next + /// char index. + /// + /// # Example + /// + /// ``` + /// # use ropey::RopeSlice; + /// # use helix_stdx::rope::RopeSliceExt; + /// let text = RopeSlice::from("😆"); + /// for i in 1..text.len_bytes() { + /// assert_eq!(text.byte_to_char(i), 0); + /// assert_eq!(text.byte_to_next_char(i), 1); + /// } + /// ``` + fn byte_to_next_char(self, byte_idx: usize) -> usize; } impl<'a> RopeSliceExt<'a> for RopeSlice<'a> { @@ -75,4 +93,48 @@ fn last_non_whitespace_char(self) -> Option { .position(|ch| !ch.is_whitespace()) .map(|pos| self.len_chars() - pos - 1) } + + /// returns the char idx of `byte_idx`, if `byte_idx` is + /// a char boundary this function behaves the same as `byte_to_char` + fn byte_to_next_char(self, mut byte_idx: usize) -> usize { + let (chunk, chunk_byte_off, chunk_char_off, _) = self.chunk_at_byte(byte_idx); + byte_idx -= chunk_byte_off; + let is_char_boundary = + is_utf8_char_boundary(chunk.as_bytes().get(byte_idx).copied().unwrap_or(0)); + chunk_char_off + byte_to_char_idx(chunk, byte_idx) + !is_char_boundary as usize + } +} + +// copied from std +#[inline] +const fn is_utf8_char_boundary(b: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (b as i8) >= -0x40 +} + +#[cfg(test)] +mod tests { + use ropey::RopeSlice; + + use crate::rope::RopeSliceExt; + + #[test] + fn next_char_at_byte() { + for i in 0..=6 { + assert_eq!(RopeSlice::from("foobar").byte_to_next_char(i), i); + } + for char_idx in 0..10 { + let len = "😆".len(); + assert_eq!( + RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len), + char_idx + ); + for i in 1..=len { + assert_eq!( + RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len + i), + char_idx + 1 + ); + } + } + } } diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index b571b83c2..bcbaa3519 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -7,6 +7,7 @@ use helix_core::syntax::HighlightEvent; use helix_core::text_annotations::TextAnnotations; use helix_core::{visual_offset_from_block, Position, RopeSlice}; +use helix_stdx::rope::RopeSliceExt; use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue}; use helix_view::graphics::Rect; use helix_view::theme::Style; @@ -32,14 +33,27 @@ fn render_background(&mut self, renderer: &mut TextRenderer, pos: LinePos) { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum StyleIterKind { + /// base highlights (usually emitted by TS), byte indices (potentially not codepoint aligned) + BaseHighlights, + /// overlay highlights (emitted by custom code from selections), char indices + Overlay, +} + /// A wrapper around a HighlightIterator /// that merges the layered highlights to create the final text style /// and yields the active text style and the char_idx where the active /// style will have to be recomputed. +/// +/// TODO(ropey2): hopefully one day helix and ropey will operate entirely +/// on byte ranges and we can remove this struct StyleIter<'a, H: Iterator> { text_style: Style, active_highlights: Vec, highlight_iter: H, + kind: StyleIterKind, + text: RopeSlice<'a>, theme: &'a Theme, } @@ -54,7 +68,7 @@ fn next(&mut self) -> Option<(Style, usize)> { HighlightEvent::HighlightEnd => { self.active_highlights.pop(); } - HighlightEvent::Source { start, end } => { + HighlightEvent::Source { start, mut end } => { if start == end { continue; } @@ -64,6 +78,9 @@ fn next(&mut self) -> Option<(Style, usize)> { .fold(self.text_style, |acc, span| { acc.patch(self.theme.highlight(span.0)) }); + if self.kind == StyleIterKind::BaseHighlights { + end = self.text.byte_to_next_char(end); + } return Some((style, end)); } } @@ -185,13 +202,17 @@ pub fn render_text<'t>( text_style: renderer.text_style, active_highlights: Vec::with_capacity(64), highlight_iter: syntax_highlight_iter, + kind: StyleIterKind::BaseHighlights, theme, + text, }; let mut overlay_styles = StyleIter { text_style: Style::default(), active_highlights: Vec::with_capacity(64), highlight_iter: overlay_highlight_iter, + kind: StyleIterKind::Overlay, theme, + text, }; let mut last_line_pos = LinePos { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 2aac3d335..7b130a38a 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -12,9 +12,7 @@ use helix_core::{ diagnostic::NumberOrString, - graphemes::{ - ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, - }, + graphemes::{next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, syntax::{self, HighlightEvent}, text_annotations::TextAnnotations, @@ -315,26 +313,14 @@ pub fn doc_syntax_highlights<'doc>( let iter = syntax // TODO: range doesn't actually restrict source, just highlight range .highlight_iter(text.slice(..), Some(range), None) - .map(|event| event.unwrap()) - .map(move |event| match event { - // TODO: use byte slices directly - // convert byte offsets to char offset - HighlightEvent::Source { start, end } => { - let start = - text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start)); - let end = - text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end)); - HighlightEvent::Source { start, end } - } - event => event, - }); + .map(|event| event.unwrap()); Box::new(iter) } None => Box::new( [HighlightEvent::Source { - start: text.byte_to_char(range.start), - end: text.byte_to_char(range.end), + start: range.start, + end: range.end, }] .into_iter(), ), From 34c7eb4bd4d7602e6291a79d860ff9bae97bddd6 Mon Sep 17 00:00:00 2001 From: Josh Robson Chase Date: Wed, 10 Apr 2024 08:36:37 -0700 Subject: [PATCH 033/126] themes/monokai: add inlay-hint style (#10334) Matching comment styling so that it doesn't get confused for actual code. --- runtime/themes/monokai.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index a849ae96d..b0faac154 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -83,6 +83,7 @@ "ui.bufferline.active" = { fg = "active_text", bg = "selection", modifiers = [ "bold", ] } +"ui.virtual.inlay-hint" = { fg = "#88846F" } "ui.text" = { fg = "text" } "ui.text.focus" = { fg = "active_text" } From 4fc0a4dafc3a94cd9afcc61dcd0df0f4eb93502e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 11 Apr 2024 16:16:47 +0900 Subject: [PATCH 034/126] Improve solidity highlighting --- runtime/queries/solidity/highlights.scm | 36 ++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/runtime/queries/solidity/highlights.scm b/runtime/queries/solidity/highlights.scm index ca08d0159..fcc8d3e31 100644 --- a/runtime/queries/solidity/highlights.scm +++ b/runtime/queries/solidity/highlights.scm @@ -1,6 +1,6 @@ ; Pragma -(pragma_directive) @tag -(solidity_version_comparison_operator _ @tag) +(pragma_directive) @keyword.directive +(solidity_version_comparison_operator _ @keyword.directive) ; Literals @@ -20,7 +20,7 @@ [ (true) (false) -] @constant.builtin +] @constant.builtin.boolean (comment) @comment @@ -29,7 +29,12 @@ ; ----------- (type_name) @type -(primitive_type) @type + +[ + (primitive_type) + (number_unit) +] @type.builtin + (user_defined_type (identifier) @type) (type_alias (identifier) @type) @@ -61,6 +66,7 @@ ; Use constructor coloring for special functions (constructor_definition "constructor" @constructor) +(error_declaration "error" @constructor) (fallback_receive_definition "receive" @constructor) (fallback_receive_definition "fallback" @constructor) @@ -96,7 +102,6 @@ (meta_type_expression "type" @keyword) [ "abstract" - "pragma" "contract" "interface" "library" @@ -104,9 +109,9 @@ "struct" "enum" "event" - "using" "assembly" "emit" + "public" "internal" "private" @@ -114,17 +119,22 @@ "pure" "view" "payable" + "modifier" - "memory" - "storage" - "calldata" "var" - "constant" + "let" (virtual) (override_specifier) (yul_leave) ] @keyword +[ + "memory" + "storage" + "calldata" + "constant" +] @keyword.storage.modifier + [ "for" "while" @@ -144,6 +154,7 @@ [ "try" "catch" + "revert" ] @keyword.control.exception [ @@ -154,6 +165,7 @@ "function" @keyword.function "import" @keyword.control.import +"using" @keyword.control.import (import_directive "as" @keyword.control.import) (import_directive "from" @keyword.control.import) (event_parameter "indexed" @keyword) @@ -173,6 +185,9 @@ [ "." "," + ":" + "->" + "=>" ] @punctuation.delimiter @@ -215,6 +230,7 @@ "new" ] @keyword.operator +; TODO: move to top when order swapped ; identifiers ; ----------- ((identifier) @variable.builtin From 009a5498ca14dfc083b3ff0ceec9231f3c917a98 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Thu, 11 Apr 2024 17:17:44 +0200 Subject: [PATCH 035/126] add LDIF support (#10330) * feat(lang): add LDIF support * style: no unnecessary glob * Update runtime/queries/ldif/highlights.scm Co-authored-by: Michael Davis --------- Co-authored-by: Michael Davis --- book/src/generated/lang-support.md | 1 + languages.toml | 11 +++++++++++ runtime/queries/ldif/highlights.scm | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 runtime/queries/ldif/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index fb1c7bd5a..24bf3eece 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -105,6 +105,7 @@ | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | | ld | ✓ | | ✓ | | +| ldif | ✓ | | | | | lean | ✓ | | | `lean` | | ledger | ✓ | | | | | llvm | ✓ | ✓ | ✓ | | diff --git a/languages.toml b/languages.toml index 6e0ad97bd..d73286f8a 100644 --- a/languages.toml +++ b/languages.toml @@ -3497,3 +3497,14 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "adl" source = { git = "https://github.com/adl-lang/tree-sitter-adl", rev = "2787d04beadfbe154d3f2da6e98dc45a1b134bbf" } + +[[language]] +name = "ldif" +scope = "source.ldif" +injection-regex = "ldif" +file-types = ["ldif"] +comment-token = "#" + +[[grammar]] +name = "ldif" +source = { git = "https://github.com/kepet19/tree-sitter-ldif", rev = "0a917207f65ba3e3acfa9cda16142ee39c4c1aaa" } diff --git a/runtime/queries/ldif/highlights.scm b/runtime/queries/ldif/highlights.scm new file mode 100644 index 000000000..882ace9fc --- /dev/null +++ b/runtime/queries/ldif/highlights.scm @@ -0,0 +1,19 @@ +(comment) @comment + +((distinguishedName + (name + (name_componet + (attributeTypeAndValue + (attributeType) @comment + (string) @type.parameter + )))) @comment) + + +(dn_spec) @constant +(changerecord) @constant +(mod_spec) @constant + +(attributeType) @type.parameter +(change_modify) @string + +(value_spec) @keyword From c9ae694aff21a4be47adc69c5465241c551e11b5 Mon Sep 17 00:00:00 2001 From: Arthur <110528300+c0rydoras@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:59:00 +0200 Subject: [PATCH 036/126] fix(languages/helm): recognize _*.tpl as helm (#10344) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index d73286f8a..fe94c597e 100644 --- a/languages.toml +++ b/languages.toml @@ -3418,7 +3418,7 @@ scope = "source.helm" roots = ["Chart.yaml"] comment-token = "#" language-servers = ["helm_ls"] -file-types = [ { glob = "templates/*.yaml" }, { glob = "templates/_helpers.tpl"}, { glob = "templates/NOTES.txt" } ] +file-types = [ { glob = "templates/*.yaml" }, { glob = "templates/_*.tpl"}, { glob = "templates/NOTES.txt" } ] [[language]] name = "glimmer" From 6d363a978d3f4c1973674305f16305f9b8f90707 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Thu, 11 Apr 2024 21:49:16 -0400 Subject: [PATCH 037/126] Read symlink when writing files (#10339) Co-authored-by: Pascal Kuthe --- helix-term/tests/test/commands/write.rs | 75 +++++++++++++++++++++++++ helix-term/tests/test/helpers.rs | 12 ++++ helix-view/src/document.rs | 14 +++-- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index c9280bbd4..350f34aab 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -529,6 +529,81 @@ async fn test_write_all_insert_final_newline_do_not_add_if_unmodified() -> anyho Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_symlink_write() -> anyhow::Result<()> { + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(not(unix))] + use std::os::windows::fs::symlink_file as symlink; + + let dir = tempfile::tempdir()?; + + let mut file = tempfile::NamedTempFile::new_in(&dir)?; + let symlink_path = dir.path().join("linked"); + symlink(file.path(), &symlink_path)?; + + let mut app = helpers::AppBuilder::new() + .with_file(&symlink_path, None) + .build()?; + + test_key_sequence( + &mut app, + Some("ithe gostak distims the doshes:w"), + None, + false, + ) + .await?; + + reload_file(&mut file).unwrap(); + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + + assert_eq!( + LineFeedHandling::Native.apply("the gostak distims the doshes"), + file_content + ); + assert!(symlink_path.is_symlink()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_symlink_write_fail() -> anyhow::Result<()> { + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(not(unix))] + use std::os::windows::fs::symlink_file as symlink; + + let dir = tempfile::tempdir()?; + + let file = helpers::new_readonly_tempfile_in_dir(&dir)?; + let symlink_path = dir.path().join("linked"); + symlink(file.path(), &symlink_path)?; + + let mut app = helpers::AppBuilder::new() + .with_file(&symlink_path, None) + .build()?; + + test_key_sequence( + &mut app, + Some("ihello:wq"), + Some(&|app| { + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + assert_eq!(Some(&path::normalize(&symlink_path)), doc.path()); + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + }), + false, + ) + .await?; + + assert!(symlink_path.is_symlink()); + + Ok(()) +} + async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index d7205b0c0..70b3f4022 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -305,6 +305,18 @@ pub fn new_readonly_tempfile() -> anyhow::Result { Ok(file) } +/// Creates a new temporary file in the directory that is set to read only. Useful for +/// testing write failures. +pub fn new_readonly_tempfile_in_dir( + dir: impl AsRef, +) -> anyhow::Result { + let mut file = tempfile::NamedTempFile::new_in(dir)?; + let metadata = file.as_file().metadata()?; + let mut perms = metadata.permissions(); + perms.set_readonly(true); + file.as_file_mut().set_permissions(perms)?; + Ok(file) +} pub struct AppBuilder { args: Args, config: Config, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f26ba8b97..d44b4240c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -893,15 +893,18 @@ impl Future> + 'static + Send } } } + let write_path = tokio::fs::read_link(&path) + .await + .unwrap_or_else(|_| path.clone()); - if readonly(&path) { + if readonly(&write_path) { bail!(std::io::Error::new( std::io::ErrorKind::PermissionDenied, "Path is read only" )); } let backup = if path.exists() { - let path_ = path.clone(); + let path_ = write_path.clone(); // hacks: we use tempfile to handle the complex task of creating // non clobbered temporary path for us we don't want // the whole automatically delete path on drop thing @@ -925,7 +928,7 @@ impl Future> + 'static + Send }; let write_result: anyhow::Result<_> = async { - let mut dst = tokio::fs::File::create(&path).await?; + let mut dst = tokio::fs::File::create(&write_path).await?; to_writer(&mut dst, encoding_with_bom_info, &text).await?; Ok(()) } @@ -934,14 +937,13 @@ impl Future> + 'static + Send if let Some(backup) = backup { if write_result.is_err() { // restore backup - let _ = tokio::fs::rename(&backup, &path) + let _ = tokio::fs::rename(&backup, &write_path) .await .map_err(|e| log::error!("Failed to restore backup on write failure: {e}")); } else { // copy metadata and delete backup - let path_ = path.clone(); let _ = tokio::task::spawn_blocking(move || { - let _ = copy_metadata(&backup, &path_) + let _ = copy_metadata(&backup, &write_path) .map_err(|e| log::error!("Failed to copy metadata on write: {e}")); let _ = std::fs::remove_file(backup) .map_err(|e| log::error!("Failed to remove backup file on write: {e}")); From 081f7d0bd8f3b59857be3fa6bcfccdaee31eaf69 Mon Sep 17 00:00:00 2001 From: Matthew Toohey Date: Fri, 12 Apr 2024 20:32:24 -0400 Subject: [PATCH 038/126] Update tree-sitter-ld and highlights (#10379) --- languages.toml | 2 +- runtime/queries/ld/highlights.scm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/languages.toml b/languages.toml index fe94c597e..03af4fc2f 100644 --- a/languages.toml +++ b/languages.toml @@ -3360,7 +3360,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "ld" -source = { git = "https://github.com/mtoohey31/tree-sitter-ld", rev = "81978cde3844bfc199851e39c80a20ec6444d35e" } +source = { git = "https://github.com/mtoohey31/tree-sitter-ld", rev = "0e9695ae0ede47b8744a8e2ad44d4d40c5d4e4c9" } [[language]] name = "hyprlang" diff --git a/runtime/queries/ld/highlights.scm b/runtime/queries/ld/highlights.scm index 4f935e753..e0c9dd1e1 100644 --- a/runtime/queries/ld/highlights.scm +++ b/runtime/queries/ld/highlights.scm @@ -4,7 +4,7 @@ . (NAME) @namespace) -(NAME) @variable +[(NAME) (SYMBOLNAME)] @variable ; Operators From 9df1266376323b3dae07e48bd1e64463d3aec1dd Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Sun, 14 Apr 2024 02:34:57 +0200 Subject: [PATCH 039/126] Add lldb-dap debugger support for Odin (#10175) --- languages.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/languages.toml b/languages.toml index 03af4fc2f..1a345add9 100644 --- a/languages.toml +++ b/languages.toml @@ -2052,6 +2052,29 @@ block-comment-tokens = { start = "/*", end = "*/" } indent = { tab-width = 4, unit = "\t" } formatter = { command = "odinfmt", args = [ "-stdin", "true" ] } +[language.debugger] +name = "lldb-dap" +transport = "stdio" +command = "lldb-dap" + +[[language.debugger.templates]] +name = "binary" +request = "launch" +completion = [ { name = "binary", completion = "filename" } ] +args = { console = "internalConsole", program = "{0}" } + +[[language.debugger.templates]] +name = "attach" +request = "attach" +completion = [ "pid" ] +args = { console = "internalConsole", pid = "{0}" } + +[[language.debugger.templates]] +name = "gdbserver attach" +request = "attach" +completion = [ { name = "lldb connect url", default = "connect://localhost:3333" }, { name = "file", completion = "filename" }, "pid" ] +args = { console = "internalConsole", attachCommands = [ "platform select remote-gdb-server", "platform connect {0}", "file {1}", "attach {2}" ] } + [[grammar]] name = "odin" source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e49ffca2952529d33e94ed63b1b75c4f1" } From 124576059544aae3ed8c16247ce01c7e1b95d06e Mon Sep 17 00:00:00 2001 From: Sufian <9989266+SufianBabri@users.noreply.github.com> Date: Mon, 15 Apr 2024 20:08:55 +0500 Subject: [PATCH 040/126] Add bufferline and cursorline colors to vim dark theme (#10444) --- runtime/themes/vim_dark_high_contrast.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/runtime/themes/vim_dark_high_contrast.toml b/runtime/themes/vim_dark_high_contrast.toml index 4e2480dba..21f3be8bb 100644 --- a/runtime/themes/vim_dark_high_contrast.toml +++ b/runtime/themes/vim_dark_high_contrast.toml @@ -1,7 +1,10 @@ "ui.background" = { bg = "black" } +"ui.bufferline" = { bg = "black" } +"ui.bufferline.active" = { fg = "light-magenta", bg = "dark-magenta" } "ui.cursor" = { fg = "green", modifiers = ["reversed"] } "ui.cursor.match" = { fg = "light-cyan", bg = "dark-cyan" } "ui.cursor.primary" = { fg = "light-green", modifiers = ["reversed"] } +"ui.cursorline.primary" = { bg = "gray" } "ui.menu" = { bg = "dark-white" } "ui.menu.selected" = { fg = "yellow" } "ui.popup" = { bg = "dark-white" } @@ -50,6 +53,7 @@ black = "#000000" red = "#ed5f74" green = "#1ea672" +gray = "#111111" yellow = "#d97917" blue = "#688ef1" magenta = "#c96ed0" From 0546273570710b97e9eebfff84298afbbb372f02 Mon Sep 17 00:00:00 2001 From: Pedro Fedricci Date: Mon, 15 Apr 2024 13:07:15 -0300 Subject: [PATCH 041/126] chore: update tree-sitter-rust to v0.21.0 (#10365) * chore: update tree-sitter-rust to 0.21.0 * fix: pretty print and textobject tests --- helix-core/src/syntax.rs | 11 ++++++----- languages.toml | 2 +- runtime/queries/rust/highlights.scm | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 78abc0b0a..3cf818f60 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -2765,10 +2765,10 @@ fn test_textobject_queries() { ) }; - test("quantified_nodes", 1..36); + test("quantified_nodes", 1..37); // NOTE: Enable after implementing proper node group capturing - // test("quantified_nodes_grouped", 1..36); - // test("multiple_nodes_grouped", 1..36); + // test("quantified_nodes_grouped", 1..37); + // test("multiple_nodes_grouped", 1..37); } #[test] @@ -2939,7 +2939,7 @@ fn assert_pretty_print( #[test] fn test_pretty_print() { - let source = r#"/// Hello"#; + let source = r#"// Hello"#; assert_pretty_print("rust", source, "(line_comment)", 0, source.len()); // A large tree should be indented with fields: @@ -2958,7 +2958,8 @@ fn test_pretty_print() { " (macro_invocation\n", " macro: (identifier)\n", " (token_tree\n", - " (string_literal))))))", + " (string_literal\n", + " (string_content)))))))", ), 0, source.len(), diff --git a/languages.toml b/languages.toml index 1a345add9..ea1607d84 100644 --- a/languages.toml +++ b/languages.toml @@ -250,7 +250,7 @@ args = { attachCommands = [ "platform select remote-gdb-server", "platform conne [[grammar]] name = "rust" -source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" } +source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "473634230435c18033384bebaa6d6a17c2523281" } [[language]] name = "sway" diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 09068b4f5..1c0f799b1 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -51,7 +51,7 @@ (lifetime "'" @label (identifier) @label) -(loop_label +(label "'" @label (identifier) @label) From 70459b2b66685ee5451b8d04101866cf5d704ac9 Mon Sep 17 00:00:00 2001 From: blinxen Date: Mon, 15 Apr 2024 21:44:00 +0200 Subject: [PATCH 042/126] Update gix to version 0.62 (#10451) This update contains a security fix for https://rustsec.org/advisories/RUSTSEC-2024-0335.html --- Cargo.lock | 69 ++++++++++++++++++++++---------------------- helix-vcs/Cargo.toml | 2 +- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad42dc7ae..8abf0972c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,9 +538,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gix" -version = "0.61.0" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e0e59a44bf00de058ee98d6ecf3c9ed8f8842c1da642258ae4120d41ded8f7" +checksum = "5631c64fb4cd48eee767bf98a3cbc5c9318ef3bb71074d4c099a2371510282b6" dependencies = [ "gix-actor", "gix-attributes", @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.36.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62129c75e4b6229fe15fb9838cdc00c655e87105b651e4edd7c183fc5288b5d1" +checksum = "7580e05996e893347ad04e1eaceb92e1c0e6a3ffe517171af99bf6b6df0ca6e5" dependencies = [ "bstr", "gix-config-value", @@ -709,9 +709,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.42.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4" +checksum = "a5fbc24115b957346cd23fb0f47d830eb799c46c89cdcf2f5acc9bf2938c2d01" dependencies = [ "bstr", "gix-command", @@ -729,9 +729,9 @@ dependencies = [ [[package]] name = "gix-dir" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3413ccd29130900c17574678aee640e4847909acae9febf6424dc77b782c6d32" +checksum = "d6943a1f213ad7a060a0548ece229be53f3c2151534b126446ce3533eaf5f14c" dependencies = [ "bstr", "gix-discover", @@ -784,9 +784,9 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd71bf3e64d8fb5d5635d4166ca5a36fe56b292ffff06eab1d93ea47fd5beb89" +checksum = "5c0d1f01af62bfd2fb3dd291acc2b29d4ab3e96ad52a679174626508ce98ef12" dependencies = [ "bstr", "encoding_rs", @@ -805,9 +805,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634b8a743b0aae03c1a74ee0ea24e8c5136895efac64ce52b3ea106e1c6f0613" +checksum = "e2184c40e7910529677831c8b481acf788ffd92427ed21fad65b6aa637e631b8" dependencies = [ "gix-features", "gix-utils", @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549621f13d9ccf325a7de45506a3266af0d08f915181c5687abb5e8669bfd2e6" +checksum = "3383122cf18655ef4c097c0b935bba5eb56983947959aaf3b0ceb1949d4dd371" dependencies = [ "bitflags 2.5.0", "bstr", @@ -929,9 +929,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b55378c719693380f66d9dd21ce46721eed2981d8789fc698ec1ada6fa176e" +checksum = "e8bbb43d2fefdc4701ffdf9224844d05b136ae1b9a73c2f90710c8dd27a93503" dependencies = [ "arc-swap", "gix-date", @@ -949,9 +949,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.49.0" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6391aeaa030ad64aba346a9f5c69bb1c4e5c6fb4411705b03b40b49d8614ec30" +checksum = "b58bad27c7677fa6b587aab3a1aca0b6c97373bd371a0a4290677c838c9bcaf1" dependencies = [ "clru", "gix-chunk", @@ -969,9 +969,9 @@ dependencies = [ [[package]] name = "gix-packetline-blocking" -version = "0.17.3" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8ef6dd3ea50e26f3bf572e90c034d033c804d340cd1eb386392f184a9ba2f7" +checksum = "c31d42378a3d284732e4d589979930d0d253360eccf7ec7a80332e5ccb77e14a" dependencies = [ "bstr", "faster-hex", @@ -994,9 +994,9 @@ dependencies = [ [[package]] name = "gix-pathspec" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a96ed0e71ce9084a471fddfa74e842576a7cbf02fe8bd50388017ac461aed97" +checksum = "d479789f3abd10f68a709454ce04cd68b54092ee882c8622ae3aa1bb9bf8496c" dependencies = [ "bitflags 2.5.0", "bstr", @@ -1099,9 +1099,9 @@ dependencies = [ [[package]] name = "gix-status" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca216db89947eca709f69ec5851aa76f9628e7c7aab7aa5a927d0c619d046bf2" +checksum = "50c413bfd2952e4ee92e48438dac3c696f3555e586a34d184a427f6bedd1e4f9" dependencies = [ "bstr", "filetime", @@ -1150,16 +1150,17 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b838b2db8f62c9447d483a4c28d251b67fee32741a82cb4d35e9eb4e9fdc5ab" +checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e" [[package]] name = "gix-traverse" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95aef84bc777025403a09788b1e4815c06a19332e9e5d87a955e1ed7da9bf0cf" +checksum = "f4029ec209b0cc480d209da3837a42c63801dd8548f09c1f4502c60accb62aeb" dependencies = [ + "bitflags 2.5.0", "gix-commitgraph", "gix-date", "gix-hash", @@ -1172,9 +1173,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0b24f3ecc79a5a53539de9c2e99425d0ef23feacdcf3faac983aa9a2f26849" +checksum = "0db829ebdca6180fbe32be7aed393591df6db4a72dbbc0b8369162390954d1cf" dependencies = [ "bstr", "gix-features", @@ -1186,9 +1187,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92" +checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" dependencies = [ "bstr", "fastrand", @@ -1207,9 +1208,9 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe78e03af9eec168eb187e05463a981c57f0a915f64b1788685a776bd2ef969c" +checksum = "359a87dfef695b5f91abb9a424c947edca82768f34acfc269659f66174a510b4" dependencies = [ "bstr", "gix-attributes", diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 872ec64b0..a9529ceab 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p parking_lot = "0.12" arc-swap = { version = "1.7.1" } -gix = { version = "0.61.0", features = ["attributes", "status"], default-features = false, optional = true } +gix = { version = "0.62.0", features = ["attributes", "status"], default-features = false, optional = true } imara-diff = "0.1.5" anyhow = "1" From 8256ca7bc3a81eba08a18caa9df15833608e6584 Mon Sep 17 00:00:00 2001 From: Alexis-Lapierre <128792625+Alexis-Lapierre@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:33:50 +0000 Subject: [PATCH 043/126] Add support for Xena OpenAutomation files (#10448) Add support for .xtc/.xoa/.xpc files * XTC stand for Xena Traffic Configuration * XOA stand for Xena OpenAutomation * XPC stand for Xena Port Configuration Theses three file time seems to be the most common file extension I encountered in the wild --- book/src/generated/lang-support.md | 1 + languages.toml | 11 +++++++++++ runtime/queries/xtc/highlights.scm | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 runtime/queries/xtc/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 24bf3eece..d00d65623 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -215,6 +215,7 @@ | wren | ✓ | ✓ | ✓ | | | xit | ✓ | | | | | xml | ✓ | | ✓ | | +| xtc | ✓ | | | | | yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` | | yuck | ✓ | | | | | zig | ✓ | ✓ | ✓ | `zls` | diff --git a/languages.toml b/languages.toml index ea1607d84..cd6386713 100644 --- a/languages.toml +++ b/languages.toml @@ -3531,3 +3531,14 @@ comment-token = "#" [[grammar]] name = "ldif" source = { git = "https://github.com/kepet19/tree-sitter-ldif", rev = "0a917207f65ba3e3acfa9cda16142ee39c4c1aaa" } + +[[language]] +name = "xtc" +scope = "source.xtc" +# Accept Xena Traffic Configuration, Xena Port Configuration and Xena OpenAutomation +file-types = [ "xtc", "xpc", "xoa" ] +comment-token = ";" + +[[grammar]] +name = "xtc" +source = { git = "https://github.com/Alexis-Lapierre/tree-sitter-xtc", rev = "7bc11b736250c45e25cfb0215db2f8393779957e" } diff --git a/runtime/queries/xtc/highlights.scm b/runtime/queries/xtc/highlights.scm new file mode 100644 index 000000000..cb0bec6ae --- /dev/null +++ b/runtime/queries/xtc/highlights.scm @@ -0,0 +1,27 @@ +(parameter) @keyword + +(change_port) @function.special + +(template) @variable + +[ + (hex_argument) + (ipv4_argument) +] @attribute + +(numeric_argument) @constant.numeric + +(index) @tag + +(string_literal_argument) @string + +(string_argument) @constant.character + +(comment) @comment + +(port_comment) @label + +[ +("[") +("]") +] @punctuation.bracket From 8e161723ee7617c9fba892b9d2799d3103f0fd71 Mon Sep 17 00:00:00 2001 From: Matthew Bourke <42789303+matt-bourke@users.noreply.github.com> Date: Tue, 16 Apr 2024 23:59:45 +1000 Subject: [PATCH 044/126] Enabled traversing multiple buffers at once (#10463) * Enable traversing multiple buffers at once * run cargo fmt * simplify iterator call --- helix-term/src/commands.rs | 19 ++++++++++--------- helix-term/src/commands/typed.rs | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6cda93dcb..8f7675bc5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -799,28 +799,29 @@ fn goto_line_start(cx: &mut Context) { } fn goto_next_buffer(cx: &mut Context) { - goto_buffer(cx.editor, Direction::Forward); + goto_buffer(cx.editor, Direction::Forward, cx.count()); } fn goto_previous_buffer(cx: &mut Context) { - goto_buffer(cx.editor, Direction::Backward); + goto_buffer(cx.editor, Direction::Backward, cx.count()); } -fn goto_buffer(editor: &mut Editor, direction: Direction) { +fn goto_buffer(editor: &mut Editor, direction: Direction, count: usize) { let current = view!(editor).doc; let id = match direction { Direction::Forward => { let iter = editor.documents.keys(); - let mut iter = iter.skip_while(|id| *id != ¤t); - iter.next(); // skip current item - iter.next().or_else(|| editor.documents.keys().next()) + // skip 'count' times past current buffer + iter.cycle().skip_while(|id| *id != ¤t).nth(count) } Direction::Backward => { 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().next_back()) + // skip 'count' times past current buffer + iter.rev() + .cycle() + .skip_while(|id| *id != ¤t) + .nth(count) } } .unwrap(); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 5d7057da6..acbd8a8c3 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -309,7 +309,7 @@ fn buffer_next( return Ok(()); } - goto_buffer(cx.editor, Direction::Forward); + goto_buffer(cx.editor, Direction::Forward, 1); Ok(()) } @@ -322,7 +322,7 @@ fn buffer_previous( return Ok(()); } - goto_buffer(cx.editor, Direction::Backward); + goto_buffer(cx.editor, Direction::Backward, 1); Ok(()) } From 68765f51c96ee1bba6c87d4c981b96a6987fe56c Mon Sep 17 00:00:00 2001 From: Idobenhamo <135357971+Idobenhamo@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:00:13 +0300 Subject: [PATCH 045/126] Support Typst 0.11 (#10321) * Update the tree sitter to support Typst 0.11 and changed the lsp to Tinymist * Fixed * Added typst-lsp & tinymist --------- Co-authored-by: Idobenhamo --- book/src/generated/lang-support.md | 2 +- languages.toml | 5 +++-- runtime/queries/typst/highlights.scm | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index d00d65623..a5013bbee 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -197,7 +197,7 @@ | tsx | ✓ | ✓ | ✓ | `typescript-language-server` | | twig | ✓ | | | | | typescript | ✓ | ✓ | ✓ | `typescript-language-server` | -| typst | ✓ | | | `typst-lsp` | +| typst | ✓ | | | `tinymist`, `typst-lsp` | | ungrammar | ✓ | | | | | unison | ✓ | | ✓ | | | uxntal | ✓ | | | | diff --git a/languages.toml b/languages.toml index cd6386713..d543aff25 100644 --- a/languages.toml +++ b/languages.toml @@ -102,6 +102,7 @@ yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] } zls = { command = "zls" } blueprint-compiler = { command = "blueprint-compiler", args = ["lsp"] } typst-lsp = { command = "typst-lsp" } +tinymist = { command = "tinymist" } pkgbuild-language-server = { command = "pkgbuild-language-server" } helm_ls = { command = "helm_ls", args = ["serve"] } ember-language-server = { command = "ember-language-server", args = ["--stdio"] } @@ -3094,7 +3095,7 @@ scope = "source.typst" injection-regex = "typst" file-types = ["typst", "typ"] comment-token = "//" -language-servers = ["typst-lsp"] +language-servers = ["tinymist", "typst-lsp"] indent = { tab-width = 2, unit = " " } [language.auto-pairs] @@ -3106,7 +3107,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "typst" -source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "ecf8596336857adfcd5f7cbb3b2aa11a67badc37" } +source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "13863ddcbaa7b68ee6221cea2e3143415e64aea4" } [[language]] name = "nunjucks" diff --git a/runtime/queries/typst/highlights.scm b/runtime/queries/typst/highlights.scm index 0bbccede0..4e012a2fe 100644 --- a/runtime/queries/typst/highlights.scm +++ b/runtime/queries/typst/highlights.scm @@ -21,6 +21,7 @@ ; OPERATOR (in ["in" "not"] @keyword.operator) +(context "context" @keyword.control) (and "and" @keyword.operator) (or "or" @keyword.operator) (not "not" @keyword.operator) @@ -45,12 +46,9 @@ (string) @string (content ["[" "]"] @operator) (bool) @constant.builtin.boolean -(builtin) @constant.builtin (none) @constant.builtin (auto) @constant.builtin (ident) @variable -(call - item: (builtin) @function.builtin) ; MARKUP (item "-" @markup.list) From 50470f755fe1b3fefbbc78122ce5fb96ecc3bf3f Mon Sep 17 00:00:00 2001 From: Kieran Moy Date: Tue, 16 Apr 2024 22:11:01 +0800 Subject: [PATCH 046/126] Add missing hyprlang support (#10383) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index d543aff25..2a322d93f 100644 --- a/languages.toml +++ b/languages.toml @@ -3390,7 +3390,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-ld", rev = "0e9695ae0 name = "hyprlang" scope = "source.hyprlang" roots = ["hyprland.conf"] -file-types = [ { glob = "hyprland.conf"} ] +file-types = [ { glob = "hyprland.conf" }, { glob = "hyprpaper.conf" }, { glob = "hypridle.conf" }, { glob = "hyprlock.conf" } ] comment-token = "#" grammar = "hyprlang" From 7775b35cba1b09e81c90835d5755bf9144195dcb Mon Sep 17 00:00:00 2001 From: Rowan Lovejoy <44379649+rowanlovejoy@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:13:02 +0100 Subject: [PATCH 047/126] Add a warning in docs about conflicts with terminal default key bindings (#10380) Add a warning about conflicts with terminal default key bindings. --- book/src/keymap.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/book/src/keymap.md b/book/src/keymap.md index 73f7a8532..65a223efb 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -24,6 +24,8 @@ # Keymap > 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type. +> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts. + ## Normal mode Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key. From 69e08d9e91341570c0750b0c963fb31b36cbad33 Mon Sep 17 00:00:00 2001 From: Hichem Date: Tue, 16 Apr 2024 20:57:22 +0200 Subject: [PATCH 048/126] allow cycling through function signatures/overloads (#9974) implement handle_event to cycle through the function signatures. To change the signature press alt+p/n . Signed-off-by: Ben Fekih, Hichem --- helix-term/src/handlers/signature_help.rs | 112 +++++++++++++--------- helix-term/src/ui/lsp.rs | 95 +++++++++++++----- 2 files changed, 140 insertions(+), 67 deletions(-) diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index 3c746548a..0bb1d3d16 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -5,7 +5,7 @@ use helix_event::{ cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx, }; -use helix_lsp::lsp; +use helix_lsp::lsp::{self, SignatureInformation}; use helix_stdx::rope::RopeSliceExt; use helix_view::document::Mode; use helix_view::events::{DocumentDidChange, SelectionDidChange}; @@ -18,7 +18,7 @@ use crate::compositor::Compositor; use crate::events::{OnModeSwitch, PostInsertChar}; use crate::handlers::Handlers; -use crate::ui::lsp::SignatureHelp; +use crate::ui::lsp::{Signature, SignatureHelp}; use crate::ui::Popup; use crate::{job, ui}; @@ -82,6 +82,7 @@ fn handle_event( } } self.state = if open { State::Open } else { State::Closed }; + return timeout; } } @@ -138,6 +139,31 @@ pub fn request_signature_help( }); } +fn active_param_range( + signature: &SignatureInformation, + response_active_parameter: Option, +) -> 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)) + } + } +} + pub fn show_signature_help( editor: &mut Editor, compositor: &mut Compositor, @@ -184,54 +210,50 @@ pub fn show_signature_help( let doc = doc!(editor); let language = doc.language_name().unwrap_or(""); - let signature = match response + if response.signatures.is_empty() { + return; + } + + let signatures: Vec = 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), - ); + .into_iter() + .map(|s| { + let active_param_range = active_param_range(&s, response.active_parameter); - 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(), + let signature_doc = if config.lsp.display_signature_help_docs { + s.documentation.map(|doc| match doc { + lsp::Documentation::String(s) => s, + lsp::Documentation::MarkupContent(markup) => markup.value, + }) + } else { + None + }; + + Signature { + signature: s.label, + signature_doc, + active_param_range, + } }) - } 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()); + .collect(); let old_popup = compositor.find_id::>(SignatureHelp::ID); + let mut active_signature = old_popup + .as_ref() + .map(|popup| popup.contents().active_signature()) + .unwrap_or_else(|| response.active_signature.unwrap_or_default() as usize); + + if active_signature >= signatures.len() { + active_signature = signatures.len() - 1; + } + + let contents = SignatureHelp::new( + language.to_string(), + Arc::clone(&editor.syn_loader), + active_signature, + signatures, + ); + let mut popup = Popup::new(SignatureHelp::ID, contents) .position(old_popup.and_then(|p| p.get_position())) .position_bias(Open::Above) diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index a3698e38d..d53437ecd 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -3,60 +3,95 @@ use arc_swap::ArcSwap; use helix_core::syntax; use helix_view::graphics::{Margin, Rect, Style}; +use helix_view::input::Event; use tui::buffer::Buffer; +use tui::layout::Alignment; +use tui::text::Text; use tui::widgets::{BorderType, Paragraph, Widget, Wrap}; -use crate::compositor::{Component, Compositor, Context}; +use crate::compositor::{Component, Compositor, Context, EventResult}; +use crate::alt; use crate::ui::Markdown; use super::Popup; -pub struct SignatureHelp { - signature: String, - signature_doc: Option, +pub struct Signature { + pub signature: String, + pub signature_doc: Option, /// Part of signature text - active_param_range: Option<(usize, usize)>, + pub active_param_range: Option<(usize, usize)>, +} +pub struct SignatureHelp { language: String, config_loader: Arc>, + active_signature: usize, + signatures: Vec, } impl SignatureHelp { pub const ID: &'static str = "signature-help"; pub fn new( - signature: String, language: String, config_loader: Arc>, + active_signature: usize, + signatures: Vec, ) -> Self { Self { - signature, - signature_doc: None, - active_param_range: None, language, config_loader, + active_signature, + signatures, } } - pub fn set_signature_doc(&mut self, signature_doc: Option) { - self.signature_doc = signature_doc; - } - - pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) { - self.active_param_range = offset; + pub fn active_signature(&self) -> usize { + self.active_signature } pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup> { compositor.find_id::>(Self::ID) } + + fn signature_index(&self) -> String { + format!("({}/{})", self.active_signature + 1, self.signatures.len()) + } } impl Component for SignatureHelp { + fn handle_event(&mut self, event: &Event, _cx: &mut Context) -> EventResult { + let Event::Key(event) = event else { + return EventResult::Ignored(None); + }; + + if self.signatures.len() <= 1 { + return EventResult::Ignored(None); + } + + match event { + alt!('p') => { + self.active_signature = self + .active_signature + .checked_sub(1) + .unwrap_or(self.signatures.len() - 1); + EventResult::Consumed(None) + } + alt!('n') => { + self.active_signature = (self.active_signature + 1) % self.signatures.len(); + EventResult::Consumed(None) + } + _ => EventResult::Ignored(None), + } + } + fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { let margin = Margin::horizontal(1); - let active_param_span = self.active_param_range.map(|(start, end)| { + let signature = &self.signatures[self.active_signature]; + + let active_param_span = signature.active_param_range.map(|(start, end)| { vec![( cx.editor .theme @@ -66,21 +101,29 @@ fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { )] }); + let sig = &self.signatures[self.active_signature]; let sig_text = crate::ui::markdown::highlighted_code_block( - &self.signature, + sig.signature.as_str(), &self.language, Some(&cx.editor.theme), Arc::clone(&self.config_loader), active_param_span, ); + if self.signatures.len() > 1 { + let signature_index = self.signature_index(); + let text = Text::from(signature_index); + let paragraph = Paragraph::new(&text).alignment(Alignment::Right); + paragraph.render(area.clip_top(1).with_height(1).clip_right(1), surface); + } + let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width); let sig_text_area = area.clip_top(1).with_height(sig_text_height); let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area); let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false }); sig_text_para.render(sig_text_area, surface); - if self.signature_doc.is_none() { + if sig.signature_doc.is_none() { return; } @@ -92,7 +135,7 @@ fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { } } - let sig_doc = match &self.signature_doc { + let sig_doc = match &sig.signature_doc { None => return, Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), }; @@ -110,13 +153,15 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { const PADDING: u16 = 2; const SEPARATOR_HEIGHT: u16 = 1; + let sig = &self.signatures[self.active_signature]; + if PADDING >= viewport.1 || PADDING >= viewport.0 { return None; } let max_text_width = (viewport.0 - PADDING).min(120); let signature_text = crate::ui::markdown::highlighted_code_block( - &self.signature, + sig.signature.as_str(), &self.language, None, Arc::clone(&self.config_loader), @@ -125,7 +170,7 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { let (sig_width, sig_height) = crate::ui::text::required_size(&signature_text, max_text_width); - let (width, height) = match self.signature_doc { + let (width, height) = match sig.signature_doc { Some(ref doc) => { let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader)); let doc_text = doc_md.parse(None); @@ -139,6 +184,12 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { None => (sig_width, sig_height), }; - Some((width + PADDING, height + PADDING)) + let sig_index_width = if self.signatures.len() > 1 { + self.signature_index().len() + 1 + } else { + 0 + }; + + Some((width + PADDING + sig_index_width as u16, height + PADDING)) } } From 36ee9ba7d68c2302572d5e39c1b84d5b98dd9f44 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Tue, 16 Apr 2024 16:47:18 -0400 Subject: [PATCH 049/126] languages/rust: add `rust-script` and `cargo` shebangs (#10484) The former is one of the more popular forks of the original idea: https://rust-script.org/ The latter is an RFC for folding that functionality into cargo itself, available on nightly: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#script --- languages.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/languages.toml b/languages.toml index 2a322d93f..51610de6c 100644 --- a/languages.toml +++ b/languages.toml @@ -202,6 +202,7 @@ scope = "source.rust" injection-regex = "rust" file-types = ["rs"] roots = ["Cargo.toml", "Cargo.lock"] +shebangs = ["rust-script", "cargo"] auto-format = true comment-tokens = ["//", "///", "//!"] block-comment-tokens = [ From 43dff1c77251c1d6b9f98d6dee83a97e46af00e8 Mon Sep 17 00:00:00 2001 From: Clara Smyth <106271826+ClaraSmyth@users.noreply.github.com> Date: Tue, 16 Apr 2024 22:56:43 +0100 Subject: [PATCH 050/126] Fix: Svelte queries (#10487) --- runtime/queries/svelte/highlights.scm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm index 22b0c551e..7ab729030 100644 --- a/runtime/queries/svelte/highlights.scm +++ b/runtime/queries/svelte/highlights.scm @@ -1,6 +1,11 @@ ; Special identifiers ;-------------------- +(tag_name) @tag +(attribute_name) @variable.other.member +(erroneous_end_tag_name) @error +(comment) @comment + ; TODO: ((element (start_tag (tag_name) @_tag) (text) @markup.heading) (#match? @_tag "^(h[0-9]|title)$")) @@ -28,11 +33,6 @@ (quoted_attribute_value (attribute_value) @markup.link.url)) (#match? @_attr "^(href|src)$")) -(tag_name) @tag -(attribute_name) @variable.other.member -(erroneous_end_tag_name) @error -(comment) @comment - [ (attribute_value) (quoted_attribute_value) From 1cce693bef8e1e5bbfdc913429232da1a6aa572c Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 17 Apr 2024 02:06:05 +0200 Subject: [PATCH 051/126] correctly describe behavior of C in tutor (#10465) --- runtime/tutor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/tutor b/runtime/tutor index f2535dd92..055718379 100644 --- a/runtime/tutor +++ b/runtime/tutor @@ -539,7 +539,7 @@ will now affect both cursors. 3. Use Insert mode to correct the lines. The two cursors will fix both lines simultaneously. - 4. Type , to remove the second cursor. + 4. Type , to remove the first cursor. --> Fix th two nes at same ime. --> From ab203b5f53426ab0701d18aff1a6c6370c080241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 17 Apr 2024 13:15:16 +0200 Subject: [PATCH 052/126] update earthfile grammar and highlight queries (#10489) --- languages.toml | 2 +- runtime/queries/earthfile/highlights.scm | 74 ++++++++++++------------ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/languages.toml b/languages.toml index 51610de6c..5ac2b2fe6 100644 --- a/languages.toml +++ b/languages.toml @@ -3503,7 +3503,7 @@ language-servers = ["earthlyls"] [[grammar]] name = "earthfile" -source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "2a6ab191f5f962562e495a818aa4e7f45f8a556a" } +source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "a079e6c472eeedd6b9a1e03ca0b6c82cd6a112a4" } [[language]] name = "adl" diff --git a/runtime/queries/earthfile/highlights.scm b/runtime/queries/earthfile/highlights.scm index a0191174f..2de8670f8 100644 --- a/runtime/queries/earthfile/highlights.scm +++ b/runtime/queries/earthfile/highlights.scm @@ -1,42 +1,48 @@ (string_array "," @punctuation.delimiter) (string_array ["[" "]"] @punctuation.bracket) -(arg_command "ARG" @keyword) -(build_command "BUILD" @keyword) -(cache_command "CACHE" @keyword) -(cmd_command "CMD" @keyword) -(copy_command "COPY" @keyword) -(do_command "DO" @keyword) -(entrypoint_command "ENTRYPOINT" @keyword) -(env_command "ENV" @keyword) -(expose_command "EXPOSE" @keyword) -(from_command "FROM" @keyword) -(from_dockerfile_command "FROM DOCKERFILE" @keyword) -(function_command "FUNCTION" @keyword) -(git_clone_command "GIT CLONE" @keyword) -(host_command "HOST" @keyword) -(import_command "IMPORT" @keyword) -(label_command "LABEL" @keyword) -(let_command "LET" @keyword) -(project_command "PROJECT" @keyword) -(run_command "RUN" @keyword) -(save_artifact_command ["SAVE ARTIFACT" "AS LOCAL"] @keyword) -(save_image_command "SAVE IMAGE" @keyword) -(set_command "SET" @keyword) -(user_command "USER" @keyword) -(version_command "VERSION" @keyword) -(volume_command "VOLUME" @keyword) -(with_docker_command "WITH DOCKER" @keyword) -(workdir_command "WORKDIR" @keyword) +[ + "ARG" + "AS LOCAL" + "BUILD" + "CACHE" + "CMD" + "COPY" + "DO" + "ENTRYPOINT" + "ENV" + "EXPOSE" + "FROM DOCKERFILE" + "FROM" + "FUNCTION" + "GIT CLONE" + "HOST" + "IMPORT" + "LABEL" + "LET" + "PROJECT" + "RUN" + "SAVE ARTIFACT" + "SAVE IMAGE" + "SET" + "USER" + "VERSION" + "VOLUME" + "WORKDIR" +] @keyword (for_command ["FOR" "IN" "END"] @keyword.control.repeat) + (if_command ["IF" "END"] @keyword.control.conditional) (elif_block ["ELSE IF"] @keyword.control.conditional) (else_block ["ELSE"] @keyword.control.conditional) -(import_command ["IMPORT" "AS"] @keyword.control.import) -(try_command ["TRY" "FINALLY" "END"] @keyword.control.exception) -(wait_command ["WAIT" "END"] @keyword.control) +(import_command ["IMPORT" "AS"] @keyword.control.import) + +(try_command ["TRY" "FINALLY" "END"] @keyword.control.exception) + +(wait_command ["WAIT" "END"] @keyword.control) +(with_docker_command ["WITH DOCKER" "END"] @keyword.control) [ (comment) @@ -65,10 +71,4 @@ (build_arg) @variable (options (_) @variable.parameter) -(options (_ "=" @operator)) -(build_arg "=" @operator) -(arg_command "=" @operator) -(env_command "=" @operator) -(label "=" @operator) -(set_command "=" @operator) -(let_command "=" @operator) +"=" @operator From 521accaf001a033edc6d9b5c68f83e9408418fe1 Mon Sep 17 00:00:00 2001 From: ath3 <45574139+ath3@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:29:28 +0200 Subject: [PATCH 053/126] Include "change" in textobject autoinfo (#10496) --- helix-term/src/commands.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8f7675bc5..0c26ad92e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5436,6 +5436,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { ("T", "Test (tree-sitter)"), ("e", "Data structure entry (tree-sitter)"), ("m", "Closest surrounding pair"), + ("g", "Change"), (" ", "... or any character acting as a pair"), ]; From 30baff907d5eb8c7ef3881a0b0cceac088b506e3 Mon Sep 17 00:00:00 2001 From: Sean Perry Date: Wed, 17 Apr 2024 15:57:57 -0700 Subject: [PATCH 054/126] Implement read command (#10447) Co-authored-by: Michael Davis Co-authored-by: Ibrahim Dursun --- book/src/generated/typable-cmd.md | 1 + helix-term/src/commands/typed.rs | 43 ++++++++++++++++++++++++++++++- helix-term/tests/test/commands.rs | 24 +++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index dbb8b5f38..cf1b550dc 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -87,3 +87,4 @@ | `:redraw` | Clear and re-render the whole UI | | `:move` | Move the current buffer and its corresponding file to a different path | | `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default | +| `:read`, `:r` | Load a file into buffer | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index acbd8a8c3..f38ae6bba 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,4 +1,5 @@ use std::fmt::Write; +use std::io::BufReader; use std::ops::Deref; use crate::job::Job; @@ -8,7 +9,7 @@ use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; use helix_core::{line_ending, shellwords::Shellwords}; -use helix_view::document::DEFAULT_LANGUAGE_NAME; +use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; use helix_view::editor::{CloseError, ConfigEvent}; use serde_json::Value; use ui::completers::{self, Completer}; @@ -2454,6 +2455,39 @@ fn yank_diagnostic( Ok(()) } +fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let scrolloff = cx.editor.config().scrolloff; + let (view, doc) = current!(cx.editor); + + ensure!(!args.is_empty(), "file name is expected"); + ensure!(args.len() == 1, "only the file name is expected"); + + let filename = args.get(0).unwrap(); + let path = PathBuf::from(filename.to_string()); + ensure!( + path.exists() && path.is_file(), + "path is not a file: {:?}", + path + ); + + let file = std::fs::File::open(path).map_err(|err| anyhow!("error opening file: {}", err))?; + let mut reader = BufReader::new(file); + let (contents, _, _) = read_to_string(&mut reader, Some(doc.encoding())) + .map_err(|err| anyhow!("error reading file: {}", err))?; + let contents = Tendril::from(contents); + let selection = doc.selection(view.id); + let transaction = Transaction::insert(doc.text(), selection, contents); + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view); + view.ensure_cursor_in_view(doc, scrolloff); + + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -3068,6 +3102,13 @@ fn yank_diagnostic( fun: yank_diagnostic, signature: CommandSignature::all(completers::register), }, + TypableCommand { + name: "read", + aliases: &["r"], + doc: "Load a file into buffer", + fun: read, + signature: CommandSignature::positional(&[completers::filename]), + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 351f07fe9..6b904aa30 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -640,3 +640,27 @@ async fn test_join_selections_space() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_read_file() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let contents_to_read = "some contents"; + let output_file = helpers::temp_file_with_contents(contents_to_read)?; + + test_key_sequence( + &mut helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?, + Some(&format!(":r {:?}:w", output_file.path())), + Some(&|app| { + assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); + }), + false, + ) + .await?; + + let expected_contents = LineFeedHandling::Native.apply(contents_to_read); + helpers::assert_file_has_content(&mut file, &expected_contents)?; + + Ok(()) +} From 1d23796ad11c8c7f88ce9f6548c02ca7c035fb4f Mon Sep 17 00:00:00 2001 From: Daniel O'Brien Date: Wed, 17 Apr 2024 23:58:33 +0100 Subject: [PATCH 055/126] Fix kanagawa theme when using cursorline (#10500) --- runtime/themes/kanagawa.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/runtime/themes/kanagawa.toml b/runtime/themes/kanagawa.toml index d6f52a070..02b5271ec 100644 --- a/runtime/themes/kanagawa.toml +++ b/runtime/themes/kanagawa.toml @@ -8,7 +8,7 @@ ## User interface "ui.selection" = { bg = "waveBlue2" } -"ui.selection.primary" = { bg = "sumiInk5" } +"ui.selection.primary" = { bg = "waveBlue2" } "ui.background" = { fg = "fujiWhite", bg = "sumiInk1" } "ui.linenr" = { fg = "sumiInk4" } @@ -123,7 +123,6 @@ sumiInk1 = "#1F1F28" # default background sumiInk2 = "#2A2A37" # lighter background, e.g. colorcolumns, folds sumiInk3 = "#363646" # lighter background, e.g. cursorline sumiInk4 = "#54546D" # darker foreground, e.g. linenumbers, fold column -sumiInk5 = "#363646" # current selection waveBlue1 = "#223249" # popup background, visual selection background waveBlue2 = "#2D4F67" # popup selection background, search background winterGreen = "#2B3328" # diff add background From f06a1669625d3ba0bf14a85c783a72d7a4e6b233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 18 Apr 2024 14:57:23 +0900 Subject: [PATCH 056/126] Add Move language support --- languages.toml | 14 +++ runtime/queries/move/highlights.scm | 157 ++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 runtime/queries/move/highlights.scm diff --git a/languages.toml b/languages.toml index 5ac2b2fe6..bc23f708d 100644 --- a/languages.toml +++ b/languages.toml @@ -3544,3 +3544,17 @@ comment-token = ";" [[grammar]] name = "xtc" source = { git = "https://github.com/Alexis-Lapierre/tree-sitter-xtc", rev = "7bc11b736250c45e25cfb0215db2f8393779957e" } + +[[language]] +name = "move" +scope = "source.move" +injection-regex = "move" +roots = ["Move.toml"] +file-types = ["move"] +comment-token = "//" +indent = { tab-width = 4, unit = " " } +language-servers = [] + +[[grammar]] +name = "move" +source = { git = "https://github.com/tzakian/tree-sitter-move", rev = "8bc0d1692caa8763fef54d48068238d9bf3c0264" } diff --git a/runtime/queries/move/highlights.scm b/runtime/queries/move/highlights.scm new file mode 100644 index 000000000..86a79dc22 --- /dev/null +++ b/runtime/queries/move/highlights.scm @@ -0,0 +1,157 @@ +(ability) @keyword + +; --- +; Primitives +; --- + +(address_literal) @constant +(bool_literal) @constant.builtin.boolean +(num_literal) @constant.numeric +[ + (hex_string_literal) + (byte_string_literal) +] @string +; TODO: vector_literal + +[ + (line_comment) + (block_comment) +] @comment + +(annotation) @function.macro + +(borrow_expression "&" @keyword.storage.modifier.ref) +(borrow_expression "&mut" @keyword.storage.modifier.mut) + +(constant_identifier) @constant +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +(function_identifier) @function + +(struct_identifier) @type +(pack_expression + access: (module_access + member: (identifier) @type)) +(apply_type + (module_access + member: (identifier) @type)) +(field_identifier) @variable.other.member + +; ------- +; Functions +; ------- + +(call_expression + access: (module_access + member: (identifier) @function)) + +(macro_call_expression + access: (macro_module_access + access: (module_access + member: [(identifier) @function.macro]) + "!" @function.macro)) + +; ------- +; Paths +; ------- + +(module_identifier) @namespace + +; ------- +; Operators +; ------- + +[ + "*" + "=" + "!" +] @operator +(binary_operator) @operator + +; --- +; Punctuation +; --- + +[ + "::" + "." + ";" + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "abort" + ; "acquires" + "as" + "break" + "const" + "continue" + "copy" + "else" + "false" + "friend" + "fun" + "has" + "if" + ; "invariant" + "let" + "loop" + "module" + "move" + "native" + "public" + "return" + ; "script" + "spec" + "struct" + "true" + "use" + "while" + + "entry" + + ; "aborts_if" + ; "aborts_with" + "address" + "apply" + "assume" + ; "axiom" + ; "choose" + "decreases" + ; "emits" + "ensures" + "except" + ; "forall" + "global" + "include" + "internal" + "local" + ; "min" + ; "modifies" + "mut" + "phantom" + "post" + "pragma" + ; "requires" + ; "Self" + "schema" + "succeeds_if" + "to" + ; "update" + "where" + "with" +] @keyword + +(primitive_type) @type.buildin + +(identifier) @variable From 8924691c5d5527fcf283fcfaf176877515335b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 18 Apr 2024 14:59:46 +0900 Subject: [PATCH 057/126] minor: Update docs --- book/src/generated/lang-support.md | 1 + 1 file changed, 1 insertion(+) diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index a5013bbee..45cc1384d 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -122,6 +122,7 @@ | mermaid | ✓ | | | | | meson | ✓ | | ✓ | | | mint | | | | `mint` | +| move | ✓ | | | | | msbuild | ✓ | | ✓ | | | nasm | ✓ | ✓ | | | | nickel | ✓ | | ✓ | `nls` | From 97f683b336741c2bff4e979be0d84345c096f2fc Mon Sep 17 00:00:00 2001 From: ves <28784043+vesdev@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:57:26 +0300 Subject: [PATCH 058/126] Improve HTML highlighting (#10503) --- runtime/themes/horizon-dark.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtime/themes/horizon-dark.toml b/runtime/themes/horizon-dark.toml index 64f4022d9..d44cefb56 100644 --- a/runtime/themes/horizon-dark.toml +++ b/runtime/themes/horizon-dark.toml @@ -4,15 +4,19 @@ constant = "purple" "constant.numeric" = "orange" "constant.builtin" = "orange" variable = "red" +attribute = "brown" comment = "light-gray" special = "purple" +"punctuation" = "red" "punctuation.bracket" = "purple" +"punctuation.delimiter" = "white" keyword = "purple" function = "blue" label = "orange" type = "orange" constructor = "orange" namespace = "orange" +tag = "red" # User Interface "ui.background" = { bg = "bg", fg = "gray" } @@ -79,6 +83,7 @@ pink = "#EE64AE" selection = "#353747" green = "#27D796" orange = "#FAB795" +brown = "#F09383" purple = "#B877DB" red = "#E95678" blue = "#25B2BC" From 6bdc6f460ed8cb816641c8d2e3f2d60f5f467637 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 13:18:23 -0700 Subject: [PATCH 059/126] refactor(themes): removed `ui.highlight` effects from `solarized_light` This now matches `solarized_dark` changes from #10261 --- runtime/themes/solarized_light.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml index 60a2a0e21..b7c0a64a6 100644 --- a/runtime/themes/solarized_light.toml +++ b/runtime/themes/solarized_light.toml @@ -91,9 +91,6 @@ # 影响 picker列表选中, 快捷键帮助窗口文本 # Affects picker list selection, shortcut key help window text "ui.text.focus" = { fg = "blue", modifiers = ["bold"]} -# file picker中, 预览的当前选中项 -# In file picker, the currently selected item of the preview -"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] } # 主光标/selection # main cursor/selection From 4713eb06b1eabd6511bc8aa560b77da62f3586b2 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 13:20:16 -0700 Subject: [PATCH 060/126] refactor(themes): change `solarized_*` ruler to `bg` Also changed the colors to better blend with the theme. --- runtime/themes/solarized_dark.toml | 2 +- runtime/themes/solarized_light.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml index df8b7bbaf..2be238bb5 100644 --- a/runtime/themes/solarized_dark.toml +++ b/runtime/themes/solarized_dark.toml @@ -90,7 +90,7 @@ "ui.selection.primary" = { bg = "base015" } "ui.virtual.indent-guide" = { fg = "base02" } -"ui.virtual.ruler" = { fg = "red" } +"ui.virtual.ruler" = { bg = "base02" } # normal模式的光标 "ui.cursor" = {fg = "base02", bg = "cyan"} diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml index b7c0a64a6..b0c0cb4cf 100644 --- a/runtime/themes/solarized_light.toml +++ b/runtime/themes/solarized_light.toml @@ -104,7 +104,7 @@ "ui.selection.primary" = { bg = "base015" } "ui.virtual.indent-guide" = { fg = "base02" } -"ui.virtual.ruler" = { fg = "red" } +"ui.virtual.ruler" = { bg = "base02" } # normal模式的光标 # normal mode cursor From 88da9e857ccc0414211c14a010476a7d711d20f8 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 13:28:05 -0700 Subject: [PATCH 061/126] feat(themes): add `ui.virtual.ruler` for `default` --- theme.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/theme.toml b/theme.toml index a853ce6fe..9eeb008db 100644 --- a/theme.toml +++ b/theme.toml @@ -56,6 +56,7 @@ label = "honey" "ui.text.focus" = { fg = "white" } "ui.text.inactive" = "sirocco" "ui.virtual" = { fg = "comet" } +"ui.virtual.ruler" = { bg = "revolver" } "ui.virtual.jump-label" = { fg = "apricot", modifiers = ["bold"] } "ui.virtual.indent-guide" = { fg = "comet" } From a5a9827f3298083e6469df3f679c49264d349789 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 13:29:19 -0700 Subject: [PATCH 062/126] fix(themes): correct typo in `theme.toml` --- theme.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme.toml b/theme.toml index 9eeb008db..83791b0ab 100644 --- a/theme.toml +++ b/theme.toml @@ -40,7 +40,7 @@ label = "honey" "diff.minus" = "#f22c86" "diff.delta" = "#6f44f0" -# TODO: diferentiate doc comment +# TODO: differentiate doc comment # concat (ERROR) @error.syntax and "MISSING ;" selectors for errors "ui.background" = { bg = "midnight" } From be8dc22272825542f245e316eb68c712669f8de3 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 13:37:16 -0700 Subject: [PATCH 063/126] feat(themes): add `ui.virtual.ruler` for `horizon-dark` --- runtime/themes/horizon-dark.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/horizon-dark.toml b/runtime/themes/horizon-dark.toml index d44cefb56..e51ec09f5 100644 --- a/runtime/themes/horizon-dark.toml +++ b/runtime/themes/horizon-dark.toml @@ -33,6 +33,7 @@ tag = "red" "ui.selection" = { bg = "selection" } "ui.virtual.indent-guide" = { fg = "gray" } "ui.virtual.whitespace" = { fg = "light-gray" } +"ui.virtual.ruler" = { bg ="dark-bg" } "ui.statusline" = { bg = "dark-bg", fg = "light-gray" } "ui.popup" = { bg = "dark-bg", fg = "orange" } "ui.help" = { bg = "dark-bg", fg = "orange" } From 368b29ca729696388d4640fc0d65ca9510759a8c Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 13:59:43 -0700 Subject: [PATCH 064/126] feat(themes): add `ui.virtual.ruler` for `base16_default` --- base16_theme.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/base16_theme.toml b/base16_theme.toml index 49f09afed..4a071a60b 100644 --- a/base16_theme.toml +++ b/base16_theme.toml @@ -29,6 +29,7 @@ "namespace" = "magenta" "ui.help" = { fg = "white", bg = "black" } "ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "black" } "markup.heading" = "blue" "markup.list" = "red" From c0aadfd4ce324853e4ae73e535dacb426bbae949 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 14:00:49 -0700 Subject: [PATCH 065/126] feat(themes): add `ui.virtual.ruler` for `vim_dark_high_contrast` --- runtime/themes/vim_dark_high_contrast.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/vim_dark_high_contrast.toml b/runtime/themes/vim_dark_high_contrast.toml index 21f3be8bb..5e1a2a450 100644 --- a/runtime/themes/vim_dark_high_contrast.toml +++ b/runtime/themes/vim_dark_high_contrast.toml @@ -18,6 +18,7 @@ "ui.text.focus" = { fg = "yellow" } "ui.virtual.wrap" = { fg = "dark-blue" } "ui.virtual.indent-guide" = { fg = "dark-blue" } +"ui.virtual.ruler" = { bg = "dark-white" } "ui.window" = { bg = "dark-white" } "diagnostic.error" = { bg = "dark-red" } From 6fdc1d6a95e09e3db78df326a81c92be38c79edc Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 14:00:55 -0700 Subject: [PATCH 066/126] feat(themes): add `ui.virtual.ruler` for `varua` --- runtime/themes/varua.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/varua.toml b/runtime/themes/varua.toml index 330503506..34a7591de 100644 --- a/runtime/themes/varua.toml +++ b/runtime/themes/varua.toml @@ -67,6 +67,7 @@ "ui.statusline.select" = { bg = "blue", fg = "bg2" } "ui.virtual.wrap" = { fg = "grey0" } "ui.virtual.inlay-hint" = { fg = "grey1" } +"ui.virtual.ruler" = { bg = "bg2"} "hint" = "blue" "info" = "aqua" From 785d09e38f9b48cc95dd3eeee254ced1318867cd Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 14:01:35 -0700 Subject: [PATCH 067/126] feat(themes): add `ui.virtual.ruler` for `poimandres` This change also propagates to `poimandres-storm` --- runtime/themes/poimandres.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/poimandres.toml b/runtime/themes/poimandres.toml index c62c2ad27..7dc0d833a 100644 --- a/runtime/themes/poimandres.toml +++ b/runtime/themes/poimandres.toml @@ -58,6 +58,7 @@ string = { fg = "brightMint" } "ui.text.inactive" = "darkerGray" "ui.virtual" = { fg = "darkerGray.b0" } "ui.virtual.indent-guide" = "#303442" +"ui.virtual.ruler" = { bg ="selection" } "ui.selection" = { bg = "focus" } "ui.selection.primary" = { bg = "selection" } From ccb0c40b5ef090e6854654db3fcec8f55d4ef5a5 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 14:01:44 -0700 Subject: [PATCH 068/126] feat(themes): add `ui.virtual.ruler` for `mellow` --- runtime/themes/mellow.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/mellow.toml b/runtime/themes/mellow.toml index 8ab66622f..db237f977 100644 --- a/runtime/themes/mellow.toml +++ b/runtime/themes/mellow.toml @@ -79,6 +79,7 @@ "ui.text.focus" = { fg = "fg" } "ui.virtual" = { fg = "gray02" } +"ui.virtual.ruler" = { bg ="gray02" } "ui.virtual.indent-guide" = { fg = "gray02" } "ui.virtual.inlay-hint" = { fg = "gray04" } From bb576868549b44c0c06a1ba640d83f324b971cc6 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 14:02:05 -0700 Subject: [PATCH 069/126] feat(themes): add `ui.virtual.ruler` for `base16_terminal` --- runtime/themes/base16_terminal.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml index 6864c0eb4..c8530c79d 100644 --- a/runtime/themes/base16_terminal.toml +++ b/runtime/themes/base16_terminal.toml @@ -15,6 +15,7 @@ "ui.cursor.primary" = { fg = "light-gray", modifiers = ["reversed"] } "ui.virtual.whitespace" = "light-gray" "ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "black" } "variable" = "light-red" "constant.numeric" = "yellow" "constant" = "yellow" From 4e16956007dafd4fde0eeb3c31d7d8b272aa4130 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 14:02:21 -0700 Subject: [PATCH 070/126] feat(themes): add `ui.virtual.ruler` for `base16_default_light` --- runtime/themes/base16_default_light.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml index e89a9d1be..16e38e749 100644 --- a/runtime/themes/base16_default_light.toml +++ b/runtime/themes/base16_default_light.toml @@ -14,6 +14,7 @@ "ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] } "ui.virtual.whitespace" = "base03" "ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "base01" } "ui.text" = "base05" "operator" = "base05" "ui.text.focus" = "base05" From 34291f0f3bbb98c542ab5993a1d23bc6eed3a271 Mon Sep 17 00:00:00 2001 From: Rolo Date: Mon, 8 Apr 2024 14:02:31 -0700 Subject: [PATCH 071/126] feat(themes): add `ui.virtual.ruler` for `base16_default_dark` --- runtime/themes/base16_default_dark.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml index 0e5f066ba..389c94821 100644 --- a/runtime/themes/base16_default_dark.toml +++ b/runtime/themes/base16_default_dark.toml @@ -3,6 +3,7 @@ "ui.background" = { bg = "base00" } "ui.virtual.whitespace" = "base03" "ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] } +"ui.virtual.ruler" = { bg = "base01" } "ui.menu" = { fg = "base05", bg = "base01" } "ui.menu.selected" = { fg = "base01", bg = "base04" } "ui.linenr" = { fg = "base03", bg = "base01" } From 2209effb0299ae0221333e8c1ff828498061593f Mon Sep 17 00:00:00 2001 From: Nuke Date: Thu, 18 Apr 2024 19:26:04 -0600 Subject: [PATCH 072/126] Update lang-support.md for new wiki page name (#10508) The prior URL invites you to create a new wiki page. I think https://github.com/helix-editor/helix/wiki/Language-Server-Configurations is the correct place to point to now. There might be more issues related to changes in wiki structure that are not caught by some CI link check because of this (it's a valid URL, just not what you want to direct to lol) --- book/src/lang-support.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/lang-support.md b/book/src/lang-support.md index 3f96673bc..aede33fa0 100644 --- a/book/src/lang-support.md +++ b/book/src/lang-support.md @@ -1,7 +1,7 @@ # Language Support The following languages and Language Servers are supported. To use -Language Server features, you must first [install][lsp-install-wiki] the +Language Server features, you must first [configure][lsp-config-wiki] the appropriate Language Server. You can check the language support in your installed helix version with `hx --health`. @@ -11,6 +11,6 @@ # Language Support {{#include ./generated/lang-support.md}} -[lsp-install-wiki]: https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers +[lsp-config-wiki]: https://github.com/helix-editor/helix/wiki/Language-Server-Configurations [lang-config]: ./languages.md [adding-languages]: ./guides/adding_languages.md From 98b4df23a35501d77b3d61281b447e39b42c846d Mon Sep 17 00:00:00 2001 From: urly3 <124311484+urly3@users.noreply.github.com> Date: Fri, 19 Apr 2024 02:29:49 +0100 Subject: [PATCH 073/126] theme: everblush (#10394) changed the statusline colors for SELECT mode the previous colours seem to be incorrect and quite ugly (sorry). I chose the magenta over the cyan that (colors that were already present) as it has more contrast with the existing INSERT colour. the statusline colours are now inline with eachother, all having the background be the 'background' colour, with varying foregrounds. Co-authored-by: urly3 --- runtime/themes/everblush.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/themes/everblush.toml b/runtime/themes/everblush.toml index 9c1fa062e..ac0c2ae05 100644 --- a/runtime/themes/everblush.toml +++ b/runtime/themes/everblush.toml @@ -61,7 +61,7 @@ "ui.statusline.inactive" = { fg = "foreground", bg = "background" } "ui.statusline.normal" = { fg = "white", bg = "background" } "ui.statusline.insert" = { fg = "blue", bg = "background" } -"ui.statusline.select" = { fg = "cyan", bg = "magenta" } +"ui.statusline.select" = { fg = "magenta", bg = "background" } "ui.text" = { fg = "foreground" } "ui.text.focus" = { fg = "blue" } "ui.virtual.ruler" = { bg = "cursorline" } From 94405f3d077a2d92940c3d923ff19edf2a9349fa Mon Sep 17 00:00:00 2001 From: RoloEdits Date: Fri, 19 Apr 2024 07:22:55 -0700 Subject: [PATCH 074/126] refactor(themes): `gruvbox` warnings to `yellow1` (#10506) --- runtime/themes/gruvbox.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index a6b0636d3..67ca066f7 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -33,7 +33,7 @@ "diff.delta" = "orange1" "diff.minus" = "red1" -"warning" = "orange1" +"warning" = "yellow1" "error" = "red1" "info" = "aqua1" "hint" = "blue1" @@ -67,7 +67,7 @@ "ui.virtual.wrap" = { fg = "bg2" } "ui.virtual.jump-label" = { fg = "purple0", modifiers = ["bold"] } -"diagnostic.warning" = { underline = { color = "orange1", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow1", style = "curl" } } "diagnostic.error" = { underline = { color = "red1", style = "curl" } } "diagnostic.info" = { underline = { color = "aqua1", style = "curl" } } "diagnostic.hint" = { underline = { color = "blue1", style = "curl" } } From 18d5cacea66242430ff88c0e13964f537444a0e3 Mon Sep 17 00:00:00 2001 From: Kevin Vigor Date: Fri, 19 Apr 2024 19:19:12 -0600 Subject: [PATCH 075/126] Override crossterm's support for NO_COLOR. (#10514) Since helix isn't usable without color support, honoring this does nobody any good. --- helix-tui/src/backend/crossterm.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 37721e896..c5c95bff0 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -91,6 +91,10 @@ impl CrosstermBackend W: Write, { pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend { + // helix is not usable without colors, but crossterm will disable + // them by default if NO_COLOR is set in the environment. Override + // this behaviour. + crossterm::style::force_color_output(true); CrosstermBackend { buffer, capabilities: Capabilities::from_env_or_default(config), From 211f3680649a697270161bdff8e5b3263aaa8d98 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 19 Apr 2024 21:25:11 -0400 Subject: [PATCH 076/126] Respect mode when starting a search (#10505) Currently the editor mode has no effect on the behavior of `search` and `rsearch`. We can pass in the right movement for the editor mode to make the behavior or `search` and `rsearch` match `search_next` and `search_prev` in select mode. --- helix-term/src/commands.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0c26ad92e..cc7b84c4b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2080,6 +2080,11 @@ fn searcher(cx: &mut Context, direction: Direction) { let config = cx.editor.config(); let scrolloff = config.scrolloff; let wrap_around = config.search.wrap_around; + let movement = if cx.editor.mode() == Mode::Select { + Movement::Extend + } else { + Movement::Move + }; // TODO: could probably share with select_on_matches? let completions = search_completions(cx, Some(reg)); @@ -2104,7 +2109,7 @@ fn searcher(cx: &mut Context, direction: Direction) { search_impl( cx.editor, ®ex, - Movement::Move, + movement, direction, scrolloff, wrap_around, From af4ff80524053b28cfdaafdbbbbca25d533b71a2 Mon Sep 17 00:00:00 2001 From: "Ben Fekih, Hichem" Date: Mon, 8 Apr 2024 18:07:56 +0200 Subject: [PATCH 077/126] Improve popup position Make the popup positions more consistent. Improvements: 1. if the signature popup content is bigger than the available space, then the popup is always shown under the cursor, even if there more space above the cursor than below 2. There is no mutation anymore inside required_size. Maybe in the future we can update all widgets to have no mutations and change the trait Signed-off-by: Ben Fekih, Hichem --- helix-term/src/ui/completion.rs | 7 +- helix-term/src/ui/editor.rs | 1 - helix-term/src/ui/mod.rs | 6 +- helix-term/src/ui/popup.rs | 154 +++++++++++++++----------------- 4 files changed, 73 insertions(+), 95 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 735bc956a..1fcb8e547 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -493,12 +493,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { None => return, }; - let popup_area = { - let (popup_x, popup_y) = self.popup.get_rel_position(area, cx.editor); - let (popup_width, popup_height) = self.popup.get_size(); - Rect::new(popup_x, popup_y, popup_width, popup_height) - }; - + let popup_area = self.popup.area(area, cx.editor); let doc_width_available = area.width.saturating_sub(popup_area.right()); let doc_area = if doc_width_available > 30 { let mut doc_width = doc_width_available; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 7b130a38a..97f90f625 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1034,7 +1034,6 @@ pub fn set_completion( self.last_insert.1.push(InsertEvent::TriggerCompletion); // TODO : propagate required size on resize to completion too - completion.required_size((size.width, size.height)); self.completion = Some(completion); Some(area) } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index b5969818c..5211c2e27 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -13,7 +13,7 @@ mod statusline; mod text; -use crate::compositor::{Component, Compositor}; +use crate::compositor::Compositor; use crate::filter_picker_entry; use crate::job::{self, Callback}; pub use completion::{Completion, CompletionItem}; @@ -143,14 +143,12 @@ pub fn raw_regex_prompt( move |_editor: &mut Editor, compositor: &mut Compositor| { let contents = Text::new(format!("{}", err)); let size = compositor.size(); - let mut popup = Popup::new("invalid-regex", contents) + let popup = Popup::new("invalid-regex", contents) .position(Some(helix_core::Position::new( size.height as usize - 2, // 2 = statusline + commandline 0, ))) .auto_close(true); - popup.required_size((size.width, size.height)); - compositor.replace_or_push("invalid-regex", popup); }, )); diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 124b4402f..d1d7d6c50 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -15,6 +15,8 @@ Editor, }; +const MIN_HEIGHT: u16 = 4; + // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // a width/height hint. maybe Popup(Box) @@ -22,11 +24,9 @@ pub struct Popup { contents: T, position: Option, margin: Margin, - size: (u16, u16), - child_size: (u16, u16), area: Rect, position_bias: Open, - scroll: usize, + scroll_half_pages: usize, auto_close: bool, ignore_escape_key: bool, id: &'static str, @@ -39,11 +39,9 @@ pub fn new(id: &'static str, contents: T) -> Self { contents, position: None, margin: Margin::none(), - size: (0, 0), position_bias: Open::Below, - child_size: (0, 0), area: Rect::new(0, 0, 0, 0), - scroll: 0, + scroll_half_pages: 0, auto_close: false, ignore_escape_key: false, id, @@ -95,66 +93,12 @@ pub fn ignore_escape_key(mut self, ignore: bool) -> Self { self } - /// Calculate the position where the popup should be rendered and return the coordinates of the - /// top left corner. - pub fn get_rel_position(&mut self, viewport: Rect, editor: &Editor) -> (u16, u16) { - let position = self - .position - .get_or_insert_with(|| editor.cursor().0.unwrap_or_default()); - - let (width, height) = self.size; - - // if there's a orientation preference, use that - // if we're on the top part of the screen, do below - // if we're on the bottom part, do above - - // -- make sure frame doesn't stick out of bounds - let mut rel_x = position.col as u16; - let mut rel_y = position.row as u16; - if viewport.width <= rel_x + width { - rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - } - - let can_put_below = viewport.height > rel_y + height; - let can_put_above = rel_y.checked_sub(height).is_some(); - let final_pos = match self.position_bias { - Open::Below => match can_put_below { - true => Open::Below, - false => Open::Above, - }, - Open::Above => match can_put_above { - true => Open::Above, - false => Open::Below, - }, - }; - - rel_y = match final_pos { - Open::Above => rel_y.saturating_sub(height), - Open::Below => rel_y + 1, - }; - - (rel_x, rel_y) - } - - pub fn get_size(&self) -> (u16, u16) { - (self.size.0, self.size.1) - } - - pub fn scroll(&mut self, offset: usize, direction: bool) { - if direction { - let max_offset = self.child_size.1.saturating_sub(self.size.1); - self.scroll = (self.scroll + offset).min(max_offset as usize); - } else { - self.scroll = self.scroll.saturating_sub(offset); - } - } - pub fn scroll_half_page_down(&mut self) { - self.scroll(self.size.1 as usize / 2, true) + self.scroll_half_pages += 1; } pub fn scroll_half_page_up(&mut self) { - self.scroll(self.size.1 as usize / 2, false) + self.scroll_half_pages = self.scroll_half_pages.saturating_sub(1); } /// Toggles the Popup's scrollbar. @@ -175,12 +119,52 @@ pub fn contents_mut(&mut self) -> &mut T { pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { // trigger required_size so we recalculate if the child changed - self.required_size((viewport.width, viewport.height)); + let (width, height) = self + .required_size((viewport.width, viewport.height)) + .expect("Component needs required_size implemented in order to be embedded in a popup"); - let (rel_x, rel_y) = self.get_rel_position(viewport, editor); + let width = width.min(viewport.width); + let height = height.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport - // clip to viewport - viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)) + let position = self + .position + .get_or_insert_with(|| editor.cursor().0.unwrap_or_default()); + + // if there's a orientation preference, use that + // if we're on the top part of the screen, do below + // if we're on the bottom part, do above + + // -- make sure frame doesn't stick out of bounds + let mut rel_x = position.col as u16; + let mut rel_y = position.row as u16; + if viewport.width <= rel_x + width { + rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); + } + + let can_put_below = viewport.height > rel_y + MIN_HEIGHT; + let can_put_above = rel_y.checked_sub(MIN_HEIGHT).is_some(); + let final_pos = match self.position_bias { + Open::Below => match can_put_below { + true => Open::Below, + false => Open::Above, + }, + Open::Above => match can_put_above { + true => Open::Above, + false => Open::Below, + }, + }; + + match final_pos { + Open::Above => { + rel_y = rel_y.saturating_sub(height); + Rect::new(rel_x, rel_y, width, position.row as u16 - rel_y) + } + Open::Below => { + rel_y += 1; + let y_max = viewport.bottom().min(height + rel_y); + Rect::new(rel_x, rel_y, width, y_max - rel_y) + } + } } fn handle_mouse_event( @@ -266,38 +250,41 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } - fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let max_width = 120.min(viewport.0); - let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport + fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { + const MAX_WIDTH: u16 = 120; + const MAX_HEIGHT: u16 = 26; - let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin); + let inner = Rect::new(0, 0, MAX_WIDTH, MAX_HEIGHT).inner(&self.margin); let (width, height) = self .contents .required_size((inner.width, inner.height)) .expect("Component needs required_size implemented in order to be embedded in a popup"); - self.child_size = (width, height); - self.size = ( - (width + self.margin.width()).min(max_width), - (height + self.margin.height()).min(max_height), + let size = ( + (width + self.margin.width()).min(MAX_WIDTH), + (height + self.margin.height()).min(MAX_HEIGHT), ); - Some(self.size) + Some(size) } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { let area = self.area(viewport, cx.editor); self.area = area; - // required_size() calculates the popup size without taking account of self.position - // so we need to correct the popup height to correctly calculate the scroll - self.size.1 = area.height; + let child_size = self + .contents + .required_size((area.width, area.height)) + .expect("Component needs required_size implemented in order to be embedded in a popup"); - // re-clamp scroll offset - let max_offset = self.child_size.1.saturating_sub(self.size.1); - self.scroll = self.scroll.min(max_offset as usize); - cx.scroll = Some(self.scroll); + let max_offset = child_size.1.saturating_sub(area.height) as usize; + let half_page_size = (area.height / 2) as usize; + let scroll = max_offset.min(self.scroll_half_pages * half_page_size); + if half_page_size > 0 { + self.scroll_half_pages = scroll / half_page_size; + } + cx.scroll = Some(scroll); // clear area let background = cx.editor.theme.get("ui.popup"); @@ -325,9 +312,8 @@ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { // render scrollbar if contents do not fit if self.has_scrollbar { let win_height = (inner.height as usize).saturating_sub(2 * border); - let len = (self.child_size.1 as usize).saturating_sub(2 * border); + let len = (child_size.1 as usize).saturating_sub(2 * border); let fits = len <= win_height; - let scroll = self.scroll; let scroll_style = cx.editor.theme.get("ui.menu.scroll"); const fn div_ceil(a: usize, b: usize) -> usize { From 4b8bcd2773459f663aa1f641dd6ff49461e04542 Mon Sep 17 00:00:00 2001 From: "Ben Fekih, Hichem" Date: Fri, 19 Apr 2024 20:54:09 +0200 Subject: [PATCH 078/126] popup: call required_size only once while rendering to speed up the rendering a little Signed-off-by: Ben Fekih, Hichem --- helix-term/src/ui/popup.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index d1d7d6c50..8d436fda9 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -118,13 +118,22 @@ pub fn contents_mut(&mut self) -> &mut T { } pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { - // trigger required_size so we recalculate if the child changed - let (width, height) = self + let child_size = self + .contents .required_size((viewport.width, viewport.height)) .expect("Component needs required_size implemented in order to be embedded in a popup"); - let width = width.min(viewport.width); - let height = height.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport + self.area_internal(viewport, editor, child_size) + } + + pub fn area_internal( + &mut self, + viewport: Rect, + editor: &Editor, + child_size: (u16, u16), + ) -> Rect { + let width = child_size.0.min(viewport.width); + let height = child_size.1.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport let position = self .position @@ -270,14 +279,14 @@ fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - let area = self.area(viewport, cx.editor); - self.area = area; - let child_size = self .contents - .required_size((area.width, area.height)) + .required_size((viewport.width, viewport.height)) .expect("Component needs required_size implemented in order to be embedded in a popup"); + let area = self.area_internal(viewport, cx.editor, child_size); + self.area = area; + let max_offset = child_size.1.saturating_sub(area.height) as usize; let half_page_size = (area.height / 2) as usize; let scroll = max_offset.min(self.scroll_half_pages * half_page_size); From 345e687573f66e8ac5098b1200137c9b8ffa481d Mon Sep 17 00:00:00 2001 From: Chris Sergienko Date: Sat, 20 Apr 2024 15:41:55 +0300 Subject: [PATCH 079/126] feat: update bash grammar to latest tree-sitter-bash rev (#10526) --- languages.toml | 2 +- runtime/queries/bash/highlights.scm | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/languages.toml b/languages.toml index bc23f708d..fb74a8a14 100644 --- a/languages.toml +++ b/languages.toml @@ -936,7 +936,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "bash" -source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "275effdfc0edce774acf7d481f9ea195c6c403cd" } +source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "f8fb3274f72a30896075585b32b0c54cad65c086" } [[language]] name = "php" diff --git a/runtime/queries/bash/highlights.scm b/runtime/queries/bash/highlights.scm index 92d61e8b8..1aa35aa7c 100644 --- a/runtime/queries/bash/highlights.scm +++ b/runtime/queries/bash/highlights.scm @@ -60,7 +60,6 @@ ">>" "<" "|" - (expansion_flags) ] @operator ( From 35b6aef5fb3a648c72d8569a1f1d005aa9d596fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Apr 2024 23:49:01 +0900 Subject: [PATCH 080/126] build(deps): bump the rust-dependencies group with 8 updates (#10532) Bumps the rust-dependencies group with 8 updates: | Package | From | To | | --- | --- | --- | | [tree-sitter](https://github.com/tree-sitter/tree-sitter) | `0.22.2` | `0.22.5` | | [serde](https://github.com/serde-rs/serde) | `1.0.197` | `1.0.198` | | [serde_json](https://github.com/serde-rs/json) | `1.0.115` | `1.0.116` | | [encoding_rs](https://github.com/hsivonen/encoding_rs) | `0.8.33` | `0.8.34` | | [chrono](https://github.com/chronotope/chrono) | `0.4.37` | `0.4.38` | | [anyhow](https://github.com/dtolnay/anyhow) | `1.0.81` | `1.0.82` | | [clipboard-win](https://github.com/DoumanAsh/clipboard-win) | `5.3.0` | `5.3.1` | | [cc](https://github.com/rust-lang/cc-rs) | `1.0.90` | `1.0.95` | Updates `tree-sitter` from 0.22.2 to 0.22.5 - [Release notes](https://github.com/tree-sitter/tree-sitter/releases) - [Changelog](https://github.com/tree-sitter/tree-sitter/blob/master/CHANGELOG.md) - [Commits](https://github.com/tree-sitter/tree-sitter/compare/v0.22.2...v0.22.5) Updates `serde` from 1.0.197 to 1.0.198 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.197...v1.0.198) Updates `serde_json` from 1.0.115 to 1.0.116 - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.115...v1.0.116) Updates `encoding_rs` from 0.8.33 to 0.8.34 - [Commits](https://github.com/hsivonen/encoding_rs/compare/v0.8.33...v0.8.34) Updates `chrono` from 0.4.37 to 0.4.38 - [Release notes](https://github.com/chronotope/chrono/releases) - [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md) - [Commits](https://github.com/chronotope/chrono/compare/v0.4.37...v0.4.38) Updates `anyhow` from 1.0.81 to 1.0.82 - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.81...1.0.82) Updates `clipboard-win` from 5.3.0 to 5.3.1 - [Commits](https://github.com/DoumanAsh/clipboard-win/commits) Updates `cc` from 1.0.90 to 1.0.95 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.90...1.0.95) --- updated-dependencies: - dependency-name: tree-sitter dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: encoding_rs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: chrono dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: clipboard-win dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8abf0972c..6af72fc42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arc-swap" @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cfg-if" @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] @@ -365,9 +365,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -2093,18 +2093,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -2113,9 +2113,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -2472,9 +2472,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.22.2" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb9c9f15eae91dcd00ee0d86a281d16e6263786991b662b34fa9632c21a046b" +checksum = "688200d842c76dd88f9a7719ecb0483f79f5a766fb1c100756d5d8a059abc71b" dependencies = [ "cc", "regex", From efae85ec20c8fdbac7388bea1989895d7617f817 Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sun, 21 Apr 2024 00:58:54 +0200 Subject: [PATCH 081/126] Simplify first-in-line computation for indent queries. (#10527) --- helix-core/src/indent.rs | 49 +++++++++------------------------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 2a0a3876c..ca72fc3a2 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -247,41 +247,18 @@ fn add_indent_level( } } -/// Computes for node and all ancestors whether they are the first node on their line. -/// The first entry in the return value represents the root node, the last one the node itself -fn get_first_in_line(mut node: Node, new_line_byte_pos: Option) -> Vec { - let mut first_in_line = Vec::new(); - loop { - if let Some(prev) = node.prev_sibling() { - // If we insert a new line, the first node at/after the cursor is considered to be the first in its line - let first = prev.end_position().row != node.start_position().row - || new_line_byte_pos.map_or(false, |byte_pos| { - node.start_byte() >= byte_pos && prev.start_byte() < byte_pos - }); - first_in_line.push(Some(first)); - } else { - // Nodes that have no previous siblings are first in their line if and only if their parent is - // (which we don't know yet) - first_in_line.push(None); - } - if let Some(parent) = node.parent() { - node = parent; - } else { - break; +/// Return true if only whitespace comes before the node on its line. +/// If given, new_line_byte_pos is treated the same way as any existing newline. +fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option) -> bool { + let mut line_start_byte_pos = text.line_to_byte(node.start_position().row); + if let Some(pos) = new_line_byte_pos { + if line_start_byte_pos < pos && pos <= node.start_byte() { + line_start_byte_pos = pos; } } - - let mut result = Vec::with_capacity(first_in_line.len()); - let mut parent_is_first = true; // The root node is by definition the first node in its line - for first in first_in_line.into_iter().rev() { - if let Some(first) = first { - result.push(first); - parent_is_first = first; - } else { - result.push(parent_is_first); - } - } - result + text.byte_slice(line_start_byte_pos..node.start_byte()) + .chars() + .all(|c| c.is_whitespace()) } /// The total indent for some line of code. @@ -852,7 +829,6 @@ pub fn treesitter_indent_for_pos<'a>( byte_pos, new_line_byte_pos, )?; - let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos)); let mut result = Indentation::default(); // We always keep track of all the indent changes on one line, in order to only indent once @@ -861,9 +837,7 @@ pub fn treesitter_indent_for_pos<'a>( let mut indent_for_line_below = Indentation::default(); loop { - // This can safely be unwrapped because `first_in_line` contains - // one entry for each ancestor of the node (which is what we iterate over) - let is_first = *first_in_line.last().unwrap(); + let is_first = is_first_in_line(node, text, new_line_byte_pos); // Apply all indent definitions for this node. // Since we only iterate over each node once, we can remove the @@ -906,7 +880,6 @@ pub fn treesitter_indent_for_pos<'a>( } node = parent; - first_in_line.pop(); } else { // Only add the indentation for the line below if that line // is not after the line that the indentation is calculated for. From 26d9610e78b287c2316577af91adfd920baa7fa6 Mon Sep 17 00:00:00 2001 From: Simran Kedia Date: Sun, 21 Apr 2024 04:00:30 +0100 Subject: [PATCH 082/126] Ignore .svn version control files (#10536) Co-authored-by: Simran Kedia --- helix-term/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index 7719a6965..cf4fbd9fa 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -51,7 +51,7 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b // in our picker. if matches!( entry.file_name().to_str(), - Some(".git" | ".pijul" | ".jj" | ".hg") + Some(".git" | ".pijul" | ".jj" | ".hg" | ".svn") ) { return false; } From d140072fdc0cf7116f302ee1bd805e126abc8396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Sun, 21 Apr 2024 16:42:50 +0200 Subject: [PATCH 083/126] feat(themes): jump-label for modus themes (#10538) Add styling for jump-labels for modus themes. I couldn't find any official approach here so picking `yello-cooler`. `cooler` is used for other meta highlights by modus and yellow seems to be used the least - only warnings, so there's little chance of colliding with other highlights. --- runtime/themes/modus_operandi.toml | 1 + runtime/themes/modus_vivendi.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/runtime/themes/modus_operandi.toml b/runtime/themes/modus_operandi.toml index e7599c018..362f97778 100644 --- a/runtime/themes/modus_operandi.toml +++ b/runtime/themes/modus_operandi.toml @@ -80,6 +80,7 @@ punctuation = "fg-dim" "ui.virtual" = "bg-active" "ui.virtual.ruler" = { bg = "bg-dim" } "ui.virtual.inlay-hint" = { fg = "fg-dim", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "yellow-cooler", modifiers = ["bold"] } "ui.selection" = { fg = "fg-main", bg = "bg-inactive" } "ui.selection.primary" = { fg = "fg-main", bg = "bg-active" } diff --git a/runtime/themes/modus_vivendi.toml b/runtime/themes/modus_vivendi.toml index 74974bcb2..952683ee7 100644 --- a/runtime/themes/modus_vivendi.toml +++ b/runtime/themes/modus_vivendi.toml @@ -83,6 +83,7 @@ punctuation = "fg-dim" "ui.virtual" = "bg-active" "ui.virtual.ruler" = { bg = "bg-dim" } "ui.virtual.inlay-hint" = { fg = "fg-dim", modifiers = ["italic"] } +"ui.virtual.jump-label" = { fg = "yellow-cooler", modifiers = ["bold"] } "ui.selection" = { fg = "fg-main", bg = "bg-inactive" } "ui.selection.primary" = { fg = "fg-main", bg = "bg-active" } From b834806dbccfbaaa84c35f4db039a99ec54ebd5e Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 8 Apr 2024 02:46:32 +0200 Subject: [PATCH 084/126] use newtype parttern for langauge server id --- Cargo.lock | 1 + Cargo.toml | 1 + helix-core/Cargo.toml | 3 +- helix-core/src/diagnostic.rs | 21 +++- helix-lsp/Cargo.toml | 1 + helix-lsp/src/client.rs | 14 ++- helix-lsp/src/file_event.rs | 16 +-- helix-lsp/src/lib.rs | 141 +++++++++++++++----------- helix-lsp/src/transport.rs | 14 +-- helix-term/src/application.rs | 11 +- helix-term/src/commands/lsp.rs | 14 ++- helix-term/src/handlers/completion.rs | 2 +- helix-term/src/ui/completion.rs | 5 +- helix-term/src/ui/spinner.rs | 8 +- helix-view/src/document.rs | 28 ++--- helix-view/src/editor.rs | 22 ++-- helix-view/src/gutter.rs | 2 +- 17 files changed, 176 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6af72fc42..017a248a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1393,6 +1393,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "slotmap", "thiserror", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 3bfc4b8e1..6206281b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ package.helix-term.opt-level = 2 [workspace.dependencies] tree-sitter = { version = "0.22" } nucleo = "0.2.0" +slotmap = "1.0.7" [workspace.package] version = "24.3.0" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 7482262eb..16d6b457f 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -25,8 +25,7 @@ smartstring = "1.0.1" unicode-segmentation = "1.11" unicode-width = "0.1" unicode-general-category = "0.6" -# slab = "0.4.2" -slotmap = "1.0" +slotmap.workspace = true tree-sitter.workspace = true once_cell = "1.19" arc-swap = "1" diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 52d77a9aa..d41119d38 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -1,4 +1,6 @@ //! LSP diagnostic utility types. +use std::fmt; + use serde::{Deserialize, Serialize}; /// Describes the severity level of a [`Diagnostic`]. @@ -47,8 +49,25 @@ pub struct Diagnostic { pub message: String, pub severity: Option, pub code: Option, - pub language_server_id: usize, + pub provider: DiagnosticProvider, pub tags: Vec, pub source: Option, pub data: Option, } + +// TODO turn this into an enum + feature flag when lsp becomes optional +pub type DiagnosticProvider = LanguageServerId; + +// while I would prefer having this in helix-lsp that necessitates a bunch of +// conversions I would rather not add. I think its fine since this just a very +// trivial newtype wrapper and we would need something similar once we define +// completions in core +slotmap::new_key_type! { + pub struct LanguageServerId; +} + +impl fmt::Display for LanguageServerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 0b502f768..f87f15f21 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -31,3 +31,4 @@ tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "io-util", "io- tokio-stream = "0.1.15" parking_lot = "0.12.1" arc-swap = "1" +slotmap.workspace = true diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index b2290e031..79d8adb31 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -2,7 +2,7 @@ file_operations::FileOperationsInterest, find_lsp_workspace, jsonrpc, transport::{Payload, Transport}, - Call, Error, OffsetEncoding, Result, + Call, Error, LanguageServerId, OffsetEncoding, Result, }; use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope}; @@ -46,7 +46,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder { #[derive(Debug)] pub struct Client { - id: usize, + id: LanguageServerId, name: String, _process: Child, server_tx: UnboundedSender, @@ -179,10 +179,14 @@ pub fn start( server_environment: HashMap, root_path: PathBuf, root_uri: Option, - id: usize, + id: LanguageServerId, name: String, req_timeout: u64, - ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { + ) -> Result<( + Self, + UnboundedReceiver<(LanguageServerId, Call)>, + Arc, + )> { // Resolve path to the binary let cmd = helix_stdx::env::which(cmd)?; @@ -234,7 +238,7 @@ pub fn name(&self) -> &str { &self.name } - pub fn id(&self) -> usize { + pub fn id(&self) -> LanguageServerId { self.id } diff --git a/helix-lsp/src/file_event.rs b/helix-lsp/src/file_event.rs index 93457fa55..8f4c16756 100644 --- a/helix-lsp/src/file_event.rs +++ b/helix-lsp/src/file_event.rs @@ -3,24 +3,24 @@ use globset::{GlobBuilder, GlobSetBuilder}; use tokio::sync::mpsc; -use crate::{lsp, Client}; +use crate::{lsp, Client, LanguageServerId}; enum Event { FileChanged { path: PathBuf, }, Register { - client_id: usize, + client_id: LanguageServerId, client: Weak, registration_id: String, options: lsp::DidChangeWatchedFilesRegistrationOptions, }, Unregister { - client_id: usize, + client_id: LanguageServerId, registration_id: String, }, RemoveClient { - client_id: usize, + client_id: LanguageServerId, }, } @@ -59,7 +59,7 @@ pub fn new() -> Self { pub fn register( &self, - client_id: usize, + client_id: LanguageServerId, client: Weak, registration_id: String, options: lsp::DidChangeWatchedFilesRegistrationOptions, @@ -72,7 +72,7 @@ pub fn register( }); } - pub fn unregister(&self, client_id: usize, registration_id: String) { + pub fn unregister(&self, client_id: LanguageServerId, registration_id: String) { let _ = self.tx.send(Event::Unregister { client_id, registration_id, @@ -83,12 +83,12 @@ pub fn file_changed(&self, path: PathBuf) { let _ = self.tx.send(Event::FileChanged { path }); } - pub fn remove_client(&self, client_id: usize) { + pub fn remove_client(&self, client_id: LanguageServerId) { let _ = self.tx.send(Event::RemoveClient { client_id }); } async fn run(mut rx: mpsc::UnboundedReceiver) { - let mut state: HashMap = HashMap::new(); + let mut state: HashMap = HashMap::new(); while let Some(event) = rx.recv().await { match event { Event::FileChanged { path } => { diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 262bc1b94..04f018bc4 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -17,6 +17,7 @@ LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures, }; use helix_stdx::path; +use slotmap::SlotMap; use tokio::sync::mpsc::UnboundedReceiver; use std::{ @@ -28,8 +29,9 @@ use thiserror::Error; use tokio_stream::wrappers::UnboundedReceiverStream; -pub type Result = core::result::Result; +pub type Result = core::result::Result; pub type LanguageServerName = String; +pub use helix_core::diagnostic::LanguageServerId; #[derive(Error, Debug)] pub enum Error { @@ -651,38 +653,42 @@ pub fn parse(method: &str, params: jsonrpc::Params) -> Result { #[derive(Debug)] pub struct Registry { - inner: HashMap>>, + inner: SlotMap>, + inner_by_name: HashMap>>, syn_loader: Arc>, - counter: usize, - pub incoming: SelectAll>, + pub incoming: SelectAll>, pub file_event_handler: file_event::Handler, } impl Registry { pub fn new(syn_loader: Arc>) -> Self { Self { - inner: HashMap::new(), + inner: SlotMap::with_key(), + inner_by_name: HashMap::new(), syn_loader, - counter: 0, incoming: SelectAll::new(), file_event_handler: file_event::Handler::new(), } } - pub fn get_by_id(&self, id: usize) -> Option<&Client> { - self.inner - .values() - .flatten() - .find(|client| client.id() == id) - .map(|client| &**client) + pub fn get_by_id(&self, id: LanguageServerId) -> Option<&Arc> { + self.inner.get(id) } - pub fn remove_by_id(&mut self, id: usize) { + pub fn remove_by_id(&mut self, id: LanguageServerId) { + let Some(client) = self.inner.remove(id) else { + log::error!("client was already removed"); + return + }; self.file_event_handler.remove_client(id); - self.inner.retain(|_, language_servers| { - language_servers.retain(|ls| id != ls.id()); - !language_servers.is_empty() - }); + let instances = self + .inner_by_name + .get_mut(client.name()) + .expect("inner and inner_by_name must be synced"); + instances.retain(|ls| id != ls.id()); + if instances.is_empty() { + self.inner_by_name.remove(client.name()); + } } fn start_client( @@ -692,28 +698,28 @@ fn start_client( doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result>> { + ) -> Result, StartupError> { let syn_loader = self.syn_loader.load(); let config = syn_loader .language_server_configs() .get(&name) .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?; - let id = self.counter; - self.counter += 1; - if let Some(NewClient(client, incoming)) = start_client( - id, - name, - ls_config, - config, - doc_path, - root_dirs, - enable_snippets, - )? { - self.incoming.push(UnboundedReceiverStream::new(incoming)); - Ok(Some(client)) - } else { - Ok(None) - } + let id = self.inner.try_insert_with_key(|id| { + start_client( + id, + name, + ls_config, + config, + doc_path, + root_dirs, + enable_snippets, + ) + .map(|client| { + self.incoming.push(UnboundedReceiverStream::new(client.1)); + client.0 + }) + })?; + Ok(self.inner[id].clone()) } /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers, @@ -730,7 +736,7 @@ pub fn restart( .language_servers .iter() .filter_map(|LanguageServerFeatures { name, .. }| { - if self.inner.contains_key(name) { + if self.inner_by_name.contains_key(name) { let client = match self.start_client( name.clone(), language_config, @@ -738,16 +744,18 @@ pub fn restart( root_dirs, enable_snippets, ) { - Ok(client) => client?, - Err(error) => return Some(Err(error)), + Ok(client) => client, + Err(StartupError::NoRequiredRootFound) => return None, + Err(StartupError::Error(err)) => return Some(Err(err)), }; let old_clients = self - .inner + .inner_by_name .insert(name.clone(), vec![client.clone()]) .unwrap(); for old_client in old_clients { self.file_event_handler.remove_client(old_client.id()); + self.inner.remove(client.id()); tokio::spawn(async move { let _ = old_client.force_shutdown().await; }); @@ -762,9 +770,10 @@ pub fn restart( } pub fn stop(&mut self, name: &str) { - if let Some(clients) = self.inner.remove(name) { + if let Some(clients) = self.inner_by_name.remove(name) { for client in clients { self.file_event_handler.remove_client(client.id()); + self.inner.remove(client.id()); tokio::spawn(async move { let _ = client.force_shutdown().await; }); @@ -781,7 +790,7 @@ pub fn get<'a>( ) -> impl Iterator>)> + 'a { language_config.language_servers.iter().filter_map( move |LanguageServerFeatures { name, .. }| { - if let Some(clients) = self.inner.get(name) { + if let Some(clients) = self.inner_by_name.get(name) { if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| { client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) }) { @@ -796,21 +805,21 @@ pub fn get<'a>( enable_snippets, ) { Ok(client) => { - let client = client?; - self.inner + self.inner_by_name .entry(name.to_owned()) .or_default() .push(client.clone()); Some((name.clone(), Ok(client))) } - Err(err) => Some((name.to_owned(), Err(err))), + Err(StartupError::NoRequiredRootFound) => None, + Err(StartupError::Error(err)) => Some((name.to_owned(), Err(err))), } }, ) } pub fn iter_clients(&self) -> impl Iterator> { - self.inner.values().flatten() + self.inner.values() } } @@ -833,7 +842,7 @@ pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> { /// Acts as a container for progress reported by language servers. Each server /// has a unique id assigned at creation through [`Registry`]. This id is then used /// to store the progress in this map. -pub struct LspProgressMap(HashMap>); +pub struct LspProgressMap(HashMap>); impl LspProgressMap { pub fn new() -> Self { @@ -841,28 +850,35 @@ pub fn new() -> Self { } /// Returns a map of all tokens corresponding to the language server with `id`. - pub fn progress_map(&self, id: usize) -> Option<&HashMap> { + pub fn progress_map( + &self, + id: LanguageServerId, + ) -> Option<&HashMap> { self.0.get(&id) } - pub fn is_progressing(&self, id: usize) -> bool { + pub fn is_progressing(&self, id: LanguageServerId) -> bool { self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default() } /// Returns last progress status for a given server with `id` and `token`. - pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> { + pub fn progress( + &self, + id: LanguageServerId, + token: &lsp::ProgressToken, + ) -> Option<&ProgressStatus> { self.0.get(&id).and_then(|values| values.get(token)) } /// Checks if progress `token` for server with `id` is created. - pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool { + pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool { self.0 .get(&id) .map(|values| values.get(token).is_some()) .unwrap_or_default() } - pub fn create(&mut self, id: usize, token: lsp::ProgressToken) { + pub fn create(&mut self, id: LanguageServerId, token: lsp::ProgressToken) { self.0 .entry(id) .or_default() @@ -872,7 +888,7 @@ pub fn create(&mut self, id: usize, token: lsp::ProgressToken) { /// Ends the progress by removing the `token` from server with `id`, if removed returns the value. pub fn end_progress( &mut self, - id: usize, + id: LanguageServerId, token: &lsp::ProgressToken, ) -> Option { self.0.get_mut(&id).and_then(|vals| vals.remove(token)) @@ -881,7 +897,7 @@ pub fn end_progress( /// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`. pub fn update( &mut self, - id: usize, + id: LanguageServerId, token: lsp::ProgressToken, status: lsp::WorkDoneProgress, ) -> Option { @@ -892,19 +908,30 @@ pub fn update( } } -struct NewClient(Arc, UnboundedReceiver<(usize, Call)>); +struct NewClient(Arc, UnboundedReceiver<(LanguageServerId, Call)>); + +enum StartupError { + NoRequiredRootFound, + Error(Error), +} + +impl> From for StartupError { + fn from(value: T) -> Self { + StartupError::Error(value.into()) + } +} /// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that /// it is only called when it makes sense. fn start_client( - id: usize, + id: LanguageServerId, name: String, config: &LanguageConfiguration, ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, -) -> Result> { +) -> Result { let (workspace, workspace_is_cwd) = helix_loader::find_workspace(); let workspace = path::normalize(workspace); let root = find_lsp_workspace( @@ -929,7 +956,7 @@ fn start_client( .map(|entry| entry.file_name()) .any(|entry| globset.is_match(entry)) { - return Ok(None); + return Err(StartupError::NoRequiredRootFound); } } @@ -981,7 +1008,7 @@ fn start_client( initialize_notify.notify_one(); }); - Ok(Some(NewClient(client, incoming))) + Ok(NewClient(client, incoming)) } /// Find an LSP workspace of a file using the following mechanism: diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index f2f35d6ab..bd671abe1 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,4 +1,4 @@ -use crate::{jsonrpc, Error, Result}; +use crate::{jsonrpc, Error, LanguageServerId, Result}; use anyhow::Context; use log::{error, info}; use serde::{Deserialize, Serialize}; @@ -37,7 +37,7 @@ enum ServerMessage { #[derive(Debug)] pub struct Transport { - id: usize, + id: LanguageServerId, name: String, pending_requests: Mutex>>>, } @@ -47,10 +47,10 @@ pub fn start( server_stdout: BufReader, server_stdin: BufWriter, server_stderr: BufReader, - id: usize, + id: LanguageServerId, name: String, ) -> ( - UnboundedReceiver<(usize, jsonrpc::Call)>, + UnboundedReceiver<(LanguageServerId, jsonrpc::Call)>, UnboundedSender, Arc, ) { @@ -194,7 +194,7 @@ async fn send_string_to_server( async fn process_server_message( &self, - client_tx: &UnboundedSender<(usize, jsonrpc::Call)>, + client_tx: &UnboundedSender<(LanguageServerId, jsonrpc::Call)>, msg: ServerMessage, language_server_name: &str, ) -> Result<()> { @@ -251,7 +251,7 @@ async fn process_request_response( async fn recv( transport: Arc, mut server_stdout: BufReader, - client_tx: UnboundedSender<(usize, jsonrpc::Call)>, + client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>, ) { let mut recv_buffer = String::new(); loop { @@ -329,7 +329,7 @@ async fn err(transport: Arc, mut server_stderr: BufReader) { async fn send( transport: Arc, mut server_stdin: BufWriter, - client_tx: UnboundedSender<(usize, jsonrpc::Call)>, + client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>, mut client_rx: UnboundedReceiver, initialize_notify: Arc, ) { diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 809393c7f..6bdf60bca 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -4,7 +4,7 @@ use helix_lsp::{ lsp::{self, notification::Notification}, util::lsp_range_to_range, - LspProgressMap, + LanguageServerId, LspProgressMap, }; use helix_stdx::path::get_relative_path; use helix_view::{ @@ -655,7 +655,7 @@ pub async fn handle_terminal_events(&mut self, event: std::io::Result { - if let Some(client) = self - .editor - .language_servers - .iter_clients() - .find(|client| client.id() == server_id) - { + if let Some(client) = self.editor.language_servers.get_by_id(server_id) { for reg in params.registrations { match reg.method.as_str() { lsp::notification::DidChangeWatchedFiles::METHOD => { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 6a5ceae6f..88bd07919 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -6,7 +6,7 @@ NumberOrString, }, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, - Client, OffsetEncoding, + Client, LanguageServerId, OffsetEncoding, }; use tokio_stream::StreamExt; use tui::{ @@ -266,7 +266,7 @@ enum DiagnosticsFormat { fn diag_picker( cx: &Context, - diagnostics: BTreeMap>, + diagnostics: BTreeMap>, format: DiagnosticsFormat, ) -> Picker { // TODO: drop current_path comparison and instead use workspace: bool flag? @@ -497,7 +497,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { struct CodeActionOrCommandItem { lsp_item: lsp::CodeActionOrCommand, - language_server_id: usize, + language_server_id: LanguageServerId, } impl ui::menu::Item for CodeActionOrCommandItem { @@ -757,7 +757,11 @@ fn format(&self, _data: &Self::Data) -> Row { } } -pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) { +pub fn execute_lsp_command( + editor: &mut Editor, + language_server_id: LanguageServerId, + cmd: lsp::Command, +) { // the command is executed on the server and communicated back // to the client asynchronously using workspace edits let future = match editor @@ -1034,7 +1038,7 @@ fn get_prefill_from_lsp_response( fn create_rename_prompt( editor: &Editor, prefill: String, - language_server_id: Option, + language_server_id: Option, ) -> Box { let prompt = ui::Prompt::new( "rename-to:".into(), diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs index 491ca5638..8bd85ef6d 100644 --- a/helix-term/src/handlers/completion.rs +++ b/helix-term/src/handlers/completion.rs @@ -251,7 +251,7 @@ fn request_completion( .into_iter() .map(|item| CompletionItem { item, - language_server_id, + provider: language_server_id, resolved: false, }) .collect(); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 1fcb8e547..2b3437e11 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -94,7 +94,7 @@ fn format(&self, _data: &Self::Data) -> menu::Row { #[derive(Debug, PartialEq, Default, Clone)] pub struct CompletionItem { pub item: lsp::CompletionItem, - pub language_server_id: usize, + pub provider: LanguageServerId, pub resolved: bool, } @@ -224,7 +224,7 @@ macro_rules! language_server { ($item:expr) => { match editor .language_servers - .get_by_id($item.language_server_id) + .get_by_id($item.provider) { Some(ls) => ls, None => { @@ -285,7 +285,6 @@ macro_rules! language_server { let language_server = language_server!(item); let offset_encoding = language_server.offset_encoding(); - // resolve item if not yet resolved if !item.resolved { if let Some(resolved) = Self::resolve_completion_item(language_server, item.item.clone()) diff --git a/helix-term/src/ui/spinner.rs b/helix-term/src/ui/spinner.rs index 379c4489f..9ce610555 100644 --- a/helix-term/src/ui/spinner.rs +++ b/helix-term/src/ui/spinner.rs @@ -1,16 +1,18 @@ use std::{collections::HashMap, time::Instant}; +use helix_lsp::LanguageServerId; + #[derive(Default, Debug)] pub struct ProgressSpinners { - inner: HashMap, + inner: HashMap, } impl ProgressSpinners { - pub fn get(&self, id: usize) -> Option<&Spinner> { + pub fn get(&self, id: LanguageServerId) -> Option<&Spinner> { self.inner.get(&id) } - pub fn get_or_create(&mut self, id: usize) -> &mut Spinner { + pub fn get_or_create(&mut self, id: LanguageServerId) -> &mut Spinner { self.inner.entry(id).or_default() } } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index d44b4240c..3393fbed7 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -624,7 +624,7 @@ fn take_with(mut_ref: &mut T, f: F) *mut_ref = f(mem::take(mut_ref)); } -use helix_lsp::{lsp, Client, LanguageServerName}; +use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName}; use url::Url; impl Document { @@ -1296,11 +1296,7 @@ fn apply_impl( }); self.diagnostics.sort_unstable_by_key(|diagnostic| { - ( - diagnostic.range, - diagnostic.severity, - diagnostic.language_server_id, - ) + (diagnostic.range, diagnostic.severity, diagnostic.provider) }); // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place @@ -1644,7 +1640,7 @@ pub fn language_servers_with_feature( }) } - pub fn supports_language_server(&self, id: usize) -> bool { + pub fn supports_language_server(&self, id: LanguageServerId) -> bool { self.language_servers().any(|l| l.id() == id) } @@ -1767,7 +1763,7 @@ pub fn lsp_diagnostic_to_diagnostic( text: &Rope, language_config: Option<&LanguageConfiguration>, diagnostic: &helix_lsp::lsp::Diagnostic, - language_server_id: usize, + language_server_id: LanguageServerId, offset_encoding: helix_lsp::OffsetEncoding, ) -> Option { use helix_core::diagnostic::{Range, Severity::*}; @@ -1844,7 +1840,7 @@ pub fn lsp_diagnostic_to_diagnostic( tags, source: diagnostic.source.clone(), data: diagnostic.data.clone(), - language_server_id, + provider: language_server_id, }) } @@ -1857,13 +1853,13 @@ pub fn replace_diagnostics( &mut self, diagnostics: impl IntoIterator, unchanged_sources: &[String], - language_server_id: Option, + language_server_id: Option, ) { if unchanged_sources.is_empty() { self.clear_diagnostics(language_server_id); } else { self.diagnostics.retain(|d| { - if language_server_id.map_or(false, |id| id != d.language_server_id) { + if language_server_id.map_or(false, |id| id != d.provider) { return true; } @@ -1876,18 +1872,14 @@ pub fn replace_diagnostics( } self.diagnostics.extend(diagnostics); self.diagnostics.sort_unstable_by_key(|diagnostic| { - ( - diagnostic.range, - diagnostic.severity, - diagnostic.language_server_id, - ) + (diagnostic.range, diagnostic.severity, diagnostic.provider) }); } /// 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) { + pub fn clear_diagnostics(&mut self, language_server_id: Option) { if let Some(id) = language_server_id { - self.diagnostics.retain(|d| d.language_server_id != id); + self.diagnostics.retain(|d| d.provider != id); } else { self.diagnostics.clear(); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index d7058d3ef..2e04037a8 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -16,7 +16,7 @@ use futures_util::stream::select_all::SelectAll; use futures_util::{future, StreamExt}; -use helix_lsp::Call; +use helix_lsp::{Call, LanguageServerId}; use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ @@ -960,7 +960,7 @@ pub struct Editor { pub macro_recording: Option<(char, Vec)>, pub macro_replaying: Vec, pub language_servers: helix_lsp::Registry, - pub diagnostics: BTreeMap>, + pub diagnostics: BTreeMap>, pub diff_providers: DiffProviderRegistry, pub debugger: Option, @@ -1020,7 +1020,7 @@ pub struct Editor { pub enum EditorEvent { DocumentSaved(DocumentSavedEventResult), ConfigEvent(ConfigEvent), - LanguageServerMessage((usize, Call)), + LanguageServerMessage((LanguageServerId, Call)), DebuggerEvent(dap::Payload), IdleTimer, Redraw, @@ -1260,8 +1260,13 @@ fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) { } #[inline] - pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> { - self.language_servers.get_by_id(language_server_id) + pub fn language_server_by_id( + &self, + language_server_id: LanguageServerId, + ) -> Option<&helix_lsp::Client> { + self.language_servers + .get_by_id(language_server_id) + .map(|client| &**client) } /// Refreshes the language server for a given document @@ -1861,7 +1866,7 @@ pub fn document_by_path_mut>(&mut self, path: P) -> Option<&mut D /// Returns all supported diagnostics for the document pub fn doc_diagnostics<'a>( language_servers: &'a helix_lsp::Registry, - diagnostics: &'a BTreeMap>, + diagnostics: &'a BTreeMap>, document: &Document, ) -> impl Iterator + 'a { Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true) @@ -1871,10 +1876,9 @@ pub fn doc_diagnostics<'a>( /// 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>, - + diagnostics: &'a BTreeMap>, document: &Document, - filter: impl Fn(&lsp::Diagnostic, usize) -> bool + 'a, + filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a, ) -> impl Iterator + 'a { let text = document.text().clone(); let language_config = document.language.clone(); diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index ebdac9e23..36f719f79 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -71,7 +71,7 @@ pub fn diagnostic<'doc>( d.line == line && doc .language_servers_with_feature(LanguageServerFeature::Diagnostics) - .any(|ls| ls.id() == d.language_server_id) + .any(|ls| ls.id() == d.provider) }); diagnostics_on_line.max_by_key(|d| d.severity).map(|d| { write!(out, "●").ok(); From 38ee845b052d306b37863526098613483000cc16 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 21 Apr 2024 16:14:39 +0200 Subject: [PATCH 085/126] don't overload LS with completion resolve requests While moving completion resolve to the event system in #9668 we introduced what is essentially a "DOS attack" on slow LSPs. Completion resolve requests were made in the render loop and debounced with a timeout. Once the timeout expired the resolve request was made. The problem is the next frame would immediately request a new completion resolve request (and mark the old one as obsolete but because LSP has no notion of cancelation the server would still process it). So we were in essence sending one completion request to the server every 150ms and only stopped if the server managed to respond before we rendered a new frame. This caused overload on slower machines/with slower LS. In this PR I revamped the resolve handler so that a request is only ever resolved once. Both by checking if a request is already in-flight and by marking failed resolve requests as resolved. --- helix-lsp/src/client.rs | 38 +++-- helix-term/src/handlers.rs | 2 +- helix-term/src/handlers/completion.rs | 2 + helix-term/src/handlers/completion/resolve.rs | 153 ++++++++++++++++++ helix-term/src/ui/completion.rs | 115 +++---------- helix-term/src/ui/menu.rs | 4 +- 6 files changed, 194 insertions(+), 120 deletions(-) create mode 100644 helix-term/src/handlers/completion/resolve.rs diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 79d8adb31..254625a3a 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -397,6 +397,16 @@ fn call( &self, params: R::Params, ) -> impl Future> + where + R::Params: serde::Serialize, + { + self.call_with_ref::(¶ms) + } + + fn call_with_ref( + &self, + params: &R::Params, + ) -> impl Future> where R::Params: serde::Serialize, { @@ -405,7 +415,7 @@ fn call( fn call_with_timeout( &self, - params: R::Params, + params: &R::Params, timeout_secs: u64, ) -> impl Future> where @@ -414,17 +424,16 @@ fn call_with_timeout( let server_tx = self.server_tx.clone(); let id = self.next_request_id(); + let params = serde_json::to_value(params); async move { use std::time::Duration; use tokio::time::timeout; - let params = serde_json::to_value(params)?; - let request = jsonrpc::MethodCall { jsonrpc: Some(jsonrpc::Version::V2), id: id.clone(), method: R::METHOD.to_string(), - params: Self::value_into_params(params), + params: Self::value_into_params(params?), }; let (tx, mut rx) = channel::>(1); @@ -741,7 +750,7 @@ pub fn will_rename( new_uri: url_from_path(new_path)?, }]; let request = self.call_with_timeout::( - lsp::RenameFilesParams { files }, + &lsp::RenameFilesParams { files }, 5, ); @@ -1026,21 +1035,10 @@ pub fn completion( pub fn resolve_completion_item( &self, - completion_item: lsp::CompletionItem, - ) -> Option>> { - let capabilities = self.capabilities.get().unwrap(); - - // Return early if the server does not support resolving completion items. - match capabilities.completion_provider { - Some(lsp::CompletionOptions { - resolve_provider: Some(true), - .. - }) => (), - _ => return None, - } - - let res = self.call::(completion_item); - Some(async move { Ok(serde_json::from_value(res.await?)?) }) + completion_item: &lsp::CompletionItem, + ) -> impl Future> { + let res = self.call_with_ref::(completion_item); + async move { Ok(serde_json::from_value(res.await?)?) } } pub fn resolve_code_action( diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index 1b7d9b8c0..d45809d36 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -11,7 +11,7 @@ pub use completion::trigger_auto_completion; pub use helix_view::handlers::Handlers; -mod completion; +pub mod completion; mod signature_help; pub fn setup(config: Arc>) -> Handlers { diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs index 8bd85ef6d..68956c85f 100644 --- a/helix-term/src/handlers/completion.rs +++ b/helix-term/src/handlers/completion.rs @@ -30,6 +30,8 @@ use crate::ui::{self, CompletionItem, Popup}; use super::Handlers; +pub use resolve::ResolveHandler; +mod resolve; #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum TriggerKind { diff --git a/helix-term/src/handlers/completion/resolve.rs b/helix-term/src/handlers/completion/resolve.rs new file mode 100644 index 000000000..fb5179e13 --- /dev/null +++ b/helix-term/src/handlers/completion/resolve.rs @@ -0,0 +1,153 @@ +use std::sync::Arc; + +use helix_lsp::lsp; +use tokio::sync::mpsc::Sender; +use tokio::time::{Duration, Instant}; + +use helix_event::{send_blocking, AsyncHook, CancelRx}; +use helix_view::Editor; + +use crate::handlers::completion::CompletionItem; +use crate::job; + +/// A hook for resolving incomplete completion items. +/// +/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion): +/// +/// > If computing full completion items is expensive, servers can additionally provide a +/// > handler for the completion item resolve request. ... +/// > A typical use case is for example: the `textDocument/completion` request doesn't fill +/// > in the `documentation` property for returned completion items since it is expensive +/// > to compute. When the item is selected in the user interface then a +/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter. +/// > The returned completion item should have the documentation property filled in. +pub struct ResolveHandler { + last_request: Option>, + resolver: Sender, +} + +impl ResolveHandler { + pub fn new() -> ResolveHandler { + ResolveHandler { + last_request: None, + resolver: ResolveTimeout { + next_request: None, + in_flight: None, + } + .spawn(), + } + } + + pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut CompletionItem) { + if item.resolved { + return; + } + let needs_resolve = item.item.documentation.is_none() + || item.item.detail.is_none() + || item.item.additional_text_edits.is_none(); + if !needs_resolve { + item.resolved = true; + return; + } + if self.last_request.as_deref().is_some_and(|it| it == item) { + return; + } + let Some(ls) = editor.language_servers.get_by_id(item.provider).cloned() else { + item.resolved = true; + return; + }; + if matches!( + ls.capabilities().completion_provider, + Some(lsp::CompletionOptions { + resolve_provider: Some(true), + .. + }) + ) { + let item = Arc::new(item.clone()); + self.last_request = Some(item.clone()); + send_blocking(&self.resolver, ResolveRequest { item, ls }) + } else { + item.resolved = true; + } + } +} + +struct ResolveRequest { + item: Arc, + ls: Arc, +} + +#[derive(Default)] +struct ResolveTimeout { + next_request: Option, + in_flight: Option<(helix_event::CancelTx, Arc)>, +} + +impl AsyncHook for ResolveTimeout { + type Event = ResolveRequest; + + fn handle_event( + &mut self, + request: Self::Event, + timeout: Option, + ) -> Option { + if self + .next_request + .as_ref() + .is_some_and(|old_request| old_request.item == request.item) + { + timeout + } else if self + .in_flight + .as_ref() + .is_some_and(|(_, old_request)| old_request.item == request.item.item) + { + self.next_request = None; + None + } else { + self.next_request = Some(request); + Some(Instant::now() + Duration::from_millis(150)) + } + } + + fn finish_debounce(&mut self) { + let Some(request) = self.next_request.take() else { return }; + let (tx, rx) = helix_event::cancelation(); + self.in_flight = Some((tx, request.item.clone())); + tokio::spawn(request.execute(rx)); + } +} + +impl ResolveRequest { + async fn execute(self, cancel: CancelRx) { + let future = self.ls.resolve_completion_item(&self.item.item); + let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else { + return; + }; + job::dispatch(move |_, compositor| { + if let Some(completion) = &mut compositor + .find::() + .unwrap() + .completion + { + let resolved_item = match resolved_item { + Ok(item) => CompletionItem { + item, + resolved: true, + ..*self.item + }, + Err(err) => { + log::error!("completion resolve request failed: {err}"); + // set item to resolved so we don't request it again + // we could also remove it but that oculd be odd ui + let mut item = (*self.item).clone(); + item.resolved = true; + item + } + }; + completion.replace_item(&self.item, resolved_item); + }; + }) + .await + } +} diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 2b3437e11..7a08c90ce 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,9 +1,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, - handlers::trigger_auto_completion, - job, + handlers::{completion::ResolveHandler, trigger_auto_completion}, }; -use helix_event::AsyncHook; use helix_view::{ document::SavePoint, editor::CompleteAction, @@ -12,17 +10,16 @@ theme::{Modifier, Style}, ViewId, }; -use tokio::time::Instant; use tui::{buffer::Buffer as Surface, text::Span}; -use std::{borrow::Cow, sync::Arc, time::Duration}; +use std::{borrow::Cow, sync::Arc}; use helix_core::{chars, Change, Transaction}; use helix_view::{graphics::Rect, Document, Editor}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; -use helix_lsp::{lsp, util, OffsetEncoding}; +use helix_lsp::{lsp, util, LanguageServerId, OffsetEncoding}; impl menu::Item for CompletionItem { type Data = (); @@ -104,7 +101,7 @@ pub struct Completion { #[allow(dead_code)] trigger_offset: usize, filter: String, - resolve_handler: tokio::sync::mpsc::Sender, + resolve_handler: ResolveHandler, } impl Completion { @@ -365,7 +362,7 @@ macro_rules! language_server { // TODO: expand nucleo api to allow moving straight to a Utf32String here // and avoid allocation during matching filter: String::from(fragment), - resolve_handler: ResolveHandler::default().spawn(), + resolve_handler: ResolveHandler::new(), }; // need to recompute immediately in case start_offset != trigger_offset @@ -383,7 +380,16 @@ fn resolve_completion_item( language_server: &helix_lsp::Client, completion_item: lsp::CompletionItem, ) -> Option { - let future = language_server.resolve_completion_item(completion_item)?; + if !matches!( + language_server.capabilities().completion_provider, + Some(lsp::CompletionOptions { + resolve_provider: Some(true), + .. + }) + ) { + return None; + } + let future = language_server.resolve_completion_item(&completion_item); let response = helix_lsp::block_on(future); match response { Ok(item) => Some(item), @@ -416,7 +422,7 @@ pub fn is_empty(&self) -> bool { self.popup.contents().is_empty() } - fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) { + pub fn replace_item(&mut self, old_item: &CompletionItem, new_item: CompletionItem) { self.popup.contents_mut().replace_option(old_item, new_item); } @@ -438,12 +444,12 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { self.popup.render(area, surface, cx); // if we have a selection, render a markdown popup on top/below with info - let option = match self.popup.contents().selection() { + let option = match self.popup.contents_mut().selection_mut() { Some(option) => option, None => return, }; if !option.resolved { - helix_event::send_blocking(&self.resolve_handler, option.clone()); + self.resolve_handler.ensure_item_resolved(cx.editor, option); } // need to render: // option.detail @@ -541,88 +547,3 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { markdown_doc.render(doc_area, surface, cx); } } - -/// A hook for resolving incomplete completion items. -/// -/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion): -/// -/// > If computing full completion items is expensive, servers can additionally provide a -/// > handler for the completion item resolve request. ... -/// > A typical use case is for example: the `textDocument/completion` request doesn't fill -/// > in the `documentation` property for returned completion items since it is expensive -/// > to compute. When the item is selected in the user interface then a -/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter. -/// > The returned completion item should have the documentation property filled in. -#[derive(Debug, Default)] -struct ResolveHandler { - trigger: Option, - request: Option, -} - -impl AsyncHook for ResolveHandler { - type Event = CompletionItem; - - fn handle_event( - &mut self, - item: Self::Event, - timeout: Option, - ) -> Option { - if self - .trigger - .as_ref() - .is_some_and(|trigger| trigger == &item) - { - timeout - } else { - self.trigger = Some(item); - self.request = None; - Some(Instant::now() + Duration::from_millis(150)) - } - } - - fn finish_debounce(&mut self) { - let Some(item) = self.trigger.take() else { return }; - let (tx, rx) = helix_event::cancelation(); - self.request = Some(tx); - job::dispatch_blocking(move |editor, _| resolve_completion_item(editor, item, rx)) - } -} - -fn resolve_completion_item( - editor: &mut Editor, - item: CompletionItem, - cancel: helix_event::CancelRx, -) { - let Some(language_server) = editor.language_server_by_id(item.language_server_id) else { - return; - }; - - let Some(future) = language_server.resolve_completion_item(item.item.clone()) else { - return; - }; - - tokio::spawn(async move { - match helix_event::cancelable_future(future, cancel).await { - Some(Ok(resolved_item)) => { - job::dispatch(move |_, compositor| { - if let Some(completion) = &mut compositor - .find::() - .unwrap() - .completion - { - let resolved_item = CompletionItem { - item: resolved_item, - language_server_id: item.language_server_id, - resolved: true, - }; - - completion.replace_item(item, resolved_item); - }; - }) - .await - } - Some(Err(err)) => log::error!("completion resolve request failed: {err}"), - None => (), - } - }); -} diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index c0e60b33e..f9f038e7a 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -241,9 +241,9 @@ pub fn len(&self) -> usize { } impl Menu { - pub fn replace_option(&mut self, old_option: T, new_option: T) { + pub fn replace_option(&mut self, old_option: &T, new_option: T) { for option in &mut self.options { - if old_option == *option { + if old_option == option { *option = new_option; break; } From e18b772654a51afd5c840e32456cb71640c3aa10 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Sun, 21 Apr 2024 23:53:31 -0400 Subject: [PATCH 086/126] Remove kirawi from `dark_plus` maintainer list (#10543) --- runtime/themes/dark_plus.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 11be7fd6a..ccb8bed96 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -1,4 +1,3 @@ -# Author: Shafkath Shuhan "namespace" = { fg = "type" } "module" = { fg = "type" } From 89a9f2be788017615f6d2406d4e12f50949a5492 Mon Sep 17 00:00:00 2001 From: Krishan <54745129+krish-r@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:11:03 +0530 Subject: [PATCH 087/126] specify direction for select_prev_sibling and select_next_sibling (#10542) * specify direction for select_prev_sibling and select_next_sibling * fix failing integration-test --- helix-core/src/object.rs | 53 +++++++++----- helix-term/tests/test/commands/movement.rs | 80 ++++++++++++++++++++++ helix-term/tests/test/movement.rs | 2 +- 3 files changed, 117 insertions(+), 18 deletions(-) diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 28629235f..17a393caf 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,4 +1,4 @@ -use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; +use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax}; pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { let cursor = &mut syntax.walk(); @@ -25,19 +25,31 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) } pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { - select_node_impl(syntax, text, selection, |cursor| { - cursor.goto_first_child(); - }) + select_node_impl( + syntax, + text, + selection, + |cursor| { + cursor.goto_first_child(); + }, + None, + ) } pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { - select_node_impl(syntax, text, selection, |cursor| { - while !cursor.goto_next_sibling() { - if !cursor.goto_parent() { - break; + select_node_impl( + syntax, + text, + selection, + |cursor| { + while !cursor.goto_next_sibling() { + if !cursor.goto_parent() { + break; + } } - } - }) + }, + Some(Direction::Forward), + ) } pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { @@ -81,13 +93,19 @@ fn select_children<'n>( } pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { - select_node_impl(syntax, text, selection, |cursor| { - while !cursor.goto_prev_sibling() { - if !cursor.goto_parent() { - break; + select_node_impl( + syntax, + text, + selection, + |cursor| { + while !cursor.goto_prev_sibling() { + if !cursor.goto_parent() { + break; + } } - } - }) + }, + Some(Direction::Backward), + ) } fn select_node_impl( @@ -95,6 +113,7 @@ fn select_node_impl( text: RopeSlice, selection: Selection, motion: F, + direction: Option, ) -> Selection where F: Fn(&mut TreeCursor), @@ -113,6 +132,6 @@ fn select_node_impl( let from = text.byte_to_char(node.start_byte()); let to = text.byte_to_char(node.end_byte()); - Range::new(from, to).with_direction(range.direction()) + Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction())) }) } diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 1f33b3944..b2471f635 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -726,3 +726,83 @@ async fn select_all_children() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_select_next_sibling() -> anyhow::Result<()> { + let tests = vec![ + // basic test + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 #[}|]# + fn dec(x: usize) -> usize { x - 1 } + fn ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + #[fn dec(x: usize) -> usize { x - 1 }|]# + fn ident(x: usize) -> usize { x } + "##}, + ), + // direction is not preserved and is always forward. + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 #[}|]# + fn dec(x: usize) -> usize { x - 1 } + fn ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + fn dec(x: usize) -> usize { x - 1 } + #[fn ident(x: usize) -> usize { x }|]# + "##}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_select_prev_sibling() -> anyhow::Result<()> { + let tests = vec![ + // basic test + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + fn dec(x: usize) -> usize { x - 1 } + #[|f]#n ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + #[|fn dec(x: usize) -> usize { x - 1 }]# + fn ident(x: usize) -> usize { x } + "##}, + ), + // direction is not preserved and is always backward. + ( + indoc! {r##" + fn inc(x: usize) -> usize { x + 1 } + fn dec(x: usize) -> usize { x - 1 } + #[|f]#n ident(x: usize) -> usize { x } + "##}, + "", + indoc! {r##" + #[|fn inc(x: usize) -> usize { x + 1 }]# + fn dec(x: usize) -> usize { x - 1 } + fn ident(x: usize) -> usize { x } + "##}, + ), + ]; + + for test in tests { + test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?; + } + + Ok(()) +} diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 9ecaf6bb0..3fa850929 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -666,7 +666,7 @@ async fn tree_sitter_motions_work_across_injections() -> anyhow::Result<()> { ( "", "", - "", + "", ), ) .await?; From 22960e0d7012a3956949df482bb05a2203e18926 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:40:04 +0100 Subject: [PATCH 088/126] Refactor Dark Plus and add new maintainer (#10574) * Make dark_plus.toml more accurate to VSCode * theme(dark_plus): make type.builtin blue * Refactor dark_plus and add myself as new maintainer Co-authored-by: NAME --------- Co-authored-by: Luca Saccarola <96259932+saccarosium@users.noreply.github.com> Co-authored-by: Luca Saccarola Co-authored-by: NAME --- runtime/themes/dark_plus.toml | 177 ++++++++++++++++------------------ 1 file changed, 81 insertions(+), 96 deletions(-) diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index ccb8bed96..c61a0347f 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -1,105 +1,90 @@ +# Author: David Else <12832280+David-Else@users.noreply.github.com> -"namespace" = { fg = "type" } -"module" = { fg = "type" } +# SYNTAX +"attribute" = "fn_declaration" +"comment" = "dark_green" +"constant" = "constant" +"constant.builtin" = "blue2" +"constant.character" = "orange" +"constant.character.escape" = "gold" +"constant.numeric" = "pale_green" +"constructor" = "type" +"diff.delta" = "blue4" +"diff.minus" = "orange_red" +"diff.plus" = "dark_green2" +"function" = "fn_declaration" +"function.builtin" = "fn_declaration" +"function.macro" = "blue2" +"keyword" = "blue2" +"keyword.control" = "special" +"keyword.directive" = "special" +"label" = "blue2" +"module" = "type" +"namespace" = "type" +"operator" = "text" +"punctuation" = "text" +"punctuation.delimiter" = "text" +"special" = "text" +"string" = "orange" +"string.regexp" = "gold" +"tag" = "blue2" +"type" = "type" +"type.builtin" = "type" +"type.enum.variant" = "constant" +"variable" = "variable" +"variable.builtin" = "blue2" +"variable.other.member" = "variable" +"variable.parameter" = "variable" -"type" = { fg = "type" } -"type.builtin" = { fg = "type" } -"type.enum.variant" = { fg = "constant" } -"constructor" = { fg = "type" } -"variable.other.member" = { fg = "variable" } - -"keyword" = { fg = "blue2" } -"keyword.directive" = { fg = "blue2" } -"keyword.control" = { fg = "special" } -"label" = { fg = "blue2" } -"tag" = "blue2" - -"special" = { fg = "text" } -"operator" = { fg = "text" } - -"punctuation" = { fg = "text" } -"punctuation.delimiter" = { fg = "text" } - -"variable" = { fg = "variable" } -"variable.parameter" = { fg = "variable" } -"variable.builtin" = { fg = "blue2" } -"constant" = { fg = "constant" } -"constant.builtin" = { fg = "blue2" } - -"function" = { fg = "fn_declaration" } -"function.builtin" = { fg = "fn_declaration" } -"function.macro" = { fg = "blue2" } -"attribute" = { fg = "fn_declaration" } - -"comment" = { fg = "dark_green" } - -"string" = { fg = "orange" } -"constant.character" = { fg = "orange" } -"string.regexp" = { fg = "gold" } -"constant.numeric" = { fg = "pale_green" } -"constant.character.escape" = { fg = "gold" } - -"markup.heading" = { fg = "blue2", modifiers = ["bold"] } -"markup.list" = "blue3" -"markup.bold" = { fg = "blue2", modifiers = ["bold"] } -"markup.italic" = { modifiers = ["italic"] } +# MARKUP +"markup.heading" = { fg = "blue2", modifiers = ["bold"] } +"markup.list" = "blue3" +"markup.bold" = { fg = "blue2", modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { modifiers = ["underlined"] } -"markup.link.text" = "orange" -"markup.quote" = "dark_green" -"markup.raw" = "orange" - -"diff.plus" = { fg = "dark_green2" } -"diff.delta" = { fg = "blue4" } -"diff.minus" = { fg = "orange_red" } - -"ui.background" = { fg = "light_gray", bg = "dark_gray2" } - -"ui.window" = { bg = "widget" } -"ui.popup" = { fg = "text", bg = "widget" } -"ui.help" = { fg = "text", bg = "widget" } -"ui.menu" = { fg = "text", bg = "widget" } -"ui.menu.selected" = { bg = "dark_blue2" } +"markup.link.url" = { modifiers = ["underlined"] } +"markup.link.text" = "orange" +"markup.quote" = "dark_green" +"markup.raw" = "orange" +# UI +"ui.background" = { fg = "light_gray", bg = "dark_gray2" } +"ui.window" = { bg = "widget" } +"ui.popup" = { fg = "text", bg = "widget" } +"ui.help" = { fg = "text", bg = "widget" } +"ui.menu" = { fg = "text", bg = "widget" } +"ui.menu.selected" = { bg = "dark_blue2" } # TODO: Alternate bg colour for `ui.cursor.match` and `ui.selection`. -"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] } -"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] } -"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] } - -"ui.selection" = { bg = "#3a3d41" } -"ui.selection.primary" = { bg = "dark_blue" } - -"ui.linenr" = { fg = "dark_gray" } -"ui.linenr.selected" = { fg = "light_gray2" } - -"ui.cursorline.primary" = { bg = "dark_gray3" } -"ui.statusline" = { fg = "white", bg = "blue" } -"ui.statusline.inactive" = { fg = "white", bg = "blue" } -"ui.statusline.insert" = { fg = "white", bg = "yellow" } -"ui.statusline.select" = { fg = "white", bg = "magenta" } - -"ui.bufferline" = { fg = "text", bg = "widget" } -"ui.bufferline.active" = { fg = "white", bg = "blue" } -"ui.bufferline.background" = { bg = "background" } - -"ui.text" = { fg = "text" } -"ui.text.focus" = { fg = "white" } - -"ui.virtual.whitespace" = { fg = "dark_gray" } -"ui.virtual.ruler" = { bg = "borders" } -"ui.virtual.indent-guide" = { fg = "dark_gray4" } -"ui.virtual.inlay-hint" = { fg = "dark_gray5"} -"ui.virtual.jump-label" = { fg = "dark_gray", modifiers = ["bold"] } - -"warning" = { fg = "gold2" } -"error" = { fg = "red" } -"info" = { fg = "light_blue" } -"hint" = { fg = "light_gray3" } - +"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] } +"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] } +"ui.selection" = { bg = "#3a3d41" } +"ui.selection.primary" = { bg = "dark_blue" } +"ui.linenr" = { fg = "dark_gray" } +"ui.linenr.selected" = { fg = "light_gray2" } +"ui.cursorline.primary" = { bg = "dark_gray3" } +"ui.statusline" = { fg = "white", bg = "blue" } +"ui.statusline.inactive" = { fg = "white", bg = "widget" } +"ui.statusline.insert" = { fg = "white", bg = "yellow" } +"ui.statusline.select" = { fg = "white", bg = "magenta" } +"ui.bufferline" = { fg = "text", bg = "widget" } +"ui.bufferline.active" = { fg = "white", bg = "blue" } +"ui.bufferline.background" = { bg = "background" } +"ui.text" = { fg = "text" } +"ui.text.focus" = { fg = "white" } +"ui.virtual.whitespace" = { fg = "#3e3e3d" } +"ui.virtual.ruler" = { bg = "borders" } +"ui.virtual.indent-guide" = { fg = "dark_gray4" } +"ui.virtual.inlay-hint" = { fg = "dark_gray5"} +"ui.virtual.jump-label" = { fg = "dark_gray", modifiers = ["bold"] } +"warning" = { fg = "gold2" } +"error" = { fg = "red" } +"info" = { fg = "light_blue" } +"hint" = { fg = "light_gray3" } "diagnostic.error".underline = { color = "red", style = "curl" } -"diagnostic".underline = { color = "gold", style = "curl" } -"diagnostic.unnecessary" = { modifiers = ["dim"] } -"diagnostic.deprecated" = { modifiers = ["crossed_out"] } +"diagnostic".underline = { color = "gold", style = "curl" } +"diagnostic.unnecessary" = { modifiers = ["dim"] } +"diagnostic.deprecated" = { modifiers = ["crossed_out"] } [palette] white = "#ffffff" From 50c90cb47c9cdbb044d1a2de034285e0d198f43e Mon Sep 17 00:00:00 2001 From: Yoav Lavi Date: Wed, 24 Apr 2024 15:06:19 +0300 Subject: [PATCH 089/126] Add support for highlighting any `.*ignore` file (#10579) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index fb74a8a14..1b2db762b 100644 --- a/languages.toml +++ b/languages.toml @@ -1643,7 +1643,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev = [[language]] name = "git-ignore" scope = "source.gitignore" -file-types = [{ glob = ".gitignore" }, { glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = ".prettierignore" }, { glob = ".eslintignore" }, { glob = ".npmignore"}, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }] +file-types = [{ glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }, { glob = ".*ignore" }] injection-regex = "git-ignore" comment-token = "#" grammar = "gitignore" From 81dc8e8d6b1476a7d522058ee0b4454115913ab5 Mon Sep 17 00:00:00 2001 From: woojiq Date: Tue, 9 Apr 2024 18:29:00 +0300 Subject: [PATCH 090/126] feat: find closest pair using tree-sitter --- helix-core/src/match_brackets.rs | 98 +++++++++++++++++++-------- helix-core/src/selection.rs | 2 +- helix-core/src/surround.rs | 111 +++++++++++++++++++++---------- helix-core/src/textobject.rs | 12 ++-- helix-term/src/commands.rs | 53 +++++++++------ 5 files changed, 189 insertions(+), 87 deletions(-) diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index b8bcc28ca..95d6a3dc4 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -9,16 +9,32 @@ const MAX_PLAINTEXT_SCAN: usize = 10000; const MATCH_LIMIT: usize = 16; -// Limit matching pairs to only ( ) { } [ ] < > ' ' " " -const PAIRS: &[(char, char)] = &[ +pub const BRACKETS: [(char, char); 7] = [ ('(', ')'), ('{', '}'), ('[', ']'), ('<', '>'), - ('\'', '\''), - ('\"', '\"'), + ('«', '»'), + ('「', '」'), + ('(', ')'), ]; +// The difference between BRACKETS and PAIRS is that we can find matching +// BRACKETS in a plain text file, but we can't do the same for PAIRs. +// PAIRS also contains all BRACKETS. +pub const PAIRS: [(char, char); BRACKETS.len() + 3] = { + let mut pairs = [(' ', ' '); BRACKETS.len() + 3]; + let mut idx = 0; + while idx < BRACKETS.len() { + pairs[idx] = BRACKETS[idx]; + idx += 1; + } + pairs[idx] = ('"', '"'); + pairs[idx + 1] = ('\'', '\''); + pairs[idx + 2] = ('`', '`'); + pairs +}; + /// Returns the position of the matching bracket under cursor. /// /// If the cursor is on the opening bracket, the position of @@ -30,7 +46,7 @@ /// If no matching bracket is found, `None` is returned. #[must_use] pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option { - if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) { + if pos >= doc.len_chars() || !is_valid_pair(doc.char(pos)) { return None; } find_pair(syntax, doc, pos, false) @@ -67,7 +83,7 @@ fn find_pair( let (start_byte, end_byte) = surrounding_bytes(doc, &node)?; let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte)); - if is_valid_pair(doc, start_char, end_char) { + if is_valid_pair_on_pos(doc, start_char, end_char) { if end_byte == pos { return Some(start_char); } @@ -140,14 +156,22 @@ fn find_pair( /// If no matching bracket is found, `None` is returned. #[must_use] pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option { - // Don't do anything when the cursor is not on top of a bracket. let bracket = doc.get_char(cursor_pos)?; + let matching_bracket = { + let pair = get_pair(bracket); + if pair.0 == bracket { + pair.1 + } else { + pair.0 + } + }; + // Don't do anything when the cursor is not on top of a bracket. if !is_valid_bracket(bracket) { return None; } // Determine the direction of the matching. - let is_fwd = is_forward_bracket(bracket); + let is_fwd = is_open_bracket(bracket); let chars_iter = if is_fwd { doc.chars_at(cursor_pos + 1) } else { @@ -159,19 +183,7 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() { if candidate == bracket { open_cnt += 1; - } else if is_valid_pair( - doc, - if is_fwd { - cursor_pos - } else { - cursor_pos - i - 1 - }, - if is_fwd { - cursor_pos + i + 1 - } else { - cursor_pos - }, - ) { + } else if candidate == matching_bracket { // Return when all pending brackets have been closed. if open_cnt == 1 { return Some(if is_fwd { @@ -187,15 +199,49 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt None } -fn is_valid_bracket(c: char) -> bool { - PAIRS.iter().any(|(l, r)| *l == c || *r == c) +/// Returns the open and closing chars pair. If not found in +/// [`BRACKETS`] returns (ch, ch). +/// +/// ``` +/// use helix_core::match_brackets::get_pair; +/// +/// assert_eq!(get_pair('['), ('[', ']')); +/// assert_eq!(get_pair('}'), ('{', '}')); +/// assert_eq!(get_pair('"'), ('"', '"')); +/// ``` +pub fn get_pair(ch: char) -> (char, char) { + PAIRS + .iter() + .find(|(open, close)| *open == ch || *close == ch) + .copied() + .unwrap_or((ch, ch)) } -fn is_forward_bracket(c: char) -> bool { - PAIRS.iter().any(|(l, _)| *l == c) +pub fn is_open_bracket(ch: char) -> bool { + BRACKETS.iter().any(|(l, _)| *l == ch) } -fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool { +pub fn is_close_bracket(ch: char) -> bool { + BRACKETS.iter().any(|(_, r)| *r == ch) +} + +pub fn is_valid_bracket(ch: char) -> bool { + BRACKETS.iter().any(|(l, r)| *l == ch || *r == ch) +} + +pub fn is_open_pair(ch: char) -> bool { + PAIRS.iter().any(|(l, _)| *l == ch) +} + +pub fn is_close_pair(ch: char) -> bool { + PAIRS.iter().any(|(_, r)| *r == ch) +} + +pub fn is_valid_pair(ch: char) -> bool { + PAIRS.iter().any(|(l, r)| *l == ch || *r == ch) +} + +fn is_valid_pair_on_pos(doc: RopeSlice, start_char: usize, end_char: usize) -> bool { PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 652612872..48eaf289c 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -122,7 +122,7 @@ pub fn is_empty(&self) -> bool { } /// `Direction::Backward` when head < anchor. - /// `Direction::Backward` otherwise. + /// `Direction::Forward` otherwise. #[inline] #[must_use] pub fn direction(&self) -> Direction { diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index ed9764883..879c2adf1 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -1,18 +1,16 @@ use std::fmt::Display; -use crate::{movement::Direction, search, Range, Selection}; +use crate::{ + graphemes::next_grapheme_boundary, + match_brackets::{ + find_matching_bracket, find_matching_bracket_fuzzy, get_pair, is_close_bracket, + is_open_bracket, + }, + movement::Direction, + search, Range, Selection, Syntax, +}; use ropey::RopeSlice; -pub const PAIRS: &[(char, char)] = &[ - ('(', ')'), - ('[', ']'), - ('{', '}'), - ('<', '>'), - ('«', '»'), - ('「', '」'), - ('(', ')'), -]; - #[derive(Debug, PartialEq, Eq)] pub enum Error { PairNotFound, @@ -34,32 +32,68 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { type Result = std::result::Result; -/// Given any char in [PAIRS], return the open and closing chars. If not found in -/// [PAIRS] return (ch, ch). +/// Finds the position of surround pairs of any [`crate::match_brackets::PAIRS`] +/// using tree-sitter when possible. /// -/// ``` -/// use helix_core::surround::get_pair; +/// # Returns /// -/// assert_eq!(get_pair('['), ('[', ']')); -/// assert_eq!(get_pair('}'), ('{', '}')); -/// assert_eq!(get_pair('"'), ('"', '"')); -/// ``` -pub fn get_pair(ch: char) -> (char, char) { - PAIRS - .iter() - .find(|(open, close)| *open == ch || *close == ch) - .copied() - .unwrap_or((ch, ch)) +/// Tuple `(anchor, head)`, meaning it is not always ordered. +pub fn find_nth_closest_pairs_pos( + syntax: Option<&Syntax>, + text: RopeSlice, + range: Range, + skip: usize, +) -> Result<(usize, usize)> { + match syntax { + Some(syntax) => find_nth_closest_pairs_ts(syntax, text, range, skip), + None => find_nth_closest_pairs_plain(text, range, skip), + } } -pub fn find_nth_closest_pairs_pos( +fn find_nth_closest_pairs_ts( + syntax: &Syntax, text: RopeSlice, range: Range, mut skip: usize, ) -> Result<(usize, usize)> { - let is_open_pair = |ch| PAIRS.iter().any(|(open, _)| *open == ch); - let is_close_pair = |ch| PAIRS.iter().any(|(_, close)| *close == ch); + let mut opening = range.from(); + // We want to expand the selection if we are already on the found pair, + // otherwise we would need to subtract "-1" from "range.to()". + let mut closing = range.to(); + while skip > 0 { + closing = find_matching_bracket_fuzzy(syntax, text, closing).ok_or(Error::PairNotFound)?; + opening = find_matching_bracket(syntax, text, closing).ok_or(Error::PairNotFound)?; + // If we're already on a closing bracket "find_matching_bracket_fuzzy" will return + // the position of the opening bracket. + if closing < opening { + (opening, closing) = (closing, opening); + } + + // In case found brackets are partially inside current selection. + if range.from() < opening || closing < range.to() - 1 { + closing = next_grapheme_boundary(text, closing); + } else { + skip -= 1; + if skip != 0 { + closing = next_grapheme_boundary(text, closing); + } + } + } + + // Keep the original direction. + if let Direction::Forward = range.direction() { + Ok((opening, closing)) + } else { + Ok((closing, opening)) + } +} + +fn find_nth_closest_pairs_plain( + text: RopeSlice, + range: Range, + mut skip: usize, +) -> Result<(usize, usize)> { let mut stack = Vec::with_capacity(2); let pos = range.from(); let mut close_pos = pos.saturating_sub(1); @@ -67,7 +101,7 @@ pub fn find_nth_closest_pairs_pos( for ch in text.chars_at(pos) { close_pos += 1; - if is_open_pair(ch) { + if is_open_bracket(ch) { // Track open pairs encountered so that we can step over // the corresponding close pairs that will come up further // down the loop. We want to find a lone close pair whose @@ -76,7 +110,7 @@ pub fn find_nth_closest_pairs_pos( continue; } - if !is_close_pair(ch) { + if !is_close_bracket(ch) { // We don't care if this character isn't a brace pair item, // so short circuit here. continue; @@ -157,7 +191,11 @@ pub fn find_nth_pairs_pos( ) }; - Option::zip(open, close).ok_or(Error::PairNotFound) + // preserve original direction + match range.direction() { + Direction::Forward => Option::zip(open, close).ok_or(Error::PairNotFound), + Direction::Backward => Option::zip(close, open).ok_or(Error::PairNotFound), + } } fn find_nth_open_pair( @@ -249,6 +287,7 @@ fn find_nth_close_pair( /// are automatically detected around each cursor (note that this may result /// in them selecting different surround characters for each selection). pub fn get_surround_pos( + syntax: Option<&Syntax>, text: RopeSlice, selection: &Selection, ch: Option, @@ -257,9 +296,13 @@ pub fn get_surround_pos( let mut change_pos = Vec::new(); for &range in selection { - let (open_pos, close_pos) = match ch { - Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?, - None => find_nth_closest_pairs_pos(text, range, skip)?, + let (open_pos, close_pos) = { + let range_raw = match ch { + Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?, + None => find_nth_closest_pairs_pos(syntax, text, range, skip)?, + }; + let range = Range::new(range_raw.0, range_raw.1); + (range.from(), range.to()) }; if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) { return Err(Error::CursorOverlap); diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index bf00a4580..412301261 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -7,9 +7,9 @@ use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; use crate::line_ending::rope_is_line_ending; use crate::movement::Direction; -use crate::surround; use crate::syntax::LanguageConfiguration; use crate::Range; +use crate::{surround, Syntax}; fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize { use CharCategory::{Eol, Whitespace}; @@ -199,25 +199,28 @@ pub fn textobject_paragraph( } pub fn textobject_pair_surround( + syntax: Option<&Syntax>, slice: RopeSlice, range: Range, textobject: TextObject, ch: char, count: usize, ) -> Range { - textobject_pair_surround_impl(slice, range, textobject, Some(ch), count) + textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count) } pub fn textobject_pair_surround_closest( + syntax: Option<&Syntax>, slice: RopeSlice, range: Range, textobject: TextObject, count: usize, ) -> Range { - textobject_pair_surround_impl(slice, range, textobject, None, count) + textobject_pair_surround_impl(syntax, slice, range, textobject, None, count) } fn textobject_pair_surround_impl( + syntax: Option<&Syntax>, slice: RopeSlice, range: Range, textobject: TextObject, @@ -226,8 +229,7 @@ fn textobject_pair_surround_impl( ) -> Range { let pair_pos = match ch { Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count), - // Automatically find the closest surround pairs - None => surround::find_nth_closest_pairs_pos(slice, range, count), + None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count), }; pair_pos .map(|(anchor, head)| match textobject { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cc7b84c4b..8610a2048 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5409,13 +5409,22 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { 'e' => textobject_treesitter("entry", range), 'p' => textobject::textobject_paragraph(text, range, objtype, count), 'm' => textobject::textobject_pair_surround_closest( - text, range, objtype, count, + doc.syntax(), + text, + range, + objtype, + count, ), 'g' => textobject_change(range), // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_pair_surround(text, range, objtype, ch, count) - } + ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround( + doc.syntax(), + text, + range, + objtype, + ch, + count, + ), _ => range, } }); @@ -5440,7 +5449,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { ("c", "Comment (tree-sitter)"), ("T", "Test (tree-sitter)"), ("e", "Data structure entry (tree-sitter)"), - ("m", "Closest surrounding pair"), + ("m", "Closest surrounding pair (tree-sitter)"), ("g", "Change"), (" ", "... or any character acting as a pair"), ]; @@ -5454,7 +5463,7 @@ fn surround_add(cx: &mut Context) { // surround_len is the number of new characters being added. let (open, close, surround_len) = match event.char() { Some(ch) => { - let (o, c) = surround::get_pair(ch); + let (o, c) = match_brackets::get_pair(ch); let mut open = Tendril::new(); open.push(o); let mut close = Tendril::new(); @@ -5505,13 +5514,14 @@ fn surround_replace(cx: &mut Context) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - let change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) { - Ok(c) => c, - Err(err) => { - cx.editor.set_error(err.to_string()); - return; - } - }; + let change_pos = + match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) { + Ok(c) => c, + Err(err) => { + cx.editor.set_error(err.to_string()); + return; + } + }; let selection = selection.clone(); let ranges: SmallVec<[Range; 1]> = change_pos.iter().map(|&p| Range::point(p)).collect(); @@ -5526,7 +5536,7 @@ fn surround_replace(cx: &mut Context) { Some(to) => to, None => return doc.set_selection(view.id, selection), }; - let (open, close) = surround::get_pair(to); + let (open, close) = match_brackets::get_pair(to); // the changeset has to be sorted to allow nested surrounds let mut sorted_pos: Vec<(usize, char)> = Vec::new(); @@ -5563,13 +5573,14 @@ fn surround_delete(cx: &mut Context) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - let mut change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) { - Ok(c) => c, - Err(err) => { - cx.editor.set_error(err.to_string()); - return; - } - }; + let mut change_pos = + match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) { + Ok(c) => c, + Err(err) => { + cx.editor.set_error(err.to_string()); + return; + } + }; change_pos.sort_unstable(); // the changeset has to be sorted to allow nested surrounds let transaction = Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None))); From 839ec4ad3934b3c390bfd54a30462789cc90cc5c Mon Sep 17 00:00:00 2001 From: woojiq Date: Tue, 9 Apr 2024 18:29:54 +0300 Subject: [PATCH 091/126] test: match around closest pair tree-sitter version --- helix-core/src/surround.rs | 10 +++--- helix-core/src/textobject.rs | 3 +- helix-term/tests/test/commands.rs | 60 +++++++++++++++++++++++++++++++ helix-term/tests/test/movement.rs | 43 ++++++++++++++++++++++ 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index 879c2adf1..e45346c92 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -331,7 +331,7 @@ fn test_get_surround_pos() { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('('), 1).unwrap(), + get_surround_pos(None, doc.slice(..), &selection, Some('('), 1).unwrap(), expectations ); } @@ -346,7 +346,7 @@ fn test_get_surround_pos_bail_different_surround_chars() { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('('), 1), + get_surround_pos(None, doc.slice(..), &selection, Some('('), 1), Err(Error::PairNotFound) ); } @@ -361,7 +361,7 @@ fn test_get_surround_pos_bail_overlapping_surround_chars() { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('('), 1), + get_surround_pos(None, doc.slice(..), &selection, Some('('), 1), Err(Error::PairNotFound) // overlapping surround chars ); } @@ -376,7 +376,7 @@ fn test_get_surround_pos_bail_cursor_overlap() { ); assert_eq!( - get_surround_pos(doc.slice(..), &selection, Some('['), 1), + get_surround_pos(None, doc.slice(..), &selection, Some('['), 1), Err(Error::CursorOverlap) ); } @@ -440,7 +440,7 @@ fn test_find_nth_closest_pairs_pos_index_range_panic() { ); assert_eq!( - find_nth_closest_pairs_pos(doc.slice(..), selection.primary(), 1), + find_nth_closest_pairs_pos(None, doc.slice(..), selection.primary(), 1), Err(Error::PairNotFound) ) } diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 412301261..7576b3a78 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -576,7 +576,8 @@ fn test_textobject_surround() { let slice = doc.slice(..); for &case in scenario { let (pos, objtype, expected_range, ch, count) = case; - let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count); + let result = + textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count); assert_eq!( result, expected_range.into(), diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 6b904aa30..1c5c6196b 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -664,3 +664,63 @@ async fn test_read_file() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn surround_delete() -> anyhow::Result<()> { + // Test `surround_delete` when head < anchor + test(("(#[| ]#)", "mdm", "#[| ]#")).await?; + test(("(#[| ]#)", "md(", "#[| ]#")).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn surround_replace_ts() -> anyhow::Result<()> { + const INPUT: &str = r#"\ +fn foo() { + if let Some(_) = None { + todo!("f#[|o]#o)"); + } +} +"#; + test(( + INPUT, + ":lang rustmrm'", + r#"\ +fn foo() { + if let Some(_) = None { + todo!('f#[|o]#o)'); + } +} +"#, + )) + .await?; + + test(( + INPUT, + ":lang rust3mrm[", + r#"\ +fn foo() { + if let Some(_) = None [ + todo!("f#[|o]#o)"); + ] +} +"#, + )) + .await?; + + test(( + INPUT, + ":lang rust2mrm{", + r#"\ +fn foo() { + if let Some(_) = None { + todo!{"f#[|o]#o)"}; + } +} +"#, + )) + .await?; + + Ok(()) +} diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 3fa850929..77098a336 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -107,6 +107,14 @@ async fn surround_by_character() -> anyhow::Result<()> { )) .await?; + // Selection direction is preserved + test(( + "(so [many {go#[|od]#} text] here)", + "mi{", + "(so [many {#[|good]#} text] here)", + )) + .await?; + Ok(()) } @@ -366,6 +374,41 @@ async fn surround_around_pair() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn match_around_closest_ts() -> anyhow::Result<()> { + test_with_config( + AppBuilder::new().with_file("foo.rs", None), + ( + r#"fn main() {todo!{"f#[|oo]#)"};}"#, + "mam", + r#"fn main() {todo!{#[|"foo)"]#};}"#, + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.rs", None), + ( + r##"fn main() { let _ = ("#[|1]#23", "#(|1)#23"); } "##, + "3mam", + r##"fn main() #[|{ let _ = ("123", "123"); }]# "##, + ), + ) + .await?; + + test_with_config( + AppBuilder::new().with_file("foo.rs", None), + ( + r##" fn main() { let _ = ("12#[|3", "12]#3"); } "##, + "1mam", + r##" fn main() { let _ = #[|("123", "123")]#; } "##, + ), + ) + .await?; + + Ok(()) +} + /// Ensure the very initial cursor in an opened file is the width of /// the first grapheme #[tokio::test(flavor = "multi_thread")] From 109f53fb60f1b126c4c9afd97dd75972725d04ac Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:48:14 +0100 Subject: [PATCH 092/126] Add debug highlights to the dark plus theme (#10593) --- runtime/themes/dark_plus.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index c61a0347f..77b9b3e7c 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -77,6 +77,9 @@ "ui.virtual.indent-guide" = { fg = "dark_gray4" } "ui.virtual.inlay-hint" = { fg = "dark_gray5"} "ui.virtual.jump-label" = { fg = "dark_gray", modifiers = ["bold"] } +"ui.highlight.frameline" = { bg = "#4b4b18" } +"ui.debug.active" = { fg = "#ffcc00" } +"ui.debug.breakpoint" = { fg = "#e51400" } "warning" = { fg = "gold2" } "error" = { fg = "red" } "info" = { fg = "light_blue" } From 31248d4e2f4779f89ce5db6821ae77eea28ed0f8 Mon Sep 17 00:00:00 2001 From: Keir Lawson Date: Fri, 26 Apr 2024 22:48:23 +0100 Subject: [PATCH 093/126] Enable metals inlay hints (#10597) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 1b2db762b..7e858b056 100644 --- a/languages.toml +++ b/languages.toml @@ -53,7 +53,7 @@ ltex-ls = { command = "ltex-ls" } markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] } markdown-oxide = { command = "markdown-oxide" } marksman = { command = "marksman", args = ["server"] } -metals = { command = "metals", config = { "isHttpEnabled" = true } } +metals = { command = "metals", config = { "isHttpEnabled" = true, metals = { inlayHints = { typeParameters = {enable = true} , hintsInPatternMatch = {enable = true} } } } } mint = { command = "mint", args = ["ls"] } nil = { command = "nil" } nimlangserver = { command = "nimlangserver" } From 5ee7411450021df3854b62f61b8722451e40277c Mon Sep 17 00:00:00 2001 From: Diogenesoftoronto <87236699+Diogenesoftoronto@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:50:29 -0400 Subject: [PATCH 094/126] Change cursor color per mode for default (#10608) --- theme.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/theme.toml b/theme.toml index 83791b0ab..81c1a91d7 100644 --- a/theme.toml +++ b/theme.toml @@ -66,6 +66,8 @@ label = "honey" # TODO: namespace ui.cursor as ui.selection.cursor? "ui.cursor.select" = { bg = "delta" } "ui.cursor.insert" = { bg = "white" } +"ui.cursor.primary.select" = { bg = "delta" } +"ui.cursor.primary.insert" = { bg = "white" } "ui.cursor.match" = { fg = "#212121", bg = "#6C6999" } "ui.cursor" = { modifiers = ["reversed"] } "ui.cursorline.primary" = { bg = "bossanova" } From 918dd3fa3747700c05c1afc22ba16f89efac10a1 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sat, 27 Apr 2024 14:36:44 +0200 Subject: [PATCH 095/126] cleanup: remove dummy diff provider, it's the exact same as not having one --- helix-vcs/src/git/test.rs | 2 +- helix-vcs/src/lib.rs | 45 ++++++--------------------------------- 2 files changed, 7 insertions(+), 40 deletions(-) diff --git a/helix-vcs/src/git/test.rs b/helix-vcs/src/git/test.rs index 0f928204a..cb3397323 100644 --- a/helix-vcs/src/git/test.rs +++ b/helix-vcs/src/git/test.rs @@ -2,7 +2,7 @@ use tempfile::TempDir; -use crate::Git; +use crate::git::Git; fn exec_git_cmd(args: &str, git_dir: &Path) { let res = Command::new("git") diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs index 7225c38eb..6382928d4 100644 --- a/helix-vcs/src/lib.rs +++ b/helix-vcs/src/lib.rs @@ -1,15 +1,10 @@ -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, Result}; use arc_swap::ArcSwap; use std::{ path::{Path, PathBuf}, sync::Arc, }; -#[cfg(feature = "git")] -pub use git::Git; -#[cfg(not(feature = "git"))] -pub use Dummy as Git; - #[cfg(feature = "git")] mod git; @@ -21,33 +16,6 @@ pub use status::FileChange; -#[doc(hidden)] -#[derive(Clone, Copy)] -pub struct Dummy; -impl Dummy { - fn get_diff_base(&self, _file: &Path) -> Result> { - bail!("helix was compiled without git support") - } - - fn get_current_head_name(&self, _file: &Path) -> Result>>> { - bail!("helix was compiled without git support") - } - - fn for_each_changed_file( - &self, - _cwd: &Path, - _f: impl Fn(Result) -> bool, - ) -> Result<()> { - bail!("helix was compiled without git support") - } -} - -impl From for DiffProvider { - fn from(value: Dummy) -> Self { - DiffProvider::Dummy(value) - } -} - #[derive(Clone)] pub struct DiffProviderRegistry { providers: Vec, @@ -104,7 +72,10 @@ impl Default for DiffProviderRegistry { fn default() -> Self { // currently only git is supported // TODO make this configurable when more providers are added - let providers = vec![Git.into()]; + let providers = vec![ + #[cfg(feature = "git")] + git::Git.into(), + ]; DiffProviderRegistry { providers } } } @@ -113,15 +84,13 @@ fn default() -> Self { /// cloning [DiffProviderRegistry] as `Clone` cannot be used in trait objects. #[derive(Clone)] pub enum DiffProvider { - Dummy(Dummy), #[cfg(feature = "git")] - Git(Git), + Git(git::Git), } impl DiffProvider { fn get_diff_base(&self, file: &Path) -> Result> { match self { - Self::Dummy(inner) => inner.get_diff_base(file), #[cfg(feature = "git")] Self::Git(inner) => inner.get_diff_base(file), } @@ -129,7 +98,6 @@ fn get_diff_base(&self, file: &Path) -> Result> { fn get_current_head_name(&self, file: &Path) -> Result>>> { match self { - Self::Dummy(inner) => inner.get_current_head_name(file), #[cfg(feature = "git")] Self::Git(inner) => inner.get_current_head_name(file), } @@ -141,7 +109,6 @@ fn for_each_changed_file( f: impl Fn(Result) -> bool, ) -> Result<()> { match self { - Self::Dummy(inner) => inner.for_each_changed_file(cwd, f), #[cfg(feature = "git")] Self::Git(inner) => inner.for_each_changed_file(cwd, f), } From f1461b49fa547a50a28f85d5de69e45d7b8143aa Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sat, 27 Apr 2024 14:36:48 +0200 Subject: [PATCH 096/126] cleanup: remove useless Git struct, using free functions instead --- helix-vcs/src/git.rs | 311 ++++++++++++++++++-------------------- helix-vcs/src/git/test.rs | 14 +- helix-vcs/src/lib.rs | 20 ++- 3 files changed, 167 insertions(+), 178 deletions(-) diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 8d935b5fc..080f9b633 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -17,191 +17,174 @@ }; use gix::{Commit, ObjectId, Repository, ThreadSafeRepository}; -use crate::{DiffProvider, FileChange}; +use crate::FileChange; #[cfg(test)] mod test; -#[derive(Clone, Copy)] -pub struct Git; +pub fn get_diff_base(file: &Path) -> Result> { + debug_assert!(!file.exists() || file.is_file()); + debug_assert!(file.is_absolute()); -impl Git { - fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Result { - // custom open options - let mut git_open_opts_map = gix::sec::trust::Mapping::::default(); + // TODO cache repository lookup - // On windows various configuration options are bundled as part of the installations - // This path depends on the install location of git and therefore requires some overhead to lookup - // This is basically only used on windows and has some overhead hence it's disabled on other platforms. - // `gitoxide` doesn't use this as default - let config = gix::open::permissions::Config { - system: true, - git: true, - user: true, - env: true, - includes: true, - git_binary: cfg!(windows), - }; - // change options for config permissions without touching anything else - git_open_opts_map.reduced = git_open_opts_map - .reduced - .permissions(gix::open::Permissions { - config, - ..gix::open::Permissions::default_for_level(gix::sec::Trust::Reduced) - }); - git_open_opts_map.full = git_open_opts_map.full.permissions(gix::open::Permissions { - config, - ..gix::open::Permissions::default_for_level(gix::sec::Trust::Full) - }); + let repo_dir = file.parent().context("file has no parent directory")?; + let repo = open_repo(repo_dir, None) + .context("failed to open git repo")? + .to_thread_local(); + let head = repo.head_commit()?; + let file_oid = find_file_in_commit(&repo, &head, file)?; - let open_options = gix::discover::upwards::Options { - ceiling_dirs: ceiling_dir - .map(|dir| vec![dir.to_owned()]) - .unwrap_or_default(), - dot_git_only: true, - ..Default::default() - }; - - let res = ThreadSafeRepository::discover_with_environment_overrides_opts( - path, - open_options, - git_open_opts_map, - )?; - - Ok(res) + let file_object = repo.find_object(file_oid)?; + 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) } +} - /// Emulates the result of running `git status` from the command line. - fn status(repo: &Repository, f: impl Fn(Result) -> bool) -> Result<()> { - let work_dir = repo - .work_dir() - .ok_or_else(|| anyhow::anyhow!("working tree not found"))? - .to_path_buf(); +pub fn get_current_head_name(file: &Path) -> Result>>> { + debug_assert!(!file.exists() || file.is_file()); + debug_assert!(file.is_absolute()); + let repo_dir = file.parent().context("file has no parent directory")?; + let repo = open_repo(repo_dir, None) + .context("failed to open git repo")? + .to_thread_local(); + let head_ref = repo.head_ref()?; + let head_commit = repo.head_commit()?; - let status_platform = repo - .status(gix::progress::Discard)? - // Here we discard the `status.showUntrackedFiles` config, as it makes little sense in - // our case to not list new (untracked) files. We could have respected this config - // if the default value weren't `Collapsed` though, as this default value would render - // the feature unusable to many. - .untracked_files(UntrackedFiles::Files) - // Turn on file rename detection, which is off by default. - .index_worktree_rewrites(Some(Rewrites { - copies: None, - percentage: Some(0.5), - limit: 1000, - })); + let name = match head_ref { + Some(reference) => reference.name().shorten().to_string(), + None => head_commit.id.to_hex_with_len(8).to_string(), + }; - // No filtering based on path - let empty_patterns = vec![]; + Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) +} - let status_iter = status_platform.into_index_worktree_iter(empty_patterns)?; +pub fn for_each_changed_file(cwd: &Path, f: impl Fn(Result) -> bool) -> Result<()> { + status(&open_repo(cwd, None)?.to_thread_local(), f) +} - for item in status_iter { - let Ok(item) = item.map_err(|err| f(Err(err.into()))) else { +fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Result { + // custom open options + let mut git_open_opts_map = gix::sec::trust::Mapping::::default(); + + // On windows various configuration options are bundled as part of the installations + // This path depends on the install location of git and therefore requires some overhead to lookup + // This is basically only used on windows and has some overhead hence it's disabled on other platforms. + // `gitoxide` doesn't use this as default + let config = gix::open::permissions::Config { + system: true, + git: true, + user: true, + env: true, + includes: true, + git_binary: cfg!(windows), + }; + // change options for config permissions without touching anything else + git_open_opts_map.reduced = git_open_opts_map + .reduced + .permissions(gix::open::Permissions { + config, + ..gix::open::Permissions::default_for_level(gix::sec::Trust::Reduced) + }); + git_open_opts_map.full = git_open_opts_map.full.permissions(gix::open::Permissions { + config, + ..gix::open::Permissions::default_for_level(gix::sec::Trust::Full) + }); + + let open_options = gix::discover::upwards::Options { + ceiling_dirs: ceiling_dir + .map(|dir| vec![dir.to_owned()]) + .unwrap_or_default(), + dot_git_only: true, + ..Default::default() + }; + + let res = ThreadSafeRepository::discover_with_environment_overrides_opts( + path, + open_options, + git_open_opts_map, + )?; + + Ok(res) +} + +/// Emulates the result of running `git status` from the command line. +fn status(repo: &Repository, f: impl Fn(Result) -> bool) -> Result<()> { + let work_dir = repo + .work_dir() + .ok_or_else(|| anyhow::anyhow!("working tree not found"))? + .to_path_buf(); + + let status_platform = repo + .status(gix::progress::Discard)? + // Here we discard the `status.showUntrackedFiles` config, as it makes little sense in + // our case to not list new (untracked) files. We could have respected this config + // if the default value weren't `Collapsed` though, as this default value would render + // the feature unusable to many. + .untracked_files(UntrackedFiles::Files) + // Turn on file rename detection, which is off by default. + .index_worktree_rewrites(Some(Rewrites { + copies: None, + percentage: Some(0.5), + limit: 1000, + })); + + // No filtering based on path + let empty_patterns = vec![]; + + let status_iter = status_platform.into_index_worktree_iter(empty_patterns)?; + + for item in status_iter { + let Ok(item) = item.map_err(|err| f(Err(err.into()))) else { continue; }; - let change = match item { - Item::Modification { - rela_path, status, .. - } => { - let path = work_dir.join(rela_path.to_path()?); - match status { - EntryStatus::Conflict(_) => FileChange::Conflict { path }, - EntryStatus::Change(Change::Removed) => FileChange::Deleted { path }, - EntryStatus::Change(Change::Modification { .. }) => { - FileChange::Modified { path } - } - _ => continue, + let change = match item { + Item::Modification { + rela_path, status, .. + } => { + let path = work_dir.join(rela_path.to_path()?); + match status { + EntryStatus::Conflict(_) => FileChange::Conflict { path }, + EntryStatus::Change(Change::Removed) => FileChange::Deleted { path }, + EntryStatus::Change(Change::Modification { .. }) => { + FileChange::Modified { path } } + _ => continue, } - Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => { - FileChange::Untracked { - path: work_dir.join(entry.rela_path.to_path()?), - } - } - Item::Rewrite { - source, - dirwalk_entry, - .. - } => FileChange::Renamed { - from_path: work_dir.join(source.rela_path().to_path()?), - to_path: work_dir.join(dirwalk_entry.rela_path.to_path()?), - }, - _ => continue, - }; - if !f(Ok(change)) { - break; } - } - - Ok(()) - } -} - -impl Git { - pub fn get_diff_base(&self, file: &Path) -> Result> { - debug_assert!(!file.exists() || file.is_file()); - debug_assert!(file.is_absolute()); - - // TODO cache repository lookup - - let repo_dir = file.parent().context("file has no parent directory")?; - let repo = Git::open_repo(repo_dir, None) - .context("failed to open git repo")? - .to_thread_local(); - let head = repo.head_commit()?; - let file_oid = find_file_in_commit(&repo, &head, file)?; - - let file_object = repo.find_object(file_oid)?; - 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) - } - } - - pub fn get_current_head_name(&self, file: &Path) -> Result>>> { - debug_assert!(!file.exists() || file.is_file()); - debug_assert!(file.is_absolute()); - let repo_dir = file.parent().context("file has no parent directory")?; - let repo = Git::open_repo(repo_dir, None) - .context("failed to open git repo")? - .to_thread_local(); - let head_ref = repo.head_ref()?; - let head_commit = repo.head_commit()?; - - let name = match head_ref { - Some(reference) => reference.name().shorten().to_string(), - None => head_commit.id.to_hex_with_len(8).to_string(), + Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => { + FileChange::Untracked { + path: work_dir.join(entry.rela_path.to_path()?), + } + } + Item::Rewrite { + source, + dirwalk_entry, + .. + } => FileChange::Renamed { + from_path: work_dir.join(source.rela_path().to_path()?), + to_path: work_dir.join(dirwalk_entry.rela_path.to_path()?), + }, + _ => continue, }; - - Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) + if !f(Ok(change)) { + break; + } } - pub fn for_each_changed_file( - &self, - cwd: &Path, - f: impl Fn(Result) -> bool, - ) -> Result<()> { - Self::status(&Self::open_repo(cwd, None)?.to_thread_local(), f) - } -} - -impl From for DiffProvider { - fn from(value: Git) -> Self { - DiffProvider::Git(value) - } + Ok(()) } /// Finds the object that contains the contents of a file at a specific commit. diff --git a/helix-vcs/src/git/test.rs b/helix-vcs/src/git/test.rs index cb3397323..95ff10b23 100644 --- a/helix-vcs/src/git/test.rs +++ b/helix-vcs/src/git/test.rs @@ -2,7 +2,7 @@ use tempfile::TempDir; -use crate::git::Git; +use crate::git; fn exec_git_cmd(args: &str, git_dir: &Path) { let res = Command::new("git") @@ -54,7 +54,7 @@ fn missing_file() { let file = temp_git.path().join("file.txt"); File::create(&file).unwrap().write_all(b"foo").unwrap(); - assert!(Git.get_diff_base(&file).is_err()); + assert!(git::get_diff_base(&file).is_err()); } #[test] @@ -64,7 +64,7 @@ fn unmodified_file() { let contents = b"foo".as_slice(); File::create(&file).unwrap().write_all(contents).unwrap(); create_commit(temp_git.path(), true); - assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); + assert_eq!(git::get_diff_base(&file).unwrap(), Vec::from(contents)); } #[test] @@ -76,7 +76,7 @@ fn modified_file() { create_commit(temp_git.path(), true); File::create(&file).unwrap().write_all(b"bar").unwrap(); - assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); + assert_eq!(git::get_diff_base(&file).unwrap(), Vec::from(contents)); } /// Test that `get_file_head` does not return content for a directory. @@ -95,7 +95,7 @@ fn directory() { std::fs::remove_dir_all(&dir).unwrap(); File::create(&dir).unwrap().write_all(b"bar").unwrap(); - assert!(Git.get_diff_base(&dir).is_err()); + assert!(git::get_diff_base(&dir).is_err()); } /// Test that `get_file_head` does not return content for a symlink. @@ -116,6 +116,6 @@ fn symlink() { symlink("file.txt", &file_link).unwrap(); create_commit(temp_git.path(), true); - assert!(Git.get_diff_base(&file_link).is_err()); - assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); + assert!(git::get_diff_base(&file_link).is_err()); + assert_eq!(git::get_diff_base(&file).unwrap(), Vec::from(contents)); } diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs index 6382928d4..539be779a 100644 --- a/helix-vcs/src/lib.rs +++ b/helix-vcs/src/lib.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use arc_swap::ArcSwap; use std::{ path::{Path, PathBuf}, @@ -74,7 +74,7 @@ fn default() -> Self { // TODO make this configurable when more providers are added let providers = vec![ #[cfg(feature = "git")] - git::Git.into(), + DiffProvider::Git, ]; DiffProviderRegistry { providers } } @@ -82,24 +82,29 @@ fn default() -> Self { /// A union type that includes all types that implement [DiffProvider]. We need this type to allow /// cloning [DiffProviderRegistry] as `Clone` cannot be used in trait objects. -#[derive(Clone)] +/// +/// `Copy` is simply to ensure the `clone()` call is the simplest it can be. +#[derive(Copy, Clone)] pub enum DiffProvider { #[cfg(feature = "git")] - Git(git::Git), + Git, + None, } impl DiffProvider { fn get_diff_base(&self, file: &Path) -> Result> { match self { #[cfg(feature = "git")] - Self::Git(inner) => inner.get_diff_base(file), + Self::Git => git::get_diff_base(file), + Self::None => bail!("No diff support compiled in"), } } fn get_current_head_name(&self, file: &Path) -> Result>>> { match self { #[cfg(feature = "git")] - Self::Git(inner) => inner.get_current_head_name(file), + Self::Git => git::get_current_head_name(file), + Self::None => bail!("No diff support compiled in"), } } @@ -110,7 +115,8 @@ fn for_each_changed_file( ) -> Result<()> { match self { #[cfg(feature = "git")] - Self::Git(inner) => inner.for_each_changed_file(cwd, f), + Self::Git => git::for_each_changed_file(cwd, f), + Self::None => bail!("No diff support compiled in"), } } } From 615d34a237d0d01d80c2204155ae8302d348815a Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sat, 27 Apr 2024 14:41:20 +0200 Subject: [PATCH 097/126] nit: remove unused `ceiling_dir` param --- helix-vcs/src/git.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 080f9b633..979f8726e 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -29,7 +29,7 @@ pub fn get_diff_base(file: &Path) -> Result> { // TODO cache repository lookup let repo_dir = file.parent().context("file has no parent directory")?; - let repo = open_repo(repo_dir, None) + let repo = open_repo(repo_dir) .context("failed to open git repo")? .to_thread_local(); let head = repo.head_commit()?; @@ -57,7 +57,7 @@ pub fn get_current_head_name(file: &Path) -> Result>>> { debug_assert!(!file.exists() || file.is_file()); debug_assert!(file.is_absolute()); let repo_dir = file.parent().context("file has no parent directory")?; - let repo = open_repo(repo_dir, None) + let repo = open_repo(repo_dir) .context("failed to open git repo")? .to_thread_local(); let head_ref = repo.head_ref()?; @@ -72,10 +72,10 @@ pub fn get_current_head_name(file: &Path) -> Result>>> { } pub fn for_each_changed_file(cwd: &Path, f: impl Fn(Result) -> bool) -> Result<()> { - status(&open_repo(cwd, None)?.to_thread_local(), f) + status(&open_repo(cwd)?.to_thread_local(), f) } -fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Result { +fn open_repo(path: &Path) -> Result { // custom open options let mut git_open_opts_map = gix::sec::trust::Mapping::::default(); @@ -104,9 +104,6 @@ fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Result Date: Sat, 27 Apr 2024 20:55:12 +0100 Subject: [PATCH 098/126] Fixed ECMAScript private member highlighting (#10554) --- book/src/themes.md | 2 ++ runtime/queries/ecma/highlights.scm | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/book/src/themes.md b/book/src/themes.md index d982dee9a..e3b95c0a7 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -179,6 +179,7 @@ #### Syntax highlighting - `parameter` - Function parameters - `other` - `member` - Fields of composite data types (e.g. structs, unions) + - `private` - Private fields that use a unique syntax (currently just ECMAScript-based languages) - `label` @@ -206,6 +207,7 @@ #### Syntax highlighting - `function` - `builtin` - `method` + - `private` - Private methods that use a unique syntax (currently just ECMAScript-based languages) - `macro` - `special` (preprocessor in C) diff --git a/runtime/queries/ecma/highlights.scm b/runtime/queries/ecma/highlights.scm index 07f7dfeda..dc8ce5e73 100644 --- a/runtime/queries/ecma/highlights.scm +++ b/runtime/queries/ecma/highlights.scm @@ -29,15 +29,24 @@ name: (identifier) @function) (method_definition name: (property_identifier) @function.method) +(method_definition + name: (private_property_identifier) @function.method.private) (pair key: (property_identifier) @function.method value: [(function) (arrow_function)]) +(pair + key: (private_property_identifier) @function.method.private + value: [(function) (arrow_function)]) (assignment_expression left: (member_expression property: (property_identifier) @function.method) right: [(function) (arrow_function)]) +(assignment_expression + left: (member_expression + property: (private_property_identifier) @function.method.private) + right: [(function) (arrow_function)]) (variable_declarator name: (identifier) @function @@ -64,6 +73,9 @@ (call_expression function: (member_expression property: (property_identifier) @function.method)) +(call_expression + function: (member_expression + property: (private_property_identifier) @function.method.private)) ; Variables ;---------- @@ -74,6 +86,7 @@ ;----------- (property_identifier) @variable.other.member +(private_property_identifier) @variable.other.member.private (shorthand_property_identifier) @variable.other.member (shorthand_property_identifier_pattern) @variable.other.member From a1d7997fe303812edafb23ab08eaa8972e4f4c1d Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 28 Apr 2024 05:11:17 +0200 Subject: [PATCH 099/126] fix lsp restart (#10614) --- helix-lsp/src/lib.rs | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 04f018bc4..0a3c6a33d 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -736,35 +736,30 @@ pub fn restart( .language_servers .iter() .filter_map(|LanguageServerFeatures { name, .. }| { - if self.inner_by_name.contains_key(name) { - let client = match self.start_client( - name.clone(), - language_config, - doc_path, - root_dirs, - enable_snippets, - ) { - Ok(client) => client, - Err(StartupError::NoRequiredRootFound) => return None, - Err(StartupError::Error(err)) => return Some(Err(err)), - }; - let old_clients = self - .inner_by_name - .insert(name.clone(), vec![client.clone()]) - .unwrap(); - + if let Some(old_clients) = self.inner_by_name.remove(name) { for old_client in old_clients { self.file_event_handler.remove_client(old_client.id()); - self.inner.remove(client.id()); + self.inner.remove(old_client.id()); tokio::spawn(async move { let _ = old_client.force_shutdown().await; }); } - - Some(Ok(client)) - } else { - None } + let client = match self.start_client( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + ) { + Ok(client) => client, + Err(StartupError::NoRequiredRootFound) => return None, + Err(StartupError::Error(err)) => return Some(Err(err)), + }; + self.inner_by_name + .insert(name.to_owned(), vec![client.clone()]); + + Some(Ok(client)) }) .collect() } From 2d6d876a233ffc854bd3eb9d07c12948b0408e4b Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 23 Apr 2024 16:15:42 +0200 Subject: [PATCH 100/126] fix popup size calculation Co-authored-by: ath3 --- helix-term/src/commands/dap.rs | 9 +- helix-term/src/commands/lsp.rs | 11 +-- helix-term/src/ui/completion.rs | 10 +- helix-term/src/ui/menu.rs | 21 +---- helix-term/src/ui/popup.rs | 156 +++++++++++++++++--------------- 5 files changed, 92 insertions(+), 115 deletions(-) diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index d62b0a4e5..eda94c44f 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -8,7 +8,7 @@ use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_dap::{self as dap, Client}; use helix_lsp::block_on; -use helix_view::{editor::Breakpoint, graphics::Margin}; +use helix_view::editor::Breakpoint; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -581,12 +581,7 @@ pub fn dap_variables(cx: &mut Context) { } let contents = Text::from(tui::text::Text::from(variables)); - let margin = if cx.editor.popup_border() { - Margin::all(1) - } else { - Margin::none() - }; - let popup = Popup::new("dap-variables", contents).margin(margin); + let popup = Popup::new("dap-variables", contents); cx.replace_or_push_layer("dap-variables", popup); } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 88bd07919..e7ba9f0fc 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -21,7 +21,6 @@ use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId}, editor::Action, - graphics::Margin, handlers::lsp::SignatureHelpInvoked, theme::Style, Document, View, @@ -733,15 +732,7 @@ pub fn code_action(cx: &mut Context) { }); picker.move_down(); // pre-select the first item - let margin = if editor.menu_border() { - Margin::vertical(1) - } else { - Margin::none() - }; - - let popup = Popup::new("code-action", picker) - .with_scrollbar(false) - .margin(margin); + let popup = Popup::new("code-action", picker).with_scrollbar(false); compositor.replace_or_push("code-action", popup); }; diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 7a08c90ce..f793dd484 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -5,7 +5,6 @@ use helix_view::{ document::SavePoint, editor::CompleteAction, - graphics::Margin, handlers::lsp::SignatureHelpInvoked, theme::{Modifier, Style}, ViewId, @@ -334,16 +333,9 @@ macro_rules! language_server { } }); - let margin = if editor.menu_border() { - Margin::vertical(1) - } else { - Margin::none() - }; - let popup = Popup::new(Self::ID, menu) .with_scrollbar(false) - .ignore_escape_key(true) - .margin(margin); + .ignore_escape_key(true); let (view, doc) = current_ref!(editor); let text = doc.text().slice(..); diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index f9f038e7a..c5006f958 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -7,18 +7,11 @@ use helix_core::fuzzy::MATCHER; use nucleo::pattern::{Atom, AtomKind, CaseMatching}; use nucleo::{Config, Utf32Str}; -use tui::{ - buffer::Buffer as Surface, - widgets::{Block, Borders, Table, Widget}, -}; +use tui::{buffer::Buffer as Surface, widgets::Table}; pub use tui::widgets::{Cell, Row}; -use helix_view::{ - editor::SmartTabConfig, - graphics::{Margin, Rect}, - Editor, -}; +use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item: Sync + Send + 'static { @@ -341,17 +334,9 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { .try_get("ui.menu") .unwrap_or_else(|| theme.get("ui.text")); let selected = theme.get("ui.menu.selected"); + surface.clear_with(area, style); - let render_borders = cx.editor.menu_border(); - - let area = if render_borders { - Widget::render(Block::default().borders(Borders::ALL), area, surface); - area.inner(&Margin::vertical(1)) - } else { - area - }; - let scroll = self.scroll; let options: Vec<_> = self diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 8d436fda9..7ee65db59 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -15,7 +15,16 @@ Editor, }; -const MIN_HEIGHT: u16 = 4; +const MIN_HEIGHT: u16 = 6; +const MAX_HEIGHT: u16 = 26; +const MAX_WIDTH: u16 = 120; + +struct RenderInfo { + area: Rect, + child_height: u16, + render_borders: bool, + is_menu: bool, +} // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // a width/height hint. maybe Popup(Box) @@ -23,7 +32,6 @@ pub struct Popup { contents: T, position: Option, - margin: Margin, area: Rect, position_bias: Open, scroll_half_pages: usize, @@ -38,7 +46,6 @@ pub fn new(id: &'static str, contents: T) -> Self { Self { contents, position: None, - margin: Margin::none(), position_bias: Open::Below, area: Rect::new(0, 0, 0, 0), scroll_half_pages: 0, @@ -71,11 +78,6 @@ pub fn position_bias(mut self, bias: Open) -> Self { self } - pub fn margin(mut self, margin: Margin) -> Self { - self.margin = margin; - self - } - pub fn auto_close(mut self, auto_close: bool) -> Self { self.auto_close = auto_close; self @@ -118,38 +120,32 @@ pub fn contents_mut(&mut self) -> &mut T { } pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { - let child_size = self - .contents - .required_size((viewport.width, viewport.height)) - .expect("Component needs required_size implemented in order to be embedded in a popup"); - - self.area_internal(viewport, editor, child_size) + self.render_info(viewport, editor).area } - pub fn area_internal( - &mut self, - viewport: Rect, - editor: &Editor, - child_size: (u16, u16), - ) -> Rect { - let width = child_size.0.min(viewport.width); - let height = child_size.1.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport - + fn render_info(&mut self, viewport: Rect, editor: &Editor) -> RenderInfo { let position = self .position .get_or_insert_with(|| editor.cursor().0.unwrap_or_default()); - // if there's a orientation preference, use that - // if we're on the top part of the screen, do below - // if we're on the bottom part, do above + let is_menu = self + .contents + .type_name() + .starts_with("helix_term::ui::menu::Menu"); + + let mut render_borders = if is_menu { + editor.menu_border() + } else { + editor.popup_border() + }; // -- make sure frame doesn't stick out of bounds let mut rel_x = position.col as u16; let mut rel_y = position.row as u16; - if viewport.width <= rel_x + width { - rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - } + // if there's a orientation preference, use that + // if we're on the top part of the screen, do below + // if we're on the bottom part, do above let can_put_below = viewport.height > rel_y + MIN_HEIGHT; let can_put_above = rel_y.checked_sub(MIN_HEIGHT).is_some(); let final_pos = match self.position_bias { @@ -163,7 +159,40 @@ pub fn area_internal( }, }; - match final_pos { + // compute maximum space available for child + let mut max_height = match final_pos { + Open::Above => rel_y, + Open::Below => viewport.height.saturating_sub(1 + rel_y), + }; + max_height = max_height.min(MAX_HEIGHT); + let mut max_width = viewport.width.saturating_sub(2).min(MAX_WIDTH); + render_borders = render_borders && max_height > 3 && max_width > 3; + if render_borders { + max_width -= 2; + max_height -= 2; + } + + // compute required child size and reclamp + let (mut width, child_height) = self + .contents + .required_size((max_width, max_height)) + .expect("Component needs required_size implemented in order to be embedded in a popup"); + + width = width.min(MAX_WIDTH); + let height = if render_borders { + (child_height + 2).min(MAX_HEIGHT) + } else { + child_height.min(MAX_HEIGHT) + }; + if render_borders { + width += 2; + } + if viewport.width <= rel_x + width + 2 { + rel_x = viewport.width.saturating_sub(width + 2); + width = viewport.width.saturating_sub(rel_x + 2) + } + + let area = match final_pos { Open::Above => { rel_y = rel_y.saturating_sub(height); Rect::new(rel_x, rel_y, width, position.row as u16 - rel_y) @@ -173,6 +202,12 @@ pub fn area_internal( let y_max = viewport.bottom().min(height + rel_y); Rect::new(rel_x, rel_y, width, y_max - rel_y) } + }; + RenderInfo { + area, + child_height, + render_borders, + is_menu, } } @@ -259,35 +294,16 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } - fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { - const MAX_WIDTH: u16 = 120; - const MAX_HEIGHT: u16 = 26; - - let inner = Rect::new(0, 0, MAX_WIDTH, MAX_HEIGHT).inner(&self.margin); - - let (width, height) = self - .contents - .required_size((inner.width, inner.height)) - .expect("Component needs required_size implemented in order to be embedded in a popup"); - - let size = ( - (width + self.margin.width()).min(MAX_WIDTH), - (height + self.margin.height()).min(MAX_HEIGHT), - ); - - Some(size) - } - fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - let child_size = self - .contents - .required_size((viewport.width, viewport.height)) - .expect("Component needs required_size implemented in order to be embedded in a popup"); - - let area = self.area_internal(viewport, cx.editor, child_size); + let RenderInfo { + area, + child_height, + render_borders, + is_menu, + } = self.render_info(viewport, cx.editor); self.area = area; - let max_offset = child_size.1.saturating_sub(area.height) as usize; + let max_offset = child_height.saturating_sub(area.height) as usize; let half_page_size = (area.height / 2) as usize; let scroll = max_offset.min(self.scroll_half_pages * half_page_size); if half_page_size > 0 { @@ -296,32 +312,30 @@ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { cx.scroll = Some(scroll); // clear area - let background = cx.editor.theme.get("ui.popup"); + let background = if is_menu { + // TODO: consistently style menu + cx.editor + .theme + .try_get("ui.menu") + .unwrap_or_else(|| cx.editor.theme.get("ui.text")) + } else { + cx.editor.theme.get("ui.popup") + }; surface.clear_with(area, background); - let render_borders = cx.editor.popup_border(); - - let inner = if self - .contents - .type_name() - .starts_with("helix_term::ui::menu::Menu") - { - area - } else { - area.inner(&self.margin) - }; - - let border = usize::from(render_borders); + let mut inner = area; if render_borders { + inner = area.inner(&Margin::all(1)); Widget::render(Block::default().borders(Borders::ALL), area, surface); } + let border = usize::from(render_borders); self.contents.render(inner, surface, cx); // render scrollbar if contents do not fit if self.has_scrollbar { let win_height = (inner.height as usize).saturating_sub(2 * border); - let len = (child_size.1 as usize).saturating_sub(2 * border); + let len = (child_height as usize).saturating_sub(2 * border); let fits = len <= win_height; let scroll_style = cx.editor.theme.get("ui.menu.scroll"); From 93e8c16614b26278a1a979a3a01a46c9ba3d681f Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 23 Apr 2024 16:31:14 +0200 Subject: [PATCH 101/126] fix required_size implementation of signature help Trunctation should always be handled by the parent. Returning None is only supposed to indicate a missing implementation Co-authored-by: Ben Fekih, Hichem" --- helix-term/src/ui/lsp.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index d53437ecd..b82f7be29 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -155,10 +155,7 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { let sig = &self.signatures[self.active_signature]; - if PADDING >= viewport.1 || PADDING >= viewport.0 { - return None; - } - let max_text_width = (viewport.0 - PADDING).min(120); + let max_text_width = viewport.0.saturating_sub(PADDING).clamp(10, 120); let signature_text = crate::ui::markdown::highlighted_code_block( sig.signature.as_str(), From e2594b64c0daf15bacdb5396d439f206fcbe11e6 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 23 Apr 2024 16:32:43 +0200 Subject: [PATCH 102/126] move popup when cursor line changes Co-authored-by: Ben Fekih, Hichem" --- helix-term/src/ui/popup.rs | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 7ee65db59..5362bdc7f 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -124,9 +124,15 @@ pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { } fn render_info(&mut self, viewport: Rect, editor: &Editor) -> RenderInfo { - let position = self + let mut position = editor.cursor().0.unwrap_or_default(); + if let Some(old_position) = self .position - .get_or_insert_with(|| editor.cursor().0.unwrap_or_default()); + .filter(|old_position| old_position.row == position.row) + { + position = old_position; + } else { + self.position = Some(position); + } let is_menu = self .contents @@ -303,14 +309,6 @@ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { } = self.render_info(viewport, cx.editor); self.area = area; - let max_offset = child_height.saturating_sub(area.height) as usize; - let half_page_size = (area.height / 2) as usize; - let scroll = max_offset.min(self.scroll_half_pages * half_page_size); - if half_page_size > 0 { - self.scroll_half_pages = scroll / half_page_size; - } - cx.scroll = Some(scroll); - // clear area let background = if is_menu { // TODO: consistently style menu @@ -330,12 +328,19 @@ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { } let border = usize::from(render_borders); + let max_offset = child_height.saturating_sub(inner.height) as usize; + let half_page_size = (inner.height / 2) as usize; + let scroll = max_offset.min(self.scroll_half_pages * half_page_size); + if half_page_size > 0 { + self.scroll_half_pages = scroll / half_page_size; + } + cx.scroll = Some(scroll); self.contents.render(inner, surface, cx); // render scrollbar if contents do not fit if self.has_scrollbar { - let win_height = (inner.height as usize).saturating_sub(2 * border); - let len = (child_height as usize).saturating_sub(2 * border); + let win_height = inner.height as usize; + let len = child_height as usize; let fits = len <= win_height; let scroll_style = cx.editor.theme.get("ui.menu.scroll"); @@ -350,7 +355,8 @@ const fn div_ceil(a: usize, b: usize) -> usize { let mut cell; for i in 0..win_height { - cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)]; + cell = + &mut surface[(inner.right() - 1 + border as u16, inner.top() + i as u16)]; let half_block = if render_borders { "▌" } else { "▐" }; From 8db93013fb33f456b241e8146bc9d85333de6878 Mon Sep 17 00:00:00 2001 From: Kitsu Date: Mon, 29 Apr 2024 10:18:05 -0300 Subject: [PATCH 103/126] fix: avoid child area overflow on split (#10620) --- helix-view/src/tree.rs | 44 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index b7ad5aa9d..307dbc71d 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -407,11 +407,13 @@ pub fn recalculate(&mut self) { } Layout::Vertical => { let len = container.children.len(); - - let width = area.width / len as u16; + let len_u16 = len as u16; let inner_gap = 1u16; - // let total_gap = inner_gap * (len as u16 - 1); + let total_gap = inner_gap * len_u16.saturating_sub(2); + + let used_area = area.width.saturating_sub(total_gap); + let width = used_area / len_u16; let mut child_x = area.x; @@ -925,13 +927,43 @@ fn all_vertical_views_have_same_width() { assert_eq!(3, tree.views().count()); assert_eq!( vec![ - tree_area_width / 3, - tree_area_width / 3, - tree_area_width / 3 - 2 // Rounding in `recalculate`. + tree_area_width / 3 - 1, // gap here + tree_area_width / 3 - 1, // gap here + tree_area_width / 3 ], tree.views() .map(|(view, _)| view.area.width) .collect::>() ); } + + #[test] + fn vsplit_gap_rounding() { + let (tree_area_width, tree_area_height) = (80, 24); + let mut tree = Tree::new(Rect { + x: 0, + y: 0, + width: tree_area_width, + height: tree_area_height, + }); + let mut view = View::new(DocumentId::default(), GutterConfig::default()); + view.area = Rect::new(0, 0, tree_area_width, tree_area_height); + tree.insert(view); + + for _ in 0..9 { + let view = View::new(DocumentId::default(), GutterConfig::default()); + tree.split(view, Layout::Vertical); + } + + assert_eq!(10, tree.views().count()); + assert_eq!( + std::iter::repeat(7) + .take(9) + .chain(Some(8)) // Rounding in `recalculate`. + .collect::>(), + tree.views() + .map(|(view, _)| view.area.width) + .collect::>() + ); + } } From ec224798e7a37cc733ba2dedac16edb4dcd87cf8 Mon Sep 17 00:00:00 2001 From: Kitsu Date: Mon, 29 Apr 2024 10:18:58 -0300 Subject: [PATCH 104/126] fix: do not stop at first url at goto_file (#10622) --- helix-term/src/commands.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8610a2048..7be2ea095 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1256,7 +1256,8 @@ fn goto_file_impl(cx: &mut Context, action: Action) { } if let Ok(url) = Url::parse(p) { - return open_url(cx, url, action); + open_url(cx, url, action); + continue; } let path = &rel_path.join(p); From 724a96abc8587e0f128e7e84cb43c8baf77cbbea Mon Sep 17 00:00:00 2001 From: Erasin Wang Date: Mon, 29 Apr 2024 23:44:03 +0800 Subject: [PATCH 105/126] Add pest support (#10616) Support [pest-parser](https://github.com/pest-parser) - [pest-language-server](https://github.com/pest-parser/pest-ide-tools) - [tree-sitter-pest](https://github.com/pest-parser/tree-sitter-pest) close #7878 --- book/src/generated/lang-support.md | 1 + languages.toml | 21 ++++++++++++ runtime/queries/pest/highlights.scm | 49 ++++++++++++++++++++++++++++ runtime/queries/pest/indents.scm | 9 +++++ runtime/queries/pest/injections.scm | 7 ++++ runtime/queries/pest/textobjects.scm | 8 +++++ 6 files changed, 95 insertions(+) create mode 100644 runtime/queries/pest/highlights.scm create mode 100644 runtime/queries/pest/indents.scm create mode 100644 runtime/queries/pest/injections.scm create mode 100644 runtime/queries/pest/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 45cc1384d..77d304cc6 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -141,6 +141,7 @@ | passwd | ✓ | | | | | pem | ✓ | | | | | perl | ✓ | ✓ | ✓ | `perlnavigator` | +| pest | ✓ | ✓ | ✓ | `pest-language-server` | | php | ✓ | ✓ | ✓ | `intelephense` | | php-only | ✓ | | | | | pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` | diff --git a/languages.toml b/languages.toml index 7e858b056..ecf1b49fd 100644 --- a/languages.toml +++ b/languages.toml @@ -67,6 +67,7 @@ openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] } pasls = { command = "pasls", args = [] } pbkit = { command = "pb", args = [ "lsp" ] } perlnavigator = { command = "perlnavigator", args= ["--stdio"] } +pest-language-server = { command = "pest-language-server" } prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] } purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] } pylsp = { command = "pylsp" } @@ -3558,3 +3559,23 @@ language-servers = [] [[grammar]] name = "move" source = { git = "https://github.com/tzakian/tree-sitter-move", rev = "8bc0d1692caa8763fef54d48068238d9bf3c0264" } + +[[language]] +name = "pest" +scope = "source.pest" +injection-regex = "pest" +file-types = ["pest"] +comment-tokens = ["//", "///", "//!"] +block-comment-tokens = { start = "/*", end = "*/" } +indent = { tab-width = 4, unit = " " } +language-servers = ["pest-language-server"] + +[language.auto-pairs] +'(' = ')' +'{' = '}' +'[' = ']' +'"' = '"' + +[[grammar]] +name = "pest" +source = { git = "https://github.com/pest-parser/tree-sitter-pest", rev = "a8a98a824452b1ec4da7f508386a187a2f234b85" } diff --git a/runtime/queries/pest/highlights.scm b/runtime/queries/pest/highlights.scm new file mode 100644 index 000000000..9d6f13c2a --- /dev/null +++ b/runtime/queries/pest/highlights.scm @@ -0,0 +1,49 @@ +(line_comment) @comment +(block_comment) @comment + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +((identifier) @variable) +((builtin) @type.builtin) +((const) @constant) + +[ + (string) + (character) +] @string + +[ + "_" + "@" + "$" +]@keyword.storage.modifier + +[ + "~" + "|" + "=" + "+" + "*" + "&" + "^" + "!" + "?" + ".." +] @operator + +[ + "PUSH" + "PEEK" + "POP" + "SOI" + "EOI" + "ANY" +] @keyword + diff --git a/runtime/queries/pest/indents.scm b/runtime/queries/pest/indents.scm new file mode 100644 index 000000000..51042f905 --- /dev/null +++ b/runtime/queries/pest/indents.scm @@ -0,0 +1,9 @@ +[ + (expression) +] @indent + +[ + "]" + "}" + ")" +] @outdent diff --git a/runtime/queries/pest/injections.scm b/runtime/queries/pest/injections.scm new file mode 100644 index 000000000..0a79c8f83 --- /dev/null +++ b/runtime/queries/pest/injections.scm @@ -0,0 +1,7 @@ +((line_comment) @injection.content + (#set! injection.language "comment") + (#set! injection.include-children)) + +((block_comment) @injection.content + (#set! injection.language "comment") + (#set! injection.include-children)) diff --git a/runtime/queries/pest/textobjects.scm b/runtime/queries/pest/textobjects.scm new file mode 100644 index 000000000..75ccd09eb --- /dev/null +++ b/runtime/queries/pest/textobjects.scm @@ -0,0 +1,8 @@ +(grammar_rule (_) @class.inside) @class.around +(term (_) @entry.inside) @entry.around + +(line_comment) @comment.inside +(line_comment)+ @comment.around + +(block_comment) @comment.inside +(block_comment)+ @comment.around From eeb8782c542c20069a81f78bec9ec0b15c0c7050 Mon Sep 17 00:00:00 2001 From: tingerrr Date: Mon, 29 Apr 2024 17:44:20 +0200 Subject: [PATCH 106/126] Add comment injection to typst queries (#10628) --- runtime/queries/typst/injections.scm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/queries/typst/injections.scm b/runtime/queries/typst/injections.scm index 06a250979..fb4fee5b2 100644 --- a/runtime/queries/typst/injections.scm +++ b/runtime/queries/typst/injections.scm @@ -5,3 +5,6 @@ lang: (ident) @injection.language (blob) @injection.content) +((comment) + @injection.content + (#set! injection.language "comment")) From d8701bfd1eb5019f7d25def83aa147546168c6d2 Mon Sep 17 00:00:00 2001 From: TobiEiss Date: Mon, 29 Apr 2024 18:17:46 +0200 Subject: [PATCH 107/126] add textobjects queries for hurl (#10594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add textobjects queries comment and function * update doc for hurl lang support * switch entry.inner to entry.outer * switch to function.inside --------- Co-authored-by: Tobias Eiß --- book/src/generated/lang-support.md | 2 +- runtime/queries/hurl/textobjects.scm | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 runtime/queries/hurl/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 77d304cc6..8418138c8 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -83,7 +83,7 @@ | hoon | ✓ | | | | | hosts | ✓ | | | | | html | ✓ | | | `vscode-html-language-server` | -| hurl | ✓ | | ✓ | | +| hurl | ✓ | ✓ | ✓ | | | hyprlang | ✓ | | ✓ | | | idris | | | | `idris2-lsp` | | iex | ✓ | | | | diff --git a/runtime/queries/hurl/textobjects.scm b/runtime/queries/hurl/textobjects.scm new file mode 100644 index 000000000..ac96f9555 --- /dev/null +++ b/runtime/queries/hurl/textobjects.scm @@ -0,0 +1,5 @@ +(comment) @comment.inside + +(comment)+ @comment.around + +(entry (_) @function.inside) @function.around From 752ed8eb15b7366ef0c9e835b261189141e38630 Mon Sep 17 00:00:00 2001 From: Luv-Ray <1357065654@qq.com> Date: Tue, 30 Apr 2024 12:13:27 +0800 Subject: [PATCH 108/126] add `try` keyword to rust highlights (#10641) --- runtime/queries/rust/highlights.scm | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 1c0f799b1..7997c5ea0 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -125,6 +125,7 @@ "match" "if" "else" + "try" ] @keyword.control.conditional [ From 31273c69e0be3b2d14f0c76d3f6a735e1d332e63 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 2 May 2024 06:25:15 -0400 Subject: [PATCH 109/126] Add completion/signature bindings to keymap.md (#10654) * Add completion/signature bindings to keymap.md PR #9974 added alt-p/alt-n keybindings to scroll through signatures. This wasn't very discoverable, as it's not in the docs or the command palette. This also removes a broken link for "comment mode" in the table of contents. * Update keymap.md --- book/src/keymap.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 65a223efb..55b467a0e 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -12,8 +12,9 @@ # Keymap - [Match mode](#match-mode) - [Window mode](#window-mode) - [Space mode](#space-mode) - - [Comment mode](#comment-mode) - [Popup](#popup) + - [Completion Menu](#completion-menu) + - [Signature-help Popup](#signature-help-popup) - [Unimpaired](#unimpaired) - [Insert mode](#insert-mode) - [Select / extend mode](#select--extend-mode) @@ -309,13 +310,31 @@ #### Space mode ##### Popup -Displays documentation for item under cursor. +Displays documentation for item under cursor. Remapping currently not supported. | Key | Description | | ---- | ----------- | | `Ctrl-u` | Scroll up | | `Ctrl-d` | Scroll down | +##### Completion Menu + +Displays documentation for the selected completion item. Remapping currently not supported. + +| Key | Description | +| ---- | ----------- | +| `Shift-Tab`, `Ctrl-p`, `Up` | Previous entry | +| `Tab`, `Ctrl-n`, `Down` | Next entry | + +##### Signature-help Popup + +Displays the signature of the selected completion item. Remapping currently not supported. + +| Key | Description | +| ---- | ----------- | +| `Alt-p` | Previous signature | +| `Alt-n` | Next signature | + #### Unimpaired These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). From cfca30887cd4df53af3086c51ab5480cdb3604d5 Mon Sep 17 00:00:00 2001 From: Hichem Date: Fri, 3 May 2024 03:53:07 +0200 Subject: [PATCH 110/126] signature: use the suggested LSP signature when changed (#10655) some LSPs does update the active signature and some not. To make both worlds happy, make the active signature more intelligent. 1. SignatureHelp store now the suggested lsp_signature 2. if the lsp_signature changes then use it 3. otherwise use the last signature from the old popup 4. in case the old signature doesn't exist anymore, show the last signature Signed-off-by: Ben Fekih, Hichem --- helix-term/src/handlers/signature_help.rs | 28 +++++++++++++++++------ helix-term/src/ui/lsp.rs | 7 ++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index 0bb1d3d16..4fc008118 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -238,19 +238,33 @@ pub fn show_signature_help( .collect(); let old_popup = compositor.find_id::>(SignatureHelp::ID); - let mut active_signature = old_popup - .as_ref() - .map(|popup| popup.contents().active_signature()) - .unwrap_or_else(|| response.active_signature.unwrap_or_default() as usize); + let lsp_signature = response.active_signature.map(|s| s as usize); - if active_signature >= signatures.len() { - active_signature = signatures.len() - 1; - } + // take the new suggested lsp signature if changed + // otherwise take the old signature if possible + // otherwise the last one (in case there is less signatures than before) + let active_signature = old_popup + .as_ref() + .map(|popup| { + let old_lsp_sig = popup.contents().lsp_signature(); + let old_sig = popup + .contents() + .active_signature() + .min(signatures.len() - 1); + + if old_lsp_sig != lsp_signature { + lsp_signature.unwrap_or(old_sig) + } else { + old_sig + } + }) + .unwrap_or(lsp_signature.unwrap_or_default()); let contents = SignatureHelp::new( language.to_string(), Arc::clone(&editor.syn_loader), active_signature, + lsp_signature, signatures, ); diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index b82f7be29..d845be4a7 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -27,6 +27,7 @@ pub struct SignatureHelp { language: String, config_loader: Arc>, active_signature: usize, + lsp_signature: Option, signatures: Vec, } @@ -37,12 +38,14 @@ pub fn new( language: String, config_loader: Arc>, active_signature: usize, + lsp_signature: Option, signatures: Vec, ) -> Self { Self { language, config_loader, active_signature, + lsp_signature, signatures, } } @@ -51,6 +54,10 @@ pub fn active_signature(&self) -> usize { self.active_signature } + pub fn lsp_signature(&self) -> Option { + self.lsp_signature + } + pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup> { compositor.find_id::>(Self::ID) } From 7e13213e7430c95cbad210994cecbfadc52c0714 Mon Sep 17 00:00:00 2001 From: Matthew Pomes Date: Fri, 3 May 2024 05:39:02 -0500 Subject: [PATCH 111/126] Add `is not` and `not in` to python syntax (#10647) --- runtime/queries/python/highlights.scm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/queries/python/highlights.scm b/runtime/queries/python/highlights.scm index 0a082f2fd..9f7d2790c 100644 --- a/runtime/queries/python/highlights.scm +++ b/runtime/queries/python/highlights.scm @@ -215,9 +215,11 @@ [ "and" "or" + "not in" "in" "not" "del" + "is not" "is" ] @keyword.operator From 61818996c63cb752afb817259a90108f884db1cb Mon Sep 17 00:00:00 2001 From: Ashley Vaughn <59515175+ashl3y-v@users.noreply.github.com> Date: Sun, 5 May 2024 06:48:50 -0700 Subject: [PATCH 112/126] =?UTF-8?q?remove=20'=20and=20add=20=E2=9F=A8?= =?UTF-8?q?=E2=9F=A9=20in=20lean=20autopairs=20(#10688)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- languages.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/languages.toml b/languages.toml index ecf1b49fd..ec3e36ed6 100644 --- a/languages.toml +++ b/languages.toml @@ -1040,6 +1040,13 @@ block-comment-tokens = { start = "/-", end = "-/" } language-servers = [ "lean" ] indent = { tab-width = 2, unit = " " } +[language.auto-pairs] +'(' = ')' +'{' = '}' +'[' = ']' +'"' = '"' +'⟨' = '⟩' + [[grammar]] name = "lean" source = { git = "https://github.com/Julian/tree-sitter-lean", rev = "d98426109258b266e1e92358c5f11716d2e8f638" } From 7d1e5f18a297c126f0c03af90c7ee6131e90fd7c Mon Sep 17 00:00:00 2001 From: Silvan Schmidt Date: Mon, 6 May 2024 17:07:34 +0200 Subject: [PATCH 113/126] fix: update link in adding_languages.md (#10677) Previously, the link would point to the now moved "How to install the default language servers" page. The link now directly points to the up-to-date page. --- book/src/guides/adding_languages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 0763a3c5c..1a47b79c6 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -16,7 +16,7 @@ ## Language configuration > 💡 If you are adding a new Language Server configuration, make sure to update > the -> [Language Server Wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) +> [Language Server Wiki](https://github.com/helix-editor/helix/wiki/Language-Server-Configurations) > with the installation instructions. ## Grammar configuration From 50b13d1aeaa1100a65369087b0afbab0b18fe08e Mon Sep 17 00:00:00 2001 From: Guilherme Salustiano Date: Mon, 6 May 2024 17:09:21 +0200 Subject: [PATCH 114/126] docs[install/pre-build binaries]: add runtime setup (#10693) --- book/src/install.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/book/src/install.md b/book/src/install.md index 07865e698..0ba4f05f3 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -41,9 +41,9 @@ # Installing Helix ## Pre-built binaries -Download pre-built binaries from the -[GitHub Releases page](https://github.com/helix-editor/helix/releases). Add the binary to your system's `$PATH` to use it from the command -line. +Download pre-built binaries from the [GitHub Releases page](https://github.com/helix-editor/helix/releases). +Add the `hx` binary to your system's `$PATH` to use it from the command line, and copy the `runtime` directory into the config directory (for example `~/.config/helix/runtime` on Linux/macOS). +The runtime location can be overriden via the HELIX_RUNTIME environment variable. ## Linux, macOS, Windows and OpenBSD packaging status From 6876f923d5076ac3abe7da3d1459d7cf162eee00 Mon Sep 17 00:00:00 2001 From: Vladyslav Karasov <36513243+cotneit@users.noreply.github.com> Date: Mon, 6 May 2024 18:11:09 +0300 Subject: [PATCH 115/126] lang(json): make field key highlighting consistent with toml and yaml (#10676) --- runtime/queries/json/highlights.scm | 3 ++- runtime/queries/json5/highlights.scm | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/runtime/queries/json/highlights.scm b/runtime/queries/json/highlights.scm index 6df6c9ebc..17bb5f75b 100644 --- a/runtime/queries/json/highlights.scm +++ b/runtime/queries/json/highlights.scm @@ -4,8 +4,9 @@ ] @constant.builtin.boolean (null) @constant.builtin (number) @constant.numeric + (pair - key: (_) @keyword) + key: (_) @variable.other.member) (string) @string (escape_sequence) @constant.character.escape diff --git a/runtime/queries/json5/highlights.scm b/runtime/queries/json5/highlights.scm index 4bf03fe31..3ec4ee296 100644 --- a/runtime/queries/json5/highlights.scm +++ b/runtime/queries/json5/highlights.scm @@ -1,11 +1,20 @@ -(string) @string - -(identifier) @constant - +[ + (true) + (false) +] @constant.builtin.boolean +(null) @constant.builtin (number) @constant.numeric -(null) @constant.builtin - -[(true) (false)] @constant.builtin.boolean +(member + name: (_) @variable.other.member) +(string) @string (comment) @comment + +"," @punctuation.delimiter +[ + "[" + "]" + "{" + "}" +] @punctuation.bracket From f86f350d5d3d30c49224ef696bac1d624930d184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Laignel?= Date: Mon, 6 May 2024 17:37:04 +0200 Subject: [PATCH 116/126] Debugger template: allow missing or empty completion list (#10332) It can be convenient to define project specific debugger templates, some of which might not necessitate prompting the user to define completion. This commit makes completion optional for debugger templates and starts the dap immediately if undefined or empty. --- helix-core/src/syntax.rs | 1 + helix-term/src/commands/dap.rs | 60 +++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 3cf818f60..bbea9bbe7 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -510,6 +510,7 @@ pub enum DebugArgumentValue { pub struct DebugTemplate { pub name: String, pub request: String, + #[serde(default)] pub completion: Vec, pub args: HashMap, } diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index eda94c44f..0e50377ac 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -172,9 +172,9 @@ pub fn dap_start_impl( let mut args: HashMap<&str, Value> = HashMap::new(); - if let Some(params) = params { - for (k, t) in &template.args { - let mut value = t.clone(); + for (k, t) in &template.args { + let mut value = t.clone(); + if let Some(ref params) = params { for (i, x) in params.iter().enumerate() { let mut param = x.to_string(); if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) { @@ -198,22 +198,22 @@ pub fn dap_start_impl( DebugArgumentValue::Boolean(_) => value, }; } + } - match value { - DebugArgumentValue::String(string) => { - if let Ok(integer) = string.parse::() { - args.insert(k, to_value(integer).unwrap()); - } else { - args.insert(k, to_value(string).unwrap()); - } - } - DebugArgumentValue::Array(arr) => { - args.insert(k, to_value(arr).unwrap()); - } - DebugArgumentValue::Boolean(bool) => { - args.insert(k, to_value(bool).unwrap()); + match value { + DebugArgumentValue::String(string) => { + if let Ok(integer) = string.parse::() { + args.insert(k, to_value(integer).unwrap()); + } else { + args.insert(k, to_value(string).unwrap()); } } + DebugArgumentValue::Array(arr) => { + args.insert(k, to_value(arr).unwrap()); + } + DebugArgumentValue::Boolean(bool) => { + args.insert(k, to_value(bool).unwrap()); + } } } @@ -272,17 +272,23 @@ pub fn dap_launch(cx: &mut Context) { templates, (), |cx, template, _action| { - let completions = template.completion.clone(); - let name = template.name.clone(); - let callback = Box::pin(async move { - let call: Callback = - Callback::EditorCompositor(Box::new(move |_editor, compositor| { - let prompt = debug_parameter_prompt(completions, name, Vec::new()); - compositor.push(Box::new(prompt)); - })); - Ok(call) - }); - cx.jobs.callback(callback); + if template.completion.is_empty() { + if let Err(err) = dap_start_impl(cx, Some(&template.name), None, None) { + cx.editor.set_error(err.to_string()); + } + } else { + let completions = template.completion.clone(); + let name = template.name.clone(); + let callback = Box::pin(async move { + let call: Callback = + Callback::EditorCompositor(Box::new(move |_editor, compositor| { + let prompt = debug_parameter_prompt(completions, name, Vec::new()); + compositor.push(Box::new(prompt)); + })); + Ok(call) + }); + cx.jobs.callback(callback); + } }, )))); } From a959c0ef9be59e63617c8e95d1ac59889d45a8b5 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Mon, 6 May 2024 16:39:06 +0100 Subject: [PATCH 117/126] Improve the structure of the documentation (#10619) --- book/src/SUMMARY.md | 7 + book/src/building-from-source.md | 158 +++++++++++++ book/src/configuration.md | 369 ----------------------------- book/src/editor.md | 386 +++++++++++++++++++++++++++++++ book/src/install.md | 314 +------------------------ book/src/keymap.md | 2 +- book/src/package-managers.md | 150 ++++++++++++ book/src/registers.md | 54 +++++ book/src/surround.md | 24 ++ book/src/syntax-aware-motions.md | 68 ++++++ book/src/textobjects.md | 47 ++++ book/src/usage.md | 199 ---------------- 12 files changed, 896 insertions(+), 882 deletions(-) create mode 100644 book/src/building-from-source.md create mode 100644 book/src/editor.md create mode 100644 book/src/package-managers.md create mode 100644 book/src/registers.md create mode 100644 book/src/surround.md create mode 100644 book/src/syntax-aware-motions.md create mode 100644 book/src/textobjects.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index ba330cf77..027b885a8 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -3,12 +3,19 @@ # Summary [Helix](./title-page.md) - [Installation](./install.md) + - [Package Managers](./package-managers.md) + - [Building from source](./building-from-source.md) - [Usage](./usage.md) + - [Registers](./registers.md) + - [Surround](./surround.md) + - [Textobjects](./textobjects.md) + - [Syntax aware motions](./syntax-aware-motions.md) - [Keymap](./keymap.md) - [Commands](./commands.md) - [Language support](./lang-support.md) - [Migrating from Vim](./from-vim.md) - [Configuration](./configuration.md) + - [Editor](./editor.md) - [Themes](./themes.md) - [Key remapping](./remapping.md) - [Languages](./languages.md) diff --git a/book/src/building-from-source.md b/book/src/building-from-source.md new file mode 100644 index 000000000..666917ea4 --- /dev/null +++ b/book/src/building-from-source.md @@ -0,0 +1,158 @@ +## Building from source + +- [Configuring Helix's runtime files](#configuring-helixs-runtime-files) + - [Linux and macOS](#linux-and-macos) + - [Windows](#windows) + - [Multiple runtime directories](#multiple-runtime-directories) + - [Note to packagers](#note-to-packagers) +- [Validating the installation](#validating-the-installation) +- [Configure the desktop shortcut](#configure-the-desktop-shortcut) + +Requirements: + +Clone the Helix GitHub repository into a directory of your choice. The +examples in this documentation assume installation into either `~/src/` on +Linux and macOS, or `%userprofile%\src\` on Windows. + +- The [Rust toolchain](https://www.rust-lang.org/tools/install) +- The [Git version control system](https://git-scm.com/) +- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang + +If you are using the `musl-libc` standard library instead of `glibc` the following environment variable must be set during the build to ensure tree-sitter grammars can be loaded correctly: + +```sh +RUSTFLAGS="-C target-feature=-crt-static" +``` + +1. Clone the repository: + + ```sh + git clone https://github.com/helix-editor/helix + cd helix + ``` + +2. Compile from source: + + ```sh + cargo install --path helix-term --locked + ``` + + 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 +> the `runtime` directory within the user's helix config directory (more +> [details below](#multiple-runtime-directories)). + +### Configuring Helix's runtime files + +#### Linux and macOS + +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 +export HELIX_RUNTIME=~/src/helix/runtime +``` + +Or, create a symbolic link: + +```sh +ln -Ts $PWD/runtime ~/.config/helix/runtime +``` + +If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again. + +#### Windows + +Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for +`Edit environment variables for your account`) or use the `setx` command in +Cmd: + +```sh +setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime" +``` + +> 💡 `%userprofile%` resolves to your user directory like +> `C:\Users\Your-Name\` for example. + +Or, create a symlink in `%appdata%\helix\` that links to the source code directory: + +| Method | Command | +| ---------- | -------------------------------------------------------------------------------------- | +| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` | +| Cmd | `cd %appdata%\helix`
`mklink /D runtime "%userprofile%\src\helix\runtime"` | + +> 💡 On Windows, creating a symbolic link may require running PowerShell or +> Cmd as an administrator. + +#### Multiple runtime directories + +When Helix finds multiple runtime directories it will search through them for files in the +following order: + +1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for + developing and testing helix only). +2. `runtime/` subdirectory of OS-dependent helix user config directory. +3. `$HELIX_RUNTIME` +4. Distribution-specific fallback directory (set at compile time—not run time— + with the `HELIX_DEFAULT_RUNTIME` environment variable) +5. `runtime/` subdirectory of path to Helix executable. + +This order also sets the priority for selecting which file will be used if multiple runtime +directories have files with the same name. + +#### Note to packagers + +If you are making a package of Helix for end users, to provide a good out of +the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment +variable at build time (before invoking `cargo build`) to a directory which +will store the final runtime files after installation. For example, say you want +to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build +script could follow are: + +1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime` +1. `cargo build --profile opt --locked --path helix-term` +1. `cp -r runtime $BUILD_DIR/usr/lib/helix/` +1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx` + +This way the resulting `hx` binary will always look for its runtime directory in +`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix` +or `HELIX_RUNTIME`. + +### Validating the installation + +To make sure everything is set up as expected you should run the Helix health +check: + +```sh +hx --health +``` + +For more information on the health check results refer to +[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck). + +### Configure the desktop shortcut + +If your desktop environment supports the +[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html) +you can configure Helix to show up in the application menu by copying the +provided `.desktop` and icon files to their correct folders: + +```sh +cp contrib/Helix.desktop ~/.local/share/applications +cp contrib/helix.png ~/.icons # or ~/.local/share/icons +``` + +To use another terminal than the system default, you can modify the `.desktop` +file. For example, to use `kitty`: + +```sh +sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop +sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop +``` diff --git a/book/src/configuration.md b/book/src/configuration.md index 5e22cebf8..0cd12568b 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -33,372 +33,3 @@ # Configuration Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository. Its settings will be merged with the configuration directory `config.toml` and the built-in configuration. -## Editor - -### `[editor]` Section - -| Key | Description | Default | -|--|--|---------| -| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling | `5` | -| `mouse` | Enable mouse mode | `true` | -| `middle-click-paste` | Middle click paste support | `true` | -| `scroll-lines` | Number of lines to scroll per scroll wheel step | `3` | -| `shell` | Shell to use when running external commands | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | -| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers | `absolute` | -| `cursorline` | Highlight all lines with a cursor | `false` | -| `cursorcolumn` | Highlight all columns with a cursor | `false` | -| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | -| `auto-completion` | Enable automatic pop up of auto-completion | `true` | -| `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. | `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` | -| `auto-info` | Whether to display info boxes | `true` | -| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` | -| `undercurl` | Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative | `false` | -| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` | -| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | -| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | -| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` | -| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | -| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` | -| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` | -| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | -| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid` -| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"` - -### `[editor.statusline]` Section - -Allows configuring the statusline at the bottom of the editor. - -The configuration distinguishes between three areas of the status line: - -`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]` - -Statusline elements can be defined as follows: - -```toml -[editor.statusline] -left = ["mode", "spinner"] -center = ["file-name"] -right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"] -separator = "│" -mode.normal = "NORMAL" -mode.insert = "INSERT" -mode.select = "SELECT" -``` -The `[editor.statusline]` key takes the following sub-keys: - -| Key | Description | Default | -| --- | --- | --- | -| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` | -| `center` | A list of elements aligned to the middle of the statusline | `[]` | -| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` | -| `separator` | The character used to separate elements in the statusline | `"│"` | -| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` | -| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` | -| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` | - -The following statusline elements can be configured: - -| Key | Description | -| ------ | ----------- | -| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) | -| `spinner` | A progress spinner indicating LSP activity | -| `file-name` | The path/name of the opened file | -| `file-absolute-path` | The absolute path/name of the opened file | -| `file-base-name` | The basename of the opened file | -| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) | -| `file-encoding` | The encoding of the opened file if it differs from UTF-8 | -| `file-line-ending` | The file line endings (CRLF or LF) | -| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written | -| `total-line-numbers` | The total line numbers of the opened file | -| `file-type` | The type of the opened file | -| `diagnostics` | The number of warnings and/or errors | -| `workspace-diagnostics` | The number of warnings and/or errors on workspace | -| `selections` | The number of active selections | -| `primary-selection-length` | The number of characters currently in primary selection | -| `position` | The cursor position | -| `position-percentage` | The cursor position as a percentage of the total number of lines | -| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) | -| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) | -| `version-control` | The current branch name or detached commit hash of the opened workspace | -| `register` | The current selected register | - -### `[editor.lsp]` Section - -| Key | Description | Default | -| --- | ----------- | ------- | -| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` | -| `display-messages` | Display LSP progress messages below statusline[^1] | `false` | -| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | -| `display-inlay-hints` | Display inlay hints[^2] | `false` | -| `display-signature-help-docs` | Display docs under signature help popup | `true` | -| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` | -| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` | - -[^1]: By default, a progress spinner is shown in the statusline beside the file path. - -[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them! - -### `[editor.cursor-shape]` Section - -Defines the shape of cursor in each mode. -Valid values for these options are `block`, `bar`, `underline`, or `hidden`. - -> 💡 Due to limitations of the terminal environment, only the primary cursor can -> change shape. - -| Key | Description | Default | -| --- | ----------- | ------- | -| `normal` | Cursor shape in [normal mode][normal mode] | `block` | -| `insert` | Cursor shape in [insert mode][insert mode] | `block` | -| `select` | Cursor shape in [select mode][select mode] | `block` | - -[normal mode]: ./keymap.md#normal-mode -[insert mode]: ./keymap.md#insert-mode -[select mode]: ./keymap.md#select--extend-mode - -### `[editor.file-picker]` Section - -Set options for file picker and global search. Ignoring a file means it is -not visible in the Helix file picker and global search. - -All git related options are only enabled in a git repository. - -| Key | Description | Default | -|--|--|---------| -|`hidden` | Enables ignoring hidden files | `true` -|`follow-symlinks` | Follow symlinks instead of ignoring them | `true` -|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `true` -|`parents` | Enables reading ignore files from parent directories | `true` -|`ignore` | Enables reading `.ignore` files | `true` -|`git-ignore` | Enables reading `.gitignore` files | `true` -|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true` -|`git-exclude` | Enables reading `.git/info/exclude` files | `true` -|`max-depth` | Set with an integer value for maximum depth to recurse | Unset by default - -Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files. - -Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory: -- Linux and Mac: `~/.config/helix/ignore` -- Windows: `%AppData%\helix\ignore` - -Example: - -```ini -# unignore in file picker and global search -!.github/ -!.gitignore -!.gitattributes -``` - -### `[editor.auto-pairs]` Section - -Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a -simple boolean value, or a specific mapping of pairs of single characters. - -To disable auto-pairs altogether, set `auto-pairs` to `false`: - -```toml -[editor] -auto-pairs = false # defaults to `true` -``` - -The default pairs are (){}[]''""``, but these can be customized by -setting `auto-pairs` to a TOML table: - -```toml -[editor.auto-pairs] -'(' = ')' -'{' = '}' -'[' = ']' -'"' = '"' -'`' = '`' -'<' = '>' -``` - -Additionally, this setting can be used in a language config. Unless -the editor setting is `false`, this will override the editor config in -documents with this language. - -Example `languages.toml` that adds `<>` and removes `''` - -```toml -[[language]] -name = "rust" - -[language.auto-pairs] -'(' = ')' -'{' = '}' -'[' = ']' -'"' = '"' -'`' = '`' -'<' = '>' -``` - -### `[editor.search]` Section - -Search specific options. - -| Key | Description | Default | -|--|--|---------| -| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` | -| `wrap-around`| Whether the search should wrap after depleting the matches | `true` | - -### `[editor.whitespace]` Section - -Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace. - -| Key | Description | Default | -|-----|-------------|---------| -| `render` | Whether to render whitespace. May either be `all` or `none`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `none` | -| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below | - -Example - -```toml -[editor.whitespace] -render = "all" -# or control each character -[editor.whitespace.render] -space = "all" -tab = "all" -nbsp = "none" -nnbsp = "none" -newline = "none" - -[editor.whitespace.characters] -space = "·" -nbsp = "⍽" -nnbsp = "␣" -tab = "→" -newline = "⏎" -tabpad = "·" # Tabs will look like "→···" (depending on tab width) -``` - -### `[editor.indent-guides]` Section - -Options for rendering vertical indent guides. - -| Key | Description | Default | -| --- | --- | --- | -| `render` | Whether to render indent guides | `false` | -| `character` | Literal character to use for rendering the indent guide | `│` | -| `skip-levels` | Number of indent levels to skip | `0` | - -Example: - -```toml -[editor.indent-guides] -render = true -character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽" -skip-levels = 1 -``` - -### `[editor.gutters]` Section - -For simplicity, `editor.gutters` accepts an array of gutter types, which will -use default settings for all gutter components. - -```toml -[editor] -gutters = ["diff", "diagnostics", "line-numbers", "spacer"] -``` - -To customize the behavior of gutters, the `[editor.gutters]` section must -be used. This section contains top level settings, as well as settings for -specific gutter components as subsections. - -| Key | Description | Default | -| --- | --- | --- | -| `layout` | A vector of gutters to display | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | - -Example: - -```toml -[editor.gutters] -layout = ["diff", "diagnostics", "line-numbers", "spacer"] -``` - -#### `[editor.gutters.line-numbers]` Section - -Options for the line number gutter - -| Key | Description | Default | -| --- | --- | --- | -| `min-width` | The minimum number of characters to use | `3` | - -Example: - -```toml -[editor.gutters.line-numbers] -min-width = 1 -``` - -#### `[editor.gutters.diagnostics]` Section - -Currently unused - -#### `[editor.gutters.diff]` Section - -The `diff` gutter option displays colored bars indicating whether a `git` diff represents that a line was added, removed or changed. -These colors are controlled by the theme attributes `diff.plus`, `diff.minus` and `diff.delta`. - -Other diff providers will eventually be supported by a future plugin system. - -There are currently no options for this section. - -#### `[editor.gutters.spacer]` Section - -Currently unused - -### `[editor.soft-wrap]` Section - -Options for soft wrapping lines that exceed the view width: - -| Key | Description | Default | -| --- | --- | --- | -| `enable` | Whether soft wrapping is enabled. | `false` | -| `max-wrap` | Maximum free space left at the end of the line. | `20` | -| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` | -| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` | -| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` | - -Example: - -```toml -[editor.soft-wrap] -enable = true -max-wrap = 25 # increase value to reduce forced mid-word wrapping -max-indent-retain = 0 -wrap-indicator = "" # set wrap-indicator to "" to hide it -``` - -### `[editor.smart-tab]` Section - -Options for navigating and editing using tab key. - -| Key | Description | Default | -|------------|-------------|---------| -| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` | -| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` | - - -Due to lack of support for S-tab in some terminals, the default keybindings don't fully embrace smart-tab editing experience. If you enjoy smart-tab navigation and a terminal that supports the [Enhanced Keyboard protocol](https://github.com/helix-editor/helix/wiki/Terminal-Support#enhanced-keyboard-protocol), consider setting extra keybindings: - -``` -[keys.normal] -tab = "move_parent_node_end" -S-tab = "move_parent_node_start" - -[keys.insert] -S-tab = "move_parent_node_start" - -[keys.select] -tab = "extend_parent_node_end" -S-tab = "extend_parent_node_start" -``` diff --git a/book/src/editor.md b/book/src/editor.md new file mode 100644 index 000000000..88541a7bc --- /dev/null +++ b/book/src/editor.md @@ -0,0 +1,386 @@ +## Editor + +- [`[editor]` Section](#editor-section) +- [`[editor.statusline]` Section](#editorstatusline-section) +- [`[editor.lsp]` Section](#editorlsp-section) +- [`[editor.cursor-shape]` Section](#editorcursor-shape-section) +- [`[editor.file-picker]` Section](#editorfile-picker-section) +- [`[editor.auto-pairs]` Section](#editorauto-pairs-section) +- [`[editor.search]` Section](#editorsearch-section) +- [`[editor.whitespace]` Section](#editorwhitespace-section) +- [`[editor.indent-guides]` Section](#editorindent-guides-section) +- [`[editor.gutters]` Section](#editorgutters-section) + - [`[editor.gutters.line-numbers]` Section](#editorguttersline-numbers-section) + - [`[editor.gutters.diagnostics]` Section](#editorguttersdiagnostics-section) + - [`[editor.gutters.diff]` Section](#editorguttersdiff-section) + - [`[editor.gutters.spacer]` Section](#editorguttersspacer-section) +- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section) +- [`[editor.smart-tab]` Section](#editorsmart-tab-section) + +### `[editor]` Section + +| Key | Description | Default | +|--|--|---------| +| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling | `5` | +| `mouse` | Enable mouse mode | `true` | +| `middle-click-paste` | Middle click paste support | `true` | +| `scroll-lines` | Number of lines to scroll per scroll wheel step | `3` | +| `shell` | Shell to use when running external commands | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | +| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers | `absolute` | +| `cursorline` | Highlight all lines with a cursor | `false` | +| `cursorcolumn` | Highlight all columns with a cursor | `false` | +| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | +| `auto-completion` | Enable automatic pop up of auto-completion | `true` | +| `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. | `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` | +| `auto-info` | Whether to display info boxes | `true` | +| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` | +| `undercurl` | Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative | `false` | +| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` | +| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | +| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | +| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` | +| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | +| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` | +| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` | +| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | +| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid` +| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"` + +### `[editor.statusline]` Section + +Allows configuring the statusline at the bottom of the editor. + +The configuration distinguishes between three areas of the status line: + +`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]` + +Statusline elements can be defined as follows: + +```toml +[editor.statusline] +left = ["mode", "spinner"] +center = ["file-name"] +right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"] +separator = "│" +mode.normal = "NORMAL" +mode.insert = "INSERT" +mode.select = "SELECT" +``` +The `[editor.statusline]` key takes the following sub-keys: + +| Key | Description | Default | +| --- | --- | --- | +| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` | +| `center` | A list of elements aligned to the middle of the statusline | `[]` | +| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` | +| `separator` | The character used to separate elements in the statusline | `"│"` | +| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` | +| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` | +| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` | + +The following statusline elements can be configured: + +| Key | Description | +| ------ | ----------- | +| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) | +| `spinner` | A progress spinner indicating LSP activity | +| `file-name` | The path/name of the opened file | +| `file-absolute-path` | The absolute path/name of the opened file | +| `file-base-name` | The basename of the opened file | +| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) | +| `file-encoding` | The encoding of the opened file if it differs from UTF-8 | +| `file-line-ending` | The file line endings (CRLF or LF) | +| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written | +| `total-line-numbers` | The total line numbers of the opened file | +| `file-type` | The type of the opened file | +| `diagnostics` | The number of warnings and/or errors | +| `workspace-diagnostics` | The number of warnings and/or errors on workspace | +| `selections` | The number of active selections | +| `primary-selection-length` | The number of characters currently in primary selection | +| `position` | The cursor position | +| `position-percentage` | The cursor position as a percentage of the total number of lines | +| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) | +| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) | +| `version-control` | The current branch name or detached commit hash of the opened workspace | +| `register` | The current selected register | + +### `[editor.lsp]` Section + +| Key | Description | Default | +| --- | ----------- | ------- | +| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` | +| `display-messages` | Display LSP progress messages below statusline[^1] | `false` | +| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | +| `display-inlay-hints` | Display inlay hints[^2] | `false` | +| `display-signature-help-docs` | Display docs under signature help popup | `true` | +| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` | +| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` | + +[^1]: By default, a progress spinner is shown in the statusline beside the file path. + +[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them! + +### `[editor.cursor-shape]` Section + +Defines the shape of cursor in each mode. +Valid values for these options are `block`, `bar`, `underline`, or `hidden`. + +> 💡 Due to limitations of the terminal environment, only the primary cursor can +> change shape. + +| Key | Description | Default | +| --- | ----------- | ------- | +| `normal` | Cursor shape in [normal mode][normal mode] | `block` | +| `insert` | Cursor shape in [insert mode][insert mode] | `block` | +| `select` | Cursor shape in [select mode][select mode] | `block` | + +[normal mode]: ./keymap.md#normal-mode +[insert mode]: ./keymap.md#insert-mode +[select mode]: ./keymap.md#select--extend-mode + +### `[editor.file-picker]` Section + +Set options for file picker and global search. Ignoring a file means it is +not visible in the Helix file picker and global search. + +All git related options are only enabled in a git repository. + +| Key | Description | Default | +|--|--|---------| +|`hidden` | Enables ignoring hidden files | `true` +|`follow-symlinks` | Follow symlinks instead of ignoring them | `true` +|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `true` +|`parents` | Enables reading ignore files from parent directories | `true` +|`ignore` | Enables reading `.ignore` files | `true` +|`git-ignore` | Enables reading `.gitignore` files | `true` +|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true` +|`git-exclude` | Enables reading `.git/info/exclude` files | `true` +|`max-depth` | Set with an integer value for maximum depth to recurse | Unset by default + +Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files. + +Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory: +- Linux and Mac: `~/.config/helix/ignore` +- Windows: `%AppData%\helix\ignore` + +Example: + +```ini +# unignore in file picker and global search +!.github/ +!.gitignore +!.gitattributes +``` + +### `[editor.auto-pairs]` Section + +Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a +simple boolean value, or a specific mapping of pairs of single characters. + +To disable auto-pairs altogether, set `auto-pairs` to `false`: + +```toml +[editor] +auto-pairs = false # defaults to `true` +``` + +The default pairs are (){}[]''""``, but these can be customized by +setting `auto-pairs` to a TOML table: + +```toml +[editor.auto-pairs] +'(' = ')' +'{' = '}' +'[' = ']' +'"' = '"' +'`' = '`' +'<' = '>' +``` + +Additionally, this setting can be used in a language config. Unless +the editor setting is `false`, this will override the editor config in +documents with this language. + +Example `languages.toml` that adds `<>` and removes `''` + +```toml +[[language]] +name = "rust" + +[language.auto-pairs] +'(' = ')' +'{' = '}' +'[' = ']' +'"' = '"' +'`' = '`' +'<' = '>' +``` + +### `[editor.search]` Section + +Search specific options. + +| Key | Description | Default | +|--|--|---------| +| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` | +| `wrap-around`| Whether the search should wrap after depleting the matches | `true` | + +### `[editor.whitespace]` Section + +Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace. + +| Key | Description | Default | +|-----|-------------|---------| +| `render` | Whether to render whitespace. May either be `all` or `none`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `none` | +| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below | + +Example + +```toml +[editor.whitespace] +render = "all" +# or control each character +[editor.whitespace.render] +space = "all" +tab = "all" +nbsp = "none" +nnbsp = "none" +newline = "none" + +[editor.whitespace.characters] +space = "·" +nbsp = "⍽" +nnbsp = "␣" +tab = "→" +newline = "⏎" +tabpad = "·" # Tabs will look like "→···" (depending on tab width) +``` + +### `[editor.indent-guides]` Section + +Options for rendering vertical indent guides. + +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides | `false` | +| `character` | Literal character to use for rendering the indent guide | `│` | +| `skip-levels` | Number of indent levels to skip | `0` | + +Example: + +```toml +[editor.indent-guides] +render = true +character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽" +skip-levels = 1 +``` + +### `[editor.gutters]` Section + +For simplicity, `editor.gutters` accepts an array of gutter types, which will +use default settings for all gutter components. + +```toml +[editor] +gutters = ["diff", "diagnostics", "line-numbers", "spacer"] +``` + +To customize the behavior of gutters, the `[editor.gutters]` section must +be used. This section contains top level settings, as well as settings for +specific gutter components as subsections. + +| Key | Description | Default | +| --- | --- | --- | +| `layout` | A vector of gutters to display | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | + +Example: + +```toml +[editor.gutters] +layout = ["diff", "diagnostics", "line-numbers", "spacer"] +``` + +#### `[editor.gutters.line-numbers]` Section + +Options for the line number gutter + +| Key | Description | Default | +| --- | --- | --- | +| `min-width` | The minimum number of characters to use | `3` | + +Example: + +```toml +[editor.gutters.line-numbers] +min-width = 1 +``` + +#### `[editor.gutters.diagnostics]` Section + +Currently unused + +#### `[editor.gutters.diff]` Section + +The `diff` gutter option displays colored bars indicating whether a `git` diff represents that a line was added, removed or changed. +These colors are controlled by the theme attributes `diff.plus`, `diff.minus` and `diff.delta`. + +Other diff providers will eventually be supported by a future plugin system. + +There are currently no options for this section. + +#### `[editor.gutters.spacer]` Section + +Currently unused + +### `[editor.soft-wrap]` Section + +Options for soft wrapping lines that exceed the view width: + +| Key | Description | Default | +| --- | --- | --- | +| `enable` | Whether soft wrapping is enabled. | `false` | +| `max-wrap` | Maximum free space left at the end of the line. | `20` | +| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` | +| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` | +| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` | + +Example: + +```toml +[editor.soft-wrap] +enable = true +max-wrap = 25 # increase value to reduce forced mid-word wrapping +max-indent-retain = 0 +wrap-indicator = "" # set wrap-indicator to "" to hide it +``` + +### `[editor.smart-tab]` Section + +Options for navigating and editing using tab key. + +| Key | Description | Default | +|------------|-------------|---------| +| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` | +| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` | + + +Due to lack of support for S-tab in some terminals, the default keybindings don't fully embrace smart-tab editing experience. If you enjoy smart-tab navigation and a terminal that supports the [Enhanced Keyboard protocol](https://github.com/helix-editor/helix/wiki/Terminal-Support#enhanced-keyboard-protocol), consider setting extra keybindings: + +``` +[keys.normal] +tab = "move_parent_node_end" +S-tab = "move_parent_node_start" + +[keys.insert] +S-tab = "move_parent_node_start" + +[keys.select] +tab = "extend_parent_node_end" +S-tab = "extend_parent_node_start" +``` diff --git a/book/src/install.md b/book/src/install.md index 0ba4f05f3..debc82b01 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -1,38 +1,10 @@ # Installing Helix - -- [Pre-built binaries](#pre-built-binaries) -- [Linux, macOS, Windows and OpenBSD packaging status](#linux-macos-windows-and-openbsd-packaging-status) -- [Linux](#linux) - - [Ubuntu](#ubuntu) - - [Fedora/RHEL](#fedorarhel) - - [Arch Linux extra](#arch-linux-extra) - - [NixOS](#nixos) - - [Flatpak](#flatpak) - - [Snap](#snap) - - [AppImage](#appimage) -- [macOS](#macos) - - [Homebrew Core](#homebrew-core) - - [MacPorts](#macports) -- [Windows](#windows) - - [Winget](#winget) - - [Scoop](#scoop) - - [Chocolatey](#chocolatey) - - [MSYS2](#msys2) -- [Building from source](#building-from-source) - - [Configuring Helix's runtime files](#configuring-helixs-runtime-files) - - [Linux and macOS](#linux-and-macos) - - [Windows](#windows) - - [Multiple runtime directories](#multiple-runtime-directories) - - [Validating the installation](#validating-the-installation) - - [Configure the desktop shortcut](#configure-the-desktop-shortcut) - - To install Helix, follow the instructions specific to your operating system. Note that: - To get the latest nightly version of Helix, you need to - [build from source](#building-from-source). + [build from source](./building-from-source.md). - To take full advantage of Helix, install the language servers for your preferred programming languages. See the @@ -45,287 +17,3 @@ ## Pre-built binaries Add the `hx` binary to your system's `$PATH` to use it from the command line, and copy the `runtime` directory into the config directory (for example `~/.config/helix/runtime` on Linux/macOS). The runtime location can be overriden via the HELIX_RUNTIME environment variable. -## Linux, macOS, Windows and OpenBSD packaging status - -[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) - -## Linux - -The following third party repositories are available: - -### Ubuntu - -Add the `PPA` for Helix: - -```sh -sudo add-apt-repository ppa:maveonair/helix-editor -sudo apt update -sudo apt install helix -``` - -### Fedora/RHEL - -```sh -sudo dnf install helix -``` - -### Arch Linux extra - -Releases are available in the `extra` repository: - -```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. - -### NixOS - -Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute, -the unstable channel usually carries the latest release. - -Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project -root. Use `nix develop` to spin up a reproducible development shell. Outputs are -cached for each push to master using [Cachix](https://www.cachix.org/). The -flake is configured to automatically make use of this cache assuming the user -accepts the new settings on first use. - -If you are using a version of Nix without flakes enabled, -[install Cachix CLI](https://docs.cachix.org/installation) and use -`cachix use helix` to configure Nix to use cached outputs when possible. - -### Flatpak - -Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix): - -```sh -flatpak install flathub com.helix_editor.Helix -flatpak run com.helix_editor.Helix -``` - -### Snap - -Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with: - -```sh -snap install --classic helix -``` - -This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`. - -### AppImage - -Install Helix using the Linux [AppImage](https://appimage.org/) format. -Download the official Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page. - -```sh -chmod +x helix-*.AppImage # change permission for executable mode -./helix-*.AppImage # run helix -``` - -## macOS - -### Homebrew Core - -```sh -brew install helix -``` - -### MacPorts - -```sh -port install helix -``` - -## Windows - -Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/) -or [MSYS2](https://msys2.org/). - -### Winget -Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer. -You can get [App Installer from the Microsoft Store](https://www.microsoft.com/p/app-installer/9nblggh4nns1#activetab=pivot:overviewtab). If it's already installed, make sure it is updated with the latest version. - -```sh -winget install Helix.Helix -``` - -### Scoop - -```sh -scoop install helix -``` - -### Chocolatey - -```sh -choco install helix -``` - -### MSYS2 - -For 64-bit Windows 8.1 or above: - -```sh -pacman -S mingw-w64-ucrt-x86_64-helix -``` - -## Building from source - -Requirements: - -Clone the Helix GitHub repository into a directory of your choice. The -examples in this documentation assume installation into either `~/src/` on -Linux and macOS, or `%userprofile%\src\` on Windows. - -- The [Rust toolchain](https://www.rust-lang.org/tools/install) -- The [Git version control system](https://git-scm.com/) -- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang - -If you are using the `musl-libc` standard library instead of `glibc` the following environment variable must be set during the build to ensure tree-sitter grammars can be loaded correctly: - -```sh -RUSTFLAGS="-C target-feature=-crt-static" -``` - -1. Clone the repository: - - ```sh - git clone https://github.com/helix-editor/helix - cd helix - ``` - -2. Compile from source: - - ```sh - cargo install --path helix-term --locked - ``` - - 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 -> the `runtime` directory within the user's helix config directory (more -> [details below](#multiple-runtime-directories)). - -### Configuring Helix's runtime files - -#### Linux and macOS - -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 -export HELIX_RUNTIME=~/src/helix/runtime -``` - -Or, create a symbolic link: - -```sh -ln -Ts $PWD/runtime ~/.config/helix/runtime -``` - -If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again. - -#### Windows - -Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for -`Edit environment variables for your account`) or use the `setx` command in -Cmd: - -```sh -setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime" -``` - -> 💡 `%userprofile%` resolves to your user directory like -> `C:\Users\Your-Name\` for example. - -Or, create a symlink in `%appdata%\helix\` that links to the source code directory: - -| Method | Command | -| ---------- | -------------------------------------------------------------------------------------- | -| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` | -| Cmd | `cd %appdata%\helix`
`mklink /D runtime "%userprofile%\src\helix\runtime"` | - -> 💡 On Windows, creating a symbolic link may require running PowerShell or -> Cmd as an administrator. - -#### Multiple runtime directories - -When Helix finds multiple runtime directories it will search through them for files in the -following order: - -1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for - developing and testing helix only). -2. `runtime/` subdirectory of OS-dependent helix user config directory. -3. `$HELIX_RUNTIME` -4. Distribution-specific fallback directory (set at compile time—not run time— - with the `HELIX_DEFAULT_RUNTIME` environment variable) -5. `runtime/` subdirectory of path to Helix executable. - -This order also sets the priority for selecting which file will be used if multiple runtime -directories have files with the same name. - -#### Note to packagers - -If you are making a package of Helix for end users, to provide a good out of -the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment -variable at build time (before invoking `cargo build`) to a directory which -will store the final runtime files after installation. For example, say you want -to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build -script could follow are: - -1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime` -1. `cargo build --profile opt --locked --path helix-term` -1. `cp -r runtime $BUILD_DIR/usr/lib/helix/` -1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx` - -This way the resulting `hx` binary will always look for its runtime directory in -`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix` -or `HELIX_RUNTIME`. - -### Validating the installation - -To make sure everything is set up as expected you should run the Helix health -check: - -```sh -hx --health -``` - -For more information on the health check results refer to -[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck). - -### Configure the desktop shortcut - -If your desktop environment supports the -[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html) -you can configure Helix to show up in the application menu by copying the -provided `.desktop` and icon files to their correct folders: - -```sh -cp contrib/Helix.desktop ~/.local/share/applications -cp contrib/helix.png ~/.icons # or ~/.local/share/icons -``` - -To use another terminal than the system default, you can modify the `.desktop` -file. For example, to use `kitty`: - -```sh -sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop -sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop -``` diff --git a/book/src/keymap.md b/book/src/keymap.md index 55b467a0e..9a6fecbab 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -1,4 +1,4 @@ -# Keymap +## Keymap - [Normal mode](#normal-mode) - [Movement](#movement) diff --git a/book/src/package-managers.md b/book/src/package-managers.md new file mode 100644 index 000000000..99d2a3500 --- /dev/null +++ b/book/src/package-managers.md @@ -0,0 +1,150 @@ +## Package managers + +- [Linux](#linux) + - [Ubuntu](#ubuntu) + - [Fedora/RHEL](#fedorarhel) + - [Arch Linux extra](#arch-linux-extra) + - [NixOS](#nixos) + - [Flatpak](#flatpak) + - [Snap](#snap) + - [AppImage](#appimage) +- [macOS](#macos) + - [Homebrew Core](#homebrew-core) + - [MacPorts](#macports) +- [Windows](#windows) + - [Winget](#winget) + - [Scoop](#scoop) + - [Chocolatey](#chocolatey) + - [MSYS2](#msys2) + +[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) + +## Linux + +The following third party repositories are available: + +### Ubuntu + +Add the `PPA` for Helix: + +```sh +sudo add-apt-repository ppa:maveonair/helix-editor +sudo apt update +sudo apt install helix +``` + +### Fedora/RHEL + +```sh +sudo dnf install helix +``` + +### Arch Linux extra + +Releases are available in the `extra` repository: + +```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. + +### NixOS + +Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute, +the unstable channel usually carries the latest release. + +Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project +root. Use `nix develop` to spin up a reproducible development shell. Outputs are +cached for each push to master using [Cachix](https://www.cachix.org/). The +flake is configured to automatically make use of this cache assuming the user +accepts the new settings on first use. + +If you are using a version of Nix without flakes enabled, +[install Cachix CLI](https://docs.cachix.org/installation) and use +`cachix use helix` to configure Nix to use cached outputs when possible. + +### Flatpak + +Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix): + +```sh +flatpak install flathub com.helix_editor.Helix +flatpak run com.helix_editor.Helix +``` + +### Snap + +Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with: + +```sh +snap install --classic helix +``` + +This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`. + +### AppImage + +Install Helix using the Linux [AppImage](https://appimage.org/) format. +Download the official Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page. + +```sh +chmod +x helix-*.AppImage # change permission for executable mode +./helix-*.AppImage # run helix +``` + +## macOS + +### Homebrew Core + +```sh +brew install helix +``` + +### MacPorts + +```sh +port install helix +``` + +## Windows + +Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/) +or [MSYS2](https://msys2.org/). + +### Winget +Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer. +You can get [App Installer from the Microsoft Store](https://www.microsoft.com/p/app-installer/9nblggh4nns1#activetab=pivot:overviewtab). If it's already installed, make sure it is updated with the latest version. + +```sh +winget install Helix.Helix +``` + +### Scoop + +```sh +scoop install helix +``` + +### Chocolatey + +```sh +choco install helix +``` + +### MSYS2 + +For 64-bit Windows 8.1 or above: + +```sh +pacman -S mingw-w64-ucrt-x86_64-helix +``` diff --git a/book/src/registers.md b/book/src/registers.md new file mode 100644 index 000000000..2edf7e27a --- /dev/null +++ b/book/src/registers.md @@ -0,0 +1,54 @@ +## Registers + +- [User-defined registers](#user-defined-registers) +- [Default registers](#default-registers) +- [Special registers](#special-registers) + +In Helix, registers are storage locations for text and other data, such as the +result of a search. Registers can be used to cut, copy, and paste text, similar +to the clipboard in other text editors. Usage is similar to Vim, with `"` being +used to select a register. + +### User-defined registers + +Helix allows you to create your own named registers for storing text, for +example: + +- `"ay` - Yank the current selection to register `a`. +- `"op` - Paste the text in register `o` after the selection. + +If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out: + +- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode). +- `"md` - Store the selection in register `m` and delete it. + +### Default registers + +Commands that use registers, like yank (`y`), use a default register if none is specified. +These registers are used as defaults: + +| Register character | Contains | +| --- | --- | +| `/` | Last search | +| `:` | Last executed command | +| `"` | Last yanked text | +| `@` | Last recorded macro | + +### Special registers + +Some registers have special behavior when read from and written to. + +| Register character | When read | When written | +| --- | --- | --- | +| `_` | No values are returned | All values are discarded | +| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable | +| `.` | Contents of the current selections | This register is not writable | +| `%` | Name of the current file | This register is not writable | +| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard | +| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard | + +When yanking multiple selections to the clipboard registers, the selections +are joined with newlines. Pasting from these registers will paste multiple +selections if the clipboard was last yanked to by the Helix session. Otherwise +the clipboard contents are pasted as one selection. + diff --git a/book/src/surround.md b/book/src/surround.md new file mode 100644 index 000000000..187d2876a --- /dev/null +++ b/book/src/surround.md @@ -0,0 +1,24 @@ +## Surround + +Helix includes built-in functionality similar to [vim-surround](https://github.com/tpope/vim-surround). +The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich): + +![Surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif) + +| Key Sequence | Action | +| --------------------------------- | --------------------------------------- | +| `ms` (after selecting text) | Add surround characters to selection | +| `mr` | Replace the closest surround characters | +| `md` | Delete the closest surround characters | + +You can use counts to act on outer pairs. + +Surround can also act on multiple selections. For example, to change every occurrence of `(use)` to `[use]`: + +1. `%` to select the whole file +2. `s` to split the selections on a search term +3. Input `use` and hit Enter +4. `mr([` to replace the parentheses with square brackets + +Multiple characters are currently not supported, but planned for future release. + diff --git a/book/src/syntax-aware-motions.md b/book/src/syntax-aware-motions.md new file mode 100644 index 000000000..aab52b1b4 --- /dev/null +++ b/book/src/syntax-aware-motions.md @@ -0,0 +1,68 @@ +## Moving the selection with syntax-aware motions + +`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the +selection according to its location in the syntax tree. For example, many languages have the +following syntax for function calls: + +```js +func(arg1, arg2, arg3); +``` + +A function call might be parsed by tree-sitter into a tree like the following. + +```tsq +(call + function: (identifier) ; func + arguments: + (arguments ; (arg1, arg2, arg3) + (identifier) ; arg1 + (identifier) ; arg2 + (identifier))) ; arg3 +``` + +Use `:tree-sitter-subtree` to view the syntax tree of the primary selection. In +a more intuitive tree format: + +``` + ┌────┐ + │call│ + ┌─────┴────┴─────┐ + │ │ +┌─────▼────┐ ┌────▼────┐ +│identifier│ │arguments│ +│ "func" │ ┌────┴───┬─────┴───┐ +└──────────┘ │ │ │ + │ │ │ + ┌─────────▼┐ ┌────▼─────┐ ┌▼─────────┐ + │identifier│ │identifier│ │identifier│ + │ "arg1" │ │ "arg2" │ │ "arg3" │ + └──────────┘ └──────────┘ └──────────┘ +``` + +If you have a selection that wraps `arg1` (see the tree above), and you use +`Alt-n`, it will select the next sibling in the syntax tree: `arg2`. + +```js +// before +func([arg1], arg2, arg3) +// after +func(arg1, [arg2], arg3); +``` + +Similarly, `Alt-o` will expand the selection to the parent node, in this case, the +arguments node. + +```js +func[(arg1, arg2, arg3)]; +``` + +There is also some nuanced behavior that prevents you from getting stuck on a +node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous +child node will be selected. In the event that `arg1` does not have a previous +sibling, the selection will move up the syntax tree and select the previous +element. As a result, using `Alt-p` with a selection on `arg1` will move the +selection to the "func" `identifier`. + +[lang-support]: ./lang-support.md +[unimpaired-keybinds]: ./keymap.md#unimpaired +[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif diff --git a/book/src/textobjects.md b/book/src/textobjects.md new file mode 100644 index 000000000..92ca5645b --- /dev/null +++ b/book/src/textobjects.md @@ -0,0 +1,47 @@ +## Selecting and manipulating text with textobjects + +In Helix, textobjects are a way to select, manipulate and operate on a piece of +text in a structured way. They allow you to refer to blocks of text based on +their structure or purpose, such as a word, sentence, paragraph, or even a +function or block of code. + +![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif) +![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif) + +- `ma` - Select around the object (`va` in Vim, `` in Kakoune) +- `mi` - Select inside the object (`vi` in Vim, `` in Kakoune) + +| Key after `mi` or `ma` | Textobject selected | +| --- | --- | +| `w` | Word | +| `W` | WORD | +| `p` | Paragraph | +| `(`, `[`, `'`, etc. | Specified surround pairs | +| `m` | The closest surround pair | +| `f` | Function | +| `t` | Type (or Class) | +| `a` | Argument/parameter | +| `c` | Comment | +| `T` | Test | +| `g` | Change | + +> 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current +document and a special tree-sitter query file to work properly. [Only +some grammars][lang-support] currently have the query file implemented. +Contributions are welcome! + +## Navigating using tree-sitter textobjects + +Navigating between functions, classes, parameters, and other elements is +possible using tree-sitter and textobject queries. For +example to move to the next function use `]f`, to move to previous +type use `[t`, and so on. + +![Tree-sitter-nav-demo][tree-sitter-nav-demo] + +For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind +documentation. + +> 💡 This feature relies on tree-sitter textobjects +> and requires the corresponding query file to work properly. + diff --git a/book/src/usage.md b/book/src/usage.md index e01482193..859cb6709 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -1,15 +1,5 @@ # Using Helix - -- [Registers](#registers) - - [User-defined registers](#user-defined-registers) - - [Special registers](#special-registers) -- [Surround](#surround) -- [Selecting and manipulating text with textobjects](#selecting-and-manipulating-text-with-textobjects) -- [Navigating using tree-sitter textobjects](#navigating-using-tree-sitter-textobjects) -- [Moving the selection with syntax-aware motions](#moving-the-selection-with-syntax-aware-motions) - - For a full interactive introduction to Helix, refer to the [tutor](https://github.com/helix-editor/helix/blob/master/runtime/tutor) which can be accessed via the command `hx --tutor` or `:tutor`. @@ -17,192 +7,3 @@ # Using Helix > 💡 Currently, not all functionality is fully documented, please refer to the > [key mappings](./keymap.md) list. -## Registers - -In Helix, registers are storage locations for text and other data, such as the -result of a search. Registers can be used to cut, copy, and paste text, similar -to the clipboard in other text editors. Usage is similar to Vim, with `"` being -used to select a register. - -### User-defined registers - -Helix allows you to create your own named registers for storing text, for -example: - -- `"ay` - Yank the current selection to register `a`. -- `"op` - Paste the text in register `o` after the selection. - -If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out: - -- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode). -- `"md` - Store the selection in register `m` and delete it. - -### Default registers - -Commands that use registers, like yank (`y`), use a default register if none is specified. -These registers are used as defaults: - -| Register character | Contains | -| --- | --- | -| `/` | Last search | -| `:` | Last executed command | -| `"` | Last yanked text | -| `@` | Last recorded macro | - -### Special registers - -Some registers have special behavior when read from and written to. - -| Register character | When read | When written | -| --- | --- | --- | -| `_` | No values are returned | All values are discarded | -| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable | -| `.` | Contents of the current selections | This register is not writable | -| `%` | Name of the current file | This register is not writable | -| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard | -| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard | - -When yanking multiple selections to the clipboard registers, the selections -are joined with newlines. Pasting from these registers will paste multiple -selections if the clipboard was last yanked to by the Helix session. Otherwise -the clipboard contents are pasted as one selection. - -## Surround - -Helix includes built-in functionality similar to [vim-surround](https://github.com/tpope/vim-surround). -The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich): - -![Surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif) - -| Key Sequence | Action | -| --------------------------------- | --------------------------------------- | -| `ms` (after selecting text) | Add surround characters to selection | -| `mr` | Replace the closest surround characters | -| `md` | Delete the closest surround characters | - -You can use counts to act on outer pairs. - -Surround can also act on multiple selections. For example, to change every occurrence of `(use)` to `[use]`: - -1. `%` to select the whole file -2. `s` to split the selections on a search term -3. Input `use` and hit Enter -4. `mr([` to replace the parentheses with square brackets - -Multiple characters are currently not supported, but planned for future release. - -## Selecting and manipulating text with textobjects - -In Helix, textobjects are a way to select, manipulate and operate on a piece of -text in a structured way. They allow you to refer to blocks of text based on -their structure or purpose, such as a word, sentence, paragraph, or even a -function or block of code. - -![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif) -![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif) - -- `ma` - Select around the object (`va` in Vim, `` in Kakoune) -- `mi` - Select inside the object (`vi` in Vim, `` in Kakoune) - -| Key after `mi` or `ma` | Textobject selected | -| --- | --- | -| `w` | Word | -| `W` | WORD | -| `p` | Paragraph | -| `(`, `[`, `'`, etc. | Specified surround pairs | -| `m` | The closest surround pair | -| `f` | Function | -| `t` | Type (or Class) | -| `a` | Argument/parameter | -| `c` | Comment | -| `T` | Test | -| `g` | Change | - -> 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current -document and a special tree-sitter query file to work properly. [Only -some grammars][lang-support] currently have the query file implemented. -Contributions are welcome! - -## Navigating using tree-sitter textobjects - -Navigating between functions, classes, parameters, and other elements is -possible using tree-sitter and textobject queries. For -example to move to the next function use `]f`, to move to previous -type use `[t`, and so on. - -![Tree-sitter-nav-demo][tree-sitter-nav-demo] - -For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind -documentation. - -> 💡 This feature relies on tree-sitter textobjects -> and requires the corresponding query file to work properly. - -## Moving the selection with syntax-aware motions - -`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the -selection according to its location in the syntax tree. For example, many languages have the -following syntax for function calls: - -```js -func(arg1, arg2, arg3); -``` - -A function call might be parsed by tree-sitter into a tree like the following. - -```tsq -(call - function: (identifier) ; func - arguments: - (arguments ; (arg1, arg2, arg3) - (identifier) ; arg1 - (identifier) ; arg2 - (identifier))) ; arg3 -``` - -Use `:tree-sitter-subtree` to view the syntax tree of the primary selection. In -a more intuitive tree format: - -``` - ┌────┐ - │call│ - ┌─────┴────┴─────┐ - │ │ -┌─────▼────┐ ┌────▼────┐ -│identifier│ │arguments│ -│ "func" │ ┌────┴───┬─────┴───┐ -└──────────┘ │ │ │ - │ │ │ - ┌─────────▼┐ ┌────▼─────┐ ┌▼─────────┐ - │identifier│ │identifier│ │identifier│ - │ "arg1" │ │ "arg2" │ │ "arg3" │ - └──────────┘ └──────────┘ └──────────┘ -``` - -If you have a selection that wraps `arg1` (see the tree above), and you use -`Alt-n`, it will select the next sibling in the syntax tree: `arg2`. - -```js -// before -func([arg1], arg2, arg3) -// after -func(arg1, [arg2], arg3); -``` - -Similarly, `Alt-o` will expand the selection to the parent node, in this case, the -arguments node. - -```js -func[(arg1, arg2, arg3)]; -``` - -There is also some nuanced behavior that prevents you from getting stuck on a -node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous -child node will be selected. In the event that `arg1` does not have a previous -sibling, the selection will move up the syntax tree and select the previous -element. As a result, using `Alt-p` with a selection on `arg1` will move the -selection to the "func" `identifier`. - -[lang-support]: ./lang-support.md -[unimpaired-keybinds]: ./keymap.md#unimpaired -[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif From 295a9a95ceb65352fc6c706a38193d25a50223fd Mon Sep 17 00:00:00 2001 From: Arthur D <110528300+c0rydoras@users.noreply.github.com> Date: Mon, 6 May 2024 18:04:08 +0200 Subject: [PATCH 118/126] feat: add support for gjs and gts (#9940) --- book/src/generated/lang-support.md | 2 + languages.toml | 70 +++++++++++++++++++++++++++++ runtime/queries/_gjs/highlights.scm | 5 +++ runtime/queries/_gjs/injections.scm | 20 +++++++++ runtime/queries/gjs/highlights.scm | 1 + runtime/queries/gjs/indents.scm | 1 + runtime/queries/gjs/injections.scm | 1 + runtime/queries/gjs/locals.scm | 1 + runtime/queries/gjs/tags.scm | 1 + runtime/queries/gjs/textobjects.scm | 1 + runtime/queries/gts/highlights.scm | 1 + runtime/queries/gts/indents.scm | 1 + runtime/queries/gts/injections.scm | 1 + runtime/queries/gts/locals.scm | 1 + runtime/queries/gts/tags.scm | 1 + runtime/queries/gts/textobjects.scm | 1 + 16 files changed, 109 insertions(+) create mode 100644 runtime/queries/_gjs/highlights.scm create mode 100644 runtime/queries/_gjs/injections.scm create mode 100644 runtime/queries/gjs/highlights.scm create mode 100644 runtime/queries/gjs/indents.scm create mode 100644 runtime/queries/gjs/injections.scm create mode 100644 runtime/queries/gjs/locals.scm create mode 100644 runtime/queries/gjs/tags.scm create mode 100644 runtime/queries/gjs/textobjects.scm create mode 100644 runtime/queries/gts/highlights.scm create mode 100644 runtime/queries/gts/indents.scm create mode 100644 runtime/queries/gts/injections.scm create mode 100644 runtime/queries/gts/locals.scm create mode 100644 runtime/queries/gts/tags.scm create mode 100644 runtime/queries/gts/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 8418138c8..f9d3dab20 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -62,6 +62,7 @@ | git-config | ✓ | | | | | git-ignore | ✓ | | | | | git-rebase | ✓ | | | | +| gjs | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` | | gleam | ✓ | ✓ | | `gleam` | | glimmer | ✓ | | | `ember-language-server` | | glsl | ✓ | ✓ | ✓ | | @@ -73,6 +74,7 @@ | gowork | ✓ | | | `gopls` | | graphql | ✓ | ✓ | | `graphql-lsp` | | groovy | ✓ | | | | +| gts | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` | | hare | ✓ | | | | | haskell | ✓ | ✓ | | `haskell-language-server-wrapper` | | haskell-persistent | ✓ | | | | diff --git a/languages.toml b/languages.toml index ec3e36ed6..ace01e968 100644 --- a/languages.toml +++ b/languages.toml @@ -197,6 +197,28 @@ inlayHints.functionLikeReturnTypes.enabled = true inlayHints.enumMemberValues.enabled = true inlayHints.parameterNames.enabled = "all" +[language-server.vscode-eslint-language-server] +command = "vscode-eslint-language-server" +args = ["--stdio"] + +[language-server.vscode-eslint-language-server.config] +validate = "on" +experimental = { useFlatConfig = false } +rulesCustomizations = [] +run = "onType" +problems = { shortenToSingleLine = false } +nodePath = "" + +[language-server.vscode-eslint-language-server.config.codeAction.disableRuleComment] +enable = true +location = "separateLine" + +[language-server.vscode-eslint-language-server.config.codeAction.showDocumentation] +enable = true + +[language-server.vscode-eslint-language-server.config.workingDirectory] +mode = "location" + [[language]] name = "rust" scope = "source.rust" @@ -3586,3 +3608,51 @@ language-servers = ["pest-language-server"] [[grammar]] name = "pest" source = { git = "https://github.com/pest-parser/tree-sitter-pest", rev = "a8a98a824452b1ec4da7f508386a187a2f234b85" } + +[[language]] +name = "gjs" +scope = "source.gjs" +file-types = ["gjs"] +roots = ["package.json", "ember-cli-build.js"] +comment-token = "//" +block-comment-tokens = { start = "/*", end = "*/" } +language-servers = [ + { except-features = [ + "format", "diagnostics", + ], name = "typescript-language-server" }, + "vscode-eslint-language-server", + "ember-language-server", +] +indent = { tab-width = 2, unit = " " } +grammar = "javascript" + +[language.auto-pairs] +'<' = '>' +"'" = "'" +"{" = "}" +"(" = ")" +'"' = '"' + +[[language]] +name = "gts" +scope = "source.gts" +file-types = ["gts"] +roots = ["package.json", "ember-cli-build.js"] +comment-token = "//" +block-comment-tokens = { start = "/*", end = "*/" } +language-servers = [ + { except-features = [ + "format", "diagnostics", + ], name = "typescript-language-server" }, + "vscode-eslint-language-server", + "ember-language-server", +] +indent = { tab-width = 2, unit = " " } +grammar = "typescript" + +[language.auto-pairs] +'<' = '>' +"'" = "'" +"{" = "}" +"(" = ")" +'"' = '"' diff --git a/runtime/queries/_gjs/highlights.scm b/runtime/queries/_gjs/highlights.scm new file mode 100644 index 000000000..f410a1070 --- /dev/null +++ b/runtime/queries/_gjs/highlights.scm @@ -0,0 +1,5 @@ +[ + (glimmer_opening_tag) + (glimmer_closing_tag) +] @constant.builtin + diff --git a/runtime/queries/_gjs/injections.scm b/runtime/queries/_gjs/injections.scm new file mode 100644 index 000000000..830463168 --- /dev/null +++ b/runtime/queries/_gjs/injections.scm @@ -0,0 +1,20 @@ +; PARSE GLIMMER TEMPLATES +(call_expression + function: [ + (identifier) @injection.language + (member_expression + property: (property_identifier) @injection.language) + ] + arguments: (template_string) @injection.content) + +; e.g.: +((glimmer_template) @injection.content + (#set! injection.language "hbs")) + +; Parse Ember/Glimmer/Handlebars/HTMLBars/etc. template literals +; e.g.: await render(hbs``) +(call_expression + function: ((identifier) @_name + (#eq? @_name "hbs")) + arguments: ((template_string) @glimmer + (#offset! @glimmer 0 1 0 -1))) diff --git a/runtime/queries/gjs/highlights.scm b/runtime/queries/gjs/highlights.scm new file mode 100644 index 000000000..519838521 --- /dev/null +++ b/runtime/queries/gjs/highlights.scm @@ -0,0 +1 @@ +; inherits: _gjs,_javascript,ecma diff --git a/runtime/queries/gjs/indents.scm b/runtime/queries/gjs/indents.scm new file mode 100644 index 000000000..519838521 --- /dev/null +++ b/runtime/queries/gjs/indents.scm @@ -0,0 +1 @@ +; inherits: _gjs,_javascript,ecma diff --git a/runtime/queries/gjs/injections.scm b/runtime/queries/gjs/injections.scm new file mode 100644 index 000000000..519838521 --- /dev/null +++ b/runtime/queries/gjs/injections.scm @@ -0,0 +1 @@ +; inherits: _gjs,_javascript,ecma diff --git a/runtime/queries/gjs/locals.scm b/runtime/queries/gjs/locals.scm new file mode 100644 index 000000000..519838521 --- /dev/null +++ b/runtime/queries/gjs/locals.scm @@ -0,0 +1 @@ +; inherits: _gjs,_javascript,ecma diff --git a/runtime/queries/gjs/tags.scm b/runtime/queries/gjs/tags.scm new file mode 100644 index 000000000..519838521 --- /dev/null +++ b/runtime/queries/gjs/tags.scm @@ -0,0 +1 @@ +; inherits: _gjs,_javascript,ecma diff --git a/runtime/queries/gjs/textobjects.scm b/runtime/queries/gjs/textobjects.scm new file mode 100644 index 000000000..519838521 --- /dev/null +++ b/runtime/queries/gjs/textobjects.scm @@ -0,0 +1 @@ +; inherits: _gjs,_javascript,ecma diff --git a/runtime/queries/gts/highlights.scm b/runtime/queries/gts/highlights.scm new file mode 100644 index 000000000..5ad3ee1bb --- /dev/null +++ b/runtime/queries/gts/highlights.scm @@ -0,0 +1 @@ +; inherits: _gjs,_typescript,ecma diff --git a/runtime/queries/gts/indents.scm b/runtime/queries/gts/indents.scm new file mode 100644 index 000000000..5ad3ee1bb --- /dev/null +++ b/runtime/queries/gts/indents.scm @@ -0,0 +1 @@ +; inherits: _gjs,_typescript,ecma diff --git a/runtime/queries/gts/injections.scm b/runtime/queries/gts/injections.scm new file mode 100644 index 000000000..5ad3ee1bb --- /dev/null +++ b/runtime/queries/gts/injections.scm @@ -0,0 +1 @@ +; inherits: _gjs,_typescript,ecma diff --git a/runtime/queries/gts/locals.scm b/runtime/queries/gts/locals.scm new file mode 100644 index 000000000..5ad3ee1bb --- /dev/null +++ b/runtime/queries/gts/locals.scm @@ -0,0 +1 @@ +; inherits: _gjs,_typescript,ecma diff --git a/runtime/queries/gts/tags.scm b/runtime/queries/gts/tags.scm new file mode 100644 index 000000000..5ad3ee1bb --- /dev/null +++ b/runtime/queries/gts/tags.scm @@ -0,0 +1 @@ +; inherits: _gjs,_typescript,ecma diff --git a/runtime/queries/gts/textobjects.scm b/runtime/queries/gts/textobjects.scm new file mode 100644 index 000000000..5ad3ee1bb --- /dev/null +++ b/runtime/queries/gts/textobjects.scm @@ -0,0 +1 @@ +; inherits: _gjs,_typescript,ecma From b437b8b0eed868078ea97737d919a95eca4b7cb0 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 6 May 2024 18:04:32 +0200 Subject: [PATCH 119/126] Add support for Inko (#10656) This adds formatting and Tree-sitter support for Inko (https://inko-lang.org/). --- book/src/generated/lang-support.md | 1 + languages.toml | 15 +++ runtime/queries/inko/highlights.scm | 193 +++++++++++++++++++++++++++ runtime/queries/inko/indents.scm | 36 +++++ runtime/queries/inko/injections.scm | 2 + runtime/queries/inko/locals.scm | 10 ++ runtime/queries/inko/textobjects.scm | 38 ++++++ 7 files changed, 295 insertions(+) create mode 100644 runtime/queries/inko/highlights.scm create mode 100644 runtime/queries/inko/indents.scm create mode 100644 runtime/queries/inko/injections.scm create mode 100644 runtime/queries/inko/locals.scm create mode 100644 runtime/queries/inko/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index f9d3dab20..27fd583c2 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -90,6 +90,7 @@ | idris | | | | `idris2-lsp` | | iex | ✓ | | | | | ini | ✓ | | | | +| inko | ✓ | ✓ | ✓ | | | janet | ✓ | | | | | java | ✓ | ✓ | ✓ | `jdtls` | | javascript | ✓ | ✓ | ✓ | `typescript-language-server` | diff --git a/languages.toml b/languages.toml index ace01e968..498e9c717 100644 --- a/languages.toml +++ b/languages.toml @@ -2691,6 +2691,21 @@ indent = { tab-width = 4, unit = "\t" } name = "ini" source = { git = "https://github.com/justinmk/tree-sitter-ini", rev = "4d247fb876b4ae6b347687de4a179511bf67fcbc" } +[[language]] +name = "inko" +auto-format = true +scope = "source.inko" +injection-regex = "inko" +file-types = ["inko"] +roots = ["inko.pkg"] +comment-token = "#" +indent = { tab-width = 2, unit = " " } +formatter = { command = "inko", args = ["fmt", "-"] } + +[[grammar]] +name = "inko" +source = { git = "https://github.com/inko-lang/tree-sitter-inko", rev = "6983354c13a14bc621d7a3619f1790149e901187" } + [[language]] name = "bicep" scope = "source.bicep" diff --git a/runtime/queries/inko/highlights.scm b/runtime/queries/inko/highlights.scm new file mode 100644 index 000000000..2f1cdc127 --- /dev/null +++ b/runtime/queries/inko/highlights.scm @@ -0,0 +1,193 @@ +; Brackets and operators +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "," + "." + ":" +] @punctuation.delimiter + +[ + "!=" + "%" + "%=" + "&" + "&=" + "*" + "**" + "**=" + "*=" + "+" + "+=" + "-" + "-=" + "/" + "/=" + "<" + "<<" + "<<=" + "<=" + "<=" + "==" + ">" + ">=" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "^" + "^=" + "|" + "|=" +] @operator + +; Keywords +[ + "as" + "for" + "impl" + "let" + "mut" + "ref" + "uni" + "move" + "recover" +] @keyword + +"fn" @keyword.function + +"import" @keyword.control.import + +[ + "and" + "or" +] @keyword.operator + +[ + "class" + "trait" +] @keyword.storage.type + +[ + "extern" + (modifier) + (visibility) +] @keyword.storage.modifier + +[ + "loop" + "while" + (break) + (next) +] @keyword.control.repeat + +"return" @keyword.control.return + +[ + "throw" + "try" +] @keyword.control.exception + +[ + "case" + "else" + "if" + "match" +] @keyword.control.conditional + +; Comments +(line_comment) @comment.line + +; Literals +(self) @variable.builtin + +(nil) @constant.builtin + +[ + (true) + (false) +] @constant.builtin.boolean + +(integer) @constant.numeric.integer + +(float) @constant.numeric.float + +(string) @string + +(escape_sequence) @constant.character.escape + +(interpolation + "${" @punctuation.special + "}" @punctuation.special) + +(constant) @constant + +; Patterns +(integer_pattern) @constant.numeric.integer + +(string_pattern) @string + +(constant_pattern) @constant + +; Types +(generic_type + name: _ @type) + +(type) @type + +; Imports +(extern_import + path: _ @string) + +; Classes +(class + name: _ @type) + +(define_field + name: _ @variable.other.member) + +; Traits +(trait + name: _ @type) + +; Implementations +(implement_trait + class: _ @type) + +(reopen_class + name: _ @type) + +(bound + name: _ @type) + +; Methods +(method + name: _ @function) + +(external_function + name: _ @function) + +(argument + name: _ @variable.parameter) + +(named_argument + name: _ @variable.parameter) + +(call + name: _ @function) + +(field) @variable.other.member + +; Identifiers/variable references +((identifier) @function + (#is-not? local)) + +(identifier) @variable diff --git a/runtime/queries/inko/indents.scm b/runtime/queries/inko/indents.scm new file mode 100644 index 000000000..973e21269 --- /dev/null +++ b/runtime/queries/inko/indents.scm @@ -0,0 +1,36 @@ +[ + (arguments) + (array) + (assign_field) + (assign_local) + (assign_receiver_field) + (binary) + (block) + (bounds) + (cast) + (class) + (class_pattern) + (compound_assign_field) + (compound_assign_local) + (compound_assign_receiver_field) + (define_constant) + (define_variable) + (grouped_expression) + (implement_trait) + (match) + (or_pattern) + (reopen_class) + (replace_field) + (replace_local) + (symbols) + (trait) + (tuple) + (tuple_pattern) + (type_arguments) +] @indent + +[ + ")" + "]" + "}" +] @outdent diff --git a/runtime/queries/inko/injections.scm b/runtime/queries/inko/injections.scm new file mode 100644 index 000000000..36849c873 --- /dev/null +++ b/runtime/queries/inko/injections.scm @@ -0,0 +1,2 @@ +((line_comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/inko/locals.scm b/runtime/queries/inko/locals.scm new file mode 100644 index 000000000..3266bcae0 --- /dev/null +++ b/runtime/queries/inko/locals.scm @@ -0,0 +1,10 @@ +[ + (method) + (block) +] @local.scope + +(argument name: _ @local.definition) +(define_variable name: _ @local.definition) +(named_argument name: _ @local.definition) + +(identifier) @local.reference diff --git a/runtime/queries/inko/textobjects.scm b/runtime/queries/inko/textobjects.scm new file mode 100644 index 000000000..304333808 --- /dev/null +++ b/runtime/queries/inko/textobjects.scm @@ -0,0 +1,38 @@ +(class + body: (_) @class.inside) @class.around + +(trait + body: (_) @class.inside) @class.around + +(method + body: (_) @function.inside) @function.around + +(reopen_class + body: (_) @class.inside) @class.around + +(implement_trait + body: (_) @class.inside) @class.around + +(external_function + body: (_) @function.inside) @function.around + +(closure + body: (_) @function.inside) @function.around + +(arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(line_comment) @comment.inside + +(line_comment)+ @comment.around + +(array (_) @entry.around) + +(tuple (_) @entry.around) + +(tuple_pattern (_) @entry.around) + +(define_field (_) @entry.inside) @entry.around From e16a4f8a2cc37f07c274c70c1bdcf03c70e61eec Mon Sep 17 00:00:00 2001 From: Matt Moriarity Date: Mon, 6 May 2024 10:33:59 -0600 Subject: [PATCH 120/126] Expose all flake outputs through flake-compat (#10673) --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 50e7e6819..d2c51ec3a 100644 --- a/default.nix +++ b/default.nix @@ -5,4 +5,4 @@ let sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; }; in - (import compat {src = ./.;}).defaultNix.default + (import compat {src = ./.;}).defaultNix From beb5afcbef9f102c209de7aa32f126ded9dda515 Mon Sep 17 00:00:00 2001 From: Szabin Date: Mon, 6 May 2024 18:51:20 +0200 Subject: [PATCH 121/126] Revert "Refactor statusline elements to build `Spans` (#9122)" (#10642) --- helix-term/src/ui/statusline.rs | 428 +++++++++++++++++++------------- 1 file changed, 250 insertions(+), 178 deletions(-) diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 8a87242f9..7437cbd07 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -4,6 +4,7 @@ use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, graphics::Rect, + theme::Style, Document, Editor, View, }; @@ -19,6 +20,7 @@ pub struct RenderContext<'a> { pub view: &'a View, pub focused: bool, pub spinners: &'a ProgressSpinners, + pub parts: RenderBuffer<'a>, } impl<'a> RenderContext<'a> { @@ -35,10 +37,18 @@ pub fn new( view, focused, spinners, + parts: RenderBuffer::default(), } } } +#[derive(Default)] +pub struct RenderBuffer<'a> { + pub left: Spans<'a>, + pub center: Spans<'a>, + pub right: Spans<'a>, +} + pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) { let base_style = if context.focused { context.editor.theme.get("ui.statusline") @@ -48,87 +58,85 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface surface.set_style(viewport.with_height(1), base_style); - let statusline = render_statusline(context, viewport.width as usize); + let write_left = |context: &mut RenderContext, text, style| { + append(&mut context.parts.left, text, &base_style, style) + }; + let write_center = |context: &mut RenderContext, text, style| { + append(&mut context.parts.center, text, &base_style, style) + }; + let write_right = |context: &mut RenderContext, text, style| { + append(&mut context.parts.right, text, &base_style, style) + }; + + // Left side of the status line. + + let config = context.editor.config(); + + let element_ids = &config.statusline.left; + element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .for_each(|render| render(context, write_left)); surface.set_spans( viewport.x, viewport.y, - &statusline, - statusline.width() as u16, + &context.parts.left, + context.parts.left.width() as u16, + ); + + // Right side of the status line. + + let element_ids = &config.statusline.right; + element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .for_each(|render| render(context, write_right)); + + surface.set_spans( + viewport.x + + viewport + .width + .saturating_sub(context.parts.right.width() as u16), + viewport.y, + &context.parts.right, + context.parts.right.width() as u16, + ); + + // Center of the status line. + + let element_ids = &config.statusline.center; + element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .for_each(|render| render(context, write_center)); + + // Width of the empty space between the left and center area and between the center and right area. + let spacing = 1u16; + + let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16; + let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing); + let center_width = center_max_width.min(context.parts.center.width() as u16); + + surface.set_spans( + viewport.x + viewport.width / 2 - center_width / 2, + viewport.y, + &context.parts.center, + center_width, ); } -pub fn render_statusline<'a>(context: &mut RenderContext, width: usize) -> Spans<'a> { - let config = context.editor.config(); - - let element_ids = &config.statusline.left; - let mut left = element_ids - .iter() - .map(|element_id| get_render_function(*element_id)) - .flat_map(|render| render(context).0) - .collect::>(); - - let element_ids = &config.statusline.center; - let mut center = element_ids - .iter() - .map(|element_id| get_render_function(*element_id)) - .flat_map(|render| render(context).0) - .collect::>(); - - let element_ids = &config.statusline.right; - let mut right = element_ids - .iter() - .map(|element_id| get_render_function(*element_id)) - .flat_map(|render| render(context).0) - .collect::>(); - - let left_area_width: usize = left.iter().map(|s| s.width()).sum(); - let center_area_width: usize = center.iter().map(|s| s.width()).sum(); - let right_area_width: usize = right.iter().map(|s| s.width()).sum(); - - let min_spacing_between_areas = 1usize; - let sides_space_required = left_area_width + right_area_width + min_spacing_between_areas; - let total_space_required = sides_space_required + center_area_width + min_spacing_between_areas; - - let mut statusline: Vec = vec![]; - - if center_area_width > 0 && total_space_required <= width { - // SAFETY: this subtraction cannot underflow because `left_area_width + center_area_width + right_area_width` - // is smaller than `total_space_required`, which is smaller than `width` in this branch. - let total_spacers = width - (left_area_width + center_area_width + right_area_width); - // This is how much padding space it would take on either side to align the center area to the middle. - let center_margin = (width - center_area_width) / 2; - let left_spacers = if left_area_width < center_margin && right_area_width < center_margin { - // Align the center area to the middle if there is enough space on both sides. - center_margin - left_area_width - } else { - // Otherwise split the available space evenly and use it as margin. - // The center element won't be aligned to the middle but it will be evenly - // spaced between the left and right areas. - total_spacers / 2 - }; - let right_spacers = total_spacers - left_spacers; - - statusline.append(&mut left); - statusline.push(" ".repeat(left_spacers).into()); - statusline.append(&mut center); - statusline.push(" ".repeat(right_spacers).into()); - statusline.append(&mut right); - } else if right_area_width > 0 && sides_space_required <= width { - let side_areas_width = left_area_width + right_area_width; - statusline.append(&mut left); - statusline.push(" ".repeat(width - side_areas_width).into()); - statusline.append(&mut right); - } else if left_area_width <= width { - statusline.append(&mut left); - } - - statusline.into() +fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option