-- 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 `` 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("") 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" }, "gg", function() edit_target(get_target(), "edit") end, { desc = "Edit URL/file in current window" }) vim.keymap.set({ "n", "x" }, "gs", function() edit_target(get_target(), "split") end, { desc = "Edit URL/file in split window" }) vim.keymap.set({ "n", "x" }, "gv", function() edit_target(get_target(), "vsplit") end, { desc = "Edit URL/file in vertically split window" })