wikilink.lua
dotfiles.git > main > 4243021 > files / nvim / lua / wikilink.lua
--- Follow [[wikilinks]] and [text](local/links.md) like Obsidian does
local fn = vim.fn
--- Returns the note name and anchor (heading or ^block) of the link under the cursor, or nil
local function link_under_cursor()
local line = vim.api.nvim_get_current_line()
local col = vim.api.nvim_win_get_cursor(0)[2] + 1
for s, target, e in line:gmatch("()%[%[(.-)%]%]()") do
if col >= s and col < e then
return target:gsub("|.*", ""):match("^([^#]*)#?(.*)$")
end
end
for s, target, e in line:gmatch("()%[[^%]]*%]%(([^)]+)%)()") do
if col >= s and col < e and not target:match("^%a+:") then
local name, anchor = vim.uri_decode(target):match("^([^#]*)#?(.*)$")
--- Markdown links use #some-heading slugs, turn them back into words
return name, (anchor:gsub("-", " "))
end
end
end
--- The vault is the nearest dir with .obsidian (or .git), falling back to the file's own dir
local function vault_root()
return vim.fs.root(0, {".obsidian", ".git"}) or fn.expand("%:p:h")
end
--- Obsidian matches by note name anywhere in the vault, or by path if one is given
local function resolve(name)
if not name:match("%.%w+$") then name = name .. ".md" end
local here = fn.expand("%:p:h")
local root = vault_root()
for _, dir in ipairs({here, root}) do
local path = vim.fs.joinpath(dir, name)
if fn.filereadable(path) == 1 then return path end
end
local matches = vim.fs.find(function(f, dir)
return vim.fs.joinpath(dir, f):sub(-#name - 1) == "/" .. name
end, {path = root, type = "file", limit = math.huge})
--- Shortest path wins, same as Obsidian
table.sort(matches, function(a, b) return #a < #b end)
if matches[1] then return matches[1] end
--- Doesn't exist yet, so make it next to the current note
return vim.fs.joinpath(here, name)
end
--- Jumps to the "# Heading" or "^block-id" in the current buffer
local function jump_to_anchor(anchor)
local pat = anchor:sub(1, 1) == "^"
and "\\V " .. fn.escape(anchor, "\\") .. "\\$"
or "\\c\\V\\^#\\+\\s\\+" .. fn.escape(anchor, "\\") .. "\\s\\*\\$"
if fn.search(pat, "cw") == 0 then
vim.notify("No anchor " .. anchor, vim.log.levels.WARN)
end
end
--- Follows the link under the cursor, otherwise runs `fallback` as a normal mode command
local function follow(fallback)
local name, anchor = link_under_cursor()
if not name then
return vim.cmd("normal! " .. fallback)
end
if name ~= "" then
vim.cmd.edit(fn.fnameescape(resolve(name)))
else
vim.cmd("normal! m'")
end
if anchor ~= "" then jump_to_anchor(anchor) end
end
return {follow = follow}