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

15K 行のアプリを TypeScript 1.8 から 2.0 に移行してみた

TypeScript

先日 TypeScript の新しいメジャーバージョン 2.0 のコンパイラの beta 版がリリースされました.

コンパイラのチェックの強化や非 null 型,tagged union など,いくつかの機能が追加・強化され,自分の趣味プロジェクトでも恩恵に与れそうだったので試しに移行してみました.

移行してみたのは下記の Electron でつくり中の Twitter Client アプリ(React+Redux)で,全体で大体 15000 行ぐらいです.

github.com

下記の手順で修正してみました.

  1. コンパイラをアップデートしてビルドしてみる
  2. --noImplicitThis--noUnusedLocals, --noUnusedParameters を有効にしてみる
  3. --strictNullChecks を有効にして nullability をチェックするようにする
  4. include で glob を使う
  5. Tagged union types で Redux の Action types をリファクタリングする
  6. 引数リストに trailling comma を入れていく
  7. インターフェースやクラスメンバを readonly 指定する

各段階でビルド&単体テスト確認をするのが大事だと思うので,なるべく細かく段階を分けました.コミットも大体各作業ごとになっているので,随時 diff のリンクを貼ります.

「説明しなくて良いからコードを見せろ」という猛者向けに,下記が今回行った作業の全 diff です.+-1000行弱ぐらいですね.

diff

コンパイラをアップデートしてビルドしてみる

TypeScript 2.0 beta は下記のコマンドで入るので,サッと入れてとりあえず npm run build してみます.

$ npm install --save-dev typescript@beta

TypeScript 2.0 では処理フローを見て型を付けるようになったので,この時点でビルドが失敗することがあります. 例えば下記みたいな場合です.

type T = 'aaa' | 'bbb';

const v: T = 'aaa';

switch (v) {
    case 'aaa': /* ここでは v の型は 'aaa' */ console.log('OK'); break;
    case 'bbb': /* ここでは v の型は 'bbb' */ console.log('OK'); break;
    default:    /* ここでは v の型は never */ console.log('NOT OK', v); break;
}

このように,制御フローを見て適宜型を切り替えてくれるようになりました. 上記のように v の型として取りうる可能性のある型がなくなると never という型になり,v を参照しようとするとエラーになります. never type も 2.0 で導入された機能です.

基本的に煩雑さが減る方向のはずなので,修正は容易なはずです.

diff

--noImplicitThis--noUnusedLocals, --noUnusedParameters を有効にしてみる

2.0 ではいくつかのチェック機能がコンパイラに追加されました.互換性が理由(?)でデフォルトはオフのようですが,特に理由が無ければ有効にしておいたほうが良さそうです. tsconfig.json"noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true のように指定できます.

機能名の通り各チェックを行って,検知した時はコンパイルエラーに落としてくれます.特に noUnusedLocals は tslint で検知してくれなかった未使用の import を検出してくれて助かりました.

diff

ここまではたとえ 2.0 にすぐに上げられないプロジェクトでも一時的にコンパイラのバージョンを上げてチェックさせることで恩恵に与れそうです.

--strictNullChecks を有効にして nullability をチェックするようにする

2.0 では null の型がそのまま null となり,T | null のように書くことで nullable な型を表現できます.また,undefined についても同様に T | undefined のように書けます. さらに tsconfig.json"strictNullChecks": true を追加することで,デフォルトで全ての型を non nullable type として扱うようにできます.

基本的には

  1. null を代入しているのに nullable でない変数の型に | null を追加する
  2. null が来る可能性があるのにチェックしていない箇所は素直にチェックする処理を入れる
  3. 型は nullable だけど実質 null な値が来ないところには null assertion 演算子(後置 !)を使う

という処理を入れていけば修正は OK です.3. は例えば下記のような感じです.

// 対象の要素が無い時 getElementById は null を返すので戻り値型は本来
// Element | null だが,静的に HTML に id が振られているので実質失敗しない
// expr! で expr が null にならないとコンパイラに伝える
const elem = document.getElementById('root')!;

また,null assertion 演算子undefined にも使えます.

変更箇所もなかなか多かったのですが,他にも細々とうまくいかないところがあり,この変更は思ったより大変でした.

例えば,strictNullChecks によってオプショナル引数の型が以前と変わります.

function foo(optional?: number) {
    // 1.8 まで -> number
    // 2.0 から -> number | undefined
    optional;
}

これだけ見ると問題ないように見えますが,型定義ファイルには,実質オプショナルでないのにオプショナル引数になっている定義が割といっぱいあります. 特にコールバックの引数の指定がまずいものが多く,1.8 までは型的には変わらなかったので問題にならなかったのですが 2.0 では問題になります.

今回例えば困ったのは react.d.tsStatelessComponent です.

interface StatelessComponent<Props> {
    (props?: Props, context?: any): ReactElement<any>;
    // ... その他雑多なプロパティ
}

上記の props? が問題です.

const MyComponent: React.StatelessComponent<Props> = props => ...;

今までは上記のようにコンポーネントの型を指定し,引数を推論させていてこれでうまくいっていました.ですが,引数は props? となぜかオプショナルに定義されているため, 2.0 からは props の型は Props | undefined に推論され,propsundefined でないかどうかのチェックを入れる必要が出てきてしまいます. (僕の知識が足りないだけで実は props?context? が実際に undefined になるようなケースがひょっとしたらあるのかもしれないですが…もしそうなら教えていただけるとうれしいです)

仕方がないので下記のようにしてワークアラウンドを入れています.

const MyComponent = (props: Props) => ...;

これだと引数の型はプログラマが指定したものになるので undefined チェックを回避できます.現状ではこれで問題になっていませんが,上記型定義の「その他雑多なプロパティ」がほしくなった場合は困ることになります.

また,react-redux.d.tsconnect() も困りました.connect()mapDispatchToProps(第2引数)だけを指定したいときは第1引数を null にできるのですが,第1引数の型定義に | null が入っていないため null を渡せません.仕方ないので手元で react-redux.d.ts に手を入れ,リポジトリに含めて typeings でインストールしたものの代わりにそちらを見るようにして凌いでいます.

型定義ファイルの更新状況などを見ながら,strictNullChecks を付けるのは少し待ったほうが良いかもしれません.DefinitelyTyped では今のところ TypeScript 2.0 向けの型定義ファイルの開発を types-2.0 ブランチで進めているようです.

include で glob を使う

TypeScript のプロジェクトファイル tsconfig.json で指定するソースコード一覧には今まで glob が使えなかったためファイルを1つずつ追加していました. 2.0 からは晴れて *** が使えるようになったのでそれを使うようにします.

**/*.ts**/*.tsxtypings/*.d.ts のように指定すれば良くなりました.

diff

Tagged union types で Redux の Action types をリファクタリングする

2.0 では tagged union types という機能が入り,union types のどの型の値が入っているのかを,各型に共通のプロパティで判断してくれます. プロパティには string literal type を指定すれば良いようです.公式の What's New に載っている例が分かりやすいです.

flux や redux では type プロパティを見てアクションの種類を判断するため,アクションの型を tagged union type にすることでうまく型がつくようになります.

/*
 * action_type.ts
 */

// 'type' プロパティがタグになる
type Action = {
    type: 'AddSomething',
    item: Something,
} | {
    type: 'Sort',
    kind: Kind,
} | {
    type: 'DeleteLast',
};
export default Action;


/*
 * actions.ts
 */
import Action from './action_type';

export function addSomething(item: SomeThing): Action {
    return {
        type: 'AddSomething',
        item,
    };
}

export function sort(kind: Kind): Action {
    return {
        type: 'Sort',
        kind,
    };
}

export function deleteLast(): Action {
    return {
        type: 'DeleteLast',
    };
}


/*
 * reducer.ts
 */
import Action from './action_type';

function reduce(state: State = DefaultState, action: Action) {
    switch (action.type) {
        case 'AddSomething':
            // ここでは action の型は {type: 'AddSomething', item: Something} になる
            const next = ...;
            return next;
        case 'Sort':
            // ここでは action の型は {type: 'Sort', kind: Kind} になる
            const next = ...;
            return next;
        case 'DeleteLast':
            // ここでは action の型は {type: 'DeleteLast'} になる
            const next = ...;
            return next;
        default:
            return state;
    }
}

大体こんな感じです.これで各アクションの処理内で間違ったプロパティにアクセスしていた時などは全てコンパイルエラーになります. 文字列リテラルが複数箇所にあるのが気になるかもしれませんが,全て string literal 型としてコンパイラがチェックするのでタイポしていてもコンパイルエラーになり安心です. 良い感じです.

ちなみに以前は type プロパティが取る値を全部 Kind というオブジェクトに入れて Kind.AddSomething のように参照していたので,:%s/Kind\.\(\w\+\)/'\1'/g のように置換して少しマクロを使ってやるだけで かなり簡単に置き換えられました.Vim 便利.

diff

引数リストに trailling comma を入れていく

個人的にかなり嬉しい機能なのですが,2.0 からは関数の引数に trailing comma が許されるようになりました(公式の例). (配列やオブジェクトの要素は前から trailing comma を許してます).

tslint がチェックしてくれるようになるのを待っても良いのですが,せっかくなので手で置き換えました.\h\w*\s*\(\n あたりで適当に grep で引っ掛けて修正します. Vim なら一度行末にコンマを追加する修正を入れれば,他の箇所は . で繰り返すだけで OK ですね.Vim 便利.

diff

インターフェースやクラスメンバを readonly 指定する

変更するつもりのないインターフェースのプロパティやクラスメンバに 2.0 から導入された readonly 指定をつけてまわります.基本的に大抵のプロパティは変更されないと思うので,デフォルトで readonly 付けるぐらいの勢いで良さそうです.:%s/^\s*\zs\ze\w\+?\=:/readonly /g とかやると雑にプロパティに readonly を付けられるので,付けてみてコンパイラに怒られたらそのフィールドを外す感じで機械的に作業できます.Vim 便利.

diff

まとめ

最近ブログ書いてなかった(ネタはあったのに)ので,久々に人柱になった内容をまとめてみました.まだ beta ではありますが,今のところ変なバグは踏んでません.--strictNullChecks 以外はとりあえずやっておいて損は無さそうです.せっかくなので,いずれ 2.0 が正式リリースされた時に役立つと良いなぁと思っています.

『SD別冊 Vim&Emacs』と『SoftwareDesign 5月号』に寄稿しました

Vim SoftwareDesign

SD別冊 Vim&Emacs と SoftwareDesign 5月号の Vim 特集にそれぞれ寄稿させていただきました.

SD別冊 VimEmacsエキスパート活用術

http://gihyo.jp/book/2016/978-4-7741-8007-6

www.amazon.co.jp

Software DesignVim 特集と Emacs 特集を集めて1冊の本にしたものです.VimEmacs について,入門から拡張の紹介,キーボードの話や昔話まであります.1冊読み終える頃にはエディタ成分を過剰多量に摂取できていることでしょう.

僕は 2015年 1月号に寄稿させていただいた「犬でも分かる!?Vim 導入&カスマイズ超基本」という記事を古くなっている箇所を改定して寄稿させていただきました.特にこれから Vim を使い始めたいと考えている方を対象に,インストール方法から Vim の入門の仕方(チュートリアルプラグイン導入,ヘルプの読み方など)を解説しています.

Software Design で「犬でも分かる!? Vim 導入&カスタマイズの超基本」という記事を書きました

全体を通して個人的に印象深かったのは,まつもとゆきひろさんの記事で EmacsGC 実装などを見て言語実装を学んだというところでした. 確かに EmacsLisp マシンとその上に Lisp で実装されたエディタという構成で,Atom にちょっと似ています(と言っても Atom は下地が Chromium なので全然厚さが違いますが).Emacs に次の GC 発動までに割り当てるメモリサイズとかの指定ができることを考えると普通のマーク・アンド・スイープなのかな.

Emacsruby-mode も実装したとのこと.Rubyend で終わる構文は地味に厄介で,ある endclass ブロックのものなのか if ブロックのものなのか…というのはぱっとみただけでは分かりません.雑にハイライトするだけなら大して問題にはならないのですが,class ... endif ... end では別のハイライト色を割り当てたいとかなるとちょっと考える必要があります.というのを vim-crystal を実装した時に考えたのを思い出しました.

Sofware Design 5月号

http://gihyo.jp/magazine/SD/archive/2016/201605

www.amazon.co.jp

VimGitHub の連携についての記事を『第1特集 コード編集の高速化からGitHub連携まで Vim[実戦]投入』に寄稿させていただきました.こちらは新規に書いた記事です.

仕事でも趣味でも GitHub を使った開発が非常に一般的になってきましたが,Vim はエディタ,Githubウェブサービス,Git は(主にコマンドラインベースの)VCS という都合上,どうしてもエディタとターミナル,ブラウザ間の移動が多くなってしまいがちです.そこで,Vim プラグインを使って VimGitHub の連携をスムーズにすることを目的とした記事を書きました.下記のような tips 形式で紹介しています.

  • Vim から Git を使うための Vim 本体の設定
  • Vim からコミットを見る(コミットブラウザ)
  • Vim から GitHub をブラウザでシュッと開く
  • Vim 内でイシュー確認
  • GitHub ではほぼ必須な Markdown ドキュメントの効率的な編集(表記法とか)
  • リポジトリ URL や issue 番号,絵文字の補完
  • Vim + Gist 連携

Git や GitHub 使ったこと無いよ!という方もいると思いますので,どうやって学べば良いかも introduction として書いてみました.

また,他の記事に目をやってみると,

  • ujihisa さんの入門記事.インストールからプラグインの概要まで幅広い内容です.
  • thinca さんの中級者向け機能紹介.矩形選択やオペレータ・テキストオブジェクト,ドットによるリピート,gn など「とりあえず Vim の基本操作は分かった」な人が読むとかなり勉強になる内容です.
  • tyru さんの正規表現解説.Vim の若干とっつきにくく強力な正規表現の解説です.ここまでしっかり Vim 正規表現とその使いみちについてまとまっている内容はなかなか無いですね.
  • mattn さんの Vim の歴史と最新の開発事情について.今年に入ってから version 8 に向けて非常に活発に開発が進んでいる Vim の開発事情と新機能がとても興味深いです.

といった感じで,Vim を使い始めたい初心者,もっと使いこなしたい中級者,Vim ジャンキー各位どの層にもうれしいラインナップとなっています.発売は明日(4/18)らしいので,是非手にとってほしいです.

なお,他の特集については Ubuntu 16.04 LTS の記事とかがとても参考になりました.Python3 や systemd,日本語入力周りの更新,LXD など色々あるもよう.

まとめ

この時期に Vim 関連の特集および別冊を立て続けに出してくるのは技術評論社なかなか攻めてきてるなという印象でした.エディタはプログラミングする上で欠かせないので,春から新しくプログラミングを始めた人とかにリーチすると良いなぁと思っています. 最近は組み込みな現場を離れてウェブサービス開発する部署にいるんですが,周りを見ると Atom 使ってる人が多いですね.Atom とか VS Code の特集も読みたい.

校正などでアドバイスをいただいた編集の方々および vim-jp の方々ありがとうございました.

WebAssembly を使って自作言語をブラウザで動かしてみよう

LLVM WebAssembly Dachs 犬言語

今日 Google の開発者ブログで WebAssembly の記事が載っていました.どうやら最新の Chrome では WebAssembly が動くようです.

googledevjp.blogspot.jp

自作言語のコンパイラLLVM フロントエンドとしてつくっているので,これは試さないわけにはいきません.

github.com

というわけで,さっそく試してみます.

準備

1. Chrome

直接 V8 をビルドするのは億劫なので Chrome のバイナリを落としてきて使います.Chrome 51.0.2677.0 以降であれば OK です.Canary 版をダウンロードしてきてインストールします. 次に chrome:flags にアクセスして WebAssembly を有効にします.

2. LLVM

WebAssembly のためのアセンブリを吐くには LLVM の experimental な WebAssembly backend を有効にしてビルドする必要があります.Homebrew のフォーミュラをいじって使います.

$ brew edit llvm
# ここで cmake の引数に -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly を追加
$ brew install llvm --HEAD

3. binaryen

binaryenLLVM IR などを WebAssembly 形式に落とすコンパイラです.適当に clone してきて cmakemake でビルドすると bin ディレクトリ内にいくつかのコマンドが生成されます.

4. Dachs

誰も試さないと思いますが一応… clone してきて cmakemake すると dachs というバイナリができているはずです.

いざ試してみる

Dachs は Boehm GC を使って配列を割り当てています.本当は BrainFxxk のサンプルを動かしたいなぁと思っていたのですが,バッファを static に取るとうまく動かなくなったので諦めました.

今回はシンプルな sqrt2.dcs を使います.

func abs(n)
    ret if n > 0.0 then n else -n end
end

func sqrt'(p, z, x)
    ret z if abs(p-z) < 0.00001
    ret sqrt'(z, z-(z*z-x)/(2.0*z), x)
end

func sqrt(x)
    ret sqrt'(0.0, x, x)
end

func main
    print(sqrt(10.0))
end

まずはコンパイルします.アセンブリをそのまま吐くオプションは無いので,まずは LLVM IR にコンパイルします.

$ dachs sqrt2.dcs --emit-llvm > sqrt2.ll

ここで少し sqrt2.ll を修正します.

  • Dachs は単体で動作する汎用言語のため main 関数がある前提ですが,WebAssembly は LLVM IR の module をコンパイルして生成するため不要な エントリポイントである main 関数を削除します.
  • mangling をサボっているので Dachs のコンパイラが吐く LLVM IR の関数名がひどくそのままでは使えないので少し関数名を修正します(C と同じ mangle 名).

sqrt2.ll

さらに llc を使ってアセンブラ形式に落とします.

$ /usr/local/opt/llvm/bin/llc sqrt2.ll -march=wasm32
$ cat sqrt2.s

sqrt2.s

これで準備ができました.binaryen を使ってアセンブリを wasm32 形式にコンパイルします.まずは WebAssembly の AST をテキスト+S式で表現した wast 形式にコンパイルします.

$ s2wasm sqrt2.s > sqrt2.wast

sqrt2.wast

中を見てみるとS式形式に変換された LLVM IR みたいなものが吐き出されているのが分かります.

(module
  (memory 1)
  (export "memory" memory)
  (export "abs" $abs)
  (export "sqrt2" $sqrt2)
  (export "sqrt" $sqrt)
  (func $abs (param $$0 f64) (result f64)
    (block $label$0
      (br_if $label$0
        (i32.or
          (f64.gt
            (get_local $$0)
            (f64.const 0)
          )
          (f64.ne
            (get_local $$0)
            (get_local $$0)

;; ...以下略

次にこのS式をシリアライズしたバイナリフォーマット wasm に変換します.ここで最初は llvm-as を使ってコンパイルしてみたのですがうまくいきませんでした.ちょっと探してみると先人の知恵があったので,sexpr-wasm を使うとうまくいくと分かりました.

$ sexpr-wasm sqrt2.wast -o sqrt2.wasm
0000000: 0061 736d 0a00 0000 140a 7369 676e 6174  .asm......signat
0000010: 7572 6573 0201 0404 0304 0404 0418 1366  ures...........f
0000020: 756e 6374 696f 6e5f 7369 676e 6174 7572  unction_signatur
0000030: 6573 0300 0100 0a06 6d65 6d6f 7279 0101  es......memory..
0000040: 0120 0c65 7870 6f72 745f 7461 626c 6503  . .export_table.
0000050: 0003 6162 7301 0573 7172 7432 0204 7371  ..abs..sqrt2..sq
0000060: 7274 7e0f 6675 6e63 7469 6f6e 5f62 6f64  rt~.function_bod
0000070: 6965 7303 2000 0102 0700 0048 9b0e 000c  ies. ......H....
0000080: 0000 0000 0000 0000 980e 000e 000f 0090  ................
0000090: 0e00 140e 0039 0001 0207 0000 9c12 008a  .....9..........
00000a0: 0e00 0e01 0cf1 68e3 88b5 f8e4 3e14 0e01  ......h.....>...
00000b0: 1412 010e 0189 0e01 8c8a 8b0e 010e 010e  ................
00000c0: 028b 0e01 0c00 0000 0000 0000 c00e 0211  ................
00000d0: 0014 1201 0c00 0000 0000 0000 000e 000e  ................
00000e0: 00

あとはこれを適当に Uint8Array に突っ込む JavaScript のコードを書き,HTML ファイルを書きます.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" />
  </head>
  <body>
    <div id="test"></div>
  </body>
  <script>
    const buf = new Uint8Array([0x00, 0x61, 0x73, 0x6d, ...]);
    const module = window.Wasm.instantiateModule(buf);
    console.log(module);

    const result = module.exports.sqrt(314);

    document.getElementById('test').innerText = `Result: ${result}`;
  </script>
</html>

index.html

最後に Canary 版の Chrome で index.html を開いてみます.

f:id:rhysd:20160324235304p:plain

画像内の DevTools のコンソールにあるように,Wasm.instantiateModule を使って JavaScript で実行できるモジュールに変換できます.今回はニュートン法平方根が期待通り計算できていることが分かりました.

まとめ

自作言語がブラウザ上で動いているのはちょうたのしい.

  • 追記

勢い余って Vim の wast filetype 対応プラグインつくりました.

github.com

Electron で Chrome のページ内検索機能を使う

Electron

比較的最近,Electron に Chrome のページ内検索を JavaScript から行える API が入りました.最近そのことを知ったので,今日昨日あたりでその機能を使って Shiba に Markdown プレビュー内の検索機能を実装しました.案外手こずってしまったのでまとめてみます.

f:id:rhysd:20160321232118g:plain

github.com

実装コード

今回 Shiba 向けに実装したコードは この diff が全てです.僕の説明よりも実際に動いているコードを見て理解したいという方はそちらを見てください.

ページ内検索 API 概要

WebContents オブジェクトに findInPage()stopFindInPage() の2つのメソッドが生えています.また,WebContents.on() で飛んでくるイベントの中に found-in-page というイベントが追加されています.使う API はコレで全てです.なお,<webview> にもほぼ同じ API が生えています.ですので,<webview> の中の検索をしたい場合はそれらを代わりに使います.今回は BrowserWindow 内の検索を実装しました.

実際に動くコードは上で示した diff を見ていただくとして,概要はこのようになっています.

const previous_text = '';
const webcontents = browser_window.webContents;
webcontents.on('found-in-page', (event, result) => {
    if (result.activeMatchOrdinal) {
        // マッチした箇所を覚えておく
        this.active = activeMatchOrdinal;
    }

    if (result.finalUpdate) {
        // M個のマッチ中 N 番目がアクティブな時,N/M という文字列をつくる
        this.result_string = `${this.active}/${result.matches}`;
    }
});

function search(text) {
    if (previous_text === text) {
        // 前回の検索時とテキストが変わっていないので次のマッチを検索
        webcontents.findInPage(text, {findNext: true});
    } else {
        // 検索開始
        previous_text = text;
        webcontents.findInPage(text);
    }
}

const input = document.querySelector('input');
input.addEventListener('keydown', event => {
    if (event.code === 'Enter') {
        search(input.value);
    }
})

const stop_button = document.querySelector('button');
stop_button.addEventListener('click', () => {
    // マッチした部分のハイライトを消して検索終了
    webcontents.stopFindInPage('clearSelection');
});

found-in-page イベントは1回の検索で2度飛んできます.

  • アクティブな(オレンジにハイライトされる)検索結果が見つかった時点で1回発火
    • この時 activeMatchOrdinalresult に入ってくる
  • ページ全体の検索が完了した時点で1回発火
    • この時マッチの総数を表す matchesresult に入ってくる

これによって JavaScript 側で検索結果の詳細を取れます.ちょっと stateful で使いにくいですが,使えなくはないです.マッチした箇所も result.selectionArea で取れます.

次に例えば <input> 要素にテキストを入力させて Enter キーで検索開始するとします.新規に検索する場合も前回の検索を継続する場合も findInPage() メソッドを使います.これもちょっと stateful です.でも前回の検索テキストを持っておけば上記のように Chrome っぽい挙動で検索が行えます.

最後に検索を終了したい場合は stopFindInPage() メソッドを使います.引数に渡す文字列で検索結果のハイライトを消すかどうかを決められます.

これで終われれば「面倒なページ内検索が Chrome の力を借りて一瞬で実装できる!」で終わったのですが,残念ながらそうはなりませんでした.

ページ内検索 API のハマりどころ

Chrome ではページ内検索はブラウザの UI として C++ で実装されています.ですが,Electron は Chromium のガワを取り払ったブラウザのため検索ウィンドウはありません.そこで JavaScript から触れるようにしようというのが上記 API でした. ですが,ここには1つ実は大きな問題があります.

上記のように,「検索窓」UI を自前で提供してやる必要があります.そこで,<input> タグ(Shiba の場合は Polymer を使っているのでデザインを揃えるために <paper-input>)を使って UI を HTML で書いていきます.いざ完成し,テキストを入れ Enter を押すと異変に気づきます.

最初の検索はうまくいき,最初のマッチがハイライトされますが,次の Enter を入力してもアクティブなマッチが次のマッチに移動しません.ここで僕は remote モジュール越しにレンダラプロセスから API を呼んでいたこともあり,違う箇所を疑いだしてハマってしまいました.

実際の挙動はこうでした.

  1. 最初の検索をする.
  2. ページ内検索はテキストエリアを含めた全ての文字列を検索するので,検索窓の中の <input> のテキストも含めて検索する
  3. 次の検索をしようとして検索窓にカーソルがある状態で Enter を押す.
  4. 検索窓内の <input> のテキストエリアの文字列が更新される(Enter 入力なので見かけ上は変更はない.が,内部的には更新される)
  5. ページ内のコンテンツが更新されると Chrome 組み込みの検索機能はページの最初から検索をやり直す
  6. 結果としてマッチは最初の場所から動かない(毎回 Enter 入力で検索がリセットされてしまう)

Electron はほぼ全ての UI を HTML/CSS でページ内に描画するため,入力フォームも HTML/CSS で書くのが自然です.ですが,ページ内検索機能は上記のように機能しなくなります.これを解決するにはフォームをページ外に持っていくしかありません(もっと良い方法あればぜひ教えて下さい).

さっと思いつく方法は次のどちらかです.

  • 新しい小さい検索窓用の BrowserWindow をつくり,そっちに検索ワードを入力させる
  • <webview> タグで検索窓をラップし埋め込む

どちらが良いかはアプリケーションによると思います.今回 Shiba では後者を採用しました.ウィンドウ右上の検索窓内の <paper-input><webview> でラップされた別のプロセスで動いています.よってページ内検索機能への影響がありません.

欠点としては <webview> を導入するとフォーカスの処理がやや煩雑になります.(ページ内検索をするとフォーカスが <input> から外れてしまうので <webview> の中の <input> にフォーカスを戻す必要がある)

ハマりどころ対応版の実装は JavaScript 部分では下記のようになりました.

本体側

const previous_text = '';
const webview = document.querySelector('webview');
webview.src = 'file:///path/to/search-box.html';
webview.on('ipc-message', event => {
    if (event.channel === 'search:start') {
        const text = event.args[0];
        if (previous_text === text) {
            remote.getCurrentWebContents().findInPage(text, {findNext: true});
        } else {
            remote.getCurrentWebContents().findInPage(text);
        }
    }
});

<webview> で読み込まれた HTML 側

const ipc = require('electron').ipcRenderer;
const input = document.querySelector('input');
input.addEventListener('keydown', event => {
    if (event.code === 'Enter') {
        // send() じゃないので注意!
        ipc.sendToHost('search:start', input.value);
    }
});

まとめ

冒頭にも書きましたが,思っていたより手こずってしまいました.しかし,一度分かってしまえば1時間足らずぐらいでページ内検索を実装してしまえそうです.お手軽感はあります.

飽くまで Chrome のページ内検索 APIJavaScript に露出させているだけです.なので,検索マッチのハイライトを変えられなかったりなど細かい制御は効きません.最初から React.js などの SPA 向けライブラリを使っている場合はどっちが良いか,アプリ次第になるかなと思います.

Issue と PR のテンプレートジェネレータつくった

GitHub CLI golang

screenshot

出張の帰りのフライトが10時間以上あったので簡単なツールをつくってみました.

余談ですが Windows PC しかなかったので,Windows PC + golang + gVim で書いてみました.Go 言語は標準ライブラリだけでもぐりぐり書けてなかなか良いですね.久々の Windows でのコーディングでしたが特に問題ありませんでした.

GitHub の issue/PR テンプレート機能

最近 issue / PR のテンプレート機能が実装されたみたいです.リポジトリ直下か .github ディレクトリのどちらかに ISSUE_TEMPLATE.md および PULL_REQUEST_TEMPLATE.md を置いておくと,issue や PR 作成時にそのテンプレートが入力フォームにデフォルトでセットされます.

Issue and Pull Request templates - GitHub Blog

また,以前からある CONTRIBUTING.md.github ディレクトリの中に置けるようです.

.github ディレクトリを生成するコマンド dot-github

github.com

前々からほしいなーと思っていた機能だったのでさっそく使いたいところですが,毎度似たようなテンプレートを各リポジトリにつくるのも手間だなぁということでテンプレートジェネレータをつくりました.これで元になるファイルだけ dotfiles なりで管理しておけば,他の PC などからでも一発でテンプレートが生成できます.

下記のように go get するとインストールできると思います.

$ go get github.com/rhysd/dot-github

テンプレートは ~/.github 内に置きます.他の場所に置きたいときは $DOT_GITHUB_HOME 環境変数をセットすることで置き場所を変更できます.

~/.github 内に置けるテンプレート生成ファイルは次の通りです.ファイルが存在しない場合は単純に生成をスキップします.

ファイル名 説明
ISSUE_TEMPLATE.md Issue 用に使われるテンプレート
PULL_REQUEST_TEMPLATE.md PR 用に使われるテンプレート
ISSUE_AND_PULL_REQUEST_TEMPLATE.md Issue および PR で上記のテンプレートが無い場合に使われる共通のテンプレート
CONTRIBUTING.md コントリビュートガイドライン用のテンプレート

dot-github コマンドによって上記のテンプレートファイルからリポジトリ内に .github が自動生成されます.

$ cd your-repo
$ dot-github

.github 内に生成されるファイルは Go 言語標準の text/template テンプレートを使ってテンプレート生成ファイルから生成されます.

Package template - The Go Programming Language

標準の機能に加えて,下記の変数が使用可能です.

変数名 説明
.IsIssue boolean ISSUE_TEMPLATE.md として展開される時に True
.IsPullRequest boolean PULL_REQUEST_TEMPLATE.md として展開される時に True
.IsContributing boolean CONTRIBUTING.md として展開される時に True
.RepoName string リポジトリ
.RepoUser string リポジトリ所持者の名前

text/template{{if}} などを使って重複を省いて issue と PR 共用のテンプレートを書いても良いですし,最初から別々にテンプレートを書いても良いです.

生成例

使用するテンプレート

  • ~/.github/ISSUE_AND_PULL_REQUEST_TEMPLATE.md
{{if .IsIssue}}
### Expected Behavior


### Actual Behavior


{{end}}
{{if .IsPullRequest}}
### Fix or Enhancement?


- [ ] All tests passed
{{end}}

### Environment
- OS: Write here
- Go version: Write here
  • ~/.github/CONTRIBUTING.md
Thank you for contributing to {{.RepoName}}!
=========================================

Please follow issue/PR template.

生成されたファイル

  • /path/to/your-repo/.github/ISSUE_TEMPLATE.md
### Expected Behavior


### Actual Behavior


### Environment
- OS: Write here
- Go version: Write here
  • /path/to/your-repo/.github/PULL_REQUEST_TEMPLATE.md
### Fix or Enhancement?


- [ ] All tests passed

### Environment
- OS: Write here
- Go version: Write here
  • /path/to/your-repo/.github/CONTRIBUTING.md
Thank you for contributing to my-project!
=========================================

Please follow issue/PR template.

まとめ

GitHub に追加された issue / PR テンプレート機能をさっと使えるようにジェネレータをつくってみました.Go 言語の流儀に則ってちゃんとテストも書いてみたので,僕みたいにリポジトリつくりまくって毎度わざわざテンプレート書くのがめんどいみたいな人はぜひともお試しください.

yak shaving 気味ですが,勢いがあったのでつくってしまいました.新幹線内とか飛行機内だと割と手が動きやすいのはどうしてなんでしょうね…

2015年を振り返る

雑感

2015年の自分向けまとめです.

まとめ

目標は達成できたかどうか分からないといけないと思っているので,2015年の趣味での目標は 「とりあえず10万行書くか」 だったんですが,測るのが面倒なので途中から「3000 contributions する」 になりました.(自分の場合は1コミット大体40行ぐらいが多いので,3000 あればまあ10万行行っとるやろという魂胆です)GitHub は仕事で使ってないので,GitHub のプロフィールページを見れば一発で達成状況が分かるという寸法です.

結果はというと…

f:id:rhysd:20151231211127p:plain

でどうにかギリギリ達成できました.来年はもっとパワフルに開発できる年にしたいです.

そんなわけで今年も割と色々書いた気がします.せっかくなので振り返ってみます.

前半

今年の前半は言語処理系まわりを色々触ってました.既存の言語の処理系を実装してみるのも間違いなく楽しいですが,自分で言語のデザインを考えてみるのもプログラミング自体を処理系の側の視点から見ることができてかなり楽しいです.もちろん実装も面白いです.

Dachs

犬言語.一応まだ死んでないつもりです.

Ruby テイストな構文ですがクラスのインスタンスに対するメソッド呼び出しは UFCS による構文糖衣で,全て関数です.関数型言語周りの機能と親和性を良くしたいなというのが狙いだったりします.変数はデフォルトで const で,const な変数のみ参照で渡せるようになっています.基本的に型はほとんど書かず頑張って推定(deduction)します.

今年の前半までは継続してつくっていて,今年は generic function type とかコピーやキャストの挙動を定義できるようにしたり,しょぼいモジュールの初期実装を入れたり,Boehm GC を乗っけたりしました.

ただ,やはりパーサのビルド時間がつらいのでそこをどうにかしないといけない感じです.犬言語では AST のノードの定義に boost::variant を使っていて,末端のノードは普通の struct ですが,例えば式のノードは using Expr = boost::variant<Literal, BinaryExpr, CallExpr, ...> のような実装になっています.これと Boost.Spirit V2 が合わさって型がめちゃくちゃ巨大になるのが一番の原因な気がしてます.別言語に乗り換えを試みるのも悪くなさそうです.

特に焦ることもないので,どういう言語機能があるとどうして自分のコーディングが楽しくなるのかとかそういったことを考えながら引き続き楽しんでやっていきたいです.

Crisp

Make a Lisp をベースにした Lisp の方言の処理系です.Crystal で書きました.動的言語の言語機能のアイデアを試すのに使えたらなーと思ってつくったので,特に実用は考えてませんが,Crystal と何かうまく連携できると良いなぁと思ってます.(が,現状思っているだけです…)

Make a Lisp で Lisp 処理系を学んでつくる (with Crystal)

後半

今年の後半はほぼ Electron アプリを JavaScript とか TypeScript で書いていた気がします.JavaScript はこれまでその場しのぎのコードをちょっと書いたことぐらいしかなかったですが,せっかくなので ES2015 ネイティブ(!)で勉強しました.TypeScript も JavaScript でありがちなケアレスミスを拾ってくれたり程良い感じがして結構気に入ってます.

NyaoVim

NyaoVim logo

Web Component で UI を拡張できる NeoVim フロントエンドです.以前は拡張するのが難しかった UI を HTML/CSS, JavaScript, Node.js, Electron API などを使って拡張する仕組みが用意されています.Neovim 本体も <neovim-editor> というコンポーネントの1つとして実装されています. まだなかなか基本機能が安定しなかったりしますが,継続して開発していきたいと考えています.

サンプルプラグインもいくつか書いてみました.

僕は基本的にはコマンドライン大好きなので Vimコマンドラインで使ってます.僕にとって GUIVimコマンドラインVim とは用途が違っていて,GUI のエディタはもっと強力な UI の表現力を持っていても良いのかなと思ったのが最初でした.

また,このフロントエンド実装は Proof of Concept な側面があると思っていて,例えば補完のインターフェースとして(既存の一覧表示ではなく)もっと良いアイデアが思いついた時に,それを簡単に試す事ができます.(例えば Vim 側で補完の候補一覧だけ生成して UI 側に渡し,UI 側でユーザからの入力を受け付けて結果を Neovim 側に戻すなど).実際に動く形で自分のアイデアが簡単に実装できるというのは大きいんじゃないかなと思っていて,僕自身も色々試してみたいと思ってます.

Web Components と Electron でつくる Neovim フロントエンドの未来

Shiba

shiba logo

マークダウンプレビューアプリです.管理しているパス(ディレクトリ/ファイル)のファイル保存を検知して自動でプレビューを更新します.Electron + Polymer でつくりましたが,色々実装が良くないところもあるのでぼちぼち作り直したいと思ってます.

  • タブを付けて複数パスを監視できるようにする
  • Markdown -> HTML の変換のパフォーマンスアップ
  • コンポーネント追加でプレビュー拡張(例えば新しいフォーマットをプレビューの対象に加えられるようにしたりとか)

Electron と Polymer と TypeScript でリッチなマークダウンプレビュアー Shiba つくった

Trendy

Trendy logo

GitHub のトレンドリポジトリのページを監視して新規リポジトリを通知したり管理したりできます.メニューバーにアイコンを置いて,通知があるときはアイコンの色を変えることで通知します.メニューバーからアクセスできるので,気になった時にさっと見ることができます.

GitHub のトレンドリポジトリを見逃さない,Trendy をつくりました

Tilectron

Tilectron logo

ウィンドウ分割可能なブラウザです.タイル型のウィンドウ管理で広いディスプレイを活用しつつ,スクロールやカーソル移動などすべての操作をキーボードのみでやることが目的です.すでにポインタ動かしたりクリックを入力したりとかは技術的にはできるのはわかってるんですが,なかなか手を付けられていない状態です…

ぼちぼち進めて行きたい.

発表

東京 Node 学園祭,VimConf 2015,Effective Modern C++ 読書会で発表させてもらいました.

  • 東京 Node 学園祭

東京Node学園祭 2015 で Electron について話した

  • VimConf 2015

vimconf でブラウザ上で Vim を使う方法を発表してきた

  • Effective Modern C++読書会

原著を一通り読んで合計 4 items 分ぐらい発表させてもらいました.C++11 や C++14 時代に必須な内容もあったりでおすすめできる内容でした.邦訳も出ました.

その他つくったものリスト

その他にも色々つくったりした気がします.結構つくって忘れているものもあるので,「こんなのもあったなー」と思い出すのもかねてリストアップ.(完全につくりかけは除く)

Web Components と Electron でつくる Neovim フロントエンドの未来

Neovim Vim Electron Web Component

この記事は Vim Advent Calendar 2015 の20日目の記事です.

まずはこちらのスクリーンショットをご覧ください.

markdown example screenshot

image popup example screenshot

mini browser example screenshot

エディタの UI やカーソル移動は Vim っぽいですが,markdown ライブプレビューやカーソル位置での画像ポップアップ,組み込みブラウザなど謎の UI が見て取れます.本記事ではこれについてボトムアップで必要な知識から順を追って紹介します.

長い……三行で

目次

  1. Web Components とは
  2. Neovim の フロントエンド向け msgpack-rpc API
  3. Neovim の UI を Web Component としてラップする
  4. Electron アプリに組み込んでみた例
  5. UI 拡張可能な Neovim フロントエンド NyaoVim
  6. 実装の経緯
  7. まとめ
  8. おまけ

Web Components とは

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 されていない機能ですが,GooglePolymer など Web Component をラップして使いやすくしたライブラリがすでにいくつかあり,再利用可能なコンポーネントが提供されています.

Neovim の UI msgpack-rpc API

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-qtneovim-dot-app などの Neovim フロントエンドが存在します.

Neovim の UI を <canvas> に描画して WebComponent としてラップする

上記で説明した msgpack-rpc を介した UI 描画イベント通知の API を使って HTML の <canvas> 上に Neovim のフロントエンド部分を描画し,それを Web Component としてラップしてみました.

github.com

HTML 上に描画するという選択をしたのは,近年 NW.jsElectron といったデスクトップアプリフレームワークが出てきていて,容易に Neovim エディタをこれらで作成したアプリに組み込めるようになるからです. また,WebSocket などで通信して描画情報を受け取ればリモートにある Neovim を手元のブラウザに描画して処理するなども可能です.

今回はローカルで実行できるようにしたいので,外部プロセスとして Neovim を実行し標準入出力で通信します.ブラウザ内では外部プロセスを起こせないので,Node.js の child_process モジュールで子プロセスを管理します.このことから分かる通り,今回のアプローチでは一般的なウェブサービスに組み込むことはできませんが,Electron アプリなどの Node.js が統合された環境では利用できます.

ユーザからの入力や Neovim プロセスからの通知などのデータフローをうまく扱いつつ,カーソル位置などの状態を適切に更新するために Flux というアーキテクチャを使います.

flux architecture

(https://facebook.github.io/flux/docs/overview.html#content より引用)

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

data flow

今回はこんな感じになりました. 緑色の部分が 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() といった canvas API で描画するだけのオブジェクトです.また,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 上につくられたエディタのプラグインとしても使えるのではないかと思います(エディタの中にエディタを組込むのがどれくらい嬉しいかは別問題ですが…)

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-editor id="neovim" font="Ricty,monospace" width="800" height="1000"></neovim-editor>
  <markdown-viewer id="mdviewer"></markdown-viewer>
</body>

あとは Neovim 側から TextChanged および TextChangedI でバッファのテキストが変更されるたびにバッファのテキストを rpcnotify() 関数で通知し,それを受けて <markdown-viewer> コンポーネントにテキストを渡す処理を JavaScript で書いてやるだけです.これによって入力をリアルタイムにプレビューに反映できます.

上記の例では,vim_command msgpack-rpc API を使って直接 Neovim 側の autocmd を定義しています.

UI 拡張可能な Neovim フロントエンド NyaoVim

実は今回は 高度に UI 拡張可能な Neovim フロントエンドをつくる のが目的で neovim-component は飽くまでこれをつくるパーツに過ぎません.

というわけで,Web Component を使って HTML, CSS, JavaScript,Node.js, Electron API,Neovim msgpack-rpc API を使って UI を拡張できる Neovim フロントエンド NyaoVim をつくっています.

NyaoVim

github.com

Vim が何でないのかが書かれている :help design-not@ja には次のような記述があります.

Vim をシェルや IDEコンポーネントとして使おう

NyaoVim では Neovim フロントエンドを1つのコンポーネントとして,アプリ内に複数の Web Component を置き,他の Web Component と連携する形で UI を拡張する仕組みを提供します.

NyaoVim が目指すゴールは下記の通りです.

  • NyaoVim 本体は(gVim のような)最小限の Neovim の UI と Web Component によってユーザが自由に UI を拡張できる仕組みのみを提供します.ユーザはそれを使って自分の好きなコンポーネントを作成・インストールして拡張します.
  • UI 拡張は Neovim プラグインとして提供できるようにし,新たなプラグインマネージャを必要としないように設計します.これによって vim-plug など Vim プラグインマネージャを使って UI 用のプラグインも管理できます.
  • Neovim(もしくは Vim)本来のエディタとしての機能性(高い応答性など)を損なわないように実装します.
  • Linux, OS X, Windows 対応のクロスプラットフォーム(現時点では Windows をサポートできる段階にまだきていないですが…)

structure

ためしてみる

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-module id="nyaovim-app">
  <template>
    <style>
      /* CSS configurations here */
    </style>

    <!-- Component tags here -->
    <neovim-editor id="nyaovim-editor" argv$="[[argv]]" font-size="14" font="Ricty,monospace"></neovim-editor>
    <popup-tooltip editor="[[editor]]"></popup-tooltip>
  </template>
</dom-module>

<script src="file:///path/to/nyaovim-app.js"></script>

HTML ファイルを設定ファイルとして使うことで,ユーザは自由にエディタ内のコンポーネントCSS でレイアウトでき,コンポーネントのプロパティを使ってカスタマイズすることができ,追加の処理を JavaScript で記述できます.

ここで editor="[[editor]]" というプロパティが目をひくかもしれませんが,これは Polymer が提供しているデータバインディングで,<neovim-editor> コンポーネントの紹介時に説明した editor オブジェクトがプラグイン側に渡ってきています.NyaoVim の UI プラグインとしては Polymer を必須にしているわけではないので,ここではふーん程度にスルーしてください.

準備ができたら nyaovim でエディタを立ち上げて何かドキュメントを開いてみましょう.http リンクでもローカルファイルへのリンクでも良いので,画像へのリンクの上にカーソルを持って行って gi を入力してみてください.

全てがうまくいっていれば下記のようにポップアップでカーソル下の画像がプレビューできます.

nyaovim-popup-tooltip screenshot

NyaoVim の README には UI プラグインの作り方も(ざっくりと)書きましたが,長くなりすぎるのでここでは紹介を避けます.気になる方がいらっしゃればリンク先を読んでみてください.

実装の経緯

元々 Lime textCUIGUI の両方をサポートするためにフロントエンド(表示側)とバックエンド(コア)を完全に分けた設計をしていてそういうアーキテクチャに興味があったので,Neovim でも似たようなことができると知り調査を始めました.

今年は Electron アプリをつくったりしていたので Electron で実装するかというのをざっくり決めて公式の node client を試したところうまく動かず,API 的にもコールバック祭になってしまいそうだったのでまずは node client を fork しました.

動的に生成される Neovim msgpack-rpc API 向けのメソッドをコールバックから bluebird の Promise を返すように書き換え,TypeScript がリポジトリ直下に置いている index.d.ts を見てくれるようになったのでそれに対応し,いくつかバグを修正しました.API をガラッと変えてしまったため,本家への PR はリジェクトされて fork を使い続けることにしました.

これで準備が整ったので NyaoVim のプロトタイプを書き始めました.最初は ReactRedux を使って DOM で Neovim の UI を描画していました.React で最小限の範囲だけ差分描画すれば問題ないかなと思っていたのですが,例えば <C-e> などで画面全体をスクロールしたりすると DOM が全書き換えになってしまい描画がもっさりしてしまう問題に当たってしまいました.

ここで react-canvasreact-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 パッケージも使えるので,良ければぜひ有用なパッケージとか有用じゃないパッケージとかつくって遊んでみてください.

おまけ

omake

github.com