🐼

Neovimのコマンドラインにエスケープ済みのいろいろを展開する

2024/04/03に公開

結論

VimのExコマンド入力中にCTRL-Rから始まる各種のマッピングで入力される内容を、
いくつかのパターンでエスケープして挿入できるマッピングを用意しておくと、結構便利です。

-- Command line内でカーソル位置周りやレジスタのテキストをエスケープして貼り付ける処理群
local func = require("kyoh86.lib.func")

-- cmdtypeとcmdlineの内容に応じてエスケープして返す
local function escape_for_cmdline(cmdline, str)
  local cmdtype = vim.fn.getcmdtype()
  if cmdtype == "/" or cmdtype == "?" then
    return vim.fn.escape(str, "/")
  elseif cmdtype == ":" and cmdline:sub(1, 1) == "!" then
    return vim.fn.shellescape(str)
  else
    return vim.fn.fnameescape(str)
  end
end

-- テキストをエスケープして現在のcmdに貼り付ける
-- @param text: string
local function put_escaped_str_to_cmdline(text)
  local cmdline = vim.fn.getcmdline()
  if cmdline == nil then
    return
  end
  local cmdpos = vim.fn.getcmdpos()
  if cmdpos == nil then
    return
  end
  local escaped_text = escape_for_cmdline(cmdline, text)
  local new_cmdline = cmdline:sub(1, cmdpos - 1) .. escaped_text .. cmdline:sub(cmdpos)
  vim.fn.setcmdline(new_cmdline, cmdpos + #escaped_text)
end

-- カーソル周りのテキストを取得する処理群
local scopes = {
  ["<C-f>"] = function()
    return vim.fn.expand("<cfile>")
  end,
  ["<C-p>"] = function()
    return vim.fn.findfile(vim.fn.expand("<cfile>"))
  end,
  ["<C-w>"] = function()
    return vim.fn.expand("<cword>")
  end,
  ["<C-a>"] = function()
    return vim.fn.expand("<cWORD>")
  end,
  ["<C-l>"] = function()
    return vim.fn.getline(".")
  end,
}

-- カーソル周りのテキストをエスケープして現在のcmdに貼り付ける
-- @param kind: "file"|"path"|"word"|"WORD"|"line"
local function put_escaped_scope_to_cmdline(kind)
  put_escaped_str_to_cmdline(scopes[kind]())
end

local registers = vim.tbl_flatten({
  { [[a]], [[b]], [[c]], [[d]], [[e]], [[f]], [[g]], [[h]], [[i]], [[j]], [[k]], [[l]], [[m]] },
  { [[n]], [[o]], [[p]], [[q]], [[r]], [[s]], [[t]], [[u]], [[v]], [[w]], [[x]], [[y]], [[z]] },
  { [[A]], [[B]], [[C]], [[D]], [[E]], [[F]], [[G]], [[H]], [[I]], [[J]], [[K]], [[L]], [[M]] },
  { [[N]], [[O]], [[P]], [[Q]], [[R]], [[S]], [[T]], [[U]], [[V]], [[W]], [[X]], [[Y]], [[Z]] },
  { [[0]], [[1]], [[2]], [[3]], [[4]], [[5]], [[6]], [[7]], [[8]], [[9]] },
  { [["]], [[-]], [[:]], [[.]], [[%]], [[#]], [[=]], [[*]], [[+]], [[_]], [[/]] },
})

-- レジスタのテキストをエスケープして現在のcmdに貼り付ける
-- @param regname: string
local function put_register_to_cmdline(regname)
  put_escaped_str_to_cmdline(vim.fn.getreg(regname))
end

local keymap_prefix = "<C-r><C-r>"

for key in pairs(scopes) do
  vim.keymap.set("c", keymap_prefix .. key, func.bind_all(put_escaped_scope_to_cmdline, key), {})
end

for _, regname in ipairs(registers) do
  vim.keymap.set("c", keymap_prefix .. regname, func.bind_all(put_register_to_cmdline, regname), {})
end

動機

VimのExコマンド入力中には、CTRL-Rから始まる各種のマッピングで、様々な情報を挿入できます。
CTRL-Rにつづけてレジスタ名を入力すれば、そのレジスタの中身が挿入できるほか、たとえば以下の様な特殊な情報が挿入できます。

  • % カレントファイル名
  • # 代替ファイル名
  • / 最後に検索したパターン
  • CTRL-F バッファ内のカーソル下のファイル名
  • CTRL-P バッファ内のカーソル下のファイル名を展開したもの
  • CTRL-W カーソル下の word
  • CTRL-A カーソル下の WORD
  • CTRL-L カーソル下の行

しかし、往々にしてコマンドでこれらを入力した際に、エスケープが必要でコマンドライン編集に手間取ってしまうことがあります。

  • /foo/bar.txt/foo\/bar.txt
  • :new foo/[bar-id]/baz.txt:new foo/\[bar-id]/baz.txt
  • :!rm file 1.txt:!rm\ file\ 1.txt

せっかく便利な情報が入力できるのに、エスケープのために手間取るのはもったいないです。

解決

そこで、コマンドライン編集中にこれらの情報をエスケープ済みで挿入できるようにしました。

  • コマンドが :! で始まる場合は shellescape でエスケープしてから挿入
  • 検索パターンの場合は escape/ をエスケープしてから挿入
  • それ以外の場合は fnameescape でエスケープしてから挿入

これを <C-R><C-R> に続けてキーを押すことで、呼び出せるようにしました。

もう少し凝ったパターンを洗い出して、より便利なエスケープをかけられるともっと便利な気もします。
まあ、一旦これで十分かなと思っているので、追々必要になったら追加していくかもしれません。

Discussion