Vim で q を prefix キーにする

前置き

Vimプラグインをたくさんいれていると、それらを呼び出すためにキーマッピングを用意したくなることはよくある。

Vim はキーシーケンスに対してマッピングを割り当てられるので、何かのキーを prefix キーとして、そのキーに続けて何かしら機能を割り当てると言うテクニックがよく用いられる。

" スペースキーを prefix にする例

" スペースキー単体では何も起きないようにする
" これをしておかないと、うっかり <Space> + 割り当ててないキーを
" 押すと <Space> の元の機能が発動する
nnoremap <Space> <Nop>

" <Space>q でウィンドウを閉じる
nnoremap <silent> <Space>q :<C-u>quit<CR>

" ... 以下、<Space> + 何かにキーを割り当てられる

しかしこの方法をもってしても、割り当てたい機能はどんどん増えていくものである。prefix キーとして利用しやすいキーはいくつかあるが、それらが枯渇するほど機能を割り当てている人も世の中にはいるようだ。

q を prefix にすることで起きる問題

q は、マクロの記録を開始するコマンドである。q + レジスタのアルファベット1文字で、そのレジスタに対してマクロの記録を開始する。

しかし、26個もあるマクロを全て器用に使いこなしている人はおそらくまずいないと思う。私なんてだいたい1つしか使わない。どこに何が記録されているか、覚えられないからだ。

となると、残りの q + アルファベットは空いていることになる。q を prefix にすればこれらが使える。何ならアルファベット以外の文字も使えることになる。

nnoremap q <Nop>
nnoremap qa :<C-u>echo 'Awesome feature!'<CR>

こうすることで、qa は無事便利機能に割り当てることができる…が、こうしてしまうと、q<Nop> にすることで、マクロの開始が一切できなくなってしまう。

これはさすがにまずい。しかしこれは単に q<Nop> を割り当てなければ解決する。

nnoremap qa :<C-u>echo 'Awesome feature!'<CR>

マクロの記録も開始できる。これで何も問題はない…?

更に潜む問題

Vim のマクロの記録は q 単体で終了する。ところが、q で始まるキーマッピングがあると、押された q がマクロの終了であるのか、はたまたキーマッピングの呼び出しなのかを判断するため、Vim はしばらくの間キーマッピングの待受をする。

待受時間を待ってやり過ごすか、q で始まるキーマッピングではない何か別のキーのコマンドを実行すれば済むのだが、いかんせんやきもきしてしまう。やはり prefix 以外のキーマッピング待ちは発生させたくない。

解決策

今回、この問題を解決するために以下のように設定してみた。

nnoremap <script> <expr> q reg_recording() is# '' ? '<SID>(q)' : 'q'

nnoremap <silent> <SID>(q) q
nnoremap <silent> <SID>(q)a :<C-u>echo 'Awesome feature!'<CR>
nnoremap <silent> <SID>(q)<Space> :<C-u>quit<CR>

q を押した際に、マクロの記録中であれば即座に q を起動してマクロの記録を終了させ、そうでなければ prefix としての q を呼び出す。prefix の設定がない場合はそのまま q として機能させることで、マクロの記録開始も行えるようにする。

こうすることで、q を prefix として機能させつつマクロの記録開始/終了に影響を与えないようにすることができる。私は <Space>q をよく q<Space>誤爆することがあったので、同じ機能を割り当ててみた。

今回はなんとなく <SID> を使ってみたが、q prefix のキーマッピングを複数のスクリプトで定義したい場合は <Plug> を使っても問題ないだろう。

nmap <expr> q reg_recording() is# '' ? '<Plug>(q)' : 'q'

nnoremap <silent> <Plug>(q) q
nnoremap <silent> <Plug>(q)a :<C-u>echo 'Awesome feature!'<CR>
nnoremap <silent> <Plug>(q)<Space> :<C-u>quit<CR>

この場合は q のキーマッピングnnoremap ではなく nmap を使うことに注意する必要がある。

残る問題

ここまでやっても一応まだ問題は残ってる。ここで定義した q で始まるキーマッピングは、マクロの記録中に呼び出すことはできない。 しかしこれはある意味当然であり、マクロ記録中に呼び出したい機能を q に割り当てないようにする他はない。

VimConf 2019 が開催されて1週間が過ぎました

今年も無事 VimConf を終えることができました。私は今年もスタッフの一人として参加しました。参加してくれた皆さん、発表してくれた皆さん、スタッフ、そのほか関係者の皆さん、ありがとうございました。おつかれさまでした。

1週間経ったこともあり、感想記事が続々上がってきています。また、(遅くなりましたが)参加した皆さんにアンケートのお願いも送信し、続々と回答が集まっています。

VimConf の運営スタッフは全てボランティアです。スタッフには金銭的な報酬は一切ありません。まあこれは別に珍しいことではなく、大抵のコミュニティドリブンのカンファレンスはどこもそうだと思います。

それでいてやることは山のようになります。みんなプライベートの時間を切り崩してなんとか対応しています。みんなできる範囲でやっているので、負担が偏ったりもします。たぶん私は他のメンバーに比べると負担は軽かった方。みんなありがとう。

話が逸れた。こういった状況の中で、多くの感想記事やアンケートでの意見を拝見していると、大変だったけどやってよかった、と思えます。今のところ発見できた感想記事は全て目を通しています。ありがとうございます。

イベントの盛り上がりと継続と言う意味で、フィードバックは非常に重要です。「もうみんな書いてるし…」「イベントから時間が経ってしまったから…」そんなことはありません。今からでも遅くないです。あなたの記事を見た誰かが、次回は参加したいと思うかもしれません。

…感想を書こうと思ったら感想にすらなっていないポエムになってしまった。 ともあれ、カンファレンスは、参加してくれる皆さんがいてくれてこそ成立しています。そして参加してくれた皆さんがまた参加したいと思ってくれたのであれば、運営スタッフの一人としてこれほど嬉しいことはありません。

ライブコーディングで作ったプラグインを整理して公開した

先日、ゴリラ.vim #9 に参加してライブコーディングをしてきた。

gorillavim.connpass.com

その際に作ったプラグインを整理して最低限の形した。整理する過程で色々変わったのでその話など。

公開したプラグインは以下。

github.com

どんなプラグインなのか

これは日本語における、ひらがな⇔カタカナの変換や全角⇔半角の変換をするプラグイン

極稀に欲しくなる操作で、サクラエディタなどには標準で付いている。Vim でもやりたかったので作ってみることにした。ネタとしても小さめでライブコーディングでもなんとかなるかなという目論見。

試験的なプラグインということもあって、無駄に最新バージョンの Vim を要求する。scriptversion 4 が通らない Vim では動かない。

プラグイン

変換するという想定でライブコーディング時には convja.vim という名前で作り始めた。

しかしあとであれこれ考えて、文字種の判定や抽出もできると便利かも(やるとは言っていない)と思い、汎用的な処理ができるように jautil.vim という名前にしてみた。無駄に汎用化してしまうのは私の悪い癖である。わかってはいる…。

インターフェース

ライブコーディング時では、以下のようなインターフェースを想定していた。

echo convja#convert('123アイウ', {'target': 'hankaku', 'type': 'kana'})
# => '123アイウ'

しかし作っているうちに紆余曲折があり、もっとシンプルに指定できるようにしたいなぁという気持ちになったので以下のような形式に落ち着いた。

echo jautil#convert('123アイウ', 'hankaku:kana')
# => '123アイウ'

簡易とはいえ特殊な文法を知る必要があるが、まあ辞書の形式を知るのと大差ないだろう。

拡張

第2引数の target に配列を渡せるようにした。これにより、まず半角カナを全角カナにし、その後全角カナをひらがなにすることで全角/半角両方のカナをひらがなに直す、ということが1度の呼び出しでできる。

echo jautil#convert('アイウエオカきくけこ', ['hankaku:kana', 'katakana'])
# => 'あいうえおかきくけこ'

また、第1引数に配列を渡すことで第1引数と第2引数を入れ替えられるようにした。これにより、partial を使って特定の変換をする関数を用意することができるようになる。

let H2Z = function('jautil#convert', [['hankaku']])
echo H2Z('abc')
" => 'abc'

今後の展望

あれこれこうできたらいいなと思っていることはあるが、優先度は激低なのでやるかは不明。ライブラリだし vital module 化したいよなー。

もっとちゃんと使いたいぜって人がいたら Issue を立てたりしてください。やるかどうかはわからないですが…。

ゴリラ.vim #1 に行ってきた

ゴリラ.vim #1 に行ってきた。

この規模の Vim イベントで定員オーバーは初めて見たかもしれない。しかもまだ実績のない第1回でこれなのだから、ゴリラさんの求心力の高さが伺える。

会場は渋谷ヒカリエにある株式会社ディー・エヌ・エーさん。セキュリティゲートの受付で思わず「うほうほ」と挨拶しそうになったけど、なんとか堪えました。危なかった。

発表は初心者向けから上級者向けまでいい感じにバラけており、後になるほど高度になっていく感じで発表順序も完璧だった。満足度が高い。

dice_zu さんが私の作った showtime.vim を使ってくれてて嬉しかった。もうちょっとちゃんとメンテしないと…。

懇親会では寿司とカツサンドをつまみながらおしゃべり。学生さんが多かった印象。ちょいちょい移動しつつ話してたけども、更にもうちょっとあちこち移動して話をすれば良かったと少し後悔。

この感じのイベントを今後は毎月のペースでやる予定とのことで、今後が楽しみです。

帰り際のゴリラさんとの会話

ゴリラさん「thinca さん、次回は発表してください!」

マンボウ「そうですね。検討してみます」

ゴリラさん「検討じゃなくて、してください!」

マンボウ「あ、は、ハイ…」

ということで次回は何か発表することになった。進撃のゴリラには敵わなかったよ…。何を話そうかなぁ。

VimConf 2018 で Bram Moolenaar さんと会った話

2018年11月24日に VimConf 2018 が開催されてから、早いものでもう 2 週間経った。

2 週間経ってしまった…。2 週間…早い…。

このままだと私の VimConf が終わらないので、正直何を書けばいいのかわからないのだけど、なんか書こうと思う。

VimConf 2018 前日

今年の VimConf は色んな意味で、忘れられないものになった。

2018 年 11 月 23 日。若干の発熱により、私は寝込んでいた。翌日には VimConf が控えていると言うのに、ここに来てまさかの風邪…。 幸いにも辛さはほぼなく、微熱が主な症状。一日中寝て過ごすことになったわけだけど、この間にも様々なことを考えた。

症状がひどくなったら VimConf は欠席せざるを得ない。私は当日にするべきタスクを持っていなかった*1ので、最悪私がいなくてもイベントの開催には支障がない。とは言え半年以上かけて準備をしてきたイベント。しかも Bram さんと会えるまたとない機会。これを逃がしたら次はないだろう。Vim に携わる者として、多少無理を押してでも行きたい…。しかし Bram さんに会えたとして、私は英語が大の苦手である。言いたいことはほとんど言えないだろう。Bram さんが何を言っているのかも、聞きとれる自信がない。正直、その点で言うと会うのが怖くすらある。そしてもちろん、症状が回復したとしても病み上がりの身。周りには迷惑をかけることになるだろう。だとしても…。

VimConf 2018 当日

そうこうして迎えた 2018 年 11 月 24 日。体調は完調とまでは行かないものの、出かけられそうなくらいにはなっていた。後悔はしたくない。普段より厚着をして、あまり好きではないマスクを着け、意を決して家を出た。

予定より少し遅れて会場に着くと、すでに開場待ちの人達がちらほら。KoRoN さんに呼ばれてスタッフルームに入る。「Bram さんもう来ているよ。mattn さんと話してる」

本当にこの時が来たのだな、と思った。その後、まだ開場前のホールで、Bram さんと会った。私は名前くらいしか言えなかった。そしてたぶん、私のことなんて Bram さんは知らないだろうと思う。スタッフジャケットを着ていたので、かろうじてスタッフだということは伝わった気がする。握手してくれた。めっちゃドキドキした。

ただでさえない文章力が崩壊してきた…。えーと、ともあれ私は Bram さんと対面することに成功した。でもやっぱり何も話せず、懇親会も含めてこの日は Bram さんとはこれっきりだった。

VimConf 2018 翌日

2018 年 11 月 25 日。VimConf 2018 も終わり興奮も冷めやらぬ中、VimConf の裏イベント、vimthon が開催された。VimConf スタッフと一部の Vim 開発者、そして Bram さんを迎えてわいわい Hackathon をするイベントだ。当日の様子をまとめた Togetter がある。

f:id:thinca:20181125093709j:plain
VimConf Hackathon

体調が悪化していたらキャンセルしようかとも思っていたが、幸いにもどちらかと言うと回復してきていたので、こちらも参加できた。

全員が揃ってしばらくしたところで、全員がそれぞれ自己紹介。中には簡単なものではなく、人によっては作ったもののデモなどをガッツリ、と言う感じの濃い自己紹介もあった。

座席の配置の関係で、最後は私の番。簡単に済ませることもできたのだけど…ここは少し勇気を出して、自分で作ったプラグインをいくつか紹介した。あまり多くの種類はデモできなかったけれど、どのプラグインを見せようかと選ぼうとした際に vimrc に書かれた私のプラグインのリストが画面に映り、これは全部私が作ったものだ、という話をした。

会の終わり際、Bram さんが帰る頃合いになった。別れを惜しみつつみんなそれぞれツーショット写真を取ったりした。その際に Bram さんに、「プラグインをたくさん作ってくれてありがとう」と言われた。多分。英語のヒアリングに一切自信がないけど、たぶん言われたと思う。本当に嬉しかった。

最高の体験

いつにも増して文章力のないただの日記になってしまった…。とにかく私は最高の体験をしました。

ここのところ Vim 活がまったく捗っておらず、いかんともしがたい状況が続いているので、Vim 活がんばろう。

おしまい。

*1:私に限らず、当日にするべきタスクを極力減らす方向でスタッフ全体で準備していた

Docker Hub で Vim の最新版をタグ付きで自動ビルドするようにした話

今まで Docker Hub 上で最新の Vim のビルドが行われるように設定していたが、ふと思い立って過去のバージョンについても一通りタグに残ってると良さそうだと思って設定した際の記録。

ちなみにこの作業を開始したのは 2018 年 1 月です。どんだけかけとるんだ。

作業前の状態

Docker には Build Trigger 機能があり、URL を叩くことで任意のタイミングでビルドを走らせることができる。

この機能を使い、IFTTT の RSS Feed サービスで Vim のコミット Feed を監視して、新しいコミットがあった際にトリガーを実行し、Webhooks サービスを使って Docker の Build Trigger の URL を叩くことで、latest が常に最新の Vim のバージョンになるようにしていた。

これだと常に最新版の Vim のイメージは作られるが、過去のバージョンについては参照できない。

目標

  • Vim の公式リポジトリに新しいコミットがあったら、自動で Docker Hub 向けにビルドを行う
    • バージョンでタグを付け、過去のビルドも参照できるようにする
  • Vim 7.4 以降の過去のバージョンについてもビルドを行い、参照できるようにする。
  • Docker Hub 側で自動ビルドを行う (自前のマシンでビルドして push はしない)
  • トリガー部分も無料でがんばる
    • 個人的には自宅サーバを使う手もあるが、これは使わないでやる

Dockerfile

まずは指定のバージョンの Vim をビルドできるように ARG として VIM_VERSION を追加した

今までは常に最新の Vimソースコードを取得してビルドしていたが、引数によって特定の Vim のバージョン(Git リポジトリ上の reference)のビルドができるようになった。

また、せっかくなので各種外部インターフェースの有効無効も環境変数で設定できるようにしてみた。デフォルトは全てオフで、VIM_ENABLE_ALL を空以外にすると外部インターフェースが有効になる。

Docker Hub 側の準備

次に Docker Hub で前述のタグを利用したビルドを行えるようにする。

Docker Cloud には実際のビルド処理をスクリプトで置き換える機能があり、実はこれは Docker Hub でも使える1

autobuild のリポジトリ内の Dockerfile と同じディレクトリに hooks/ ディレクトリを作成し、その中にbuild という名前の実行可能なスクリプトを置いておけばよい。今回の場合は以下のようなスクリプトを用意した。

#!/bin/bash

# 実際にはもっと色々している
# VIM_VERSION に指定している ${SOURCE_BRANCH} については後述
docker build --build-arg "VIM_VERSION=${SOURCE_BRANCH}" --build-arg "VIM_ENABLE_ALL=" -t "${IMAGE_NAME}" .
docker build --build-arg "VIM_VERSION=${SOURCE_BRANCH}" --build-arg "VIM_ENABLE_ALL=yes" -t "${IMAGE_NAME}-full" .

通常ビルド(外部インターフェースなし)と、フルビルドをそれぞれ用意することにした。追加のイメージができるので、push 時に追加で push する必要がある。このために、動作を置き換えるのではなく追加の処理をするために post_push スクリプトを用意する。通常の push のあとに実行される。

中身は v8.0.xxxx-full のような full 付きのタグを push しているのと、現在のビルドが最新版だった場合は同じイメージを latest としても push している。

この結果、以下のようなファイル配置になった。

|- Dockerfile
`- hooks/
   |- build
   `- post_push

ビルドのトリガー方法

Vim のコミット Feed を参照して、新しいコミットがあったらビルドする点は変わらない。ただし、今までは常に最新版をビルドしていたが、今回からは指定したバージョンをビルドする必要がある。

ビルドトリガーから任意のパラメータを環境に渡す方法は見付けられなかった。そこで一般的なフローと同様に、タグが push された際にそのタグをビルドするようにした。これは先ほどのスクリプト内で参照している ${SOURCE_BRANCH} で取れる。

厄介なのは、Autobuild で連携しているリポジトリを取得する部分は変更できなさそうだということだ。どうも Docker Hub の Autobuild は Dockerfile があるリポジトリとプロダクトのリポジトリが同一である想定であるらしく、連携しているリポジトリにタグが push されるとビルドがトリガーされてそこから対象タグを clone し、Dockerfile を参照してビルドする。

つまりどういうことかと言うと、今回のようにプロダクトのリポジトリDockerfile を管理しているリポジトリが別になっている場合、Dockerfile しかないリポジトリに対して v8.0.0000 のようなバージョンのタグを無駄に push することになる。 仕方ないので今回はこの方法で行くことにした。つまり、Dockerfile を管理しているリポジトリに対して Vim のバージョンのタグを push すると、Docker Hub 上でそのバージョンの Vim のビルドが走る仕組みになる。

ビルドをトリガーする

Web hook を叩く方法から Git リポジトリにタグを作る方法になったので、GitHub API でタグを作ることにする。

これまでと同じように IFTTT でやりたいところだが、GitHub API は User-Agent の指定を必須としているところ、IFTTT は User-Agent を送らず、また HTTP リクエストヘッダを自由にカスタマイズすることができない。つまり IFTTT は今回は使えない。

そこで今回採用したのが Google Apps Script だ。採用した理由は、無料だから。

で、今回のために作ったのが以下。

https://github.com/thinca/gas-docker-vim-updater

せっかくの機会なので TypeScript に入門してみた。ので、余計に時間がかかった…。

RSS フィードを読む機能が必要だったため、今回のためにそのためのライブラリを自作した。過去に読んだフィードは Google スプレッドシートに記録し、新着を抽出する。

Google Apps Script には、定期的に関数を実行する機能があるため、これを使って RSS フィードをチェックし、更新があれば GitHub API を叩いてタグを作成するようにした。

不要になったタグを削除する

タグはビルドのトリガーにするために必要なもので、ビルドが終わったタグについては不要になる。これは消したい。

Docker Hub には、ビルドが終わって push された際にそのイベントを WebHooks で通知する機能がある。そして Google Apps Script は Web アプリとしてエントリーポイントを持つことができる。これを利用して、ビルドが終わって Docker イメージが push されたらそのイベントを Google Apps Script に通知し、そこから GitHub API を叩いてタグを消すようにした。

過去のバージョンをビルドする

最新版はビルドされるようになった。しかしこれだと過去のバージョンはビルドされない。

そこで少しずつタグを作っていく Ruby スクリプトを書いて、これを走らせた。実際には運用しながら少しずつスクリプトを修正、改善していった。

一気にタグを作らなかったのは、そうすると最新版が来たときにキューが詰まっていてなかなかビルドされなくなるからである。実際に全てビルドし終えるのに1ヶ月以上かかった。また、キューを大量に積むとなんとなくまずそうな気がしたというのもある。

中にはそもそもビルドが通らないバージョンが存在したり、Docker Hub が謎のエラーでビルドを失敗させたりしてかなりカオスだった。

課題 - 失敗したビルドに対応する

過去のバージョンをビルドしていてわかったことだが、前項で少し触れたように、ビルドできるはずのバージョンがビルドに失敗することは割とある。例えば apk でパッケージの更新に失敗すると言ったわかりやすいネットワーク由来のものから、コンテナの構築になぜか失敗するというよくわからないものまで様々な理由で失敗する場合がある。これらの失敗は多くの場合、再ビルドを実行すれば問題なくビルドが通る。

さて、これに対応したいところなのだが、難しいことに、Vim はビルドできないバージョンがリリースされることがありえる。これは今でも Bram さんの手による暖かみのあるパッチリリースが行われている賜物である。

そう、つまりこれだと、Docker Hub 由来でたまたま失敗したのか、そもそもビルドが通らないのかがわからない。従って、失敗していたら再実行するというソリューションは使うのが難しい。やるとしても回数制限してやる感じになりそう。

というわけでつらいので今のところやってない。せめて Vim のリリースが全てビルドが通ることが保証されてればなー2

やってみての感想

結果的にかなりあれこれすることになって大仕事になった割にどれだけ活用できるか不明だけど、あれこれ触れたのはよかった。しかし、時間をかけすぎた感がやばい…。

CI とかバグ調査等に使える可能性がなきにしもあらずなので使いたい方はどんどん使ってください。


  1. Docker Cloud と Docker Hub の違いをよくわかってない…。

  2. とは言えコンパイラのバージョンとか外部インターフェースのバージョンの依存で落ちる場合までは保証できないだろうしどちらにしてもつらそう。

Vim の問題を調査したときの記録

先日私の環境で起きた Vim に関する問題を調査した際の記録。一例なので汎用的に使える手法ではないけど、こういう感じのことをしているよというのを書き留めておきます。

結果的に無駄だった工程も書いています。1 本道で調査が進むことの方が珍しく、だいたい回り道します。

問題

cgn で検索のマッチ対象を別のキーワードに置き換えたあと . で繰り返すと、Bell が発生する。」

cgnc + gn で、gn は検索にマッチした範囲です。つまり検索にマッチした内容を c で書き換え、. で次のマッチ対象にも同様の変更を行います。1 つずつ確認しながら置換したい場合などによく使われる操作です。

問題について

Vim では不正な操作をした場合など様々な場面で警告としてベルを鳴らします。 例えばノーマルモードでそれ以上移動できない方向に移動しようとした場合など、割と些細なことでも鳴ったりします。

人によっては煩わしくて鳴らないように設定している人もいると思います。プラグインの利用者であればそれも良いかと思います。無効にしてしまえば今回の問題は解決します。

しかし、ベルを無効にすることはコンパイラの警告を全て無視しているようなものです。私は Vim プラグインの開発をしているので、些細な問題にも気付けるようにベルは有効にしています。

調査

ベルの厄介なところは、「何か問題が起きた」ということしかわからないところです。メッセージすらない…ハードモードです。

何由来のベルか調べてみる

ベルは 'belloff' オプションを使うことで種類ごとに抑制できます。これを使えば何の操作でのベルか絞れるかも…? と思って設定してみることに。

たぶんこれかな…と思い最初に error を設定してみると、見事エラーは抑制されました。 error は「その他のエラー」。何もわからないということがわかった…1。結果的にこの調査は完全に無意味でした。そういうこともある。

キーマッピングを確認する

. でエラーが起きるので、. に何か機能が割り当てられていないか確認。repeat.vim をインストールしていたのでこの辺りかもしれないと睨みました。 しかし実際に nmap . で設定を調べてみると、何も割り当てられておらず。repeat.vim にはいくつか派生バージョンがありますが、私が使っているものは repeat#set() が呼ばれるまでキーマッピングの設定をしないようです。

またもあてが外れましたが、徐々に絞れてきています。

autocmd を確認する

. がデフォルトのままだとすると、そこでエラーが起きるとすれば autocmd の発動によるものである可能性が高いです。 . で起きるイベントとなると…たぶん TextChanged かな? ということで TextChanged について調べることに。

:autocmd TextChanged
--- Auto-Commands ---
ALERunOnTextChangedGroup  TextChanged
    *         call ale#Queue(g:ale_lint_delay)
anzu  TextChanged
    *         call anzu#clear_search_cache()
vimconsole  TextChanged
    *         :call <sid>text_changed()
plugin-wtrans  TextChanged
    wtrans://source/*
              call wtrans#buffer#translate(expand('<amatch>'))
neosnippet  TextChanged
    *         call neosnippet#handlers#_restore_unnamed_register()
plugin-unite  TextChanged
    <buffer=2>
              call unite#handlers#_on_text_changed()

内容は記事執筆時点で再現のために出したものなので調査時とは少し違うかもしれません。 ともあれこの中に原因があると踏んで、1 つずつイベントを削除して、問題が再現するか確認します。ただし plugin-wtransplugin-unite はパターン的に発動しないことが明確なので最初から除外可能です。

:autocmd! ALERunOnTextChangedGroup TextChanged

1 つずつ消して、問題が再現するか試していきます。しかし全部消してもエラーは出続けました。むむむ。

実際に起きている autocmd を確認する

ざっくり雑に TextChanged かな、と疑ってみましたが、そもそもこの予想が外れていそうです。実際に起きているイベントを確認してみることに。 こういう場合は 'verbosefile' を使うと便利です。

:set verbosefile=/home/thinca/log.txt
:8verbose normal .
:set verbosefile=

起きたイベントがファイルに記録されます。ログレベル2をもっと上げれば、実行された全てのスクリプトの行が記録されるので、それを使うこともありますが…今回はベル。ベルは記録されていないので、全てのログを出してもどこで問題が起きたかはわかりません。そこで今回はイベントだけ出します。

と言うわけで実行してみた結果(一部抜粋):

InsertEnter Auto commands for "*" を実行しています
InsertLeave Auto commands for "*" を実行しています

InsertEnterInsertLeave でした!そもそも TextChanged は実行されてなかった。

考えてみれば cgn は一旦挿入モードに入ってテキストを変更するので、. でリピートしたとしてもそこは変わらないわけですね。

というわけで今度こそ。

InsertEnter のイベントを確認する

先ほどやったように 1 つずつイベントを消して、再現するかどうか確認します。すると、以下の箇所で問題が発生していることがわかりました。

neocomplete  InsertEnter
    *         call neocomplete#handler#_do_auto_complete('InsertEnter')

ここまでわかればあとは該当コードを追っていけば良さそうです。

該当コードを追ってみる

この先はおまけ。該当コードは以下のようなもの。

function! neocomplete#handler#_do_auto_complete(event) abort "{{{
  if s:check_in_do_auto_complete(a:event)
    return
  endif

  if g:neocomplete#auto_complete_delay > 0 && has('timers')
        \ && (!has('gui_macvim') || has('patch-8.0.95'))
    if exists('s:timer')
      call timer_stop(s:timer.id)
    endif
    if a:event !=# 'Manual'
      let s:timer = { 'event': a:event }
      let s:timer.id = timer_start(
            \ g:neocomplete#auto_complete_delay,
            \ function('s:complete_delay'))
      return
    endif
  endif

  return s:do_auto_complete(a:event)
endfunction"}}}

+timer が利用可能な場合に少し待ったのちに s:complete_delay() を呼び出しています。s:complete_delay() 内では、補完処理を行っています。

ここで実際に起きている処理を思い出して見ます。cgn. で繰り返すと、一瞬挿入モードに入ってすぐに抜けるのでした。つまり s:complete_delay() が呼び出される頃にはすでに挿入モードを抜けています。今回の問題は、ノーマルモードで補完処理を行おうとして起きていたものでした。

ちなみに、この問題はすでに修正してもらえました。Shougo さん、ありがとうございます!

まとめ

Vim で問題が起きた際の調査の一例でした。

今回の例は再現率が 100 % だったのでなんとかなりました。トリガーがわからないとその分難しくなります。

他に汎用的な手法として、プラグインを少しずつ無効にしていってどのプラグインが原因か調べる方法もあります。プラグイン数が多いと大変ですが、確実性は高いです。問題を起こしているプラグインさえ特定できれば、最悪原因がわからなくても最小構成での再現を作ることでバグレポートを出すこともできます。

バグ調査は地道な作業ですが、快適な編集環境のためにできることからやっていきましょう。


  1. 正確にはその他のエラー以外のエラーではないことがわかった。

  2. 例で設定しているのは8。詳細は :help 'verbose'