From 48a3965ab43718ce2a49724cbcc294b04c328b81 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 6 Nov 2022 08:56:45 -0600 Subject: [PATCH 001/117] Fix range offsets in multi-selection paste (#4608) * Fix range offsets in multi-selection paste d6323b7cbc21a9d3ba29738c76581dad93f9f415 introduced a regression with multi-selection paste where pasting would not adjust the ranges correctly. To fix it, we need to track the total number of characters inserted in each changed selection and use that offset to slide each new range forwards. * Inherit selection directions on paste * Add an integration-test for multi-selection pasting --- helix-term/src/commands.rs | 6 +++++- helix-term/tests/test/commands.rs | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ef963477..ae9e35f1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3476,6 +3476,7 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa let text = doc.text(); let selection = doc.selection(view.id); + let mut offset = 0; let mut ranges = SmallVec::with_capacity(selection.len()); let transaction = Transaction::change_by_selection(text, selection, |range| { @@ -3501,8 +3502,11 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa .as_ref() .map(|content| content.chars().count()) .unwrap_or_default(); + let anchor = offset + pos; - ranges.push(Range::new(pos, pos + value_len)); + let new_range = Range::new(anchor, anchor + value_len).with_direction(range.direction()); + ranges.push(new_range); + offset += value_len; (pos, pos, value) }); diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index aadf104b..e78e6c9f 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -193,3 +193,25 @@ async fn test_goto_file_impl() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_multi_selection_paste() -> anyhow::Result<()> { + test(( + platform_line(indoc! {"\ + #[|lorem]# + #(|ipsum)# + #(|dolor)# + "}) + .as_str(), + "yp", + platform_line(indoc! {"\ + lorem#[|lorem]# + ipsum#(|ipsum)# + dolor#(|dolor)# + "}) + .as_str(), + )) + .await?; + + Ok(()) +} From 1536a6528968f38adfac2e991b29006f5ded5968 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 3 Nov 2022 20:55:13 -0500 Subject: [PATCH 002/117] Fix whitespace handling in command-mode completion 8584b38cfbe6ffe3e5d539ad953c413e44e90bfa switched to shellwords for completion in command-mode. This changes the conditions for choosing whether to complete the command or use the command's completer. This change processes the input as shellwords up-front and uses shellword logic about whitespace to determine whether the command or argument should be completed. --- helix-core/src/shellwords.rs | 84 ++++++++++++++++++++++++++++---- helix-term/src/commands/typed.rs | 18 ++++--- 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index e8c5945b..3375bef1 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -17,18 +17,18 @@ pub fn escape(input: &str) -> Cow<'_, str> { } } +enum State { + OnWhitespace, + Unquoted, + UnquotedEscaped, + Quoted, + QuoteEscaped, + Dquoted, + DquoteEscaped, +} + /// Get the vec of escaped / quoted / doublequoted filenames from the input str pub fn shellwords(input: &str) -> Vec> { - enum State { - OnWhitespace, - Unquoted, - UnquotedEscaped, - Quoted, - QuoteEscaped, - Dquoted, - DquoteEscaped, - } - use State::*; let mut state = Unquoted; @@ -140,6 +140,70 @@ pub fn shellwords(input: &str) -> Vec> { args } +/// Checks that the input ends with an ascii whitespace character which is +/// not escaped. +/// +/// # Examples +/// +/// ```rust +/// use helix_core::shellwords::ends_with_whitespace; +/// assert_eq!(ends_with_whitespace(" "), true); +/// assert_eq!(ends_with_whitespace(":open "), true); +/// assert_eq!(ends_with_whitespace(":open foo.txt "), true); +/// assert_eq!(ends_with_whitespace(":open"), false); +/// #[cfg(unix)] +/// assert_eq!(ends_with_whitespace(":open a\\ "), false); +/// #[cfg(unix)] +/// assert_eq!(ends_with_whitespace(":open a\\ b.txt"), false); +/// ``` +pub fn ends_with_whitespace(input: &str) -> bool { + use State::*; + + // Fast-lane: the input must end with a whitespace character + // regardless of quoting. + if !input.ends_with(|c: char| c.is_ascii_whitespace()) { + return false; + } + + let mut state = Unquoted; + + for c in input.chars() { + state = match state { + OnWhitespace => match c { + '"' => Dquoted, + '\'' => Quoted, + '\\' if cfg!(unix) => UnquotedEscaped, + '\\' => OnWhitespace, + c if c.is_ascii_whitespace() => OnWhitespace, + _ => Unquoted, + }, + Unquoted => match c { + '\\' if cfg!(unix) => UnquotedEscaped, + '\\' => Unquoted, + c if c.is_ascii_whitespace() => OnWhitespace, + _ => Unquoted, + }, + UnquotedEscaped => Unquoted, + Quoted => match c { + '\\' if cfg!(unix) => QuoteEscaped, + '\\' => Quoted, + '\'' => OnWhitespace, + _ => Quoted, + }, + QuoteEscaped => Quoted, + Dquoted => match c { + '\\' if cfg!(unix) => DquoteEscaped, + '\\' => Dquoted, + '"' => OnWhitespace, + _ => Dquoted, + }, + DquoteEscaped => Dquoted, + } + } + + matches!(state, OnWhitespace) +} + #[cfg(test)] mod test { use super::*; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f4dfce7a..304b30f9 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2183,10 +2183,11 @@ pub(super) fn command_mode(cx: &mut Context) { static FUZZY_MATCHER: Lazy = Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); - // simple heuristic: if there's no just one part, complete command name. - // if there's a space, per command completion kicks in. - // we use .this over split_whitespace() because we care about empty segments - if input.split(' ').count() <= 1 { + let parts = shellwords::shellwords(input); + let ends_with_whitespace = shellwords::ends_with_whitespace(input); + + if parts.is_empty() || (parts.len() == 1 && !ends_with_whitespace) { + // If the command has not been finished yet, complete commands. let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST .iter() .filter_map(|command| { @@ -2202,8 +2203,13 @@ pub(super) fn command_mode(cx: &mut Context) { .map(|(name, _)| (0.., name.into())) .collect() } else { - let parts = shellwords::shellwords(input); - let part = parts.last().unwrap(); + // Otherwise, use the command's completer and the last shellword + // as completion input. + let part = if parts.len() == 1 { + &Cow::Borrowed("") + } else { + parts.last().unwrap() + }; if let Some(typed::TypableCommand { completer: Some(completer), From 3d283b2ca43bb077f52c48fec5e4870cd314b4e3 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 3 Nov 2022 20:58:54 -0500 Subject: [PATCH 003/117] Escape filenames in command completion This changes the completion items to be rendered with shellword escaping, so a file `a b.txt` is rendered as `a\ b.txt` which matches how it should be inputted. --- helix-core/src/shellwords.rs | 14 +++++++------- helix-term/src/commands/typed.rs | 1 + helix-term/src/ui/prompt.rs | 6 +----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 3375bef1..7742896c 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; /// Auto escape for shellwords usage. -pub fn escape(input: &str) -> Cow<'_, str> { +pub fn escape(input: Cow) -> Cow { if !input.chars().any(|x| x.is_ascii_whitespace()) { - Cow::Borrowed(input) + input } else if cfg!(unix) { Cow::Owned(input.chars().fold(String::new(), |mut buf, c| { if c.is_ascii_whitespace() { @@ -311,15 +311,15 @@ mod test { #[test] #[cfg(unix)] fn test_escaping_unix() { - assert_eq!(escape("foobar"), Cow::Borrowed("foobar")); - assert_eq!(escape("foo bar"), Cow::Borrowed("foo\\ bar")); - assert_eq!(escape("foo\tbar"), Cow::Borrowed("foo\\\tbar")); + assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar")); + assert_eq!(escape("foo bar".into()), Cow::Borrowed("foo\\ bar")); + assert_eq!(escape("foo\tbar".into()), Cow::Borrowed("foo\\\tbar")); } #[test] #[cfg(windows)] fn test_escaping_windows() { - assert_eq!(escape("foobar"), Cow::Borrowed("foobar")); - assert_eq!(escape("foo bar"), Cow::Borrowed("\"foo bar\"")); + assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar")); + assert_eq!(escape("foo bar".into()), Cow::Borrowed("\"foo bar\"")); } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 304b30f9..36080d39 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2219,6 +2219,7 @@ pub(super) fn command_mode(cx: &mut Context) { completer(editor, part) .into_iter() .map(|(range, file)| { + let file = shellwords::escape(file); // offset ranges to input let offset = input.len() - part.len(); let range = (range.start + offset)..; diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index ca2872a7..51ef688d 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -1,6 +1,5 @@ use crate::compositor::{Component, Compositor, Context, Event, EventResult}; use crate::{alt, ctrl, key, shift, ui}; -use helix_core::shellwords; use helix_view::input::KeyEvent; use helix_view::keyboard::KeyCode; use std::{borrow::Cow, ops::RangeFrom}; @@ -336,10 +335,7 @@ impl Prompt { let (range, item) = &self.completion[index]; - // since we are using shellwords to parse arguments, make sure - // that whitespace in files is properly escaped. - let item = shellwords::escape(item); - self.line.replace_range(range.clone(), &item); + self.line.replace_range(range.clone(), item); self.move_end(); } From 140df92d7936e1fd036128d98ab565d92e9d2bd8 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 3 Nov 2022 21:02:26 -0500 Subject: [PATCH 004/117] Fix command-mode completion behavior when input is escaped If `a\ b.txt` were a local file, `:o a\ ` would fill the prompt with `:o aa\ b.txt` because the replacement range was calculated using the shellwords-parsed part. Escaping the part before calculating its length fixes this edge-case. --- helix-term/src/commands/typed.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 36080d39..2f387bfd 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2216,12 +2216,15 @@ pub(super) fn command_mode(cx: &mut Context) { .. }) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str) { + let part_len = shellwords::escape(part.clone()).len(); + completer(editor, part) .into_iter() .map(|(range, file)| { let file = shellwords::escape(file); + // offset ranges to input - let offset = input.len() - part.len(); + let offset = input.len() - part_len; let range = (range.start + offset)..; (range, file) }) From eddf9f0b7f2eacac690afad05abdae398bb17366 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Mon, 7 Nov 2022 12:39:18 +0800 Subject: [PATCH 005/117] Run clippy on workspace in CI (#4614) --- .github/workflows/build.yml | 2 +- helix-core/src/test.rs | 1 + xtask/src/querycheck.rs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef47a277..15734361 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -101,7 +101,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --all-targets -- -D warnings + args: --workspace --all-targets -- -D warnings - name: Run cargo doc uses: actions-rs/cargo@v1 diff --git a/helix-core/src/test.rs b/helix-core/src/test.rs index 3e54d2c2..17523ed7 100644 --- a/helix-core/src/test.rs +++ b/helix-core/src/test.rs @@ -148,6 +148,7 @@ pub fn plain(s: &str, selection: Selection) -> String { } #[cfg(test)] +#[allow(clippy::module_inception)] mod test { use super::*; diff --git a/xtask/src/querycheck.rs b/xtask/src/querycheck.rs index 7014c7d6..454d0e5c 100644 --- a/xtask/src/querycheck.rs +++ b/xtask/src/querycheck.rs @@ -17,8 +17,8 @@ pub fn query_check() -> Result<(), DynError> { let language_name = &language.language_id; let grammar_name = language.grammar.as_ref().unwrap_or(language_name); for query_file in query_files { - let language = get_language(&grammar_name); - let query_text = read_query(&language_name, query_file); + let language = get_language(grammar_name); + let query_text = read_query(language_name, query_file); if let Ok(lang) = language { if !query_text.is_empty() { if let Err(reason) = Query::new(lang, &query_text) { From da8f29eaa7d51be7b0417111281b6cc526842f85 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Mon, 7 Nov 2022 05:32:40 -0800 Subject: [PATCH 006/117] Fixed disorienting selection palette on Gruvbox theme (#4626) Co-authored-by: ryan.palmer --- runtime/themes/gruvbox.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index aa7a464a..4f0a7d9f 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -52,9 +52,9 @@ "ui.help" = { bg = "bg1", fg = "fg1" } "ui.text" = { fg = "fg1" } "ui.text.focus" = { fg = "fg1" } -"ui.selection" = { bg = "bg3", modifiers = ["reversed"] } -"ui.cursor.primary" = { modifiers = ["reversed"] } -"ui.cursor.match" = { bg = "bg2" } +"ui.selection" = { bg = "bg2" } +"ui.cursor.primary" = { bg = "fg4", fg = "bg1" } +"ui.cursor.match" = { bg = "bg3" } "ui.menu" = { fg = "fg1", bg = "bg2" } "ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] } "ui.virtual.whitespace" = "bg2" From 6cafd81e41f353a7005eef4248a09a2b1849bc5c Mon Sep 17 00:00:00 2001 From: Tobias Kohlbau Date: Mon, 7 Nov 2022 14:53:29 +0100 Subject: [PATCH 007/117] tutor: fix wording in recap for chapter 5 (#4629) In recap for chapter 5.1 specify that the cursor is duplicted to the next suitable line instead of the next line. Signed-off-by: Tobias Kohlbau --- runtime/tutor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/tutor b/runtime/tutor index 43edb360..eb1d9885 100644 --- a/runtime/tutor +++ b/runtime/tutor @@ -596,8 +596,8 @@ _________________________________________________________________ = CHAPTER 5 RECAP = ================================================================= - * Type C to copy the current selection to below and Alt-C for - above. + * Type C to duplicate the cursor to the next suitable line + and Alt-C for previous suitable line. * Type s to select all instances of a regex pattern inside the current selection. From 5bfe84fb4f78379921fdd5eed03f118181e90a4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 18:06:05 -0600 Subject: [PATCH 008/117] build(deps): bump libloading from 0.7.3 to 0.7.4 (#4639) Bumps [libloading](https://github.com/nagisa/rust_libloading) from 0.7.3 to 0.7.4. - [Release notes](https://github.com/nagisa/rust_libloading/releases) - [Commits](https://github.com/nagisa/rust_libloading/compare/0.7.3...0.7.4) --- updated-dependencies: - dependency-name: libloading dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93459aa0..a4528289 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -655,9 +655,9 @@ checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libloading" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", "winapi", From 535cf90093573d86af6e5510c90aee476703fed7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 18:07:21 -0600 Subject: [PATCH 009/117] build(deps): bump regex from 1.6.0 to 1.7.0 (#4640) Bumps [regex](https://github.com/rust-lang/regex) from 1.6.0 to 1.7.0. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.6.0...1.7.0) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4528289..f0bf11f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -876,9 +876,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", From 188aff059bf9558b4fa28b03d5929020ff76cdb3 Mon Sep 17 00:00:00 2001 From: Danillo Melo Date: Mon, 7 Nov 2022 22:43:00 -0300 Subject: [PATCH 010/117] Improve Ruby TextObjects (#4601) --- runtime/queries/ruby/textobjects.scm | 50 +++++++++++----------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/runtime/queries/ruby/textobjects.scm b/runtime/queries/ruby/textobjects.scm index 34888c17..2d48fa6f 100644 --- a/runtime/queries/ruby/textobjects.scm +++ b/runtime/queries/ruby/textobjects.scm @@ -1,11 +1,6 @@ -; Class -(class) @class.around - -(class [(constant) (scope_resolution)] !superclass - (_)+ @class.inside) - -(class [(constant) (scope_resolution)] (superclass) - (_)+ @class.inside) +; Class and Modules +(class + body: (_)? @class.inside) @class.around (singleton_class value: (_) @@ -17,37 +12,32 @@ (#match? @class_const "Class") (#match? @class_method "new") (do_block (_)+ @class.inside)) @class.around + +(module + body: (_)? @class.inside) @class.around -; Functions -(method) @function.around +; Functions and Blocks +(singleton_method + body: (_)? @function.inside) @function.around -(method (identifier) (method_parameters) - (_)+ @function.inside) - -(do_block !parameters - (_)+ @function.inside) - -(do_block (block_parameters) - (_)+ @function.inside) - -(block (block_parameters) - (_)+ @function.inside) - -(block !parameters - (_)+ @function.inside) - -(method (identifier) !parameters - (_)+ @function.inside) +(method + body: (_)? @function.inside) @function.around + +(do_block + body: (_)? @function.inside) @function.around + +(block + body: (_)? @function.inside) @function.around ; Parameters (method_parameters - (_) @parameter.inside) + (_) @parameter.inside) @parameter.around (block_parameters - (_) @parameter.inside) + (_) @parameter.inside) @parameter.around (lambda_parameters - (_) @parameter.inside) + (_) @parameter.inside) @parameter.around ; Comments (comment) @comment.inside From 13126823f83cb90a3aabfc2326c0907d1ca2d921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 8 Nov 2022 20:48:06 +0900 Subject: [PATCH 011/117] lsp: Support insertReplace Fixes #4473 --- helix-lsp/src/client.rs | 1 + helix-term/src/ui/completion.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 0b443ccf..81b7d8ad 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -311,6 +311,7 @@ impl Client { String::from("additionalTextEdits"), ], }), + insert_replace_support: Some(true), ..Default::default() }), completion_item_kind: Some(lsp::CompletionItemKindCapability { diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index de7c3232..5ec8cf89 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -113,7 +113,8 @@ impl Completion { let edit = match edit { lsp::CompletionTextEdit::Edit(edit) => edit.clone(), lsp::CompletionTextEdit::InsertAndReplace(item) => { - unimplemented!("completion: insert_and_replace {:?}", item) + // TODO: support using "insert" instead of "replace" via user config + lsp::TextEdit::new(item.replace, item.new_text.clone()) } }; From c94feed83d746e71fb030639d740af85162b0763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 8 Nov 2022 21:03:54 +0900 Subject: [PATCH 012/117] core: Move state into the history module --- helix-core/src/comment.rs | 40 ++++++++++++++++++-------------------- helix-core/src/history.rs | 19 +++++++++++++++--- helix-core/src/lib.rs | 2 -- helix-core/src/state.rs | 17 ---------------- helix-term/src/commands.rs | 1 - helix-view/src/document.rs | 4 ++-- 6 files changed, 37 insertions(+), 46 deletions(-) delete mode 100644 helix-core/src/state.rs diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs index 44f6cdfe..ec5d7a45 100644 --- a/helix-core/src/comment.rs +++ b/helix-core/src/comment.rs @@ -100,43 +100,41 @@ mod test { #[test] fn test_find_line_comment() { - use crate::State; - // four lines, two space indented, except for line 1 which is blank. - let doc = Rope::from(" 1\n\n 2\n 3"); - - let mut state = State::new(doc); + let mut doc = Rope::from(" 1\n\n 2\n 3"); // select whole document - state.selection = Selection::single(0, state.doc.len_chars() - 1); + let mut selection = Selection::single(0, doc.len_chars() - 1); - let text = state.doc.slice(..); + let text = doc.slice(..); let res = find_line_comment("//", text, 0..3); // (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1) assert_eq!(res, (false, vec![0, 2], 2, 1)); // comment - let transaction = toggle_line_comments(&state.doc, &state.selection, None); - transaction.apply(&mut state.doc); - state.selection = state.selection.map(transaction.changes()); + let transaction = toggle_line_comments(&doc, &selection, None); + transaction.apply(&mut doc); + selection = selection.map(transaction.changes()); - assert_eq!(state.doc, " // 1\n\n // 2\n // 3"); + assert_eq!(doc, " // 1\n\n // 2\n // 3"); // uncomment - let transaction = toggle_line_comments(&state.doc, &state.selection, None); - transaction.apply(&mut state.doc); - state.selection = state.selection.map(transaction.changes()); - assert_eq!(state.doc, " 1\n\n 2\n 3"); + let transaction = toggle_line_comments(&doc, &selection, None); + transaction.apply(&mut doc); + selection = selection.map(transaction.changes()); + assert_eq!(doc, " 1\n\n 2\n 3"); + assert!(selection.len() == 1); // to ignore the selection unused warning // 0 margin comments - state.doc = Rope::from(" //1\n\n //2\n //3"); + doc = Rope::from(" //1\n\n //2\n //3"); // reset the selection. - state.selection = Selection::single(0, state.doc.len_chars() - 1); + selection = Selection::single(0, doc.len_chars() - 1); - let transaction = toggle_line_comments(&state.doc, &state.selection, None); - transaction.apply(&mut state.doc); - state.selection = state.selection.map(transaction.changes()); - assert_eq!(state.doc, " 1\n\n 2\n 3"); + let transaction = toggle_line_comments(&doc, &selection, None); + transaction.apply(&mut doc); + selection = selection.map(transaction.changes()); + assert_eq!(doc, " 1\n\n 2\n 3"); + assert!(selection.len() == 1); // to ignore the selection unused warning // TODO: account for uncommenting with uneven comment indentation } diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 5cd72b07..51174c02 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -1,9 +1,15 @@ -use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction}; +use crate::{Assoc, ChangeSet, Range, Rope, Selection, Transaction}; use once_cell::sync::Lazy; use regex::Regex; use std::num::NonZeroUsize; use std::time::{Duration, Instant}; +#[derive(Debug, Clone)] +pub struct State { + pub doc: Rope, + pub selection: Selection, +} + /// Stores the history of changes to a buffer. /// /// Currently the history is represented as a vector of revisions. The vector @@ -366,12 +372,16 @@ impl std::str::FromStr for UndoKind { #[cfg(test)] mod test { use super::*; + use crate::Selection; #[test] fn test_undo_redo() { let mut history = History::default(); let doc = Rope::from("hello"); - let mut state = State::new(doc); + let mut state = State { + doc, + selection: Selection::point(0), + }; let transaction1 = Transaction::change(&state.doc, vec![(5, 5, Some(" world!".into()))].into_iter()); @@ -420,7 +430,10 @@ mod test { fn test_earlier_later() { let mut history = History::default(); let doc = Rope::from("a\n"); - let mut state = State::new(doc); + let mut state = State { + doc, + selection: Selection::point(0), + }; fn undo(history: &mut History, state: &mut State) { if let Some(transaction) = history.undo() { diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 8f869e35..5f60c048 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -21,7 +21,6 @@ pub mod register; pub mod search; pub mod selection; pub mod shellwords; -mod state; pub mod surround; pub mod syntax; pub mod test; @@ -103,7 +102,6 @@ pub use smallvec::{smallvec, SmallVec}; pub use syntax::Syntax; pub use diagnostic::Diagnostic; -pub use state::State; pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING}; pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction}; diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs deleted file mode 100644 index dcc4b11b..00000000 --- a/helix-core/src/state.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{Rope, Selection}; - -#[derive(Debug, Clone)] -pub struct State { - pub doc: Rope, - pub selection: Selection, -} - -impl State { - #[must_use] - pub fn new(doc: Rope) -> Self { - Self { - doc, - selection: Selection::point(0), - } - } -} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ae9e35f1..31498a7b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3465,7 +3465,6 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa .any(|value| get_line_ending_of_str(value).is_some()); // Only compiled once. - #[allow(clippy::trivial_regex)] static REGEX: Lazy = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); let mut values = values .iter() diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 9a7febd2..08708528 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -16,11 +16,11 @@ use std::sync::Arc; use helix_core::{ encoding, - history::{History, UndoKind}, + history::{History, State, UndoKind}, indent::{auto_detect_indent_style, IndentStyle}, line_ending::auto_detect_line_ending, syntax::{self, LanguageConfiguration}, - ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction, + ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, Syntax, Transaction, DEFAULT_LINE_ENDING, }; From 7ed9e9cf2567ee5e23cd8694ffccb4b38602c02a Mon Sep 17 00:00:00 2001 From: Doug Kelkhoff <18220321+dgkf@users.noreply.github.com> Date: Tue, 8 Nov 2022 07:19:59 -0500 Subject: [PATCH 013/117] Dynamically resize line number gutter width (#3469) * dynamically resize line number gutter width * removing digits lower-bound, permitting spacer * removing max line num char limit; adding notes; qualified successors; notes * updating tests to use new line number width when testing views * linenr width based on document line count * using min width of 2 so line numbers relative is useful * lint rolling; removing unnecessary type parameter lifetime * merge change resolution * reformat code * rename row_styler to style; add int_log resource * adding spacer to gutters default; updating book config entry * adding view.inner_height(), swap for loop for iterator * reverting change of current! to view! now that doc is not needed --- book/src/configuration.md | 2 +- helix-term/src/commands.rs | 16 ++-- helix-term/src/ui/editor.rs | 17 +++-- helix-view/src/editor.rs | 9 ++- helix-view/src/gutter.rs | 57 ++++++++++++-- helix-view/src/lib.rs | 2 +- helix-view/src/tree.rs | 4 +- helix-view/src/view.rs | 145 ++++++++++++++++-------------------- 8 files changed, 143 insertions(+), 109 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 9f3fe2fa..20abeba5 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -46,7 +46,7 @@ on unix operating systems. | `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 `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", "line-numbers"]` | +| `gutters` | Gutters to display: Available are `diagnostics` 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"]` | | `auto-completion` | Enable automatic pop up of auto-completion. | `true` | | `auto-format` | Enable automatic formatting on save. | `true` | | `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 31498a7b..4e3c321c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -870,7 +870,7 @@ fn goto_window(cx: &mut Context, align: Align) { let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - let height = view.inner_area().height as usize; + let height = view.inner_height(); // respect user given count if any // - 1 so we have at least one gap in the middle. @@ -1372,9 +1372,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { return; } - let height = view.inner_area().height; + let height = view.inner_height(); - let scrolloff = config.scrolloff.min(height as usize / 2); + let scrolloff = config.scrolloff.min(height / 2); view.offset.row = match direction { Forward => view.offset.row + offset, @@ -1412,25 +1412,25 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { fn page_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_area().height as usize; + let offset = view.inner_height(); scroll(cx, offset, Direction::Backward); } fn page_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_area().height as usize; + let offset = view.inner_height(); scroll(cx, offset, Direction::Forward); } fn half_page_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_area().height as usize / 2; + let offset = view.inner_height() / 2; scroll(cx, offset, Direction::Backward); } fn half_page_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_area().height as usize / 2; + let offset = view.inner_height() / 2; scroll(cx, offset, Direction::Forward); } @@ -4342,7 +4342,7 @@ fn align_view_middle(cx: &mut Context) { view.offset.col = pos .col - .saturating_sub((view.inner_area().width as usize) / 2); + .saturating_sub((view.inner_area(doc).width as usize) / 2); } fn scroll_up(cx: &mut Context) { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 2cd2ad05..f2a588e3 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -79,7 +79,7 @@ impl EditorView { surface: &mut Surface, is_focused: bool, ) { - let inner = view.inner_area(); + let inner = view.inner_area(doc); let area = view.area; let theme = &editor.theme; @@ -736,9 +736,10 @@ impl EditorView { // avoid lots of small allocations by reusing a text buffer for each line let mut text = String::with_capacity(8); - for (constructor, width) in view.gutters() { - let gutter = constructor(editor, doc, view, theme, is_focused, *width); - text.reserve(*width); // ensure there's enough space for the gutter + for gutter_type in view.gutters() { + let gutter = gutter_type.style(editor, doc, view, theme, is_focused); + let width = gutter_type.width(view, doc); + text.reserve(width); // ensure there's enough space for the gutter for (i, line) in (view.offset.row..(last_line + 1)).enumerate() { let selected = cursors.contains(&line); let x = viewport.x + offset; @@ -751,13 +752,13 @@ impl EditorView { }; if let Some(style) = gutter(line, selected, &mut text) { - surface.set_stringn(x, y, &text, *width, gutter_style.patch(style)); + surface.set_stringn(x, y, &text, width, gutter_style.patch(style)); } else { surface.set_style( Rect { x, y, - width: *width as u16, + width: width as u16, height: 1, }, gutter_style, @@ -766,7 +767,7 @@ impl EditorView { text.clear(); } - offset += *width as u16; + offset += width as u16; } } @@ -882,7 +883,7 @@ impl EditorView { .or_else(|| theme.try_get_exact("ui.cursorcolumn")) .unwrap_or_else(|| theme.get("ui.cursorline.secondary")); - let inner_area = view.inner_area(); + let inner_area = view.inner_area(doc); let offset = view.offset.col; let selection = doc.selection(view.id); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index bcd8dedb..db97cbb1 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -456,6 +456,7 @@ impl std::str::FromStr for GutterType { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "diagnostics" => Ok(Self::Diagnostics), + "spacer" => Ok(Self::Spacer), "line-numbers" => Ok(Self::LineNumbers), _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."), } @@ -589,7 +590,11 @@ impl Default for Config { line_number: LineNumber::Absolute, cursorline: false, cursorcolumn: false, - gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers], + gutters: vec![ + GutterType::Diagnostics, + GutterType::Spacer, + GutterType::LineNumbers, + ], middle_click_paste: true, auto_pairs: AutoPairConfig::default(), auto_completion: true, @@ -1308,7 +1313,7 @@ impl Editor { .primary() .cursor(doc.text().slice(..)); if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) { - let inner = view.inner_area(); + let inner = view.inner_area(doc); pos.col += inner.x as usize; pos.row += inner.y as usize; let cursorkind = config.cursor_shape.from_mode(self.mode); diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 2c207d27..61a17791 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,21 +1,54 @@ use std::fmt::Write; use crate::{ + editor::GutterType, graphics::{Color, Style, UnderlineStyle}, Document, Editor, Theme, View, }; +fn count_digits(n: usize) -> usize { + // NOTE: if int_log gets standardized in stdlib, can use checked_log10 + // (https://github.com/rust-lang/rust/issues/70887#issue) + std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count() +} + pub type GutterFn<'doc> = Box Option