NeovimでMarkdownチェックボックスのトグルキーマップを設定

Neovimでプラグインを使わずに「Markdownのチェックボックスのトグル」ができるキーマップを設定しました。備忘録として残しておきます。

挙動まとめ

「チェック無し」→「チェックあり」→「箇条書き」→「チェック無し」を循環します。

- [ ] foo
- [x] foo
- foo
- [ ] foo
...

そもそも箇条書きでない場合は箇条書きに。

foo
- [ ] foo
...

-がチェックボックスに入ってたら箇条書きに戻します(この記法は自分がObsidian時代に使ってたものです)

- [-] foo
- foo
...

インデントやカーソル位置も保ちます。

- foo
- bar<CURSOR>
- foo
- [ ] bar<CURSOR>

ビジュアルモードでまとめてチェックもできます。

コード全体

先に実装の全体を載せます。GitHubで見たい人はこちら

↓クリックで開閉
-- チェックボックスのトグル
local function toggle_checkbox_line(line)
local indent = vim.fn.matchstr(line, [[^\s*]])
local content_start = #indent + 1
local new_line
local col_offset = 0
local checkbox_end_pos = 0 -- チェックボックスの終了位置
if vim.fn.match(line, [=[\v^\s*[-+*]\s+\[ \]\s+]=]) >= 0 then
-- [ ] → [x]
new_line =
vim.fn.substitute(line, [=[\v^(\s*[-+*]\s+)\[ \]]=], [=[\1[x]]=], "")
col_offset = 0
checkbox_end_pos = #indent + 2 + 4 -- インデント + "- " + "[ ] "
elseif vim.fn.match(line, [=[\v^\s*[-+*]\s+\[x\]\s+]=]) >= 0 then
-- [x] → 箇条書き
new_line =
vim.fn.substitute(line, [=[\v^(\s*[-+*]\s+)\[x\]\s+]=], [[\1]], "")
col_offset = -4 -- "[x] "の分
checkbox_end_pos = #indent + 2 + 4
elseif vim.fn.match(line, [[\v^\s*[-+*]\s+\[-\]\s+]]) >= 0 then
-- [-] → 箇条書き
new_line =
vim.fn.substitute(line, [=[\v^(\s*[-+*]\s+)\[-\]\s+]=], [[\1]], "")
col_offset = -4 -- "[-] "の分
checkbox_end_pos = #indent + 2 + 4
elseif vim.fn.match(line, [=[\v^\s*[-+*]\s+]=]) >= 0 then
-- 箇条書き → [ ]
new_line = vim.fn.substitute(line, [=[\v^(\s*[-+*]\s+)]=], [[\1[ ] ]], "")
col_offset = 4
checkbox_end_pos = #indent + 2
else
-- 普通のテキスト → リスト
new_line = indent .. "- [ ] " .. line:sub(content_start)
col_offset = 6
checkbox_end_pos = 0
end
return new_line, col_offset, checkbox_end_pos
end
local function toggle_checkbox()
local cursor_pos = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local col = cursor_pos[2]
local new_line, col_offset, checkbox_end_pos = toggle_checkbox_line(line)
-- カーソルがチェックボックスより右にある場合のみオフセットを適用
local new_col = col
if col >= checkbox_end_pos then
new_col = math.max(0, col + col_offset)
end
vim.api.nvim_set_current_line(new_line)
vim.api.nvim_win_set_cursor(0, { cursor_pos[1], new_col })
end
local function toggle_checkbox_visual()
-- ビジュアルモードの選択範囲を取得
local start_pos = vim.fn.getpos("v") -- Visualモード開始位置
local end_pos = vim.fn.getpos(".") -- 現在カーソル位置
local start_line = start_pos[2]
local end_line = end_pos[2]
-- 小さい方を先にする
if start_line > end_line then
start_line, end_line = end_line, start_line
end
for line_num = start_line, end_line do
local line = vim.fn.getline(line_num)
local new_line, _, _ = toggle_checkbox_line(line)
vim.fn.setline(line_num, new_line)
end
end
vim.keymap.set({ "n", "i" }, "<C-q>", toggle_checkbox, {
desc = "チェックボックスのトグル",
buffer = true,
})
vim.keymap.set("x", "<C-q>", toggle_checkbox_visual, {
desc = "チェックボックスのトグル",
buffer = true,
})

筆者は.config/nvim/after/ftplugin/markdown.luaに書いています。

全体ざっくり

local function toggle_checkbox_line(line)
-- ある1行に対して
-- 「加工したテキスト」「ズレた文字数」「チェックボックス終了位置」を返す
return new_line, col_offset, checkbox_end_pos
end
local function toggle_checkbox()
-- カーソル行のチェックボックスをトグル
end
local function toggle_checkbox_visual()
-- ビジュアルモードの選択範囲のチェックボックスをトグル
end
vim.keymap.set({ "n", "i" }, "<C-q>", toggle_checkbox, {
desc = "チェックボックスのトグル",
buffer = true,
})
vim.keymap.set("x", "<C-q>", toggle_checkbox_visual, {
desc = "チェックボックスのトグル",
buffer = true,
})

正規表現はvim.fn.match

今回のパターンマッチングはvim.fn.matchで書きました。

Luaでもline:match("%w")のようなパターンマッチング(Lua Pattern)はできますが、 正規表現ではありません 。今回は使ってませんがVimの正規表現だと(a|b)のような書き方ができ、Lua Patternだとできません。

単純な検索以外はVimの正規表現をオススメします。

文字列の囲みにクオートは使わない

今回は[[foo]][=[foo]=]のように囲んで文字列にしています。long bracketsと呼ぶそうです(参考:公式ドキュメント

vim.fn.substitute(line, [=[\v^(\s*[-+*]\s+)\[ \]]=], [=[\1[x]]=], "")

クオートだとエスケープ周りが面倒であるため、極力使っていません。ただ、正規表現の中に[]が混じっているときはややこしいので[=[foo]=]のようにレベルを加えてます。


以上、Neovimでチェックボックス関係のキーマップ設定の解説でした。「箇条書き経由しなくていいや」という方はその分岐だけ抜くなりしてカスタマイズできると思います。

まだ正規表現周りの書き方で目が滑るので、もうちょいリファクタしたい。