Neovim のフロートウィンドウ機能を使って git-messenger.vim をつくりなおした

git-messenger.vim は,カーソル下の行のコミット情報を表示する Vim プラグインです.

github.com

他所のプロジェクトのコードや OSS のコードを読んでいると,なぜこうなってるんだろう?と思うことがよくあります.コミットメッセージをまともに書いているプロジェクトでは,その部分の変更を加えたコミットメッセージに答えがあることがあるのですが,毎回 git blame で対象のコミットを引っ張ってきて git show で読むのは結構大変です.

git-messenger.vim では,その負担を軽減するために,(1) カーソルがいる行にコミット情報を git から引っ張ってきて (2) 良い感じに表示する という機能を提供します.

もう6年も前につくったプラグインなのですが,Vim の制約上いくつかの問題があり,syohex さんの Emacs port に比べて良いものになりませんでした.Vim のバルーン(マウスを一定時間バッファ上に置いておくと出せるツールチップ)が GUI にしか対応していないのが主な理由でした.

先日,Neovim にフロートウィンドウ機能が入り,CUI でもツールチップのような UI が簡単に使えるようになったので,git-messenger.vimフルスクラッチでつくりなおしました.Neovim のフロートウィンドウについては後の章でまとめました.

git-messenger.vim

インストール

すべて Vim script で実装されているので,他のプラグインと同様に rhysd/git-messenger.vim リポジトリをインストールするだけで OK です.あともちろん git コマンドも要ります.

コマンドの実行は以前は system() を使っていたのですが,大きいリポジトリでもユーザの入力をブロックしないように job を使って書いています.なので,Neovim または Vim (8 以降)が必要です.

また,フロートウィンドウは最近 Neovim の master に入ったところなのでまだ安定版としてはリリースされていません.開発版の 0.4.0-dev を使う必要があります.例えば macOS であれば brew install neovim --HEAD で入ります.

使い方

:GitMessenger もしくはデフォルトでマップされている <Leader>gm を入力すると,カーソルしたのコミット情報がポップアップウィンドウで表示されます.

  • Commit hash
  • Author, Committer
  • Summary (コミットメッセージの1行目)
  • Body (コミットメッセージの2行目以降)

ポップアップウィンドウは Neovim 0.4.0 以降では前述のフロートウィンドウで,それ以外ではプレビューウィンドウで実装されています.その後カーソルを動かすとポップアップは自動で閉じます.

https://github.com/rhysd/ss/blob/master/git-messenger.vim/demo.gif?raw=true

基本的にはこれだけですが,ポップアップを開いたあとにカーソルを動かさずそのままもう一度 :GitMessenger コマンドか <Leader>gm を入力するとポップアップウィンドウの中にカーソルを移動できます.コミットメッセージが長すぎて全部表示できなかった時のスクロールやメッセージのクリップボードへのコピーなどができます.

ウィンドウ内ではいくつかローカルなマッピングが定義されており,o でより古いコミットを手繰ることができます.直近のコミットにほしい情報が書いてあるとは限らない(例えばフォーマッタでの整形が挟まったり,別のリファクタリングが挟まったりなど)ので,そういうときはより古いコミットのメッセージにほしい情報があることがあります.さらに O で新しいコミットに戻ったり,q でウィンドウを閉じたりできます(? でヘルプ)

https://github.com/rhysd/ss/blob/master/git-messenger.vim/history.gif?raw=true

カスタマイズ

  • デフォルトのマッピング <Leader>gm が気に入らない場合は g:git_messenger_no_default_mappingsv:true をセットして <Plug>(git-messenger) をマップすれば OK です
  • 他にもいくつか <Plug> マップが定義されています
  • いくつかの挙動はグローバル変数で制御できるようになっています
  • Neovim のみポップアップウィンドウ内のハイライトをカスタマイズできます.デフォルトの色合いがお使いのカラースキームに合わないときは自分で色をカスタマイズできます.Neovim のみなのはウィンドウローカルにハイライト色を変更できる winhighlight というオプションを Neovim だけが持っているためです

詳しくはリポジトリREADME.md:help git-messenger で確認できます.

Neovim のフロートウィンドウについて

Vim のウィンドウは分割することでタイル型に配置されるレイアウトしか対応しておらず,ウィンドウが重なるようなレイアウトは(無理矢理バッファを書き換えてそれっぽく見せるようなことをしない限り)実現できませんでした.

そこで Neovim では CSSposition: relativeposition: absolute のように位置基準でウィンドウの重なりを許すウィンドウレイアウトを実装しました.

何が良いのか

この機能の設計で特に優れていると感じる点は,レイアウト以外はほぼ完全に普通の Vim のウィンドウと同じところです.

  • CUI で使える
  • プラグイン開発者はウィンドウの開き方だけ分かれば,あとは普通の Vim のウィンドウと同様に扱える
  • ウィンドウ内のコンテンツは普通の Vim のバッファなので,setline() などで自由に追加・変更・削除できる
  • filetype をセットしたり,自由にハイライトできる
  • ウィンドウを開く以外はバルーンのような専用の API が要らない

この機能により,プラグイン開発者はウィンドウの上にオーバーレイするような UI を変なハックをしたり妥協することなく実装することができます. ぱっと思いつくのは

  • 補完ウィンドウの自作
  • ドキュメント情報表示ツールチップ
  • fuzzy finder の選択ウィンドウ(VS Code のような)
  • ターミナル表示

などに使えそうです.

使い方

フロートウィンドウを開くのには nvim_open_win(),サイズや配置を変えるのには nvim_win_set_config() を使います.

" 開くウィンドウの幅
let width = 40

" 開くウィンドウの高さ
let height = 10

" ウィンドウを開いたあと,カーソルをそのウィンドウ内に移動するか
let enter = v:true

" カーソルを :wincmd やマウスでウィンドウ内に移動できるか
let focusable = v:true

" 何に対して相対的にウィンドウの配置位置を決めるか
"   - "editor": エディタのスクリーンに対して.スクリーン上の絶対座標で指定
"   - "win": 現在のウィンドウ位置に対して.これを使う場合はオプションに 'win' というキーで別途対象ウィンドウのウィンドウ ID を指定する
"   - "cursor": カーソル位置に対して
let relative = 'cursor'

" 基準となるウィンドウの角を四隅のどこにするかを指定します(デフォルト "NW"
"   - "NW": 左上
"   - "NE": 右上
"   - "SE": 右下
"   - "SW": 左下
let anchor = 'NW'

" 'relative' で指定した位置に対する相対的なオフセット
let row = 1
let col = 0

" Neovim 内ではなく,GUI フロントエンド側にウィンドウを出すよう依頼するか
let external = v:false

" 現在のバッファをフロートウィンドウで開く
" カーソルのすぐ下に 40x10 のウィンドウが開かれる
let win_id = nvim_open_win(bufnr('%'), enter, {
    \   'width': width,
    \   'height': height,
    \   'relative': relative,
    \   'anchor': anchor,
    \   'row': row,
    \   'col': col,
    \   'external': external,
    \})

" 新しいバッファを開いたり
enew

" 普通に setline() を使ってコンテンツをセットしたり
call setline('.', ['hello', 'world!'])

" filetype を指定してハイライトを定義したり
set filetype=ruby bufhidden=wipe nomodified buftype=nofile

" フロートウィンドウを別のハイライトで描画したり
set winhighlight=Normal:MyNormal,NormalNC:MyNormalNC

" nvim_win_set_config() でウィンドウのレイアウトをやり直せる
call nvim_win_set_config(win_id, {
    \   'width': 60,
    \   'height': 30,
    \   'relative': 'editor',
    \   'row': 10,
    \   'col': 10,
    \})

フロートウィンドウにはボーダーが無いので,デフォルトのままではフロートウィンドウと下のウィンドウの境界がわかりません.なので特に最後の既存のウィンドウと別のハイライトで背景色を描画するのは重要です.また,フロートウィンドウ内で新しいバッファを開いた際はウィンドウを閉じると中身も自動で開放されるよう bufhidden=wipe を指定しておきます.

nvim_win_set_config() でウィンドウのレイアウトをやりなおすことができるので,これを使ってウィンドウのサイズ変更や位置変更をします.

ちなみに rowcolwidthheight がエディタのスクリーンからはみ出してしまってもエラーになりません.Neovim はなるべくウィンドウをスクリーン内に収めるように描画します.git-messenger.vim では現在のカーソル位置からポップアップを出すのに十分な幅・高さがあるかを確認し,カーソルの上下左右にフロートウィンドウを出し分けます.

Vim の構文ハイライトでクリスマスツリー🎄を飾ってメリクリする

Vim Advent Calendar 2018 の24日目の記事です.昨日は Kaoriya さんのVim に VOICEROID で喋らせたでした.

もうすぐクリスマスなので,クリスマスツリーを飾りたいと思います.ただ飾るだけだと Vim のネタにならないので,CrystalWastvim-gfm-sytnax, vim-github-actions といったファイルタイププラグインをつくった経験を活かし,構文ハイライトを使って Vim の中でクリスマスツリーを飾っていきたいと思います.

Vim の構文ハイライトについての情報は下記の help ドキュメントを読んでいただければ,ある程度網羅的な情報が手に入るので,実際に何かのファイルタイプを追加するプラグイン(ファイルタイププラグイン)を実装する時は,まずざっと一通りドキュメントを眺めることをおすすめします.

この記事はまだファイルタイプを書いたことが無い人を対象とします.網羅的な情報ではなく,ハイライトをつけていく過程を実例とともにコードで説明して,ファイルタイププラグインを実装する大まかな手順を把握していただくのを目的として進めていきます.今回はインデントについては時間の都合(や題材の都合)で省略します.

今回使ったコードはすべてこのリポジトリにあります.

github.com

ファイルタイププラグインとは

本題に入る前にファイルタイププラグインを知らない方向けにざっくりとした説明をしておきます.

特定の拡張子のファイルを開いたときなど,set filetype=... のようにファイルタイプをセットしたときにそのファイルタイプの構文ハイライトやインデント設定などを行ってくれるプラグインをファイルタイププラグインと呼びます(:help filetype-plugin). Vimcvim をはじめ,デフォルトで多くのファイルタイプをサポートしていますが,自分で新しいファイルタイプを足すこともできます.有名どころだと typescript-vimvim-toml など無数にあります.

また,vim-css3-syntaxvim-jsx-pretty, vim-gfm-syntax のように,自前でファイルタイプを定義するのではなく,既存のハイライトに独自のハイライトを足すようなプラグインもあります.

下記のような場合にファイルタイププラグインを自作する必要があります

  1. Vim が公式で対応していない言語のコードをハイライトしたい(e.g. 自作言語,自作設定ファイル)
  2. 既存のファイルタイププラグインではうまく動かない(or メンテされていない)などの事情で,自分でつくりたい
  3. アプリのログを読むときに重要な箇所をハイライトしたい

0. まずは対象の仕様を把握する

ファイルタイププラグインを書き始める前に,まずは対象を把握します.特定の言語のファイルタイププラグインをつくる場合は,まずはその言語の構文の仕様を眺めたりします.例えば vim-wasm をつくる際には WebAssembly の Text Format の仕様を読んだりしました.

今回はクリスマスなので,下記のクリスマスツリーをハイライト(飾り付け)していきたいと思います.

asciiart.eu の Laura T さんの作品をベースにしてつくってみました.

        *       ☆           *
   *            .o      *      *
     *         .o.'.
              .'.'o'.       *
        *    o'.*.'.o.
  *         .'.o.'.'.*.   *    *
      *    .*.'.o.'.o.'.     *
*         o'.'.'.'*'.'.'.        *
      *  .'.'*'.'o'.'.'*'o  *
  *           [_____]           *
        *      \___/      *       *

          Merry Christmas!

ツリーのてっぺんには☆が飾られており,o* で装飾された木だと思ってください.周りの * はクリスマスなので雪が降っています.このままでも綺麗ですが(?),色を付けて飾りつけていきましょう.

新しいファイルタイプとして christmastree を定義することにします.このファイルタイプが定義されている時は Vim はバッファにクリスマスツリーが描かれているとしてハイライトするようにこれから実装していきます.

1. リポジトリとファイルタイプの認識

まずは Vim プラグインリポジトリを用意します.ディレクトリを runtimepath に直接追加する(.vimrc 内で set rtp+=/path/to/vim-syntax-christmas-tree)か,お好みのプラグインマネージャでロードするプラグインのリストに追加してください.

mkdir vim-syntax-christmas-tree && cd vim-syntax-christmas-tree
git init .

ディレクトリ構成はこのようになっています. christmas-tree.txt は上記のアスキーアートが描かれたテキストファイルです.

vim-syntax-christmas-tree/
├── christmas-tree.txt
├── ftdetect/
│   └── christmastree.vim
├── syntax/

まずはファイルタイプを認識させます.Vimftdetect/*.vim の中身を起動時にロードしてくれるので,ftdetect/christmastree.vim をつくり,この中にファイルタイプを認識するための :autocmd を書きます.

autocmd BufNewFile,BufReadPost christmas-tree.txt setlocal filetype=christmastree

今回は christmas-tree.txt という固定のファイル名のファイルを読んだときのみ,ファイルタイプ christmastree をセットするようにしています.実際は TypeScript なら *.ts のように拡張子でパターンを指定することが多いです.

ここで一旦 Vim を新たに開き,christmas-tree.txt を開いて :set ft で今のファイルタイプが christmastree になっているのを確認します.

2. 構文ハイライトファイルをつくる

Vim は構文ハイライトファイルとして syntax/{filetype}.vimsyntax/{filetype}/*.vim を探すようになっているので,syntax/christmastree.vim をつくります. まだ何もハイライトしない空の状態です.

if exists("b:current_syntax")
    finish
endif

" ここにハイライト処理の実装を書いていく

let b:current_syntax = "christmastree"

変数 b:current_syntax はそのバッファをハイライトしている構文の名前です.すでにハイライト済みかどうかはその変数があるかどうかでチェックし,まだハイライトされていなければハイライト処理を行うという構造になっています.ハイライトを行った後は,すでにハイライトしたということが分かるように,b:current_syntax を定義しておきます.

3. はじめてのハイライト: :syn match

まずはてっぺんの☆をハイライトしてみます.

これは単に という文字列を色をつけてハイライトすれば良さそうです.

syn match christmastreeStar "☆"

hi def link christmastreeStar Identifier

:syn match {ハイライトグループ} {パターン} を使っています.これにより,指定したパターン(正規表現)にマッチする文字列を指定したハイライトグループでハイライトできます.

ここでは christmastreeStar というハイライトグループをつくることにし,"☆" というパターン(正規表現)にマッチする箇所とそのハイライトグループを紐づけます.この時点ではハイライトグループを紐づけただけなのでまだ色はつきません.ハイライトグループ名は小文字始まりのファイルタイプ名をプレフィックスとしてつけます.これはハイライトグループ名が一意でないといけない(他とかぶると上書きされてしまう)ためです.

次の行の hi def link {ハイライトグループ} {別のハイライトグループ} という行で,christmastreeStarIdentifier というハイライトグループに紐づけています.

Vim はデフォルトでいくつかのハイライトグループを定義しています.例えば (Normal: 通常のテキスト,Visual: ビジュアルモード選択 など).また,習慣として,同じ構文を同じグループにまとめたものが推奨されるハイライトグループ(Identifier: 変数名,Statement: 文 など)として明記されています.いずれも :help :hi で一覧を確認できるので,ざっと見ておいたほうが良いです.カラースキームプラグインはこれらの推奨ハイライトグループやデフォルトのハイライトグループに対して色を指定することで好きな色合いを実現しています.(カラースキームについては,もしよければフルスクラッチからさいきょうの Vim カラースキームをつくろう!を参照してみてください)

Vim:hi と打つと今のハイライトグループとそれに指定されている色がリストで表示されるので,そこで実際の色を確認できます.

:hi def link はデフォルトでハイライトグループを別のハイライトグループにリンクします.なので,ここでは christmastreeStar というハイライトグループにマッチするテキストは Identifier と同じ色でハイライトされます.

実際にプログラミング言語のハイライトを書く場合は,対象の構文が何にあたるかでリンクするハイライトグループを決めます.例えば if 文なら Statement,関数定義なら Function など.今回は残念ながら(?)クリスマスツリーに適した推奨ハイライトグループが無いため,適当に手元のカラースキームで合いそうな色を選択しました.

ちなみに

syn match Identifier "☆"

のように新しいハイライトグループを定義せず直接共通のハイライトグループを指定することもできますが,カラースキームがハイライト色を上書きできなくなるためやめておいたほうが良いです.:hi def はすでにハイライトがリンクされているときに何もしないので,カラースキーム側で先に :hi link してやることで,ハイライト色を上書きできるようになります.

実際に Vim で開いて確認してみます.:term ++close vim christmas-tree.txt とすることで,Vim の中で直接 Vim を開いて最新のハイライトを確認できます.

f:id:rhysd:20181223221759p:plain

やりました! がハイライトされていることが分かります.

ちなみに本来は短い特定の文字列は :syn keyword でキーワードとしてハイライトするのが一般的ですが,:syn keyword'iskeyword' に指定されている文字でないとハイライトしてくれないので, には使えず :syn match を使いました.

4. 葉をハイライトする: :syn region

次にツリーの葉を緑でハイライトしてみます.先程の :syn match を使っても良いですが,今回は :syn region を使ってハイライトします.周りには雪が舞っていてそれらは葉に含めたくないので,行ごとにハイライトするのが良さそうです.

syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ oneline

hi def link christmastreeLeaves String

新たなハイライトグループ christmastreeLeaves を定義しています. syn region はハイライトするテキストの始め(start=)と終わり(end=)によってハイライトする範囲を指定できます.始まりと終わりがダブルクォートな文字列リテラル"...")や,{...} によるブロック構文などは :syn region を使うことが多いです. ここでは,木は .o で始まり,.o で終わっていることに目をつけて,空白の後に .o が続く場合を始めとし,空白または改行に続く .o を終端とします. 最後に,葉は行ごとにハイライトすると決めたので,oneline オプションを指定しておきます.これによって,ハイライトが次の行に継続してハイライトされることがなくなります.

実際に Vim で開いて確認してみます.

f:id:rhysd:20181223221829p:plain

無事,意図通り緑色(私のローカルのカラースキームでは文字列が緑なので String を指定している)でハイライトされています.少し木っぽくなってきた気がします.

5. ハイライトをネストさせる: contained, containedin=, contains=

飾り * をハイライトしてみます. のとき同様に * にマッチするハイライトを新しいハイライトグループ christmastreeGlitter で指定し,

syn match christmastreeGlitter /\*/

hi def link christmastreeGlitter Constant

実際に Vim で開いて確認してみます.

f:id:rhysd:20181223221901p:plain

雪のほうがハイライトされてしまいました.一方でハイライトしたほうの木の * のほうはハイライトされていません.これは,葉のハイライトのほうが優先されてしまっているためです. ここでやりたいことは,葉の中の * だけをハイライトするということです.

これはハイライトを「ネストさせる」ことで実現できます.Vim のハイライトは正規表現でマッチする領域を重ねた層のような構造になっており,contains=, containedin=, contained などの :syn のオプションでそれらを制御します.

今回の例では

syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ contains=christmastreeGlitter oneline keepend
syn match christmastreeGlitter /\*/ contained containedin=christmastreeLeaves

hi def link christmastreeLeaves String
hi def link christmastreeGlitter Constant

のようにすることで実現できます.

contained をつけたハイライトはトップレベルではハイライトされなくなり,後述の contains= でネストしてマッチした場合のみハイライトされるようになります.これによって,外の雪は白いままになります.

ハイライトをネストさせるには,別のハイライトを含む側に contains= で含むハイライトのグループ名をコンマ区切りで書き,含まれる側のハイライトに含むハイライトグループを containedin= で指定します. containedin= は無くても良いですが,あったほうが意図しないハイライトを防げて良いと思います.

実際に Vim で開いて確認してみます.

f:id:rhysd:20181223221920p:plain

無事,木の飾りの * だけに色をつけることができました.

実際のファイルタイププラグインでは,このようにハイライトをネストさせることで特定の構文のみに適用されるハイライトを定義します.例えば if 文が関数ブロックの中でしか書けないような言語であれば,関数ブロック全体にマッチするハイライトを定義し,その中にネストして if 文のハイライトをマッチさせるような実装が考えられます.

ちなみに,しれっと keepend というオプションを足していることに気付いたでしょうか?:syn region は同じ構文でネストしたハイライトをうまく扱う(例えば { { ... } } のようにネストしていても内側と外側にうまくハイライトマッチできる)ような挙動になっているのですが,これだと外側(christmastreeLeaves)のマッチするテキストの末尾で内側(christmastreeGlitter)が同時にマッチするとき,内側が優先されて外側のマッチが終了しなくなります.この挙動を変えるのが keepend で,内側のハイライトと外側のハイライトが同時に終了するようになります.

6. 複数のハイライトをまとめる

木の飾りは *o の2種類あります.それぞれに違う色を割り当てられたほうが華やかになるので,o* とは別のハイライトを割り当てます. ここで先程のセクションと同様にして o もハイライトしても良いですが,せっかくなので1つの「クラスタ」にまとめることにします.

複数のハイライトグループをひとまとめにする :syn cluster を使います.

syn cluster christmastreeGlitters contains=christmastreeGlitterSmall,christmastreeGlitterLarge
syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ contains=@christmastreeGlitters oneline keepend
syn match christmastreeGlitterLarge /o/ contained containedin=christmastreeLeaves
syn match christmastreeGlitterSmall /\*/ contained containedin=christmastreeLeaves

hi def link christmastreeLeaves String
hi def link christmastreeGlitterSmall Constant
hi def link christmastreeGlitterLarge Special

まずは *christmastreeGlitterSmallochristmastreeGlitterLarge として別のハイライトグループにし,それぞれに別のハイライト色を指定できるようにします. 次に :syn cluster {クラスタ名} contains={コンマ区切りのハイライトグループ} を使って @christmastreeGlitters をひとまとめにします. クラスタ:syn regioncontains= のように,コンマ区切りで複数のハイライトグループを指定する箇所で使えます.

実際に Vim で開いて確認してみます.

f:id:rhysd:20181223221944p:plain

今回の例では,まとめずに *o で別個にハイライトを指定するのに比べてあまり嬉しさが分かりませんが,数が増えたり,階層構造が深くなったりしたときに管理がかなり楽になります.

実際のファイルタイププラグインでは,例えば関数ブロックの中に含められるハイライトグループをひとまとめにして1つのクラスタにしたりといった管理をしているのを見ます.vim-wasmでは,WebAssembly のテキストフォーマットはS式で記述するので,(...) で囲まれた中のみでハイライトが効く(逆に括弧の外であるトップレベルではコメント以外ハイライトされない)ように,括弧の中だけでハイライトすべきハイライトグループを1つのクラスタにしています.

7. 完成

最後に忘れていた植木鉢を :syn match でハイライトさせて完成です.[___]\___/ に分けてマッチさせていますが,それぞれ同じ構文の一部として同じハイライトグループ名 christmastreeFlowerpot を指定しています. このように,1つのハイライトグループを複数の :syn で構成することもできます.

これでようやく一通り飾り終えることができました. syntax/christmastree.vim の全体像は下記の通りです.

if exists("b:current_syntax")
    finish
endif

syn cluster christmastreeGlitters contains=christmastreeGlitterSmall,christmastreeGlitterLarge
syn match christmastreeStar /☆/
syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ contains=@christmastreeGlitters oneline keepend
syn match christmastreeGlitterLarge /o/ contained containedin=christmastreeLeaves
syn match christmastreeGlitterSmall /\*/ contained containedin=christmastreeLeaves
syn match christmastreeFlowerpot /\[_\+\]/
syn match christmastreeFlowerpot /\\_\+\//

hi def link christmastreeStar Identifier
hi def link christmastreeLeaves String
hi def link christmastreeGlitterSmall Constant
hi def link christmastreeGlitterLarge Special
hi def link christmastreeFlowerpot PreProc

let b:current_syntax = "christmastree"

実際に Vim で開いて確認してみます.

f:id:rhysd:20181223222000p:plain

無事綺麗に飾りつけられました.お疲れ様でした.

8. さらにピカピカさせる

構文ハイライトだけでも綺麗ですが,ftplugin を使ってさらに華やかにしてみます.

Vim はファイルタイプをセットしたときに ftplugin/{filetype}.vim または ftplugin/{filetype}/*.vim を読みに行くため,そのファイルの中にコードを書いておけば,特定のファイルタイプをセットしたときに処理を行うことができます.

今回は ftplugin/christmastree.vim を作成し,構文ハイライトの b:current_syntax のように,もう ftplugin を読み終えたことを示す b:did_ftplugin をチェックする処理を最初に書きます.これにより,意図せず複数の ftplugin スクリプトが読まれてしまうことを防ぎます. こうすることで,例えば別の ftplugin を作成して既存の ftplugin を置き換えると言ったことができるようになります.

if exists("b:did_ftplugin")
    finish
endif
let b:did_ftplugin = 1

今回はタイマーを使って,一定時間ごとに飾りの色が変わるようにしてみました.

function! s:christmas_glitter_tick(timer) abort
    let small = get(b:, 'christmastree_glitter_small_colors', ['Constant', 'Statement', 'Special', 'Ignore'])
    let large = get(b:, 'christmastree_glitter_large_colors', ['Special', 'Keyword', 'Identifier', 'Normal', 'Statement', 'Constant', 'Ignore'])

    let idx = s:tick % len(small)
    execute 'hi link christmastreeGlitterSmall' small[idx]

    let idx = s:tick % len(large)
    execute 'hi link christmastreeGlitterLarge' large[idx]

    let s:tick += 1
endfunction

function! s:christmas_glitter_start() abort
    if exists('s:timer_id')
        return
    endif
    let s:tick = 1
    let s:timer_id = timer_start(1000, function('s:christmas_glitter_tick'), {'repeat': -1})
endfunction

function! s:christmas_glitter_stop() abort
    if !exists('s:timer_id')
        return
    endif
    call timer_stop(s:timer_id)
    unlet! s:timer_id
    unlet! s:tick
endfunction

if get(g:, 'christmastree_glitter_update', 1)
    call s:christmas_glitter_start()
    autocmd BufWipeout <buffer> call <SID>christmas_glitter_stop()
endif

command! -nargs=0 -bar -buffer ChristmasTreeTurnOn call <SID>christmas_glitter_start()
command! -nargs=0 -bar -buffer ChristmasTreeTurnOff call <SID>christmas_glitter_stop()

詳しくは説明しませんが,timer_start() を使って1秒毎に christmastreeGlitterSmallchristmastreeGlitterLarge のリンクするハイライトグループを書き換えることでハイライト色を変更しています.

JavaScriptsetInterval のようにタイマーは非同期に呼ばれるので,s:christmas_glitter_tick() が実行されるわずかな時間しかユーザの操作をブロックしません. なので,Vim の中にクリスマスツリーをピカピカ光らせながら普段のコーディングができます.

https://github.com/rhysd/ss/blob/master/vim-syntax-christmas-tree/demo.gif?raw=true

ftplugin/christmastree.vim はファイルタイプ christmastree がセットされるたびに毎回読まれるので,その中に直に処理を書けば良く, g:christmastree_glitter_update0 にセットされていないときのみタイマーを開始するようにしています.

また,手動でタイマーを止めたり開始したりできるように,:ChristmasTreeTurnOn:ChristmasTreeTurnOff も定義しています.-buffer 付きで定義しているので,そのバッファ内のみで使えるコマンドとして定義されます.

これでクリスマスの準備もバッチリですね!(?)

Vim に WebAssembly のテキストフォーマットのサポートを入れた

VimWebAssembly のテキストフォーマット (wast) の対応を入れ,同時に filetype=wast で使われるファイルのメンテナになりました.

Wasm のテキストフォーマットとは

WebAssembly にはバイナリ形式とテキスト形式の2つのフォーマットがあります.Wasm はネットワークを介して配布される前提のため,サイズの小さいバイナリ形式のほうが良いですが,デバッグなどで処理を追いたい人間にとってはテキストフォーマットも必要になるためです.

emscriptenデバッグオプションをつけてコンパイルすると,生成物 .wasm のほかにテキストフォーマットの .wast およびそのソースマップを生成してくれます.

こんな感じのS式です

f:id:rhysd:20180801085232p:plain:w424

contribution の流れ

0. Vim プラグインとして実装

まだブラウザが Wasm を初めてサポートし始めた頃,その頃の binaryen の出力を眺めるときにハイライトが無いと不便なので,まずはたたき台として vim-wasm をつくりました.

github.com

今年の6月に入って WebAssembly の仕様をざっくり眺めて vim.wasm をつくりました.再び emscripten の出力を見る機会があり,以前つくったものが大分間違っていたことが分かったので,仕様に従って vim-wasm をそれに従って大幅に修正しました.

1. Vim にプルリクを出す

WebAssembly は主要な各ブラウザが対応しており,ウェブ標準として定義されているので,テキストフォーマットの対応が Vim 本家に入っていると有意義だと考えました. vim-wasm が良い感じに .wast ファイルをサポートできていることが確認できたので,Vim に取り込んでもらうことを提案します.

Vimvim_dev にパッチを送るか,vim/vim にプルリクを作成することでレビューを依頼できます.通常,runtime/syntaxruntime/indent といった各 filetype 対応のファイルにはメンテナがそれぞれついているので,まずはそちらに依頼すべきですが,今回は新規追加なので直接 Bram にレビューしてもらいました.

github.com

2. master に取り込んでもらう

レビューで OK が出ると,Bram が master ブランチに取り込みます. 今回は runtime/ 以下のファイル追加なので,他の runtime/ 向けの変更もまとめて1つのパッチが作成され,master ブランチに追加されました.

github.com

3. filetype=wast のメンテナになりました

先程書いたように,各 filetype のサポートはそれぞれにメンテナがいます.

そんなわけで,僕も vim/runtime/{syntax,indent,ftplugin}/wast.vim のメンテナになりました.開発は引き続き vim-wasm のほうでやっていくので,何か問題を発見された際は,Vimリポジトリvim_dev ではなく,まずはvim-wasm のほうに issue やプルリクをつくっていただけると助かります.