1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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" })
|