diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-07 20:58:17 +0200 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-08 12:37:59 +0200 |
| commit | b3bf3fb2ee8605344a4e4d68807d0dfa22f48561 (patch) | |
| tree | 01509ab814cce648ed4d79670f0f9233524673bc /.config/nvim/plugin | |
| parent | cdb8a3148654d740f84a611b1424ef88bf6f9014 (diff) | |
| download | dotfiles-b3bf3fb2ee8605344a4e4d68807d0dfa22f48561.tar.gz dotfiles-b3bf3fb2ee8605344a4e4d68807d0dfa22f48561.zip | |
refactor(nvim): extract follow engine into dotfiles.follow module
Diffstat (limited to '.config/nvim/plugin')
| -rw-r--r-- | .config/nvim/plugin/50-follow.lua | 180 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-notes.lua | 72 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-nvim-help.lua | 23 |
3 files changed, 103 insertions, 172 deletions
diff --git a/.config/nvim/plugin/50-follow.lua b/.config/nvim/plugin/50-follow.lua index 6784ad6..1e48cd5 100644 --- a/.config/nvim/plugin/50-follow.lua +++ b/.config/nvim/plugin/50-follow.lua @@ -1,183 +1,19 @@ --- Following file names and URL inside Neovim +-- Following file names and URLs inside Neovim. The engine lives in +-- `lua/dotfiles/follow.lua`; per-scheme behavior is registered by the feature +-- that owns it — see 50-notes.lua (`notes://` and `[[wiki-links]]`) and +-- 50-nvim-help.lua (`nvim-help://`). -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 +local follow = require("dotfiles.follow") -- Add `'` to the list of characters included in `<cfile>` because `'` is a valid URI character vim.opt.isfname:append("'") --- 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%+%-%.]+)://(.*)") - if scheme ~= nil then -- if the target is a URL (and not a file name) - uri = vim.uri_decode(uri) - target = scheme .. "://" .. uri - end - if scheme == "notes" then - target = vim.g.notes_dir .. "/" .. target - elseif scheme == "nvim-help" then - local tagfiles = {} - for _, path in pairs(vim.opt.runtimepath:get()) do - table.insert(tagfiles, path .. "/doc/tags") - end - vim.opt_local.tags = tagfiles - local matches = vim.fn.taglist(uri) - if #matches == 0 then - vim.notify("No help page found for " .. target, vim.log.levels.WARN) - return - end - target = matches[1].filename - end - vim.cmd(edit_cmd .. " " .. vim.fn.fnameescape(target)) - if scheme == "nvim-help" then - vim.opt_local.bufhidden = "wipe" - vim.opt_local.buftype = "nofile" - vim.opt_local.swapfile = false - vim.opt_local.readonly = true - end -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 = vim.g.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 - --- Follow a wiki-link if the cursor is on one, else fall back to URL/file following. -local function follow(edit_cmd) - if vim.fn.mode() == "n" and vim.bo.filetype == "markdown" then - local inner = get_wikilink_target() - if inner ~= nil then - follow_wikilink(inner, edit_cmd) - return - end - end - edit_target(get_target(), edit_cmd) -end - vim.keymap.set({ "n", "x" }, "<Leader>gg", function() - follow("edit") + follow.follow("edit") end, { desc = "Edit URL/file in current window" }) vim.keymap.set({ "n", "x" }, "<Leader>gs", function() - follow("split") + follow.follow("split") end, { desc = "Edit URL/file in split window" }) vim.keymap.set({ "n", "x" }, "<Leader>gv", function() - follow("vsplit") + follow.follow("vsplit") end, { desc = "Edit URL/file in vertically split window" }) diff --git a/.config/nvim/plugin/50-notes.lua b/.config/nvim/plugin/50-notes.lua index dee4da3..fec0dd2 100644 --- a/.config/nvim/plugin/50-notes.lua +++ b/.config/nvim/plugin/50-notes.lua @@ -325,3 +325,75 @@ vim.api.nvim_create_user_command("NotesRenameSection", function(opts) end) end end, { nargs = "*", desc = "Rename the heading at the cursor and update wiki-links" }) + +------------------------------------------------------------------------------------------------------------------------ +-- Following notes (`notes://` scheme and `[[wiki-links]]`), wired into the follow engine +------------------------------------------------------------------------------------------------------------------------ + +local follow = require("dotfiles.follow") + +-- `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) diff --git a/.config/nvim/plugin/50-nvim-help.lua b/.config/nvim/plugin/50-nvim-help.lua new file mode 100644 index 0000000..926bb68 --- /dev/null +++ b/.config/nvim/plugin/50-nvim-help.lua @@ -0,0 +1,23 @@ +-- `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.follow").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, +}) |
