Skip to content

Instantly share code, notes, and snippets.

@g0xA52A2A
Last active August 21, 2022 17:58
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save g0xA52A2A/4a03da0be21e4f39e72d66ad8340d131 to your computer and use it in GitHub Desktop.
Save g0xA52A2A/4a03da0be21e4f39e72d66ad8340d131 to your computer and use it in GitHub Desktop.

Vim will move the cursor to the beginning of an object after invoking operator upon it. From an interactive editing perspective this may be considered annoying however it is the consistent choice as operators can be destructive. As such restoring the cursor to its prior position after invoking an operator on an object may not make sense.

There are many ways possible to alter this behaviour to your preference with mappings and/or scripting. But with custom operator mappings this can be particularly ugly.

A common method is to drop a mark or save a view in the mapping. However the mapping must end with g@ so Vim will wait for the operator, meaning the mapping itself can't move the cursor movement back to its original location. As such the movement would have to be placed inside the function that operatorfunc has been set to. I consider this quite ugly as functions should not have to have such a priori knowledge about the mappings that are calling them. Note that dropping a mark or saving a view inside the operatorfunc function is not an option as the cursor has already been moved by the time this is invoked.

A neat solution to this is to save a view whenever operatorfunc is set with an autocmd. Then whenever the cursor is moved check if operatorfunc is set, if so restore the view, dispose of the temporary variable containing said view, and unset operatorfunc with autocmd's disabled as to avoid a loop.

To avoid having an autocmd always fire when the cursor is moved we can instantiate it only when operatorfunc is set. Now that this instantiation occurs whenever operatorfunc is set we can make two further refinements. Firstly we can stop checking if operatorfunc is set and secondly the autocmd that restores the view can be one shot.

function! OpfuncSteady() abort
  let w:opfuncview = winsaveview()
  autocmd OpfuncSteady CursorMoved,TextYankPost *
    \ call winrestview(w:opfuncview)
    \ | unlet w:opfuncview
    \ | noautocmd set operatorfunc=
    \ | autocmd! OpfuncSteady CursorMoved,TextYankPost *
endfunction

augroup OpfuncSteady
  autocmd!
  autocmd OptionSet operatorfunc call OpfuncSteady()
augroup END

One could even create re-implementations of Vim's built in operators to leverage this technique, though I'll leave that as an exercise for the reader.

@romainl
Copy link

romainl commented Aug 19, 2019

That's an interesting, somewhat holistic, approach. Just like having the quickfix window open automatically in a plugin-agnostic way. I like it.

Sometimes, when looking at my vim-qf and things like this, I feel there's room for a "global smoothing" plugin that would remove the need of a number of hacks but there would be an uphill battle against all the snippets and plugins using those hacks.

@g0xA52A2A
Copy link
Author

Thank you for the kind words, I'm quite proud I did this without a mapping.

I agree there's certainly room for such a plugin. Though similarly I'd only be happy with it if mappings were kept to a minimum, or (unlikely) none at all if possible.

Perhaps an amalgamation of such hacks would be a good article for this year's Vimways?

@romainl
Copy link

romainl commented Aug 20, 2019

Perhaps an amalgamation of such hacks would be a good article for this year's Vimways?

You have got until late October to write it.

@mroavi
Copy link

mroavi commented Aug 21, 2022

Hi @George-B. I'm interested in testing whether this approach works with the repeat . operator. Could you share a small example showing how to use your implementation? For example, I would like to create a mapping for gq that formats the lines covered by the motion and leaves the cursor in the same place.

@g0xA52A2A
Copy link
Author

Hi Martin,

I've been meaning to factor this out into a proper plugin for some time but just haven't got around to it. This doesn't work with . as-is and it's not something I've looked into, though I'm sure it could be done with the help of something like repeat.vim.

For example, I would like to create a mapping for gq that formats the lines covered by the motion and leaves the cursor in the same place.

I've not implemented in my own config as this is potentially what I describe as "destructive" in the post above. That is after the operation is performed the line and column the cursor was initially on may no longer reside within the object that was operated upon. For example if you are near the end of a paragraph and gqip ends up reducing the number of lines a paragraph takes up you'd be restoring you cursor to a position no longer inside that paragraph. This is precisely why Vim's default behavior, whilst not the most elegant at first glance, makes sense.

That said assuming you're not relying on an external 'formatprg' Vim's gw has you covered. It does not suffer from what I have just described as it is more sophisticated in that it truly "puts the cursor back at the same position in the text" as described in the help.

The same could be implemented for this but that probably outgrows the scope of a gist and would be something for a proper plugin.

Cheers,
George

@mroavi
Copy link

mroavi commented Aug 21, 2022

Ohh wow! I did not know that qw was something. It also works with the repeat operator. Thank you very much!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment