Vim/Neovimでヤンクした範囲をハイライトする(hlyank / vim.hl.on_yank)
yi(やyapでヤンクしても、Vimの画面は何も変わりません。カーソルが範囲の先頭に戻るだけなので、どこまで取れたかは貼り付けてみるまで分からないままです。ヤンクした範囲を一瞬だけ色で塗っておけば、その場で確かめられます。Vimは本体同梱のhlyankパッケージ、Neovimはvim.hl.on_yank()がこれを受け持ちます。
設定は数行で済みますが、使っていると「ddでは光らない」「マクロの中では光らない」という場面に出会います。設定の誤りではなく、どちらも実装に書かれた条件どおりの動きです。この記事では、ヤンクのたびに届くv:eventの中身を実機で表示して、その条件を確かめます。Neovimでは名前がvim.highlightからvim.hlへ、さらに0.13でvim.hl.hl_op()へと変わってきたので、古い設定がどう扱われるかも見ておきます。
検証環境:Vim 9.1(patches 1-1752)、NVIM v0.12.4、NVIM v0.13.0-dev-1946+g659164699f(nightlyを一時ディレクトリに展開して使用)。いずれもmacOS。tmuxの中で設定ファイルを読ませずに起動し、ヤンクの直後にVimではgetmatches()、Neovimではextmarkの数を表示して、光っているかどうかを判定しました。v:eventの値と警告文は実行結果をそのまま載せています。
目次(9項目)
ヤンクした範囲が光ると何がうれしいか
テキストオブジェクトは範囲を目で見ないまま使う操作です。yi(はカーソルを囲むいちばん内側の括弧を選びますが、括弧が入れ子になっていると、思っていた外側の括弧ではなく内側が取れていることがあります。yapは段落の後ろの空行まで含み、yipは含みません。この差も、貼り付けて空行が増えたときに初めて気づくことが多いです。
ヤンクの直後に範囲が塗られれば、取り違えはその場で分かります。yyやywのような基本のヤンクそのものはVimでコピーする操作(yy / yw / 矩形選択)にまとめてあるので、ここでは範囲を見えるようにする設定だけを扱います。
Vimで光らせる(:packadd hlyank)
Vimにはhlyankというパッケージが同梱されています。$VIMRUNTIME/pack/dist/opt/hlyank/にあるオプションのパッケージなので、読み込むまでは何もしません。vimrcに次の1行を書きます。
packadd hlyank
pack/dist/optの仕組みはVimにプラグインを入れる方法(標準パッケージとvim-plug)で説明しています。このパッケージはvim/vimのリポジトリに2025年3月19日に入りました。パッチ番号の付かないランタイムの更新で、直前のパッチは9.1.1227です。中身はVim9 scriptで、範囲の計算にgetregionpos()を使っています。
読み込んだ状態でyiwを押し、すぐに:echo getmatches()を打つと、塗られた範囲がそのまま見えます。
[{'group': 'IncSearch', 'id': 1000, 'priority': 10, 'pos1': [1, 1, 5]}]
1行目の1桁目から5文字をIncSearchで塗っています。0.6秒待ってから同じことをすると[]が返り、既定の300ミリ秒で消えていることが分かります。設定できる変数は3つです。
| 変数 | 既定値 | 意味 |
|---|---|---|
g:hlyank_hlgroup | 'IncSearch' | 塗る色(ハイライトグループ名) |
g:hlyank_duration | 300 | 消えるまでのミリ秒。3000が上限 |
g:hlyank_invisual | v:true | ビジュアル選択からのyも光らせるか |
g:hlyank_durationに1000を入れると0.6秒後もまだ残り、g:hlyank_hlgroupを'Visual'にするとgetmatches()のgroupがVisualに変わりました。上限のほうは、10000を入れて試しています。2.5秒後には塗りが1つ残っていて、3.5秒後には0になりました。ソースがmin([get(g:, "hlyank_duration", 300), 3000])で丸めているので、大きな値を入れても3秒で止まります。
g:hlyank_invisualをv:falseにしたときだけ、少し癖があります。ビジュアル選択かどうかをv:event.visualではなくvisualmode()で見ているためです。visualmode()は最後に使ったビジュアルモードを覚えていて、ヤンクに関係なく値が残ります。手元ではvjからEscで抜けただけのあと、次のyiwが光りませんでした。その次のyiwからはまた光ります。光らなかった1回でvisualmode(1)が値を消すからです。
:packadd hlyankのあとでyi(。外側のsum(ではなく、カーソルのあるprice(の中身だけが光るNeovim 0.12で光らせる(vim.hl.on_yank)
Neovimにはパッケージではなく関数が用意されています。ヤンクの直後に起きるTextYankPostイベントからvim.hl.on_yank()を呼ぶと、そのヤンクの範囲が塗られます。init.luaなら次のとおりです。
vim.api.nvim_create_autocmd('TextYankPost', {
callback = function()
vim.hl.on_yank({ higroup = 'Visual', timeout = 300 })
end,
})
init.vim派なら、ヘルプに載っているautocmd TextYankPost * silent! lua vim.hl.on_yank {higroup='Visual', timeout=300}の1行でも同じです。引数を省くとIncSearchで150ミリ秒光ります。この150は、塗りが消えるまでをタイマーで測って確かめました。結果は156ミリ秒でした。渡せる値は次の6つです。
| キー | 既定値 | 意味 |
|---|---|---|
higroup | 'IncSearch' | 塗る色 |
timeout | 150 | 消えるまでのミリ秒。上限は無い |
on_visual | true | ビジュアル選択からのyも光らせるか |
on_macro | false | マクロの実行中も光らせるか |
priority | vim.hl.priorities.user | ほかのハイライトと重なったときの優先度 |
event | vim.v.event | 判定に使うイベントの中身 |
古い名前のvim.highlight.on_yank()も、0.12.4ではまだ動きます。画面には何も出ず、ヤンクした範囲もちゃんと光りました。ただし非推奨の記録は残っていて、:checkhealth vim.deprecatedを開くと次のように出ます(行頭の警告の記号は省いています)。
vim.deprecated:
- WARNING vim.highlight is deprecated. Feature will be removed in Nvim 2.0.0
- ADVICE:
- use vim.hl instead.
削除の予定は2.0なので急ぐ必要はありませんが、0.11より前の記事からコピーした設定はこの形のままです。
3 lines yankedのとおり5行目の空行も範囲に入っているが、空行には塗る文字が無いので色は見えない色と長さを変える
どちらも既定の色はIncSearchです。このグループは、/で検索語を打っている途中に一致箇所を塗る色でもあります。ヤンクを目立たせたくてIncSearchそのものの色を変えると、検索中の色まで変わってしまいます。ヤンク専用のグループを作って、それを指定するほうが安全です。
" Vim
highlight YankFlash ctermbg=22 guibg=#1f5f3f
let g:hlyank_hlgroup = 'YankFlash'
-- Neovim
vim.api.nvim_set_hl(0, 'YankFlash', { bg = '#1f5f3f' })
-- on_yank に higroup = 'YankFlash' を渡す
どちらも、塗りがYankFlashで付くことをgetmatches()とextmarkの中身で確かめました。
長さを伸ばしたときは、続けてヤンクしたときの振る舞いが分かれます。3秒に設定して1行目と2行目で続けてyiwを押すと、Vimでは['YankFlash[1, 1, 5]', 'YankFlash[2, 1, 6]']と2つの塗りが同時に残りました。Neovimは新しいヤンクで前の塗りを消すので、残ったのは2行目の1つだけでした。on_yankが前回のタイマーを止めてから塗り直す作りになっているためです。長めにするなら、Neovimのほうが画面が散らかりません。
dやcやマクロでは光らない理由
TextYankPostという名前ですが、このイベントは削除でも変更でも起きます。テキストがレジスタに入る操作すべてが対象だからです。Neovim 0.12.4で、イベントのたびにv:eventの3つの値を記録する設定を入れ、5つの操作を順に打ちました。
| 操作 | operator | regtype | visual | on_yank |
|---|---|---|---|---|
| yy | y | V | false | 光る |
| dd | d | V | false | 光らない |
| ciw | c | v | false | 光らない |
| viw→y | y | v | true | 光る |
| Ctrl-v j y | y | ^V1 | true | 光る |
矩形選択のregtypeはCtrl-vの制御文字の後ろに幅が付いた値で、表では^V1と書いています。ddでもciwでもイベントは届いています。光らないのは、on_yankの中に次の判定があるからです(0.12.4のruntime/lua/vim/hl.luaから抜粋)。
if not on_macro and vim.fn.reg_executing() ~= '' then
return
end
if event.operator ~= 'y' or event.regtype == '' then
return
end
if not on_visual and event.visual then
return
end
2つ目の判定で、operatorがy以外のものはすべて落ちます。Vimのhlyankもv:event.operator ==? 'y'のときだけ塗る作りで、手元でもddとciwは光りませんでした。黒穴レジスタに捨てる"_yiwは、そもそもTextYankPostが起きません。記録にも1件も残りませんでした。レジスタの種類はVimのレジスタの使い分けで整理しています。
1つ目の判定がマクロの分です。reg_executing()はマクロを実行している間だけレジスタ名を返すので、qa yiw qで記録して@aで流すと、中のyiwは塗られません。on_macro = trueを渡すと塗られました。ここはVimと違うところで、Vimのhlyankにはこの判定が無く、@aの中のヤンクも光りました。
3つ目はon_visual = falseのときで、表のvisualがtrueの行だけが落ちます。試すと、viw→yは光らず、続くyiwは光りました。Vimのg:hlyank_invisualのような1回ぶんのずれは起きません。Neovimはv:event.visualを見ているからです。
名前が2度変わった経緯
Neovimのこの関数は、2回名前が変わっています。
| 版 | 名前 | 扱い |
|---|---|---|
| 0.10まで | vim.highlight.on_yank() | 正式な名前 |
| 0.11・0.12 | vim.hl.on_yank() | vim.highlightは非推奨(2.0で削除予定) |
| 0.13(開発版) | vim.hl.hl_op() | on_yankは非推奨(0.14で削除予定) |
1回目はモジュール名の短縮で、:help deprecated-0.11に「vim.highlight Renamed to vim.hl」とあります。2回目は0.13で、関数がヤンク専用ではなくなったための改名です。nightlyのnews.txtには次の1文があります。
vim.hl.hl_op() highlights text regions for TextYankPost and TextPutPost events. It replaces the now deprecated vim.hl.on_yank().
0.12系の非推奨とは扱いが違い、nightlyでvim.hl.on_yank()を呼ぶと、ヤンクした瞬間に画面の下へ警告が出ました。
vim.hl.on_yank is deprecated. Run ":checkhealth vim.deprecated" for more information
:checkhealth vim.deprecatedにはFeature will be removed in Nvim 0.14と出ます。中身はhl_op()をそのまま呼ぶだけなので、光り方は変わりません。0.12と0.13の両方で同じ設定を使うなら、ある方を選ぶ1行で済みます。0.12.4ではvim.hl.hl_opがnilなのでon_yankが選ばれ、nightlyではhl_opが選ばれて警告も出ませんでした。
local hl = vim.hl.hl_op or vim.hl.on_yank
vim.api.nvim_create_autocmd('TextYankPost', {
callback = function() hl({ higroup = 'Visual', timeout = 300 }) end,
})
0.13でほかに何が変わるかはNeovim 0.13の新機能と、意味が変わるキーにまとめています。
Neovim 0.13で貼り付けた範囲も光らせる
改名のきっかけになったのが、0.13で増えたTextPutPreとTextPutPostの2つのイベントです。TextPutPostはpやPで貼り付けた直後に起き、v:eventの形はTextYankPostとほぼ同じです。hl_op()はoperatorがy・p・Pのどれかなら塗るので、2つのイベントに同じ関数をつなぐだけで、貼り付けた範囲も光ります。
vim.api.nvim_create_autocmd({ 'TextYankPost', 'TextPutPost' }, {
callback = function()
vim.hl.hl_op({ higroup = 'Visual', timeout = 300 })
end,
})
nightlyでこの設定を入れ、yy、p、yiw、P、ddの順に打つと、記録は次のようになりました。ddだけが塗られず、残りの4つは塗られています。
TextYankPost operator=y regtype=V visual=false
TextPutPost operator=p regtype=V visual=false
TextYankPost operator=y regtype=v visual=false
TextPutPost operator=P regtype=v visual=false
TextYankPost operator=d regtype=V visual=false
ヤンクの塗りと貼り付けの塗りは別々に管理されていて、yyの直後にpを押すと、ヤンクした行と貼り付けた行の2か所が同時に塗られていました。hl_op()の中でyankとputを別の状態として持っているためです。0.12.4にはTextPutPost自体が無く、exists('##TextPutPost')は0でした。
Vimにも同じ名前のイベントが入りつつあります。vim/vimのリポジトリでは、2026年5月のパッチ9.2.0470でTextPutPostが加わり、hlyankもg:hlput_enableを立てると貼り付けを光らせるように更新されました。手元のVim 9.1.1752には入っておらずexists('##TextPutPost')も0だったので、Vim側の動きは確かめていません。
検証を始める前は、ddで光らないのはTextYankPostが削除では起きないからだと思い込んでいました。v:eventを記録してみるとddでもciwでもイベントは毎回届いていて、落としているのはon_yankの側でした。もう1つ引っかかったのはVimのg:hlyank_invisualで、v:falseにした直後のテストでyiwが1回だけ光らず、しばらく自分のテスト手順を疑っていました。ソースを開いてvisualmode()を見ていると分かるまで、直前にvjから抜けた操作が効いているとは考えもしませんでした。
まとめ
VimとNeovimで、ヤンクの範囲を光らせる仕組みは次のように違います。どれもdやcでは光らず、塗るのはヤンク(と0.13の貼り付け)だけです。
| 項目 | Vim 9.1 hlyank | Neovim 0.12 on_yank | Neovim 0.13 hl_op |
|---|---|---|---|
| 有効にする方法 | packadd hlyank | TextYankPostから呼ぶ | TextYankPostとTextPutPostから呼ぶ |
| 既定の色 | IncSearch | IncSearch | IncSearch |
| 既定の長さ | 300ミリ秒(上限3000) | 150ミリ秒 | 150ミリ秒 |
| マクロの中 | 光る | 光らない(on_macroで変更) | 光らない(on_macroで変更) |
| 続けてヤンク | 前の塗りも残る | 前の塗りを消す | 前の塗りを消す |
| 貼り付け | 光らない | 光らない | 光る |
Neovimでは、0.12のうちはvim.hl.on_yank()、0.13へ上げたらvim.hl.hl_op()に替えてTextPutPostも足します。