読者です 読者をやめる 読者になる 読者になる

()<left> -> (<right>) -> :-)?

tech vim

ちょっとやってみたいことあって,久方ぶりに vimscript を書いてみることにした.

きっかけ

以前,こんな vimrc の記事 を見て,なるほどねー,と導入した.

inoremap {} {}<left>
inoremap [] []<left>
inoremap () ()<left>
inoremap "" ""<left>
inoremap '' ''<left>
inoremap <> <><left>
[Did you mean Kenta YAMAMOTO's blog vimキーマップ][01]

幾度か使っているうち,ひとつだけ思うところがありました.

それは,括弧の内容を打ち終わった後.

括弧から抜けて次の入力に移行するまでの手続き.

'[esc] -> [l] -> [a]'.

insert モードを抜け,右に移動し,そして再び insert モードに入る*1...

たった 1 文字分の移動に,特に勢いで流れに任せて入力しているようなケースでは特に,"たった" とは言え,この 3 つのステップで滞るのがどうしても慣れなかったりしていたわけです。
挙句,環境によっては平気で [→] キーを探す自分が居たり...

ということで考えてみることに.*2

考え方

イメージは,;

  1. 括弧内の編集が終わって,insert モードを抜ける時.
  2. 括弧の外となる位置までカーソルを移動して.
  3. 再びインサートモードに.

な感じ?

それぞれ.

1 については,
autocmd で InsertLeave イベントを拾う.ここでやりたい処理を実行すればいいのかな,と.

2 については,
col('.') で現在位置拾って cursor() で 1 つ転がせばよろし,程度に簡単なものと考えていた.そしたら全然違った orz 一番手間取ったところ.

3 については,
ネットで ここで :stopinsert というコマンドの存在を知ったことをきっかけ に startinsert というコマンド? に辿り着くことができた.助かった,ありがとう epanda さん、ben さん,って感じ :)
ちなみにこの最後のステップは,できたらでいいかな,程度に思っている.

材料

そしてそれぞれに,今回集めた材料はこんなとこ.;

こんなところでしょうか.

そして,この度,つまづき戸惑ったところが.;

  • マルチバイトを相手にした時.col('.') では上手く行かない事を知る.
    → matchstr() そして matchend() を知る.
  • matchend が返す値が,想定していたのとどうやらちょっと違うことに気づいて混乱.
    → 1 文字目は 0 .
  • stridx(),{needle} に '' が入るケースで,勿論 {haystack} には在るはずない,返すのは -1 じゃないの知らなくてちょっと戸惑ったこと.
    → 0 なんだ.
  • startinsert で insert モードに入る時,":set iminsert=0" って文字が挿入されて苦しめられたこと.
    → vimrc で,そうするよう記載していた自分自身の原因.でも何か体感できて妙にスッキリw

これから,つまづいたポイントについて,備忘録の意味も込め恥をしのんで書いておこう思います.

カーソルの位置を知るために

何はともあれ,現在の位置が取れるようになればよい.
それできれば,あとはズラすだけ.

な程度に,軽い気持ちで,col('.') を引っ張り出してくる.

が.
マルチバイトが相手になると,話が変わってくるんですね.

col() が拾うのは,"何文字" ではなく,"バイト".

思い描いている処理をするには,そのままではだめで,工夫が必要みたい.

おつむ弱いの自分. 「ただ 1 文字横に移動したいだけなのに...」,と途方に暮れる.

col('.') で拾って,それがマルチバイトか否か見て,そうだったらそのバイト分,そうじゃなかったら...って,んあーただ 1 文字横に行きたいだけなのに,こんなに色んな事しなきゃいけないの? と眩暈.

こんなのとか見せらると,そんなに色々考える必要あんのっ,...て,一気にやる気なくす.
このエントリーのオチも何か,スマートじゃないような気がして腑に落ちないし.

な中見つけたのが,コチラ
なるほど!

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

matchstr() のヘルプをにらめっこしていたら,それ繋がりで,matchend() を知ることに.
無知ってイタイ.

これで,一気に解決へグッと近づくわけです.

でもここでも,大きな勘違いしているわけで,それを知る由もなく prz

matchend() が返してくれるその値は "インデックス"

バイトではない.

そこを完璧勘違いしていた.
のでジットリとはまる.

ヘルプにもきちんと書いてあるのに.;

match() と同じだが、返されるのはマッチした部分文字列の終了後のインデックスである。

そう.インデックス.
文字列を相手にしているし,しかも col() の経験もあったのもあり,"バイト" を返しているものと完全に勘違い決め込んで作業していた.

1 文字目がターゲットとなった場合,それは "0" .

リストも扱えることを考えれば,容易に想像できただろうに.

ズレていることに気づかない無知ってホント悲しい.
信じる,と言う行為.間違った扱い方していると,本当に空虚で,その分恐ろしい.

stridx() は '' は,有るもんだとする?

隣の文字が閉じ括弧かどうかの識別をこれでやれば良いと思ってました.

{haystack}の中に{needle}がないときは-1を返す。

ということで,
隣の文字,たとえば next として,こんなイメージで書いたわけです.

if (stridx(")]}", next) != -1
    call cursor(line('.'), next + 2)
    startinsert
endif

閉じ括弧に出会ったら,括弧抜けて,再び insert モードへ. 閉じ括弧でなかったら,スーン...

ところが,思いも寄らないタイミングで insert モードに入る.

この "思いも寄らないタイミング" と言うのは,どうやら行末のご様子.

行末に来て,隣の文字として,拾う文字がない場合. next の値は '' となるケース.

試しに,:echo stridx(")}]", '') ってやってみると,-1 ではなく 0 が返ってきた.

ということで,隣の文字が '' の場合も条件に加味することに.

if next != '' && (stridx(")]}", next) != -1
    call cursor(line('.'), next + 2)
    startinsert
endif

そうなん?

":set iminsert=0" って文字が挿入される

最初,もう根の深いマニアックな話で,自分には扱えない問題かと思っていた.

ら違った.

いきなりビッ!と ":set iminsert=0" なんて文字列が入って,しかもカーソルが変なとこ飛んじゃう.

この iminsert って何だってことで見てみたら,
insert モードに入ったときの ime を制御するもの.

何か気になって,改めて vimrc 見て...思い出した(w

" インサートモード時imeオフ
set noimdisable
set iminsert=0 imsearch=0
set noimcmdline
inoremap <silent> <ESC> <ESC>:set iminsert=0<CR>

この inoremap <silent> <ESC> <ESC>:set iminsert=0<CR> なのね.

今回の括弧を抜ける処理,
autocmd InsertLeave ... で呼び出すようにしている.

insert モード抜けようと [esc] キーで,これが発動してるのね.

まぁ,ドキドキ イライラさせられましたが.
逆に,この仕組みが,このように働いている事を目の当たりに出来て,ちょっと得した気分にもなったり.

ということで,iminsert に 0 を入れる処理,別なところで動くようにしたのですが. これでいいのかな?(笑

ということで

こんな感じにしました.

これが良いのかどうか,正解なのかどうかは,正直,今の自分のレベルでは,まったく自信ない(恥

いつかもう少し後で見たら,「どーしてこんなことやってんだか...」なんて思う自分がもしかしたら居るのだろうか,と想いを馳せつつ.

あともしかしたら,呼び出し方.
autocommad よりは map 使った方がスマートなのかな.
この辺りの判断基準,摘み分けセンスがないのも怖い.

ということで,はい,おしまい.

うーん,知らないことたくさん...

augroup ExitBracket
    autocmd!
    autocmd InsertLeave * call ExitBracket()
augroup END
function! ExitBracket()
    exe "set iminsert=0"
    if mode() ==# 'n'
        let matchend_idx = matchend(getline('.'), '.',  col('.') - 1)
        let check_str = strpart(getline('.'), matchend_idx, 1)

        let str_bracket = ')]}>"'''
        if check_str != '' && stridx(str_bracket, check_str) != -1
            if col('$') == matchend_idx + 2
                startinsert!
            else
                call cursor(line('.'), matchend_idx + 2)
                startinsert
            endif
        endif
    endif
endfunction

*1:細かいこと言うと '[esc] -> [l] -> [l] -> [i]' とか,位置によっては '[esc] -> [A]' もありますが.

*2:insert モードでも移動できるよう [Ctrl] キーといつもの jkhl キーを合わせる方法もあるのですが,自分の場合,<C-j> か何かで引っ掛かってイラッとすることが多々あったので,現在コメント化している.ここ皆さん,どうしてるんだろ,っていーっつも思っている.