2

I want to write a function which allows me to execute different movement commands using the same key pressed repeatedly. So, I wrote a function Jumptails and mapped it to the key <End>. The first time I press <End> it jumps to the end of line ($), next to the end of the visible window (L) and finally to the end of file (G). If another key is being pressed, the move list shall be reset so that the next time I press <End> it starts from scratch using $.

For this to work I need to know from inside that function if the function has been executed before. I tried to use the colon register @:, but it does not seem to contain the last executed function, only the last executed "interactive" :ex command.

Is there some way to achieve something like this?

Here's the code I came up with so far:

let w:lastpos = []

func Jumptails() 
  let l:lastcmd = @:
  echom "lastcmd: " @:
  if l:lastcmd =~ 'Jumptails'  " <= doesn't work and is always false
    echom "inside"
  else
    let w:lastpos = ['G', 'L', '$']
  endif

  " extract next command and cycle command list
  let l:max = len(w:lastpos) - 1
  let l:current = w:lastpos[l:max]
  call remove(w:lastpos, l:max)
  call insert(w:lastpos, l:current)
  execute "normal! " . l:current
endfunc

nnoremap ^[[e~ :call Jumptails()<Cr>
5
  • 2
    store a buffer-local flag, indicating where you jumped last time. Then in your function, you can check this buffer-local flag Jul 28, 2022 at 11:37
  • @ChristianBrabandt I'm not sure how that would help? The OP doesn't want to know where they jumped to last time. They want to know if anything else has been done since.
    – Rich
    Jul 28, 2022 at 13:57
  • @Rich I read it like: I need to know from inside that function if the function has been executed before that's what you can do with storing the state in a buffer-local flag. If you manually moved the cursor e.g. to the last position, then you would have to manually check the position of the cursor, which may be a bit more complicated I believe Jul 28, 2022 at 14:34
  • @ChristianBrabandt I don't think that's what they meant. I'm writing up an answer now.
    – Rich
    Jul 28, 2022 at 14:38
  • Answers do not belong in Question bodies; post them separately so they can be voted on appropriately.
    – D. Ben Knoble
    Mar 6, 2023 at 21:59

3 Answers 3

2

An interesting idea!

The problem

It's not really possible to implement your mapping in the way you suggest. Vim only stores the last executed command internally (for use with the dot command), and even then, motions are not stored anywhere. It's possible to knock something up that allows you to track this, but doing so is pretty hacky.

So we need to come at it from another angle. Christian Brabandt had the bright idea in the comments of not tracking the last command at all and just checking the cursor position, which is probably the best solution.

But when he made that suggestion, I'd already written this:

A solution

nnoremap <silent> <End> :call Jumptails()<CR>

let s:positions = ['$', 'L', 'G']

" Reset Jumptails state and highlighting
function s:reset() abort
  unlet b:position_index
  match
endfunction

function! Jumptails() abort
  if !exists('b:position_index')
    let b:position_index = 0
  else
    if b:position_index == 2
      echo "End of file!"
      call s:reset()
      return
    endif

    let b:position_index = b:position_index + 1
  endif

  " Move
  execute 'normal!' s:positions[b:position_index]

  " Set a highlight for the cursor position
  match Cursor /\%#/

  redraw

  " Wait for next key typed
  let command = getchar()

  if command == "\<End>"
    " Jump to next position
    call Jumptails()
  else
    " Not a Jumptails command

    " Reset Jumptails state and highlighting
    call s:reset()

    " And pass on the typed key for regular processing
    if type(command) == type(0)
      " Convert to string
      let command = nr2char(command)
    endif
    call feedkeys(command, 't')
  endif
endfunction

How it works

  1. Instead of checking what the last command is when the function is run, it waits for the next keypress after it's run, with getchar().
  2. If the next keypress is another request to jump, it calls Jumptails() again and performs the next motion in the list.
  3. If the keypress is anything else, it just processes it normally, with feedkeys().
  4. While waiting for a keypress, the cursor is in the command line, so you can't see the position you've jumped to! As a workaround, the code above sets up a :match to highlight the cursor position.

Or you could use a plugin

The above is essentially creating a ad hoc submode. You could probably implement this pretty quickly with submode or tinykeymap.

2
  • wow, that's brilliant :) I'll give it a try and also implement the other direction (up). Thanks a lot!
    – Tom
    Jul 28, 2022 at 17:33
  • Hm, unfortunately doesn't work for me, because I can't map the key <End>, only this works: ^[[4~. But I cannot use this in if command == ....
    – Tom
    Jul 28, 2022 at 17:50
1

Ok, since I was unable to compare the keycode I get (^[[4~) with what getchar() returns, I came up with another solution:

function! Jumptails()
  let l:colat = col('.')

  execute "normal! $"

  if l:colat == col('.')
    " we are already at EOL
    execute "normal! L$"
    if l:colat == col('.')
      " we are also already at EOW
      execute "normal! G"
    endif
  endif
endfunction

function! Jumpheads()
  let l:colat = col('.')
  let l:rowat = line('.')

  execute "normal! 0"

  if l:colat == col('.')
    " we are already at BOL
    execute "normal! H"
    if l:colat == col('.') && l:rowat == line('.')
      " we are also already at BOW
      execute "normal! gg"
    endif
  endif
endfunction

nnoremap <silent> ^[[4~ :call Jumptails()<CR>
nnoremap <silent> ^[[1~ :call Jumpheads()<CR>
nnoremap <silent> <End> :call Jumptails()<CR>
nnoremap <silent> <Pos1> :call Jumpheads()<CR>

inoremap <silent> ^[[4~ <Esc> :call Jumptails()<CR> i
inoremap <silent> ^[[1~ <Esc> :call Jumpheads()<CR> i
inoremap <silent> <End> <Esc> :call Jumptails()<CR> i
inoremap <silent> <Pos1> <Esc> :call Jumpheads()<CR> i

It "checks" the current position by storing the current column, then just doing one of the desired movements, comparing if the new column equals the one before and if so, do the next movement, etc.

The Jumpheads() function does the same in the opposite direction (up) using the <Pos1> key. And I could even add something like jump to end/begin of paragraph or function if such a command exists in vim.

As a side effect the cycle is being broken if I do something else in between.

Also, it's surprisingly simple ;)

0

Here is another way to solve the original problem, not an attempt to solve the stated one:

function! JumpTails()
    let currentCursorPosition = getcurpos()
    let didntMove = 1
    while 1
        normal! $
        if getcurpos() != currentCursorPosition
            let didntMove = 0
            break
        endif
        keepjumps normal! L
        if getcurpos() != currentCursorPosition
            let didntMove = 0
            break
        endif
        keepjumps normal! G
        let didntMove = 0
        break
    endwhile
endfunction
nnoremap <key> <Cmd>call JumpTails()<CR>

The idea is quite simple: perform each motion if the previous one didn't move the cursor.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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