FlowType のプラグイン作った

この記事は Vim アドベントカレンダー 2016 の23日目の記事です。
id:yuttie さんの comfortable-motion.vim よさそうなので入れてみたが自分の MacVim な環境では "E118: 関数の引数が多過ぎます: 128_tick" が出たので追いかけようと思います。
…と思ったけど、修正版が上がってたのでしばらく使ってみよう。
慣性スクロール素敵。


TL;DR

  • FlowType のプラグイン作った
  • complete() の微妙な挙動に気づいた
  • パッチ作るために Vim をいじった

はじまり

Facebook の FlowType ようの Vim プラグインは有る。
GitHub - flowtype/vim-flow: A vim plugin for Flow

諸々挙動が気に入らなくて、最初 PR しようかと思ったが、ローカルの npm のモジュールを使う Pull Request が放置されたり、PR の敷居が高そうだし、直すなら全部書き換えくらいな雰囲気なコードなので自分専用で作った。
GitHub - heavenshell/vim-flood: A simple Vim plugin for facebook flow


どうせなら、job と channel 使って非同期で色々やりたかった。
型チェックのエラー表示は QuickFix に非同期で出す、補完も非同期でやる方法を知りたかった。
# ちなみに Flow は割と速いので同期と非同期の差はほとんど無い


今現在 Flow が持つ機能全て実装してある。
TypeScript には GitHub - Quramy/tsuquyomi: A Vim plugin for TypeScript という素晴らしい IDE があるが、これには及ばない。
エディタ機能については Language Service がある TypeScript が勝ってる。

非同期の補完

同期の補完を作るのは omnifunc を使ってやればいい。簡単。
非同期の場合は、どうやったらいいのか最初分からなかったが、miyakogi さんの asyncjedi に答えがあった。

を独自の補完関数にマッピングしてやり、補完候補のリストを作り、complete() に渡せば良い。

complete() の微妙な挙動

一通り機能を実装し終わった後に実際に使っていると、補完候補が 1 件の場合の動作が混乱した。
complete() は補完候補を作るが、completeopt で menu かつ noinsert や noselect を指定している場合の挙動がわかりづらかった。


noselect や noinsert は補完候補が沢山ある場合は、候補のみを表示して、実際には補完せずユーザーに選ばせる。
menu は補完候補が 1 件の場合はそれが挿入される。
また普通の omnifunc の場合補完時はエコーエリアに何が起きているかを表示してくれるが、complete() はしない。


つまり補完候補が 1 件の場合、何も表示されず補完候補が何もないように錯覚する。
# 指摘されて気づいたが completeopt=menuone,noselect とかのように menuone の場合は起きない


なんか分かり辛いなーと思い、この挙動を自分で変えられないかと思った。

Vim のコードを読む。

とりあえず vim のコードを clone して src の下で grep する。
キーワード的に noinsert とか noselect あたりを使うと、edit.c に当たる。
edit.c を読んでいくと、set_completion という関数に当たる。


complete() 時noselect や noinsert が何をやっているかというと、補完時に completeopt にそれらがあれば、KEY_DOWN と KEY_UP のイベントを中で呼び、補完候補の選択位置を変えてる。
vim/edit.c at master · vim/vim · GitHub


そのため completeopt=menu,noselect,noinsert の場合は、補完候補の位置が隠れている状態になる。


ということは補完候補が 1 つなら completeopt=menu と同じ挙動にすれば良いと思う。
本来なら complete() 時もメッセージ出してやればいいのだろうけど、いじる箇所が多くて大変そうなので、簡単な方法から試してみる。


変更を入れる場所がわかったので、あとは補完候補の数を調べれば良い。
compl_length というそれっぽい変数があったので、これかと思い、条件文を加えて、ビルドして Vim を動かしてみたが、うまく動かなかった。


やむをえないので gdb を使う。
ついでに Mac より Linuxデバッグした方がやりやすいので、VMUbuntu を立ち上げ、Vimデバッグオプション付きでビルドする。
出来上がったバイナリを gdb 経由で起動し、set_completion にブレークポイントを張る。


簡単に再現するように以下のような Vim script をでっち上げる。

set completeopt=menu,noselect
inoremap <F5> <C-R>=ListMonths()<CR>

func! ListMonths()
  call complete(col('.'), ['January'])
  return ''
endfunc

inoremap <F6> <C-R>=ListMonths2()<CR>

func! ListMonths2()
  call complete(col('.'), ['January', 'Feb'])
  return ''
endfunc

F5 や F6 を押せば自動的に gdbブレークポイントを張ったところで止まる。
で、compl_length の変数の中身を見たが違った。
どこかで補完候補を持ってるはずと、コードを眺めていたら、引数で list_T *list という引数がある。
この中を gdb で見ると、lv_len というものを持っており、これに補完候補数が格納されている。


というわけでこれを使えば良い。
noselect_patch.diff · GitHub
できた。テストとかはまぁ後でいいやと思い、vim-jp にぶん投げて反応を見る。
補完候補が 1 つの際の noselect,noinsert の挙動 · Issue #984 · vim-jp/issues · GitHub


で、menuone でええやんと教えてもらうが、menu の時の挙動がわかりづらいことを説明して、vim_dev に仕様変更としてどうよ?と投げてもらったが、既存補完系のプラグインの動き壊すと言われた。


まぁそうだよなーもっともだし、completeopt が menu,noselect,noinsert の時のみこの変更が動くようにすればいいのかなーと思ったが、completeopt=menuone,noselect,noinsert なら困らんしとモチベーションが若干落ちたので、放置したままになってしまった。
意欲が湧けばまた取り掛かる。

結論らしきもの

Vim のコードをいじるのは今でもハードルが高いが、コードを変更して、make すればいいだけなので、ここら辺は昨今のフロントエンドの開発や Golang の開発と変わらないなーということで、みんなもっと、パッチ書けばいいと思う。