0

This is a feature that notepad++ has: when in a wrapped line, the first press of <Home> sends you to the beginning of the wrapped line. The second press sends you to the beginning of the text (soft BoL), and the third sends you to the true beginning of the line (hard BoL). After that, pressing <Home> toggles between soft BoL and hard BoL.

Similarly, pressing <End> inside of a line wrap sends you to the end of the wrapped line on the first keypress, and to the true end of the line after that.

Here is an example:

1 | // Code that generates Lorem Ipsum
2 | if (true) {
3 |     return "Lorem ipsum dolor sit amet, consectetur 
        adipiscing elit, sed d* eiusmod tempor incididunt 
        ut labore et dolore magna aliqua";
4 | else { return ""; }

Thus, if my cursor was at the star (*) in the "2nd" line of line 3, I would want pressing <Home> to send me to the 'a' of "adipiscing." I know that this can be done with g^. However, I want the next keypress to send me to the 'r' of "return," which is equivalent to ^. Then, another keypress should put me before the indentation, equivalent to 0. Pressing <Home> again at this point should return me to the 'r' as if I had pressed ^. Afterwards, the cursor should toggle between 'r' (the first non-whitespace character) and the first character in the line.

If, instead, with my cursor at the star (*), I pressed <End>, it should be equivalent to g$, landing on the 't' of "incididunt." Another press should bring me to the semicolon at the end of the line.

The difficult part of this is that vim seemingly does not store the wrapped lines, meaning I can't determine where a line starts or ends.

3
  • @ kadekai from return back to virtual column 1. It would just toggle, something like the behavior of this: vi.stackexchange.com/questions/23205/…
    – ArcWand
    Oct 11, 2022 at 18:53
  • Yes, the former.
    – ArcWand
    Oct 11, 2022 at 19:26
  • For future readers looking for similar but not identical functionality, there are a couple of related questions here and here.
    – Rich
    Oct 12, 2022 at 15:48

1 Answer 1

1

There are two possible approaches: either try to move the cursor, and use the information of whether or not you moved, or you can try to directly calculate what movement should be made.

The solution I am currently using is based on trying to move the cursor, and seeing where we end up. This is slightly difficult due to the fact that you cannot call normal in a function from an expression keymap. To workaround this, I make the keymap directly run the command, which results in an extremely long command string:

function GoEoL()
    return "
      \ let __GoEoL_last_cursor_pos=col('.')
    \ | execute 'normal g$'
    \ | if col('.') == __GoEoL_last_cursor_pos
        \ | execute 'normal $'
    \ | endif
    \ | unlet __GoEoL_last_cursor_pos
    \"
endfunction
nnoremap <expr> L "<Cmd>".GoEoL()."<CR>"

When going to the end of a line, the keymap tries to do g$. If this doesn't move the cursor, then it will do $.

For some reason, I needed to have the concatenation step when defining the keymap in order for the keymap to work. The variable names here are chosen to not interfere with other user defined variables, as I am not aware of any method to force a variable to only have a scope of the current command, besides using lua which I feel would make the function unnecessarily verbose.

Going to the beginning of the line is a similar story, but it's more complicated due to the fact that I want the ability to toggle between the first character and the first non-blank character:

function GoBoL()
    return "
    \  let __GoBoL_last_cursor_pos=col('.')
    \| execute 'normal g^'
    \| if col('.') == __GoBoL_last_cursor_pos
        \| let __GoBoL_last_cursor_pos=col('.')
        \| execute 'normal ^'
        \| if col('.') == __GoBoL_last_cursor_pos
            \| let __GoBoL_last_cursor_pos=col('.')
            \| execute 'normal 0'
        \| endif
    \| endif
    \| unlet __GoBoL_last_cursor_pos
    \"
endfunction
nnoremap <expr> H "<Cmd>".GoBoL()."<CR>"

Conveniently, this gives the correct behavior when either wrap or nowrap are set.

The approach of directly calculating what key would give the correct movement based on the current virtual column is based on this stackexchange post. From the same stackexchange that was linked in the question, I found the WinTextWidth() function helpful (modified slightly here):

" Find the width of the text on the current line
" Used to find the beginning and end of wrapped lines
function WinTextWidth()
    " Account for signs, if there are any
    redir => signs
    execute 'silent sign place buffer=' . bufnr('%')
    redir END
    let d = (signs !~# '^\n---[^\n]*\n$') ? 2 : 0

    " Total width of the window
    " Minus the width of the number column, if present
    " Minus the width of the fold column
    " Minus the sign column, if present
    return (winwidth(0)
                \ - (max([len(line('$')), &numberwidth]) * (&nu || &rnu))
                \ - &foldcolumn - d)
endfunction
" Go to the beginning of the line
function GoBoL()
    let winwidth = WinTextWidth()
    let whitelen = indent(line('.'))

    if virtcol('.') < whitelen + 2 && virtcol('.') != 1
        " If at or before first non-blank character, but not at the head of 
        " the line, go to the head of the line.
        return '0'
    elseif virtcol('.') > winwidth && virtcol('.') % winwidth - whitelen == 1 + strdisplaywidth(&showbreak) || col('.') == 1
        " If at the start of the line but not on the first line, or if we 
        " are at the first character, go to the first non-blank character.
        return '^'
    else
        " Else, go to the start of the wrapped line.
        " Can use either g^ or g0 here, since wrapped lines cannot start 
        " with spaces
        return 'g0'
    endif
endfunction
noremap <expr> <Home> GoBoL()

The GoBoL() function handles going to the beginning of the line (note that G, B, and L are all capitals!). We want to use either a soft BoL (^) or hard BoL (0) if the column of the cursor is on the first column of text. We use virtcol('.') to get the position of the cursor so that tabs and such are counted (<Tab> is usually ~4 characters wide, but only counts as one character).

If the cursor is not in the 1st column of a wrapped line, then we can just use g^ to achieve the desired result. Otherwise, we have to consider if the cursor is already in the first wrapped line or not. If it is, then we use 0; otherwise, we'll use ^.

End of line is similar:

function GoEoL()
    let winwidth = WinTextWidth()

    if virtcol('.') % winwidth == 0
        return '$'
    else
        return 'g$'
    endif
endfunction
nnoremap <expr> <End> GoEoL()

It's a bit simpler, since we aren't asking for it to also account for whitespace. Again, we use the virtual column modulo the width of the window to determine if the cursor is at a position; this time, checking for the end of the line.

Note that this will not be able to handle the case where nowrap is set; thus, if you switch between the modes frequently this approach is not preferred.

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.