じぶん advent calendar 2019, 13日目の記事です.
Vim 8.2 がリリースされました.
$ git log v8.2.0000 commit 98056533b96b6b5d8849641de93185dd7bcadc44 (tag: v8.2.0000) Author: Bram Moolenaar <Bram@vim.org> Date: Thu Dec 12 14:18:35 2019 +0100 Vim 8.2 release
8.1 も結構最近だったような気がしたけど,振り返ると長かった.
$ git log v8.1.0000 commit b1c9198afb7ff902588b45fbe44f0760a9f48375 (tag: v8.1.0000) Author: Bram Moolenaar <Bram@vim.org> Date: Thu May 17 17:04:55 2018 +0200 Vim 8.1 release Update version number and information. Fix a couple of tests.
この間 (v8 に入る頃から), vim は激動の時代を歩んだと言ってもいいかもしれません. 5年前には全く想定されなかった様々な機能が入りました.
- async /
+job
,+channel
(これはもうちょっと前) :Terminal
- popup
- audio (!!?)
- ...
いわゆるモダンさは一歩譲るが卓越した根本的な設計思想からくる操作性を 誇る,という立ち位置だった Vim も,その姿を徐々に変えつつあります.
折しも世間でエディタごとの言語-specific なツール一辺倒の時代が黄昏を迎え, Language Server Protocol が広がり始めたころでもあります.
そんな中,Bramが突如公開した謎のゲーム…
https://github.com/vim/killersheep
Silly game to show off the new features of Vim 8.2: * Popup windows with colors and mask * Text properties to highlight text * Sound
マジで音の間抜けさがやばいのでみんなやってみましょう.
ともかく実装が気になるので,新機能のチェックがてら覗いてみましょう.
plugin/killersheep.vim
普通に起動するだけ. call killersheep#Start(s:dir)
ですね.:echohl
の存在を初めて知った.
マジで音の間抜けさがやばいので plugin/
下のファイル再生だけでいいからしてみてほしい.
autoload/killersheep.vim
killersheep#Start(sounddir)
:
func killersheep#Start(sounddir) let s:dir = a:sounddir if !has('sound') if executable('afplay') let s:sound_cmd = 'afplay' let g:killersheep_sound_ext = '.mp3' " 中略: 再生に使えるコマンドと対応する拡張子の設定 endif endif if !s:did_init let s:did_init = 1 call s:Init() endif call s:Clear() call s:Intro() endfunc
順に見ていきましょう
func s:Init() hi def KillerCannon ctermbg=blue guibg=blue " 中略: 必要な highlight を定義 " 略: 環境に合わせて g:killersheep_sound_ext を設定 let g:killersheep_sound_ext = ".ogg" endfunc
ではあとは s:Clear
と s:Intro
を呼べばよろしい.
先に s:Intro
を見てみましょう.予備知識: #{}
(:h literal-Dict
) は key を
クオートしなくてよくて便利な辞書リテラルです (#{zero:0} == {'zero': 0}
).
これを作っているところだ:
func s:Intro() hi SheepTitle cterm=bold gui=bold hi introHL ctermbg=cyan guibg=cyan call prop_type_delete('sheepTitle') call prop_type_add('sheepTitle', #{highlight: 'SheepTitle'}) call prop_type_delete('introHL') call prop_type_add('introHL', #{highlight: 'introHL'}) let s:intro = popup_create([ \ #{text: ' The sheep are out to get you!', \ props: [#{col: 4, length: 29, type: 'sheepTitle'}]}, \ s:NoProp('In the game:'), \ #{text: ' h move cannon left', \ props: [#{col: 6, length: 1, type: 'sheepTitle'}]}, \ " 中略 \ ], #{ \ filter: function('s:IntroFilter'), \ callback: function('s:IntroClose'), \ border: [], \ padding: [], \ mapping: 0, \ drag: 1, \ close: 'button', \ }) if has('sound') || len(s:sound_cmd) let s:keep_playing = 1 call s:PlayMusic() endif call s:IntroHighlight(0) endfunc func s:NoProp(text) return #{text: a:text, props: []} endfunc
さてさて.まずは prop_type_add( {name} , {props})
:
{name}
という text property type を作る.{props}
は
というわけで,ハイライトだけ定義した(SheepTitle
: 太字,introHL
: 水色) textprop が作られた.
つぎ popup_create( {what}, {options})
.
what
を表示するポップアップをつくる.what
は次のどれか:- buffer number
- 文字列
- 文字列のリスト
- a list of text lines with text properties ←コレ!
- 引数の詳細は込み入っていて別のヘルプにかかれているが,この例では
#{text: "text", props: [list]
の形式で1行を表現する.各行について,props
ではその行のテキストに適応すべきプロパティを以下の要素で指定している(1行に複数プロパティを指定することもできる.col
: byte で数えた starting columnlength
: バイトで数えたテキストの長さtype
: text property type お名前.
- さらに第2引数
options
は dictionary で,様々なオプションがあるがここではfilter
: 打鍵されたキーについて呼ばれる関数callback
: ポップアップが閉じるときに呼ばれる関数drag
: マウスでドラッグできる(!?)border
/padding
: css と同じ雰囲気の数値指定.close
:"button"
では右上のX
クリックでポップアップが度汁.ほかclick
ではどこでもクリックで,none
はとくにそういうのなし.
buftype="popup"
なバッファを一つ作って,window-ID を返す(失敗時は0).
IntroFilter
はゲームを始めるところなので後で読みます.IntroClose
は s:Clear()
なので多分読み飛ばします.
func s:PlayMusic() if s:keep_playing let fname = s:dir .. '/music' .. g:killersheep_sound_ext if has('sound') let s:music_id = sound_playfile(fname, {x -> s:PlayMusic()}) elseif len(s:sound_cmd) let s:music_job = job_start(s:sound_cmd .. ' ' .. fname) " Detecting job exit is a bit slow, use a timer to loop. let s:music_timer = timer_start(14100, {x -> s:PlayMusic()}) endif endif endfunc
素直に音声を再生している(+sound がないときにタイマーでループしてるの悔しい).
sound_playfile
の第2引数はコールバックで,sound ID と status (0:成功,1:中断,2:再生後にエラー)を受け取る.
この場合は単にループ再生なので自身をもう一回呼び出しています.
も一つ
const s:introHL = [[4, 3], [8, 5], [14, 3], [18, 3], [22, 2], [25, 3], [29, 4]] let s:intro_timer = 0 func s:IntroHighlight(idx) let idx = a:idx if idx >= len(s:introHL) let idx = 0 endif let buf = winbufnr(s:intro) call prop_remove(#{type: 'introHL', bufnr: buf}, 1) call prop_add(1, s:introHL[idx][0], \ #{length: s:introHL[idx][1], bufnr: buf, type: 'introHL'}) let s:intro_timer = timer_start(300, { -> s:IntroHighlight(idx + 1)}) endfunc
ここで The sheep are out to get you! の cyan を動かしているわけだ.
s:introHL
にこの文面の単語ごとの開始地点と長さを記録しておいいて,- 今の text property での着色を消して,
- 次の単語を着色して,
timer_start
で制御し,idx
を単語数で wrap しながらループ
結構かんたんじゃん.(もりもり整備されたAPIの音がする…)
続きは明日
advent calendar, なんというか夜ふかしして長い記事を書く趣旨ではないと思うので,起動の準備ができたところで続きは 明日(かもう一寸先か)に読むことにします.