この広告は、90日以上更新していないブログに表示しています。
この記事はVim Advent Calendar 2015 の20日目の記事です.
まずはこちらのスクリーンショットをご覧ください.



エディタの UI やカーソル移動はVim っぽいですが,markdown ライブプレビューやカーソル位置での画像ポップアップ,組み込みブラウザなど謎の UI が見て取れます.本記事ではこれについてボトムアップで必要な知識から順を追って紹介します.
<canvas> と Node.jsAPI で Neovim フロントエンドを作成しHTML 標準としてWeb Components という新機能が策定されています.Custom Element, Template, Shadow DOM, HTML import といくつかの機能に分かれていますが,<div></div> といった慣れ親しんだ要素に加え,<foo-bar></foo-bar> といったカスタム要素をつくることができるようになります.CSS は本来グローバルですが,カスタム要素内で定義されているCSS はコンポーネント内のみに適用され外に染み出しません.また,従来の DOM 要素のようにプロパティや対応する class にメソッドを生やしたりしてカスタマイズ可能にしたりできます.具体的なカスタム要素のつくり方やもっと正しい説明が知りたい方は下記の html5rocks の記事などが役に立ちます.
Custom Elements; HTML に新しい要素を定義する
まだ仕様が fix されていない機能ですが,Google のPolymer など Web Component をラップして使いやすくしたライブラリがすでにいくつかあり,再利用可能なコンポーネントが提供されています.
Neovim にはmsgpack-rpc で呼び出せるAPI があり,プラグインを別プロセスで動かして Neovim 本体と通信する remote plugin などで使われています.ウィンドウ・タブ・バッファの情報を収集したり Neovim に指定文字列を入力したり,コマンドを実行したりVim script を eval したり色々できますが,その中に1つvim_ui_attach という特別なものがあります.
vim_ui_attach を呼ぶと,それ以降 Neovim 側から呼び出し元側のプロセスに UI の描画イベントが通知されるようになります.それぞれの通知はイベント名とその引数を持っています.
| イベント名 | 説明 |
|---|---|
put | 引数で渡されたテキストをカーソル位置以降に描画する |
cursor_goto | 引数で指定された (line, col) にカーソルを移動する |
highlight_set | 描画する文字や背景の色をセットする |
clear | 画面全体をクリアする |
eol_clear | カーソル位置から行末までクリアする |
scroll | 引数で与えられた行数だけ縦にスクロールする |
set_scroll_region | スクロールする範囲をセットする |
resize | 画面のサイズ(行数,桁数)を変更する |
update_fg | foreground color を引数で与えられた色にセットする |
update_bg | background color を引数で与えられた色にセットする |
mode_change | 現在のモードを引数で指定されたモードに変更する(normal, insert など) |
busy_start | 入力を受け付けない状態を開始する |
busy_stop | 入力を受け付けない状態を終了する |
mouse_on | マウスを有効にする |
mouse_off | マウスを無効にする |
bell | ベル音を鳴らす |
visual_bell | ビジュアルベルを表示する |
set_title | ウィンドウタイトルを引数で指定された文字列にセットする |
set_icon | ウィンドウアイコンを引数で指定されたパスの画像にセットする |
見ての通り,かなりステートフルな通知が飛んでくるので,カーソル位置などの状態を保持しつつ来たイベントを順番に処理して描画していけば Neovim の画面を描画することができます.
Neovim ではnvim --embed で起動することでヘッドレスで実行し,エディタの描画を別の UI プロセスに任せるフロントエンド-バックエンド型のアーキテクチャをサポートしていて,すでにneovim-qt やneovim-dot-app などの Neovim フロントエンドが存在します.
<canvas> に描画して WebComponent としてラップする上記で説明した msgpack-rpc を介した UI 描画イベント通知のAPI を使って HTML の<canvas> 上に Neovim のフロントエンド部分を描画し,それを Web Component としてラップしてみました.
HTML 上に描画するという選択をしたのは,近年NW.js やElectron といったデスクトップアプリフレームワークが出てきていて,容易に Neovim エディタをこれらで作成したアプリに組み込めるようになるからです.また,WebSocket などで通信して描画情報を受け取ればリモートにある Neovim を手元のブラウザに描画して処理するなども可能です.
今回はローカルで実行できるようにしたいので,外部プロセスとして Neovim を実行し標準入出力で通信します.ブラウザ内では外部プロセスを起こせないので,Node.js のchild_process モジュールで子プロセスを管理します.このことから分かる通り,今回のアプローチでは一般的なウェブサービスに組み込むことはできませんが,Electron アプリなどの Node.js が統合された環境では利用できます.
ユーザからの入力や Neovim プロセスからの通知などのデータフローをうまく扱いつつ,カーソル位置などの状態を適切に更新するためにFlux というアーキテクチャを使います.

(https://facebook.github.io/flux/docs/overview.html#content より引用)
ユーザからの入力や Neovim プロセスからのイベント通知といった状態を変更する処理を Action として定義し,ディスパッチャを通してのみアクションを発行できるようにします.発行されたアクションは store を変更し,store を listen していた view が store の状態変更を受けて描画処理をします.これによってデータの流れる方向を単方向にします.つまり,GUI ではよくある publisher / subscriber な実装パターンです.

今回はこんな感じになりました.緑色の部分がJavaScript(実際に書いたのは TypeScript ですが)で書かれていて,view を描画するScreen,Neovim プロセスとやりとりするProcessHandler(ここは双方向にならざるを得ない),処理に対応するAction 群,状態を一括管理するStore のそれぞれのクラスで構成されています.ユーザからの入力は Action として発行され,Store を通じてそれを subscribe している ProcessHandler に送られて最終的に Neovim プロセスに届きます.また逆に Neovim プロセスからの通知は ProcessHandler が一旦受けた後アクションとして発行され,Store を通じてそれを subscribe している Screen が描画情報を反映します.
Flux は React.js と一緒に語られることが多いですが,特にそういった制約は無く,今回はScreen は<canvas> にfillRect() やfillText() といったcanvasAPI で描画するだけのオブジェクトです.また,Store は単一のデカイEventEmitter として実装されています.最終的に上記のクラスをすべて持ったeditor オブジェクトのプロパティに上記クラスのオブジェクトを全て持って,editor オブジェクトを通してすべてのオブジェクトにアクセスできるようにしておきます.少し一般的な flux なウェブアプリと違うのは,store がコンポーネントローカルなところでしょうか.これはコンポーネントが複数設置される可能性などを考えてこうなっています.
今回はこの Neovim フロントエンド実装を Web Component として定義したいのでneovim-editor という Polymer element を作成 します.コンポーネントのカスタマイズ(e.g. Neovim に渡す引数)はコンポーネントのプロパティとして設定できるようにします.上記のeditor オブジェクトをコンポーネントのプロパティとして持つことで,<neovim-component> 要素を通じてScreen,ProcessHandler,Store といったすべての情報にJavaScript 経由でアクセスできます.
なお,試していませんが,おそらくAtom editor や VisualStudio Code といった Electron 上につくられたエディタのプラグインとしても使えるのではないかと思います(エディタの中にエディタを組込むのがどれくらい嬉しいかは別問題ですが…)
というわけで本記事冒頭でお見せした3枚のスクリーンショットは全て<neovim-component> に別のコンポーネントを組み合わせた例でした.neovim-component リポジトリ ではいくつかの例を Electron アプリとして公開しています.
https://github.com/rhysd/neovim-component/tree/master/example
各 example のディレクトリ内の README に従えば実行できるはずです.各 example は 100〜300行程度で書かれています.
例えばNeovim と markdown プレビューを合体させた markdown エディタの例 ではmarked を使ってつくった<markdown-viewer>コンポーネント(markdown テキストをセットすると HTML で描画してくれる)を下記のように配置しています.
<body><neovim-editorid="neovim" font="Ricty,monospace"width="800"height="1000"></neovim-editor><markdown-viewerid="mdviewer"></markdown-viewer></body>
あとは Neovim 側からTextChanged およびTextChangedI でバッファのテキストが変更されるたびにバッファのテキストをrpcnotify() 関数で通知し,それを受けて<markdown-viewer>コンポーネントにテキストを渡す処理をJavaScript で書いてやるだけです.これによって入力をリアルタイムにプレビューに反映できます.
上記の例では,vim_command msgpack-rpcAPI を使って直接 Neovim 側の autocmd を定義しています.
実は今回は高度に UI 拡張可能な Neovim フロントエンドをつくる のが目的でneovim-component は飽くまでこれをつくるパーツに過ぎません.
というわけで,Web Component を使って HTML,CSS,JavaScript,Node.js, ElectronAPI,Neovim msgpack-rpcAPI を使って UI を拡張できる Neovim フロントエンドNyaoVim をつくっています.

Vim が何でないのかが書かれている:help design-not@ja には次のような記述があります.
NyaoVim では Neovim フロントエンドを1つのコンポーネントとして,アプリ内に複数の Web Component を置き,他の Web Component と連携する形で UI を拡張する仕組みを提供します.
NyaoVim が目指すゴールは下記の通りです.

NyaoVim はまだつくりはじめた段階で,つい一昨日 UIプラグインをロードできる実装を入れたところです.<neovim-component> も含めてまだ実用できるレベルになっていませんが,version 0.0.2 としてnpm パッケージとして公開しています.
$ npm install -g nyaovim$ nyaovim
実行するとシンプルな Neovim のGUI エディタが立ち上がります.
サンプルプラグインとしてnyaovim-popup-tooltip を作成したので,これを入れてみましょう.
https://github.com/rhysd/nyaovim-popup-tooltip
UIプラグインはnyaovim-plugin というディレクトリを runtimepath に含んだ普通の Neovimプラグインなので,他のプラグイン同様にプラグインマネージャでインストールできます.例えばneobundle.vim を使う場合は下記のようにinit.vim に書いて:NeoBundleInstall します.
NeoBundle'rhysd/nyaovim-popup-tooltip'一度でもnyaovim を実行していると~/.config/nyaovim/nyaovimrc.html が生成されているはずです.このHTMLファイルが NyaoVim の設定ファイルです.nyaovim-popup-tooltip/nyaovim-plugin/popup-tooltip.html で提供されている<popup-tooltip> を下記のように追加します.
<dom-moduleid="nyaovim-app"><template><style>/* CSS configurations here */</style><!-- Component tags here --><neovim-editorid="nyaovim-editor" argv$="[[argv]]" font-size="14" font="Ricty,monospace"></neovim-editor><popup-tooltip editor="[[editor]]"></popup-tooltip></template></dom-module><scriptsrc="file:///path/to/nyaovim-app.js"></script>
HTML ファイルを設定ファイルとして使うことで,ユーザは自由にエディタ内のコンポーネントをCSS でレイアウトでき,コンポーネントのプロパティを使ってカスタマイズすることができ,追加の処理をJavaScript で記述できます.
ここでeditor="[[editor]]" というプロパティが目をひくかもしれませんが,これはPolymer が提供しているデータバインディングで,<neovim-editor>コンポーネントの紹介時に説明したeditor オブジェクトがプラグイン側に渡ってきています.NyaoVim の UIプラグインとしては Polymer を必須にしているわけではないので,ここではふーん程度にスルーしてください.
準備ができたらnyaovim でエディタを立ち上げて何かドキュメントを開いてみましょう.http リンクでもローカルファイルへのリンクでも良いので,画像へのリンクの上にカーソルを持って行ってgi を入力してみてください.
全てがうまくいっていれば下記のようにポップアップでカーソル下の画像がプレビューできます.

NyaoVim の README にはUI プラグインの作り方も(ざっくりと)書きましたが,長くなりすぎるのでここでは紹介を避けます.気になる方がいらっしゃればリンク先を読んでみてください.
元々Lime text がCUI とGUI の両方をサポートするためにフロントエンド(表示側)とバックエンド(コア)を完全に分けた設計をしていてそういうアーキテクチャに興味があったので,Neovim でも似たようなことができると知り調査を始めました.
今年は Electron アプリをつくったりしていたので Electron で実装するかというのをざっくり決めて公式の node client を試したところうまく動かず,API 的にもコールバック祭になってしまいそうだったのでまずはnode client を fork しました.
動的に生成される Neovim msgpack-rpcAPI 向けのメソッドをコールバックからbluebird の Promise を返すように書き換え,TypeScript がリポジトリ直下に置いているindex.d.ts を見てくれるようになったのでそれに対応し,いくつかバグを修正しました.API をガラッと変えてしまったため,本家への PR はリジェクトされて fork を使い続けることにしました.
これで準備が整ったので NyaoVim のプロトタイプを書き始めました.最初はReact とRedux を使って DOM で Neovim の UI を描画していました.React で最小限の範囲だけ差分描画すれば問題ないかなと思っていたのですが,例えば<C-e> などで画面全体をスクロールしたりすると DOM が全書き換えになってしまい描画がもっさりしてしまう問題に当たってしまいました.
ここでreact-canvas やreact-pixi を使うという手もありましたが今後もメンテされ続けるかがかなり怪しかったため,素直に<canvas> に直接描画するためにスクラッチから実装しなおしました.
この時に Web Component として実装して他の UI として組み立てるというアイデアを思いつき,<neovim-editor> を実装した後は NyaoVim のプロトタイプを全て捨てて新しく実装しました(というか実装中です).
Vim は 'Vim is a text editor' というフレーズが示すように編集に関係ない機能を受け付けない方針で開発されてきたため,グラフィカルな表現は苦手な面がありました.僕もその方針はとても気に入っているのですが,やはりグラフィカルな補助がほしくなるときもあります(例えばドキュメントとか最近は大体 HTML で出力されますし).
そんなわけで今回僕は Web 周りの技術を使ってユーザが自由に拡張できる UIプラグイン機構をneovim-component をつくって Neovim フロントエンドNyaoVim をつくりました.HTML/CSS でつくれる UI なら何でもつくれますし,膨大な npm パッケージも使えるので,良ければぜひ有用なパッケージとか有用じゃないパッケージとかつくって遊んでみてください.

引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。