1

I am trying to write a function for insert mode mapping in which I want to get the character before the cursor character. For texts that are purely ASCII, this is easy, since its byte index is the character index. However, for Unicode characters, this is complicated.

function! GetPreCursorChar()
  let line_text = getline('.')
  " I do not know how to do it now.
endfunction
1
  • Perhaps you can use cursor_chars from wordcount()? Nov 23, 2020 at 10:17

3 Answers 3

2

You can use getline('.')[:col('.')-2] to find the string of characters before the current cursor position.

Note that you need col('.')-2, first because col(...) returns 1 for the first column, while the substring syntax is 0-based (therefore -1) and also because when slicing a string with [start:end], the index for the end is inclusive, so the byte at that index is included in the result (therefore another -1, totaling an adjustment of -2.)

Note also that this won't work in the first column, when col('.') will return 1, so you might need to use a special handler for that case (for example, returning an empty string.)

At that point, you can use the strcharpart() function to access a specific character by index, which will give you access to the last character of that part (which happens to be the character just before the cursor.)

To help you find the index of the last character, you can use the strchars() function to find how many characters are in that string.

Putting it all together:

function! GetPreCursorChar()
  if col('.') <= 1
    " Cursor is on the first column.
    return ''
  endif
  let before_cursor = getline('.')[:col('.')-2]
  return strcharpart(before_cursor, strchars(before_cursor)-1)
endfunction
1
  • That is a clever idea. Thanks for this. My implementation is far longer than this...
    – jdhao
    Nov 24, 2020 at 5:14
1

We could match against the character before the cursor (.\ze\%#) and get the match(es), but match() doesn't work too well here; let's use substitute() instead:

let char_before = substitute(line_text, '\(.\)\%#', '\1', '')

But this may not work; the cursor position is a property of the buffer, not the line line_text string. Another idea:

.substitute/\(.\)\%#/\=execute('let char_before="'.escape(submatch(1), '"\').'"')/n
4
  • This is more complicated than I think. The substitube hack is hard to read and understand... I wonder if there are clearer ways
    – jdhao
    Nov 23, 2020 at 15:43
  • For the example with :substitute, you can use /n to not execute the substitution and just evaluate the expression. I'm not sure you need execute() there either, will a straight \=let ... just work? (I'll try that myself a bit later on...)
    – filbranden
    Nov 24, 2020 at 2:13
  • Doh, clever @filbranden ! I think execute() is required (expression, not statement)
    – D. Ben Knoble
    Nov 24, 2020 at 2:16
  • Yeah, I was thinking why I had that pattern in mind and then I realized it's actually about using add() to collect all the matches in a list...
    – filbranden
    Nov 24, 2020 at 2:57
0

This is my implementation. First, we find the character index for cursor char, and we can know the character index of pre-cursor char. Then we can use strcharpart() to get that character.

function! GetCursorCharIdx() abort
  " This function returns the character-based index for character under
  " cursor.

  " Get the character under cursor
  let line_text = getline('.')
  let cur_byte_idx = col('.')
  echo 'cur_byte_idx:' cur_byte_idx

  if cur_byte_idx == 1
    echomsg 'cursor char idx:' 0
    return 0
  endif

  " character index starts from zero
  let [ch_idx, byte_idx] = [-1, 0]

  for c in split(line_text, '\zs')
    let ch_idx += 1
    let byte_idx += byteidx(c, 1)
    echomsg ch_idx c byte_idx

    if byte_idx+1 == cur_byte_idx
      let pre_char = strcharpart(line_text, ch_idx, 1)
      echomsg 'pre char is:' pre_char 'pre char index:' ch_idx

      let cursor_char = strcharpart(line_text, ch_idx+1, 1)
      echomsg 'cursor char' cursor_char 'index:' ch_idx+1

      return ch_idx + 1
    endif
  endfor
endfunction

Now we can get the pre-cursor char like this:

let line_text = getline('.')
let cur_ch_idx = GetCursorCharIdx()
let pre_char = strcharpart(line_text, cur_ch_idx-1, 1)

Clearly, @filbranden's idea is more concise.

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.