diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-24 16:54:10 +0200 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-24 17:02:52 +0200 |
| commit | 7ffbef8b2e8d3ec84aaae44c765f8b599cee5825 (patch) | |
| tree | feb6e96dbc8462e406770d0f9c8b97de21786473 | |
| parent | 856d0b3bbfb190d5768e59be94429c11e6fbdda9 (diff) | |
| download | dotfiles-7ffbef8b2e8d3ec84aaae44c765f8b599cee5825.tar.gz dotfiles-7ffbef8b2e8d3ec84aaae44c765f8b599cee5825.zip | |
refactor(nvim): rewrite goto plugin (now called follow)
`lua/dotfiles/follow.lua` provides an engine to register "followable
entities" like URL, wiki-links etc and can be easily expanded.
The engine provides a function to navigate between followable entities
and a function to edit them inside neovim.
| -rw-r--r-- | .config/nvim/lua/dotfiles/follow.lua | 147 | ||||
| -rw-r--r-- | .config/nvim/lua/dotfiles/goto.lua | 153 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-follow.lua | 256 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-goto.lua | 112 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-notes.lua | 28 |
5 files changed, 412 insertions, 284 deletions
diff --git a/.config/nvim/lua/dotfiles/follow.lua b/.config/nvim/lua/dotfiles/follow.lua new file mode 100644 index 0000000..deff888 --- /dev/null +++ b/.config/nvim/lua/dotfiles/follow.lua @@ -0,0 +1,147 @@ +-- Generic "follow the thing under the cursor" engine. A *followable entity* is a `find`/`open` +-- pair: `find` locates the entity's occurrences in the buffer, `open` acts on one. The engine +-- cycles the cursor between all entities' matches and opens the entity under the cursor, trying +-- entities in registration order, first match wins. `scheme://` resolvers handle targets that +-- carry a scheme. Entities and schemes are wired up in plugin/50-follow.lua. +-- +-- API: +-- register(name, find, open) — add a followable entity (entity contract below) +-- register_scheme(name, handler) — add a `scheme://` resolver (scheme contract below) +-- open(edit_cmd[, target]) — open the entity under the cursor / selection, or `target` +-- next(backward) — move the cursor to the next/previous entity match (wraps) +-- entities — the ordered registry: a list of { name, find, open } +-- `edit_cmd` is one of "edit" | "split" | "tabedit"; `target` is a path or `scheme://uri`. + +local M = {} + +-- Ordered registry of followable entities (the "global table"): +-- { name = string, find = fun(): match[], open = fun(edit_cmd, target) } +-- A `match` is `{ from = {lnum, col}, to = {lnum, col}, target = any }` with 1-based `lnum` and +-- 0-based, end-exclusive byte `col`. A `find` may enumerate the whole buffer (its matches become +-- `next` stops) or return only the match under the cursor (open-only, e.g. `<cfile>` — return a +-- match whose `from` sits at the cursor so it never becomes a `next` target). +M.entities = {} + +-- scheme name -> { resolve = fun(uri): target|nil, err?, after = fun()? } (see `open_target`) +local scheme_handlers = {} + +-- Register a followable entity. `find` and `open` are both required (passed together) so an entity +-- can never be half-defined. Registration order is the try order; `<cfile>`-style fallbacks last. +function M.register(name, find, open) + assert( + type(find) == "function" and type(open) == "function", + "follow.register: both find and open are required" + ) + M.entities[#M.entities + 1] = { name = name, find = find, open = open } +end + +-- Register a `scheme://` resolver. See `scheme_handlers` for the shape. +function M.register_scheme(name, handler) + scheme_handlers[name] = handler +end + +-- The visual selection text (used to follow a target spanning an explicit selection). +local function get_visual_selection() + return table.concat( + vim.fn.getregion(vim.fn.getpos("v"), vim.fn.getpos("."), { type = vim.fn.mode() }), + "\n" + ) +end + +-- Open `target` (a path or `scheme://uri`) with `edit_cmd`, dispatching to a registered scheme +-- resolver when the target carries a scheme. The string-opening half of `M.open`, used as the +-- `open` for the url/markdown-link entities. +local function open_target(edit_cmd, target) + local scheme, uri = string.match(target, "^(%a[%w%+%-%.]+)://(.*)") + local target_is_url = scheme ~= nil + if target_is_url then + uri = vim.uri_decode(uri) + target = scheme .. "://" .. uri + end + local handler = scheme ~= nil and scheme_handlers[scheme] or nil + if handler ~= nil then + local resolved, err = handler.resolve(uri) + if resolved == nil then + if err ~= nil then + vim.notify(err, vim.log.levels.WARN) + end + return + end + target = resolved + end + if target_is_url then + target = vim.fn.fnameescape(target) + end + vim.cmd(edit_cmd .. " " .. target) + if handler ~= nil and handler.after ~= nil then + handler.after() + end +end + +-- Compare two `{lnum, col}` positions in buffer order (by line, then column): `pos_le` is `a <= b` +-- (a is at or before b), `pos_lt` is `a < b` (a is strictly before b). +local function pos_le(a, b) + return a[1] < b[1] or (a[1] == b[1] and a[2] <= b[2]) +end +local function pos_lt(a, b) + return a[1] < b[1] or (a[1] == b[1] and a[2] < b[2]) +end + +-- Move the cursor to the start of the next (or previous, when `backward`) entity match, wrapping +-- around the buffer. Matches whose `from` is at the cursor (open-only entities) are skipped. +function M.next(backward) + local c = vim.api.nvim_win_get_cursor(0) + local cur = { c[1], c[2] } + local best, wrap + for _, e in ipairs(M.entities) do + for _, m in ipairs(e.find()) do + local p = m.from + if not backward then + if pos_lt(cur, p) and (best == nil or pos_lt(p, best)) then + best = p + end + if wrap == nil or pos_lt(p, wrap) then + wrap = p + end + else + if pos_lt(p, cur) and (best == nil or pos_lt(best, p)) then + best = p + end + if wrap == nil or pos_lt(wrap, p) then + wrap = p + end + end + end + end + local dest = best or wrap + if dest ~= nil then + vim.api.nvim_win_set_cursor(0, { dest[1], dest[2] }) + end +end + +-- Open the entity under the cursor (or the visual selection) with `edit_cmd`. With an explicit +-- `target` (a path or `scheme://uri`), open that directly via the scheme-dispatching opener — this +-- form is what the url/markdown-link entities register as their `open`. Entities are tried in +-- registration order; the first whose `find` covers the cursor wins. +function M.open(edit_cmd, target) + if target ~= nil then + open_target(edit_cmd, target) + return + end + if vim.fn.mode():find("^[vV\22]") then + open_target(edit_cmd, get_visual_selection()) + return + end + local c = vim.api.nvim_win_get_cursor(0) + local cur = { c[1], c[2] } + for _, e in ipairs(M.entities) do + for _, m in ipairs(e.find()) do + if pos_le(m.from, cur) and pos_lt(cur, m.to) then + e.open(edit_cmd, m.target) + return + end + end + end +end + +return M diff --git a/.config/nvim/lua/dotfiles/goto.lua b/.config/nvim/lua/dotfiles/goto.lua deleted file mode 100644 index 55c076b..0000000 --- a/.config/nvim/lua/dotfiles/goto.lua +++ /dev/null @@ -1,153 +0,0 @@ --- Generic "follow the thing under the cursor" engine: extracts a target (a markdown link, --- `<cfile>`, or the visual selection), parses an optional `scheme://uri`, and opens it. Features --- register per-scheme resolvers and pre-target follow handlers (e.g. wiki-links) so this file stays --- scheme-agnostic — see 50-notes.lua and 50-nvim-help.lua. - -local M = {} - --- scheme name -> { resolve = fun(uri): target|nil, err?, after = fun()? } --- resolve maps the decoded uri to a path to edit; returning nil aborts (with an optional message). after, if present, --- runs in the freshly-opened buffer. -local scheme_handlers = {} - --- Follow handlers that get first crack before generic target extraction. Each is fun(edit_cmd): handled(boolean), tried --- in registration order. -local follow_handlers = {} - --- Register a `scheme://` resolver. See `scheme_handlers` for the handler shape. -function M.register_scheme(name, handler) - scheme_handlers[name] = handler -end - --- Register a pre-target follow handler. See `follow_handlers` for the shape. -function M.register_handler(fn) - follow_handlers[#follow_handlers + 1] = fn -end - -local function get_visual_selection() - return table.concat( - vim.fn.getregion(vim.fn.getpos("v"), vim.fn.getpos("."), { type = vim.fn.mode() }), - "\n" - ) -end - --- Returns a suitable target for `vim.cmd.edit()` by looking for a Markdown link under the cursor. --- Returns `nil` if no target was found, if the current buffer `'filetype'` is not `markdown`. -local function get_markdown_link_target() - local function get_link_node(node) - local function type_is_link(type) - return type == "full_reference_link" or type == "inline_link" or type == "shortcut_link" - end - if type_is_link(node:type()) then - return node - elseif node:parent() ~= nil and type_is_link(node:parent():type()) then - return node:parent() - end - return nil - end - - local function follow_link_label(label) - -- Escape Lua pattern magic chars so the label is matched literally. - local escaped = label:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1") - -- Reference labels are case-insensitive in CommonMark, so match each letter against either case. - local insensitive = escaped:gsub("%a", function(c) - return "[" .. c:upper() .. c:lower() .. "]" - end) - local label_pattern = "^%[" .. insensitive .. "%]: (.*)" - for _, line in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, true)) do - local match = line:match(label_pattern) - if match ~= nil then - return match - end - end - vim.notify( - "No link destination found for link label [" .. label .. "]", - vim.log.levels.ERROR - ) - return nil - end - - if vim.o.filetype == "markdown" and vim.treesitter.get_parser(0) ~= nil then - -- `ignore_injections = false` because the `link_destination`|`link_label` types are injected by `markdown_inline` - local node = vim.treesitter.get_node({ ignore_injections = false }) - if node == nil then - vim.notify("No node found under cursor", vim.log.levels.ERROR) - return nil - end - local link_node = get_link_node(node) - if link_node == nil then - return nil - end - for child in link_node:iter_children() do - local text = vim.treesitter.get_node_text(child, 0) - if child:type() == "link_destination" then - return text - elseif - child:type() == "link_label" - or (link_node:type() == "shortcut_link" and child:type() == "link_text") - then - -- shortcut_link text don't include the `[]` unlike link_label - if link_node:type() ~= "shortcut_link" then - text = text:sub(2, -2) - end - return follow_link_label(text) - end - end - vim.notify("No link destination/label found in link_node children", vim.log.levels.ERROR) - return nil - end - return nil -end - --- Returns a suitable target for `vim.cmd.edit()`. --- In normal mode, looks for a filename/link under the cursor. --- In selection mode, use the visual selection as-is. -local function get_target() - if vim.fn.mode() == "n" then - return get_markdown_link_target() or vim.fn.expand("<cfile>") - else - return get_visual_selection() - end -end - --- Edit the file/URL under the cursor or in visual selection. --- target: URL or absolute file name --- edit-type: edit|split|vsplit -local function edit_target(target, edit_cmd) - local scheme, uri = string.match(target, "^(%a[%w%+%-%.]+)://(.*)") - local target_is_url = scheme ~= nil - if target_is_url then - uri = vim.uri_decode(uri) - target = scheme .. "://" .. uri - end - local handler = scheme ~= nil and scheme_handlers[scheme] or nil - if handler ~= nil then - local resolved, err = handler.resolve(uri) - if resolved == nil then - if err ~= nil then - vim.notify(err, vim.log.levels.WARN) - end - return - end - target = resolved - end - if target_is_url then - target = vim.fn.fnameescape(target) - end - vim.cmd(edit_cmd .. " " .. target) - if handler ~= nil and handler.after ~= nil then - handler.after() - end -end - --- Follow a registered handler (e.g. a wiki-link) if one claims the cursor else fall back to generic URL/file following. -function M.follow(edit_cmd) - for _, fn in ipairs(follow_handlers) do - if fn(edit_cmd) then - return - end - end - edit_target(get_target(), edit_cmd) -end - -return M diff --git a/.config/nvim/plugin/50-follow.lua b/.config/nvim/plugin/50-follow.lua new file mode 100644 index 0000000..da3bd62 --- /dev/null +++ b/.config/nvim/plugin/50-follow.lua @@ -0,0 +1,256 @@ +-- +-- 50-follow.lua +-- +-- Wires the follow engine (lua/dotfiles/follow.lua): registers the followable entities (in the +-- order they are tried, first match wins) and the `notes://` / `nvim-help://` schemes, then maps +-- the gf-family keys to "open the entity under the cursor" and `]u` / `[u` to cycle between +-- entities. +-- +-- Keymaps: +-- `gf` follow the entity under the cursor in the current window +-- `<C-w>f` / `<C-w><C-f>` ... in a split +-- `<C-w>gf` ... in a new tab +-- `]u` / `[u` go to the next / previous followable entity +-- + +if vim.env.NOTES_DIR == nil then + vim.notify("NOTES_DIR is not set", vim.log.levels.ERROR) + return +end +local notes_dir = vim.fs.normalize(vim.env.NOTES_DIR) + +local df = require("dotfiles.follow") + +---------------------------------------------------------------------------------------------------- +-- Wiki-links -------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + +-- All `[[wiki-link]]` matches in the buffer (markdown only), targeting the inner text. +local function wikilink_matches() + if vim.bo.filetype ~= "markdown" then + return {} + end + local out = {} + for i, line in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do + local init = 1 + while true do + local s, e, inner = line:find("%[%[(.-)%]%]", init) + if s == nil then + break + end + out[#out + 1] = { from = { i, s - 1 }, to = { i, e }, target = inner } + init = e + 1 + end + end + return out +end + +-- Follow a `[[note]]` / `[[note#Heading]]` / `[[dir/]]` wiki-link relative to the notes dir, +-- creating parent directories (and the note itself on save) as needed. +local function open_wikilink(edit_cmd, inner) + local name, heading = inner:match("^(.-)#(.*)$") + if name == nil then + name = inner + end + local target = notes_dir .. "/" .. name + if name:sub(-1) == "/" then -- directory link: create and open it + vim.fn.mkdir(target, "p") + vim.cmd(edit_cmd .. " " .. vim.fn.fnameescape(target)) + return + end + if not name:match("%.%w+$") then -- default to a Markdown note + target = target .. ".md" + end + vim.fn.mkdir(vim.fs.dirname(target), "p") + vim.cmd(edit_cmd .. " " .. vim.fn.fnameescape(target)) + if heading ~= nil and heading ~= "" then + vim.fn.cursor(1, 1) + -- `\V` matches the heading literally; `\v` brackets the heading markers. + local pat = [[\v^#+\s+\V]] .. vim.fn.escape(heading, [[\]]) .. [[\v\s*$]] + if vim.fn.search(pat, "cW") == 0 then + vim.notify("No heading '" .. heading .. "' in " .. name, vim.log.levels.WARN) + end + end +end + +df.register("wikilink", wikilink_matches, open_wikilink) + +---------------------------------------------------------------------------------------------------- +-- Markdown links ---------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + +-- Resolve a reference link `label` to its destination by scanning the buffer for its +-- `[label]: dest` definition (case-insensitive, per CommonMark). Returns the destination, or nil +-- if undefined. +local function resolve_link_label(label) + -- Escape Lua pattern magic chars so the label is matched literally. + local escaped = label:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1") + -- Reference labels are case-insensitive, so match each letter against either case. + local insensitive = escaped:gsub("%a", function(c) + return "[" .. c:upper() .. c:lower() .. "]" + end) + local label_pattern = "^%[" .. insensitive .. "%]: (.*)" + for _, line in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, true)) do + local match = line:match(label_pattern) + if match ~= nil then + return match + end + end + return nil +end + +-- The Markdown link under the cursor (open-only), targeting its destination. Resolves reference +-- labels to their destination. Returns silently when the cursor is not on a link. +local function markdown_link_matches() + if vim.o.filetype ~= "markdown" or vim.treesitter.get_parser(0) == nil then + return {} + end + -- `ignore_injections = false` because `link_destination`/`link_label` are injected by + -- markdown_inline. + local node = vim.treesitter.get_node({ ignore_injections = false }) + if node == nil then + return {} + end + local function is_link(type) + return type == "full_reference_link" or type == "inline_link" or type == "shortcut_link" + end + local link_node + if is_link(node:type()) then + link_node = node + elseif node:parent() ~= nil and is_link(node:parent():type()) then + link_node = node:parent() + end + if link_node == nil then + return {} + end + local dest + for child in link_node:iter_children() do + local text = vim.treesitter.get_node_text(child, 0) + if child:type() == "link_destination" then + dest = text + break + elseif + child:type() == "link_label" + or (link_node:type() == "shortcut_link" and child:type() == "link_text") + then + -- shortcut_link text doesn't include the `[]` unlike link_label + if link_node:type() ~= "shortcut_link" then + text = text:sub(2, -2) + end + dest = resolve_link_label(text) + break + end + end + if dest == nil then + return {} + end + -- Cursor-derived match: anchor `from` at the cursor so it is open-only (never a `next` stop). + local c = vim.api.nvim_win_get_cursor(0) + return { { from = { c[1], c[2] }, to = { c[1], c[2] + 1 }, target = dest } } +end + +df.register("markdown-link", markdown_link_matches, df.open) + +---------------------------------------------------------------------------------------------------- +-- URLs -------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + +-- All `scheme://…` matches in the buffer. +local function url_matches() + local out = {} + for i, line in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do + local init = 1 + while true do + local s, e = line:find("%a[%w+.-]*://%S+", init) + if s == nil then + break + end + out[#out + 1] = { from = { i, s - 1 }, to = { i, e }, target = line:sub(s, e) } + init = e + 1 + end + end + return out +end + +df.register("url", url_matches, df.open) + +---------------------------------------------------------------------------------------------------- +-- <cfile> fallback -------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + +-- A single open-only match at the cursor when there is a `<cfile>` under it. +local function cfile_matches() + if vim.fn.expand("<cfile>") == "" then + return {} + end + local c = vim.api.nvim_win_get_cursor(0) + return { { from = { c[1], c[2] }, to = { c[1], c[2] + 1 }, target = true } } +end + +-- Fall back to Vim's built-in gf-family (so 'path', 'suffixesadd', … apply); `feedkeys` with `n` +-- bypasses our own mappings to avoid recursion. +local builtin_gf = { + edit = vim.keycode("gf"), + split = vim.keycode("<C-w>f"), + tabedit = vim.keycode("<C-w>gf"), +} +local function open_cfile(edit_cmd, _) + vim.api.nvim_feedkeys(builtin_gf[edit_cmd], "nx", false) +end + +df.register("cfile", cfile_matches, open_cfile) + +---------------------------------------------------------------------------------------------------- +-- Schemes ----------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + +-- `notes://<path>` resolves to `<path>` relative to the notes dir. +df.register_scheme("notes", { + resolve = function(uri) + return notes_dir .. "/" .. uri + end, +}) + +-- `nvim-help://<tag>` — open Neovim help for <tag> in a scratch, read-only buffer. +df.register_scheme("nvim-help", { + resolve = function(uri) + local tagfiles = {} + for _, path in pairs(vim.opt.runtimepath:get()) do + tagfiles[#tagfiles + 1] = path .. "/doc/tags" + end + vim.opt_local.tags = tagfiles + local matches = vim.fn.taglist(uri) + if #matches == 0 then + return nil, "No help page found for nvim-help://" .. uri + end + return matches[1].filename + end, + after = function() + vim.opt_local.bufhidden = "wipe" + vim.opt_local.buftype = "nofile" + vim.opt_local.swapfile = false + vim.opt_local.readonly = true + end, +}) + +---------------------------------------------------------------------------------------------------- +-- Keymaps ----------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + +local function opener(edit_cmd) + return function() + df.open(edit_cmd) + end +end + +vim.keymap.set({ "n", "x" }, "gf", opener("edit"), { desc = "Follow entity under cursor" }) +vim.keymap.set({ "n", "x" }, "<C-w>f", opener("split"), { desc = "Follow entity in a split" }) +vim.keymap.set({ "n", "x" }, "<C-w><C-f>", opener("split"), { desc = "Follow entity in a split" }) +vim.keymap.set({ "n", "x" }, "<C-w>gf", opener("tabedit"), { desc = "Follow entity in a new tab" }) + +vim.keymap.set("n", "]u", function() + df.next(false) +end, { desc = "Go to next followable entity" }) +vim.keymap.set("n", "[u", function() + df.next(true) +end, { desc = "Go to previous followable entity" }) diff --git a/.config/nvim/plugin/50-goto.lua b/.config/nvim/plugin/50-goto.lua deleted file mode 100644 index 8c72c15..0000000 --- a/.config/nvim/plugin/50-goto.lua +++ /dev/null @@ -1,112 +0,0 @@ --- --- 50-goto.lua --- - -local follow = require("dotfiles.goto") - --- Add `'` to the list of characters included in `<cfile>` because `'` is a valid URI character -vim.opt.isfname:append("'") - -vim.keymap.set({ "n", "x" }, "<Leader>gg", function() - follow.follow("edit") -end, { desc = "Edit URL/file in current window" }) -vim.keymap.set({ "n", "x" }, "<Leader>gs", function() - follow.follow("split") -end, { desc = "Edit URL/file in split window" }) -vim.keymap.set({ "n", "x" }, "<Leader>gv", function() - follow.follow("vsplit") -end, { desc = "Edit URL/file in vertically split window" }) - ------------------------------------------------------------------------------------------------------------------------- --- Following notes (`notes://` scheme and `[[wiki-links]]`), wired into the follow engine ------------------------------------------------------------------------------------------------------------------------- - --- `notes://<path>` resolves to `<path>` relative to the notes dir. -follow.register_scheme("notes", { - resolve = function(uri) - return notes_dir .. "/" .. uri - end, -}) - --- Returns the inner text of a `[[wiki-link]]` under the cursor, or `nil`. -local function get_wikilink_target() - local line = vim.api.nvim_get_current_line() - local col = vim.api.nvim_win_get_cursor(0)[2] + 1 - local init = 1 - while true do - local s, e, inner = line:find("%[%[(.-)%]%]", init) - if s == nil then - return nil - end - if col >= s and col <= e then - return inner - end - init = e + 1 - end -end - --- Follow a `[[note]]` / `[[note#Heading]]` / `[[dir/]]` wiki-link relative to the --- notes dir, creating parent directories (and the note itself on save) as needed. -local function follow_wikilink(inner, edit_cmd) - local name, heading = inner:match("^(.-)#(.*)$") - if name == nil then - name = inner - end - local target = notes_dir .. "/" .. name - if name:sub(-1) == "/" then -- directory link: create and open it - vim.fn.mkdir(target, "p") - vim.cmd(edit_cmd .. " " .. vim.fn.fnameescape(target)) - return - end - if not name:match("%.%w+$") then -- default to a Markdown note - target = target .. ".md" - end - vim.fn.mkdir(vim.fs.dirname(target), "p") - vim.cmd(edit_cmd .. " " .. vim.fn.fnameescape(target)) - if heading ~= nil and heading ~= "" then - vim.fn.cursor(1, 1) - -- `\V` matches the heading literally; `\v` brackets the heading markers. - local pat = [[\v^#+\s+\V]] .. vim.fn.escape(heading, [[\]]) .. [[\v\s*$]] - if vim.fn.search(pat, "cW") == 0 then - vim.notify("No heading '" .. heading .. "' in " .. name, vim.log.levels.WARN) - end - end -end - --- Wiki-links take precedence over generic URL/file following, but only on a --- `[[...]]` under the cursor in a markdown buffer in normal mode. -follow.register_handler(function(edit_cmd) - if vim.fn.mode() ~= "n" or vim.bo.filetype ~= "markdown" then - return false - end - local inner = get_wikilink_target() - if inner == nil then - return false - end - follow_wikilink(inner, edit_cmd) - return true -end) - --- `nvim-help://<tag>` — open Neovim help for <tag> in a scratch, read-only --- buffer. Registered with the follow engine (see 50-follow.lua). - -require("dotfiles.goto").register_scheme("nvim-help", { - resolve = function(uri) - local tagfiles = {} - for _, path in pairs(vim.opt.runtimepath:get()) do - tagfiles[#tagfiles + 1] = path .. "/doc/tags" - end - vim.opt_local.tags = tagfiles - local matches = vim.fn.taglist(uri) - if #matches == 0 then - return nil, "No help page found for nvim-help://" .. uri - end - return matches[1].filename - end, - after = function() - vim.opt_local.bufhidden = "wipe" - vim.opt_local.buftype = "nofile" - vim.opt_local.swapfile = false - vim.opt_local.readonly = true - end, -}) diff --git a/.config/nvim/plugin/50-notes.lua b/.config/nvim/plugin/50-notes.lua index 30c655c..4e0494c 100644 --- a/.config/nvim/plugin/50-notes.lua +++ b/.config/nvim/plugin/50-notes.lua @@ -23,7 +23,7 @@ -- `<Leader>nn`: fuzzy-find notes by tags -- `<Leader>nf`: fuzzy-find notes by file name -- `<Leader>ng`: fuzzy-find notes by content --- `<Tab>` / `<S-Tab>` (in notes buffers): go to next/previous URL or wiki-link +-- `<Tab>` / `<S-Tab>` (in notes buffers): go to next/previous followable entity -- if vim.env.NOTES_DIR == nil then @@ -33,8 +33,10 @@ end local notes_dir = vim.fs.normalize(vim.env.NOTES_DIR) local tagfile = vim.fs.joinpath(vim.fn.stdpath("state"), "notes-tags") +local follow = require("dotfiles.follow") + ---------------------------------------------------------------------------------------------------- --- Tags --------------------------------------------------------------------------------------------- +-- Tags -------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- -- Generates ctags for (markdown) files in `notes_dir` @@ -417,36 +419,24 @@ local function rename_section_command(opts) end ---------------------------------------------------------------------------------------------------- --- Navigation -------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------- - --- Moves the cursor to the start of the next (or previous, when `backward`) URL or `[[wiki-link]]`, --- wrapping around the buffer. "URL" means a `scheme://…` target, matching the follow engine in --- 50-goto.lua. -local function goto_url(backward) - local pattern = [[\v(\a[0-9A-Za-z+.-]*://\S+|\[\[.{-}\]\])]] - vim.fn.search(pattern, backward and "bw" or "w") -end - ----------------------------------------------------------------------------------------------------- -- Buffer init & pickers --------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- -- Initializes a notes buffer -- * Include generated notes tags in 'tags' option -- * Enable wikilink completion --- * Map <Tab>/<S-Tab> to jump between URLs/wiki-links +-- * Map <Tab>/<S-Tab> to jump between followable entities local function init_notes_buffer() if not vim.tbl_contains(vim.opt_local.tags:get(), tagfile) then vim.opt_local.tags:append(tagfile) end vim.bo.omnifunc = "v:lua.dotfiles_wikilink_source" vim.keymap.set("n", "<Tab>", function() - goto_url(false) - end, { buffer = true, desc = "Go to next URL/wiki-link" }) + follow.next(false) + end, { buffer = true, desc = "Go to next followable entity" }) vim.keymap.set("n", "<S-Tab>", function() - goto_url(true) - end, { buffer = true, desc = "Go to previous URL/wiki-link" }) + follow.next(true) + end, { buffer = true, desc = "Go to previous followable entity" }) end local function find_notes_by_tag() |
