@@ -8,6 +8,21 @@ This module is an intermediary between Neorg and the completion engine of your c
8
8
module (this usually just involves setting the `engine` field in the [configuration](#configuration) section),
9
9
please read the corresponding wiki page for the engine you selected ([`nvim-cmp`](@core.integrations.nvim-cmp)
10
10
or [`nvim-compe`](@core.integrations.nvim-compe)) to complete setup.
11
+
12
+ Completions are provided in the following cases (examples in (), `|` represents the cursor location):
13
+ - TODO items (`- (|`)
14
+ - @ tags (`@|`)
15
+ - # tags (`#|`)
16
+ - file path links (`{:|`) provides workspace relative paths (`:$/workspace/relative/path:`)
17
+ - header links (`{*|`)
18
+ - fuzzy header links (`{#|`)
19
+ - footnotes (`{^|`)
20
+ - file path + header links (`{:path:*|`)
21
+ - file path + fuzzy header links (`{:path:#|`)
22
+ - file path + footnotes (`{:path:^|`)
23
+
24
+ Header completions will show only valid headers at the current level in the current or specified file. All
25
+ link completions are smart about closing `:` and `}`.
11
26
--]]
12
27
13
28
local neorg = require (" neorg.core" )
@@ -28,11 +43,192 @@ module.config.public = {
28
43
}
29
44
30
45
module .setup = function ()
31
- return { success = true , requires = { " core.integrations.treesitter" } }
46
+ return { success = true , requires = { " core.dirman " , " core. integrations.treesitter" } }
32
47
end
33
48
34
49
module .private = {
35
50
engine = nil ,
51
+
52
+ --- Get a list of all norg files in current workspace. Returns { workspace_path, norg_files }
53
+ --- @return table ?
54
+ get_norg_files = function ()
55
+ local dirman = neorg .modules .get_module (" core.dirman" )
56
+ if not dirman then
57
+ return nil
58
+ end
59
+
60
+ local current_workspace = dirman .get_current_workspace ()
61
+ local norg_files = dirman .get_norg_files (current_workspace [1 ])
62
+ return { current_workspace [2 ], norg_files }
63
+ end ,
64
+
65
+ --- Get the closing characters for a link completion
66
+ --- @param context table
67
+ --- @param colon boolean should there be a closing colon ?
68
+ --- @return string " " , " :" , or " :}" depending on what ' s needed
69
+ get_closing_chars = function (context , colon )
70
+ local offset = 1
71
+ local closing_colon = " "
72
+ if colon then
73
+ closing_colon = " :"
74
+ if string.sub (context .full_line , context .char + offset , context .char + offset ) == " :" then
75
+ closing_colon = " "
76
+ offset = 2
77
+ end
78
+ end
79
+
80
+ local closing_brace = " }"
81
+ if string.sub (context .full_line , context .char + offset , context .char + offset ) == " }" then
82
+ closing_brace = " "
83
+ end
84
+
85
+ return closing_colon .. closing_brace
86
+ end ,
87
+
88
+ --- Get the lines in a given norg file path.
89
+ --- @param file string file path , norg syntax accepted
90
+ --- @return table<string>
91
+ get_lines = function (file )
92
+ local dirutils = neorg .modules .get_module (" core.dirman.utils" )
93
+ if not dirutils then
94
+ return {}
95
+ end
96
+ local expanded = dirutils .expand_path (file , true )
97
+
98
+ local lines
99
+ if expanded then
100
+ if not string.match (expanded , " %.norg$" ) then
101
+ expanded = expanded .. " .norg"
102
+ end
103
+ local ok
104
+ ok , lines = pcall (vim .fn .readfile , expanded )
105
+ if not ok then
106
+ lines = {}
107
+ end
108
+ end
109
+ return lines
110
+ end ,
111
+
112
+ --- Find linkable headers in the given file
113
+ --- @param file string file path , norg syntax is accepted
114
+ --- @param context table
115
+ --- @param heading_level number ?
116
+ --- @return table<string>
117
+ find_headers = function (file , context , heading_level )
118
+ local leading_whitespace = " "
119
+ if context .before_char == " " then
120
+ leading_whitespace = " "
121
+ end
122
+
123
+ local closing_chars = module .private .get_closing_chars (context , false )
124
+ leading_whitespace = leading_whitespace or " "
125
+ local ret = {}
126
+
127
+ local lines = module .private .get_lines (file )
128
+ for _ , line in ipairs (lines ) do
129
+ local heading = { line :match (" ^%s*(%*+)%s+(.+)$" ) }
130
+ if not vim .tbl_isempty (heading ) and (not heading_level or # heading [1 ] == heading_level ) then
131
+ -- remove potential GTD status from link
132
+ local stripped_heading = string.gsub (heading [2 ], " ^%(.%)%s?" , " " )
133
+ table.insert (ret , leading_whitespace .. stripped_heading .. closing_chars )
134
+ end
135
+ -- local marker_or_drawer = { line:match("^%s*(%|%|?%s+(.+))$") }
136
+ -- if not vim.tbl_isempty(marker_or_drawer) then
137
+ -- -- TODO: how do you link to these things
138
+ -- -- what even are they?
139
+ -- table.insert(ret, marker_or_drawer[2])
140
+ -- end
141
+ end
142
+
143
+ return ret
144
+ end ,
145
+
146
+ --- Find footers in the given file
147
+ --- @param file string file path , norg syntax is accepted
148
+ --- @return table<string>
149
+ find_footnotes = function (file , context )
150
+ local ret = {}
151
+ local leading_whitespace = " "
152
+ if context .before_char == " " then
153
+ leading_whitespace = " "
154
+ end
155
+
156
+ local closing_chars = module .private .get_closing_chars (context , false )
157
+ leading_whitespace = leading_whitespace or " "
158
+ local lines = module .private .get_lines (file )
159
+ for _ , line in ipairs (lines ) do
160
+ local footnote = { line :match (" ^%s*%^%^? (.+)$" ) }
161
+ if not vim .tbl_isempty (footnote ) then
162
+ table.insert (ret , leading_whitespace .. footnote [1 ] .. closing_chars )
163
+ end
164
+ end
165
+
166
+ return ret
167
+ end ,
168
+
169
+ generate_file_links = function (context , _prev , _saved , _match )
170
+ local res = {}
171
+ local dirman = neorg .modules .get_module (" core.dirman" )
172
+ if not dirman then
173
+ return {}
174
+ end
175
+
176
+ local files = module .private .get_norg_files ()
177
+ if not files or not files [2 ] then
178
+ return {}
179
+ end
180
+
181
+ local closing_chars = module .private .get_closing_chars (context , true )
182
+ for _ , file in pairs (files [2 ]) do
183
+ assert (type (file ) == " string" )
184
+ local bufnr = dirman .get_file_bufnr (file )
185
+
186
+ if vim .api .nvim_get_current_buf () ~= bufnr then
187
+ -- using -6 to go to the end (-1) and remove '.norg' 5 more chars
188
+ local link = " {:$" .. file :sub (# files [1 ] + 1 , - 6 ) .. closing_chars
189
+ table.insert (res , link )
190
+ end
191
+ end
192
+
193
+ return res
194
+ end ,
195
+
196
+ generate_local_heading_links = function (context , _prev , _saved , match )
197
+ local heading_level = match [2 ] and # match [2 ]
198
+ return module .private .find_headers (vim .api .nvim_buf_get_name (0 ), context , heading_level )
199
+ end ,
200
+
201
+ generate_foreign_heading_links = function (context , _prev , _saved , match )
202
+ local file = match [1 ]
203
+ local heading_level = match [2 ] and # match [2 ]
204
+ if file then
205
+ return module .private .find_headers (file , context , heading_level )
206
+ end
207
+ return {}
208
+ end ,
209
+
210
+ generate_local_footnote_links = function (context , _prev , _saved , _match )
211
+ return module .private .find_footnotes (vim .api .nvim_buf_get_name (0 ), context )
212
+ end ,
213
+
214
+ generate_foreign_footnote_links = function (context , _prev , _saved , match )
215
+ if match [2 ] then
216
+ return module .private .find_footnotes (match [2 ], context )
217
+ end
218
+ return {}
219
+ end ,
220
+
221
+ --- The node context for normal norg (ie. not in a code block)
222
+ normal_norg = function (current , previous )
223
+ -- If no previous node exists then try verifying the current node instead
224
+ if not previous then
225
+ return current and (current :type () ~= " translation_unit" or current :type () == " document" ) or false
226
+ end
227
+
228
+ -- If the previous node is not tag parameters or the tag name
229
+ -- (i.e. we are not inside of a tag) then show auto completions
230
+ return previous :type () ~= " tag_parameters" and previous :type () ~= " tag_name"
231
+ end ,
36
232
}
37
233
38
234
module .load = function ()
@@ -70,21 +266,12 @@ module.public = {
70
266
71
267
-- Define completions
72
268
completions = {
73
- { -- Create a new completion
269
+ { -- Create a new completion (for `@|tags`)
74
270
-- Define the regex that should match in order to proceed
75
271
regex = " ^%s*@(%w*)" ,
76
272
77
273
-- If regex can be matched, this item then gets verified via TreeSitter's AST
78
- node = function (current , previous )
79
- -- If no previous node exists then try verifying the current node instead
80
- if not previous then
81
- return current and (current :type () ~= " translation_unit" or current :type () == " document" ) or false
82
- end
83
-
84
- -- If the previous node is not tag parameters or the tag name
85
- -- (i.e. we are not inside of a tag) then show autocompletions
86
- return previous :type () ~= " tag_parameters" and previous :type () ~= " tag_name"
87
- end ,
274
+ node = module .private .normal_norg ,
88
275
89
276
-- The actual elements to show if the above tests were true
90
277
complete = {
@@ -185,7 +372,7 @@ module.public = {
185
372
},
186
373
},
187
374
},
188
- {
375
+ { -- `#|tags`
189
376
regex = " ^%s*%#(%w*)" ,
190
377
191
378
complete = {
@@ -203,7 +390,7 @@ module.public = {
203
390
204
391
descend = {},
205
392
},
206
- {
393
+ { -- `@|end` tags
207
394
regex = " ^%s*@e?n?" ,
208
395
node = function (_ , previous )
209
396
if not previous then
@@ -222,7 +409,7 @@ module.public = {
222
409
completion_start = " @" ,
223
410
},
224
411
},
225
- {
412
+ { -- TODO items `- (|)`
226
413
regex = " ^%s*%-+%s+%(([x%*%s]?)" ,
227
414
228
415
complete = {
@@ -249,6 +436,92 @@ module.public = {
249
436
completion_start = " -" ,
250
437
},
251
438
},
439
+ { -- links for file paths `{:|`
440
+ regex = " ^.*{:([^:}]*)" ,
441
+
442
+ node = module .private .normal_norg ,
443
+
444
+ complete = module .private .generate_file_links ,
445
+
446
+ options = {
447
+ type = " File" ,
448
+ completion_start = " {" ,
449
+ },
450
+ },
451
+ { -- links that have a file path, suggest any heading from the file `{:...:#|}`
452
+ regex = " ^.*{:(.*):#[^}]*" ,
453
+
454
+ complete = module .private .generate_foreign_heading_links ,
455
+
456
+ node = module .private .normal_norg ,
457
+
458
+ options = {
459
+ type = " Reference" ,
460
+ completion_start = " #" ,
461
+ },
462
+ },
463
+ { -- links that have a file path, suggest direct headings from the file `{:...:*|}`
464
+ regex = " ^.*{:(.*):(%*+)[^}]*" ,
465
+
466
+ complete = module .private .generate_foreign_heading_links ,
467
+
468
+ node = module .private .normal_norg ,
469
+
470
+ options = {
471
+ type = " Reference" ,
472
+ completion_start = " *" ,
473
+ },
474
+ },
475
+ { -- # links to headings in the current file `{#|}`
476
+ regex = " ^.*{#[^}]*" ,
477
+
478
+ complete = module .private .generate_local_heading_links ,
479
+
480
+ node = module .private .normal_norg ,
481
+
482
+ options = {
483
+ type = " Reference" ,
484
+ completion_start = " #" ,
485
+ },
486
+ },
487
+ { -- * links to headings in current file `{*|}`
488
+ regex = " ^(.*){(%*+)[^}]*" ,
489
+ -- the first capture group is a nothing group so that match[2] is reliably the heading
490
+ -- level or nil if there's no heading level.
491
+
492
+ complete = module .private .generate_local_heading_links ,
493
+
494
+ node = module .private .normal_norg ,
495
+
496
+ options = {
497
+ type = " Reference" ,
498
+ completion_start = " *" ,
499
+ },
500
+ },
501
+ { -- ^ footnote links in the current file `{^|}`
502
+ regex = " ^(.*){%^[^}]*" ,
503
+
504
+ complete = module .private .generate_local_footnote_links ,
505
+
506
+ node = module .private .normal_norg ,
507
+
508
+ options = {
509
+ type = " Reference" ,
510
+ completion_start = " ^" ,
511
+ },
512
+ },
513
+ { -- ^ footnote links in another file `{:path:^|}`
514
+ regex = " ^(.*){:(.*):%^[^}]*" ,
515
+
516
+ complete = module .private .generate_foreign_footnote_links ,
517
+
518
+ node = module .private .normal_norg ,
519
+
520
+ options = {
521
+ type = " Reference" ,
522
+ completion_start = " ^" ,
523
+ },
524
+ },
252
525
},
253
526
254
527
--- Parses the public completion table and attempts to find all valid matches
@@ -267,13 +540,13 @@ module.public = {
267
540
-- If the completion data has a regex variable
268
541
if completion_data .regex then
269
542
-- Attempt to match the current line before the cursor with that regex
270
- local match = context .line :match (saved .. completion_data .regex .. " $" )
543
+ local match = { context .line :match (saved .. completion_data .regex .. " $" ) }
271
544
272
545
-- If our match was successful
273
- if match then
546
+ if not vim . tbl_isempty ( match ) then
274
547
-- Construct a variable that will be returned on a successful match
275
548
local items = type (completion_data .complete ) == " table" and completion_data .complete
276
- or completion_data .complete (context , prev , saved )
549
+ or completion_data .complete (context , prev , saved , match )
277
550
local ret_completions = { items = items , options = completion_data .options or {} }
278
551
279
552
-- Set the match variable for the integration module
0 commit comments