0

I would like to execute a command every single time I enter visual mode and another command every time I leave visual mode. Is there a way to achieve this?

My objective is to automatically record what I do when I enter visual mode. I want to be able to automatically do :normal! qv to store everything in the v register, and when when I'm back to normal mode :normal! q to stop recording.

  • I want to be able to automatically do :normal! qv q does not work inside normal (read :h q). Also, visual mode is only good for text select, and you can restore a selection with gv, for example. No need to save all the keypresses. – Matt Oct 7 at 11:47
  • I don't think there is a non hacky way to do it. From :h autocmd-events you can see there is no event for when you switch to visual or normal mode, only insert mode. But creating a mapping for changing modes shouldn't be too hard. The hard part is to log you keystrokes independently. I think I would try to use :h -w and parse the produced file afterwards – statox Oct 7 at 11:48
  • 1
    Also your question sounds like a XY problem: You should explain us what you want to do in the end, maybe there is an easier way to do it. – statox Oct 7 at 11:49
  • @Matt I think there is a good reason for doing it, to be able to repeat operators while visually seeing what range is selected by the text-object. It changes the syntax to v -> text-object -> operator instead of operator-> text-object, but can be quite useful in my opinion. Also it allows to expand, now I can do "visual in quote", and to expand to an upper quote level if I repeat "in quote". Recording it allows to repeat it with no effort. What are your thoughts? – Blasco Oct 7 at 12:41
4

If your Vim version contains the patch 8.1.2044, you could try this:

let s:last_mode = 'n'
augroup visual_enter_normal_enter
    au!
    au SafeState * call s:fire_visualenter_or_normalenter()
augroup END

fu! s:fire_visualenter_or_normalenter() abort
    if reg_executing() isnot# '' | return | endif
    let mode = mode()
    if s:last_mode is# 'n'
    \ && index(['v', 'V', "\<c-v>"], mode) != -1
    \ && exists('#User#VisualEnter')
        do <nomodeline> User VisualEnter
    elseif s:last_mode isnot# 'n'
    \ && mode is# 'n'
    \ && exists('#User#NormalEnter')
        do <nomodeline> User NormalEnter
    endif
    let s:last_mode = mode
endfu

augroup record_keys_in_visual_mode
    au!
    au User VisualEnter call s:recording_start()
    au User NormalEnter call s:recording_stop()
augroup END

fu! s:recording_start() abort
    if reg_recording() is# ''
        let @v = mode()
        call feedkeys('qV', 'int')
    endif
endfu

fu! s:recording_stop() abort
    if reg_recording() is# 'V'
        call feedkeys('q', 'int')
    endif
endfu

I haven't tested the code a lot, but it seems to do what you want; i.e. record all the keys typed while in visual mode inside the register v.

Note that SafeState is fired frequently, so s:fire_visualenter_or_normalenter() will be run frequently too, and I don't know how much impact it will have on Vim's performance, nor whether it can cause unexpected issues.


If you don't have SafeState, you can use a timer which repeats invoking its callback forever:

let s:last_mode = 'n'
let s:waiting_time = 10
call timer_start(s:waiting_time, {-> s:fire_visualenter_or_normalenter()}, {'repeat': -1})

fu! s:fire_visualenter_or_normalenter() abort
    if reg_executing() isnot# '' | return | endif
    let mode = mode()
    if s:last_mode is# 'n'
    \ && index(['v', 'V', "\<c-v>"], mode) != -1
    \ && exists('#User#VisualEnter')
        do <nomodeline> User VisualEnter
    elseif s:last_mode isnot# 'n'
    \ && mode is# 'n'
    \ && exists('#User#NormalEnter')
        do <nomodeline> User NormalEnter
    endif
    let s:last_mode = mode
endfu

augroup record_keys_in_visual_mode
    au!
    au User VisualEnter call s:recording_start()
    au User NormalEnter call s:recording_stop()
augroup END

fu! s:recording_start() abort
    if reg_recording() is# ''
        let @v = mode()
        call feedkeys('qV', 'int')
    endif
endfu

fu! s:recording_stop() abort
    if reg_recording() is# 'V'
        call feedkeys('q', 'int')
    endif
endfu

If you notice a too big impact on Vim's performance, try to increase the value of s:waiting_time.

If you notice that some typed keys are missing from the register v, try to decrease the value of s:waiting_time.


For more information, see:

:h SafeState
:h User
:h reg_executing(
:h reg_recording(
:h mode(
:h index(
:h exists(
:h feedkeys(
:h timer_start(
:h :do
  • That's amazing. Thank you very much, you made my day – Blasco Oct 7 at 12:39
  • It doesn't work perfectly though. If I go to visual, then to insert, then to normal, it seems not to catch it – Blasco Oct 7 at 12:45
  • 1
    @Matt It's probably not necessary here, but I use it to be consistent with other comparisons where is is useful to avoid this issue: github.com/Kuniwak/vint/issues/233 – user938271 Oct 7 at 13:17
  • 1
    @Matt is# is a comparison operator just like ==#. I can use it as much as possible and avoid it only if necessary. Or I can use ==# as much as possible and avoid it only if necessary. But in practice, is# is more reliable when you intend to compare strings, so I prefer to use is# when I compare strings. I don't want to ask myself every time I perform a string comparison whether one of the operand could be a number; it is obvious here, but it may not always be the case. – user938271 Oct 7 at 19:43
  • 1
    If "language feature" was referring to the automatic conversion of a string into a number, I don't want to rely on it; I find it makes the code's behavior harder to predict. Even bram thinks that it may have been a bad idea (which is hard to get rid of now): github.com/vim/vim/pull/3857#issuecomment-457746952 – user938271 Oct 7 at 20:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.