diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-07 14:19:26 +0200 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-06-07 14:57:24 +0200 |
| commit | a189faf7411549d27ba9e7218a0c86347197c6cd (patch) | |
| tree | 42f5667519bcbdf7f1c545925057153cef1637c2 /.config/nvim/plugin | |
| parent | e5ceebe1d6444d501ad799f76bafb071fedb0840 (diff) | |
| download | dotfiles-a189faf7411549d27ba9e7218a0c86347197c6cd.tar.gz dotfiles-a189faf7411549d27ba9e7218a0c86347197c6cd.zip | |
feat(nvim): add follow plugin (open file/URL)
Diffstat (limited to '.config/nvim/plugin')
| -rw-r--r-- | .config/nvim/plugin/50-follow.lua | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/.config/nvim/plugin/50-follow.lua b/.config/nvim/plugin/50-follow.lua new file mode 100644 index 0000000..2999b12 --- /dev/null +++ b/.config/nvim/plugin/50-follow.lua @@ -0,0 +1,126 @@ +-- Following file names and URL inside Neovim + +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 + +-- 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 + +vim.keymap.set({ "n", "x" }, "<Leader>gg", function() + edit_target(get_target(), "edit") +end, { desc = "Edit URL/file in current window" }) +vim.keymap.set({ "n", "x" }, "<Leader>gs", function() + edit_target(get_target(), "split") +end, { desc = "Edit URL/file in split window" }) +vim.keymap.set({ "n", "x" }, "<Leader>gv", function() + edit_target(get_target(), "vsplit") +end, { desc = "Edit URL/file in vertically split window" }) |
