Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit c6bdb07

Browse files
feat(links): Add unused refactored links structure (#802)
* feat(links): Add basic structure for links refactor * feat(links): Add all link types * feat(links): Add autocompletion * feat(links): Add missing handlers
1 parent 4c28d40 commit c6bdb07

20 files changed

+1186
-9
lines changed

lua/orgmode/files/file.lua

+5-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ end
5555
---Load the file
5656
---@return OrgPromise<OrgFile | false>
5757
function OrgFile.load(filename)
58-
local ext = vim.fn.fnamemodify(filename, ':e')
59-
if ext ~= 'org' and ext ~= 'org_archive' then
58+
if not utils.is_org_file(filename) then
6059
return Promise.resolve(false)
6160
end
6261
local bufnr = vim.fn.bufnr(filename) or -1
@@ -69,6 +68,10 @@ function OrgFile.load(filename)
6968
}))
7069
end
7170

71+
if not vim.loop.fs_stat(filename) then
72+
return Promise.resolve(false)
73+
end
74+
7275
return utils.readfile(filename, { schedule = true }):next(function(lines)
7376
return OrgFile:new({
7477
filename = filename,

lua/orgmode/files/init.lua

+1-4
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,7 @@ function OrgFiles:_files(skip_resolve)
338338
all_files = vim.tbl_flatten(all_files)
339339

340340
return vim.tbl_filter(function(file)
341-
local ext = vim.fn.fnamemodify(file, ':e')
342-
local is_org = ext == 'org' or ext == 'org_archive'
343-
344-
if not is_org then
341+
if not utils.is_org_file(file) then
345342
return false
346343
end
347344

lua/orgmode/init.lua

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local auto_instance_keys = {
1010
org_mappings = true,
1111
notifications = true,
1212
completion = true,
13+
links = true,
1314
}
1415

1516
---@class Org
@@ -22,6 +23,7 @@ local auto_instance_keys = {
2223
---@field completion OrgCompletion
2324
---@field org_mappings OrgMappings
2425
---@field notifications OrgNotifications
26+
---@field links OrgLinks
2527
local Org = {}
2628
setmetatable(Org, {
2729
__index = function(tbl, key)
@@ -51,6 +53,7 @@ function Org:init()
5153
paths = require('orgmode.config').org_agenda_files,
5254
})
5355
:load_sync(true, 20000)
56+
self.links = require('orgmode.org.links'):new({ files = self.files })
5457
self.agenda = require('orgmode.agenda'):new({
5558
files = self.files,
5659
})
@@ -61,11 +64,12 @@ function Org:init()
6164
capture = self.capture,
6265
agenda = self.agenda,
6366
files = self.files,
67+
links = self.links,
6468
})
6569
self.clock = require('orgmode.clock'):new({
6670
files = self.files,
6771
})
68-
self.completion = require('orgmode.org.autocompletion'):new({ files = self.files })
72+
self.completion = require('orgmode.org.autocompletion'):new({ files = self.files, links = self.links })
6973
self.statusline_debounced = require('orgmode.utils').debounce('statusline', function()
7074
return self.clock:get_statusline()
7175
end, 300)

lua/orgmode/org/autocompletion/init.lua

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---@class OrgCompletion
22
---@field files OrgFiles
3+
---@field links OrgLinks
34
---@field private sources OrgCompletionSource[]
45
---@field private sources_by_name table<string, OrgCompletionSource>
56
---@field menu string
@@ -8,10 +9,11 @@ local OrgCompletion = {
89
}
910
OrgCompletion.__index = OrgCompletion
1011

11-
---@param opts { files: OrgFiles }
12+
---@param opts { files: OrgFiles, links: OrgLinks }
1213
function OrgCompletion:new(opts)
1314
local this = setmetatable({
1415
files = opts.files,
16+
links = opts.links,
1517
sources = {},
1618
sources_by_name = {},
1719
}, OrgCompletion)

lua/orgmode/org/links/_meta.lua

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---@meta
2+
3+
---@class OrgLinkType
4+
---@field get_name fun(self: OrgLinkType): string
5+
---@field follow fun(self: OrgLinkType, link: string): boolean
6+
---@field autocomplete fun(self: OrgLinkType, link: string): string[]

lua/orgmode/org/links/hyperlink.lua

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
local OrgLinkUrl = require('orgmode.org.links.url')
2+
local Range = require('orgmode.files.elements.range')
3+
4+
---@class OrgHyperlink
5+
---@field url OrgLinkUrl
6+
---@field desc string | nil
7+
---@field range? OrgRange
8+
local OrgHyperlink = {}
9+
10+
local pattern = '%[%[([^%]]+.-)%]%]'
11+
12+
---@param str string
13+
---@param range? OrgRange
14+
---@return OrgHyperlink
15+
function OrgHyperlink:new(str, range)
16+
local this = setmetatable({}, { __index = OrgHyperlink })
17+
local parts = vim.split(str, '][', { plain = true })
18+
this.url = OrgLinkUrl:new(parts[1] or '')
19+
this.desc = parts[2]
20+
this.range = range
21+
return this
22+
end
23+
24+
---@return string
25+
function OrgHyperlink:to_str()
26+
if self.desc then
27+
return string.format('[[%s][%s]]', self.url:to_string(), self.desc)
28+
else
29+
return string.format('[[%s]]', self.url:to_string())
30+
end
31+
end
32+
33+
---@param line string
34+
---@param pos number
35+
---@return OrgHyperlink | nil, { from: number, to: number } | nil
36+
function OrgHyperlink.at_pos(line, pos)
37+
local links = {}
38+
local found_link = nil
39+
local position
40+
for link in line:gmatch(pattern) do
41+
local start_from = #links > 0 and links[#links].to or nil
42+
local from, to = line:find(pattern, start_from)
43+
local current_pos = { from = from, to = to }
44+
if pos >= from and pos <= to then
45+
found_link = link
46+
position = current_pos
47+
break
48+
end
49+
table.insert(links, current_pos)
50+
end
51+
if not found_link then
52+
return nil, nil
53+
end
54+
return OrgHyperlink:new(found_link), position
55+
end
56+
57+
---@return OrgHyperlink | nil, { from: number, to: number } | nil
58+
function OrgHyperlink.at_cursor()
59+
local line = vim.fn.getline('.')
60+
local col = vim.fn.col('.') or 0
61+
return OrgHyperlink.at_pos(line, col)
62+
end
63+
64+
---@return OrgHyperlink[]
65+
function OrgHyperlink.all_from_line(line, line_number)
66+
local links = {}
67+
for link in line:gmatch(pattern) do
68+
local start_from = #links > 0 and links[#links].to or nil
69+
local from, to = line:find(pattern, start_from)
70+
if from and to then
71+
local range = Range.from_line(line_number)
72+
range.start_col = from
73+
range.end_col = to
74+
table.insert(links, OrgHyperlink:new(link, range))
75+
end
76+
end
77+
78+
return links
79+
end
80+
81+
return OrgHyperlink

lua/orgmode/org/links/init.lua

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
local config = require('orgmode.config')
2+
local utils = require('orgmode.utils')
3+
local OrgLinkUrl = require('orgmode.org.links.url')
4+
local OrgHyperlink = require('orgmode.org.links.hyperlink')
5+
6+
---@class OrgLinks:OrgLinkType
7+
---@field private files OrgFiles
8+
---@field private types OrgLinkType[]
9+
---@field private types_by_name table<string, OrgLinkType>
10+
---@field private stored_links table<string, string>
11+
---@field private headline_search OrgLinkHeadlineSearch
12+
local OrgLinks = {
13+
stored_links = {},
14+
}
15+
OrgLinks.__index = OrgLinks
16+
17+
---@param opts { files: OrgFiles }
18+
function OrgLinks:new(opts)
19+
local this = setmetatable({
20+
files = opts.files,
21+
types = {},
22+
types_by_name = {},
23+
}, OrgLinks)
24+
this:_setup_builtin_types()
25+
return this
26+
end
27+
28+
---@param link string
29+
---@return boolean
30+
function OrgLinks:follow(link)
31+
for _, source in ipairs(self.types) do
32+
if source:follow(link) then
33+
return true
34+
end
35+
end
36+
37+
local org_link_url = OrgLinkUrl:new(link)
38+
if org_link_url.protocol and org_link_url.protocol ~= 'file' and org_link_url.protocol ~= 'id' then
39+
utils.echo_warning(string.format('Unsupported link protocol: %q', org_link_url.protocol))
40+
return false
41+
end
42+
43+
return self.headline_search:follow(link)
44+
end
45+
46+
---@param link string
47+
---@return string[]
48+
function OrgLinks:autocomplete(link)
49+
local pattern = '^' .. vim.pesc(link:lower())
50+
51+
local items = vim.tbl_filter(function(stored_link)
52+
return stored_link:lower():match(pattern)
53+
end, vim.tbl_keys(self.stored_links))
54+
55+
for _, source in ipairs(self.types) do
56+
utils.concat(items, source:autocomplete(link))
57+
end
58+
59+
utils.concat(items, self.headline_search:autocomplete(link))
60+
return items
61+
end
62+
63+
---@param headline OrgHeadline
64+
function OrgLinks:store_link_to_headline(headline)
65+
self.stored_links[self:get_link_to_headline(headline)] = headline:get_title()
66+
end
67+
68+
---@param headline OrgHeadline
69+
---@return string
70+
function OrgLinks:get_link_to_headline(headline)
71+
local title = headline:get_title()
72+
73+
if config.org_id_link_to_org_use_id then
74+
local id = headline:id_get_or_create()
75+
if id then
76+
return ('id:%s::*%s'):format(id, title)
77+
end
78+
end
79+
80+
return ('file:%s::*%s'):format(headline.file.filename, title)
81+
end
82+
83+
---@param file OrgFile
84+
---@return string
85+
function OrgLinks:get_link_to_file(file)
86+
local title = file:get_title()
87+
88+
if config.org_id_link_to_org_use_id then
89+
local id = file:id_get_or_create()
90+
if id then
91+
return ('id:%s::*%s'):format(id, title)
92+
end
93+
end
94+
95+
return ('file:%s::*%s'):format(file.filename, title)
96+
end
97+
98+
---@param link_location string
99+
function OrgLinks:insert_link(link_location)
100+
local selected_link = OrgHyperlink:new(link_location)
101+
local desc = selected_link.url:get_target()
102+
if desc and (desc:match('^%*') or desc:match('^#')) then
103+
desc = desc:sub(2)
104+
end
105+
106+
if selected_link.url:get_protocol() == 'id' then
107+
link_location = ('id:%s'):format(selected_link.url:get_path())
108+
end
109+
110+
local link_description = vim.trim(vim.fn.OrgmodeInput('Description: ', desc or ''))
111+
112+
link_location = '[' .. vim.trim(link_location) .. ']'
113+
114+
if link_description ~= '' then
115+
link_description = '[' .. link_description .. ']'
116+
end
117+
118+
local insert_from
119+
local insert_to
120+
local target_col = #link_location + #link_description + 2
121+
122+
-- check if currently on link
123+
local link, position = OrgHyperlink.at_cursor()
124+
if link and position then
125+
insert_from = position.from - 1
126+
insert_to = position.to + 1
127+
target_col = target_col + position.from
128+
else
129+
local colnr = vim.fn.col('.')
130+
insert_from = colnr
131+
insert_to = colnr + 1
132+
target_col = target_col + colnr
133+
end
134+
135+
local linenr = vim.fn.line('.') or 0
136+
local curr_line = vim.fn.getline(linenr)
137+
local new_line = string.sub(curr_line, 0, insert_from)
138+
.. '['
139+
.. link_location
140+
.. link_description
141+
.. ']'
142+
.. string.sub(curr_line, insert_to, #curr_line)
143+
144+
vim.fn.setline(linenr, new_line)
145+
vim.fn.cursor(linenr, target_col)
146+
end
147+
148+
---@param link_type OrgLinkType
149+
function OrgLinks:add_type(link_type)
150+
if self.types_by_name[link_type:get_name()] then
151+
error('Link type ' .. link_type:get_name() .. ' already exists')
152+
end
153+
self.types_by_name[link_type:get_name()] = link_type
154+
table.insert(self.types, link_type)
155+
end
156+
157+
---@private
158+
function OrgLinks:_setup_builtin_types()
159+
self:add_type(require('orgmode.org.links.types.http'):new({ files = self.files }))
160+
self:add_type(require('orgmode.org.links.types.id'):new({ files = self.files }))
161+
self:add_type(require('orgmode.org.links.types.line_number'):new({ files = self.files }))
162+
self:add_type(require('orgmode.org.links.types.custom_id'):new({ files = self.files }))
163+
self:add_type(require('orgmode.org.links.types.headline'):new({ files = self.files }))
164+
165+
self.headline_search = require('orgmode.org.links.types.headline_search'):new({ files = self.files })
166+
end
167+
168+
return OrgLinks

0 commit comments

Comments
 (0)