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

Better f à la clever-f and vim-sneak like jump motion #274

Closed
sudormrfbin opened this issue Jun 15, 2021 · 30 comments · Fixed by #8875
Closed

Better f à la clever-f and vim-sneak like jump motion #274

sudormrfbin opened this issue Jun 15, 2021 · 30 comments · Fixed by #8875
Labels
A-keymap Area: Keymap and keybindings C-enhancement Category: Improvements

Comments

@sudormrfbin
Copy link
Member

sudormrfbin commented Jun 15, 2021

Since we're already deviating from the behavior of f in vim (multiline, not restricted to the current line), it'd we useful if we could go a step further and implement something like clever-f. It would enable pressing f again immediately after a previous f search (pressing any other key resets this) in lieu of ;. This is really useful when you get the count wrong for f and want to spam f to go to the desired match.

Another really useful feature would be something along the lines of vim-easymotion and vim-sneak for jumping anywhere in the view (uses two letters to find the jump point).

@pickfire pickfire added C-enhancement Category: Improvements A-keymap Area: Keymap and keybindings labels Jun 15, 2021
@pickfire
Copy link
Contributor

I think an issue with this is that it is not very discoverable.

@sudormrfbin
Copy link
Member Author

We could highlight the matches (similar to how clever-f does it) which would prompt the user to find out more since that is not the default behavior:

clever-f highlight

@pickfire
Copy link
Contributor

Ah, doom emacs seemed to have this as well. Why not? But doom emacs behavior is a bit different, if you do f then it will only highlight those in forward and F will only highlight those backwards, maybe that is an improvement over this.

@sudormrfbin
Copy link
Member Author

if you do f then it will only highlight those in forward and F will only highlight those backwards, maybe that is an improvement over this

That's how it's also done in sneak, which IMO is an improvement too

@CBenoit
Copy link
Member

CBenoit commented Jun 18, 2021

That's definitely a cool idea!
For the record, I think it would probably be useful to also keep the old command for usage in plugins.

@Omnikar
Copy link
Contributor

Omnikar commented Nov 4, 2021

Something like EasyMotion is one of the things that I would most like to see added to Helix, it's super convenient.

@sudormrfbin
Copy link
Member Author

The blocker for this is virtual text support, but @cessen had some concerns about the extensibility of the current rendering system to fit in virtual text #411 (comment).

@ggandor
Copy link

ggandor commented May 29, 2022

+1 for clever-f

Especially: @Omnikar who already started working on this in #1162

A modal editor with built-in Sneak-like movements would be beyond awesome in itself; but I'd like to bring Lightspeed and especially Leap to your attention, that take a more sophisticated approach than either Sneak or EasyMotion, aiming for the lowest cognitive overhead.

There are three separate ideas that could be implemented on top of Sneak:

  • Ahead-of-time displayed labels, i.e., labeling all targets right after the first keypress, and then filtering them after the second; the advantage is of course that usually you can type the three chars in a more or less continuous manner, without any interruption in the flow (this could be opt-out for those who tend to type really fast, and/or really care for visual noise reduction).

  • An invariant Sneak guarantees is that a label is always one character only. If there are lots of matches, we can use dedicated keys to switch between groups of them. This is better than EasyMotion's approach (labeling everything at once, using n-char labels), in that it eliminates a large number of implementation/UI problems and edge cases.
    But we can get the best of both worlds by simultaneously displaying labels for two groups, with different colors, so that you can always process the label before switching to the subsequent group.

  • Smart shifting between Sneak/EasyMotion-like behaviour: instead of one, there are two configurable label sets, a bigger "unsafe" and a smaller "safe" one. The latter contains keys that are very unlikely to be used right after a jump (like another jump/search command).
    If there are <= targets than the size of the safe set, we jump to the first target automatically, and wait for a potential label to be entered, or feed forward the input (Sneak-like); if there are more targets, we're automatically switching to the "unsafe" set, and jump only if the user selects a label (EasyMotion-like).
    The heuristic behind this is that the probability of the user aiming for the very first target lessens with the number of targets; also, the probability of being able to reach the first target by other means (www, f, etc.) increases. That is, staying in place in exchange of more comfortable labels becomes a more and more acceptable trade-off.

These features can in fact be added independently, but the combination of all three provides the most fluid experience of course. This Leap-style navigation would take some more work, but it's not fundamentally more complex than vanilla Sneak - displaying labels, i.e., virtualtext support is the only thing that is necessary in the core. I'd love to help if there is interest in (parts of) this!

@kellpossible
Copy link

I like using hop.nvim, and the hop line has replaced relative line numbers for me too.

@txtyash
Copy link
Contributor

txtyash commented Jun 19, 2022

hop.nvim built-in 🔥

@Omnikar Omnikar mentioned this issue Sep 11, 2022
3 tasks
@hadronized
Copy link
Contributor

hop.nvim creator here. Has anyone started working on adding such a mode to helix? If not, I might be interested working on it.

@pascalkuthe
Copy link
Member

pascalkuthe commented Dec 26, 2022

There is #3791 which is mostly just blocked on some extensions to the rendering system

@hadronized
Copy link
Contributor

Nice! Happy to see that!

@mawkler
Copy link

mawkler commented Jan 20, 2023

I would also like to give my vote to a behviour like leap.nvim's. Out of all the "cursor jumping" plugins for Neovim that I've tried Leap's approach is the most clever one in my opinion. Like @ggandor mentions, a big advantage with Leap is that it gives you one unique virtual label already after your first keystroke. This gives you more time to process the key to press to get to where you want, resulting in a smoother experience with basically no interruption.

You're already looking at the first and second key to press before even initiating the jump, and while you're typing the second character you're seeing the third character to type (if the two characters are unique you don't have to type a third). This is possible because you always type the character of the target and the real character after it.

@cd-a
Copy link
Contributor

cd-a commented Jan 20, 2023

I wholeheartedly have to agree with @mawkler here. Leap's implementation, after testing pretty much every single jump plugin out there, was by far the most intuitive, efficient and thought through in my opinion.

Reasons basically what @mawkler mentioned, and the readme of the repo he linked has great explanation as well.

@hadronized
Copy link
Contributor

That’s what’s going to be a problem with motion plugins: people will always find a given plugin better and smarter, and there is no consensus. So #3791 is really ambitious to implement both worlds, that’s truly amazing. 🙂

@pascalkuthe
Copy link
Member

What is the difference between hop.nvim and Lea.nvim? I don't really get that from the readme. #5340 seemed liked it worked Luke leap from looking at leaps readme

@hadronized
Copy link
Contributor

@pascalkuthe Hop is in the EasyMotion family. It creates a hint everywhere on the screen and you press those hints. The strategy is configurable (the labels can be distributed by sorting them depending on the distance to the cursor, keep them always the same regarding the buffer screen, etc.). Kitty implements such a thing too.

Leap / Lightspeed etc. are in the Sneak-based family. They are more opinionated and will distribute the labels in a different way. For instance, it works with two characters (Hop has lots of motions, like the 2-char, 1-char, pattern based, generalized via the API, etc.). It will start showing labels after you type the first character.

It’s a matter of preference, but yes, you will find a bit of overlap.

@pascalkuthe
Copy link
Member

@pascalkuthe Hop is in the EasyMotion family. It creates a hint everywhere on the screen and you press those hints. The strategy is configurable (the labels can be distributed by sorting them depending on the distance to the cursor, keep them always the same regarding the buffer screen, etc.). Kitty implements such a thing too.

Leap / Lightspeed etc. are in the Sneak-based family. They are more opinionated and will distribute the labels in a different way. For instance, it works with two characters (Hop has lots of motions, like the 2-char, 1-char, pattern based, generalized via the API, etc.). It will start showing labels after you type the first character.

It’s a matter of preference, but yes, you will find a bit of overlap.

Thanks for this. So the difference seems to be that leap.nvim is mostly just a two character search that automatically puts you at the first match and gives you the option to type a third character in case of ambiguities. By comparison #5340 will start showing 2 char patterns (of random characters) at word boundaries immediately (which are just "random") or allow you to type a single char and then show labels. I think implementing something like leap is actually easier. I think we could definitely have both and let the user decide. A leap like jump mode would actually be easier to implement I believe

@hadronized
Copy link
Contributor

I’m not sure it’s easier, I think it’s similar, because once you have the logic to generate the combinations of labels and the logic to place them as virtual text, the algorithms should be pretty similar. In Hop, I have some short-circuit where I will jump to sole occurrences, etc. Sneak has a more dynamic way of presenting things (you type a character, it already highlights stuff, then you type a second one and it might jump on sole occurrence or ask you for a label), but ultimately, it’s similar — and Leap is actually a subset of Hop, since Hop is more general, we could have a « HopLeap » mode. In that regard, I agree that sticking to a single kind of movement is easier.

Something that Hop users use probably the most is the concept of jumping to a word. It highlights all words and you jump to them directly. Most of the time, it’s faster than the 2-char, but if you think of worse complexity, then you might end up with 4 char to types (one to trigger Hop, and three characters to type). That last situation happens when you have a LOT of things on screen, and Hop does multi-window thing. It’s actually pretty hard to end up with three characters to type if your key set is large enough. For instance here, I have four buffers open and only a few 3-char sequences to type:

before
Screenshot 2023-01-23 at 15 37 51

after
Screenshot 2023-01-23 at 15 38 08

@hadronized
Copy link
Contributor

hadronized commented Jan 23, 2023

And invoking :HopChar2MW by typing ap:

Screenshot 2023-01-23 at 15 39 05

So yes, supporting everything Hop supports (or EasyMotion, really) is more work than Leap, but I don’t think Helix should support everything. It should probably support something like the « words » mode, and the dynamic 2-char from Leap. It should be enough, I guess?

@cd-a
Copy link
Contributor

cd-a commented Jan 23, 2023

Would be great if the leap like behavior would also be implemented. So much more convenient to jump and the other one is just so distracting :)

@ggandor
Copy link

ggandor commented Jan 23, 2023

I’m not sure it’s easier, I think it’s similar, because once you have the logic to generate the combinations of labels and the logic to place them as virtual text, the algorithms should be pretty similar.

Showing the labels ahead of time complicates things significantly, I think in that case the feature should be designed with that in mind from the start.

Something that Hop users use probably the most is the concept of jumping to a word. It highlights all words and you jump to them directly. Most of the time, it’s faster than the 2-char

if you forget about the pause between typing the trigger and the label, which is (mostly) eliminated by the "dynamic" 2-char mode

Leap is actually a subset of Hop, since Hop is more general

Neither is more general than the other, both can be fed with custom targets/patterns, as well as custom callbacks (to do something else instead of jumping).

The difference is that Leap does not have built-in commands for word/etc-wise motions, because the philosophy is that it's better not having to think about the kind of motion itself, and if "dynamic" 2-char mode is always at least an 80% solution in terms of net speed (and often the best), plus it eliminates the friction (=pause), then it makes sense to rely on it all the time. As noted, this is an opinionated approach, no need to agree with it.

@pascalkuthe
Copy link
Member

#5340 actually already implements hop.nvim like word jumping. It's about 1k loc (3.5k of that PR is my softwrap/virtual text PR so the line count is misleading). I know the co-debase pretty well (and helped with #5340) and I am pretty confident that leap/sneak like behavior could be added with less code for a variety of reasons.

That does not detract from the usefulness of either approach. I think both approaches have merits and it's probably personal preference to some degree. It would be sweet to have both (with shared code where possible) and people can bind whatever command they want (the default would be what @archseer prefers as usual).

@ggandor
Copy link

ggandor commented Jan 23, 2023

Everyone is tossing around "Hop-like", "Sneak-like" etc., as if we'd be talking about some binary choice between two particular behaviors that can be summarized in one sentence, while in fact there is a vast design space for such a feature, with many different, often orthogonal aspects.

  1. Where can you jump?

    • "extensions" of built-in Vim motions, i.e., "words", "lines", etc. (EasyMotion, Hop)
    • Fixed-length pattern (1-char, 2-char)
    • n-char pattern
    • All of the above, some of the above...
  2. Among those, what is provided as a default (built-in command), and what is left as an exercise to the reader (via API)?

    • Sneak: 2-char mode, no extension API
    • Leap: (dynamic) 2 char mode, API for everything else
    • Hop: everything built-in
    • etc.
  3. Given a match, how to "label" it (how to select the target)?

    • EasyMotion/Hop labels every match, it uses as many characters as needed for the labels (if there are gazillions of matches, it might even use 3-character labels).

    • Sneak only labels N matches at a time, where N is the number of available label characters. If there are more matches, you can shift to the next "group" with some dedicated key (like tab or space).

    • Leap works the same way as Sneak, but it labels two groups at the same time (with different colors), so your brain can process the label in the next group while pressing the switch key.

  4. At what phase do you see the target labeled if you use "pattern" input (1-char, 2-char, n-char)?

    • Obvious: after finishing the pattern.
      • Disadvantage: annoying pause while your brain processes the label that appears on the screen.
    • Leap's idea (for 2-char mode): after the first input character.
      • Disadvantage: complicated implementation, additional visual noise.
  5. Are the labels assigned once and for all?

    • For example, Pounce uses a fuzzy search algorithm, and reassigns the labels as you type (a label can change or disappear at the next keypress).
  6. Skip labeling the first target, and jump to it automatically?

    • Sneak: yep
    • Hop: nope
    • Leap: depends... (see above)
  7. Besides selecting a label, also allow for walking from match to match (like you can do with ;/, after f/t in Vim)?

@hadronized
Copy link
Contributor

hadronized commented Jan 24, 2023

and it's probably personal preference to some degree

Yes, I 100% agree with that.

Sneak only labels N matches at a time, where N is the number of available label characters. If there are more matches, you can shift to the next "group" with some dedicated key (like tab or space). Obviously, matches/groups are ordered according to the distance from the cursor.

Hey! I didn’t know that! I find that peculiar, and defeats completely things like cross-window jumps (in Hop, every commands like :Hop*MW. So it’s wrong saying that you can jump everywhere in 3 key presses, since you have to switch groups for some labels you don’t see, right?

Also,

Disadvantage: annoying pause while your brain processes the label that appears on the screen.

I agree and disagree. That « pause » is just you reading the hint label. In Hop, most of the label are 1 or 2 characters long (I never really type 3 characters long in real world use cases), and as such, your brain read the whole thing at once. In Leap, you see them ahead of time but does it really matter? I.e. you still need to think about the next key you type before actually jumping. I mean, I agree that in theory AOT labelling seems superior, but in practice, not so sure.


If we get both motions in Helix, I think it’s great and there’s really nothing to debate, as it’s a personal thing as mentioned above. To be honest, at first, I thought that Helix wouldn’t even implement any motion plugin (since it has relative line number) and because it’s such a niche world. I planned on implementing / using a Kitty kitten to do that (and I still consider it, because it allows to do the same thing with all my TUIs, not only Helix).

@mawkler
Copy link

mawkler commented Jan 24, 2023

@phaazon I've been using leap.nvim (or its predecessor lightspeed.nvim) for almost a year and I find that I basically always can get to where I want in 3 (or less) key presses. Far less than 1% of the time do I have to shift group.

@hadronized
Copy link
Contributor

@mawkler It’s probably similar to Hop then (I never have to type more than 3 key presses — 1 to trigger a Hop motion, and two key label at max). I was just mentioning the fact that @ggandor said that if we have gazillions matches (which is unlikely but it can happen multi-window on really big documents), Hop will use 3-char, hence 4 characters, while that switch group thing from Leap will probably require more.

@ggandor
Copy link

ggandor commented Jan 24, 2023

I find that peculiar, and defeats completely things like cross-window jumps (in Hop, every commands like :Hop*MW. So it’s wrong saying that you can jump everywhere in 3 key presses, since you have to switch groups for some labels you don’t see, right?

Yeah, right. This is one example where we optimize for the common case, and not over-generalize.

It's definitely easier to type 3 labels instead of pressing tab/space, say, 8 times, but similar to @mawkler's experience, on my laptop I don't remember ever having to switch to an actual invisible group in practice (when not stress-testing Leap on artificial examples), so even on a big screen it's probably a rare thing. (Remember, Leap labels 2*N matches right away.)

On the other hand, the fact that "a label is always one character, period", is a very nice invariant to rely on. Not only results it in a bit less visual noise, but more importantly, the handling of conflicts becomes far easier (which is far from trivial even with 1-char labels in case of 2-char patterns and AOT labeling).

It's not a clear win, I just explained the rationale. I've been toying with the idea of multi-char labels, but always rejected it for the above reasons.

I agree that in theory AOT labelling seems superior, but in practice, not so sure.

There's a super-simple way of finding that out, try it in practice.

If we get both motions in Helix, I think it’s great and there’s really nothing to debate, as it’s a personal thing as mentioned above.

I just listed a bunch of things above illustrating why the phrase "both motions" (meaning "leap" and/or/vs "hop") makes absolutely no sense in this context, as they're not representing one particular feature or behavior, but a configuration based on multiple sets of (possibly interdependent) choices and approaches, so in any case, there are things to think through before rushing to implement this. Anyway, as a Neovim user at the moment I don't care too much about how much bloat Helix accumulates, but still.

@semin-park
Copy link

From the implementation side of view, I'd like to point out that #5340 already supports both hop and leap style behaviors. jump_to_identifier_label could be thought of :HopWord and jump_to_char_label could be thought of :HopChar1 (which is similar to leap but with one character).

We can have further discussion on how to tackle the design space, but just wanted to point out that whichever direction we go, existing code should be in good shape in either direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-keymap Area: Keymap and keybindings C-enhancement Category: Improvements
Projects
None yet
Development

Successfully merging a pull request may close this issue.