1

I hope someone can riddle me this one: I have set up the function below to have a slightly more fancy behavior of the End key, moving first to the end of the line, but if the cursor is already at the end of the line, moving to the end of the next line. This works just fine:

inoremap <silent> <End> <c-\><c-n>@=<SID>ExtEnd()<cr>a
nnoremap <silent> <End> @=<SID>ExtEnd()<cr>
vnoremap <silent> <End> @=<SID>ExtEnd()<cr>

function! s:ExtEnd()
   if strlen(getline('.')) == 0            | return 'j'
   elseif strlen(getline('.')) == col('.') | return 'j'
   else                                    | return '$'
   endif
endfunction

Here is my issue: I wanted to make this even more fancy by checking if the cursor is in front of the first non-blank character on the line. If that is the case, I would have it move to the first non-blank character on the first press of End, then to the end of the line on the next, and finally to the end of the next line on the third. To achieve this, I need to figure out the position of the first non-blank character on the line. I didn't find a function to do that directly, so I thought of a slightly kludgy solution of saving my cursor position (spos), jumping to the first non-blank character, saving that position (fpos), and then jumping back to the original saved position. The resulting code looks like so (this isn't finished yet, the actual cursor motions defined within the if-block are unchanged at this point):

function! s:ExtEnd()
   let spos=getpos(".")
   normal! ^
   let fpos=getpos(".")
   call setpos(".", spos)
   if strlen(getline('.')) == 0            | return 'j'
   elseif strlen(getline('.')) == col('.') | return 'j'
   else                                    | return '$'
   endif
endfunction

In theory (well, my theory anyway) the additional four lines

   let spos=getpos(".")
   normal! ^
   let fpos=getpos(".")
   call setpos(".", spos)

should have no effect, but they do: Now my cursor goes to the end of the line on the first press of End, but then it will jump to the first non-blank character on the next line, which doesn't seem to make any sense at all to me. Even if those four lines should change the cursor position (which they shouldn't) that should not result in the downward motion of the cursor. It's almost like the code is not executed in the sequence given.

What is going wrong here?

3 Answers 3

2

You do save and restore the cursor position. However note, that when moving to the first non-blank character ^, you also set the desired column for the cursor (called curswant attribute). That one is not reset on your setpos() call. Therefore, when your function returns j to move down a line, it will move down and to the desired column position (curswant).

To prevent that, use the getcurpos() function instead of getpos() function, which additionally also stores the curswant attribute of the cursor. getpos() can handle either a 4 member list (the traditional way, without the curswant attribute) or a 5 member list (including the curswant attribute). Those functions are quite new, the getcurpos() function is only available in Vim 8.

Also please note, that your function could possibly be written easier by makeing use of expression mappings (however, it does not allow :norm commands).

4
  • Excellent, thanks very much, this explains my problem! However, there should be a way to restore that curswant attribute in Vim 7.x, too. I can't believe that was not possible before 8.x. Also, I wonder if you could expand on your comment regarding expression mappings. Thanks again!
    – Pirx
    Commented Mar 14, 2018 at 14:24
  • no Vim 7 did not have this function. However you could use winsaveview()/winrestview() functions instead. Commented Mar 14, 2018 at 14:28
  • Great, that's exactly what I needed!
    – Pirx
    Commented Mar 14, 2018 at 14:39
  • @Pirx to use an expression mapping, simply replace your current maps with lines like: nnoremap <silent> <End> <SID>ExtEnd() See :h map-expr
    – Rich
    Commented Mar 14, 2018 at 14:41
1

Whilst I admire your kludge (not being sarcastic! I love that sort of workaround!) you actually can find the position of the first non-blank on the current line without moving the cursor about:

let first_non_blank = match(getline('.'), '\S')

Note that the value it returns is zero-based, so if there are no spaces at the start of the line it will return 0.

1
  • Awesome, thanks! Added to my answer below.
    – Pirx
    Commented Mar 14, 2018 at 15:02
0

Just to complete this following the answers given by Christian, here is the complete function that does what I described in my original question:

function! s:ExtEnd()
   let savedview=winsaveview()
   normal! ^
   let fnzcol=getpos(".")[2]
   call winrestview(savedview)
   if strlen(getline('.')) == 0            | return 'j'
   elseif getpos(".")[2] < fnzcol          | return '^'
   elseif strlen(getline('.')) == col('.') | return 'j'
   else                                    | return '$'
   endif
endfunction

With Rich's suggestion above, we have the more elegant version below:

function! s:ExtEnd()
   let fnzcol=match(getline('.'), '\S')
   if strlen(getline('.')) == 0            | return 'j'
   elseif getpos(".")[2] < fnzcol          | return '^'
   elseif strlen(getline('.')) == col('.') | return 'j'
   else                                    | return '$'
   endif
endfunction

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.