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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
-- Generic "follow the thing under the cursor" engine: extracts a target (a markdown link,
-- `<cfile>`, or the visual selection), parses an optional `scheme://uri`, and opens it. Features
-- register per-scheme resolvers and pre-target follow handlers (e.g. wiki-links) so this file stays
-- scheme-agnostic — see 50-notes.lua and 50-nvim-help.lua.
local M = {}
-- scheme name -> { resolve = fun(uri): target|nil, err?, after = fun()? }
-- resolve maps the decoded uri to a path to edit; returning nil aborts (with an optional message). after, if present,
-- runs in the freshly-opened buffer.
local scheme_handlers = {}
-- Follow handlers that get first crack before generic target extraction. Each is fun(edit_cmd): handled(boolean), tried
-- in registration order.
local follow_handlers = {}
-- Register a `scheme://` resolver. See `scheme_handlers` for the handler shape.
function M.register_scheme(name, handler)
scheme_handlers[name] = handler
end
-- Register a pre-target follow handler. See `follow_handlers` for the shape.
function M.register_handler(fn)
follow_handlers[#follow_handlers + 1] = fn
end
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
-- 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%+%-%.]+)://(.*)")
local target_is_url = scheme ~= nil
if target_is_url then
uri = vim.uri_decode(uri)
target = scheme .. "://" .. uri
end
local handler = scheme ~= nil and scheme_handlers[scheme] or nil
if handler ~= nil then
local resolved, err = handler.resolve(uri)
if resolved == nil then
if err ~= nil then
vim.notify(err, vim.log.levels.WARN)
end
return
end
target = resolved
end
if target_is_url then
target = vim.fn.fnameescape(target)
end
vim.cmd(edit_cmd .. " " .. target)
if handler ~= nil and handler.after ~= nil then
handler.after()
end
end
-- Follow a registered handler (e.g. a wiki-link) if one claims the cursor else fall back to generic URL/file following.
function M.follow(edit_cmd)
for _, fn in ipairs(follow_handlers) do
if fn(edit_cmd) then
return
end
end
edit_target(get_target(), edit_cmd)
end
return M
|