できること
Neovimで、MarkdownのTODOをキー操作で切り替えています。外部プラグインは不要です。
TODOの行にカーソルを置き、Space → x と押します。もう一度押すと、元に戻ります。
- [] 記事を書く ↔ - [x] 記事を書く
- 同じ行にいれば、カーソルの位置はどこでもよい
:Todoコマンドでも切り替えられる-、+、*の箇条書きと、番号付きリストに対応- 本文のない
- []や- [x]も切り替えられる
自分のLeaderキーはSpaceです。下の例では、その設定からNeovimのAPIへの接続まで載せています。
未完了の [] は手元の略記です。GitHubでも使う場合の注意点は、末尾にまとめます。
ソースコード:自分のNeovim設定への組み込み例
使うのは、Lua標準関数とNeovimのAPIだけです。Tree-sitterや描画プラグインも必要ありません。
共通設定でLeaderキーをSpaceにする
自分は ~/.config/nvim/lua/core/options.lua で指定しています。
vim.g.mapleader = " "
このファイルは init.lua の require("core.options") で読み込みます。Leaderキーの指定は、キーバインドの登録より先に済ませます。
Markdown用のファイルに、実装と登録処理を置く
次が ~/.config/nvim/ftplugin/markdown.lua の全体です。Markdown用に読み込まれる設定ファイルです。
先頭は折り返しの設定です。TODOの実装とNeovimへの接続は、toggle_todo から下にあります。
-- Markdown は文章として読みやすい表示を優先する。
vim.opt_local.wrap = true
vim.opt_local.linebreak = true
vim.opt_local.breakindent = true
vim.opt_local.breakindentopt = { "shift:2", "min:20" }
vim.opt_local.showbreak = ""
local function toggle_todo()
local line = vim.api.nvim_get_current_line()
local prefix, state, suffix = line:match("^(%s*[-+*]%s+)%[([ xX]?)%](.*)$")
if not prefix then
prefix, state, suffix = line:match("^(%s*%d+[.)]%s+)%[([ xX]?)%](.*)$")
end
if not prefix then
return
end
local checked = state == "x" or state == "X"
vim.api.nvim_set_current_line(prefix .. (checked and "[]" or "[x]") .. suffix)
end
vim.api.nvim_buf_create_user_command(0, "Todo", toggle_todo, { desc = "Toggle Markdown TODO checkbox" })
vim.keymap.set("n", "<leader>x", toggle_todo, { buffer = true, desc = "Toggle Markdown TODO checkbox" })
実装の要点
切り替えは、現在行を書き換えるだけ
Luaの string.match で、現在行を三つに分けます。
| 変数 | - [ ] 記事を書く の場合 |
|---|---|
prefix | "- " |
state | " " |
suffix | " 記事を書く" |
x と X は完了、それ以外は未完了として扱います。前後の文字列を残し、角括弧の中だけを切り替えます。
関数をキーとコマンドに登録する
自分は ftplugin/markdown.lua の末尾で、同じ関数を登録しています。
vim.keymap.set("n", "<leader>x", toggle_todo, { buffer = true })
vim.api.nvim_buf_create_user_command(0, "Todo", toggle_todo, {})
これは説明用に desc を省いた例です。Space → x と :Todo のどちらでも、toggle_todo が呼ばれます。
登録先はMarkdownのバッファだけです。呼ばれた関数が、現在行を読み書きします。
編集だけを担当し、描画には関わらない
書き換えるのは、バッファ内の [] と [x] です。アイコンの表示や、プラグインの状態は参照しません。
render-markdown.nvimなどによる装飾は任意です。導入しなくても、このキーバインドは動きます。
Enterで次のリスト項目を作る処理も別機能です。この切り替え処理の依存先には含まれません。
使うときの注意点
[] は、GitHub Flavored Markdownの書式 ではありません。GitHubでも使うなら、未完了を [ ] に戻す形に変えます。
vim.api.nvim_set_current_line(prefix .. (checked and "[ ]" or "[x]") .. suffix)
この実装には、次の制約もあります。
- 切り替えは文字列だけを判定するため、コードブロック内も対象になる
- 引用の
> - []には対応しない [ ]から完了にして戻すと、元の[ ]ではなく[]になる- 普通の箇条書きに、チェックボックスを新規追加する機能はない
動作確認
切り替え処理は、プラグインなしで検証できます。以下のテストは、Neovim v0.12.0-nightly+fc7e5cf で9件すべて通っています。
切り替えテストと実行方法
ftplugin/markdown.lua を配置し、次を check-todo.lua として保存します。
dofile(vim.fn.stdpath("config") .. "/ftplugin/markdown.lua")
local cases = {
{ "- [] 記事を書く", "- [x] 記事を書く" },
{ "- [ ] 記事を書く", "- [x] 記事を書く" },
{ "- [x] 記事を書く", "- [] 記事を書く" },
{ "- [X] 記事を書く", "- [] 記事を書く" },
{ " + [] 確認", " + [x] 確認" },
{ "1) [x] 確認", "1) [] 確認" },
{ "- []", "- [x]" },
{ "- 普通の箇条書き", "- 普通の箇条書き" },
{ "> - [] 引用", "> - [] 引用" },
}
for _, case in ipairs(cases) do
vim.api.nvim_set_current_line(case[1])
vim.cmd("Todo")
assert(vim.api.nvim_get_current_line() == case[2], case[1])
end
print("TODO toggle: " .. #cases .. " cases passed")
保存先のディレクトリで実行します。-l に対応したNeovimが必要です。
nvim --headless -u NONE -l check-todo.lua
-u NONE で通常の設定を読み込まずに起動します。dofile で対象ファイルだけを読み込みます。
成功すると TODO toggle: 9 cases passed と出力されます。
このテストは、切り替え処理だけを確認します。描画やEnterのリスト継続は対象外です。