Neovim で文字列を動的にチャチャッと切り替える
はじめに
開発中、プログラム中の文字列をこまめに切り替えたい(トグルしたい)ことがあります。たとえば
local debug = false;
の false
を true
にする、あるいはその逆を行うケースが考えられます。 特定のフラグや設定値を変えてビルドし直して、再度値を変えてビルドし直して…というステップは開発においてよくあることです。 特定の文字列を1手で切り替えたいとき、dial.nvim というプラグインが役に立ちます。
dial.nvim
dial.nvim は Neovim の <C-a>
/ <C-x>
を様々な用途に利用できるよう拡張するプラグインです。 ノーマルモードにおけるデフォルトの <C-a>
/ <C-x>
は数値やアルファベットの増減のみ行いますが、 dial.nvim を入れると日付や真偽値 (true / false) の切り替えも行えるようになります。
キーマップの設定方法などの説明は README に譲りますが、たとえば上の例であれば
local augend = require("dial.augend")
require("dial.config").augends:register_group {
default = {
augend.constant.new {
elements = { "true", "false" },
},
-- ... other configs ...
},
}
という設定をいれることで true / false のトグルが可能となります。
変換先を設定ファイルに書かずにトグルを行う
さて、ここからが本題です。次のようなケースではどうでしょうか。
local completion_plugin = "cmp" -- switch: cmp, blink
local os = "windows" -- switch: windows, macos, linux
completion_plugin
は "cmp"
または "blink"
のいずれかであると分かっている、os
は "windows"
, "macos"
, "linux"
のいずれかであると分かっているようなケースです。
もちろん予め設定ファイルにてそれらの文字列を切り替えるようなルールを書いておくこともできますが、こういった specific な文字列をいちいち定義するのは手間です。バッファ上に記された switch: cmp, blink
のような形式のヒントを読み取り、cmp
と blink
を <C-a>
/ <C-x>
で切り替えられるようにできないでしょうか。
dial.nvim で解決する方法
dial.nvim でこれを直接的に(=簡潔な設定のみで)サポートする方法は現状ありませんが、ユーザ側で頑張って設定を書くことでやりたいことが実現できます。
local augend = require("dial.augend")
-- `inline_constant_*` 関数で使用される、文字列候補を格納しておく内部状態。
elements = {}
--- カーソル行から増減可能な文字列リテラルの場所を見つけて返す関数。
local function inline_constant_find(line, cursor)
-- カーソル行に含まれる `switch: xxx,yyy,zzz` のような形のパターンを検索
local pattern = "switch: (.+)$"
local match = line:match(pattern)
if not match then
return
end
elements = {}
for option in match:gmatch "([^,]+)" do
table.insert(elements, option:match "^%s*(.-)%s*$")
end
-- 上で取得した文字列候補にヒットする場所を返す
local vim_regex_ptn = ([[\V\(%s\)]]):format(table.concat(elements, [[\|]]))
return require("dial.augend.common").find_pattern_regex(vim_regex_ptn, false)(line, cursor)
end
--- テキストのトグルを行う関数。
--- たとえば `elements = {"xxx", "yyy", "zzz"}` のとき、
--- inline_constant_add("xxx", 1, 1) は `{text = "yyy", cursor = 3}` を返す。
--- これはつまり、本文中の `xxx` の1文字目にカーソルがある状態で `<C-a>` を押した場合
--- テキストを `yyy` に変えてカーソルを3文字目に移動させることを表す。
local function inline_constant_add(text, addend, cursor)
local n_patterns = #elements
local n = 1
for i, elem in ipairs(elements) do
if text == elem then
n = i
end
end
n = (n + addend - 1) % n_patterns + 1
text = elements[n]
cursor = #text
return { text = text, cursor = cursor }
end
require("dial.config").augends:register_group {
default = {
-- 複雑な処理を実現するための augend
augend.user.new {
find = inline_constant_find,
add = inline_constant_add,
},
-- ... other configs ...
},
}
augend.user
はユーザ定義された関数を用いて増減ルールを指定できる機能であり、使いこなせば複雑な増減操作が可能となります。 上で書いた例では、カーソル行が switch: xxx,yyy,zzz
のような形式で終了している場合、 {"xxx", "yyy", "zzz"}
をトグル文字列の候補とします。 検索パターンを工夫すれば TypeScript や Python の型ヒントなど、別の記法にも対応できます。
課題
文字列を動的にトグルするためには、トグル可能な文字列の候補を取得する必要があります。 現在はカーソル行から正規表現によるパターンマッチで取得しているため、異なるパターンで記述したり、改行を挟んだりするだけで使えなくなります。 「その文字列リテラルに入りうるパターン」を言語サーバから取得できる言語(TypeScript など)であれば 言語サーバからその情報を取得して用いるのが最も賢い方法ですが、まだ実現はできていません。
おわりに + 余談
dial.nvim は作者自身も愛用しているものの、issue などで作者自身が思いもよらなかったユースケースに気付かされることが時折あります。 以前見かけた例としては、Tailwind CSS のクラス名を増減できるようにしている人がいました (bg-red-50
→ bg-red-100
→ bg-red-200
→ … のような感じ)。 作者の発想を超えたところで活躍しているのを見るとなんだか嬉しいものですね。
dial.nvim は工夫次第で色々なことができるので、ぜひいろいろ試してみてください。
Discussion