Neovimの検索と置換を便利にする

· 6 min read

Neovimの検索でラベルを表示するプラグインや、カーソル下の単語をすぐに検索・置換できる設定を紹介します。

nvim-hlslensとは?

nvim-hlslensは検索でマッチした箇所にラベルをつけてくれるプラグインです。

カーソルのある行なら「何番目 / マッチした件数」がvirtual textで表示されます。
「マッチした所はあといくつあるの?」「今何件目?」という情報がすぐ目に入って便利です。

virtual textの詳細

カーソル行以外は「ジャンプのヒント / 何番目」と表示されます。
ジャンプのヒントのとおりに入力すれば、一発でその箇所まで移動できます。たとえば1つ前のマッチ箇所ならN、2つ後ろなら2nです。
数字とnNを組み合わせる入力はプラグインを入れなくてもできますが、hlslensの表示があれば使いこなしやすくなりそうです。

nvim-hlslensの設定

プラグインの設定はREADMEのとおりrequire('hlslens').setup()とキーマップだけですぐに動きます。筆者の設定は後で解説します。

vim-asterisknvim-ufovim-visual-multiと併せて使う場合はREADMEのIntegrate with other pluginsを見てみましょう。

起動と停止の方法

/?による検索でhlslensがスタートします。READMEのとおりにマッピングすれば*#nNなどでも起動できます。

停止するには:nohlsearchrequire('hlslens').stop()HlSearchLensDisableHlSearchLensToggleなどを呼び出しましょう。

「起動」「停止」など仰々しいですが、要するにプラグインを入れる前と変わらない使い方で検索・ハイライトの表示切り替えをすれば問題ないです。

カーソル下の単語をすぐに検索・置換

デフォルトでは、*#を入力するとカーソルがマッチした次/前の箇所にジャンプします。

筆者はジャンプせずに検索と置換をしたいため、俺的にはずせない【Vim】こだわりのmap(説明付き)を参考に、*#に手を加えています。

ノーマルモードで検索

筆者は「単純にカーソル下の単語をハイライトしたい」という衝動に駆られることがしばしばあるため、*は次のように設定しています。

local hlslens = require("hlslens")
local opts = { silent = true }
vim.keymap.set("n", "*", function()
local current_word = vim.fn.expand("<cword>")
vim.fn.histadd("search", current_word)
vim.fn.setreg("/", current_word)
vim.opt.hlsearch = true
hlslens.start()
end, opts)

vim.fn.expand("<cword>")でカーソル下の単語を拾い、検索履歴と検索レジスタに突っ込みます。後は検索ハイライトとhlslensをオンにするだけ。

常にカーソル下の単語をハイライトするプラグイン

上記は「検索した単語」のハイライト(強調表示)でした。
「カーソル下の単語を常に」邪魔しない程度にハイライトしたいなら、vim-illuminateを使ってみましょう。

vim-illuminateの例

componentsがうっすらと色付けされています。カーソルを別の単語に移動すればハイライト箇所も勝手に変わります。

ノーマルモードで置換

カーソル下の単語をすぐ置換するため、#の動作を書き換えています。

次のように書き換えています。

vim.keymap.set("n", "#", function()
local current_word = vim.fn.expand("<cword>")
vim.api.nvim_feedkeys(":%s/" .. current_word .. "//g", "n", false)
-- :%s/word/CURSOR/g
local ll = vim.api.nvim_replace_termcodes("<Left><Left>", true, true, true)
vim.api.nvim_feedkeys(ll, "n", false)
vim.opt.hlsearch = true
hlslens.start()
end, opts)

vim.api.nvim_feedkeys(":%s/" .. current_word .. "//g", "n", false)で置換の呼び出してから、カーソル箇所を移動しています。

カーソル箇所の移動

カーソルを2回左移動することですぐに置換後の文字列が入力できます。

ビジュアルモードで検索

選択した文字列を*で検索できるようにマッピングしています。

vim.keymap.set("v", "*", function()
local current_word = getVisualSelection()
current_word = vim.fn.substitute(vim.fn.escape(current_word, "\\"), "\\n", "\\\\n", "g")
vim.fn.histadd("search", current_word)
vim.fn.setreg("/", current_word)
vim.opt.hlsearch = true
hlslens.start()
end, opts)

getVisualSelectionの実装は後で説明します。
やっていることは選択範囲の文字列の取得とエスケープで後はノーマルモードと同じです。

ビジュアルモードで置換

同様に、選択した文字列を#で置換できるようにマッピングしています。

vim.keymap.set("v", "#", function()
local current_word = getVisualSelection()
current_word = vim.fn.substitute(vim.fn.escape(current_word, "\\"), "\\n", "\\\\n", "g")
vim.api.nvim_feedkeys(":%s/" .. current_word .. "//g", "n", false)
-- :%s/word/CURSOR/g
local ll = vim.api.nvim_replace_termcodes("<Left><Left>", true, true, true)
vim.api.nvim_feedkeys(ll, "n", false)
vim.opt.hlsearch = true
hlslens.start()
end, opts)

選択中の文字列を取得する

選択中の文字列を取得する専用の関数は今のところは無く、いずれgetregionが入るっぽいです(Pull Request #21115 · neovim/neovim)。

いくつかやり方はあるようですが、今回はhow to search with selected text in visual mode ?を参考に、レジスタを使って書きました。

local function getVisualSelection()
vim.cmd('noau normal! "vy')
local text = vim.fn.getreg("v")
vim.fn.setreg("v", {})
if #text > 0 then
return text
else
return ""
end
end

更新があれば随時書き足します。


nvim-hlslensのおかげで「nnnnnnnnnnnあっ!NNN」から脱却しつつあります。