-
Notifications
You must be signed in to change notification settings - Fork 28
/
markdown.lua
318 lines (304 loc) · 9.22 KB
/
markdown.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
local configs = require('dropbar.configs')
local bar = require('dropbar.bar')
local initialized = false
local groupid = vim.api.nvim_create_augroup('DropBarMarkdown', {})
---@class markdown_heading_symbol_t
---@field name string
---@field level integer
---@field lnum integer
local markdown_heading_symbol_t = {}
markdown_heading_symbol_t.__index = markdown_heading_symbol_t
---Create a new markdown heading symbol object
---@param opts markdown_heading_symbol_t?
---@return markdown_heading_symbol_t
function markdown_heading_symbol_t:new(opts)
return setmetatable(
vim.tbl_deep_extend('force', {
name = '',
level = 0,
lnum = 0,
}, opts or {}),
self
)
end
---@class markdown_heading_symbols_parsed_list_t
---@field end { lnum: integer, inside_code_block: boolean }
---@field symbols markdown_heading_symbol_t[]
local markdown_heading_symbols_parsed_list_t = {}
markdown_heading_symbols_parsed_list_t.__index =
markdown_heading_symbols_parsed_list_t
---Create a new markdown heading symbols parsed object
---@param opts markdown_heading_symbols_parsed_list_t?
function markdown_heading_symbols_parsed_list_t:new(opts)
return setmetatable(
vim.tbl_deep_extend('force', {
['end'] = { lnum = 0, inside_code_block = false },
symbols = {},
}, opts or {}),
self
)
end
---@type markdown_heading_symbols_parsed_list_t[]
local markdown_heading_buf_symbols = {}
setmetatable(markdown_heading_buf_symbols, {
__index = function(_, k)
markdown_heading_buf_symbols[k] =
markdown_heading_symbols_parsed_list_t:new()
return markdown_heading_buf_symbols[k]
end,
})
---Parse markdown file and update markdown heading symbols
---Side effect: change markdown_heading_buf_symbols
---@param buf integer buffer handler
---@param lnum_end integer update symbols backward from this line (1-based, inclusive)
---@param incremental? boolean incremental parsing
---@return nil
local function parse_buf(buf, lnum_end, incremental)
if not vim.api.nvim_buf_is_valid(buf) then
markdown_heading_buf_symbols[buf] = nil
return
end
local symbols_parsed = markdown_heading_buf_symbols[buf]
local lnum_start = symbols_parsed['end'].lnum
if not incremental then
lnum_start = 0
symbols_parsed.symbols = {}
symbols_parsed['end'] = { lnum = 0, inside_code_block = false }
end
local lines = vim.api.nvim_buf_get_lines(buf, lnum_start, lnum_end, false)
symbols_parsed['end'].lnum = lnum_start + #lines + 1
for idx, line in ipairs(lines) do
if line:match('^```') then
symbols_parsed['end'].inside_code_block =
not symbols_parsed['end'].inside_code_block
end
if not symbols_parsed['end'].inside_code_block then
local _, _, heading_notation, heading_str = line:find('^(#+)%s+(.*)')
local level = heading_notation and #heading_notation or 0
if level >= 1 and level <= 6 then
table.insert(
symbols_parsed.symbols,
markdown_heading_symbol_t:new({
name = heading_str,
level = #heading_notation,
lnum = idx + lnum_start,
})
)
end
end
end
end
---Convert a markdown heading symbol into a dropbar symbol
---@param symbol markdown_heading_symbol_t markdown heading symbol
---@param symbols markdown_heading_symbol_t[] markdown heading symbols
---@param list_idx integer index of the symbol in the symbols list
---@param buf integer buffer handler
---@param win integer window handler
---@return dropbar_symbol_t
local function convert(symbol, symbols, list_idx, buf, win)
local kind = 'MarkdownH' .. symbol.level
return bar.dropbar_symbol_t:new(setmetatable({
buf = buf,
win = win,
name = symbol.name,
icon = configs.opts.icons.kinds.symbols[kind],
name_hl = 'DropBarKind' .. kind,
icon_hl = 'DropBarIconKind' .. kind,
data = {
heading_symbol = symbol,
},
}, {
---@param self dropbar_symbol_t
__index = function(self, k)
parse_buf(buf, -1, true) -- Parse whole buffer before opening menu
if k == 'children' then
self.children = {}
local lev = symbol.level
for i, heading in vim.iter(symbols):enumerate():skip(list_idx) do
if heading.level <= symbol.level then
break
end
if i == list_idx + 1 or heading.level < lev then
lev = heading.level
end
if heading.level <= lev then
table.insert(self.children, convert(heading, symbols, i, buf, win))
end
end
return self.children
end
if k == 'siblings' or k == 'idx' then
self.siblings = { convert(symbol, symbols, list_idx, buf, win) }
for i = list_idx - 1, 1, -1 do
if symbols[i].level < symbol.level then
break
end
if symbols[i].level < self.siblings[1].data.heading_symbol.level then
while
symbols[i].level
< self.siblings[1].data.heading_symbol.level
do
table.remove(self.siblings, 1)
end
table.insert(
self.siblings,
1,
convert(symbols[i], symbols, i, buf, win)
)
else
table.insert(
self.siblings,
1,
convert(symbols[i], symbols, i, buf, win)
)
end
end
self.sibling_idx = #self.siblings
for i = list_idx + 1, #symbols do
if symbols[i].level < symbol.level then
break
end
if symbols[i].level == symbol.level then
table.insert(
self.siblings,
convert(symbols[i], symbols, i, buf, win)
)
end
end
return self[k]
end
if k == 'range' then
self.range = {
start = {
line = symbol.lnum - 1,
character = 0,
},
['end'] = {
line = symbol.lnum - 1,
character = 0,
},
}
for heading in vim.iter(symbols):skip(list_idx) do
if heading.level <= symbol.level then
self.range['end'] = {
line = heading.lnum - 2,
character = 0,
}
break
end
end
return self.range
end
end,
}))
end
---Attach markdown heading parser to buffer
---@param buf integer buffer handler
---@return nil
local function attach(buf)
if vim.b[buf].dropbar_markdown_heading_parser_attached then
return
end
local function _update()
local cursor = vim.api.nvim_win_get_cursor(0)
parse_buf(buf, cursor[1])
end
vim.b[buf].dropbar_markdown_heading_parser_attached = vim.api.nvim_create_autocmd(
{ 'TextChanged', 'TextChangedI' },
{
desc = 'Update markdown heading symbols on buffer change.',
group = groupid,
buffer = buf,
callback = _update,
}
)
_update()
end
---Detach markdown heading parser from buffer
---@param buf integer buffer handler
---@return nil
local function detach(buf)
if vim.b[buf].dropbar_markdown_heading_parser_attached then
vim.api.nvim_del_autocmd(
vim.b[buf].dropbar_markdown_heading_parser_attached
)
vim.b[buf].dropbar_markdown_heading_parser_attached = nil
markdown_heading_buf_symbols[buf] = nil
end
end
---Initialize markdown heading source
---@return nil
local function init()
if initialized then
return
end
initialized = true
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[buf].filetype == 'markdown' then
attach(buf)
end
end
vim.api.nvim_create_autocmd({ 'FileType' }, {
desc = 'Attach markdown heading parser to markdown buffers.',
group = groupid,
callback = function(info)
if info.match == 'markdown' then
attach(info.buf)
else
detach(info.buf)
end
end,
})
vim.api.nvim_create_autocmd({ 'BufDelete', 'BufUnload', 'BufWipeOut' }, {
desc = 'Detach markdown heading parser from buffer on buffer delete/unload/wipeout.',
group = groupid,
callback = function(info)
if vim.bo[info.buf].filetype == 'markdown' then
detach(info.buf)
end
end,
})
end
---Get dropbar symbols from buffer according to cursor position
---@param buf integer buffer handler
---@param win integer window handler
---@param cursor integer[] cursor position
---@return dropbar_symbol_t[] symbols dropbar symbols
local function get_symbols(buf, win, cursor)
if vim.bo[buf].filetype ~= 'markdown' then
return {}
end
if not initialized then
init()
end
local buf_symbols = markdown_heading_buf_symbols[buf]
if buf_symbols['end'].lnum < cursor[1] then
parse_buf(
buf,
cursor[1] + configs.opts.sources.markdown.parse.look_ahead,
true
)
end
local result = {}
local current_level = 7
for idx, symbol in vim.iter(buf_symbols.symbols):enumerate():rev() do
if #result >= configs.opts.sources.markdown.max_depth then
break
end
if symbol.lnum <= cursor[1] and symbol.level < current_level then
current_level = symbol.level
table.insert(
result,
1,
convert(symbol, buf_symbols.symbols, idx, buf, win)
)
if current_level == 1 then
break
end
end
end
return result
end
return {
get_symbols = get_symbols,
}