From 5b4540fc2d8201f4bfe336cc10c1096059217225 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 21 Dec 2021 04:17:33 -0500 Subject: Auto pairs selection (#1254) * use auto pairs with selections Previously, the auto pairs code was converting the user selection into its cursor form, and setting the transaction's selection to that cursor. This has the effect of destroying the user's selection if they type a pair character that gets auto completed. This fixes the code to work with the user's selection, inserting auto pairs where appropriate, but either keeping or extending the user's selection. * use movement::Direction instead of bool * assume 0-width cursor is forward--- helix-core/src/selection.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'helix-core/src/selection.rs') diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 116a1c7c..884c98ac 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -7,6 +7,7 @@ use crate::{ ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, prev_grapheme_boundary, }, + movement::Direction, Assoc, ChangeSet, RopeSlice, }; use smallvec::{smallvec, SmallVec}; @@ -82,6 +83,13 @@ impl Range { std::cmp::max(self.anchor, self.head) } + /// Total length of the range. + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.to() - self.from() + } + /// The (inclusive) range of lines that the range overlaps. #[inline] #[must_use] @@ -102,6 +110,18 @@ impl Range { self.anchor == self.head } + /// `Direction::Backward` when head < anchor. + /// `Direction::Backward` otherwise. + #[inline] + #[must_use] + pub fn direction(&self) -> Direction { + if self.head < self.anchor { + Direction::Backward + } else { + Direction::Forward + } + } + /// Check two ranges for overlap. #[must_use] pub fn overlaps(&self, other: &Self) -> bool { -- cgit v1.2.3-70-g09d2 From bd2ab5be4331a2ab6d406378a9c0868eb2f72819 Mon Sep 17 00:00:00 2001 From: Matouš Dzivjak Date: Wed, 29 Dec 2021 10:17:46 +0100 Subject: feat(commands): ensure_selections_forward (#1393) * feat(commands): ensure_selections_forward Add command that ensures that selections are in forward direction. Fixes: https://github.com/helix-editor/helix/issues/1332 * Add keybinding for ensure_selections_forward Add `A-:` keybinding for the ensure_selections_forward command. * Re-use range.flip for flip_selections command--- helix-core/src/selection.rs | 9 +++++++++ helix-term/src/commands.rs | 17 ++++++++++++++++- helix-term/src/keymap.rs | 2 ++ 3 files changed, 27 insertions(+), 1 deletion(-) (limited to 'helix-core/src/selection.rs') diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 884c98ac..06ea9d67 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -122,6 +122,15 @@ impl Range { } } + // flips the direction of the selection + pub fn flip(&self) -> Self { + Self { + anchor: self.head, + head: self.anchor, + horiz: self.horiz, + } + } + /// Check two ranges for overlap. #[must_use] pub fn overlaps(&self, other: &Self) -> bool { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 04dd222b..9c1650b8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -266,6 +266,7 @@ impl MappableCommand { change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)", collapse_selection, "Collapse selection onto a single cursor", flip_selections, "Flip selection cursor and anchor", + ensure_selections_forward, "Ensure the selection is in forward direction", insert_mode, "Insert before selection", append_mode, "Insert after selection (append)", command_mode, "Enter command mode", @@ -1904,7 +1905,21 @@ fn flip_selections(cx: &mut Context) { let selection = doc .selection(view.id) .clone() - .transform(|range| Range::new(range.head, range.anchor)); + .transform(|range| range.flip()); + doc.set_selection(view.id, selection); +} + +fn ensure_selections_forward(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let selection = doc + .selection(view.id) + .clone() + .transform(|r| match r.direction() { + Direction::Forward => r, + Direction::Backward => r.flip(), + }); + doc.set_selection(view.id, selection); } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 08c8032b..72833212 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -617,6 +617,8 @@ impl Default for Keymaps { "A-(" => rotate_selection_contents_backward, "A-)" => rotate_selection_contents_forward, + "A-:" => ensure_selections_forward, + "esc" => normal_mode, "C-b" | "pageup" => page_up, "C-f" | "pagedown" => page_down, -- cgit v1.2.3-70-g09d2 From 2e02a1d6bc004212033b9c4e5ed0de0fd880796c Mon Sep 17 00:00:00 2001 From: Matouš Dzivjak Date: Thu, 6 Jan 2022 03:12:02 +0100 Subject: feat(commands): shrink_selection (#1340) * feat(commands): shrink_selection Add `shrink_selection` command that can be used to shrink previously expanded selection. To make `shrink_selection` work it was necessary to add selection history to the Document since we want to shrink the selection towards the syntax tree node that was initially selected. Selection history is cleared any time the user changes selection other way than by `expand_selection`. This ensures that we don't get some funky edge cases when user calls `shrink_selection`. Related: https://github.com/helix-editor/helix/discussions/1328 * Refactor shrink_selection, move history to view * Remove useless comment * Add default key mapping for extend&shrink selection * Rework contains_selection method * Shrink selection without expand selects first child--- book/src/keymap.md | 2 ++ helix-core/src/object.rs | 29 ++++++++++++++++++-- helix-core/src/selection.rs | 64 +++++++++++++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 36 ++++++++++++++++++++++++- helix-term/src/keymap.rs | 2 ++ helix-view/src/view.rs | 3 +++ 6 files changed, 133 insertions(+), 3 deletions(-) (limited to 'helix-core/src/selection.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 581e70e9..70ec13b3 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -261,6 +261,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire | `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` | | `[space` | Add newline above | `add_newline_above` | | `]space` | Add newline below | `add_newline_below` | +| `]o` | Expand syntax tree object selection. | `expand_selection` | +| `[o` | Shrink syntax tree object selection. | `shrink_selection` | ## Insert Mode diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 717c5994..21fa24fb 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,7 +1,5 @@ use crate::{Range, RopeSlice, Selection, Syntax}; -// TODO: to contract_selection we'd need to store the previous ranges before expand. -// Maybe just contract to the first child node? pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { let tree = syntax.tree(); @@ -34,3 +32,30 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) } }) } + +pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { + let tree = syntax.tree(); + + selection.clone().transform(|range| { + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + + let descendant = match tree.root_node().descendant_for_byte_range(from, to) { + // find first child, if not possible, fallback to the node that contains selection + Some(descendant) => match descendant.child(0) { + Some(child) => child, + None => descendant, + }, + None => return range, + }; + + let from = text.byte_to_char(descendant.start_byte()); + let to = text.byte_to_char(descendant.end_byte()); + + if range.head < range.anchor { + Range::new(to, from) + } else { + Range::new(from, to) + } + }) +} diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 06ea9d67..1515c4fc 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -140,6 +140,11 @@ impl Range { self.from() == other.from() || (self.to() > other.from() && other.to() > self.from()) } + #[inline] + pub fn contains_range(&self, other: &Self) -> bool { + self.from() <= other.from() && self.to() >= other.to() + } + pub fn contains(&self, pos: usize) -> bool { self.from() <= pos && pos < self.to() } @@ -544,6 +549,39 @@ impl Selection { pub fn len(&self) -> usize { self.ranges.len() } + + // returns true if self ⊇ other + pub fn contains(&self, other: &Selection) -> bool { + // can't contain other if it is larger + if other.len() > self.len() { + return false; + } + + let (mut iter_self, mut iter_other) = (self.iter(), other.iter()); + let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next()); + + loop { + match (ele_self, ele_other) { + (Some(ra), Some(rb)) => { + if !ra.contains_range(rb) { + // `self` doesn't contain next element from `other`, advance `self`, we need to match all from `other` + ele_self = iter_self.next(); + } else { + // matched element from `other`, advance `other` + ele_other = iter_other.next(); + }; + } + (None, Some(_)) => { + // exhausted `self`, we can't match the reminder of `other` + return false; + } + (_, None) => { + // no elements from `other` left to match, `self` contains `other` + return true; + } + } + } + } } impl<'a> IntoIterator for &'a Selection { @@ -982,4 +1020,30 @@ mod test { &["", "abcd", "efg", "rs", "xyz"] ); } + #[test] + fn test_selection_contains() { + fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool { + let sela = Selection::new(a.iter().map(|a| Range::new(a.0, a.1)).collect(), 0); + let selb = Selection::new(b.iter().map(|b| Range::new(b.0, b.1)).collect(), 0); + sela.contains(&selb) + } + + // exact match + assert!(contains(vec!((1, 1)), vec!((1, 1)))); + + // larger set contains smaller + assert!(contains(vec!((1, 1), (2, 2), (3, 3)), vec!((2, 2)))); + + // multiple matches + assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2)))); + + // smaller set can't contain bigger + assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2)))); + + assert!(contains( + vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)), + vec!((3, 4), (7, 9)) + )); + assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6)))); + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index bf12b122..5c26a5b2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -362,6 +362,7 @@ impl MappableCommand { rotate_selection_contents_forward, "Rotate selection contents forward", rotate_selection_contents_backward, "Rotate selections contents backward", expand_selection, "Expand selection to parent syntax node", + shrink_selection, "Shrink selection to previously expanded syntax node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save the current selection to the jumplist", @@ -5467,6 +5468,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); } + fn rotate_selection_contents_forward(cx: &mut Context) { rotate_selection_contents(cx, Direction::Forward) } @@ -5482,7 +5484,39 @@ fn expand_selection(cx: &mut Context) { if let Some(syntax) = doc.syntax() { let text = doc.text().slice(..); - let selection = object::expand_selection(syntax, text, doc.selection(view.id)); + + let current_selection = doc.selection(view.id); + + // save current selection so it can be restored using shrink_selection + view.object_selections.push(current_selection.clone()); + + let selection = object::expand_selection(syntax, text, current_selection); + doc.set_selection(view.id, selection); + } + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + +fn shrink_selection(cx: &mut Context) { + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); + let current_selection = doc.selection(view.id); + // try to restore previous selection + if let Some(prev_selection) = view.object_selections.pop() { + if current_selection.contains(&prev_selection) { + // allow shrinking the selection only if current selection contains the previous object selection + doc.set_selection(view.id, prev_selection); + return; + } else { + // clear existing selection as they can't be shrinked to anyway + view.object_selections.clear(); + } + } + // if not previous selection, shrink to first child + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let selection = object::shrink_selection(syntax, text, current_selection); doc.set_selection(view.id, selection); } }; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 72833212..49f8469a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -569,11 +569,13 @@ impl Default for Keymaps { "d" => goto_prev_diag, "D" => goto_first_diag, "space" => add_newline_above, + "o" => shrink_selection, }, "]" => { "Right bracket" "d" => goto_next_diag, "D" => goto_last_diag, "space" => add_newline_below, + "o" => expand_selection, }, "/" => search, diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 94d67acd..89a6c196 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -80,6 +80,8 @@ pub struct View { // uses two docs because we want to be able to swap between the // two last modified docs which we need to manually keep track of pub last_modified_docs: [Option; 2], + /// used to store previous selections of tree-sitter objecs + pub object_selections: Vec, } impl View { @@ -92,6 +94,7 @@ impl View { jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel last_accessed_doc: None, last_modified_docs: [None, None], + object_selections: Vec::new(), } } -- cgit v1.2.3-70-g09d2 From f064894e5778071ef8cc563380f48cfdd9acba59 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 23 Jan 2022 02:37:23 -0500 Subject: Fix Clippy lints in tests (#1563) Co-authored-by: Blaž Hrastnik --- helix-core/src/auto_pairs.rs | 4 ++-- helix-core/src/chars.rs | 9 ++++----- helix-core/src/diff.rs | 2 +- helix-core/src/history.rs | 4 ++-- helix-core/src/indent.rs | 4 ++-- helix-core/src/line_ending.rs | 2 +- helix-core/src/selection.rs | 16 ++++++++-------- helix-core/src/surround.rs | 1 + helix-core/src/transaction.rs | 2 +- helix-tui/src/widgets/reflow.rs | 4 ++-- helix-view/src/view.rs | 4 ++-- 11 files changed, 26 insertions(+), 26 deletions(-) (limited to 'helix-core/src/selection.rs') diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 9d1152bc..40802680 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -322,7 +322,7 @@ mod test { use super::*; use smallvec::smallvec; - const LINE_END: &'static str = crate::DEFAULT_LINE_ENDING.as_str(); + const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str(); fn differing_pairs() -> impl Iterator { PAIRS.iter().filter(|(open, close)| open != close) @@ -339,7 +339,7 @@ mod test { expected_doc: &Rope, expected_sel: &Selection, ) { - let trans = hook(&in_doc, &in_sel, ch).unwrap(); + let trans = hook(in_doc, in_sel, ch).unwrap(); let mut actual_doc = in_doc.clone(); assert!(trans.apply(&mut actual_doc)); assert_eq!(expected_doc, &actual_doc); diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs index c8e5efbd..54991574 100644 --- a/helix-core/src/chars.rs +++ b/helix-core/src/chars.rs @@ -91,12 +91,11 @@ mod test { #[test] fn test_categorize() { - const EOL_TEST_CASE: &'static str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; - const WORD_TEST_CASE: &'static str = - "_hello_world_あいうえおー12345678901234567890"; - const PUNCTUATION_TEST_CASE: &'static str = + const EOL_TEST_CASE: &str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; + const WORD_TEST_CASE: &str = "_hello_world_あいうえおー12345678901234567890"; + const PUNCTUATION_TEST_CASE: &str = "!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~"; - const WHITESPACE_TEST_CASE: &'static str = "      "; + const WHITESPACE_TEST_CASE: &str = "      "; for ch in EOL_TEST_CASE.chars() { assert_eq!(CharCategory::Eol, categorize_char(ch)); diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs index 3349213d..6960c679 100644 --- a/helix-core/src/diff.rs +++ b/helix-core/src/diff.rs @@ -58,7 +58,7 @@ mod tests { let mut old = Rope::from(a); let new = Rope::from(b); compare_ropes(&old, &new).apply(&mut old); - old.to_string() == new.to_string() + old == new } } } diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 4b1c8d3b..bb95213c 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -448,8 +448,8 @@ mod test { change: crate::transaction::Change, instant: Instant, ) { - let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter()); - history.commit_revision_at_timestamp(&txn, &state, instant); + let txn = Transaction::change(&state.doc, vec![change].into_iter()); + history.commit_revision_at_timestamp(&txn, state, instant); txn.apply(&mut state.doc); } diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index ac2a1208..a8ea3012 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -416,7 +416,7 @@ where ", ); - let doc = Rope::from(doc); + let doc = doc; use crate::diagnostic::Severity; use crate::syntax::{ Configuration, IndentationConfiguration, LanguageConfiguration, Loader, @@ -454,7 +454,7 @@ where let language_config = loader.language_config_for_scope("source.rust").unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap(); - let syntax = Syntax::new(&doc, highlight_config.clone(), std::sync::Arc::new(loader)); + let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader)); let text = doc.slice(..); let tab_width = 4; diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs index 3541305c..8eb426e1 100644 --- a/helix-core/src/line_ending.rs +++ b/helix-core/src/line_ending.rs @@ -250,7 +250,7 @@ mod line_ending_tests { assert_eq!(get_line_ending_of_str(&text[..6]), Some(LineEnding::CR)); assert_eq!(get_line_ending_of_str(&text[..12]), Some(LineEnding::LF)); assert_eq!(get_line_ending_of_str(&text[..17]), Some(LineEnding::Crlf)); - assert_eq!(get_line_ending_of_str(&text[..]), None); + assert_eq!(get_line_ending_of_str(text), None); } #[test] diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 1515c4fc..c6eceb4b 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -766,16 +766,16 @@ mod test { fn test_contains() { let range = Range::new(10, 12); - assert_eq!(range.contains(9), false); - assert_eq!(range.contains(10), true); - assert_eq!(range.contains(11), true); - assert_eq!(range.contains(12), false); - assert_eq!(range.contains(13), false); + assert!(!range.contains(9)); + assert!(range.contains(10)); + assert!(range.contains(11)); + assert!(!range.contains(12)); + assert!(!range.contains(13)); let range = Range::new(9, 6); - assert_eq!(range.contains(9), false); - assert_eq!(range.contains(7), true); - assert_eq!(range.contains(6), true); + assert!(!range.contains(9)); + assert!(range.contains(7)); + assert!(range.contains(6)); } #[test] diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index b53b0a78..58eb23cf 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -172,6 +172,7 @@ mod test { use ropey::Rope; use smallvec::SmallVec; + #[allow(clippy::type_complexity)] fn check_find_nth_pair_pos( text: &str, cases: Vec<(usize, char, usize, Option<(usize, usize)>)>, diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index d8d389f3..30995e8c 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -761,7 +761,7 @@ mod test { #[test] fn combine_with_utf8() { - const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!"; + const TEST_CASE: &str = "Hello, これはヘリックスエディターです!"; let empty = Rope::from(""); let a = ChangeSet::new(&empty); diff --git a/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs index 21847783..33e52bb4 100644 --- a/helix-tui/src/widgets/reflow.rs +++ b/helix-tui/src/widgets/reflow.rs @@ -404,8 +404,8 @@ mod test { let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\ では、"; let (word_wrapper, word_wrapper_width) = - run_composer(Composer::WordWrapper { trim: true }, &text, width); - let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width); + run_composer(Composer::WordWrapper { trim: true }, text, width); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); assert_eq!(line_truncator, vec!["コンピュータ上で文字"]); let wrapped = vec![ "コンピュータ上で文字", diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 89a6c196..adfcd071 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -356,7 +356,7 @@ mod tests { let text = rope.slice(..); assert_eq!( - view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4), + view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4), Some(0) ); @@ -389,7 +389,7 @@ mod tests { let text = rope.slice(..); assert_eq!( - view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4), + view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4), Some(0) ); -- cgit v1.2.3-70-g09d2