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--- helix-core/src/object.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) (limited to 'helix-core/src/object.rs') 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) + } + }) +} -- cgit v1.2.3-70-g09d2 From 939261fc078cea86d0e7f712222b9ae22785478c Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 8 Jan 2022 09:31:05 -0600 Subject: expand_selection to current node with no children (#1454) --- helix-core/src/object.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-core/src/object.rs') diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 21fa24fb..3363e20b 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -12,7 +12,7 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) .root_node() .descendant_for_byte_range(from, to) .and_then(|node| { - if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) { + if node.start_byte() == from && node.end_byte() == to { node.parent() } else { Some(node) -- cgit v1.2.3-70-g09d2 From 392dfa0841fb106300eedf26f8628e48a32d0ea4 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 20 Jan 2022 09:52:33 -0600 Subject: add select_next_sibling and select_prev_sibling commands (#1495) * add select_next_sibling and select_prev_sibling commands * refactor objects to use higher order functions * address clippy feedback * move selection cloning into commands * add default keybindings under left/right brackets * use [+t,]+t for selecting sibling syntax nodes * setup Alt-{j,k,h,l} default keymaps for syntax selection commands * reduce boilerplate of select_next/prev_sibling in commands * import tree-sitter Node type in commands--- book/src/keymap.md | 6 ++-- helix-core/src/object.rs | 90 +++++++++++++++++++++++++++------------------- helix-term/src/commands.rs | 34 ++++++++++++++++-- helix-term/src/keymap.rs | 7 ++-- 4 files changed, 94 insertions(+), 43 deletions(-) (limited to 'helix-core/src/object.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index ea890575..905ec48f 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -119,6 +119,10 @@ | `Alt-K` | Remove selections matching the regex | `remove_selections` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | +| `Alt-k` | Expand selection to parent syntax node | `expand_selection` | +| `Alt-j` | Shrink syntax tree object selection | `shrink_selection` | +| `Alt-h` | Select previous sibling node in syntax tree | `select_prev_sibling` | +| `Alt-l` | Select next sibling node in syntax tree | `select_next_sibling` | ### Search @@ -262,8 +266,6 @@ 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 3363e20b..b06f4144 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,56 +1,72 @@ use crate::{Range, RopeSlice, Selection, Syntax}; +use tree_sitter::Node; -pub fn expand_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()); +pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + select_node_impl(syntax, text, selection, |descendant, from, to| { + if descendant.start_byte() == from && descendant.end_byte() == to { + descendant.parent() + } else { + Some(descendant) + } + }) +} - // find parent of a descendant that matches the range - let parent = match tree - .root_node() - .descendant_for_byte_range(from, to) - .and_then(|node| { - if node.start_byte() == from && node.end_byte() == to { - node.parent() - } else { - Some(node) - } - }) { - Some(parent) => parent, - None => return range, - }; +pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + select_node_impl(syntax, text, selection, |descendant, _from, _to| { + descendant.child(0).or(Some(descendant)) + }) +} - let from = text.byte_to_char(parent.start_byte()); - let to = text.byte_to_char(parent.end_byte()); +pub fn select_sibling( + syntax: &Syntax, + text: RopeSlice, + selection: Selection, + sibling_fn: &F, +) -> Selection +where + F: Fn(Node) -> Option, +{ + select_node_impl(syntax, text, selection, |descendant, _from, _to| { + find_sibling_recursive(descendant, sibling_fn) + }) +} - if range.head < range.anchor { - Range::new(to, from) - } else { - Range::new(from, to) - } +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 shrink_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { +fn select_node_impl( + syntax: &Syntax, + text: RopeSlice, + selection: Selection, + select_fn: F, +) -> Selection +where + F: Fn(Node, usize, usize) -> Option, +{ let tree = syntax.tree(); - selection.clone().transform(|range| { + selection.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, - }, + let node = match tree + .root_node() + .descendant_for_byte_range(from, to) + .and_then(|node| select_fn(node, from, to)) + { + Some(node) => node, None => return range, }; - let from = text.byte_to_char(descendant.start_byte()); - let to = text.byte_to_char(descendant.end_byte()); + let from = text.byte_to_char(node.start_byte()); + let to = text.byte_to_char(node.end_byte()); if range.head < range.anchor { Range::new(to, from) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9ac12931..de4bfa49 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -11,6 +11,7 @@ use helix_core::{ object, pos_at_coords, regex::{self, Regex, RegexBuilder}, search, selection, shellwords, surround, textobject, + tree_sitter::Node, unicode::width::UnicodeWidthChar, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -363,6 +364,8 @@ impl MappableCommand { 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", + select_next_sibling, "Select the next sibling in the syntax tree", + select_prev_sibling, "Select the previous sibling in the syntax tree", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save the current selection to the jumplist", @@ -5502,7 +5505,7 @@ fn expand_selection(cx: &mut Context) { // 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); + let selection = object::expand_selection(syntax, text, current_selection.clone()); doc.set_selection(view.id, selection); } }; @@ -5528,7 +5531,7 @@ fn shrink_selection(cx: &mut Context) { // 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); + let selection = object::shrink_selection(syntax, text, current_selection.clone()); doc.set_selection(view.id, selection); } }; @@ -5536,6 +5539,33 @@ fn shrink_selection(cx: &mut Context) { cx.editor.last_motion = Some(Motion(Box::new(motion))); } +fn select_sibling_impl(cx: &mut Context, sibling_fn: &'static F) +where + F: Fn(Node) -> Option, +{ + 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_sibling(syntax, text, current_selection.clone(), sibling_fn); + doc.set_selection(view.id, selection); + } + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + +fn select_next_sibling(cx: &mut Context) { + select_sibling_impl(cx, &|node| Node::next_sibling(&node)) +} + +fn select_prev_sibling(cx: &mut Context) { + select_sibling_impl(cx, &|node| Node::prev_sibling(&node)) +} + fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 79a06206..e5990d72 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -552,6 +552,11 @@ impl Default for Keymaps { "S" => split_selection, ";" => collapse_selection, "A-;" => flip_selections, + "A-k" => expand_selection, + "A-j" => shrink_selection, + "A-h" => select_prev_sibling, + "A-l" => select_next_sibling, + "%" => select_all, "x" => extend_line, "X" => extend_to_line_bounds, @@ -569,13 +574,11 @@ 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, -- cgit v1.2.3-70-g09d2