Vim 8.0 Advent Calendar

この記事は 2016 年 12 月に Qiita 上で行われた Vim 8.0 Advent Calendar を 1 つにまとめたものです。

目次


前書き

2016年9月、Vim の新しいメジャーバージョンである Vim 8.0 がリリースされました。 このアドベントカレンダーでは Vim 8.0 に含まれる新しい機能や変更などを紹介していきます。

注意:

  • 正確には Vim 7.4.0 以降に追加された機能になるので、リリースから時間が経っている機能もあります。
    • この Advent Calendar では便宜上、Vim 7.4 から Vim 8.0 の間に入った機能を Vim 8.0 の新機能として紹介します。ご了承ください。
  • 全ての新機能を紹介するものではありません。

Vim 8.0 Advent Calendar 1 日目 関数機能の強化

Vim 8.0 では Vim script の関数機能が強化されました。この記事では Partials とラムダを紹介します。

Partials

これまでの Vim script では function() 関数で関数参照(Funcref)を作成できました。これにより、関数を変数に入れ、直接呼び出すことができます。

let Foo = function('strftime')
echo Foo('%Y-%m-%d')
" => 2016-12-01
echo Foo('%Y-%m-%d', 1482634800)
" => 2016-12-25

これに加え、function() 関数に事前に引数を渡すことで、引数部分をバインドした関数参照を作れるようになりました。これを Partial と呼びます。

let Foo = function('strftime', ['%Y-%m-%d'])
echo Foo()
" => 2016-12-01
echo Foo(1482634800)
" => 2016-12-25

function() の第2引数以降に辞書を渡すことで、self をバインドすることも可能です。

function! Value() dict
  return self.value
endfunction

let dict = {'value': 10}
let Foo = function('Value', dict)

echo Foo()
" => 10

辞書から辞書関数の値を参照すると、自動的に辞書をバインドした関数参照が得られます。

let dict = {'value': 20}
function! dict.get_value() dict
  return self.value
endfunction

let Foo = dict.get_value

echo Foo()
" => 20

Partial から、バインドしている引数や辞書を得るには get() を使います。

let dict = {'value': 20}
function! AddN(n) dict
  return a:n + self.value
endfunction

let Add30 = function('AddN', [30], dict)
echo Add30()
" => 50
echo get(Add30, 'name')
" => AddN
echo string(get(Add30, 'func'))
" => function('AddN')
echo get(Add30, 'dict')
" => {'value': 20}
echo get(Add30, 'args')
" => [30]

ラムダ

これまでは関数を作るためには :function Ex コマンドを使って、複数行に渡ってコードを書く必要がありました。 そのため、sort() のような一部の関数を受け取る関数の実装が面倒でした。

function! MyCompare(i1, i2)
  return a:i1 - a:i2
endfunction
echo sort([3, 5, 4, 1, 2], 'MyCompare')
" => [1, 2, 3, 4, 5]

そこで新しくラムダ構文が追加されました。{args -> expr} という形式で、本文には式のみが書けます。

echo sort([3, 5, 4, 1, 2], { i1, i2 -> i1 - i2 })
" => [1, 2, 3, 4, 5]

また、map()filter() は今まで文字列で式を渡していましたが、関数参照を渡せるようになりました。 これらの関数は、配列の添字の {index} もしくは辞書のキー {key} と、各要素の値である {val} の 2 つを引数に取ります。間違いやすいので注意してください。

echo map(range(10), { index, val -> val * 2 })
" => [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

substitute() 関数も同様に {sub} に関数を渡せるようになりました。渡した関数は、引数としてマッチした対象を配列で受け取ります。配列の 0 番目はマッチした対象の全体、1 番目以降はパターン内のグループです。

echo substitute('char count sample', '\w\+', { m -> m[0] . '(' . len(m[0]) . ')' }, 'g')
" => char(4) count(5) sample(6)

クロージャ

関数内で更に関数を定義した際に、クロージャを作れるようになりました。これはどういうことかと言うと、ネストした関数の内側から、外側の関数のローカルスコープを参照できるということです。 クロージャを使うには :function Ex コマンドに closure フラグを渡します。

function! Counter()
  let c = 0
  function! s:count() closure
    let c += 1
    return c
  endfunction
  return funcref('s:count')
endfunction

let C1 = Counter()
let C2 = Counter()
echo C1()
" => 1
echo C2()
" => 1
echo C1()
" => 2
echo C2()
" => 2

関数のスコープに注意してください。関数内であろうとも、関数自体のスコープは関数ローカルにはなりません。グローバル関数を定義すれば、それはグローバル関数になります。恐らくこれは歴史的な理由によるものだと思われます。

もしくはラムダを使うことでもクロージャを作成できます。ラムダを使う場合、ラムダ内の式が静的にパースされて、外側の変数を参照していた場合にクロージャと判定されます。よって、例えば :executeeval() などで動的に参照されるだけの場合、クロージャにはならず外側の変数は参照できません。

function! OuterFunc(arg)
  let var = 0

  " これはクロージャになります。
  let closure = { -> var }

  " これはクロージャにはなりません。
  let not_closure = { -> eval('var') }

  " これは a:arg の参照によりクロージャになり、var も参照できます。
  let closure = { -> [a:arg, eval('var')][-1] }
endfunction

2種類の関数参照

関数参照は function() 関数で生成できますが、これによって生成される関数参照は名前参照になります。これはつまり、関数が同名で再定義された場合、参照先の関数も新しい関数になってしまうということです。 一方で先ほどのクロージャは何度も再定義されることが多く、問題になる場合があります。そこで、関数が上書きされても元の値を参照し続ける新しい参照を作るために、funcref() 関数が追加されました。 funcref() 関数の引数は function() 関数と全く同じで、Partial も作成可能です。違いは、funcref() 呼び出し時点での関数自体への参照が得られる点です。これにより参照先の関数が後で上書きされても、元の関数を参照し続けることができます。 注意点として、組み込み関数に対しては使えません。組み込み関数の関数参照を作る場合には function() を使う必要があります。

Vim 8.0 Advent Calendar 2 日目 チャンネル

Vim 8.0 では、外部リソースとのやりとりを行う機能としてチャンネルが追加されました。 本記事では、チャンネルの基本的な使い方として、ソケット通信を行う方法について簡単に説明します。 詳細については Vim 付属の help を参照してください。

チャンネルを使う

この例では、ローカルホストの HTTP サーバに対して ch_sendraw() 関数を使ってリクエストを送り、結果をハンドラで受け取って表示しています。

" リモートからのレスポンスがあった際に呼ばれるハンドラ関数を定義します
function! s:handle(ch, msg) abort
  " レスポンスを表示します。実際にはタイミング次第ではレスポンスが分割される可能性もあり得ます
  echo a:msg
  " ch_close() 関数でチャンネルを閉じることができます
  " リモートから切断された場合は自動的に閉じられます
  if ch_status(a:ch) !=# 'closed'
    call ch_close(a:ch)
  endif
  echo ch_status(s:ch)
  " => closed
endfunction

" チャンネルを開く際に渡すオプションを用意します
" ここではモードを "raw"、コールバックに先ほど定義したハンドラ関数の参照を指定しています
let s:options = {'mode': 'raw', 'callback': function('s:handle')}

" ch_open() でチャンネルを開きます
let s:ch = ch_open('localhost:80', s:options)
" ここで s:ch 変数は channel 型の値になります。channel 型は、チャンネル機能のために新しく追加された型です
echo ch_status(s:ch)
" => open
" 生の HTTP を叩きます
call ch_sendraw(s:ch, "GET / HTTP/1.0\r\n\r\n")

ここで、s:handle() 関数は非同期に呼び出されます。つまり、サーバからのレスポンスがあるまでの間、ユーザーは編集を続けることができます。 ただし、マルチスレッドではない点に注意してください。s:handle() が呼び出され、実行されている間はユーザーは Vim の操作ができません。重い処理はしないように注意が必要です。

チャンネルのモード

先ほどの例では raw モードでチャンネルを使用しました。チャンネルには他にも多くのメッセージの送信/受信の方法があります。

チャンネルで行う通信には 4 つのモードがあります。モードによって Vim はメッセージを解釈し、コールバックをメッセージ単位で呼び出してくれたり、メッセージのエンコード/デコードを行ってくれます。

  • JSON JSON 単位でメッセージをやりとりします。 メッセージには JSONVim のオブジェクトに変換したものが渡されます。

  • JS JSON に似ていますが、JavaScript のオブジェクトを使ってメッセージをやりとりします。 オブジェクトのキーの "" が省略されたり、配列に空の要素が許可されたりなどの違いがあります。

  • NL 行単位でメッセージをやりとりします。 メッセージには末尾の改行を取り除いたものが渡されます。

  • RAW Vim はメッセージを解釈しません。そのままのデータが使われます。

その他のメッセージの読み書きの方法

例では ch_sendraw() と、ハンドラを使った読み書きを行いました。他にもいくつか紹介します。

ch_sendexpr() ch_sendraw()

チャンネルに対してメッセージを送ります。 JSON モードや JS モードの際は ch_sendexpr() を使い、Vim のオブジェクトを渡すことでエンコードされた値が送られます。 NL モードや RAW モードの際は ch_sendraw() を使い、文字列を渡すことで生のデータを送れます。

ch_read() ch_readraw()

チャンネルのバッファにあるメッセージを読みます。読みとったメッセージはバッファから取り除かれます。また、ハンドラに処理されたメッセージもバッファからすでにないため、読めません。 JSON モードや JS モードの際は ch_read() を使い、デコードされた値が得られます。 NL モードや RAW モードの際は ch_readraw() を使い、生のデータを得られます。NL モードの場合は、改行単位でメッセージを読み取ります。

ch_evalexpr() ch_evalraw()

チャンネルに対してメッセージを送り、そのレスポンスを待って、レスポンスを返します。send と read を一度にやるものです。使い分けについても前述のものと同じです。

JSON/JS NL/RAW
送信 ch_sendexpr() ch_sendraw()
受信 ch_read() ch_readraw()
送受信 ch_evalexpr() ch_evalraw()

チャンネルには他にもまだまだ機能やオプションがあります。詳細は :help channel を参照してみてください。

Vim 8.0 Advent Calendar 3 日目 ジョブ

ジョブ機能を使うことで、外部プロセスを非同期で実行することができます。

ジョブを使う

この例では、ジョブを使って外部コマンド git grep -n word を実行し、結果を 1 行ずつ非同期で処理し、quickfix に追加しています。

function! s:handler(ch, msg) abort
  caddexpr a:msg
  cwindow
endfunction

call setqflist([])
let s:job = job_start(
\   ['git', 'grep', '-n', 'word'],
\   {'out_cb': function('s:handler')})

このように、ジョブを使うことで外部プロセスをバックグラウンドで実行し、チャンネルとコールバック関数を使うことで結果を非同期に処理できます。処理に時間のかかるソースコードのチェック処理などを裏で実行し、結果が出たら表示するといったことが可能になります。

ジョブのオプション

ジョブで指定できるオプションについて簡単に紹介します。ここで紹介しているものが全てではありません。詳細については help を参照してください。

let job_options = {}
モード

ジョブと接続される、標準入力、標準出力、標準エラー出力の各チャンネルのモードを指定します。デフォルトは nl で、改行を 1 つのメッセージとします。

" 標準入力のモードです。
let job_options.in_mode = 'nl'
" 標準出力のモードです。
let job_options.out_mode = 'nl'
" 標準エラー出力のモードです。
let job_options.err_mode = 'nl'
標準入出力の接続先

標準入出力についての細かい指定が行えます。

" 標準入力を使いません。
let job_options.in_io = 'null'

" 標準入力をチャンネルに接続します。デフォルトです。
let job_options.in_io = 'pipe'

" 標準入力をファイルから読み込みます。
let job_options.in_io = 'file'
" 標準入力をファイルから読み込む際のファイルへのパスです。
let job_options.in_name = '/path/file'

" 標準入力をバッファから読み込みます。
let job_options.in_io = 'buffer'
" 標準入力をバッファから読み取る際の読み取るバッファのバッファ番号です。
let job_options.in_buf = 1
" 標準入力をバッファから読み取る際のバッファの読み取り範囲の先頭行です。デフォルトは 1 です。
let job_options.in_top = 1
" 標準入力をバッファから読み取る際のバッファの読み取り範囲の最終行です。デフォルトはバッファの最終行です。
let job_options.in_bot = 9999  " 注意: 最終行にしたい場合はこの値は指定してはいけません。


" 標準出力を使いません。
let job_options.out_io = 'null'

" 標準出力をチャンネルに接続します。デフォルトです。
let job_options.out_io = 'pipe'

" 標準出力をファイルに出力します。
let job_options.out_io = 'file'
" 標準出力をファイルに出力する際のファイルへのパスです。
let job_options.out_name = '/path/file'

" 標準出力をバッファに出力します。
let job_options.out_io = 'buffer'
" 標準出力をバッファに出力する際のバッファ番号です。
" 指定がない場合や、存在しないバッファだった場合は、新しくバッファが作成されます。
let job_options.out_buf = 1
" 出力先がバッファの場合に 0 を指定すると、出力先バッファの 'modifiable' オプションをオフにします。
" 出力は行われますが、ユーザーはバッファを変更できなくなります。
let job_options.out_modifiable = 0
" 出力先がバッファの場合に 1 を指定すると、新しく作られたバッファの 1 行目にメッセージを出力します。
" メッセージは "Reading from channel output..." のようなものです。
let job_options.out_msg = 1

標準エラー出力は、標準出力のオプションのキーの outerr に変えたものが同じように用意されています。

コールバック

ジョブ側で何かが起きた際に呼び出される関数を指定します。指定しない場合は特に何も呼び出されません。

" 標準出力もしくは標準エラー出力から何か読み出せるようになった際に呼び出されます。
" 下記 2 つとは併用できません。
let job_options.callback = { ch, msg => [] }
" 標準出力から何か読み出せるようになった際に呼び出されます。
let job_options.out_cb = { ch, msg => [] }
" 標準エラー出力から何か読み出せるようになった際に呼び出されます。
let job_options.err_cb = { ch, msg => [] }
" チャンネルが閉じられた際に呼び出されます。
let job_options.close_cb = { ch => [] }
" ジョブ(外部プロセス)が終了した際に呼び出されます。
let job_options.exit_cb = { job, exit_status => [] }
その他
" ch_evalexpr() などでデータを読み取る際のタイムアウト時間(ミリ秒)です。
let job_options.timeout = 2000
" 標準出力を読み取る際のタイムアウト時間(ミリ秒)です。"timeout" を上書きします。
let job_options.out_timeout = 2000
" 標準エラー出力を読み取る際のタイムアウト時間(ミリ秒)です。"timeout" を上書きします。
let job_options.err_timeout = 2000

" Vim 終了時に、ジョブに対して job_stop() を呼び出します。
" デフォルトは 'term' で、Vim 終了時にジョブを停止します。
" 空文字列を指定すると、何もしません。
let job_options.stoponexit = 'term'

ジョブを制御する

ジョブを停止する

job_stop() 関数でジョブを終了させることができます。実際にはシグナルを送信したりします。 実際に何が起きるかは OS 依存です。

" ジョブを停止します。
" 第2引数には他に 'hup' 'quit' 'int' 'kill' や、シグナルの数値などが指定できます。
" 省略時は 'term' になります。
call job_stop(job, 'term')
ジョブの状態や情報を得る

job_status() 関数や job_info() 関数で、ジョブの状態や情報を取得できます。

echo job_status(job)
" => 'run' (ジョブが実行中の場合)
" => 'fail' (ジョブの開始に失敗した場合)
" => 'dead' (ジョブの実行が終了している場合)

" ジョブに紐付けられたチャンネルです。
let ch = job_getchannel(job)

" 様々な情報を辞書で取得します。
echo job_info(job)
" 'status'     job_status() の戻り値と同じです。
" 'channel'    job_getchannel() の戻り値と同じです。
" 'exitval'    終了コードです。'status' が 'dead' の場合のみ参照できます。
" 'exit_cb'    exit_cb オプションに指定された関数参照の値です。
" 'stoponexit' stoponexit オプションの値です。

Vim 8.0 Advent Calendar 4 日目 JSON サポート

チャンネルやジョブが追加されたのに合わせて、外部と JSON でのやり取りを行うことを想定して、JSON サポートが追加されました。

エンコード/デコードする

json_encode()json_decode() を使うことで、Vim の内部データと JSON 文字列を相互に変換できます。

let obj = {'users': [{'name': 'thinca', 'lang': 'vim'}]}
let json = json_encode(obj)
echo json
" => {"users":[{"lang":"vim","name":"thinca"}]}
echo json_decode(json)
" => {'users': [{'lang': 'vim', 'name': 'thinca'}]}

追加された値

Vim script には、true/false などの bool 値や、null などの値は存在しませんでした。 このままだと JSON と相互に変換するのに支障が出るため、新しくこれらを表す値が追加されました。

  • v:false
  • v:true
  • v:null
  • v:none

これらによって、bool 値や null を含む JSON も正しく相互変換されます。

let json = '{"is_vimmer":true,"has_free_time":false,"future":null}'
let obj = json_decode(json)
echo obj
" => {'future': v:null, 'is_vimmer': v:true, 'has_free_time': v:false}
echo json_encode(obj)
" => {"future":null,"is_vimmer":true,"has_free_time":false}

js_encode() js_decode()

チャンネルには JSON モードの他に JS モードがありました。これらに対応する js_encode() js_decode() があります。これらは JavaScript のオブジェクトのような形式を扱います。

let js = '{vimmers:["thinca",,],}'
let obj = js_decode(js)
echo obj
" => {'vimmers': ['thinca', v:none]}
echo js_encode(obj)
" => {vimmers:["thinca",,]}
  • オブジェクトのキーは必要がなければダブルクォートで囲われません。
  • 末尾カンマを許容します。
  • 配列内の空の要素(連続するカンマ)を許容し、この場合は v:none が使われます。

かなり特殊な値になるので、通常は JSON を使えばよいでしょう。

Vim 8.0 Advent Calendar 5 日目 タイマー

Vim 8.0 は新しくタイマー機能が追加されました。これにより、指定時間後に関数を呼び出すことができます。

タイマーを開始する

以下の例では 1 秒毎に関数を呼び出し、その度にカウントダウンを行い、最後に BOMB! と表示して終了します。

let dict = {'count': 10}
function! dict.countdown(timer) abort
  let self.count -= 1
  if self.count
    echo self.count
  else
    echo 'BOMB!'
    call timer_stop(a:timer)
  endif
endfunction

let timer = timer_start(1000, dict.countdown, {'repeat': -1})

タイマーが起動した後、タイマーによって関数が実行されている間以外は、ユーザーは編集を続けることができます。 例によって Vim はシングルスレッドですので、タイマーによって Vim script が実行されている間はユーザーは操作ができません。

関数の解説

timer_start({time}, {callback}, [, {options}])

タイマーを開始します。{time} ミリ秒後に {callback} 関数を呼び出します。 関数はタイマー ID を返します。この ID を使ってタイマーの操作ができます。また、{callback} 関数も引数にこの ID を受け取ります。 {options} には辞書でオプションを渡せます。今のところ有効なオプションは以下のものです。

  • "repeat" {callback} を繰り返し呼び出す回数を指定します。 正数を指定すると、{time} ミリ秒毎に指定した回数だけ {callback} が呼び出されます。 -1 を指定すると、制限なく呼び出され続けます。 指定しなかった場合は 1 回だけ呼び出されます。
timer_stop({timer})

指定したタイマーを停止します。{callback} 関数は呼び出されなくなります。

timer_pause({timer}, {paused})

タイマーを一時停止したり再開したりします。{paused} が TRUE の場合は一時停止、FALSE の場合は再開になります。

timer_info([{timer}])

タイマーの情報を返します。{timer} 引数を渡すと指定したタイマーの情報を、引数を省略した場合は全てのタイマーの情報を配列で返します。 情報は辞書で、ID や残り時間、呼び出される関数など一通りの情報が得られます。

timer_stopall()

タイマーは一歩間違えると暴発し、一切の操作ができなくなるような事態も起き得ます。timer_stopall() を呼び出すことで、全てのタイマーを停止することができます。

Vim 8.0 Advent Calendar 6 日目 パッケージ

Vim のパッケージ機能を使うことで、簡単なプラグインの管理を行うことができます。

パッケージとは

まず、パッケージ機能におけるパッケージとはどんなものかについて説明します。 1 つのパッケージは、複数のプラグインを含んでいます。また、プラグインはそれぞれ、Vim 起動時に読み込まれるか、あとから指定して読み込まれるかに分けられます。 パッケージは以下のようなディレクトリ構造になっています。

package/
|- start/
|  |- plugin1/
|  |- plugin2/
|  `- plugin3/
`- opt/
   |- plugin4/
   |- plugin5/
   `- plugin6/

start/ ディレクトリ以下にあるものが Vim 起動時に読み込まれるプラグインで、opt/ ディレクトリ以下にあるものがあとから指定して読み込まれるプラグインです。 plugin1 plugin2 などがプラグイン名になります。この名前は後から読み込む際に指定する名前になります。

パッケージを配置する

パッケージは、'packpath' オプションで指定されたディレクトリの中の pack/ ディレクトリ内から探されます。 'packpath' の初期値は 'runtimepath' の初期値と同じです。

具体例を挙げて見ていきます。 ~/.vim などが 'packpath' の 1 つとして登録されています。 この中の pack/ ディレクトリ内の、パッケージ名のディレクトリにパッケージを配置します。 つまり、パッケージ名を pack1pack2 とすると、以下のようになります。

~/.vim/
|- pack/
|  |- pack1/
|  |  |- start/
|  |  `- opt/
|  |- pack2/
|  |  |- start/
|  |  `- opt/
:  :

このようにパッケージ、およびその中のプラグインを配置しておくことで、Vim は起動時、vimrc ロード後のプラグイン読み込み前に、各パッケージの start/ ディレクトリ内のプラグイン'runtimepath' に自動的に追加し、その後プラグインを読み込みます。

オプショナルなプラグインのロード

opt/ 以下のプラグインは、:packadd Ex コマンドを使うことでロードできます。

:packadd plugin4

プラグイン名(=プラグインディレクトリ名)を指定することで、未ロードだった場合はロードされます。プラグイン'runtimepath' に追加され、プラグイン内の plugin/**/*.vim ファイルがロードされます。

また、vimrc 内からロードする場合は :packadd! を使います。! を付けると、'runtimepath' への追加のみが行われ、ロードはスキップされます。vimrc 内の場合、その後で別途ロード処理があるため、その場ではロードしないようにこちらを使用します。

パッケージの用途について

パッケージは Vim に標準で入った簡易プラグイン管理システムです。標準であることが強みでしょう。あまり多くのプラグインを使っていない人などは、この機能で十分な人もいるでしょう。 また、パッケージは見た限りだと、パッケージ単位でのプラグインの配布なども想定されていそうです。ディストリビューションなどでの配布や、関連したプラグインをまとめたものをパッケージにする用途が考えられます。

一方で、パッケージ機能はロード周りの世話はしてくれますが、昨今のプラグインマネージャプラグインが行ってくれるような、プラグイン自体のインストールや更新は行ってくれません。このレベルでプラグインを管理したい人は、やはりプラグインマネージャプラグインを利用するのが良いと思います。

Vim 8.0 Advent Calendar 7 日目 ウィンドウ ID

Windows ID を使うことで、特定のウィンドウの追跡が容易になります。

ウィンドウ ID がなかった時代

ウィンドウの指定はウィンドウ番号で行っていました。これはウィンドウの位置に対応して左上から順に振られます。

+-------------------------------+
|               |               |
|               |       2       |
|               |               |
|       1       |---------------|
|               |               |
|               |       3       |
|               |               |
+-------------------------------+

ウィンドウへ移動したり、ウィンドウに紐付けられた変数にアクセスする際は、このウィンドウ番号を使っていました。 しかし、このウィンドウ番号はウィンドウを移動すると変わってしまいます。例えば上の例で、ウィンドウ番号 1 の場所で <C-w>L を行うと、以下のようになります(括弧内は元のウィンドウ番号です)。

+-------------------------------+
|               |               |
|    (2->)1     |               |
|               |               |
|---------------|    (1->)3     |
|               |               |
|    (3->)2     |               |
|               |               |
+-------------------------------+

これだと困る、ということで埋まれたのがウィンドウ ID です。

ウィンドウ ID とは

ウィンドウ ID は全てのウィンドウに振られる ID です。 ウィンドウ番号と違い、ウィンドウを移動しても変わりません。

ウィンドウ ID の取得
" 現在アクティブなウィンドウのウィンドウ ID を取得します
let win_id = win_getid()
" 現在タブページからウィンドウ番号を指定してウィンドウ ID を取得します
let win_id = win_getid(winnr)
" タブページとウィンドウ番号を指定してウィンドウ ID を取得します
let win_id = win_getid(winnr, tabnr)

" バッファ名やバッファ番号から最初に見付かったバッファが
" 表示されているウィンドウのウィンドウ ID を取得します
let win_id = bufwinid(buf)

" 指定したバッファ番号のバッファを表示している
" ウィンドウのウィンドウ ID を配列で全て取得します
let win_ids = win_findbuf(bufnr)
ウィンドウ ID の使用
" 現在のタブページからウィンドウ ID のウィンドウを探してウィンドウ番号を返します
" 見付からなかった場合は 0 を返します
let winnr = win_id2win(win_id)
" ウィンドウ ID のウィンドウを探して、
" そのタブページ番号とウィンドウ番号を要素 2 の配列で返します
" 見付からなかった場合は [0, 0] を返します
let [tabnr, winnr] = win_id2tabwin(win_id)

" 指定のウィンドウ ID のウィンドウに移動します。成功したら TRUE を返します
let succeed = win_gotoid(win_id)

ウィンドウ番号とウィンドウ ID の併用

ウィンドウ ID は 1000 から振られます。これにより、1000 以上の場合はウィンドウ ID、1000 未満の場合はウィンドウ番号と仮定することで、既存の関数でウィンドウ番号を渡していた箇所でウィンドウ ID を渡せるようになっています。以下の関数で利用できます。

  • arglistid()
  • getcwd()
  • getloclist()
  • gettabwinvar()
  • haslocaldir()
  • setloclist()
  • settabwinvar()
  • winheight()
  • winwidth()

Vim 8.0 Advent Calendar 8 日目 defaults.vim

今回は新しく Vim に追加された defaults.vim という機構について解説します。

背景

Vim には今でも多くの新機能が追加され便利になっていっていますが、一方で互換性も重視しています。 中には、シンタックスハイライトなど明らかに便利であるにも関わらず、デフォルトでは有効になっていない機能があります。 新しく Vim を使い始めるユーザーにとって、互換性が理由で便利な機能がすぐに使えないことはあまり嬉しくはないでしょう。 そこで追加されたのが defaults.vim です。あくまで Vim 自体のデフォルト値は変えずに、ユーザーに便利な設定を提供します。

defaults.vim の読み込み

ユーザーの vimrc ファイルが存在しない場合、$VIMRUNTIME/defaults.vim ファイルは自動で読み込まれます。 読み込みたくない場合は、vimrc を読み込まない場合と同様に vim -u NONEvim -u NORC などを指定して Vim を起動します。 また、システム管理者が defaults.vim を読み込ませたくない場合、システムワイドの vimrc にて let g:skip_defaults_vim = 1 を行うことで defaults.vim の設定は適用されなくなります。

なお、この defaults.vim の追加によって、Vim は通常の起動でコンパチブルモードで起動することがなくなりました。このことは非互換の変更として help にも記載されています。

defaults.vim の内容

例えば以下のような設定があります。

  • Vi 互換モードをオフにする (set nocompatible)
  • filetype の検出やプラグインなどを有効にする
  • シンタックスハイライトを有効にする
  • 'nrformats' オプションの値から octal を取り除く
  • 間違えて <C-u> したときに undo できるようにする
  • :DiffOrig Ex コマンドの定義 (:help :DiffOrig)

詳細は実際にファイルの中身を見てみてください。:e $VIMRUNTIME/defaults.vim で開けます。

defaults.vim から始める vimrc

defaults.vim は最初に使い始める vimrc の叩き台としても機能します。 新しく自分用の vimrc を書き始めたいと思ったとき、defaults.vim の内容を継承したい場合は、defaults.vim ファイルを vimrc ファイルにコピーするか、もしくは vimrc の先頭に以下のように書きます。

source $VIMRUNTIME/defaults.vim

ここから、自分の気に入らない部分をちょっとずつ手を加えていくとよいでしょう。

逆に言うと、これらを行わずにユーザーの vimrc ファイルを作成すると、defaults.vim が読み込まれなくなることから、挙動が変わってしまうことに注意してください。

Vim 8.0 Advent Calendar 9 日目 2 進数のサポート

Vim 8.0 では 2 進数のサポートが強化されています。

2 進数の数値リテラル

0b もしくは 0B で始まる 2 進数リテラルが追加されました。

echo 0b1010 == 10
" => 1

<C-a> <C-x> の 2 進数サポート

'nrformats' オプションに指定できる値に bin が追加されました。 これはデフォルトで含まれているため、特に設定せずに利用可能です。 0b0B で始まる 2 進数の数値の上で <C-a><C-x> を実行すると、数値を増減できます。

0b1000
↓<C-x>
0b0111

printf() の 2 進数サポート

printf() 関数のフォーマットに %b が追加されました。 これを使うことで、数値を 2 進数の文字列に変換できます。

echo printf('0b%b', 10)
" => 0b1010

もちろんフラグにも対応しています。8 桁の 0 埋めパディングをするには以下のようにします。

echo printf('0b%08b', 10)
" => 0b00001010

Vim 8.0 Advent Calendar 10 日目 quickfix に追加された機能

Vim 8.0 では quickfix 周りに便利な機能が追加されました。

quickfix の各項目の場所で Ex コマンドを実行する

quickfix の各項目に対して Ex コマンドを実行する :cdo Ex コマンドが追加されました。この Ex コマンドを使うことで、quickfix に対する柔軟な操作が可能になります。

例えば、プロジェクトの中から単語 foo を探し、それらを全て bar に書き換えるには以下のようにします。

" 単語 foo を探します。結果は quickfix に入ります。
:vimgrep /\<foo\>/ **/*
" 検索結果の各行にて、置換を行い、バッファを保存します。
:cdo s/\<foo\>/bar/g | update

quickfix 内の各ファイルで Ex コマンドを実行する

上記の例において、検索結果の各ファイルについて複数の結果のデータがある場合に、各ファイルについて何か処理がしたい場合があるかもしれません。この場合に :cdo を使ってしまうと、各ファイルにて結果のデータの数だけ処理が実行されてしまいます。 こういった場合のために、cfdo Ex コマンドも追加されています。こちらの Ex コマンドは、quickfix に存在している各ファイルについて繰り返しが行えます。 各ファイルが開かれた際のカーソル位置は、ファイル内の最初の quickfix の項目の位置になります。

quickfix の一番下にスクロールする

Vim の非同期機能が強化されたため、今後は quickfix の内容が非同期で更新されることもあるでしょう。そういった場合に、常に quickfix の一番下の内容が見たい場合もあります。 新しく追加された :cbottom Ex コマンドを実行すると、quickfix ウィンドウへ移動しなくても、quickfix ウィンドウの内容を一番下までスクロールさせることができます。

quickfix の履歴を参照する

quickfix の内容は、最新の 10 個まで履歴が保存されていて、以前の内容に戻すことができます。詳細は :help quickfix-error-lists に記載されています。 この機能は、以前までは実際に :colder などを実行して前に戻ってみないと古いものが存在するかどうかがわかりませんでした。 そこで追加されたのが :chistory Ex コマンドです。この Ex コマンドを実行すると、quickfix にどのような履歴があり、現在どれが参照されているのかが以下のように表示されます(以下は help からの抜粋です)。

  error list 1 of 3; 43 errors
> error list 2 of 3; 0 errors
  error list 3 of 3; 15 errors

ロケーションリスト版

上記の Ex コマンド :cdo :cfdo :cbottom :chistory はそれぞれ、ロケーションリスト版として :ldo :lfdo :lbottom :lhistory が用意されています。

Vim 8.0 Advent Calendar 11 日目 タイムスタンプで管理されるようになった viminfo ファイル

今回は、ユーザーの作業が記録されている viminfo ファイルについてです。

viminfo ファイルの概要

viminfo ファイルは、ユーザーが行った様々な操作を記録しておくファイルです。例えば、レジスタの内容、コマンドラインの履歴、検索文字列の履歴や、ジャンプリストなど、様々な情報が記録されます。これらをファイルに記録することで、次回 Vim を使った際にも、前回の履歴を引き継いで使うことができます。 viminfo ファイルは、基本的には起動時に読み込まれ、終了時にファイルに書き出されます。

viminfo ファイルのマージ

1 つの viminfo ファイルを複数の Vim のセッションで使った場合、viminfo ファイルはマージされます。

どのようにマージが起きるか、例を挙げます。 同時に 2 つの Vim を立ち上げて、それぞれ A B とします。起動時には viminfo は空で、どちらも最初は履歴がありません。 この時、

  1. A で Ex コマンド :echo 1 を実行
  2. B で Ex コマンド :echo 2 を実行
  3. A で Ex コマンド :echo 3 を実行
  4. B で Ex コマンド :echo 4 を実行
  5. B を終了
  6. A を終了

とします。

5 の時点で、B の履歴が viminfo ファイルに書き込まれ、viminfo ファイルには新しいものが上に来るように以下のような順番で記録されます(実際のファイルの形式とは異なります)。

:echo 2
:echo 4

次に 6 で A が終了する際、viminfo ファイルに更新があることを Vim が検出すると、一旦新しくなった viminfo を読み込みます。続いて A の Vim のセッションで記録された履歴を追記し、viminfo ファイルに書き出します。以下のようになります。

:echo 2
:echo 4
:echo 1
:echo 3

このように、コマンドラインヒストリがマージされます。

発生する問題

上記の例で、1 つ問題が発生します。ユーザーは複数の Vim を行き来し、:echo 1 から :echo 4 まで順番に実行したのにも関わらず、履歴の順番はぐちゃぐちゃになってしまっています。 これは特に Vim を長時間起動していた場合、つい先ほど実行したコマンドが履歴の奥深くに潜ってしまうことを意味します。例だと、ユーザーが最後に実行したのは :echo 4 ですが、これは履歴の一番下から 3 番目に来てしまっています。 最近実行したものは、履歴の中でも最近に出てきてくれた方が嬉しいでしょう。

タイムスタンプを使った新しい viminfo ファイル

そこで Vim 8.0 では、コマンドなどの履歴を保存する際に、それらが実行された時間のタイムスタンプも一緒に保存するようになりました。 これによって履歴は読み込まれる時にタイムスタンプ順でソートされ、ユーザーが実行した順番で履歴を参照することができます。 以下のものがタイムスタンプで管理されるようになりました。

Vim 8.0 Advent Calendar 12 日目 連番の生成

Vim で連番と言えば今まででも、マクロを使う方法や Vim script を活用する方法などがありましたが、より手軽な方法が追加されました。

g<C-a> g<C-x> コマンド

今まではノーマルモードにて <C-a> <C-x> を実行することで、カーソル位置の数値を増減できましたが、ビジュアルモード中でも同様の操作が可能になりました。

加えて、連番を作り出すための g<C-a> g<C-x> がビジュアルモードのコマンドに追加されました。

使用例

例えば以下のようなバッファがあった場合:

1
1
1
1
1

2 行目以降をビジュアルモードで選択して、g<C-a> を実行すると、以下のようになります。

1
2
3
4
5

連番を作ることができました。2 行目以降を選択する点に注意してください。

これは見付かった数値に対して、見付かった順に [見付かった回数×count] 分、数値を足します。 例えば 2g<C-a> を実行すると、以下のようになります。

1
3
5
7
9

この操作は行指向で、選択範囲の各行で最初に見付かった数値のみを増減させます。また、数値が見付からなかった行はスキップされます。

1. これは 1 つ目の項目です。
1. これは 2 つ目の項目であり、
   折り返しが含まれています。
1. この項目にも折り返しが含まれて
   います。3 つ目の項目です。
1. 4 つ目の項目です。

このテキストに対して、<C-v> で 2 行目から最終行まで矩形選択を行い、g<C-a> を実行すると、以下のようになります。

1. これは 1 つ目の項目です。
2. これは 2 つ目の項目であり、
   折り返しが含まれています。
3. この項目にも折り返しが含まれて
   います。3 つ目の項目です。
4. 4 つ目の項目です。

数値が存在しない行は飛ばされます。 ここで V で行単位で選択をしてしまうと、3 つ目の項目の中身が含まれてしまってうまくいかなくなります。

1. これは 1 つ目の項目です。
2. これは 2 つ目の項目であり、
   折り返しが含まれています。
3. この項目にも折り返しが含まれて
   います。6 つ目の項目です。
5. 4 つ目の項目です。

'nrformats' オプションに alpha が含まれていれば、アルファベットの増減も可能です。

a. ...
a. ...
a. ...
a. ...
a. ...

a. ...
b. ...
c. ...
d. ...
e. ...

g<C-x> は数値を減らす点以外は g<C-a> と同様に動作します。

Vim 8.0 Advent Calendar 13 日目 undo を分割せずにカーソルを移動

普段はあまり気にしないかもしれませんが、undo の単位は重要です。この undo について、新しい機能が追加されました。

挿入モードでの操作の分割

挿入モードでの操作中に、<Left> などでカーソル位置を動かすと、undo 情報や . でのリピートが壊れてしまいます。 例えば以下のキーマッピングがあった場合、

inoremap ( ()<Left>

挿入モードで foo(bar と入力することで foo(bar) が入力できます。

入力: afoo(bar<Esc>
↓
foo(bar)

しかし、ここで u コマンドで undo を行うとfoo() となってしまいます。また、. コマンドでリピートを実行すると、bar が挿入されます。

foo(bar)
↓
入力: u
↓
foo()
foo(bar)
↓
入力: j.
↓
foo(bar)
bar

これは <Left> により入力情報が途切れてしまうためです。つまり、<Left> を行った時点で、挿入モードを一旦抜けて入り直したのと同じことになります。

挿入モードでの <C-g>U コマンド

この問題を避けるために、<C-g>U コマンドが追加されました。これはカーソルを動かすコマンドの直前で使用します。 このコマンドを使うために、キーマッピングを以下のように書き換えます。

inoremap ( ()<C-g>U<Left>

最初の例と同様に foo(bar を入力し、その後それぞれ u コマンドや . コマンドを実行すると、入力された foo(bar) 全体が 1 つの入力として動作します。

foo(bar)
↓
入力: u
↓
foo(bar)
↓
入力: j.
↓
foo(bar)
foo(bar)

この <C-g>U コマンドは、カーソルが行を跨がない移動をする場合のみ有効です。

<C-g>U コマンドは便利ですが、手で入力するにはちょっと複雑です。今回の例のように、キーマッピングで使うとよいでしょう。

Vim 8.0 Advent Calendar 14 日目 新しいオプション その 1

新オプション紹介その 1 です。追加されたものの中でも便利なオプションについて解説します。

'breakindent' (真偽値) 'breakindentopt' (カンマ区切り文字列)

オンにすると、折り返して表示される行がインデントされて表示されます。 つまり以下のようになります。左が 'breakindent' がオフ、右がオンです。また、'showbreak' オプションの値に > が設定されています。4 行目の折り返しに注目してください。 f:id:thinca:20161230184910p:plain

また、'breakindentopt' オプションで細かい挙動を制御できます。このオプションはカンマ区切りの文字列で、以下の要素を指定できます。

  • min:{n}
    • 深いインデントが短すぎる幅で折り返されないように、1 行の最小の幅を指定します。未指定の場合は 20 になります。
  • shift:{n}
    • 折り返された位置をずらします。正数で右に、負数で左にずらします。未指定の場合は 0 で、ずらしません。
  • sbr
    • インデントの左側に 'showbreak' を表示します。

例えば、shift:4,sbr を指定すると、以下のようになります。

f:id:thinca:20161230184911p:plain

'showbreak' である > 記号が行頭に移動し、折り返しのインデントが if のインデントに比べて右に 4 つずれています。

'fixendofline' (真偽値)

通常、Vim は保存時にファイルの末尾に必ず改行を入れます。これは POSIX にて、テキストは行の集合であり、行は必ず改行で終わるとされているからだと思われます。 このファイル末尾の改行の付与は、ファイル末尾に改行のないファイルを開いて編集し、保存した場合にも行われます。 これは特にチームで作業している場合に困る場合があります。また、一部の CSV の処理系など、ファイル末尾の改行の有無で意味が合わるファイルを扱う場合も困ります。

そこで 'fixendofline' オプションが追加されました。このオプションはデフォルトではオンで、保存時にファイル末尾に改行を追加します。 オフの場合、'endofline' オプションに従って改行を付与します。オンなら改行が付与され、オフなら改行は付与されません。 この 'endofline' オプションは、既存のファイルを開いた際にはファイルの末尾の改行を見て自動でオンオフされます。

つまりまとめると以下のようになります。

  • ファイル末尾の改行をいじって欲しくない場合は、set nofixendofline を vimrc ファイルに書きます。
  • ファイル末尾の改行の有無を操作したい場合は、上記に加えて、都度 'endofline' オプションの値を変更します。

'belloff' (カンマ区切り文字列)

Vim はエラーが発生した際にベルを鳴らします。これを無効化したい場合、以前は以下のような設定を書いていました。

set visualbell t_vb=

新しく追加された 'belloff' オプションを使うと、代わりに以下のように書けます。

set belloff=all

'belloff' オプションは、どんな時にベルを鳴らさないようにしたいのかが細かく指定できます。特定の場合のベルのみ無効にしたい!といったことも可能です。 どのような値が指定可能かは help を参照してください。

Vim 8.0 Advent Calendar 15 日目 新しいオプション その 2

新オプション紹介その 2 です。その 1 に比べると地味なオプション達を簡単に紹介します。それぞれ詳細は help を参照してください。

'renderoptions' (特殊形式文字列)

テキストレンダラの設定です。このオプションを設定することによって、Windows ではレンダリングDirectX を使えます。 また、DirectX に対して様々なオプションを設定できます。

'emoji' (真偽値)

オンにするとユニコード絵文字を全角とみなします。デフォルトはオフです。

'langremap' (真偽値)

元々 'langnoremap' オプションがありました。このオプションは真偽値のオプションであったため、これをオフにしようとすると以下のようになります。

set nolangnoremap

これは二重否定でわかりづらい、ということで追加されたのが 'langremap' オプションです。'langnoremap' オプションは今も互換性のために残されていて、この 2 つのオプションは常に逆の値を指すようになっています。

'signcolumn' (特定文字列)

sign の桁を表示するかどうかを設定します。デフォルトは auto で、sign が存在する場合のみ表示されます。 その他、yes no で常にオン/オフが可能です。

sign は本来の目的以外でも、行全体をハイライトするために hack 的にプラグインから使われる場合があり、このような時に no を設定することで余計な sign カラムを非表示にすることができます。

'tagcase' (特定文字列)

タグファイル内を検索する際の大文字小文字の区別する方法を指定します。 元々はこれは 'ignorecase' オプションの値に依存していましたが、'ignorecase'インタラクティブな検索などで使われることもあり、オンにしている人が多いかと思います。一方、タグファイル内の検索はプログラミング言語の識別子などが入っていることもあり、大文字小文字は区別して欲しい場合が多いです。そこで、それぞれ独立して設定できるようにするために 'tagcase' オプションが追加されました。 デフォルト値は followic で、互換性を保つために 'ignorecase' に追従します。常に大文字小文字を区別して欲しい場合は、match を設定します。他にもいくつか設定できる値があります。

'termguicolors' (真偽値)

オンにすると、ターミナル内でも GUI 用の 24 ビットカラーのカラースキームが使用できます。ただし、ISO-8613-3 互換のターミナルが必要です。 対応していないターミナルでオンにすると残念なことになるので注意してください。

'luadll' 'perldll' 'pythondll' 'pythonthreedll' 'rubydll' 'tcldll' (文字列)

Vim には様々な言語のインターフェースがあり、ビルド時にこれらを指定できます。 ダイナミックリンクも可能でしたが、これまでは、その dll のファイル名はビルド時に指定したものに固定でした。 しかし、タイナミックリンクである以上、ファイル名は環境によって変わる場合があります。そこで、オプションによって dll のファイル名を指定できるようになりました。 'pythonthreedll'Python 3 のためのものです。本来 'python3dll' としたかったようですが、オプション名に数値が使えないという制約が存在したため、このようになっています。

Vim 8.0 Advent Calendar 16 日目 新しい Ex コマンド

Vim 8.0 で利用できる新しい Ex コマンドのうち、まだ紹介していないものを紹介します。

:filter[!] {pat} {command}

{command} の出力のうち、{pat} で指定した正規表現にマッチする行だけを表示します。[!] を指定すると、逆にマッチしない行だけを表示します。 {pat}/foo/ のように / などの記号で囲われた形式です。ただし、パターンが記号などを含まない場合は / は省略できます。

以下に使用例を挙げます。

" マークを記録してあるファイルのうち、.txt で終わるものを表示します。
filter /\.txt$/ oldfiles
" 読み込まれた Vim script のうち、パスに vimrc を含むものを表示します。
filter vimrc scriptnames
" 開かれているバッファのうち、.vim を含むものを表示します。
filter /\.vim/ buffers
" キーマッピングのどこかに <C-r> を含むものを表示します。
execute "filter /\<C-r>/ map"
" 現在のバッファから、foo を含む行を表示します。
filter foo %print
" ↑の例は :global でも実現できます。
global/foo/print

最後に注意点として、この Ex コマンドは全ての出力をフィルタするわけではありません。例えば、:echo の結果はフィルタされません。

:keeppatterns {command}

検索履歴に手を加えずに {command} を実行します。 :substitute (:s///) や :global などの一部の Ex コマンドは、通常は Vim script 中で使った場合でもパターンが検索履歴に追加されてしまいます。これはプラグインの中などで使う場合に問題になります。 そこでこの :keeppatterns Ex コマンドを使って :keeppatterns global/.../ などのようにすることで、検索履歴が変更されてしまうのを防ぐことができます。

これは {command} の実行中はずっと手を加えないということではなく、直接指定した Ex コマンドが検索を使うものだった場合だけ有効です。つまり以下の場合は、検索履歴は変更されてしまいます。

" :execute を挟む
keeppatterns execute "s/\<CR>//ge"

" 関数経由
function! s:work() abort
  s/\e//ge
endfunction

keeppatterns call s:work()

:noswapfile {command}

{command} を実行します。このとき、新しいバッファが開かれた場合はスワップファイルを作成しません。 これはプラグインが仮想バッファを作成する際に便利です。

この Ex コマンドも :keeppatterns Ex コマンドと同様、バッファを開くコマンドを {command} に直接指定した場合のみ有効です。 ただし、:vertical Ex コマンドや :leftabove Ex コマンドなどの、ウィンドウを開く先を指定する修飾子コマンドは含まれていても問題ありません。

:clearjumps

現在のウィンドウのジャンプリストを空にします。

カーソルを大きく移動させるコマンドを実行したり、バッファを移動したりした場合、そのカーソルの移動はジャンプリストに記録され、あとから辿って移動することができます。 これは多くの場合便利ですが、例えばプラグインが仮想バッファを開いた際などに、戻れてしまうと不便な場合もあります。そういった場合にこの Ex コマンドが使えます。

:helpclose

現在のタブページにヘルプウィンドウがあれば、1 つだけ閉じます。 これは現在のウィンドウがヘルプウィンドウではない場合にも動作するので、離れた場所にあるヘルプウィンドウを閉じるのに便利です。 1 つもヘルプウィンドウがない場合は特にエラーにもならず、何も起きません。

Vim 8.0 Advent Calendar 17 日目 新しい関数 ~文字列操作編~

Vim 8.0 では新しく便利な組み込み関数が多数追加されています。今回はその中から、文字列操作に関連するものを紹介します。

matchstrpos({expr}, {pat}[, {start}[, {count}]])

Vim script には元々、指定した文字列から、正規表現にマッチした位置を取り出す match() 関数と、マッチした文字列を取り出す matchstr() という関数があります。 これら関数は便利ですが、マッチした位置とマッチした文字列両方が欲しい場合には少し問題があります。この場合、それぞれの関数を呼び出すことになるのですが、関数を 2 回呼び出すのは手間がかかる上に、同じ正規表現マッチを 2 回行うのはパフォーマンス的にも無駄です。

そこで、matchstrpos() 関数が追加されました。引数は match() 関数や matchstr() 関数と同じで、戻り値が違います。"マッチした文字列"、"マッチした先頭の位置"、"マッチした末尾の位置" の 3 要素の配列を返します。

echo matchstrpos('fizz buzz fizzbuzz', 'b\w\+')
" => ['buzz', 5, 9]

strcharpart({src}, {start}[, {len}])

Vim script には元々、文字列の一部を切り出す strpart() 関数がありますが、これはバイト単位で動作するため、マルチバイト文字に対して使うと文字列のバイトの途中で切り取られてしまうという問題がありました。 そこで strcharpart() が追加されました。これはバイト単位ではなく、文字単位で文字列の一部を切り取ります。

echo strcharpart('あいうえお', 1, 3)
" => いうえ

strgetchar({str}, {index})

文字列内の指定した index の文字の文字コードを取得します。これはバイトではなく文字単位で動作します。文字数はマルチバイトで数えられ、結果はマルチバイト文字の 1 文字の文字コードになります。

let code = strgetchar('あいうえお', 4)
echo printf('0x%x', code)
" => 0x304a
echo nr2char(code)
" => お

byteidxcomp({expr}, {nr})

Vim script には byteidx() という関数があります。これは、マルチバイト文字を考慮して、{expr} に与えた文字列の 0 オリジンで {nr} 番目の文字が、文字列内の何バイト目かを返します。{nr} が文字列の文字数と同じ場合は文字列全体のバイト数を返し、{nr} がそれより大きい場合は -1 を返します。

echo byteidx('あいうえお', 2)
" => 6
echo byteidx('あいうえお', 5)
" => 15
echo byteidx('あいうえお', 6)
" => -1

新しく追加された byteidxcomp() は、合成文字を個別にカウントします。

let s = 'e' . nr2char(0x301)
echo s
" => é
echo byteidx(s, 1)
" => 3   (合成文字を 1 文字とみなし、文字列が 1 文字であるため文字列全体のバイト数を返します)
echo byteidx(s, 2)
" => -1  (合成文字を 1 文字とみなし、{nr} が文字数より大きいため -1 を返します)
echo byteidxcomp(s, 1)
" => 1   (合成文字を別々の文字とみなし、"e" までをカウントして 1 を返します)
echo byteidxcomp(s, 2)
" => 3   (合成文字を別々の文字とみなし、文字列が 2 文字であるため文字列全体のバイト数を返します)

glob2regpat()

glob() 関数などで使われる、いわゆるワイルドカードなどが含まれるファイルパターンを正規表現に変換します。

echo glob2regpat('*.vim')

今のところ、ワイルドカード*** も、正規表現.* に変換されるようです。実際のワイルドカード*ディレクトリを辿らず、/ などのディレクトリ区切り文字にはマッチしないため、若干挙動が異なってしまう点に注意してください。

Vim 8.0 Advent Calendar 18 日目 新しい関数 ~情報取得編~

今回は新しく追加された関数の中から、情報を取得するものを中心に紹介します。

wordcount()

現在バッファの統計情報を辞書で取得します。 この情報は g<C-g> コマンドで表示できるものですが、表示だけだとスクリプトから扱うのが困難であるため、関数が追加されました。

辞書には以下の情報が含まれます。

キー 説明
bytes バッファ内のバイト数です。
chars バッファ内の文字数です。
words バッファ内の単語数です。
cursor_bytes カーソル位置より前のバイト数です。ビジュアルモードでない場合のみ存在します。
cursor_chars カーソル位置より前の文字数です。ビジュアルモードでない場合のみ存在します。
cursor_words カーソル位置より前の単語数です。ビジュアルモードでない場合のみ存在します。
visual_bytes ビジュアル選択領域内のバイト数です。ビジュアルモードの場合のみ存在します。
visual_chars ビジュアル選択領域内の文字数です。ビジュアルモードの場合のみ存在します。
visual_words ビジュアル選択領域内の単語数です。ビジュアルモードの場合のみ存在します。

getbufinfo([{expr}]) getbufinfo([{dict}])

バッファの情報を辞書の配列で取得します。引数を与えない場合、全てのバッファの情報を取得します。 配列の各要素の辞書は、以下のエントリーを持っています。

キー 説明
bufnr バッファ番号です。
changed バッファが変更されているなら('modified' がオンなら) TRUE になります。
changedtick バッファが変更された回数(b:changedtick の値)です。
hidden 隠れバッファであるなら('hidden' がオンなら) TRUE になります。
listed バッファがバッファリストに表示されるなら('buflisted' がオンなら) TRUE になります。
loaded バッファがロード済みなら TRUE になります。
name バッファ名(バッファのファイルのフルパス)です。
signs サインの情報のリストです。リストの各要素は辞書で、以下の要素を持ちます。
キー説明
idサインの ID
lnum行番号
nameサインの名前
variables バッファローカル変数を参照する辞書です。
windows バッファを表示しているウィンドウのウィンドウ ID のリストです。

引数を渡した場合は、取得したいバッファの条件を指定することで絞り込みができます。詳細は help を参照してみてください。

getwininfo([{winid}])

ウィンドウの情報を辞書の配列で取得します。引数を与えない場合、全てのタブページのウィンドウの情報を取得します。 配列の各要素の辞書は、以下のエントリーを持っています。

キー 説明
bufnr ウィンドウが開いているバッファのバッファ番号です。
height ウィンドウの高さです。
loclist このウィンドウがロケーションリストだった場合は 1 です。
quickfix このウィンドウが quickfix ウィンドウだった場合は 1 です。
tabnr ウィンドウがあるタブページのタブページ番号です。
variables ウィンドウローカル変数を参照する辞書です。
width ウィンドウの幅です。
winid ウィンドウ ID です。
winnr ウィンドウ番号です。

引数にウィンドウ ID を渡した場合は、指定したウィンドウ ID の情報のみを含む配列を取得できます。

gettabinfo([{arg}])

タブページの情報を辞書の配列で取得します。引数を与えない場合、全てのタブページのウィンドウの情報を取得します。 配列の各要素の辞書は、以下のエントリーを持っています。

キー 説明
tabnr タブページ番号です。
variables タブページローカル変数を参照する辞書です。
windows タブページで表示されているウィンドウのウィンドウ ID のリストです。

引数にタブページ番号を渡した場合は、指定したタブページ番号の情報のみを含む配列を取得できます。

getcharsearch() setcharsearch({dict})

文字検索の情報を取得、設定できます。文字検索とは、f F t T で行う、指定した文字やその手前に飛ぶ機能のことです。 文字検索の情報を持つ辞書を取得、および設定できます。この辞書は以下の要素を持ちます。

key 説明
char 検索文字です。空文字列にすると、文字検索を解除します。
forward 検索方向です。1 ならば前方、0 ならば後方です。
untill 検索の種類です。1 の場合は、文字の手前(tT)、0 の場合は文字自体(fF) の検索です。

getcmdwintype()

getcmdtype()コマンドラインウィンドウ版です。 q: q/ q?コマンドラインウィンドウを開いている時に、現在のコマンドラインウィンドウがどのタイプかを返します。戻り値は : / ? のいずれかで、コマンドラインウィンドウが開かれていない場合は空文字列を返します。

getcompletion({pat}, {type} [, {filtered}])

コマンドラインの補完の結果を取得できます。{type} は以下のうちのどれかです。

{type} 説明
augroup autocmd のグループ名です。
buffer バッファ名です。
behave :behave Ex コマンドの引数です。
color カラースキームです。
command Ex コマンドです。
compiler :compiler Ex コマンドの引数です。
cscope :cscope Ex コマンドの引数です。
dir ディレクトリ名です。
environment 環境変数です。
event autocmd のイベント名です。
expression Vim の式です。
file ファイル名とディレクトリ名です。
file_in_path 'path' にあるファイル名とディレクトリ名です。
filetype ファイルタイプの名前です。
function 関数名です。
help help の項目です。
highlight ハイライトグループです。
history :history Ex コマンドの引数です。
locale ロケールの名前(locale -a の出力)です。
mapping キーマッピングの名前です。
menu メニューです。
option オプションです。
shellcmd シェルコマンドです。
sign :sign Ex コマンドの引数です。
syntax syntax ファイルのファイル名です。
syntime :syntime Ex コマンドの引数です。
tag tags ファイルから読み取れるタグです。
tag_listfiles tag と同じです。
user ユーザー名です。
var Vim script の変数です。

{pat} で候補を絞り込めます。コマンドラインに入力されている文字列を渡します。 {filtered} に 1 を渡すと、'wildignore' オプションを結果に適用します。

arglistid([{winnr} [, {tabnr}]])

Vim には引数リストという機能があります。これはグローバルなものが 1 つあり、それとは別にウィンドウ毎にローカルなものが作成できます。 引数リストの使い方についてはここでは省略しますが、この関数はこの引数リストの ID を取得できます。 指定したウィンドウにローカルな引数リストがなく、グローバルなものが使用されている場合は 0 を返します。引数が無効だった場合は -1 を返します。

Vim 8.0 Advent Calendar 19 日目 新しい関数 ~特殊操作編~

関数紹介編の最後です。特殊な操作をするものや、その他雑多な関数を紹介します。

uniq({list} [, {func} [, {dict}]])

配列内の連続する同じ要素を削除します。 全体から重複を削除したい場合は事前に sort() 関数を使う必要があります。

let new_uniq_list = uniq(sort(copy(list)))

比較にはデフォルトで文字列表現を使います。{func}{dict} を与えることで、sort() 関数と同様に比較方法を指定することが可能です。

execute({command} [, {silent}])

Ex コマンド {command} を実行し、コマンドラインへの出力を結果として返します。 {command} は文字列か、文字列の配列です。

{silent}"" "silent" "silent!" のいずれかで、コマンドのプレフィックスのように使われます。デフォルトは "silent" です。

echo filter(split(execute('scriptnames'), "\n"), { i, line -> line =~# '^\s*1:' })
" =>   1: ~/.vim/vimrc

Ex コマンドの実行中に :redir Ex コマンドを使うことはできません。

matchaddpos({group}, {pos}[, {priority}[, {id}[, {dict}]]])

matchadd() 関数のようにウィンドウ内にハイライトを追加しますが、matchadd() 関数はパターンを指定するのに対し、matchaddpos() 関数は位置を指定します。 {pos} には位置のリストを指定します。位置は以下のうちのいずれかです。

説明
数値 行番号です。行全体を強調表示します。
数値を 1 つ持ったリスト 行番号です。行全体を強調表示します。
数値を 2 つ持ったリスト 行番号と、その行の桁です。単位はバイトで、指定した文字を強調表示します。
数値を 3 つ持ったリスト 行番号、桁、文字列長(バイト単位)です。

一度に指定できる位置は 8 個までです。

setfperm({fname}, {mode})

ファイルのパーミッションを設定します。{mode}getfperm() 関数の戻り値と同様のフォーマットである、"rwxrwxrwx" 形式で指定します。

systemlist({expr} [, {input}])

system() 関数と同様ですが、戻り値は行単位のリストになります。戻り値内の NULL 文字(\0)は改行文字に変換されます。 これにより、結果に NULL 文字が含まれてるコマンドの結果も取得できます。

perleval({expr})

if_perl を使って Perl の式を評価し、結果を Vimデータ形式に変換して返します。 これは pyeval() 関数や luaeval() 関数の Perl 版です。

echo perleval('{"foo" => "bar"}')
" => {'foo': 'bar'}

exepath({expr})

実行コマンドのフルパスを取得します。絶対パス相対パス$PATH の中に存在するファイルが実行ファイルだった場合、そのフルパスを返します。

echo exepath('git')
" => /usr/bin/git

isnan({expr})

{expr}NaN 値であるかを判定します。

echo isnan(0.0 / 0.0)
" => 1

reltimefloat({time})

reltime() 関数の経過時間の戻り値を秒数の Float 値に変換します。

let start = reltime()
sleep 1
echo reltimefloat(reltime(start))
" => 1.000287

以前から経過時間を文字列に変換する reltimestr() 関数がありましたが、こちらは文字列であり、かつ先頭に空白が含まれているため、表示以外の目的で使うには少々扱いづらいという問題がありました。reltimefloat() 関数を使うことで直接 Float 値が得られるため、平均の計算などがやりやすくなります。

Vim 8.0 Advent Calendar 20 日目 新しいイベント

Vim 8.0 では autocmd イベントも新しく追加されています。

TabNew

新しくタブページが開かれた際に発生します。例えば :tabnew Ex コマンドを使うと、以下の順番でイベントが発生します。

  1. WinLeave
  2. TabLeave
  3. WinNew
  4. WinEnter
  5. TabNew
  6. TabEnter

TabClosed

タブページが閉じられた際に発生します。例えば、:tabclose Ex コマンドでカレントタブページを閉じると、以下の順番でイベントが発生します。

  1. BufLeave
  2. WinLeave
  3. TabLeave
  4. TabClosed
  5. WinEnter
  6. TabEnter
  7. BufEnter

WinNew

新しいウィンドウが作成された際に発生します。Vim 起動時に開かれるウィンドウに対しては発生しません。

CmdUndefined

定義されていないユーザー定義コマンドを実行しようとした際に発生します。 パターンはコマンド名に対してマッチングが行われ、<amatch><afile> は実行しようとしたユーザー定義コマンド名に設定されます。しかし、<amatch> は正しく展開されないようなので、<afile> を使うのがよいでしょう。 イベントの実行中に存在しなかったコマンドを定義すれば、イベント終了後にコマンドが実行されます。

以下の例は、Foo で始まる未定義の Ex コマンドを実行すると、その場で自分自身のコマンド名を出力する Ex コマンドを定義します。

augroup example
  autocmd!
  autocmd CmdUndefined Foo* execute 'command! -nargs=*' expand('<afile>') 'echo' string(expand('<afile>'))
augroup END
FooBar
" => FooBar

OptionSet

オプションが設定された際に発生します。 パターンは、常に短縮していないオプション名に対してマッチングが行われ、<amatch> にはオプション名が設定されます。 また、以下の組み込み変数に設定されたオプションの値の情報が格納されます。

組み込み変数名 説明
v:option_old 変更前のオプションの値です。
v:option_new 変更後のオプションの値です。
v:option_type 設定された変数のスコープです。globallocal が入ります。

'key' オプションの場合は、セキュリティのためイベントは発生しません。

TextChanged

ノーマルモードでカレントバッファのテキストが変更された際に発生します。このとき、b:changedtick が更新されます。 このイベントはそれなりの頻度で発生します。重い処理を行う場合は十分注意すべきです。

TextChangedI

挿入モードでカレントバッファのテキストが変更された際に発生します。ただし、補完のポップアップメニューが表示されているときは発生しません。 このイベントは非常に頻繁に発生することに気を付けてください。重い処理を行うと、ユーザー体験が著しく損なわれます。

Vim 8.0 Advent Calendar 21 日目 新しい組み込み変数

今回は新しく追加された組み込み変数を紹介します。

タイプを表す定数

type() 関数を使うと、変数のタイプを得ることができます。ここで得られる値は数値で、各タイプに数値が割り当てられています。 ある変数が特定のタイプであるかどうかを判定したい場合、今までは以下のようにしていました。

" 以下の例では変数 var が文字列かどうかを判定しています。

" 文字列の type の値は 1 なので、これと比較して判定します。
if type(var) == 1
endif

" マジックナンバーを避けるため、以下のようにすることが多いです。
if type(var) == type('')
endif

この方法には、以下のような問題がありました。

  • 若干トリッキーで、慣れないと理解しづらいコードになります。
  • type() 関数を余計に呼ぶため、オーバーヘッドがあります。
  • 関数参照の場合は type(function('type')) のようになり、長い上に function() 関数に渡す関数名が人によってバラバラで統一感がありません。
  • 新しく追加された job 型などの値は気軽に生成できません。

そこで、type() 関数の戻り値を表す定数が新たに追加されました。以下の表が定数の一覧です。

定数 定数の値 型の値の例
数値 v:t_number 0 10
文字列 v:t_string 1 'foo'
関数参照 v:t_func 2 function('type')
リスト v:t_list 3 [0, 1, 2]
辞書 v:t_dict 4 {'one': 1}
浮動小数点数 v:t_float 5 1.23
真偽値 v:t_bool 6 v:true v:false
特殊値 v:t_none 7 v:null v:none
ジョブ v:t_job 8 job_start(cmd)
チャンネル v:t_channel 9 ch_open(host)

v:completed_item

補完された対象を表す変数です。 以前までは、CompleteDone イベントにより補完の完了を知ることはできましたが、どの候補が選択されたかを知ることができませんでした。新しく追加されたこの変数を参照することで、どの候補が選択されたのかわかります。 選択された候補は辞書です。詳しい構造については :help complete-items で説明されています。補完に失敗した場合は空の辞書になります。

v:hlsearch

検索による強調表示が行われているかを表す変数です。 検索のハイライトは 'hlsearch' オプションをオンにして検索を行うことで行われますが、:nohlsearch Ex コマンドを使うことで、一時的にハイライトを無効にできます。このとき 'hlsearch' オプションの値はそのままなので、ハイライトが行われているのか、:nohlsearch Ex コマンドで消されているのかが今まではわかりませんでした。 v:hlsearch 変数は、ハイライトが行われている時は 1、行われていない時は 0 になります。 また、値を書き換えることでハイライトの状態を変更できます。ただし、'hlsearch' オプションがオフの場合はハイライトを有効にできないため、1 を入れても値は 0 のままです。エラーも発生しません。 また、関数の呼び出しは、最後に使用された検索パターンを保存して呼び出し終了後にリストアします。つまり関数内でこの変数を書き換えても、関数の終了時に復元されてしまうので注意してください。

v:progpath

Vim を起動した際のコマンドを示す文字列です。つまり、Vim コマンド自身のコマンドライン引数の 0 番目です。 システムに複数の Vim がある場合に、どの Vim で起動されたかのヒントになります。Vim 内から別の Vim を起動して処理を行いたい場合などに便利です。

echo exepath(v:progpath)

コマンドが相対パスでカレントディレクトリが移動した場合や、$PATH が変更された場合など、確実に起動した Vim が得られるわけではない点に注意してください。

v:vim_did_enter

Vim が起動して、VimEnter イベントが発生する直前までは 0 です。VimEnter イベントが発生する直前に 1 になります。 似たような値に has('vim_starting') があります。こちらの値は逆で、起動中は 1、起動後は 0 になります。値が変わるタイミングは v:vim_did_enter と同じです。両者は完全に代替可能です。 ではなぜこの変数が追加されたのかと言うと、実はこれを追加した際、Bram さんは has('vim_starting') の存在を完全に忘れていました。あとで指摘された際、この変数の追加をリバートすることも検討したようです。 しかし、has() 関数は基本的に Vimコンパイル時に組み込まれている機能を調べるためのもので、一部の例外を除き Vim 実行中に値が変化しないこと、そのような慣習のため、Vim が起動中かどうかを調べる方法が has() 関数にあることはユーザーにとってわかりづらいことなどを Gary さんに指摘され、残すことになったようです。

Vim 8.0 Advent Calendar 22 日目 新しいスタイルのテスト

Vim 8.0 では、Vim 本体のテストのスタイルが新しくなりました。

新しいテストのサンプル

新しいスタイルのテストは Vim 本体のテストのために追加されたものですが、基本的に Vim script の機能であるため、プラグインのテストにも利用できます。 以下に、新しいスタイルで書かれた簡単なテストコードを示します。

" テスト対象の関数
function! Add(a, b) abort
  return a:a + a:b
endfunction

" --------------------------

function! Test_Add() abort
  call assert_equal(5, Add(2, 3))
endfunction

function! s:run_test() abort
  let v:errors = []

  call Test_Add()

  if empty(v:errors)
    echo 'Test Passed!'
  else
    echo 'Test Failed!'
    for error in v:errors
      echo error
    endfor
  endif
endfunction

call s:run_test()

実行すると、以下のようにテストをパスします。

Test Passed!

テストに失敗する例も示します。Test_Add() 関数を以下のように書き換えます。

function! Test_Add() abort
  call assert_equal(10, Add(2, 3))
endfunction

実行すると、以下のようにテストに失敗します。

Test Failed!
function <SNR>1_run_test[3]..Test_Add line 1: Expected 10 but got 5

テストの仕組み

以上の例から見て取れるのは 2 点です。

  • 値のチェックに使っている assert_equal() 関数
  • テストの結果のチェックに使っている v:errors 組み込み変数

新しいテストでは、これらを使ってテストを書きます。

仕組みは単純です。v:errors 組み込み変数は配列です。assert_ で始まるアサート系の関数を呼び出し、アサートに失敗すると、この v:errors に失敗のメッセージが追加されます。

例で行っているように、テスト開始前に v:errors を空にし、いくつかのアサート系の呼び出したあと、最後に v:errors の中身を確認することでテストを行います。

アサート系関数

追加されたアサート系の関数を紹介します。 ほとんどの関数は {msg} 引数を持っており、これを渡すことで v:errors に入るメッセージを指定できます。省略した場合は関数毎に用意されたメッセージが使用されます。

assert_equal({expected}, {actual} [, {msg}])

{actual}{expected} と等しい事をテストします。型の自動変換は行われません。

assert_notequal({expected}, {actual} [, {msg}])

{actual}{expected} と等しくない事をテストします。

assert_inrange({lower}, {upper}, {actual} [, {msg}])

{actual}{lower} 以上 {upper} 以下の数値である事をテストします。

assert_match({pattern}, {actual} [, {msg}])

{actual}正規表現 {pattern} にマッチする事をテストします。

assert_notmatch({pattern}, {actual} [, {msg}])

{actual}正規表現 {pattern} にマッチしない事をテストします。

assert_true({actual} [, {msg}])

{actual} が TRUE である事をテストします。ここでの TRUE は、非ゼロの数値か、v:true です。それ以外の型や値の場合は失敗します。

assert_false({actual} [, {msg}])

{actual} が FALSE である事をテストします。ここでの FALSE は、ゼロの数値か、v:false です。それ以外の型や値の場合は失敗します。

assert_exception({error} [, {msg}])

v:exception に文字列 {error} が含まれている事をテストします。

assert_fails({cmd} [, {error}])

{cmd} を実行した結果、エラーが発生する事をテストします。{error} が渡された場合、v:errmsg に格納されている発生したエラーメッセージに文字列 {error} が含まれている事をテストします。

その他のテスト用関数

assert 系以外で追加されたテストを補助する関数です。ただし、ほとんどの関数は Vim 本体のテストのためのものです。簡単に紹介します。

テスト用関数 説明
test_alloc_fail({id}, {countdown}, {repeat}) メモリの確保を強制的に失敗させます。
test_autochdir({expr}) 起動中に 'autochdir' を有効にします。
test_disable_char_avail() typeahead なしの状態でテストします。
test_garbagecollect_now() 直ちにメモリを解放します。
test_null_channel() null のチャンネルを返します。
test_null_dict() null の辞書を返します。
test_null_job() null の Job を返します。
test_null_list() null のリストを返します。
test_null_partial() null の部分適用関数を返します。
test_null_string() null の文字列を返します。
test_settime({expr}) Vim が使う内部時間を変更します。

Vim 8.0 Advent Calendar 23 日目 雑多な変更

今回は、今までの変更に収まらなかった細かい変更点について見ていきます。

GTK+ 3

GUI として、GTK+ 3 に対応しました。GTK+ 2 と同じように使えます。

+num64

Vim script の整数の型が 64 bit になりました。 利用可能な環境であれば 64 bit になることになっていますが、基本的には一般的なほぼ全ての環境では利用可能かと思います。 has('num64') を使うことで、64 bit が有効かどうかを判定できます。

if has('num64')
  echo 1000000000000000000
  " => 1000000000000000000
endif

これにより、32 bit による桁溢れなどを利用している一部のプラグインがうまく動作しなくなる可能性があります。

ユーザー定義コマンドでの新しい置き換えテキスト <mods>

Vim の Ex コマンドの中には、他の Ex コマンドに前置することで他のコマンドを修飾するものがあります。:aboveleft:hide などがそうで、これらはコマンド修飾子と呼ばれます。 これまでは、ユーザー定義コマンドに対してコマンド修飾子が指定されても、その存在を知ることはできませんでした。そこで追加されたのが <mods> です。

以下のように <mods> を使うことで、指定された修飾コマンドの存在を知ることができます。

command! ShowMods echo split(<q-mods>)

ShowMods
" => []
aboveleft ShowMods
" => ['aboveleft']
hide leftabove ShowMods
" => ['aboveleft', 'hide']

指定された順序に関係なく、決められた順序で <mods> に入ります。対応しているコマンド修飾子は以下です。

  • :aboveleft :leftabove
    • どちらも同じ意味のコマンドで、どちらが指定されても aboveleft が得られます。
  • :belowright :rightbelow
    • どちらも同じ意味のコマンドで、どちらが指定されても belowright が得られます。
  • :botright
  • :browse
  • :confirm
  • :hide
  • :keepalt
  • :keepjumps
  • :keepmarks
  • :keeppatterns
  • :lockmarks
  • :noswapfile
  • :silent
  • :tab
  • :topleft
  • :verbose
  • :vertical

また、以下のコマンド修飾子には対応していません。

型チェックの廃止

以前のバージョンの Vim では、変数に対して、元から入っている値の型に暗黙に変換不可能な型の値を代入しようとすると、エラーになっていました。

" Vim 7.4.1546 より前
let var = 10
let var = []
" => E706: 変数の型が一致しません: var

これは元々意図的にチェックが行われエラーになっていたのですが、静的型付き言語ならともかく、動的型付き言語である Vim script でこれを行ってもわずらわしいだけであったため、このチェックはなくなりました。

" Vim 7.4.1546 以降
let var = 10
let var = []
" => エラーなし

printf('%s') が全ての型を受け入れる

printf() 関数のフォーマット文字列である %s は、文字列を表示します。以前のバージョンの Vim では、文字列に暗黙に変換できない値を渡すとエラーになっていました。

" Vim 7.4.2220 より前
echo printf('%s', [1, 2, 3])
" => E730: リスト型を文字列として扱っています

しかしこれはあまり便利ではないため、文字列型でない場合は string() 関数を適用した結果が使用されるように変更されました。

" Vim 7.4.2220 以降
echo printf('%s', [1, 2, 3])
" => [1, 2, 3]

辞書のキーに空文字列を使える

以前のバージョンの Vim では、辞書のキーに空文字列が使えませんでした。

" Vim 7.4.1707 より前
echo {'': 10}
" => E713: 辞書型に空のキーを使うことはできません

しかし、空文字列を許可しない積極的な理由はなく、空文字列が使えた方が便利であるため、キーに空文字列が使えるようになりました。

" Vim 7.4.1707 以降
echo {'': 10}
" => {'': 10}

正規表現 \%C

新しく追加された正規表現のアトム \%C を使うと、合成文字をスキップすることができます。

let s = 'e' . nr2char(0x301)
echo s
" => é
echo s =~# 'e'
" => 0
echo s =~# 'e\%C'
" => 1

ハイライトグループ EndOfBuffer

新しく追加されたハイライトグループ EndOfBuffer は、ファイルの末尾以降に表示される ~ の文字がある行の部分のハイライトになります。標準では NonText と同じようにハイライトされます。 例えば以下のように背景色と前景色を同じにすることで、ファイル末尾以降を塗り潰す、といったことができます。

highlight EndOfBuffer ctermfg=DarkGray ctermbg=DarkGray guifg=DarkGray guibg=DarkGray

サポートが終了した環境

以下の環境や機能は、使っている人がほぼいない、大きくなった Vim が動作するのにそぐわないなどの理由により、Vim のコードを綺麗に保つためにサポートが終了しました。

これらの環境や機能で Vim が使いたい場合は、古い Vim を使う必要があります。

Vim 8.0 Advent Calendar 24 日目 内部的な変更

今回は、利用者にはあまり影響がない Vim の開発側の変更についてです。

GitHub へ移行

移行当時、割と大きく取り上げられていたので、ご存知の方も多いでしょう。 それまで Vim は、Google Code で Mercurial を使って開発されていました。しかし Google Code のサービス終了に伴い、2015 年 8 月に VimリポジトリGitHub へ移行、リポジトリも Git に変更されました。

https://github.com/vim/vim

また、以前は Vim へのパッチの投稿は vim_dev のメーリングリスト上でのみ行われていましたが、現在では GitHub 上の Pull Request でも受け付けています。貢献への敷居がかなり下がったと言えます。

大規模なソースコードの分割

Vim は長い歴史もあり、かなり巨大なプログラムです。ソースコードもかなりの量があり、eval.c などは 2 万行を超えていました。 Vim の開発が GitHub に移ったりなどさまざまな変化がある中で、コードのテストカバレッジCoveralls で計測するようになりました。 しかし、この時にこの巨大なソースコードが問題になったようで、これを回避するためにソースコードの分割が行われました。 概ね、以下のような分割が行われました。

  • eval.c
    • eval.c
    • list.c
    • dict.c
    • evalfunc.c
    • userfunc.c
  • spell.c
    • spell.c
    • spellfile.c

ANSI-C スタイルの関数定義

Vim 本体のソースコードは、以前までは K&R と呼ばれるスタイルで関数定義を行っていました。以下のような形式です。

/*
 * Add a watcher to a list.
 */
    void
list_add_watch(l, lw)
    list_T  *l;
    listwatch_T *lw;
{
    lw->lw_next = l->lv_watch;
    l->lv_watch = lw;
}

関数の引数部分の、型の宣言が括弧の外に書かれています。C 言語を知っている人の中でも、そもそもこのような書き方ができること自体知らない人もいるのではないでしょうか。 Vim の歴史は古く、そのためこの古い書き方がずっと使われてきました。 しかしこの度、現代で広く使われている ANSI-C スタイルの関数定義に書き換えられました。以下のような形式です。

/*
 * Add a watcher to a list.
 */
    void
list_add_watch(list_T *l, listwatch_T *lw)
{
    lw->lw_next = l->lv_watch;
    l->lv_watch = lw;
}

ビジュアルモードが常に有効

Vim には大きく分けて、Tiny、Small、Normal、Big、Huge の 5 つのビルドがあり、後になるほど多くの機能が含まれるようになっています。 ビジュアルモード(+visual 機能)は、以前までは Small 以上の Vim に含まれる機能でしたが、7.4.200 からは Tiny も含む全ての Vim で有効になるようになりました。

Vim 8.0 Advent Calendar 25 日目 ユーザーをハッピーにする

長かったこの連載もついに最終回です。

Vim ユーザーをハッピーにする」

2015 年の年末、Vim に以下のようなコミットが行われました。

Author: Bram Moolenaar <Bram@vim.org>
Date:   Thu Dec 31 16:10:23 2015 +0100

    patch 7.4.1005
    Problem:    Vim users are not always happy.
    Solution:   Make them happy.

Vim ユーザーをハッピーにする、と書かれたこのコミットは、1 つの Ex コマンドを追加するものでした。

:smile

:help version8 のページ内の :smile Ex コマンドの説明には、コミットメッセージと同様、以下のようにだけ書かれています。

|:smile|                make the user happy

この Ex コマンドを実行しても、何かとても便利なことが起きるわけではありません。しかし、これこそが Vim の願いであると感じます。

一部の例外はあるかもしれませんが、ソフトウェアはユーザーをハッピーにするために存在するものだと思います。少なくとも私はそうであると信じていますし、Vim はそのためにあると思っています。

今後も、Vim はユーザーをハッピーにするために進化を続けます。そして Vim を使った多くのユーザーがハッピーになることを願っています。

f:id:thinca:20161230184917p:plain

Happy Vimming!