Skip to content

Commit e7da5f8

Browse files
WIP LINK REGISTRY
1 parent 71eef93 commit e7da5f8

File tree

13 files changed

+1020
-0
lines changed

13 files changed

+1020
-0
lines changed

lua/orgmode/init.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ local auto_instance_keys = {
2020
---@field capture OrgCapture
2121
---@field clock OrgClock
2222
---@field completion OrgCompletion
23+
---@field links OrgLinkHandlerRegistry
2324
---@field org_mappings OrgMappings
2425
---@field notifications OrgNotifications
2526
local Org = {}
@@ -66,6 +67,9 @@ function Org:init()
6667
files = self.files,
6768
})
6869
self.completion = require('orgmode.org.autocompletion'):new({ files = self.files })
70+
self.links = require('orgmode.org.links'):new({
71+
files = self.files,
72+
})
6973
self.statusline_debounced = require('orgmode.utils').debounce('statusline', function()
7074
return self.clock:get_statusline()
7175
end, 300)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
local utils = require('orgmode.utils')
2+
local Org = require('orgmode')
3+
local Internal = require('orgmode.org.hyperlinks.builtin.internal')
4+
5+
---@class OrgLinkHandlerCustomId:OrgLinkHandlerInternal
6+
local CustomId = Internal:new()
7+
8+
function CustomId:new(custom_id)
9+
---@class OrgLinkHandlerCustomId
10+
local this = Internal:new()
11+
this.custom_id = custom_id
12+
setmetatable(this, self)
13+
self.__index = self
14+
return this
15+
end
16+
17+
---@param input string
18+
function CustomId.parse(input)
19+
return CustomId:new(input)
20+
end
21+
22+
function CustomId:__tostring()
23+
return string.format('#%s', self.custom_id)
24+
end
25+
26+
function CustomId:follow()
27+
local headlines = Org.files:get_current_file():find_headlines_with_property_matching('custom_id', self.custom_id)
28+
29+
if #headlines == 0 then
30+
return utils.echo_warning(('Could not find custom ID "%s".'):format(self.custom_id))
31+
end
32+
33+
self.goto_oneof(headlines)
34+
end
35+
36+
function CustomId:insert_description()
37+
return self.custom_id
38+
end
39+
40+
function CustomId:complete(lead, context)
41+
local file = self.get_file_from_context(context)
42+
local headlines = file:find_headlines_with_property_matching('CUSTOM_ID', lead)
43+
44+
local completions = {}
45+
for _, headline in pairs(headlines) do
46+
local id = headline:get_property('CUSTOM_ID')
47+
table.insert(completions, self:new(id):__tostring())
48+
end
49+
50+
return completions
51+
end
52+
53+
return CustomId
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
local utils = require('orgmode.utils')
2+
local Org = require('orgmode')
3+
local Internal = require('orgmode.org.hyperlinks.builtin.internal')
4+
local Id = require('orgmode.org.hyperlinks.builtin.id')
5+
6+
---@class OrgLinkHandlerHeadline:OrgLinkHandlerInternal
7+
local Headline = Internal:new()
8+
9+
function Headline:new(headline)
10+
---@class OrgLinkHandlerHeadline
11+
local this = Internal:new()
12+
this.headline = headline
13+
setmetatable(this, self)
14+
self.__index = self
15+
return this
16+
end
17+
18+
---@param input string
19+
function Headline.parse(input)
20+
return Headline:new(input)
21+
end
22+
23+
function Headline:__tostring()
24+
return string.format('*%s', self.headline)
25+
end
26+
27+
function Headline:follow()
28+
local headlines = Org.files:get_current_file():find_headlines_by_title(self.headline)
29+
30+
if #headlines == 0 then
31+
return utils.echo_warning(('Could not find headline "%s".'):format(self.headline))
32+
end
33+
34+
self.goto_oneof(headlines)
35+
end
36+
37+
function Headline:resolve()
38+
local headlines = Org.files:get_current_file():find_headlines_by_title(self.headline)
39+
40+
if #headlines == 0 then
41+
return self
42+
end
43+
44+
local id = headlines[1]:get_property('id')
45+
if not id then
46+
return self
47+
end
48+
49+
return Id:new(id):resolve()
50+
end
51+
52+
function Headline:insert_description()
53+
return self.headline
54+
end
55+
56+
function Headline:complete(lead, context)
57+
local file = self.get_file_from_context(context)
58+
local headlines = file:find_headlines_by_title(lead)
59+
60+
local completions = {}
61+
for _, headline in pairs(headlines) do
62+
table.insert(completions, Headline:new(headline:get_title()):__tostring())
63+
end
64+
65+
return completions
66+
end
67+
68+
return Headline
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
local Internal = require('orgmode.org.hyperlinks.builtin.internal')
2+
3+
---@class OrgLinkHandlerLineNumber:OrgLinkHandlerInternal
4+
local LineNumber = Internal:new()
5+
6+
function LineNumber:new(line_number)
7+
---@class OrgLinkHandlerLineNumber
8+
local this = Internal:new()
9+
this.line_number = line_number
10+
setmetatable(this, self)
11+
self.__index = self
12+
return this
13+
end
14+
15+
---@param input string
16+
function LineNumber.parse(input)
17+
return LineNumber:new(tonumber(input))
18+
end
19+
20+
function LineNumber:__tostring()
21+
return string.format('%d', self.line_number)
22+
end
23+
24+
function LineNumber:follow()
25+
vim.cmd(('normal! %dGzv'):format(self.line_number))
26+
end
27+
28+
return LineNumber
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
local utils = require('orgmode.utils')
2+
local Org = require('orgmode')
3+
local Internal = require('orgmode.org.hyperlinks.builtin.internal')
4+
5+
---@class OrgLinkHandlerPlain:OrgLinkHandlerInternal
6+
local Plain = Internal:new()
7+
8+
function Plain:new(text)
9+
---@class OrgLinkHandlerPlain
10+
local this = Internal:new()
11+
this.text = text
12+
setmetatable(this, self)
13+
self.__index = self
14+
return this
15+
end
16+
17+
---@param input string
18+
function Plain.parse(input)
19+
return Plain:new(input)
20+
end
21+
22+
function Plain:__tostring()
23+
return self.text
24+
end
25+
26+
function Plain:follow()
27+
local anchors = vim.fn.matchbufline(0, ('<<<?%s[^>]*>>>?'):format(self.text), 0, '$')
28+
29+
if #anchors >= 1 then
30+
vim.fn.cursor(anchors[1].lnum, anchors[1].byteidx)
31+
return
32+
end
33+
34+
-- TODO from here, behaviour should depend on org-link-search-must-match-exact-headline
35+
-- TODO #+NAME tag support should be added, but it didn't exists yet
36+
37+
local plain_text_matches = vim.fn.matchbufline(0, self.text, 0, '$')
38+
39+
if #plain_text_matches >= 1 then
40+
vim.fn.cursor(plain_text_matches[1].lnum, plain_text_matches[1].byteidx)
41+
return
42+
end
43+
44+
return utils.echo_warning(('No matches found for "%s".'):format(self.text))
45+
end
46+
47+
function Plain:insert_description()
48+
return self.text
49+
end
50+
51+
-- TODO #+NAME tag support should be added, but it didn't exists yet
52+
function Plain:complete(lead, context)
53+
local file = self.get_file_from_context(context)
54+
local completions = {}
55+
56+
local anchors = file.content:gmatch(('<<<?%s[^>]*>>>?'):format(lead))
57+
for anchor in anchors do
58+
table.insert(completions, Plain:new(anchor):__tostring())
59+
end
60+
61+
local headlines = file:find_headlines_by_title(lead)
62+
for _, headline in pairs(headlines) do
63+
table.insert(completions, Plain:new(headline:get_title()):__tostring())
64+
end
65+
66+
return completions
67+
end
68+
69+
return Plain
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
local fs = require('orgmode.utils.fs')
2+
local Link = require('orgmode.org.links.link_handler')
3+
local Id = require('orgmode.org.links.handlers.id')
4+
local Internal = require('orgmode.org.links.handlers.internal')
5+
6+
---@class OrgLinkHandlerFile:OrgLinkHandler
7+
---@field new fun(self: OrgLinkHandlerFile, path: string, target: OrgLinkHandlerInternal | nil, prefix: boolean | nil): OrgLinkHandlerFile
8+
---@field parse fun(link: string, prefix: boolean | nil): OrgLinkHandlerFile | nil
9+
---@field path string
10+
---@field skip_prefix boolean
11+
---@field target OrgLinkHandlerInternal | nil
12+
local File = Link:new('file')
13+
14+
function File:new(path, target, skip_prefix)
15+
---@class OrgLinkHandlerFile
16+
local this = Link:new()
17+
this.skip_prefix = skip_prefix or false
18+
this.path = path
19+
this.target = target
20+
setmetatable(this, self)
21+
self.__index = self
22+
return this
23+
end
24+
25+
function File.parse(input, skip_prefix)
26+
if input == nil or #input == 0 then
27+
return nil
28+
end
29+
local deliniator_start, deliniator_stop = input:find('::')
30+
31+
---@type OrgLinkHandlerInternal | nil
32+
local target = nil
33+
local path = input
34+
35+
if deliniator_start then
36+
---@class OrgLinkHandlerInternal | nil
37+
target = Internal.parse(input:sub(deliniator_stop + 1), true)
38+
path = input:sub(0, deliniator_start - 1)
39+
end
40+
41+
return File:new(path, target, skip_prefix)
42+
end
43+
44+
-- TODO make protocol prefix optional. Based on what?
45+
function File:__tostring()
46+
local v = ''
47+
if self.skip_prefix then
48+
v = ('%s'):format(self.path)
49+
else
50+
v = ('%s:%s'):format(self.protocol, self.path)
51+
end
52+
53+
if self.target then
54+
v = string.format('%s::%s', v, self.target)
55+
end
56+
57+
return v
58+
end
59+
60+
function File:follow()
61+
vim.cmd('edit ' .. fs.get_real_path(self.path))
62+
63+
if self.target then
64+
self.target:follow()
65+
end
66+
end
67+
68+
local function autocompletions_filenames(lead)
69+
local Org = require('orgmode')
70+
local filenames = Org.files:filenames()
71+
72+
local matches = {}
73+
for _, f in ipairs(filenames) do
74+
local realpath = fs.substitute_path(lead) or lead
75+
if f:find('^' .. realpath) then
76+
local path = f:gsub('^' .. realpath, lead)
77+
table.insert(matches, { real = f, path = path })
78+
end
79+
end
80+
81+
print(vim.inspect(matches))
82+
return matches
83+
end
84+
85+
function File:resolve()
86+
local Org = require('orgmode')
87+
local path = fs.get_real_path(self.path)
88+
if not path then
89+
return self
90+
end
91+
local file = Org.files:get(path)
92+
if not file then
93+
return self
94+
end
95+
local id = file:get_property('id')
96+
if not id then
97+
return self
98+
end
99+
100+
return Id:new(id, self.target):resolve()
101+
end
102+
103+
function File:insert_description()
104+
local Org = require('orgmode')
105+
if self.target then
106+
return self.target:insert_description()
107+
end
108+
109+
local path = fs.get_real_path(self.path)
110+
if not path then
111+
return nil
112+
end
113+
local file = Org.files:get(path)
114+
if not file then
115+
return nil
116+
end
117+
118+
return file:get_title()
119+
end
120+
121+
function File:complete(lead, context)
122+
context = context or {}
123+
local deliniator_start, deliniator_stop = lead:find('::')
124+
125+
if not deliniator_start then
126+
return self:_complete(lead, context)
127+
else
128+
local path = lead:sub(0, deliniator_start - 1)
129+
local target_lead = lead:sub(deliniator_stop + 1)
130+
return self:_complete_targets(path, target_lead, context)
131+
end
132+
end
133+
134+
function File:_complete(lead, context)
135+
return vim.tbl_map(function(f)
136+
return self:new(f, nil, context.skip_prefix):__tostring()
137+
end, autocompletions_filenames(lead))
138+
end
139+
140+
function File:_complete_targets(path, target_lead, context)
141+
return vim.tbl_map(
142+
function(t)
143+
return self:new(path, t, context.skip_prefix):__tostring()
144+
end,
145+
Internal:complete(target_lead, {
146+
filename = fs.get_real_path(path),
147+
only_internal = true,
148+
})
149+
)
150+
end
151+
152+
return File

0 commit comments

Comments
 (0)