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 (=バッファのカーソルの位置) と行の文字列を比較するのも何か意味合いが違うなと感じたのでやっぱ無しで…