Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add select_next_sibling and select_prev_sibling commands #1495

Merged
82 changes: 48 additions & 34 deletions helix-core/src/object.rs
Original file line number Diff line number Diff line change
@@ -1,56 +1,70 @@
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();
select_node_impl(syntax, text, selection, |descendant, from, to| {
if descendant.start_byte() == from && descendant.end_byte() == to {
descendant.parent()
} else {
Some(descendant)
}
})
}

selection.clone().transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
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))
})
}

// 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 select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
find_sibling_recursive(descendant, |node| node.next_sibling())
})
}

let from = text.byte_to_char(parent.start_byte());
let to = text.byte_to_char(parent.end_byte());
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
find_sibling_recursive(descendant, |node| node.prev_sibling())
})
}

if range.head < range.anchor {
Range::new(to, from)
} else {
Range::new(from, to)
}
fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
where
F: Fn(Node) -> Option<Node>,
{
sibling_fn(node).or_else(|| {
node.parent()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea to recurse up the tree until you find a sibling. 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ this, was admiring exactly the same thing

.and_then(|node| find_sibling_recursive(node, sibling_fn))
})
}

pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
fn select_node_impl<F>(
syntax: &Syntax,
text: RopeSlice,
selection: &Selection,
the-mikedavis marked this conversation as resolved.
Show resolved Hide resolved
select_fn: F,
) -> Selection
where
F: Fn(Node, usize, usize) -> Option<Node>,
{
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,
},
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)
Expand Down
36 changes: 36 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
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",
Expand Down Expand Up @@ -5524,6 +5526,40 @@ fn shrink_selection(cx: &mut Context) {
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}

fn select_next_sibling(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_next_sibling(syntax, text, current_selection);
doc.set_selection(view.id, selection);
}
};
motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}

fn select_prev_sibling(cx: &mut Context) {
let motion = |editor: &mut Editor| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to factor these too so it's not duplicated everywhere except the sibling call, but I couldn't figure out how to make the borrow checker happy. I tried making a fn(&Syntax, RopeSlice, &Selection) argument, but it didn't like that the function argument gets captured by the motion closure.

Copy link
Member Author

@the-mikedavis the-mikedavis Jan 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little afraid of the borrow checker 😅, I'll give this a shot but I'm not optimistic

edit: it didn't go well 🙃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do something like this:

fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement)
where
F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range,
{
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc
.selection(view.id)
.clone()
.transform(|range| move_fn(text, range, dir, count, behaviour));
doc.set_selection(view.id, selection);
}
use helix_core::movement::{move_horizontally, move_vertically};
fn move_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
}
fn move_char_right(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Forward, Movement::Move)
}
fn move_line_up(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Backward, Movement::Move)
}
fn move_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
}
fn extend_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
}
fn extend_char_right(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend)
}
fn extend_line_up(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Backward, Movement::Extend)
}
fn extend_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
}

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_prev_sibling(syntax, text, current_selection);
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);

Expand Down