summaryrefslogtreecommitdiffstats
path: root/.config
diff options
context:
space:
mode:
Diffstat (limited to '.config')
-rw-r--r--.config/nvim/plugin/50-follow.lua126
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" })