[vim/vim] new optional argument for "virtcol()" to give index of first screen cell instead of last one (#7964)

79 views
Skip to first unread message

lacygoill

unread,
Mar 15, 2021, 1:25:45 AM3/15/21
to vim/vim, Subscribed

Is your feature request about something that is currently impossible or hard to do? Please describe the problem.

It is not easy to get the index of the first screen cell of a file position, like the cursor, or a visual mark. This information is necessary when we intend to use it later in a regex with a \%v atom.

virtcol() gives the index of the last screen cell, if 'virtualedit' is empty (which it is by default). This is problematic if we are on a character which occupies several screen cells, like a tab.

vim9script

setline(1, "the\tquick\tbrown\tfox")

norm! 22|

var vcol: number = virtcol('.')

norm! 0

search('\%' .. vcol .. 'v')
the cursor has not been repositioned right before "fox"

✘

Notice that when virtcol('.') was evaluated and saved into a variable, the cursor was on the tab right before "fox". Also notice that search() failed to restore the cursor position after :norm! 0. That's because \%v expects the index of the first screen cell of a character, not the last one.

Describe the solution you'd like

A new optional boolean argument for virtcol() ({firstcell}), which – when true – would make virtcol() give the index of the first cell instead of the last one. And which would be immune to 'virtualedit'. That is, if we're on the middle of a tab character while 'virtualedit' has the value all, virtcol('.', true) would still give the index of the first screen cell of the tab.

Describe alternatives you've considered

Alternatively, we can use this expression:

virtcol([line('.'), getline('.')->byteidx(charcol('.') - 2) + 1]) + 1

Which works as expected when used in the previous snippet:

vim9script

setline(1, "the\tquick\tbrown\tfox")

norm! 22|

var vcol: number = virtcol([line('.'), getline('.')->byteidx(charcol('.') - 2) + 1]) + 1

norm! 0

search('\%' .. vcol .. 'v')
the cursor *has* been repositioned right before "fox"

✔

But:

  • it requires 4 extra functions
  • it makes the code harder to read
  • it's probably much slower

Additional context

The previous example is a bit contrived. We don't really need a regex, nor a \%v atom. We could restore the cursor position with a :norm command:

exe 'norm! ' .. vcol .. '|'

But there are times where we really need a regex and a \%v atom. In those cases, virtcol() is not easy enough to use.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or unsubscribe.

lacygoill

unread,
Mar 15, 2021, 1:36:15 AM3/15/21
to vim/vim, Subscribed

Alternatively, we can use this expression:

virtcol([line('.'), getline('.')->byteidx(charcol('.') - 2) + 1]) + 1

It can also be tested and compared to virtcol('.') like this:

vim9script
setline(1, "a\tb\tc")
for i in range(5)
    echom virtcol('.')
    norm! l
endfor
1
8
9
16
17

Those are the indexes of the last screen cells of the five characters on the line, which won't match anything with \%v.

vim9script
setline(1, "a\tb\tc")
for i in range(5)
    echom virtcol([line('.'), getline('.')->byteidx(charcol('.') - 2) + 1]) + 1
    norm! l
endfor
1
2
9
10
17

Those are the indexes of the first screen cells of the five characters on the line, which will match all the characters on the line when used in a \%v atom.

lacygoill

unread,
Mar 15, 2021, 1:42:39 AM3/15/21
to vim/vim, Subscribed

Those are the indexes of the last screen cells of the five characters on the line, which won't match anything with %v.

The actual values depend on 'tabstop'. Also, some of those values will match a character with \%v; namely 1, 9 and 17. That's because those screen cells are occupied by characters which only need 1 screen cell; IOW, the first and last screen cells of these characters are the same. But the values 8 and 16 won't match anything with \%v.

lacygoill

unread,
Mar 15, 2021, 1:46:51 AM3/15/21
to vim/vim, Subscribed

BTW, I use the term "screen cell", but I'm not sure it's correct. The help at :h virtcol() uses "screen column"; but in other locations, it also uses "display cell" or "screen cell". Not sure whether there are differences between all of them.

lacygoill

unread,
Mar 15, 2021, 3:14:00 PM3/15/21
to vim/vim, Subscribed

Closing because there is a simple workaround. Instead of writing this:

'\%' .. virtcol('.') .. 'v.'

We can write this:

'.\%' .. (virtcol('.') + 1) .. 'v'

Example:

vim9script
setline(1, "the\tquick\tbrown\tfox")
norm! 22|
var vcol: number = virtcol('.')
norm! 0
search('.\%' .. (vcol + 1) .. 'v')

lacygoill

unread,
Mar 15, 2021, 3:14:01 PM3/15/21
to vim/vim, Subscribed

Closed #7964.

lacygoill

unread,
Mar 15, 2021, 3:26:54 PM3/15/21
to vim/vim, Subscribed

Ah no, I re-open because there are other cases where this workaround can't work, and we really need the index of the first screen cell. Here is one – real – example:

vim9script

com -bar -range=% RemoveTabs RemoveTabs(<line1>, <line2>)



def RemoveTabs(line1: number, line2: number)

    var view: dict<number> = winsaveview()

    var mods: string = 'sil keepj keepp'

    var range: string = ':' .. line1 .. ',' .. line2

    var pat: string = '\t'

    RemoveTabsRep = (): string =>

          synstack('.', col('.'))

        ->mapnew((_, v: number): string => synIDattr(v, 'name'))

        ->match('heredoc') >= 0

            ? "\t"

            : repeat(' ', strdisplaywidth("\t", VirtcolFirstCell('.') - 1))

    for i in [1, 2]

        exe mods .. ' ' .. range .. 's/' .. pat .. '/\=RemoveTabsRep()/ge'

    endfor

    winrestview(view)

enddef

var RemoveTabsRep: func



def VirtcolFirstCell(filepos: string): number

    var lnum: number = line(filepos)

    var col: number = getline(filepos)->byteidx(charcol(filepos) - 2) + 1

    return virtcol([lnum, col]) + 1

enddef

This installs a custom Ex command :RemoveTabs whose purpose is to replace all the tabs inside an arbitrary range with spaces. The command must not break any possible alignment. So, if a tab occupies 3 screen cells, it should be replaced with 3 spaces. If another tab occupies 7 screen cells, then it should be replaced with 7 spaces.

Notice that – for it to work correctly – it needs the custom function VirtcolFirstCell(). This is something which I need in quite a few places. So, I've turned it into an exportable function which I import whenever necessary. Still, I think it would be better if Vim could give the information with a builtin function. This way, this line:

: repeat(' ', strdisplaywidth("\t", VirtcolFirstCell('.') - 1))

                                    ^-------------------^

Could be rewritten like this:

: repeat(' ', strdisplaywidth("\t", virtcol('.', true) - 1))

                                    ^----------------^

And we could get rid of VirtcolFirstCell(), as well as an import in every script where we need this kind of information.

lacygoill

unread,
Mar 15, 2021, 3:26:55 PM3/15/21
to vim/vim, Subscribed

Reopened #7964.

Bram Moolenaar

unread,
Mar 15, 2021, 4:44:58 PM3/15/21
to vim/vim, Subscribed

The internal function getvvcol() already does this, it returns the start, cursor and end position.
We can add an argument that specifies one of these.

Bram Moolenaar

unread,
May 26, 2022, 7:11:24 AM5/26/22
to vim/vim, Subscribed

Closed #7964 as completed via 0f7a3e1.


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issue/7964/issue_event/6684810334@github.com>

Reply all
Reply to author
Forward
0 new messages