summaryrefslogtreecommitdiffstats
path: root/.config/nvim/plugin/50-follow.lua
blob: 2999b12398ef553a9a7c6840b6f327545c72baf0 (plain)
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" })