9

Consider the following line:

€   €

This file is encoded in UTF-8 and between the two Euro marks is a Tab character (the Stack Exchange software expands this to spaces). The byte value is:

e2 82 ac 09 e2 82 ac 0a

In my script I would like to get the character under the cursor.

Vim has several useful functions for dealing with strings in a multibyte-aware fashion, but getting the cursor position seems difficult:

  • col('.') will return the byte position. So for the tab it will return 4 rather than 2.
  • virtcol('.') almost works, but will expand tabs, so this isn't a true character position either.
  • All other functions behave as one of the two above.

What I have so far is:

fun! Getchar()
    let l:line = substitute(getline('.'), '\t', repeat(' ', &tabstop), 'g')
    return strcharpart(l:line, virtcol('.') - 1, 1)
endfun
nnoremap GG :echo Getchar()<CR>

Which almost works, but not for the above example since the displayed width is less than &tabstop spaces long.

2
  • 2
    I would simply use yl plus some saving and restoring of register to take care of. Commented Feb 19, 2017 at 18:34
  • Me too, @ChristianBrabandt. I'd put it into register ", which I don't use (my default is +/*). But you can use any one that won't bother you. Examples: nnoremap q :<C-u>exe 'norm! ""yl' \| echo @"<CR> nnoremap q ""yl:echo @"<CR>
    – awvalenti
    Commented Feb 20 at 19:47

4 Answers 4

8

We discussed it less then 10 days ago on reddit.

One solution is matchstr(getline('.'), '\%'.col('.').'c.') which doesn't mess with any register.

I've put the resulting functions (we can ask for the characters before, or after the cursor) in my vimscript library along with other functions on the subject.

3
  • Also note that we must not use virtcol here but actually col since the regexp will be \%1c. where 1 is an example interpolated col. If you have a virtcol value you must use \%1v. instead (:h ordinary-atom)
    – SidOfc
    Commented Dec 8, 2020 at 18:53
  • Isn't this highly inefficient for very long lines? getline('.') returns the entire line no matter how long it is.
    – Flux
    Commented Oct 17, 2021 at 7:31
  • 1
    @Flux, it depends on what you're doing. If you just test a character before expanding an abbreviation or something like that, you won't notice anything. Control statements (loops, if...) are where we waste most of our time in vimscript language pre v9. Commented Oct 17, 2021 at 11:28
4

The following works and is faster than the matchstr() solution:

fun! Getchar()
  return strcharpart(strpart(getline('.'), col('.') - 1), 0, 1)
endfun
nnoremap GG :echo Getchar()<CR>

See my question Reliably get the character at a byte index in a string.

1
  • Best solution in my opinion. Commented Dec 23, 2020 at 11:22
3

If you have the patch 8.2.2324, you can use the charcol() function in a :def function:

def Getchar(): string
    return getline('.')[charcol('.') - 1]
enddef
nnoremap GG :echom Getchar()<CR>

It works because in Vim9 script, inside a string slice, indexes match characters, not bytes. This is documented at :h expr-[]:

In Vim9 script: If expr8 is a String this results in a String that contains the expr1'th single character from expr8. To use byte indexes use |strpart()|.

You don't have to write a new :def function to leverage this syntax. You can include it in an existing legacy function, using the :vim9 command modifier:

fun! Getchar()
    vim9 return getline('.')[charcol('.') - 1]
endfun
nnoremap GG :echo Getchar()<CR>

:vim9 requires the patch 8.2.2511.

During my limited tests, charcol() + :vim9 in a legacy function was a bit faster than strcharpart() (between 5% and 10% faster). charcol() in a :def function was about twice as fast.


To get the character after the cursor:

vim9 echom getline('.')[charcol('.')]

To get the character before the cursor:

vim9 echom charcol('.') == 1 ? '' : getline('.')[charcol('.') - 2]

Or a bit simpler:

vim9 echom getline('.')->strpart(0, col('.') - 1)[-1]

If you have the patch 8.2.2344, which provides the slice() function, then you can get away without :vim9.

To get the character under the cursor:

echom getline('.')->slice(charcol('.') - 1, charcol('.'))

The character after the cursor:

echom getline('.')->slice(charcol('.'), charcol('.') + 1)

The character before the cursor:

echom getline('.')->slice(charcol('.') - 2, charcol('.') - 1)
1

I have been using this in my plugin.

let char = matchstr(getline('.')[col('.') - 1 :], '^.')

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.