VimによるToDo管理を支える技術
背景
ToDo管理、大変ですよね。
世間には色んなツールがありますし、様々な方法論も溢れています。
しかしそのどれも、非常に大きな問題を抱えています。
Vimじゃない
Vimじゃないんです。そう。われわれはVimに魂を奪われた悲しき獣。
Vim以外の媒体で文字を書くと、全身に蕁麻疹が出て、手足は震え視野が狭窄し、やがて死に至ります。
解決は小さく
もう1つの問題として、世にある様々なToDo管理は
- 完成度の高い、巨大なツール
- 本が何冊も出るような、複雑な方法論
を必要としています。
そのため、学習コストが高く、始めるのも慣れるにも時間がかかります。
怠惰だからToDo管理をしたいのに、怠惰な人間には辛い初期コストを要するという理不尽。
私はそんな理不尽に抗えるほどまともな人間ではないので、小さく始めて、問題に直面するたび、少しずつ改善していくようにしたいものです。
スコープも小さく
これもToDo管理をしようとした私がよくくじける原因の1つですが、
何でもかんでも管理しようとして、本当にやるべきことを見失ってしまいがちです。
ToDo管理として常時にらむ対象は優先度に従って並べられる程度には少なく、自明に直近でやるべきことだけにしています。
それ以外は適当なリマインドリスト(Notion, GitHub Issue)などに流して、暇なときに眺める程度。たまに思い付きで取り組むこともありますが、基本はリスト化した段階で満足して、放置しています。
解決策たち
本稿では、私がそうして向き合うために必要とした小さな解決策たちを、一個一個列挙して紹介しようと思います。
todo.mdファイルを開くキーマップ
ToDoはすべて1ファイルで管理しています。
プライベートのタスクも、仕事のタスクも、それぞれそのための端末に保存してあるため、端末を切り替えればToDoファイルの中身はそれぞれ。
ただ、ToDoファイルへのアクセスは手軽であればあるほど良いのは間違いありません。
開くのが面倒くさいファイルは、やがて忘れ去られて陳腐化します。
そのため、あえてToDoファイルを開くためだけのキーマップを設定しました。
vim.keymap.set("n", "<leader><leader>t", [[<cmd>new ~/.local/state/to]] .. [[do.md<cr>]], { remap = false, desc = "作業メモを編集する" })
nnoremap <leader><leader>t <cmd>new ~/.local/state/todo.md<cr>
\\t
と入力するだけで、ファイル~/.local/state/todo.md
をウィンドウ分割で開きます。
シンボリックリンク
(Vimは関係ないですが)
ToDoファイル自体、場合によっては外部のストレージと同期しておきたい場合もあるでしょう。
私はプライベートの予定は自分のGoogle Driveと同期しています。
しかし、外部ストレージとの同期はサービスによって置き場所が変わったりするので、サービスごとの設定が必要になったりと面倒が増えます。
そんな面倒を避けるため、スタティックリンクを張ってどの端末でも同じ~/.local/state/todo.md
でアクセスできるようにしています。
例:
$ ln -s $HOME/gdrive/my/todo.md $HOME/.local/state/todo.md
リンクをブラウザで開くキーマップ
ToDoファイルには、取り組むべき課題のお題目だけを書くようにしています。
そのため、詳細は例えばGitHubのIssueなど、外部のタスク管理サービスに任せています。
- hogeする kyoh86/todo#123
- fugaする kyoh86/todo#123
そして、ToDoファイルに張られたリンクを直接Vimから開けるようにしています。
local function open_cursor()
local target = vim.fn.expand("<cfile>")
if target == nil then
print("No target found at cursor.")
return
end
target = string.gsub(target --[[@as string]], [[\.+$]], "")
local repo, number = string.match(target, "^([a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+)#(%d+)$")
if repo and number then
-- targetがowner/repo#nnnの形ならgh を呼んで指定のリポジトリのIssueを開く
local cmd = { "gh", "issue", "view", "--repo", repo, "--web", number }
vim.fn.jobstart(cmd, {
on_exit = function(_, code)
if code ~= 0 then
vim.print("Failed to open issue.")
vim.print(repo, number)
end
end,
})
else
vim.ui.open(target)
end
end
vim.keymap.set("n", "<plug>(open-cursor-file)", open_cursor, { silent = true, remap = false, nowait = true })
vim.keymap.set("n", "gx", "<plug>(open-cursor-file)", {})
(Vim script版は誰か書いてくれてもいいんですよ)
外部ファイルのパスや、owner/repo#nnn
の形で書かれたIssueリンクがあれば、カーソルを合わせてgx
とタイプすることで直接開かれるようになっています。
Vimの<cfile>
は、万能ではないですが便利ですね。
チェックボックスを操作するキーマップ
各タスクをMarkdownのチェックリストで書いておくと、1日/1週など特定期間での振り返りがしやすくなります。
とはいえMarkdownのチェックリストは、Vimだと扱いにくいので、カーソル行のチェックリストをチェック済/未チェックで切り替えるキーマップがあると便利です。
local function get_range(args)
-- get target range from user command args
local from = args.line1
local to = args.line2
local another = vim.fn.line("v")
if another == nil then
return nil, nil
end
if from == to and from ~= another then
if another < from then
from = another
else
to = another
end
end
return from, to
end
--- markdownのCheckbox化・CheckboxのオンオフのToggle
local LIST_PATTERN = [[^\s*\([\*+-]\|[0-9]\+\.\)\s\+]]
local function toggle_checkbox(args)
local from, to = get_range(args)
if from == nil or to == nil then
vim.notify("failed to get range to toggle checkbox", vim.log.levels.ERROR)
return
end
local curpos = vim.fn.getcursorcharpos()
local lines = vim.fn.getline(from, to)
for lnum = from, to, 1 do
local line = lines[lnum - from + 1] --[[@as string]]
if not vim.regex(LIST_PATTERN):match_str(line) then
-- not list -> add list marker and blank box
vim.fn.setline(lnum, vim.fn.substitute(line, [[\v\S|$]], [[- [ ] \0]], ""))
if lnum == curpos[1] then
vim.fn.setcursorcharpos({ curpos[1], curpos[2] + 6 })
end
elseif vim.regex(LIST_PATTERN .. [[\[ \]\s\+]]):match_str(line) then
-- blank box -> check
vim.fn.setline(lnum, vim.fn.substitute(line, "\\[ \\]", "[x]", ""))
elseif vim.regex(LIST_PATTERN .. [[\[x\]\s\+]]):match_str(line) then
-- checked box -> uncheck
vim.fn.setline(lnum, vim.fn.substitute(line, "\\[x\\]", "[ ]", ""))
else
-- list but no box -> add box after list marker
vim.fn.setline(lnum, vim.fn.substitute(line, [[\S\+]], "\\0 [ ]", ""))
if lnum == curpos[1] then
vim.fn.setcursorcharpos({ curpos[1], curpos[2] + 4 })
end
end
end
end
vim.api.nvim_buf_create_user_command(0, "MarkdownToggleCheckbox", toggle_checkbox, { range = true, force = true })
vim.keymap.set({ "n", "i", "x" }, "<leader>mcc", "<cmd>MarkdownToggleCheckbox<cr>", { buffer = true })
こちらの機能は別記事で@kawarimidollさんが書いている記事にならっています。
Issueへのリンクを気軽に張る
Issueに気軽に飛べるようにするなら、当然そのリンクを気軽に張れるようにするのも大事です。
Fuzzy FinderなどでIssueを列挙し、リンクをバッファに張る機能をつけておくと便利です。
この例ではddu.vimを使っていますが、他のFuzzy Finderでも同様のことはできるでしょう。便利。
Discussion