Humanity

Edit the world by your favorite way

最近の Vim で入った面白 Vim script 関数

:h eval.txt を眺めてたら結構色々関数追加されてるなーと思ったのでまとめてみた。 (追記: 結構古くからある関数も交じってた…) 基本的に詳細な仕様は書くつもりはありません (古くなるかもしれないし help 見た方が正確)。

  • wordcount()
    • g<C-g> を押すと現在のバッファのバイト数、文字数、単語数 (ビジュアルモードの時は選択されたバイト数、文字数、単語数も) を表示してくれますが、それを関数で取れるようにしたやつです
  • getcharsearch()
    • [tfTF] コマンド (例: f<char>) の情報を返してくれるやつです
    • どの char を押したか、前方検索 or 後方検索、[tT] か [fF] かを返してくれます
      • 要するに直前に [tfTF] コマンドのどれを使ったか、引数の char は何なのかの情報
  • eventhandler()
  • complete_check()
    • ニッチだけど補完系プラグインでは便利そう
    • 候補の取得に時間がかかった場合ユーザ入力があった場合に中断できる
  • getcompletion({pat}, {type} [, {filtered}])
    • set wildmode してればコマンドライン<Tab> を押せば補完が効くが、その補完の候補を取得できる
    • 例えば getcompletion('', 'shellcmd') で PATH が通った全ての実行ファイル名を取得できる (ちょっと時間かかる)
  • searchdecl({name} [, {global} [, {thisblock}]])
    • gdgD の関数版。ううーんできればカーソルの状態変えずに位置だけ返してほしかった…
  • getbufinfo([{expr}]) or getbufinfo([{dict}])
  • getwininfo([{winid}])
    • こんなんあったのか…!
    • :ls の結果を Dictionary で取れる…もう :ls をパースとかする必要なんてなかったんや…
    • (むしろなぜ今までなかったのか)
  • setbufline({expr}, {lnum}, {text})
    • なぜ今まで(ry)。前から Vimmer がほしいほしい言ってたやつ…
    • 別バッファの指定された行を追加/更新する関数
    • :terminal を実装する過程でひょっこり追加された記憶
  • Window ID 系
    • Window ID に関しては Vim 8.0 Advent Calendar 7 日目 ウィンドウ ID - Qiita
      • タブを跨いでも一意なのが便利
    • win_findbuf({bufnr})
      • bufnr のバッファを表示している Window ID のリストを返す
    • win_gotoid({expr})
      • 引数の window ID のウィンドウに移動する
      • execute tabnr 'tabn' | execute winnr 'wincmd w' とかやってたのが関数一つでできるようになった
    • win_id2tabwin({expr})
      • 引数の Window ID のタブ番号、ウィンドウ番号を返す
        • let [tabnr, winnr] = win_id2tabwin(winid)
  • index の代わりに N 文字目の N (以下文字インデックス) を受け取る文字列操作 (7.4 からあるものもあるけど)
    • (文字の定義はエンコーディング (encoding オプション) によって変わります)
    • strchars({expr} [, {skipcc}])
      • 文字数を返す
    • strcharpart({src}, {start} [, {len}])
      • 文字列のスライス (strpart()) の文字インデックス版
    • strgetchar({str}, {index})
      • 文字列の index (文字インデックス) の位置の文字の数値を返す
      • 文字列で取得したい場合は nr2char() で要変換
    • strwidth({expr})
      • 文字列の文字幅を返す
      • タブ文字の文字幅は1
    • strdisplaywidth({expr} [, {col}])
      • 基本 strwidth({expr}) と同じだが、タブ文字は tabstop, display オプションに影響される
  • タグファイル系
    • tagfiles()
    • taglist({expr} [, {filename}])
  • 実行ファイルパス取得
    • exepath('prog') で PATH 中の prog のフルパスを取得
  • changenr()
    • undotree().seq_last と同じ?
    • これいつから追加されたんだろう。もしかしたら結構古いかも
      • :helpgrep changenr したら version7.txt に載ってたので 7.0 で追加されたっぽいです

上のはちょっと便利系関数ですがこれはマジ便利なので知ってほしい系関数(語彙力)です。

  • Channel 系
    • ch_*()
  • Job 系
    • job_*()
  • Terminal 系
    • term_*()
  • 部分適用
    • function() で引数、self を束縛できるようになった
  • 小ネタ: Vim 8 からは関数名から Funcref を取得する時は function() よりも 新規追加された funcref() のが安全
    • function() は関数名の string で lookup しようとするので 同じ名前で再定義された場合は再定義後の function を見に行ってしまう
    • まぁ再定義とかあんまりないけど… (以下 2018/2/14 追記)
      • :functionclosure 属性を使う場合は頻繁に再定義されるケースがほとんどだと思います
      • 例えば以下のコードで外側の関数 (s:add()) が実行される度に内側の s:add1() は再定義されます
function! s:add(n)
  function! s:add1(m) closure
    return a:n + a:m
  endfunction
  return funcref('s:add1')
endfunction

例えば上記のコードで function() を使ってしまっていると、 以下のように期待と異なる結果が出力されてしまいます。

function! s:add(n)
  function! s:add1(m) closure
    return a:n + a:m
  endfunction

  " ここは funcref() を使うべき!
  return function('s:add1')
endfunction

let s:f = s:add(1)
let s:g = s:add(2)

" この時点で s:add1() が 2 + a:m に再定義されてしまっている。
" s:f は function() を使っているので "s:add1" という関数名で定義を探し出す。
" よって再定義後の関数を実行してしまう

" 1+1 = 2 のつもりが 3 が出力されてしまう
echo s:f(1)

funcref() ならば Funcref のリファレンスで関数の定義を探すため、このような事は起こりません。

おまけ: カーソル下の1文字を取得

  • getline()col('.')-1 (index) だとマルチバイト文字の場合にちぎれる
  • 以前までは normal! yl を使っていた
    • レジスタを退避したりしなければならなかったりして面倒

と思ってたけどこれでいいじゃんと思った。

echo matchstr(getline('.')[col('.')-1 :], '^.')

ちなみに Vim にはカーソルのバッファ中のバイトオフセットから行番号を取得する byte2line({byte}) という関数があるのだけど、なぜか byte2col({col}) という関数はない。 あれば正規表現使わず

echo nr2char(strgetchar(getline('.'), byte2col(wordcount().cursor_bytes)-1)

とかできるかなーとか思ったけど結局必要なかったので提案しようにも理由がなくなってしまったので決め手に欠ける…

set virtualedit=all とかして行末以降にカーソルがある状態を考慮してなかったのと、 col (=バッファのカーソルの位置) と行の文字列を比較するのも何か意味合いが違うなと感じたのでやっぱ無しで…

Vim script で Chrome Debugging Protocol を扱うライブラリを作りたい

github.com

タイトルの通りなのですが、Chrome Debugging Protocol (以下 CDP) を扱うためには、

  1. CDP は WebSocket ベースなので WebSocket を実装しなきゃならない
  2. バイト列をパースする際にヌルバイトを Vim では扱えない

となって悩んでいます。 ヌルバイトを扱う部分は Python/Ruby/Lua インターフェースを使ったり、外部コマンドでやったりと策がないわけではないのですが、 どうせなら Vim で扱う方法を考えてみようというわけです。

CDP を実装しようとしたきっかけ

Chrome を (CDP が許す限り) 意のままに操ることができるからです。 まだ CDP をざっくりとしか調べてないのであまり風呂敷を広げすぎるのもアレかと思うのですが、 大体こんな事ができるんじゃないかと思っています。 できるか分からない部分に関して一応 (多分) とか付けときます。

  • (多分) Chrome を画像ビューアとして使ったり?
  • (多分) ライブリロード的なことができたり?
  • なんと JS のデバッグもできる (本来の機能)

正直今回この記事を書こうと思ったモチベーションはこんな事ができるんだよっていう紹介ではなくて、 バイト列をパースする際にヌルバイトを扱えない問題に対する提案がメインです。 あと単純に自分が何に悩んでいるかを整理したいというのもあります。

WebSocket の実装

対象の Chrome は通常 localhost で動いていて、over SSL でもないので Vim 8 の channel で扱えます。 channel は ch_open() でソケット通信したり、job_start() で実行したコマンドの入出力が channel で扱えます。 ようはざっくり入出力を抽象化したオブジェクト (ハンドル) と考えればいいです。

最初のハンドシェーク部分に関してはまんま HTTP なので実装できましたが、 バイト列 (=ヌルバイトを含む文字列) を Vim で扱う際にできれば (if_python などの外部インターフェースを使わず) pure vim script で実装したい… そうなると今の channel の機能では扱えないことが分かりました (次の章で解説)。

ちなみに、先ほど channel を入出力を抽象化したオブジェクト (ハンドル) と言いましたが、そう言ってしまうにはまだ機能が少なくて、 例えばファイル/バッファも channel で扱えるようになって ch_read() 等で固定長読み込む事ができるようになれば、getline() (行単位で文字列を取得) だけじゃなく もっと自由度と抽象度の高いスクリプトが書けるようになると思います。

バイト列 (=ヌルバイトを含む文字列) を Vim で扱う

閑話休題

まず、Vim script の文字列ではヌルバイトを含む文字列を扱う事ができません。 扱えないといっても、 echo "hell\0o"hell になる (多言語の人から見たら信じられない仕様だと思いますw) ように Vim script の文字列の値として使えないだけで、バッファ上で扱うことはできます。 ヌルバイトを含んだバッファで :%!xxd とすると現在のバッファの内容が hex dump した内容に置き換わりますが、 そうするとヌルバイトもちゃんと xxd コマンドに渡されていることが分かります。 ちなみに xxd コマンドは Vim に標準添付されているので、一度バッファ上に内容を持ってしまえば job 機能から xxd を呼び出して hex dump した内容を持てば Vim script で扱うことが可能です。 つまり目標としては「WebSocket で受け取ったバイト列をバッファに読み込む」です。

しかし、調べてみると ch_open('localhost:12345') のように開いたチャンネル (ソケット) からは受け取り方法としてバッファを指定できないことが分かりました。 job 機能だったら job_start() の option に {'out_io': 'buffer', 'out_buf': bufnr} のようにバッファを指定できます。 しかし channel にはそれが無いのです。

しかし、さらに調べる内にある方法でできなくはないことが分かりました。 できなくはないけどがかなり辛いハックになりそうだということも分かりました (つらい)。

その方法とは、ch_logfile({fname} [, {mode}]) で書き込んだログファイルに現在 Vim で使用している channel が受け取ったメッセージの内容が書き込まれていたので、これをパースすればできなくはないということです。 しかしこの関数はそもそも引数から分かる通り、ロギングしたい channel を渡す設計ではなく Vim で使用している全 channel のログが {fname} にすべて書き込まれます。 なんでこんな設計になっているかというと、まぁ恐らくテスト用 (あるいはデバッグ用) に追加した関数だからだと思います。 それに現在ロギングしているかどうかも Vim script で判断できないので、この関数を使うと既存でロギングされていたファイルへは書き込まれなくなることが予想されます。

そんな感じで ch_logfile() はそもそもそんな用途に使うもんじゃねーよ感がビンビンですが、 ただ各行に channel の id と一緒に吐き出されるのでパースするのは不可能じゃなさそうです。 この channel id は ch_info() で取得することができます。 ただ複数行のメッセージが来た場合も考えて単純に各行取得したんじゃパースできないため、ちゃんとやるのも結構大変な印象です。

本来なら先ほど言ったように ch_open() の option でも job option みたいに {'out_io': 'buffer', 'out_buf': bufnr} みたいに渡せると バッファ経由でヌルバイト含むバイト列も扱えそうなのですが、何とかならないでしょうか…

[追記] CDP の実装途中のスクリプト

一応共有しておきます。

WIP: [Preview] Chrome Debugging Protocol in Vim script · GitHub

今回の記事関係ないけど channel 使った HTTP の実装は vital.vim に入れたい… ただ channel では https:// な URL にリクエスト送れないので、その場合は別の実装に fallback したり、 あと Promise で無理矢理書いてる所を Observable のライブラリ作ってそれで置き換えるとかやりたいです。 Vital.Async.Observable がほしいって話は前から言ってますが一向にやる気配がありませんね…?(ウッ)

あとバイト列を扱う Vital.Data.Blob もほしい。 ほしい物がありすぎてパンク気味です…(実装力不足が悩ましい…)

Unicode の文字情報を開く Vim プラグイン作った

ふと必要になったので作りました。

github.com

動作には open-browser.vim が必要です。

github.com

なにこれ

Vim から FileFormat.Info の指定された文字のページをブラウザで開くプラグインです。

えっどういうこと?

Vim でファイルを開いている時にふと見慣れない文字に出くわすことがあると思います(絵文字とか)。 Vimga コマンドで Unicode のコードポイント情報は見れますが、他の情報が見たい場合もあります。 例えば

  • コードポイントについてる名前
  • サポートされた Unicode のバージョン

等です。そういう時便利なのが FileFormat.Info というサイトです。

例えば 💩 (U+1F4A9) という文字のページを見ると 'PILE OF POO' という名前で「Unicode 6.0.0 (October 2010)」でサポートされた事が分かります。 あと â (U+00E2) という文字LATIN SMALL LETTER A (U+0061)CIRCUMFLEX ACCENT (U+0302) の合成済み文字であるといったことが分かったりします。Unicode奥が深い…

どういう風につかうの

" カーソル下の文字のページを開きます
:OpenBrowserUnicode

" http://www.fileformat.info/info/unicode/char/21ba/ を開きます
:OpenBrowserUnicode U+21ba

" 上と同じ。"U+{16進数}" の形式でなければ引数で与えられた文字列の最初の文字のページを開きます
:OpenBrowserUnicode ↺

ぜひ

使ってみてください。