Vim初心者に贈る、Vimの各種モードを完全に理解するとっておきの方法

つい先日, 2018-11-24 (土) に開催された VimConf 2018にて、弊Quipper社の ujihisa さんが登壇しVimの技術発表を行いました。本記事ではその発表の解説と、その裏話をめいっぱい記します。

発表タイトルは "Modes" という一単語のみで、公式サイトに載っているtalk abstractによると以下のようなものです。

Abstract

Discover what is happening internally when you switch modes, such as insert mode, normal mode, and operator-pending mode. This talk first revisits how modes are when you use Vim, then I'll let you dive into the Vim C implementation to see what's exactly happening there. I'll even show what modifications can be done when you make change, with using existing pull requests. This talk is both for Vim beginners and advanced users who don't always look at the Vim C implementation.

"Modes"

https://speakerdeck.com/ujihisa/vimconf-2018

発表動画: https://www.youtube.com/watch?v=3uUYf7eiI8Q

発表内容三行要約

  • Vimのmode (insert modeとかnormal modeとか) を横断的に理解しよう、とくにmodeのswitching
  • vimtutorや:hでどのように書かれているか確認し、実際に動きを見る
  • gdb + termdebugで、上記mode switchingまでのすべてとmode switching自体の挙動を実装とともに見る

内容のメインテーマは、Vimの基本中の基本である各種モードの理解なのですが、むしろこのメインテーマという骨格を元にして、旨味のある肉をいっぱいくっつけて提供するというスタイルの発表を意識的に行いました。骨格部分であるVimのモードについてはujihisaさんでなく誰でも調べれば簡単に話せる内容ですが、そのための具体的方法や周辺的な詳細事項などである肉の部分はujihisaさんでなければできないような内容を重点的に盛り込みました。なおこの肉部分はスライドではなく口頭での語りや、Vim側でのデモをメインに行いました。(なのでスライドだけみてると話が飛躍しまくっているように見えるかもしれません。)

解説

こちら、僕が使っているキーボードです。

このiキーを打鍵すると、

  • Normal modeならInsert modeに移る
  • Insert modeまたはCommand-line modeなら文字iをバッファなどに入力する
  • Operator-pending modeやVisual modeなどならtext-objectのiwなどの一部と認識される
  • などなど

現在のモードによって違う挙動をとります。

モードによって同じキー入力に対してVimは違う挙動をとる。オブジェクト指向プログラミング言語におけるmethod dispatchingみたいに、あるキー入力とある挙動との割当をVimはどのように行っているのか。そういった "モード" について、探求していこう。

Vimのモードについてのみストイックに追求していく発表です。発展的話題や新規性のある内容を披露するような発表ではありません。

まずVimのモードを理解するために仕様・ドキュメントを確認し、その上でデモって動きを見せます。このとき、Vimの内部の情報も見せたいので、GDBやtermdebug.vimというツールも使用しちゃいましょう。

この発表の対象視聴者層は、僕のようなVim初心者です。僕はVim歴20年程度でいまだにこの壮大なVim世界の一部しか知らないVim初心者で、この発表は同様にVim初心者に向けたものです。本ブログ記事のタイトルにもそのように示しているとおり、この姿勢は一貫しています。

この発表でみんなのVim理解が盤石なものになり、かつ、Vim本体への開発がしたくてたまらなくなるみたいなそんな感じのあれを発表のゴールとしています。

じゃ、いきましょう。まずは仕様の確認から。

みんなvimtutorは使ったことありますよね? もしなかったらVim人生の9割ぐらい損しているので今すぐやりましょう。25~30分くらいでできます(※今すぐといったけどVimConf 2018の最中にやったら貴重な発表聞き逃すのでオススメしないよ! ブログ記事読んでる方なら今すぐやっても安心ですね)。 これvimtutorは最初から説明文がいろいろ入った特殊なVimが立ち上がるというもので、書いてるとおりに指を動かしていくとVimの基本中の基本を完全に理解できるという便利なツールです。 Vimをインストールしたら最初から入ってるので、いますぐターミナルを開いてvimtutorと入力して起動してみましょう。

英語に自信がなかったらコマンドラインオプションで得意な言語(例: ja)を指定すると便利できます。

余談ながらこの日本語版vimtutorを書いたのは本日最初の基調講演を行ったmattnさんで、その監訳はこのVimConfの主催者の一人であるKoRoNさんです。

んでそのvimtutorなんですが、1.0から1.3ぐらいまでのセクションではカーソル移動やファイルを閉じたりといった編集操作の説明で、1.4がついに文字入力を解説します。ここでユーザはiを入力して生まれて初めてInsert modeの世界に突入するわけです。 その次に説明するのはAですね。

ちょっとここで実際にiとAをvimtutorの中でお見せしましょう。

カーソルがここのときにiを打鍵すると、

こんな風にカーソル位置左側を起点とするInsert modeに入り、

Insert modeなのであとはなにか入力するとそのままそこに文字として入っていきます。

一旦 <esc>でNormal modeに戻り、次はAしてみました。カーソルが末尾に行っていますね。Aはカーソルのある行の末尾からInsert modeをはじめるコマンドです。

vimtutorでも触れられているけれど、Vimの最重要機能の一つとして、 :help があります。Vimに組み込まれたVim公式ドキュメントですね。 :helpVimの中で入力すると発動します。でも長いので略称の :h というのもあって、そっちでもいいです。 引数をつけるとそのトピックの項目を開いてくれますが、とりあえず引数なしで起動しましょう。そうすると help.txt というヘルプ自体のヘルプやVimの概要いろいろなトピックへのリンクなどがあって、横断的に勉強したいときに便利です。

リンクをたどっていくと、modeの一覧などが見れます

Insert modeとかNormal modeとかが見えますね。

ほかにも、リンクをたどっていくと、mode-switchingという項目にたどり着きます。

ここにこんな便利な表があります。

左に縦に並んでるのが今のモードで、そのモードから表にあるどのキーを入力すると、右に横に並んでる新モードに突入できる、みたいな意味です。 たとえばNormalからVisualに行きたいなら、v, V, <C-v> の3キーが使えるということが分かりますね。

ちょっと飛ばして、NormalからInsertのやつを見ましょう。 *1 のとこですね。入りきらなくて脚注になってます。

*1 Go from Normal mode to Insert mode by giving the command "i", "I", "a", "A", "o", "O", "c", "C", "s" or S".

結構ありますね。

  • i (前述) カーソル左側からInsert modeに
  • I カーソル行先頭に移動後Insert modeに
  • a カーソル右側からInsert modeに
  • A (前述) カーソル行末尾に移動後Insert modeに
  • o カーソル次の行を作成後そこからInsert modeに
  • O カーソル前の行を作成後そこからInsert modeに
  • c (今回の解説範囲外: motionを受け取り、motion部分を削除後Insert modeに)
  • C カーソル位置から行末まで削除後Insert modeに
  • s (今回の解説範囲外: clと同じ意味Insert modeに)
  • S (今回の解説範囲外: カーソル行全部消してからInsert modeに)

(実はもっとあるということをあとで話します)

他にもいろいろ解説がここに掲載されていますが今回の発表からは割愛します。読めば分かる内容ですので。 スクショでは英語ですがVimのヘルプはすべて多言語翻訳されており、日本語版は素晴らしい出来のものをvim-jpの多数のメンテナによって多大な努力でもってメンテされています。

あとこれがVimのモード全一覧です。僕は初心者なので知らないやつも結構あります。

左の7つがBASIC modesと呼ばれるもので、右の7つがAdditional modesと呼ばれてます。とりあえず右のものの中ではoperator-pending modeが群を抜いて重要なモードですね。

今回の発表では各モードについての詳細の説明は避けます。(後述しますが、一旦Normal modeとInsert modeとその間のswitchingのみに着目していきましょう)

ここまでの要約です。こんな内容の話をしてきました。

vim map

Vimの知識体系はかなり多岐に渡ります。これは発表前日夜に雑に描いたものなんで、上がユーザから直接見える、各機能の使い勝手というか仕様というかそのあたりの層で、真ん中がそれぞれに対応する実装、そして下が実際のVim開発自体の知識、みたいな感じです。基本的に上から下に行くほど知識が深くなり、左右に広がるほど知識が広がる的なイメージです。

今回の発表は、この知識マップでいうと

こんな風に細長い感じのを意識してます。

とりあえずこの仕様の部分に関しては終わりましたね。 次はいよいよ実装を見ていきましょう。

understanding implementation

Vimソースコードhttps://github.com/vim/vim にあります。 VimコアのCのファイルはすべてsrcディレクトリ以下にあるので、基本的にVim本体のコアの実装を知りたければこの src/**/*.c を見ていけばいいということがわかります。

ところで唐突だけどまだ自己紹介をしてなかったのでここで自己紹介します。こんにちは、ujihisaです。 なんでこのタイミングやねんと思うかもしれないけれどこの後の発表の前提を共有する必要があるのでここでやります。

一番下に書いてあるけど, 5年前の2013年にVimConf作った人です。スタッフ業もしてます。ここ、一行であっさり書いてますけど、スタッフ業というのは、たとえば英語でBramさんにemailを書いて推敲して推敲して勇気を振り絞って送信してそのあと半年くらいやりとりしてついに今回呼んで今日Bramさんと初対面したり、またそれができるためVimConfのスポンサーを求めて各所に走り回り、vimconf webサイトを作り、その他膨大量の仕事をしてきた、という意味です。VimConfは8人のこのコアスタッフ (+ 2人の当日スタッフ) で作った、俺が作った、ということをひっそりと声を大にして主張します!

(vimconf業の内容については、スタッフとして今年から一緒に戦ってきたaomoriringoさんの記事が分かりやすいです。)

で、僕はVim暦20年くらいで、まだ初心者です。たとえば先程の発表 (注: VimConf 2018 ujihisa発表前のdaisuzuさんの発表)では、Vimプラギンの使用を減らして、Vim本体機能をもっとよく理解して使っていこうというものでしたが、僕はVimプラギンを大量に雑にばんばん使いまくってます。これが僕の使ってるプラギン一覧ですね。特にこの太字のやつらは、ないと僕が死んでしまうレベルの高頻度で使っているやつです。

(ちなみに知ってる人は分かると思うけどこのプラギンリスト、deprecatedなものがたくさんあります...)

Vimプラギンの指定含め、僕のvimrcはここで公開しています。全然整理されてないダメvimrcです。とりあえずこれのあるVimを使って、僕の普段のVim捌きはこんな感じです (デモ)

(普段どうVimを使ってるかをまず先に見せることで、後述するVimからgdbとTermdebugを用いたVim自体の内部を追うというかなり特殊な行為の前に、まずVimConfでライブで人がVimを使ってる様子に慣れてもらうというのが主目的。また副目的として、よりちゃんとした自己紹介を行うという意図もあり。自己紹介、どんな人かを箇条書きしてもよくわからないですよね。自分の前でVimを操作してもらうのが一番てっとり早い自己紹介というのは誰も異論ないと思います。Vimをどのように使うかの個人差は凄まじいですからね、その人の哲学・慣習などすべてが顕れます。)

vim起動直後

僕はターミナルのVimでなくgVimを使っています。このvimrcがある状態でgVimを起動すると、まずこのようにいきなりvimshellが立ち上がるよう設定してます。

set filetype=ruby

なんか簡単なコードを書いてみますか。こういうときはよく :new して空のバッファを作り、 :set filetype=ruby みたいにして書捨てスクリプトを書くやつのファイルタイプを指定します。僕は一番rubyに慣れているRubyistなので、 :new からの :set filetype=ruby は一日30回くらいやります (なのでそれ用のkey mappingをいちいち用意してます)

quickrun

とりあえずめちゃくちゃ簡単なコードを書いて quickrun.vimで実行してみました。下に出てるやつがプログラム出力ですね。

Rubyだけでなく他の言語、例えばCでもなんか書いてみましょう。:new して :set filetype=c して空のCのバッファを作り、neosnippetでCのhello worldを一瞬で入力し、それをquickrunしてみます。

c

こんな感じになりました。僕は普段Cをめったに書かないけどまあこんな風にVimを使えますよというデモです。

(ここでCのものすごく簡単なmain()を見せることで、後述するgdbvimを起動したときに一番最初にでてくるmain()を自明なものとするのが裏の目的)

working at quipper

さっきRubyのコード実行例を見せたことからわかるように、Rubyに詳しい人です。現職でもRubyというかRuby on Railsで仕事をしています。

んでまあここで強調しているように、基本的に僕は低レイヤ技術をよくわかっていないスクリプト使いです。ScalaHaskellコンパイルする言語とはいえ、例えばghcコンパイルしたHaskellのコードのバイナリを解析したりとかは日常的に行ったりしていません。RubyScalaででかいWebシステムのあれをあれしたり、Microservices作ったり運用したりといったことは経験があります。

ここでちょっと余談なんですが、実際のところこのタイミングが唯一無二のチャンスなので、ちょっと強調して主張したいことを述べます。

過去20年くらいのプログラミング経験のうち多くの時間をVimとともに過ごしており、これまでに書いたコードのほとんどすべてはVimで書かれています。いまプログラマとしてRailsなどでお金を稼いで生活しています。僕の友達の多くはプログラマで、そのうちの多くをVimコミュニティの人が占めています。ここにいる僕はVimから多大なる恩恵を受けている者なんだ、ということです。これまでVimプラギン開発したりいろいろな勉強会を主催したり発表したり技術記事をいくつか執筆したりと、Vimにおける開発やコミュニティ活動や教育への貢献をやってきたけど、受けた恩恵を考えると当たり前でまだまだ不足してます。今日この場には開発者や一般にコミュニティに貢献している人に、あらためて感謝を申し上げたいです!

閑話休題。さきほど言ったように、僕は Ruby自体のCで書かれた処理系の実装や、Vim自体のCで書かれた処理系の実装はほんのちょっとしか知りません。少なくとも日常的にいじっているわけではないです。 Cなど低レイヤ技術に詳しい人は様々なノウハウを持っていると思いますが、僕はよく知りません。どのようなアプローチでVimの処理系内部を見ていけばよいのでしょうか?

先程VimコアのすべてのC実装がどこにあるかを説明しました。それらを先頭からゆっくり見ていくという手もあります。 また、どれか関数または機能を一つ選び、それをもとにその周辺を注意深く読むという手もあります。 いわゆる静的解析ですね。

僕はあまりその分野に詳しくないので、静的に見ても挫折するのが目に見えています。ちょっと気合を入れて動的に解析できるよう準備し、デバッガを使って動的に挙動を見ていきましょう!

はい、前半の発表あと、右上のujihisaさんのとこに余談でジャンプしてて、いま本題後半に入ったとこです。いまここですね。

GDBです。これはデバッガで、今回これを単に「プログラムの中に入り込んで」Cのコードを表示させて一行ずつ実行する便利ツールとして使っていきます。

GDBを使うときは対象となるプログラムを特殊なフラグを立ててコンパイルする必要があるので、さっそくやっていきましょう。

具体的には上に書いてる CFLAGSSTRIP 環境変数をセットした上でmake経由でgccなどを実行すればOKです。 Vimの場合、こういうのの設定は src/Makefile を直接編集せよとのことなので、そのようにします。

(なお、このあたりは過去にQuipperのブログで執筆しました。お時間あるときに参照してみると便利です: "VimのデバグにGDBを使う")

手元のsrc/Makefileのgit diffはこんな感じです。一番強いらしいデバッグフラグの -ggdb3 ついてます。あとついでに PREFIX も設定して、 make install でデプロイされるやつらのpathを /usr/local/bin みたいなsudoがいるとこではなくホームディレクトリ以下にやってます。これでmake installにsudoがいらなくて便利です。

こんな風に、生成されたバイナリが with debug_infonot stripped なら自動優勝です。

んで、これでもう make -C src all install のようにするとgdb利用可能なvimが用意できるんですが、これ実際にやると結構不便です。

gdb -tui ~/git/vim/local/bin/vim みたいにしてターミナル上のgdbの中でvimが立ち上がるんですが、vim自体がインタラクティブなツールで、ターミナルの1つの画面につねにvimgdbが同時に出力され、vimgdbが同時に入力待ちになり、カオスになります。よって、なんらかのツールを使って、gdbのUIとvimのUIを分離する必要があります。

:Termdebug

それをやってくれる超便利Vimプラギンがあり、それがtermdebug.vimです。作者はBramさん本人で、これはVim本体にすでに組み込まれてます。 Vim:terminal が入った去年ぐらいに導入されたもので、Bramさんの過去の発表動画でもtermdebugが紹介されていますね。

termdebugでvimを起動するとこんな感じのUIになります。この画面全体はVimで、左は実行中のVimの実装で、右下がGDBの操作用のターミナルで、右上がデバグ対象の別プロセスのVimです。この右上と右下どちらもvim 8の:terminalを使ってる感じです。これらをいい感じに統合してるのがtermdebug.vimです。

ではがんがんやっていきましょう。いきますよ。

まずvimを起動するtermdebug起動しましょう。

次にgdbstart を使ってそのvimを起動。

(編集注: ここから膨大量の濃密度デモがあるのですが、紙面の都合上、ぜひともこの記事ではなく前述の動画にてライブ感をもってお楽しみください! CTRL-Oの説明やrestart_edit グローバル変数の説明などが含まれます)

というわけで実装の中身を見るとこが終わりました。

short summary

さっきはこんなことを見ていきましたね。

もう時間がないのでさらりとだけ。コア開発への貢献について。

方法はいろいろあるのですが、基本的には https://github.com/vim/vim のissueみてpull request投げる感じです。

本家だけでなく、最初のmattnさんの基調講演で言及されていたように、vim-jpによる日本語でのvimのissue管理もあります。 こっちだとvim-jpの猛者どもによる便利な事前レビューも受けることができます。実際に僕はものすごく便利いただいて最高でした。

https://github.com/vim-jp/issues/issues

github

上のこれは実際に僕が出したpull requestの一つですね。 前述のCTRL-Oによる、Insert modeからの一時的なNormal modeに入るいわゆる restart_edit に値がセットされているという情報を、Vimプラギン側に可視化すると便利なシーンがあるため、そうするよう mode() というVim script関数を書き換えたというものです。これはマージされ、すでに最新のVimには含まれております。

diff

diffはそのまんまな感じです。これにテストコードの変更もあります。テストコードはもちろんVim scriptで書きます。雰囲気ですらすら書けると思います。すらすらとじゃなかったですが。

done

というわけで、このような内容の発表でした! 気持ちが入ってしまって、たくさん書いてしまった・・・。

発表の準備と発表練習

Quipper社の業務として勤務時間中に発表の準備をしました。基本的に、構成・話すこと・話のトーンなどの練習が9割で、スライド作成が最後のおまけの1割という認識でいつも準備してるんですが、今回はこのスライド作成に至るまでものすごい時間がかかりました。長い戦いでした…。練習は何回やったか覚えてないです。当日は全部忘れました。でも練習したおかげで体が勝手に動いていい感じにあれできました。

発表直後に、Bramさんが席を立って僕の方に来て, This is the only and the first presentation that showed Vim source code at a tech conference! といった内容のことを満面の笑顔で情熱的に伝えてくれたとき、すべてが報われた気がしました。

VimConf 2018をQuipperがスポンサーするとは

VimConf 2018のスポンサープランはこんな感じになっています。 https://vimconf.wordpress.com/2018/06/29/looking-for-companies-sponsoring-vimconf-2018/

https://vimconf.org/2018/#link-sponsors に掲載されているように、QuipperはSilver planを選択しました。

f:id:ujihisa:20181208171223j:plain

上記のujihisaさんの発表の中でもちらりと出てきていましたが、このようなチラシを社内のプロの力によって開発しました。 Silver planはノベルティを配布する権利があり、このノベリティ開発は意識的に優先度を高めました。

チラシを作るにあたって、初期に以下の方針を明確に定めました。

  • 確実に「見てもらえる」ものにする。伝えたいメッセージを絞る
  • Quipper社がどんな技術を使ってどんな製品を作っているかをわかりやすく説明する
  • Quipper社にしか作れないものにする。独自性と必然性

表面はソフトウェアエンジニアなどの採用にあたる技術的側面にのみ着目し、裏面はそれを用いて何をやって誰にどんな価値を届けているかという点のみに着目した作りとしています。 VimConf 2018に参加した方、もしまだそのチラシがあれば、よかったら再び手にとって見てみてくださると便利です。VimConf 2018のためだけに僕たちが知恵を絞って作ったプロダクトです。

文責: ujihisa