Skip to content

Commit 1bc9256

Browse files
authored
Refactoring/hyperlinks (#588)
1 parent ff7d4bc commit 1bc9256

22 files changed

+3007
-2093
lines changed

lua/orgmode/objects/link.lua

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
local Url = require('orgmode.objects.url')
2+
3+
---@class Link
4+
---@field url Url
5+
---@field desc string | nil
6+
local Link = {}
7+
8+
---@param str string
9+
function Link:init(str)
10+
local parts = vim.split(str, '][', true)
11+
self.url = Url.new(parts[1] or '')
12+
self.desc = parts[2]
13+
end
14+
15+
---@return string
16+
function Link:to_str()
17+
if self.desc then
18+
return string.format('[[%s][%s]]', self.url.str, self.desc)
19+
else
20+
return string.format('[[%s]]', self.url.str)
21+
end
22+
end
23+
24+
---@param str string
25+
function Link.new(str)
26+
local self = setmetatable({}, { __index = Link })
27+
self:init(str)
28+
return self
29+
end
30+
31+
---@param line string
32+
---@param pos number
33+
---@return Link | nil
34+
function Link.at_pos(line, pos)
35+
local links = {}
36+
local found_link = nil
37+
local pattern = '%[%[([^%]]+.-)%]%]'
38+
for link in line:gmatch(pattern) do
39+
local start_from = #links > 0 and links[#links].to or nil
40+
local from, to = line:find(pattern, start_from)
41+
if pos >= from and pos <= to then
42+
found_link = link
43+
break
44+
end
45+
table.insert(links, { link = link, from = from, to = to })
46+
end
47+
return (found_link and Link.new(found_link) or nil)
48+
end
49+
50+
return Link

lua/orgmode/objects/url.lua

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
local utils = require('orgmode.utils')
2+
local fs = require('orgmode.utils.fs')
3+
4+
---@class Url
5+
---@field str string
6+
local Url = {}
7+
8+
function Url:init(str)
9+
self.str = str
10+
end
11+
12+
function Url.new(str)
13+
local self = setmetatable({}, { __index = Url })
14+
self:init(str)
15+
return self
16+
end
17+
18+
---@return boolean
19+
function Url:is_file_line_number()
20+
return self:get_linenumber() and true
21+
end
22+
23+
---@return boolean
24+
function Url:is_headline()
25+
return self:is_file_headline() or self:is_internal_headline()
26+
end
27+
28+
---@return boolean
29+
function Url:is_file_headline()
30+
return self:is_file() and self:get_headline() and true
31+
end
32+
33+
function Url:is_custom_id()
34+
return self:is_file_custom_id() or self:is_internal_custom_id()
35+
end
36+
37+
---@return boolean
38+
function Url:is_file_custom_id()
39+
return self:is_file() and self:get_custom_id() and true
40+
end
41+
42+
---@return boolean
43+
function Url:is_file_anchor()
44+
return self:get_dedicated_target() and true
45+
end
46+
47+
---@return boolean
48+
function Url:is_org_link()
49+
return (self:get_dedicated_target() or self:get_custom_id() or self:get_headline()) and true
50+
end
51+
52+
function Url:is_file()
53+
return self.str:find('^file:') or self.str:find('^./') or self.str:find('^/')
54+
end
55+
56+
function Url:is_file_plain()
57+
return self:is_file() and not self:is_org_link() and not self:is_file_line_number()
58+
end
59+
60+
---@return boolean
61+
function Url:is_http_url()
62+
return self:get_http_url() and true
63+
end
64+
65+
---@return boolean
66+
function Url:is_internal_headline()
67+
return self.str:find('^*') and true
68+
end
69+
70+
function Url:is_internal_custom_id()
71+
return self.str:find('^#')
72+
end
73+
74+
function Url:is_dedicated_anchor_or_internal_title()
75+
return self:get_dedicated_target() ~= nil
76+
end
77+
78+
---@return string | false
79+
function Url:extract_path()
80+
local url = self
81+
if url:is_file_headline() or url:is_file_custom_id() then
82+
return url.str:match('^file:([^:]-)::') or url.str:match('^(./[^:]-)::') or url.str:match('^(/[^:]-)::')
83+
elseif url:is_file_line_number() then
84+
return url.str:match('^file:([^:]-) %+') or url.str:match('^(./[^:]-) %+') or url.str:match('^(/[^:]-) %+')
85+
elseif url:is_file_plain() then
86+
return url.str:match('^file:([^:]-)$') or url.str:match('^(./[^:]-)$') or url.str:match('^(/[^:]-)$')
87+
else
88+
return false
89+
end
90+
end
91+
92+
---@return string | false
93+
function Url:get_file_real_path()
94+
local filepath = self:get_filepath()
95+
return filepath and fs.get_real_path(filepath)
96+
end
97+
98+
---@return string | false
99+
function Url:get_headline()
100+
return self.str:match('^file:[^:]+::%*(.-)$')
101+
or self.str:match('^./[^:]+::%*(.-)$')
102+
or self.str:match('^/[^:]+::%*(.-)$')
103+
or self.str:match('^%*(.-)$')
104+
end
105+
106+
---@return string | false
107+
function Url:get_custom_id()
108+
return self.str:match('^file:[^:]+::#(.-)$')
109+
or self.str:match('^./[^:]+::#(.-)$')
110+
or self.str:match('^/[^:]+::#(.-)$')
111+
or self.str:match('^#(.-)$')
112+
end
113+
114+
---@return number | false
115+
function Url:get_linenumber()
116+
-- official orgmode convention
117+
return self.str:match('^file:[^:]+::(%d+)$')
118+
or self.str:match('^./[^:]+::(%d+)$')
119+
or self.str:match('^/[^:]+::(%d+)$')
120+
-- for backwards compatibility
121+
or self.str:match('^file:[^:]+ %+(%d+)$')
122+
or self.str:match('^./[^:]+ %+(%d+)$')
123+
or self.str:match('^/[^:]+ %+(%d+)$')
124+
end
125+
126+
---@return string | false
127+
function Url:get_filepath()
128+
return
129+
-- for backwards compatibility
130+
self.str:match('^file:([^:]+) %+%d+')
131+
or self.str:match('^(%./[^:]+) %+%d+')
132+
or self.str:match('^(/[^:]+) %+%d+')
133+
-- official orgmode convention
134+
or self.str:match('^file:([^:]+)::')
135+
or self.str:match('^(%./[^:]+)::')
136+
or self.str:match('^(/[^:]+)::')
137+
or self.str:match('^file:([^:]+)$')
138+
or self.str:match('^(%./[^:]+)$')
139+
or self.str:match('^(/[^:]+)$')
140+
or self.str:match('^(%./)$')
141+
or self.str:match('^(/)$')
142+
end
143+
--
144+
---@return string
145+
function Url:get_headline_completion()
146+
return self.str:match('^.+::%*(.*)$') or self.str:match('^%*(.*)$')
147+
end
148+
149+
---@return string
150+
function Url:get_custom_id_completion()
151+
return self.str:match('^.+::#(.*)$') or self.str:match('^#(.*)$')
152+
end
153+
154+
---@return string | false
155+
function Url:get_dedicated_target()
156+
return not self:get_filepath()
157+
and not self:get_linenumber()
158+
and not self:get_headline()
159+
and self.str:match('^([%w%s%-%+%=_]+)$')
160+
end
161+
162+
---@return string | false
163+
function Url:get_http_url()
164+
return self.str:match('^https?://.+$')
165+
end
166+
167+
return Url

lua/orgmode/org/autocompletion/omni.lua

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
local Files = require('orgmode.parser.files')
22
local config = require('orgmode.config')
33
local Hyperlinks = require('orgmode.org.hyperlinks')
4+
local Url = require('orgmode.objects.url')
5+
local Link = require('orgmode.objects.link')
46

57
local data = {
68
directives = { '#+title', '#+author', '#+email', '#+name', '#+filetags', '#+archive', '#+options', '#+category' },
@@ -33,7 +35,10 @@ local properties = {
3335
local links = {
3436
line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?]]),
3537
rgx = vim.regex([[\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?$]]),
36-
fetcher = Hyperlinks.find_matching_links,
38+
fetcher = function(url)
39+
local hyperlinks, mapper = Hyperlinks.find_matching_links(url)
40+
return mapper(hyperlinks)
41+
end,
3742
}
3843

3944
local metadata = {
@@ -86,6 +91,19 @@ local headline_contexts = {
8691
todo_keywords,
8792
}
8893

94+
---Determines an URL for link handling. Handles a couple of corner-cases
95+
---@param base string The string to complete
96+
---@return string
97+
local function get_url_str(line, base)
98+
local line_base = line:match('%[%[(.-)$') or line
99+
line_base = line_base:gsub(base .. '$', '')
100+
return (line_base or '') .. (base or '')
101+
end
102+
103+
--- This function is registered to omnicompletion in ftplugin/org.vim.
104+
---
105+
--- If the user want to use it in his completion plugin (like cmp) he has to do
106+
--- that in the configuration of that plugin.
89107
---@return table
90108
local function omni(findstart, base)
91109
local line = vim.api.nvim_get_current_line():sub(1, vim.api.nvim_call_function('col', { '.' }) - 1)
@@ -101,7 +119,7 @@ local function omni(findstart, base)
101119
return -1
102120
end
103121

104-
local fetcher_ctx = { base = base, line = line }
122+
local url = Url.new(get_url_str(line, base))
105123
local results = {}
106124

107125
for _, context in ipairs(ctx) do
@@ -112,7 +130,7 @@ local function omni(findstart, base)
112130
then
113131
local items = {}
114132
if context.fetcher then
115-
items = context.fetcher(fetcher_ctx)
133+
items = context.fetcher(url)
116134
else
117135
items = { unpack(context.list) }
118136
end

0 commit comments

Comments
 (0)