diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 3363e20bcc04..e3575ba85634 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,23 +1,18 @@ use crate::{Range, RopeSlice, Selection, Syntax}; -pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { - let tree = syntax.tree(); - - selection.clone().transform(|range| { +pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + selection.transform(|range| { let from = text.char_to_byte(range.from()); let to = text.char_to_byte(range.to()); // 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) - } - }) { + let parent = match node_under_range(syntax, text, &range).and_then(|node| { + if node.start_byte() == from && node.end_byte() == to { + node.parent() + } else { + Some(node) + } + }) { Some(parent) => parent, None => return range, }; @@ -33,14 +28,9 @@ 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) { +pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + selection.transform(|range| { + let descendant = match node_under_range(syntax, text, &range) { // find first child, if not possible, fallback to the node that contains selection Some(descendant) => match descendant.child(0) { Some(child) => child, @@ -59,3 +49,54 @@ pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) } }) } + +#[inline(always)] +pub fn next_sibling_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + transform_node_under_selection(syntax, text, selection, tree_sitter::Node::next_sibling) +} + +#[inline(always)] +pub fn prev_sibling_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { + transform_node_under_selection(syntax, text, selection, tree_sitter::Node::prev_sibling) +} + +fn transform_node_under_selection<'a, F>( + syntax: &'a Syntax, + text: RopeSlice, + selection: Selection, + f: F, +) -> Selection +where + F: Fn(&tree_sitter::Node<'a>) -> Option>, +{ + selection.transform(|range| { + let next_sibling = match node_under_range(syntax, text, &range) { + // find first child, if not possible, fallback to the node that contains selection + Some(descendant) => match f(&descendant) { + Some(sib) => sib, + None => return range, + }, + None => return range, + }; + + let from = text.byte_to_char(next_sibling.start_byte()); + let to = text.byte_to_char(next_sibling.end_byte()); + + if range.head < range.anchor { + Range::new(to, from) + } else { + Range::new(from, to) + } + }) +} + +pub fn node_under_range<'a>( + syntax: &'a Syntax, + text: RopeSlice, + range: &Range, +) -> Option> { + let tree = syntax.tree(); + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + tree.root_node().descendant_for_byte_range(from, to) +} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5c26a5b2a5f5..784c2cf87649 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -363,6 +363,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", + next_sibling_selection, "Move selection to next sibling syntax node", + prev_sibling_selection, "Move selection to previous sibling syntax node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save the current selection to the jumplist", @@ -5490,7 +5492,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); } }; @@ -5516,7 +5518,37 @@ 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); + } + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + +fn next_sibling_selection(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::next_sibling_selection(syntax, text, current_selection.clone()); + doc.set_selection(view.id, selection); + } + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + +fn prev_sibling_selection(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::prev_sibling_selection(syntax, text, current_selection.clone()); doc.set_selection(view.id, selection); } }; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 49f8469a1f7d..935cefcbf50a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -570,12 +570,14 @@ impl Default for Keymaps { "D" => goto_first_diag, "space" => add_newline_above, "o" => shrink_selection, + "s" => prev_sibling_selection, }, "]" => { "Right bracket" "d" => goto_next_diag, "D" => goto_last_diag, "space" => add_newline_below, "o" => expand_selection, + "s" => next_sibling_selection, }, "/" => search,