helix icon indicating copy to clipboard operation
helix copied to clipboard

Add `jump_after_surrounding_pair` motion for jumping out of pairs

Open the-mikedavis opened this issue 3 years ago • 3 comments

jump_after_surrounding_pair is a motion suggested by @dead10ck (https://github.com/helix-editor/helix/discussions/1209#discussioncomment-1741613) which jumps the cursor out to the point after current auto-pairs pairing.

This can be used as a tabout mechanism.

With auto-pairs enabled, you'll occasionally find yourself stuck when working across lines. Consider this Rust example

if condition {
    "yes"
} else {
    "no"
}

If you type this from beginning to end character-by-character, you'll get stuck and have to go to normal mode. The {/} break on lines and respect indentation which is problematic when you finish typing the "yes" expression. You have to go to normal mode and hit j to continue inserting to add the else branch.

With this command, you hit C-space (a temporary binding worth discussing) to jump to the next auto-pair's end while staying in insert mode. You can also use this generally for auto-pairs that aren't broken across lines so you don't need to type the closing pair character.

I think it's possible to implement this as a macro keybind:

<esc>mamli

But because of mam, not without macro keybinds. It's might also be nice to have it as a command for future refactoring and tuning, plus it respects counts and can be re-used with A-..

I have this bound as C-space locally which works nicely and is easy to remember but we might want to formalize this into tabout by binding it to tab (though that would take some work so that tab could still indent).

https://asciinema.org/a/513079

the-mikedavis avatar Aug 08 '22 02:08 the-mikedavis

This isn't as much of a problem for this, since insert mode bindings can be remapped, but I should point out that as described in #3352, on MacOS, ctrl-space is a system binding and can't be used as an editor keybind.

Omnikar avatar Aug 08 '22 05:08 Omnikar

C-space is also often used as a leader key for tmux/terminal.

ChrHorn avatar Aug 08 '22 12:08 ChrHorn

Mentioned this in the chat room, but I'll post here too:

I'm not super experienced with directly working with tree-sitter yet, but I was actually thinking it shouldn't need to be aware of text objects or auto pairs at all. I was thinking it would be purely from the parsed syntax. If the node happens to be a string, function body, if expression, or closure arguments that is surrounded by a pair, that should just be coincidental. With that kind of approach, we would have motions analogous to w, e, b, etc, just based on tree-sitter syntax

dead10ck avatar Aug 08 '22 14:08 dead10ck

So, how come hitting > in auto-pairs will just replace the trailing >, but } won't simply because it's on another line?

AceofSpades5757 avatar Aug 12 '22 02:08 AceofSpades5757

So, how come hitting > in auto-pairs will just replace the trailing >, but } won't simply because it's on another line?

? Is this question regarding this PR? And I'm not really sure I understand it anyway. If the cursor is resting on top of a closing char of a pair, it will skip over it; otherwise, it will just insert the char you typed.

dead10ck avatar Aug 12 '22 02:08 dead10ck

So, how come hitting > in auto-pairs will just replace the trailing >, but } won't simply because it's on another line?

? Is this question regarding this PR? And I'm not really sure I understand it anyway. If the cursor is resting on top of a closing char of a pair, it will skip over it; otherwise, it will just insert the char you typed.

It was addressing part of the issue for why this PR was created. If the cursor ignored the newline character, then it would skip over the }, removing the need to hit Escape, and this probably wouldn't have been an issue.

I still like the new command though 😄

AceofSpades5757 avatar Aug 12 '22 02:08 AceofSpades5757

Oh, I see. Yeah, although it also tries to address cases where you may have a few heterogeneous characters, e.g. foo(map["string"]) where your cursor would be on the " after having typed everything. In order to escape the function call, you have to type "]), whereas with a tabout command, you'd just hit tab 3 times.

dead10ck avatar Aug 12 '22 02:08 dead10ck

Oh, I see. Yeah, although it also tries to address cases where you may have a few heterogeneous characters, e.g. foo(map["string"]) where your cursor would be on the " after having typed everything. In order to escape the function call, you have to type "]), whereas with a tabout command, you'd just hit tab 3 times.

🤔 foo{map<item>} Will skip right over both pairs. In this case, I wouldn't use the tab.

EDIT: Never mind. I totally misread what you said. Though I'd probably just hit escape, lol.

AceofSpades5757 avatar Aug 12 '22 02:08 AceofSpades5757

This is what I was thinking for the node movement: https://github.com/dead10ck/helix/commit/18cd3692cf345495ffdd2fdbf0df0395a9ecd326

It has a couple differences, though, like this will go to the end of the current node first, if it's in the middle of a node, and depending on how granular the grammar is, you could end up needing to repeat it more times to get to the place you want.

Other options I'm considering are just jumping straight to the parent, instead of going to the end of the current node first -- consequence would be e.g. you'd not be able to move to the next function argument position before escaping out to the end of a function call.

The other thing I'm considering is whether it should do parents or siblings, or simply walk all nodes in the tree regardless of depth, or make all of these options with different commands.

The other thing I'm considering is whether it would be useful to still have a lexical motion for files that don't have a grammar, like maybe do this if a grammar is available, and if not, fall back to the lexical surround pairs like you did

dead10ck avatar Aug 12 '22 02:08 dead10ck

This is what I was thinking for the node movement: dead10ck@18cd369

It has a couple differences, though, like this will go to the end of the current node first, if it's in the middle of a node, and depending on how granular the grammar is, you could end up needing to repeat it more times to get to the place you want.

Other options I'm considering are just jumping straight to the parent, instead of going to the end of the current node first -- consequence would be e.g. you'd not be able to move to the next function argument position before escaping out to the end of a function call.

The other thing I'm considering is whether it should do parents or siblings, or simply walk all nodes in the tree regardless of depth, or make all of these options with different commands.

The other thing I'm considering is whether it would be useful to still have a lexical motion for files that don't have a grammar, like maybe do this if a grammar is available, and if not, fall back to the lexical surround pairs like you did

You're commit looks like it's pretty far behind the master branch.

I like the dichotomy between the next and prev commands you provided in your version.

AceofSpades5757 avatar Aug 12 '22 03:08 AceofSpades5757

This implementation appears to do what you wanted, except that if it doesn't find a match, then it'll move a character ahead. Is this intentional?

The difference is that it's not looking for matches at all, it's simply traversing the syntax tree. Similarly to how e will go to the end of the next word, this command will go to the end of the parent node in the tree.

You're commit looks like it's pretty far behind the master branch.

It's actually based on top of #2267.

dead10ck avatar Aug 12 '22 04:08 dead10ck

This implementation appears to do what you wanted, except that if it doesn't find a match, then it'll move a character ahead. Is this intentional?

The difference is that it's not looking for matches at all, it's simply traversing the syntax tree. Similarly to how e will go to the end of the next word, this command will go to the end of the parent node in the tree.

I like your idea of traveling the tree, and I see how it would make sense in the context of the node tree. Should it select text as well?

AceofSpades5757 avatar Aug 12 '22 04:08 AceofSpades5757

I like your idea of traveling the tree, and I see how it would make sense in the context of the node tree. Should it select text as well?

I'm actually not sure, I thought about it, but I don't think it would have the same utility as selecting a word. It will often select syntax elements like )}]", etc, and often span lines.

dead10ck avatar Aug 12 '22 04:08 dead10ck

Mentioned this in the chat room, but I'll post here too:

I'm not super experienced with directly working with tree-sitter yet, but I was actually thinking it shouldn't need to be aware of text objects or auto pairs at all. I was thinking it would be purely from the parsed syntax. If the node happens to be a string, function body, if expression, or closure arguments that is surrounded by a pair, that should just be coincidental. With that kind of approach, we would have motions analogous to w, e, b, etc, just based on tree-sitter syntax

But back to your original message I was looking at, I'm not sure but it sounds like both jump_after_surrounding_pair and node navigation would be worthy additions. With this one specifically targeting auto-pairs while yours is associated with the tree-sitter syntax tree.

AceofSpades5757 avatar Aug 12 '22 04:08 AceofSpades5757

Yeah, they could definitely be complementary.

dead10ck avatar Aug 12 '22 04:08 dead10ck

This implementation appears to do what you wanted, except that if it doesn't find a match, then it'll move a character ahead. Is this intentional?

Hmm no, I think currently this is ignoring the possibility of not being within a pair which should be fixed. There's also a bug where it'll skip over two pair characters in some cases. I suspect a nefarious off-by-one error 🧐

Ideally this would be tab/C-i and it would be smart enough to either insert indentation or jump out of the auto-pairs like tabout features in other editors. It might take some workshopping to come up with a general enough strategy for choosing between those two modes.

the-mikedavis avatar Aug 12 '22 16:08 the-mikedavis

On that note, I think there should be a backwards directions as well, depending on the implementation: jump_before_surrounding_pair.

AceofSpades5757 avatar Aug 12 '22 17:08 AceofSpades5757

Ideally this would be tab/C-i and it would be smart enough to either insert indentation or jump out of the auto-pairs like tabout features in other editors. It might take some workshopping to come up with a general enough strategy for choosing between those two modes.

I was thinking this could be a general enough feature that it could really do any command. We could have a config option like

supertab = "jump_after_surrounding_pair"

And it could be rebound to the tree-sitter variant or any other command.

dead10ck avatar Aug 13 '22 12:08 dead10ck

Let's close this in favor of #4443

the-mikedavis avatar Oct 26 '22 01:10 the-mikedavis

Actually I was thinking it could be useful to have both. Some users might find it more helpful for the jumps to be more predictable than tree-sitter syntax trees can be, and also for docs where there is no grammar available.

dead10ck avatar Oct 26 '22 03:10 dead10ck

Ah yeah that makes sense. I'll revisit this when #4443 is in, it still has a bug or two to fix when there are multiple adjacent pair closing characters

the-mikedavis avatar Oct 26 '22 12:10 the-mikedavis