Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Both f-like and t-like HopChar1 #82

Closed
ouuan opened this issue May 29, 2021 · 20 comments
Closed

Both f-like and t-like HopChar1 #82

ouuan opened this issue May 29, 2021 · 20 comments

Comments

@ouuan
Copy link

ouuan commented May 29, 2021

Now when HopChar1 is used to move the cursor, it behaves like f. When HopChar1 is used as an operator motion, it behaves like t.

I want to have two different commands, one always behaves like f and the other always behaves like t.

@hadronized
Copy link
Owner

Good call, I’ll add some modes to support those. There is a PR I need to review about that.

@michielappelman
Copy link

With the new direction option implemented in #93 it is now getting even more confusing as to when a motion is inclusive or exclusive the character to Hop to...

require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })

  • as hop motion is inclusive (f-like)
  • as operator is exclusive (t-like)

require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })

  • as hop motion is inclusive (F-like)
  • as operator is inclusive (F-like)

@hiberabyss
Copy link

Is it possible to make motion and operator have the same behavior? I would prefer operator performing like f .

@ranelpadon
Copy link

Shifted from using vim sneak, I'm looking forward to this also since it's big deal. At a minimum, I think the behavior must be consistent both in normal and operation-pending modes. Thanks :)

@ranelpadon
Copy link

ranelpadon commented Sep 1, 2021

@phaazon In hindsight, I agree with this suggestion of @hiberabyss above:

Is it possible to make motion and operator have the same behavior? I would prefer operator performing like f .

that is, even not exposing options to configure, hopefully the default operator is inclusive. HopChar1 actually is really potent, and could even be a powerful plugin on its own due to these advantages:

  • could precisely and quickly go to any character in the current line
  • could precisely and quickly go to any character in other lines
  • could be used as operator motions in d, c, or y.
  • could be executed with minimal keystrokes only (convenience/UX) and could compete vertically even with usual :10j or 10gg.

No reason to use the other variants actually (e.g. HopWord, HopChar2, etc). Yet, the only annoying part is that it's not including the hop character in the operator motion mode. For example, if I have my cursor at start of the line (i.e. in d in def foo):

def foo():
    print('foo')

def bar():
    print('bar')

If I need to yank that function, naturally I want to type this: y :HopChar1 ) <suggested-char> (i.e. ) is the last char in the foo function). But this will not yank it fully (i.e. the ) hop char is omitted):

def foo():
    print('foo'

so need to include the start char of next function (d) just to copy the full function of the current function which is not a good UX, like y :HopChar1 d <suggested-char>. Ideally, the user don't need to lookahead on next context just to execute something on the current context since it increases the cognitive load. Actually, this is also the annoying part in vim-sneak (which needs to type 2 chars): motion is inclusive like f or / but operator motion is exclusive like t.

I think setting it like f as a default motion/operator motion (at least in HopChar1) would make its behavior consistent, intuitive, and probably will be one of the major reasons to convince other users. Then, you could add options to configure it later on if still needed. I'm planning to write an article on this plugin also, but this feature seems to hold me back. Thanks :)

@ggandor
Copy link

ggandor commented Sep 1, 2021

Actually, this is also the annoying part in vim-sneak (which needs to type 2 chars): motion is inclusive like f or / but operator motion is exclusive like t.

Sneak aims to be consistent with the behaviour of the native search command (/), which is also an exclusive motion (i.e., lands on the target in Normal and Visual mode, but operations will not include it).

Also, with :h forced-motion (yv, dv...) you can make any motion inclusive/exclusive. You can even include the v modifier in your custom mappings. (N.B., - apologies for the self-plug -, Lightspeed already has a so-called full-inclusive mode, that is the inclusive sibling of s/S/z/Z, that extends the operated area to the very end, i.e. the second char. The plugin does not make default mappings for that mode at the moment, but no one stops you from making a custom mapping that adds the <c-x> prefix after the command.)

HopChar1 actually is really potent, and could even be a powerful plugin on its own due to these advantages: (...) No reason to use the other variants actually (e.g. HopWord, HopChar2, etc).

Particularly because you're coming from Sneak, you might be interested in this. TLDR, in my humble opinion, a feature like HopChar1 makes little sense in the first place. If the target is rather close (but not that close so that you can just {count}f to it), then you would most probably reach it directly with a 2-character pattern that you can type without interruption (well, at least in Sneak, where you jump to the first match automatically). And if it is distant, then it's still much better to type 2 on-screen char + 1 label than 1 on-screen char+ 2 labels...

That said, I agree that the most useful arrangement would be if there would be two versions of HopChar1 (no "default" one), corresponding to f and t (both inclusive as operations).

@ranelpadon
Copy link

ranelpadon commented Sep 1, 2021

@ggandor Thanks for your useful insights!

I have this mappings actually, and just realized from you that it's called as forced-motion:

" Backward motion, inclusive the current cursor
onoremap b vb
onoremap F vF
onoremap T vT

I don't agree with this though. Typing fab is better than 2fa. I use Colemak keyboard layout, so I really care about ergonomics and efficiency. Typing numbers require more effort and cognitive load. For distant cases, I could still type fab also in best/most cases, or fabc worst case (so it's still less keystrokes than Sneak most of the time):

TLDR, in my humble opinion, a feature like HopChar1 makes little sense in the first place. If the target is rather close (but not that close so that you can just {count}f to it), then you would most probably reach it directly with a 2-character pattern that you can type without interruption (well, at least in Sneak, where you jump to the first match automatically). And if it is distant, then it's still much better to type 2 on-screen char + 1 label than 1 on-screen char+ 2 labels...

Will take a look into your lightspeed project, seems really interesting also! :)

@ranelpadon
Copy link

@ggandor I tried your lightspeed plugin, putting here my observations for the benefit of other people here also:

Horizontal Motion (jump to e):

  • fe in lightspeed: seems no jump chars and for limited/adjacent lines only?
    Screen Shot 2021-09-02 at 1 56 26 AM

  • fe in hop.nvim (HopChar1): with jump chars and across multiple lines. 3/4 keystrokes to execute: feX (where X is the suggested char/chars).
    Screen Shot 2021-09-02 at 2 16 15 AM

Vertical Motion (jump to the end of the first method)

  • using s) in lightspeed: the last ) in order_by() is not even highlighted. and assuming we could jump to ,s instance, it would still be 4 keystrokes: s),s
    Screen Shot 2021-09-02 at 2 13 31 AM
  • using f) in hop.nvim (HopChar1): with jump chars for the ) occurrences. 3 keystrokes to execute: f)a
    Screen Shot 2021-09-02 at 2 16 53 AM

That's why I said that HopChar1 is potent. Let me know if my understanding is incorrect. I haven't included yet the inclusiveness part, just want to compare the keystrokes efficiency first. :)

@ranelpadon
Copy link

ranelpadon commented Sep 1, 2021

So, for now I'll stick with hop.nvim, and for the missing, inclusive operator-motion of HopChar1, this mapping works (as per the forced-motion suggestion of @ggandor above):

noremap f :HopChar1<CR>
onoremap f v:HopChar1<CR>

So, this is good enough for me now.

@ggandor
Copy link

ggandor commented Sep 1, 2021

@ranelpadon

Pardon me in advance for hijacking the thread, but I'm always eager to discuss these design issues, as I've been personally doing an awful lot of thinking on them.

fe in lightspeed: seems no jump chars and for limited/adjacent lines only?

Yeah f and t work the same way as in Sneak's or clever-f', they're just multi-line enhancements of the native motions, you can repeat like fXff or give them a count. Of course, you can jump anywhere on the screen with them if there are no matches in-between. But there are no hints/labels, because in most of the time, that would be less efficient than other approaches. I will try to argue for this further below.

In the following examples, x and y are for targeted chars, l1, l2 are for labels (or hints). f refers to 1-char search, while s to 2-char search.

I don't agree with this though. Typing fab is better than 2fa

I agree with you that using count is anti-ergonomical most of the time, but if it's not more than 2 or 3 (so it's not much cognitive load ultimately), it's still better to {count}fx than fx--slight-pause--l, because you can type the sequence in one go.

On the other hand, any matches beyond that are probably easier to reach by sxy in Sneak or Lightspeed. (HopChar2 unfortunately cannot jump to the first match directly.)

Now, after this, there is this grey zone where Lightspeed really shines (no pun intended): sxyl. In Sneak, or with HopChar2, that motion is less than perfect, because it is actually sxy--slight-pause--l. But in Lightspeed, it's (close to) sxyl; in most cases, the pause is almost zero, because you already see the label after x, and not just after xy. It's as if you should type anxyz on-screen pattern.
My big "discovery" was exactly this, when - originally - tried to port Sneak 1-1 to Lua. 2-character search is a sweet spot, that can be repurposed as a really universal, swiss-army-knife motion, that works in almost every situation, if we implement this one enhancement (and others that are possible because of this: what we call "shortcuts", and auto-jumping to unique characters).

And don't forget that, we're probably in fx-l1-l2 land now... I don't think I should argue for why sxyl is just easier then.
But even if it's just fxl... you're right that fxl is in some cases less keystroke than sxyl. But - and that is a big but: you cannot type it continuously.

using s) in lightspeed: the last ) in order_by() is not even highlighted.

Already answered here :)

and assuming we could jump to ,s instance, it would still be 4 keystrokes: s),s

That's true, but that ,s is done in one go, so the difference is not that big. (And this is a very degenerate example, I have to say, ), is probably the singe most difficult pair of characters to type. :) )

Also, keep in mind that if you use f and t with labels, then in operator-pending mode you cannot use them without labels anymore (i.e., just dfx). And that is the most frequent case actually! Using two different mappings for the "labeled" and the "non-labeled" mode beats the purpose (low cognitive load).

To conclude: indeed, there are some cases when fx--pause--l is technically less keystrokes, but that --pause-- is too much of a tradeoff, and especially Lightspeed's improved, universal sx(y/l)(l) - the parens mean that you might not need to type them - just feels easier to use. I'm happy to hear counter-arguments (or brand new ideas...) though.

@ranelpadon
Copy link

ranelpadon commented Sep 1, 2021

@ggandor Isn't that counting will also have pause/cognitive load since you need to visually/mentally count the position of the target?

I agree with you that using count is anti-ergonomical most of the time, but if it's not more than 2 or 3 (so it's not much cognitive load ultimately), it's still better to {count}fx than fx--slight-pause--l, because you can type the sequence in one go.

I'm curious, how do you propose yanking the entire def get() header/contents then using your plugin, assuming cursor is at d of def, and without using linewise yanking?

Already answered here :)

You might be right:

And this is a very degenerate example, I have to say

I have no plans of using t/T anymore, or non-labeled f I think, since labelled f seems to address most, if not all, of my usual use cases:

Also, keep in mind that if you use f and t with labels, then in operator-pending mode you cannot use them without labels anymore (i.e., just dfx). And that is the most frequent case actually! Using two different mappings for the "labeled" and the "non-labeled" mode beats the purpose (low cognitive load).

Actually, I'm still confused also on how to use your plugin, even when watching the pressed keys in your sample GIFs. Seems that some of the pressed keys have no relation to the transition key/target. Maybe I need to play with it more. Thanks also for these interesting points. At the end of the day, we all just want to have the best Vim/Neovim experience at all cost. hehe

@ggandor
Copy link

ggandor commented Sep 1, 2021

Isn't that counting will also have pause/cognitive load since you need to visually/mentally count the position of the target?

If you don't know it instantly, without thinking, then you shouldn't use count, for sure. I'm talking about the straightforward cases. If it's 2-3, not too distant, i.e. not several lines below, with a lot of stuff in-between, you don't have to count, you just "know".

I'm curious, how do you propose yanking the entire def get() header/contents then using your plugin, assuming cursor is at d of def, and without using linewise yanking?

"Full-inclusive" mode (soon to be re-branded as the bi-directional "x-mode") to the rescue: yz<c-x>') (in this example, no label is needed ') is unique). (z<c-x> can be sqeezed into a single mapping, I will use x/X for this I think, so that would be yx').) Actually I would just use y4j in this specific case, but generally, the above method.

I have no plans of using t/T anymore, or non-labeled f I think, since labelled f seems to address most, if not all, of my usual use cases

That is curious, {op}fx and {op}tx are among the most frequently used operations for me, it's hard to imagine editing without them.

Actually, I'm still confused also on how to use your plugin, even when watching the pressed keys in your sample GIFs.

That is a pity, but then feel free to raise an issue and ask questions there, so that we can improve the documentation. (Maybe this answer helps?)

Thanks also for these interesting points. At the end of the day, we all just want to have the best Vim/Neovim experience at all cost. hehe

Yep, thank you too. On that note, personally I think that simple intra-window navigation/operations are handled by Lightspeed in a superior, uniform, less cognitively tasking way now (of course, that's why I wrote it), and as a user I'd like to see Hop evolving more towards structural editing and/or complex motions, like https://github.com/mfussenegger/nvim-ts-hint-textobject, or selecting a range of lines for operations (as if using two HopLines after each other)... things like that. The possibilities are endless.

@ranelpadon
Copy link

ranelpadon commented Sep 2, 2021

@ggandor Yeah, this clarification here helps, especially this part:

Lightspeed's s/S is not like HopChar1, it's like an enhanced version of HopChar2. You should enter two characters as a search pattern, and you'll see one target label, that is an invariant. It is just a visual aid that the labels are visible right after the first input, so your brain can process them before you actually need to type them (this is one of the main inventions of this plugin). But you should type the second character in the pattern anyway. The advantage over Hop(/EasyMotion/Sneak)'s implementation is that while you're typing that second character, you already see the label, so there's no pause needed after that.

I think you might want to emphasize that in the docs.

I'm thinking though that you still need to look into the target label after typing the initial char, so there's also an slight pause, just like in HopChar1? You couldn't type smoothly if there's a label needed, and the visual effort on looking in that label is the same to that of HopChar1? It seems an improvement over the Sneak/HopChar2 behavior due to predictive hinting, but the effort is the same/greater when compared to HopChar1?

For example, when jumping to a in ab:

HopChar1:
fa --- scan the target label --- type the label

lightspeed:
sa --- scan the target label -- type the label(s)

Am I missing something?

@ggandor
Copy link

ggandor commented Sep 2, 2021

@ranelpadon

It seems an improvement over the Sneak/HopChar2 behavior due to predictive hinting, but the effort is the same/greater when compared to HopChar1?

Yes, in themselves, sabl and fa-l are more or less equivalent in terms of ergonomics, I'd say. Although the "pause" (-) might ultimately be more annoying/disorienting than just typing sabl in one go. (That might be subjective, there's room for debate.)

But here are my more convincing reasons against relying on labeled f:

  • You should always use labels from then on, and cannot just {op}fa, so you've made your life harder in the most common case.

  • fa-l is actually not that frequent. You might very well end up with fa-ll (two labels) if using f, and you don't know that beforehand.

  • On the other hand, s is 20-50x more precise, so on average there's a much bigger chance that it will ultimately be just sab (not even needing a label), if the a. matches tend to be distinct - especially if the target is so close that it is within the fa-l (one label) range.
    And if the context is so that there are a lot of similar matches (ab pairs everywhere, e.g. a variable appearing many times in a function body), then there's a fair chance that you can use shortcuts, i.e. sa-l, instead of sabl. It's like falling back to 1-character search, i.e. fa-l automatically. I'm telling you, it's clever :)

In short: Lightspeed's s, with the different shortcutting methods, is always close to optimal, while (Hop's or EasyMotion's) f might indeed be the fastest in 5-10% of the time, but equivalent, worse or significantly worse in the remaining 90%. It's easier to just rely on that one motion then, and reach for that automatically, than guessing and scanning the context all the time, because that beats the purpose of such plugins...

@gibfahn
Copy link

gibfahn commented Oct 18, 2021

Thanks for all the in-depth discussion folks, really helped me work out exactly the behaviour I'm looking for in a plugin, and it's great to see so many performant and interesting options (easy motion, sneak, hop, lightspeed).

I found @ggandor's railways vs jetpacks argument rather compelling, worth a read if you haven't already seen it.

hadronized added a commit that referenced this issue Nov 15, 2021
This allows to use Hop in either the `f` way or `t` way. This is mostly
useful for operator-pending modes such as `y` and `d`.

Relates to #82.
hadronized added a commit that referenced this issue Nov 15, 2021
This allows to use Hop in either the `f` way or `t` way. This is mostly
useful for operator-pending modes such as `y` and `d`.

Relates to #82.
@hadronized
Copy link
Owner

This is now implemented and part of the HEAD. I will make a release later tonight after I have implemented another feature.

@hadronized
Copy link
Owner

If you want to try it out, update your nightly version of Hop and simply pass the inclusive_jump = true option to the commands you want in inclusive mode (defaults to exclusive).

@ouuan
Copy link
Author

ouuan commented Nov 16, 2021

With the new direction option implemented in #93 it is now getting even more confusing as to when a motion is inclusive or exclusive the character to Hop to...

require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR })

  • as hop motion is inclusive (f-like)

  • as operator is exclusive (t-like)

require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR })

  • as hop motion is inclusive (F-like)

  • as operator is inclusive (F-like)

It's still inconsistent in the above ways. And the inclusive jump is neither f nor t when the exclusive jump is actually f-like, as it's one character after f.

UPD: Now this is being tracked at #191.

@ouuan
Copy link
Author

ouuan commented Nov 18, 2021

It's still inconsistent in the above ways. And the inclusive jump is neither f nor t when the exclusive jump is actually f-like, as it's one character after f.

@phaazon Could you reopen this or open a new issue?

@ouuan
Copy link
Author

ouuan commented Feb 21, 2022

See ed30204 for my fix. The current behavior of the inclusive_jump option makes it difficult to make changes, so I'll leave the work of documentation and proper handling of the breaking change to the maintainers, if they want to merge it.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants