Create sequential marks in VIM vs the direct

2020-12-17 @Technology

I’ve finally addressed an issue that’s irritated me for years. VIM default ‘mark’ functionality. It actually functions similar to myriads of other applications: keystroke navigable web browsers, pdf viewers, the Unix LESS pager, etc.

That is, as you navigate content, type a shortcut (’m' in the VIM case) followed by some alphanumeric character to assign a mark to that text location. Another shortcut key followed by the same mark character returns there. In the case of VIM that’s the single quote followed by the mark. To leverage an existing marker naturally overwrites the old.

Now in the W3M terminal web browser (next to VIM, my other favourite application of all space and time), the mark feature functions better yet; actually, best of any application I’ve encountered:

A single stroke creates a mark at the cursor position, and not only that, but creates a subtle, one-character-width highlight at the precise location. The same stroke over that position removes the mark. Two sets of navigational strokes then proceed back and forth through all marks, no need for other mental bookkeeping.

(The above mentioned strokes are freely mappable to any keys. In W3M I’ve assigned m to the MARK command, . to NEXT_MARK and , to PREV_MARK. It’s quiet admirable how seamlessly and rapidly I can navigate across notable locations of text content across pages.)

And that’s what I desired in VIM: this kind of autonomous mark mechanism that frees me from the mental bookkeeping in tracking used and unused markings. Alternatively, you can view the existing-mark roster via the :marks command, but this being a cumbersome extra step.

As far as accessing the marks, most of the time, I actually prefer the ‘sequential access’ across the marks in lieu of the ‘direct’. I simply desire to navigate from one saved position to the next, each of roughly similar importance. Again, no mental bookkeeping.

Incredibly, VIM already enabled such sequential access across all these symbolic marks. The ]' combination navigates to the next sequential position with a mark present, while [' navigates to the previous.

Fantastic. All I now needed was the flexibility for the sequential creation of marks.

And of course, there is always some vimscript plugin developed by someone to address a desirable feature. But I avoid plugins. For ten or more years of VIM usage, I think I have two external plugins.

If I were to seek a plugin for every similar case, 1) my VIM setup would be saturated in dozens of them by now, 2) their management would become notably unwieldy, 3) I would never have the pleasure to not only discover unused VIM features but also to maintain algorithmic thinking in shape.

Anyway, I went ahead and created the following rather simple vimscript function called NewMark(), to which I’ve mapped the ;m combination. It essentially finds the next alphabetically unused lower-case mark (as upper case marks serve a file-global role), and applies it. This thanks to the getmarklist() built-in function available since the more recent VIM versions.

Having consumed the entire alphabet, it then proceeds round robin to ‘a’, overwriting the previous. Though never have I required over 26 mark positions in any individual buffer.

You can place the function and mapping in your vimrc config or in any loadable script you prefer (mine resides in ~/.vim/scripts/helpers.vim, loadable from the main).

function! NewMark()
    if (!exists('b:last_mark'))
        let list = getmarklist(bufname())
        call filter(list, {idx, val -> val['mark'] =~ '[a-z]'})
        if (len(list) == 0)
            let b:last_mark = 'z'
        else
            let b:last_mark = strpart(list[-1].mark, 1)
        endif
    endif
    if (b:last_mark == 'z')
        let b:last_mark = 'a'
    else
        let b:last_mark = nr2char(char2nr(b:last_mark) + 1)
    endif
    execute "mark" b:last_mark
    echo "Mark set:" b:last_mark
endfunction

And the normal-mode mapping:

nmap ;m :call NewMark()<CR>

The function also echoes the newly assigned mark character upon invocation. That enables you to instantly recognize the mark character for that position, should you wish to directly navigate to it without first reviewing the :marks list.

Hence we now have a direct and a sequential way to both create and navigate marks!

Summary:

With the above function in place: create sequential VIM marks via ;m, navigating across them via the already built in ]' and [' strokes.

Questions, comments? Connect.