カーソル位置の文字を取得 + Vim における文字列について

現在のカーソル行の文字列を取得するには getline('.') とすればよく,さらに現在のカーソル列は col('.') で取得することができる*1ので

echo getline('.')[col('.')-1]

とすればカーソル位置の文字が表示される.


というのはシングルバイト文字の場合でしか成立せず,カーソル位置にマルチバイト文字があると違う値が表示される.
UTF-8 環境で日本語なら あたりが表示されたりするんじゃないだろうか.
というのも,col('.') によって得られるのはバイト列と見たときのインデックスであり,文字列 str を str[idx] と参照したときも idx はバイトインデックスとして解釈されるからだ.
Vim では基本的に文字列はバイト列として扱われると考えて問題無いと思う.strlen('あ') == 3 である.


ただし.正規表現マッチのときはマルチバイト文字も1文字として解釈される.

matchstr('あiうeお', '.') " => あ
split('あiうeお', '\zs')[2] " => う
strlen(substitute('あiうeお', '.', 'x', 'g')) " => 5

正規表現以外では char2nr() の引数もそのような解釈をする.

char2nr('a')  " => 0x61
char2nr('あ') " => 0x3042

他にもあるかもしれない.


matchstr() は第3引数としてマッチングを開始するバイトインデックスを指定することができる.
よって,マルチバイト文字も考慮してカーソル位置の文字を取得するには

matchstr(getline('.'), '.', col('.')-1)

とすればよい.
これと同様の方法でコマンドラインモードにおけるカーソル位置の文字も取得できる.

matchstr(getcmdline(), '.', getcmdpos()-1)


別のアプローチとしては,ノーマルモードで yl することでカーソル位置の文字をレジスタに入れる形で取得することができる.
レジスタを壊すので,事前にレジスタの内容を退避させておく必要があることに注意.

function! s:current_cursor_char()
  let s = @"
  normal! yl
  let c = @"
  let @" = s
  return c
endfunction

これはノーマルモードおよびインサートモードで利用できるが,コマンドラインモードでは利用できないという欠点がある.


カーソル位置ではなく,カーソル位置から n 文字前 / n 文字後を取得したい,ということもあるかもしれない.
n 文字後の場合は matchstr() 方式をそのまま応用できる.

" n 文字後を取得
matchstr(getline('.'), '.', col('.')-1, n)

範囲を越えていた場合には空文字列が返る.
もちろんコマンドラインモードでもこの方法で可能だ.


問題は n 文字前を取得したい場合で,うまい方法を思いつけずにいる.
とりあえず

function! s:prev_cursor_char(n)
  let p = getpos('.')
  for i in range(1, a:n)
    normal! h
  endfor

  let s = @"
  normal! yl
  let c = @"
  let @" = s
  call setpos('.', p)
  return c
endfunction

このようにして一応取得することができる.が,コマンドラインモードでは使えないしそもそもダサい.
実装上,範囲を越えていた場合は行頭の文字を,空行の場合は空文字列を返すことになる.


ところで inoremap foo Foo() として,Foo() の中で normal! を使うと E523 というエラーが出るのは何故でしょうか…
inoremap foo =Foo() とすると問題無いのですが.

*1:ただし 1-origin.0 はエラーを表す