0

I have made a simple function to echo the visually selected text. The function is simplistic as I am only interested in max one-line selection. The problem is, if the last character of the visual selection is multi-byte, then it is shown as <c3> instead of the displayed one.

function VSelection()
  let l:sel = {
        \ 'c_start': getpos("'<")[2],
        \ 'c_end': getpos("'>")[2]
        \ }
  let l:w = getline('.')[l:sel['c_start'] -1: l:sel['c_end'] -1]
  echo 'Selection: ' | echohl Comment |
        \ echon l:w | echohl NONE | echon ' len: ' . strchars(l:w)
endfunction

vnoremap <C-j> :<c-u>call VSelection()<CR>

Given the <C-j> mapping, and the following text foobar ßeslöäabc, I make the following selections (what is between the | represents the visual selection and what comes after ---> is the echo):

  1. |foobar| ßeslöäabc ---> Selection: foobar len: 6
  2. foobar |ßesl|öäabc ---> Selection: ßesl len: 4
  3. foobar ß|eslöä|abc ---> Selection: eslö<c3> len: 5
  4. foo|bar ßeslöäab|c ---> Selection: bar ßeslöäab len: 12

So the problem seems to be occuring only when the last char selected is multi-byte (nr 3) Instead of eslöä it echoes eslö<c3>. I have been looking into functions like byteidx, strcharpart, strpart trying to understand and fix the problem, but I did not manage.

1 Answer 1

2

This is indeed a tricky one, since the column at both start and end will be the index of the first byte of a multi-byte character and subtracting them will give you a length in bytes and not in characters.

The way I got to find the contents of the selection was to use strpart() twice, the first time using the start and end index to get all characters before the end character, then one more time to get exactly a single character at the end of the selection. strpart() takes an optional fourth argument to request a character, so that works perfectly for the last part.

Together with some simplification (you can use col() to get the column only, and you don't need to assemble a dictionary for the two positions), I got to this:

function! VSelection()
  let start = col("'<")
  let end = col("'>")
  let l:w = strpart(getline('.'), start-1, end-start)
        \ . strpart(getline('.'), end-1, 1, v:true)
  echo 'Selection: ' | echohl Comment |
        \ echon l:w | echohl NONE | echon ' len: ' . strchars(l:w)
endfunction

UPDATE: It seems the optional fourth argument of strpart() is a recent addition to Vim and might not be available in NeoVim or older versions of Vim.

An alternative then is to use strpart() to get the whole rest of the line starting at end, and then strcharpart() to take only the first character in that string. That way, we can use the byte index to access the rest of the string, but then recognize it as a full character.

function! VSelection()
  let start = col("'<")
  let end = col("'>")
  let l:w = strpart(getline('.'), start-1, end-start)
        \ . strcharpart(strpart(getline('.'), end-1), 0, 1)
  echo 'Selection: ' | echohl Comment |
        \ echon l:w | echohl NONE | echon ' len: ' . strchars(l:w)
endfunction

But perhaps a much simpler approach here is to simply use y to yank the visual selection into a register and get the contents from that register. You can have your function save and restore the register around the y operation.

function! VSelection()
  let savereg = getreg('"')
  let savetype = getregtype('"')
  silent normal! gvy
  let l:w = getreg('"')
  call setreg('"', savereg, savetype)
  echo 'Selection: ' | echohl Comment |
        \ echon l:w | echohl NONE | echon ' len: ' . strchars(l:w)
endfunction

You need gv to restore the last Visual selection (once you do :call in your mapping you're no longer in Visual mode), and the silent is so that Vim doesn't print "N lines yanked", which seems to interfere with the echos at the end.

Saving and restoring the register works flawlessly, once you're done with the function the default register is left in the exact same state it was before the call.

This approach also has an advantage of properly working with selections that spawn multiple lines, and also Visual Line and Visual Block selections.

12
  • 1
    Huh. weird thing, I just realized vim and neovim are different. vim: strpart({src}, {start} [, {len} [, {chars}]]) nvim: strpart({src}, {start} [, {len}]) So it does not work on neovim, which is unfortunate because it is what I use... Do you think there is other option that works on both?
    – skamsie
    Nov 27, 2020 at 17:40
  • 1
    It does work, thanks!
    – skamsie
    Nov 27, 2020 at 18:21
  • 1
    @filbranden getreg/setreg is not sufficient to restore the " register properly (look at where the register is pointing before and after via getreginfo). You may want to use a letter register like z instead, which can be restored
    – Mass
    Nov 27, 2020 at 20:27
  • 1
    @Mass I spent quite a while looking at register save/restore. Not sure that using a letter register makes it better, since the unnamed register also potentially points at something different after the operation. I posted a question about it: vi.stackexchange.com/q/28354/18609. Please take a look if you have ideas or opinions about it. Cheers!
    – filbranden
    Dec 9, 2020 at 21:34
  • 1
    @filbranden thanks, you're right, I was so focused on the register at hand, I forgot completely that after yanking to e.g., z. the " would be pointing at z. I wrote the patch for getreginfo and I still forget how this works most of the time!
    – Mass
    Dec 10, 2020 at 2:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.