Skip to content

Commit 9f4ef68

Browse files
committedMar 23, 2025
feat: improve completion source
## Details Continue to provide completions once inside of square brackets and not just when starting a list item or block quote. Requires handling a generic `paragraph` node which is the type of the body for both and handling different scenarios for the node. So as not to provide bad completions do not return anything if the user is typing outside of the start of the handled node types. This does mean we no longer have suggestions before starting a marker, meaning going to the next line in an existing list now returns nothing as we do not know if you want to continue the list and handling this behavior well is complicated. In theory anytime the user goes to a new line outside of few contexts they could start a new list or a block quote, so empty lines could always be provided with completions, but IMO this is pretty bad behavior. So as not to over-complicate things we now always require intent from the user, meaning starting a list item (via `-`, `*`, or `+`) or a block quote (via `>`) is what triggers completions, otherwise do not provide completions. Create unit tests for completions source to validate behavior.
1 parent 62d6681 commit 9f4ef68

File tree

6 files changed

+200
-52
lines changed

6 files changed

+200
-52
lines changed
 

Diff for: ‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- enabled flag for link footnote [#362](https://github.com/MeanderingProgrammer/render-markdown.nvim/pull/362)
88
[9721ffe](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/9721ffe230ec90e49c49ee33b5ca44c3fc689214)
99
- blink.cmp source registration [b8d93e8](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/b8d93e83a02dadddc6a566b1f60dab87190c1296)
10+
[62d6681](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/62d6681332365cfddbe916c888752834d9f7ad0c)
1011

1112
### Bug Fixes
1213

Diff for: ‎doc/render-markdown.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*render-markdown.txt* For 0.10.0 Last change: 2025 March 19
1+
*render-markdown.txt* For 0.10.0 Last change: 2025 March 22
22

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*

Diff for: ‎lua/render-markdown/health.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local state = require('render-markdown.state')
55
local M = {}
66

77
---@private
8-
M.version = '8.1.6'
8+
M.version = '8.1.7'
99

1010
function M.check()
1111
M.start('version')

Diff for: ‎lua/render-markdown/integ/source.lua

+57-50
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ local manager = require('render-markdown.manager')
33
local state = require('render-markdown.state')
44
local util = require('render-markdown.core.util')
55

6-
local list_markers = {
6+
local markers = {
77
list_marker_minus = '-',
88
list_marker_star = '*',
99
list_marker_plus = '+',
10+
block_quote_marker = '>',
1011
}
1112

1213
---@class render.md.Source
@@ -19,8 +20,7 @@ end
1920

2021
---@return string[]
2122
function M.trigger_characters()
22-
local characters = vim.tbl_values(list_markers)
23-
return vim.list_extend(characters, { '>', ' ' })
23+
return vim.list_extend(vim.tbl_values(markers), { ' ', '[', '!', ']' })
2424
end
2525

2626
---@param buf integer 0 for current buffer
@@ -29,26 +29,35 @@ end
2929
---@return lsp.CompletionItem[]?
3030
function M.items(buf, row, col)
3131
buf = buf == 0 and util.current('buf') or buf
32-
local node = M.get_node(buf, row, col)
33-
if node == nil then
32+
local node = M.node(buf, row, col)
33+
if node == nil or node:range() ~= row then
3434
return nil
3535
end
36-
local result, config = {}, state.get(buf)
36+
37+
local marker_node = node:named_child(0)
38+
if marker_node == nil then
39+
return nil
40+
end
41+
42+
local marker = vim.treesitter.get_node_text(marker_node, buf)
43+
local text = vim.treesitter.get_node_text(node, buf)
44+
if M.ignore(marker, text:gsub('\n$', '')) then
45+
return nil
46+
end
47+
48+
local result = {}
49+
local config = state.get(buf)
50+
local prefix = Str.spaces('end', marker) == 0 and ' ' or ''
3751
if node:type() == 'block_quote' then
38-
local quote_row = node:range()
39-
if quote_row == row then
40-
local prefix = M.space_prefix(buf, node)
41-
for _, component in pairs(config.callout) do
42-
table.insert(result, M.item(prefix .. component.raw, component.rendered, nil))
43-
end
52+
for _, component in pairs(config.callout) do
53+
table.insert(result, M.item(prefix, component.raw, component.rendered, nil))
4454
end
4555
elseif node:type() == 'list_item' then
4656
local checkbox = config.checkbox
47-
local prefix = M.list_prefix(buf, row, node)
48-
table.insert(result, M.item(prefix .. '[ ] ', checkbox.unchecked.icon, 'unchecked'))
49-
table.insert(result, M.item(prefix .. '[x] ', checkbox.checked.icon, 'checked'))
57+
table.insert(result, M.item(prefix, '[ ] ', checkbox.unchecked.icon, 'unchecked'))
58+
table.insert(result, M.item(prefix, '[x] ', checkbox.checked.icon, 'checked'))
5059
for name, component in pairs(checkbox.custom) do
51-
table.insert(result, M.item(prefix .. component.raw .. ' ', component.rendered, name))
60+
table.insert(result, M.item(prefix, component.raw .. ' ', component.rendered, name))
5261
end
5362
end
5463
return result
@@ -59,7 +68,7 @@ end
5968
---@param row integer
6069
---@param col integer
6170
---@return TSNode?
62-
function M.get_node(buf, row, col)
71+
function M.node(buf, row, col)
6372
-- Parse current row to get up to date node
6473
local has_parser, parser = pcall(vim.treesitter.get_parser, buf)
6574
if not has_parser or parser == nil then
@@ -72,55 +81,53 @@ function M.get_node(buf, row, col)
7281
lang = 'markdown',
7382
pos = { row, col },
7483
})
75-
local children = vim.tbl_keys(list_markers)
76-
vim.list_extend(children, { 'block_quote_marker', 'block_continuation' })
84+
if node ~= nil and node:type() == 'paragraph' then
85+
node = node:prev_sibling()
86+
end
87+
local children = vim.list_extend(vim.tbl_keys(markers), { 'block_continuation' })
7788
if node ~= nil and vim.tbl_contains(children, node:type()) then
7889
node = node:parent()
7990
end
8091
return node
8192
end
8293

8394
---@private
84-
---@param buf integer
85-
---@param row integer
86-
---@param node TSNode
87-
---@return string
88-
function M.list_prefix(buf, row, node)
89-
local marker_node = node:named_child(0)
90-
if marker_node == nil then
91-
return ''
92-
end
93-
local marker_row = marker_node:range()
94-
if marker_row == row then
95-
return M.space_prefix(buf, marker_node)
95+
---@param marker string
96+
---@param text string
97+
---@return boolean
98+
function M.ignore(marker, text)
99+
local prefix = vim.pesc(vim.trim(marker)) .. '%s+'
100+
local patterns = {
101+
-- The first non-space after the marker is not '['
102+
prefix .. '[^%[]',
103+
-- After '[' there is another '[' or a space
104+
prefix .. '%[.*[%[%s]',
105+
-- There is already text enclosed by '[' ']'
106+
prefix .. '%[.*%]',
107+
}
108+
for _, pattern in ipairs(patterns) do
109+
if text:find(pattern) ~= nil then
110+
return true
111+
end
96112
end
97-
local marker = list_markers[marker_node:type()]
98-
return marker ~= nil and marker .. ' ' or ''
113+
return false
99114
end
100115

101116
---@private
102-
---@param buf integer
103-
---@param node TSNode
104-
---@return string
105-
function M.space_prefix(buf, node)
106-
local text = vim.treesitter.get_node_text(node, buf)
107-
return Str.spaces('end', text) == 0 and ' ' or ''
108-
end
109-
110-
---@private
111-
---@param raw string
112-
---@param rendered string
113-
---@param name? string
117+
---@param prefix string
118+
---@param label string
119+
---@param detail string
120+
---@param description? string
114121
---@return lsp.CompletionItem
115-
function M.item(raw, rendered, name)
122+
function M.item(prefix, label, detail, description)
116123
---@type lsp.CompletionItem
117124
return {
118-
label = raw,
125+
kind = 12,
126+
label = prefix .. label,
119127
labelDetails = {
120-
detail = rendered,
121-
description = name,
128+
detail = detail,
129+
description = description,
122130
},
123-
kind = 12,
124131
}
125132
end
126133

Diff for: ‎tests/comp_spec.lua

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---@module 'luassert'
2+
3+
local util = require('tests.util')
4+
5+
---@param row integer
6+
---@param col integer
7+
---@param expected lsp.CompletionItem[]
8+
local function assert_items(row, col, expected)
9+
local source = require('render-markdown.integ.source')
10+
local actual = source.items(0, row, col) or {}
11+
table.sort(actual, function(a, b)
12+
return a.label < b.label
13+
end)
14+
assert.are.same(expected, actual)
15+
end
16+
17+
---@param prefix string
18+
---@param label string
19+
---@param detail string
20+
---@param description? string
21+
---@return lsp.CompletionItem
22+
local function item(prefix, label, detail, description)
23+
---@type lsp.CompletionItem
24+
return {
25+
kind = 12,
26+
label = prefix .. label,
27+
labelDetails = {
28+
detail = detail,
29+
description = description,
30+
},
31+
}
32+
end
33+
34+
describe('comp.md', function()
35+
it('checkbox', function()
36+
util.setup('tests/data/comp.md')
37+
38+
---@param prefix string
39+
---@return lsp.CompletionItem[]
40+
local function items(prefix)
41+
return {
42+
item(prefix, '[ ] ', '󰄱 ', 'unchecked'),
43+
item(prefix, '[-] ', '󰥔 ', 'todo'),
44+
item(prefix, '[x] ', '󰱒 ', 'checked'),
45+
}
46+
end
47+
48+
assert_items(2, 1, items(' '))
49+
assert_items(4, 2, items(''))
50+
assert_items(6, 3, items(''))
51+
assert_items(8, 4, items(''))
52+
assert_items(10, 5, {})
53+
assert_items(12, 6, {})
54+
assert_items(14, 10, {})
55+
assert_items(16, 6, {})
56+
assert_items(17, 0, {})
57+
end)
58+
59+
it('callout', function()
60+
util.setup('tests/data/comp.md')
61+
62+
---@param prefix string
63+
---@return lsp.CompletionItem[]
64+
local function items(prefix)
65+
return {
66+
item(prefix, '[!ABSTRACT]', '󰨸 Abstract'),
67+
item(prefix, '[!ATTENTION]', '󰀪 Attention'),
68+
item(prefix, '[!BUG]', '󰨰 Bug'),
69+
item(prefix, '[!CAUTION]', '󰳦 Caution'),
70+
item(prefix, '[!CHECK]', '󰄬 Check'),
71+
item(prefix, '[!CITE]', '󱆨 Cite'),
72+
item(prefix, '[!DANGER]', '󱐌 Danger'),
73+
item(prefix, '[!DONE]', '󰄬 Done'),
74+
item(prefix, '[!ERROR]', '󱐌 Error'),
75+
item(prefix, '[!EXAMPLE]', '󰉹 Example'),
76+
item(prefix, '[!FAILURE]', '󰅖 Failure'),
77+
item(prefix, '[!FAIL]', '󰅖 Fail'),
78+
item(prefix, '[!FAQ]', '󰘥 Faq'),
79+
item(prefix, '[!HELP]', '󰘥 Help'),
80+
item(prefix, '[!HINT]', '󰌶 Hint'),
81+
item(prefix, '[!IMPORTANT]', '󰅾 Important'),
82+
item(prefix, '[!INFO]', '󰋽 Info'),
83+
item(prefix, '[!MISSING]', '󰅖 Missing'),
84+
item(prefix, '[!NOTE]', '󰋽 Note'),
85+
item(prefix, '[!QUESTION]', '󰘥 Question'),
86+
item(prefix, '[!QUOTE]', '󱆨 Quote'),
87+
item(prefix, '[!SUCCESS]', '󰄬 Success'),
88+
item(prefix, '[!SUMMARY]', '󰨸 Summary'),
89+
item(prefix, '[!TIP]', '󰌶 Tip'),
90+
item(prefix, '[!TLDR]', '󰨸 Tldr'),
91+
item(prefix, '[!TODO]', '󰗡 Todo'),
92+
item(prefix, '[!WARNING]', '󰀪 Warning'),
93+
}
94+
end
95+
96+
assert_items(20, 1, items(' '))
97+
assert_items(22, 2, items(''))
98+
assert_items(24, 3, items(''))
99+
assert_items(26, 4, items(''))
100+
assert_items(28, 7, items(''))
101+
assert_items(30, 8, {})
102+
assert_items(32, 15, {})
103+
assert_items(34, 6, {})
104+
end)
105+
end)

Diff for: ‎tests/data/comp.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Heading
2+
3+
-
4+
5+
-
6+
7+
- [
8+
9+
- [-
10+
11+
- [-]
12+
13+
- [-]
14+
15+
- [-] todo
16+
17+
- text
18+
19+
# Heading
20+
21+
>
22+
23+
>
24+
25+
> [
26+
27+
> [!
28+
29+
> [!TIP
30+
31+
> [!TIP]
32+
33+
> [!TIP] My Tip
34+
35+
> text

0 commit comments

Comments
 (0)
Please sign in to comment.