Skip to content

Commit efeb35f

Browse files
Merge pull request #112 from levouh/link-file-context
2 parents 61c0801 + 8552ffd commit efeb35f

File tree

5 files changed

+156
-36
lines changed

5 files changed

+156
-36
lines changed

Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
test:
22
nvim --headless --noplugin -u tests/minimal_init.vim -c "PlenaryBustedDirectory tests/plenary/ {minimal_init = 'tests/minimal_init.vim'}"
3+
testfile:
4+
nvim --headless --noplugin -u tests/minimal_init.vim -c "PlenaryBustedFile $(FILE)"
35
ci:
46
nvim --noplugin -u tests/minimal_init.vim -c "TSUpdateSync org" -c "qa!" && make test
57
docs:

lua/orgmode/org/autocompletion/omni.lua

+33-9
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,38 @@ local data = {
99
metadata = { 'DEADLINE:', 'SCHEDULED:', 'CLOSED:' },
1010
}
1111

12-
local directives = { rgx = vim.regex([[^\#+\?\w*$]]), line_rgx = vim.regex([[^\#\?+\?\w*$]]), list = data.directives }
12+
local directives = {
13+
line_rgx = vim.regex([[^\#\?+\?\w*$]]),
14+
rgx = vim.regex([[^\#+\?\w*$]]),
15+
list = data.directives,
16+
}
17+
1318
local begin_blocks = {
14-
rgx = vim.regex([[\(^\s*\)\@<=\#+\?\w*$]]),
1519
line_rgx = vim.regex([[^\s*\#\?+\?\w*$]]),
20+
rgx = vim.regex([[\(^\s*\)\@<=\#+\?\w*$]]),
1621
list = data.begin_blocks,
1722
}
23+
1824
local properties = {
1925
line_rgx = vim.regex([[\(^\s\+\|^\s*:\?$\)]]),
2026
rgx = vim.regex([[\(^\|^\s\+\)\@<=:\w*$]]),
27+
extra_cond = function(line, _)
28+
return not string.find(line, 'file:.*$')
29+
end,
2130
list = data.properties,
2231
}
32+
2333
local links = {
24-
line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|\#\)\?\(\w\+\)\?]]),
25-
rgx = vim.regex([[\(\*\|\#\)\?\(\w\+\)\?$]]),
34+
line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?]]),
35+
rgx = vim.regex([[\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?$]]),
2636
fetcher = Hyperlinks.find_matching_links,
2737
}
28-
local metadata = { rgx = vim.regex([[\(\s*\)\@<=\w\+$]]), list = data.metadata }
38+
39+
local metadata = {
40+
rgx = vim.regex([[\(\s*\)\@<=\w\+$]]),
41+
list = data.metadata,
42+
}
43+
2944
local tags = {
3045
rgx = vim.regex([[:\([0-9A-Za-z_%@\#]*\)$]]),
3146
fetcher = function()
@@ -38,6 +53,9 @@ local tags = {
3853
local filetags = {
3954
line_rgx = vim.regex([[\c^\#+FILETAGS:\s\+]]),
4055
rgx = vim.regex([[:\([0-9A-Za-z_%@\#]*\)$]]),
56+
extra_cond = function(line, _)
57+
return not string.find(line, 'file:.*$')
58+
end,
4159
fetcher = function()
4260
return vim.tbl_map(function(tag)
4361
return ':' .. tag .. ':'
@@ -46,8 +64,8 @@ local filetags = {
4664
}
4765

4866
local todo_keywords = {
49-
rgx = vim.regex([[\(^\(\*\+\s\+\)\?\)\@<=\w*$]]),
5067
line_rgx = vim.regex([[^\*\+\s\+\w*$]]),
68+
rgx = vim.regex([[\(^\(\*\+\s\+\)\?\)\@<=\w*$]]),
5169
fetcher = function()
5270
return config:get_todo_keywords().ALL
5371
end,
@@ -75,19 +93,25 @@ local function omni(findstart, base)
7593
if findstart == 1 then
7694
for _, context in ipairs(ctx) do
7795
local word = context.rgx:match_str(line)
78-
if word then
96+
if word and (not context.extra_cond or context.extra_cond(line, base)) then
7997
return word
8098
end
8199
end
82100
return -1
83101
end
84102

103+
local fetcher_ctx = { base = base, line = line }
85104
local results = {}
105+
86106
for _, context in ipairs(ctx) do
87-
if (not context.line_rgx or context.line_rgx:match_str(line)) and context.rgx:match_str(base) then
107+
if
108+
(not context.line_rgx or context.line_rgx:match_str(line))
109+
and context.rgx:match_str(base)
110+
and (not context.extra_cond or context.extra_cond(line, base))
111+
then
88112
local items = {}
89113
if context.fetcher then
90-
items = context.fetcher(base)
114+
items = context.fetcher(fetcher_ctx)
91115
else
92116
items = { unpack(context.list) }
93117
end

lua/orgmode/org/hyperlinks.lua

+83-22
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,87 @@ local Files = require('orgmode.parser.files')
22
local utils = require('orgmode.utils')
33
local Hyperlinks = {}
44

5-
function Hyperlinks.find_by_custom_id_property(base, skip_mapping)
6-
local headlines = Files.get_current_file():find_headlines_with_property_matching('CUSTOM_ID', base:sub(2))
7-
if skip_mapping then
5+
local function get_file_from_context(ctx)
6+
return (
7+
ctx.hyperlinks and ctx.hyperlinks.filepath and Files.get(ctx.hyperlinks.filepath, true)
8+
or Files.get_current_file()
9+
)
10+
end
11+
12+
local function update_hyperlink_ctx(ctx)
13+
if not ctx.line then
14+
return
15+
end
16+
17+
-- TODO: Support text search, see here [https://orgmode.org/manual/External-Links.html]
18+
local hyperlinks_ctx = {
19+
filepath = false,
20+
headline = false,
21+
custom_id = false,
22+
}
23+
24+
local file_match = ctx.line:match('file:(.-)::')
25+
file_match = file_match and vim.fn.fnamemodify(file_match, ':p') or file_match
26+
27+
if file_match and Files.get(file_match) then
28+
hyperlinks_ctx.filepath = Files.get(file_match).filename
29+
hyperlinks_ctx.headline = ctx.line:match('file:.-::(%*.-)$')
30+
31+
if not hyperlinks_ctx.headline then
32+
hyperlinks_ctx.custom_id = ctx.line:match('file:.-::(#.-)$')
33+
end
34+
35+
ctx.base = hyperlinks_ctx.headline or hyperlinks_ctx.custom_id or ctx.base
36+
end
37+
38+
ctx.hyperlinks = hyperlinks_ctx
39+
end
40+
41+
function Hyperlinks.find_by_filepath(ctx)
42+
local filenames = Files.filenames()
43+
local file_base = ctx.base:gsub('^file:', '')
44+
if vim.trim(file_base) ~= '' then
45+
filenames = vim.tbl_filter(function(f)
46+
return f:find('^' .. file_base)
47+
end, filenames)
48+
end
49+
50+
-- Outer checks already filter cases where `ctx.skip_add_prefix` is truthy,
51+
-- so no need to check it here
52+
return vim.tbl_map(function(path)
53+
return 'file:' .. path
54+
end, filenames)
55+
end
56+
57+
function Hyperlinks.find_by_custom_id_property(ctx)
58+
local file = get_file_from_context(ctx)
59+
local headlines = file:find_headlines_with_property_matching('CUSTOM_ID', ctx.base:sub(2))
60+
if ctx.skip_add_prefix then
861
return headlines
962
end
1063
return vim.tbl_map(function(headline)
1164
return '#' .. headline.properties.items.CUSTOM_ID
1265
end, headlines)
1366
end
1467

15-
function Hyperlinks.find_by_title_pointer(base, skip_mapping)
16-
local headlines = Files.get_current_file():find_headlines_by_title(base:sub(2))
17-
if skip_mapping then
68+
function Hyperlinks.find_by_title_pointer(ctx)
69+
local file = get_file_from_context(ctx)
70+
local headlines = file:find_headlines_by_title(ctx.base:sub(2))
71+
if ctx.skip_add_prefix then
1872
return headlines
1973
end
2074
return vim.tbl_map(function(headline)
2175
return '*' .. headline.title
2276
end, headlines)
2377
end
2478

25-
function Hyperlinks.find_by_dedicated_target(base, skip_mapping)
26-
if not base or base == '' then
79+
function Hyperlinks.find_by_dedicated_target(ctx)
80+
if not ctx.base or ctx.base == '' then
2781
return {}
2882
end
29-
local term = string.format('<<<?(%s[^>]*)>>>?', base):lower()
83+
local term = string.format('<<<?(%s[^>]*)>>>?', ctx.base):lower()
3084
local headlines = Files.get_current_file():find_headlines_matching_search_term(term, true)
31-
if skip_mapping then
85+
if ctx.skip_add_prefix then
3286
return headlines
3387
end
3488
local targets = {}
@@ -45,32 +99,39 @@ function Hyperlinks.find_by_dedicated_target(base, skip_mapping)
4599
return targets
46100
end
47101

48-
function Hyperlinks.find_by_title(base, skip_mapping)
49-
if not base or base == '' then
102+
function Hyperlinks.find_by_title(ctx)
103+
if not ctx.base or ctx.base == '' then
50104
return {}
51105
end
52-
local headlines = Files.get_current_file():find_headlines_by_title(base)
53-
if skip_mapping then
106+
local headlines = Files.get_current_file():find_headlines_by_title(ctx.base)
107+
if ctx.skip_add_prefix then
54108
return headlines
55109
end
56110
return vim.tbl_map(function(headline)
57111
return headline.title
58112
end, headlines)
59113
end
60114

61-
function Hyperlinks.find_matching_links(base, skip_mapping)
62-
base = vim.trim(base)
63-
local prefix = base:sub(1, 1)
64-
if prefix == '#' then
65-
return Hyperlinks.find_by_custom_id_property(base, skip_mapping)
115+
function Hyperlinks.find_matching_links(ctx)
116+
ctx = ctx or {}
117+
ctx.base = ctx.base and vim.trim(ctx.base) or nil
118+
119+
update_hyperlink_ctx(ctx)
120+
121+
if ctx.base:find('^file:') and not ctx.skip_add_prefix then
122+
return Hyperlinks.find_by_filepath(ctx)
66123
end
67124

125+
local prefix = ctx.base:sub(1, 1)
126+
if prefix == '#' then
127+
return Hyperlinks.find_by_custom_id_property(ctx)
128+
end
68129
if prefix == '*' then
69-
return Hyperlinks.find_by_title_pointer(base, skip_mapping)
130+
return Hyperlinks.find_by_title_pointer(ctx)
70131
end
71132

72-
local results = Hyperlinks.find_by_dedicated_target(base, skip_mapping)
73-
local all = utils.concat(results, Hyperlinks.find_by_title(base, skip_mapping))
133+
local results = Hyperlinks.find_by_dedicated_target(ctx)
134+
local all = utils.concat(results, Hyperlinks.find_by_title(ctx))
74135
return all
75136
end
76137

lua/orgmode/org/mappings.lua

+10-4
Original file line numberDiff line numberDiff line change
@@ -488,14 +488,20 @@ function OrgMappings:open_at_point()
488488
end
489489
local parts = vim.split(link, '][', true)
490490
local url = parts[1]
491+
local link_ctx = { base = url, skip_add_prefix = true }
491492
if url:find('^file:') then
492-
if url:find(' +') then
493+
if url:find(' +', 1, true) then
493494
parts = vim.split(url, ' +', true)
494495
url = parts[1]
495496
local line_number = parts[2]
496497
return vim.cmd(string.format('edit +%s %s', line_number, url:sub(6)))
497498
end
498-
return vim.cmd(string.format('edit %s', url:sub(6)))
499+
500+
if url:find('^file:(.-)::') then
501+
link_ctx.line = url
502+
else
503+
return vim.cmd(string.format('edit %s', url:sub(6)))
504+
end
499505
end
500506
if url:find('^https?://') then
501507
if not vim.g.loaded_netrwPlugin then
@@ -507,12 +513,12 @@ function OrgMappings:open_at_point()
507513
if stat and stat.type == 'file' then
508514
return vim.cmd(string.format('edit %s', url))
509515
end
516+
510517
local current_headline = Files.get_closest_headline()
511518
local headlines = vim.tbl_filter(function(headline)
512519
return headline.line ~= current_headline.line and headline.id ~= current_headline.id
513520
end, Hyperlinks.find_matching_links(
514-
url,
515-
true
521+
link_ctx
516522
))
517523
if #headlines == 0 then
518524
return

tests/plenary/org/autocompletion_spec.lua

+28-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ describe('Autocompletion', function()
8585
result = OrgmodeOmniCompletion(1, '')
8686
assert.are.same(4, result)
8787

88+
mock_line(api, ' [[file:')
89+
result = OrgmodeOmniCompletion(1, '')
90+
assert.are.same(4, result)
91+
8892
mock.revert(api)
8993
end)
9094

@@ -196,7 +200,30 @@ describe('Autocompletion', function()
196200
{ menu = '[Org]', word = ':PRIVATE:' },
197201
}, result)
198202

199-
-- TODO: Add hyperlinks test
203+
-- TODO: Add more hyperlink tests
204+
local MockFiles = mock(Files, true)
205+
local filename = 'work.org'
206+
local headlines = {
207+
{ title = 'Item for work 1' },
208+
{ title = 'Item for work 2' },
209+
}
210+
211+
MockFiles.filenames.returns({ filename })
212+
MockFiles.get.returns({
213+
filename = filename,
214+
find_headlines_by_title = function()
215+
return headlines
216+
end,
217+
})
218+
219+
mock_line(api, string.format(' [[file:%s::*', filename))
220+
result = OrgmodeOmniCompletion(0, '*')
221+
assert.are.same({
222+
{ menu = '[Org]', word = '*' .. headlines[1].title },
223+
{ menu = '[Org]', word = '*' .. headlines[2].title },
224+
}, result)
225+
226+
mock.revert(MockFiles)
200227

201228
mock.revert(api)
202229
end)

0 commit comments

Comments
 (0)