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鈥檒l 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

Conversation

the-mikedavis
Copy link
Member

@the-mikedavis the-mikedavis commented Jan 12, 2022

builds on expand_selection and shrink_selection to move to next and previous sibling nodes in the syntax tree

mostly I'm interested in all four of those together an alternate means of motion, like so:

select-movement.mp4

Where I have

  • A-j shrink selection
  • A-k expand selection
  • A-h select prev sibling
  • A-l select next sibling

in my personal config. I don't know if these commands are worthy of a default keybind 馃

What do you think?

@pickfire
Copy link
Contributor

pickfire commented Jan 12, 2022

Seemed interesting, do you find you using it frequently and needs to keep holding down alt? If that's the case something like our extend state would be nice, otherwise the keys you show are quite good already.

Or maybe { and }? Last time I use that for paragraph but I navigate in a similar manner, I think would be nice if the language support tree-sitter than we use this, and if not we fallback to paragraph.

Comment on lines 99 to 120
selection.clone().transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());

let sibling = match tree
.root_node()
.descendant_for_byte_range(from, to)
.and_then(find_prev_sibling)
{
Some(sibling) => sibling,
None => return range,
};

let from = text.byte_to_char(sibling.start_byte());
let to = text.byte_to_char(sibling.end_byte());

if range.head < range.anchor {
Range::new(to, from)
} else {
Range::new(from, to)
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

This whole thing seemed like it is duplicated, probably can reuse.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep it's all duplicated. I tried out refactoring it to take the find_prev_sibling/find_next_sibling function as an argument but I couldn't figure out the borrowing rules. I'll keep looking into it 馃

(ofc if you'd like to push a commit to cut down on that boilerplate, I'd be grateful 馃檪)

Copy link
Member Author

Choose a reason for hiding this comment

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

I figured out how to do higher order functions but now I think I may have overdone it 馃槃

@the-mikedavis
Copy link
Member Author

Ah that's a good idea, an extended state would be great

That's not something we can setup in the config toml currently, right? I'd love a built-in keybinding for that but idk how many others would find this way of doing motion useful

@pickfire
Copy link
Contributor

That's not something we can setup in the config toml currently, right? I'd love a built-in keybinding for that but idk how many others would find this way of doing motion useful

Yeah, extended state is not possible with current config mechanism. Not sure if we want this also, I think as of now integrating to the editor is easier since cases like this is rare. cc @sudormrfbin

@sudormrfbin
Copy link
Member

(By extended state I assume you mean "sticky" mode, like in Z) - It's not currently configurable primarily because I couldn't find a good syntax for it in the toml config file. How should a sub-keymap be marked as sticky ? Maybe a special properties field for keybinds ? I'm not sure about the design.

@the-mikedavis
Copy link
Member Author

err yep, "sticky"

I haven't really thought this through, but one way I'm thinking that could work: things like keys.insert and keys.normal but with a custom mode name like keys.foo and then you can bind something to switch into that mode, like

[keys.normal]
"T" = "enter_mode_once treejump" # not sticky
"A-t" = "enter_mode treejump" # sticky

[keys.treejump]
"j" = "shrink_selection"
"k" = "expand_selection"
"h" = "select_prev_sibling"
"l" = "select_next_sibling"

but for this to work you'd need to be able to bind keys to commands that take arguments which I think is not currently possible, right?

@pickfire
Copy link
Contributor

Using [s like @dead10ck did in #1506 seemed like a good idea too, but I think it may be more preferable be { since it seemed more useful.

Copy link
Member

@dead10ck dead10ck left a comment

Choose a reason for hiding this comment

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

Looks great to me! As for bindings, I just went with what I thought was consistent with the current expand/shrink bindings, but I personally plan to bind them to C-{h,j,k,l} in my own config. I find these extremely useful, so I want them almost as easily accessible as moving the cursor itself.

helix-core/src/object.rs Outdated Show resolved Hide resolved
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

}

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

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)
}

Copy link
Contributor

@pickfire pickfire left a comment

Choose a reason for hiding this comment

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

Looks good to me, tested and it works.

@the-mikedavis
Copy link
Member Author

I think it may be more preferable be { since it seemed more useful.

yeah looks like [ is otherwise lsp diagnostic jumping, maybe {/} for these selection commands would fit better? There isn't anything bound for those keys yet

@pickfire
Copy link
Contributor

pickfire commented Jan 14, 2022

Hmm, I wonder if we should not use ]s since it is used for next spell in vim and kak, should we pick a key that is not used in vim and at the same time be clearer? Maybe ]t which means next tree item?

Copy link
Contributor

@pickfire pickfire left a comment

Choose a reason for hiding this comment

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

Looks good to me. @dead10ck what do you think about the key? If it's good then I guess I can merge this.

@dead10ck
Copy link
Member

That seems fine to me. It seems consistent with the default bindings. I'm going to rebind it anyway, so I don't have big concerns about the default.

Copy link
Member

@archseer archseer left a comment

Choose a reason for hiding this comment

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

I'd be okay with adding the Alt- mappings and we can experiment with them in master. It makes it easy to repeat without a sticky mode since you can just hold down Alt

}

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.

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)
}

Copy link
Member

@dead10ck dead10ck left a comment

Choose a reason for hiding this comment

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

Nicely done with the refactor!

Comment on lines +121 to +124
| `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` |
Copy link
Contributor

Choose a reason for hiding this comment

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

This seemed a bit weird to me, shouldn't it be hl to expand/shrink and hj to navigate between sibling?

Copy link
Member

Choose a reason for hiding this comment

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

This makes sense to me. If you picture the tree vertically, with expand, you are going up the tree, shrink you are going down. Next sibling is moving laterally to the right, and previous is left.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think about it the same way 鈽濓笍

I could see it going the other way too because often A-l will move your selection downwards (to the next function block for example), but that depends on the size of the selection (e.g. A-l will move right in a function's argument list if it's all on one line) which can be a little confusing

doc.set_selection(view.id, selection);
}
};
motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}

fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
Copy link
Contributor

@pickfire pickfire Jan 17, 2022

Choose a reason for hiding this comment

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

I think we can remove the &'static and just leave F. It seemed weird to pass the function as reference. In the meantime, I think we can just do select_sibling_impl(cx, Node::next_sibling).

Copy link
Member Author

Choose a reason for hiding this comment

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

the borrow checker gets upset here without the &:

error[E0507]: cannot move out of `sibling_fn`, a captured variable in an `Fn` closure
  --> helix-core/src/object.rs:30:44
   |
24 |       sibling_fn: F,
   |       ---------- captured outer variable
...
29 |       select_node_impl(syntax, text, selection, |descendant, _from, _to| {
   |  _______________________________________________-
30 | |         find_sibling_recursive(descendant, sibling_fn)
   | |                                            ^^^^^^^^^^ move occurs because `sibling_fn` has type `F`, which does not implement the `Copy` trait
31 | |     })
   | |_____- captured by this `Fn` closure

For more information about this error, try `rustc --explain E0507`.

and also if we only remove the 'static lifetime bounds:

error[E0310]: the parameter type `F` may not live long enough
    --> helix-term/src/commands.rs:5546:41
     |
5530 | fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &F)
     |                        - help: consider adding an explicit lifetime bound...: `F: 'static`
...
5546 |     cx.editor.last_motion = Some(Motion(Box::new(motion)));
     |                                         ^^^^^^^^ ...so that the reference type `&F` does not outlive the data it points at

error[E0310]: the parameter type `F` may not live long enough
    --> helix-term/src/commands.rs:5546:41
     |
5530 | fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &F)
     |                        - help: consider adding an explicit lifetime bound...: `F: 'static`
...
5546 |     cx.editor.last_motion = Some(Motion(Box::new(motion)));
     |                                         ^^^^^^^^^^^^^^^^ ...so that the type `[closure@helix-term/src/commands.rs:5534:18: 5544:6]` will meet its required lifetime bounds

For more information about this error, try `rustc --explain E0310`.

Is there a way to make the borrow checker happy in these cases?

Copy link
Member

Choose a reason for hiding this comment

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

From what I understand the 'static bound is necessary for functions and there isn't such a thing as a non-'static fn. I can't find a decent explainer article for it anymore though

}

fn select_next_sibling(cx: &mut Context) {
select_sibling_impl(cx, &|node| Node::next_sibling(&node))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
select_sibling_impl(cx, &|node| Node::next_sibling(&node))
select_sibling_impl(cx, Node::next_sibling)

Does this compile?

Copy link
Member Author

Choose a reason for hiding this comment

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

looks like the typing is slightly off, rustc wants a &Node::next_sibling:

error[E0308]: mismatched types
    --> helix-term/src/commands.rs:5550:29
     |
5550 |     select_sibling_impl(cx, Node::next_sibling)
     |                             ^^^^^^^^^^^^^^^^^^
     |                             |
     |                             expected reference, found fn item
     |                             help: consider borrowing here: `&Node::next_sibling`
     |
     = note: expected reference `&'static _`
                  found fn item `for<'r> fn(&'r helix_core::tree_sitter::Node<'_>) -> std::option::Option<helix_core::tree_sitter::Node<'_>> {helix_core::tree_sitter::Node::<'_>::next_sibling}`

error[E0308]: mismatched types
    --> helix-term/src/commands.rs:5554:29
     |
5554 |     select_sibling_impl(cx, Node::prev_sibling)
     |                             ^^^^^^^^^^^^^^^^^^
     |                             |
     |                             expected reference, found fn item
     |                             help: consider borrowing here: `&Node::prev_sibling`
     |
     = note: expected reference `&'static _`
                  found fn item `for<'r> fn(&'r helix_core::tree_sitter::Node<'_>) -> std::option::Option<helix_core::tree_sitter::Node<'_>> {helix_core::tree_sitter::Node::<'_>::prev_sibling}`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `helix-term` due to 2 previous errors

But then doing &Node::next_sibling:

error[E0631]: type mismatch in function arguments
    --> helix-term/src/commands.rs:5550:29
     |
5550 |     select_sibling_impl(cx, &Node::next_sibling)
     |     -------------------     ^^^^^^^^^^^^^^^^^^^
     |     |                       |
     |     |                       expected signature of `for<'r> fn(helix_core::tree_sitter::Node<'r>) -> _`
     |     |                       found signature of `for<'r> fn(&'r helix_core::tree_sitter::Node<'_>) -> _`
     |     required by a bound introduced by this call
     |
note: required by a bound in `select_sibling_impl`
    --> helix-term/src/commands.rs:5532:8
     |
5530 | fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
     |    ------------------- required by a bound in this
5531 | where
5532 |     F: Fn(Node) -> Option<Node>,
     |        ^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `select_sibling_impl`

@archseer archseer merged commit 392dfa0 into helix-editor:master Jan 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants