Neovimで見出し付きwikilinkのジャンプとリンク生成

gfを実行するとカーソル下のファイルが開きます。筆者は「Markdownのときは見出しの箇所までジャンプしてほしい」と考えました。実装してみたのでその備忘録です。

見出し付きリンクへのジャンプとは

次のような## この見出しにジャンプしたいへジャンプするためのリンクを作り、ジャンプさせます。

test.md
# タイトル
……適当な文章なのだ。
## この見出しにジャンプしたい
適当な文章なのだ……

リンクは次のような書き方を想定しています。

リンク
[[test.md##この見出しにジャンプしたい|タイトル]]

現在のファイルのリンクを取得するキーマップ

まずはリンクを作ってクリップボードに入れるキーマップを紹介します。

リンク生成時のタイトルはそのMarkdownファイルの一行目のh1とします。

実装全体

先に実装全体を載せます。次の項目から小分けにして解説します。

.config/nvim/after/ftplugin/markdown.lua
vim.keymap.set("n", "<space>mw", function()
-- 現在の位置をmで記録
vim.cmd("mark z")
-- 現在のテキストの見出しに移動
vim.fn.search("^#", "b")
-- 見出しテキストを取得してエスケープする
local heading_link = ""
if vim.fn.line(".") ~= 1 then
local line = vim.api.nvim_get_current_line()
heading_link = line:gsub("(#+)%s+([^#]*)", function(hashes, heading)
-- 見出しテキストをエスケープ
local escaped_heading = heading:gsub("[][%s^$*(){}|]", "-")
return hashes .. escaped_heading
end)
-- 一行目に移動
vim.api.nvim_win_set_cursor(0, { 1, 0 })
end
-- タイトルの取得
local line = vim.api.nvim_get_current_line()
local title = line:match("#%s+([^#]*)")
-- リンクの取得
local current_file = vim.fn.expand("%")
local link = "[[" .. current_file .. heading_link .. "|" .. title .. "]]"
vim.fn.setreg("+", link)
vim.print("yank '" .. link .. "'")
-- 元の位置に戻る
vim.cmd("normal `z")
end, { desc = "現在行のwikilinkを取得", buffer = true })

位置の記録

このキーマップでは途中でカーソル行を移動します。そのため、処理が終わったら元の位置に戻れるようにあらかじめ記録しておきます。

vim.cmd("mark z")

上記はノーマルモードのmzと同じです。

いったん見出しに移動

「現在のカーソル行より前にある見出し」に移動します。

vim.fn.search("^#", "b")

第2引数は「カーソル行よりも上を検索する」というフラグです。ノーマルモードの?による検索と同じことをしています。

見出しテキストの取得

移動後の見出しが1行目ではない場合、見出しテキストを抜き出します。前述のとおり1行目ならタイトルとなるため、見出しテキストは空です。

local heading_link = ""
if vim.fn.line(".") ~= 1 then
local line = vim.api.nvim_get_current_line()
heading_link = line:gsub("(#+)%s+([^#]*)", function(hashes, heading)
-- 見出しテキストをエスケープ
local escaped_heading = heading:gsub("[][%s^$*(){}|]", "-")
return hashes .. escaped_heading
end)
-- 1行目に移動
vim.api.nvim_win_set_cursor(0, { 1, 0 })
end

見出しのテキストをLua Patternで取得します。

local heading_link = ""
if vim.fn.line(".") ~= 1 then
local line = vim.api.nvim_get_current_line()
heading_link = line:gsub("(#+)%s+([^#]*)", function(hashes, heading)
-- 見出しテキストをエスケープ
local escaped_heading = heading:gsub("[][%s^$*(){}|]", "-")
return hashes .. escaped_heading
end)
-- 一行目に移動
vim.api.nvim_win_set_cursor(0, { 1, 0 })
end

見出しの一部の文字は-に置換しています。ジャンプのキーマップを簡単にするためにこのような仕様にしました(後述)。

たとえば、次のように変換されます。

  • 変換前:## これが 見出しです $についての見出しです
  • 変換後:##これが-見出しです--についての見出しです

タイトルの取得

Lua Patternでタイトル(ファイルの1行目の見出しテキスト)を取得します。

local line = vim.api.nvim_get_current_line()
local title = line:match("#%s+([^#]*)")

リンクの生成

wikilinkの形式でリンクを生成します。

local current_file = vim.fn.expand("%")
local link = "[[" .. current_file .. heading_link .. "|" .. title .. "]]"

リンクのコピー

リンクを+レジスタに入れることでコピーします。

vim.fn.setreg("+", link)

元の位置に戻る

カーソルを最初に記録した位置に戻します。

vim.cmd("normal `z")

ジャンプするためのキーマップ

カーソル位置にあるwikilinkを解釈してジャンプするキーマップです。Stack Exchangeの投稿を参考にLua化して実装しました。

.config/nvim/after/ftplugin/markdown.lua
vim.keymap.set("n", "gf", function()
local cfile = vim.fn.expand("<cfile>")
-- ([^#]*) -> ファイル名 -> %1
-- (#+) -> 見出しのシャープ -> %2
-- ([^#]*) -> 見出しテキスト -> %3
local arg = cfile:gsub("([^#]*)(#+)([^#]*)", function(filename, hashes, heading)
-- 見出しテキストの`-`をエスケープ
local escaped_heading = heading:gsub("-", ".")
return "+/" .. hashes .. "\\ " .. escaped_heading .. " " .. filename
end)
vim.cmd("edit " .. arg)
end, { desc = "gfを拡張: 見出しリンクも辿れる", buffer = true })

リンクテキストの取得

リンクが[[test.md##この見出しにジャンプしたい|タイトル]]だとすると、cfiletest.md##この見出しにジャンプしたいの部分です。

local cfile = vim.fn.expand("<cfile>")

見出しテキストに半角スペースが含まれるとこのAPIでは取得できません。そのため、前述のとおりリンク生成時に-へ置換したのでした。

置換

リンク生成時に置換されて-になった部分は.に変換しておきます。

-- ([^#]*) -> ファイル名 -> %1
-- (#+) -> 見出しのシャープ -> %2
-- ([^#]*) -> 見出しテキスト -> %3
local arg = cfile:gsub("([^#]*)(#+)([^#]*)", function(filename, hashes, heading)
-- 見出しテキストの`-`をエスケープ
local escaped_heading = heading:gsub("-", ".")
return "+/" .. hashes .. "\\ " .. escaped_heading .. " " .. filename
end)

最初から-ではなく.に置換すればいいのでは?と思うかもしれません。それはそうなのですが、筆者の好みです。

ファイルを開く

実際にファイルを開くときには次のようなコマンドが実行されます。

:edit +/##\ この見出しにジャンプしたい test.md

:edit+/fooのようなオプションがあります。これを使うとファイルを開いた時に検索し、カーソルを移動してくれます(詳しくは:help +cmd)。

見出しテキストに()などの記号が含まれると正しく移動できません。そのため、前述のとおりリンク生成時に-へ置換したのでした。


以上、見出し付きwikilinkをNeovimで扱うためのキーマップでした。ちょっとオレオレ仕様ですが、メモ管理でしか使わないのでこれで満足です。

筆者はよく引っかかるのですが、Lua Patternは正規表現でありません。使えない概念がいくつかあります。Lua Patternを使うときはLua Patterns Viewerで想定どおりかどうかチェックしましょう。