Skip to content

Commit 0271d79

Browse files
committed
feat: use treesitter for signature help hl
1 parent 9b9be31 commit 0271d79

File tree

5 files changed

+186
-168
lines changed

5 files changed

+186
-168
lines changed

lua/blink/cmp/utils.lua

-104
Original file line numberDiff line numberDiff line change
@@ -40,110 +40,6 @@ function utils.is_blocked_buffer()
4040
return buftype ~= ''
4141
end
4242

43-
function utils.split_lines(text)
44-
local lines = {}
45-
for s in text:gmatch('[^\r\n]+') do
46-
table.insert(lines, s)
47-
end
48-
return lines
49-
end
50-
51-
--- Combines adjacent paragraph lines together
52-
--- @param lines string[]
53-
--- @return string[]
54-
--- TODO: Likely buggy
55-
function utils.combine_markdown_lines(lines)
56-
local combined_lines = {}
57-
58-
local special_starting_chars = { '#', '>', '-', '|' }
59-
local in_code_block = false
60-
local prev_is_special = false
61-
for _, line in ipairs(lines) do
62-
if line:match('^%s*```') then in_code_block = not in_code_block end
63-
64-
local is_special = line:match('^%s*[' .. table.concat(special_starting_chars) .. ']') or line:match('^%s*%d\\.$')
65-
local is_empty = line:match('^%s*$')
66-
local has_linebreak = line:match('%s%s$')
67-
68-
if #combined_lines == 0 or in_code_block or is_special or prev_is_special or is_empty or has_linebreak then
69-
table.insert(combined_lines, line)
70-
elseif line:match('^%s*$') then
71-
table.insert(combined_lines, '')
72-
else
73-
combined_lines[#combined_lines] = combined_lines[#combined_lines] .. '' .. line
74-
end
75-
76-
prev_is_special = is_special
77-
end
78-
79-
return combined_lines
80-
end
81-
82-
--- Highlights the given range with treesitter with the given filetype
83-
--- @param bufnr number
84-
--- @param filetype string
85-
--- @param start_line number
86-
--- @param end_line number
87-
--- TODO: fallback to regex highlighting if treesitter fails
88-
function utils.highlight_with_treesitter(bufnr, filetype, start_line, end_line)
89-
local Range = require('vim.treesitter._range')
90-
91-
local root_lang = vim.treesitter.language.get_lang(filetype)
92-
if root_lang == nil then return end
93-
94-
local success, trees = pcall(vim.treesitter.get_parser, bufnr, root_lang)
95-
if not success or not trees then return end
96-
97-
trees:parse({ start_line, end_line })
98-
99-
trees:for_each_tree(function(tree, tstree)
100-
local lang = tstree:lang()
101-
local highlighter_query = vim.treesitter.query.get(lang, 'highlights')
102-
if not highlighter_query then return end
103-
104-
local root_node = tree:root()
105-
local _, _, root_end_row, _ = root_node:range()
106-
107-
local iter = highlighter_query:iter_captures(tree:root(), bufnr, start_line, end_line)
108-
local line = start_line
109-
while line < end_line do
110-
local capture, node, metadata, _ = iter(line)
111-
if capture == nil then break end
112-
113-
local range = { root_end_row + 1, 0, root_end_row + 1, 0 }
114-
if node then range = vim.treesitter.get_range(node, bufnr, metadata and metadata[capture]) end
115-
local start_row, start_col, end_row, end_col = Range.unpack4(range)
116-
117-
if capture then
118-
local name = highlighter_query.captures[capture]
119-
local hl = 0
120-
if not vim.startswith(name, '_') then hl = vim.api.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) end
121-
122-
-- The "priority" attribute can be set at the pattern level or on a particular capture
123-
local priority = (
124-
tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
125-
or vim.highlight.priorities.treesitter
126-
)
127-
128-
-- The "conceal" attribute can be set at the pattern level or on a particular capture
129-
local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
130-
131-
if hl and end_row >= line then
132-
vim.api.nvim_buf_set_extmark(bufnr, require('blink.cmp.config').highlight.ns, start_row, start_col, {
133-
end_line = end_row,
134-
end_col = end_col,
135-
hl_group = hl,
136-
priority = priority,
137-
conceal = conceal,
138-
})
139-
end
140-
end
141-
142-
if start_row > line then line = start_row end
143-
end
144-
end)
145-
end
146-
14743
--- Gets characters around the cursor and returns the range, 0-indexed
14844
--- @param range 'prefix' | 'full'
14945
--- @param regex string

lua/blink/cmp/windows/documentation.lua

+9-47
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
local config = require('blink.cmp.config').windows.documentation
2-
local utils = require('blink.cmp.utils')
32
local sources = require('blink.cmp.sources.lib')
43
local autocomplete = require('blink.cmp.windows.autocomplete')
54
local docs = {}
@@ -48,58 +47,21 @@ function docs.show_item(item)
4847

4948
-- todo: cancellation
5049
-- todo: only resolve if documentation does not exist
51-
sources.resolve(item, function(resolved_item)
52-
if resolved_item ~= nil and resolved_item.documentation == nil then
53-
resolved_item.documentation = item.documentation
54-
end
55-
if resolved_item ~= nil and resolved_item.detail == nil then resolved_item.detail = item.detail end
56-
57-
item = resolved_item or item
50+
sources.resolve(item):map(function(item)
5851
if item.documentation == nil and item.detail == nil then
5952
docs.win:close()
6053
return
6154
end
6255

63-
local detail_lines = {}
64-
if item.detail and item.detail ~= '' then detail_lines = utils.split_lines(item.detail) end
65-
66-
local doc_lines = {}
67-
if item.documentation ~= nil then
68-
local doc = type(item.documentation) == 'string' and item.documentation or item.documentation.value
69-
doc_lines = utils.split_lines(doc)
70-
if type(item.documentation) ~= 'string' and item.documentation.kind == 'markdown' then
71-
-- if the rendering seems bugged, it's likely due to this function
72-
doc_lines = utils.combine_markdown_lines(doc_lines)
73-
end
74-
end
75-
76-
local combined_lines = vim.list_extend({}, detail_lines)
77-
-- add a blank line for the --- separator
78-
if #detail_lines > 0 and #doc_lines > 0 then table.insert(combined_lines, '') end
79-
vim.list_extend(combined_lines, doc_lines)
80-
81-
vim.api.nvim_buf_set_lines(docs.win:get_buf(), 0, -1, true, combined_lines)
82-
vim.api.nvim_set_option_value('modified', false, { buf = docs.win:get_buf() })
83-
84-
-- Highlight with treesitter
85-
vim.api.nvim_buf_clear_namespace(docs.win:get_buf(), require('blink.cmp.config').highlight.ns, 0, -1)
86-
87-
if #detail_lines > 0 then utils.highlight_with_treesitter(docs.win:get_buf(), vim.bo.filetype, 0, #detail_lines) end
88-
89-
-- Only add the separator if there are documentation lines (otherwise only display the detail)
90-
if #detail_lines > 0 and #doc_lines > 0 then
91-
vim.api.nvim_buf_set_extmark(docs.win:get_buf(), require('blink.cmp.config').highlight.ns, #detail_lines, 0, {
92-
virt_text = { { string.rep('', docs.win.config.max_width) } },
93-
virt_text_pos = 'overlay',
94-
hl_eol = true,
95-
hl_group = 'BlinkCmpDocDetail',
96-
})
97-
end
98-
99-
if #doc_lines > 0 then
100-
local start = #detail_lines + (#detail_lines > 0 and 1 or 0)
101-
utils.highlight_with_treesitter(docs.win:get_buf(), 'markdown', start, start + #doc_lines)
56+
if docs.shown_item ~= item then
57+
require('blink.cmp.windows.lib.docs').render_detail_and_documentation(
58+
docs.win:get_buf(),
59+
item.detail,
60+
item.documentation,
61+
docs.win.config.max_width
62+
)
10263
end
64+
docs.shown_item = item
10365

10466
if autocomplete.win:get_win() then
10567
docs.win:open()

lua/blink/cmp/windows/lib/docs.lua

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
local docs = {}
2+
3+
--- @param bufnr number
4+
--- @param detail? string
5+
--- @param documentation? lsp.MarkupContent | string
6+
function docs.render_detail_and_documentation(bufnr, detail, documentation, max_width)
7+
local detail_lines = {}
8+
if detail and detail ~= '' then detail_lines = docs.split_lines(detail) end
9+
10+
local doc_lines = {}
11+
if documentation ~= nil then
12+
local doc = type(documentation) == 'string' and documentation or documentation.value
13+
doc_lines = docs.split_lines(doc)
14+
if type(documentation) ~= 'string' and documentation.kind == 'markdown' then
15+
-- if the rendering seems bugged, it's likely due to this function
16+
doc_lines = docs.combine_markdown_lines(doc_lines)
17+
end
18+
end
19+
20+
local combined_lines = vim.list_extend({}, detail_lines)
21+
-- add a blank line for the --- separator
22+
if #detail_lines > 0 and #doc_lines > 0 then table.insert(combined_lines, '') end
23+
vim.list_extend(combined_lines, doc_lines)
24+
25+
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, combined_lines)
26+
vim.api.nvim_set_option_value('modified', false, { buf = bufnr })
27+
28+
-- Highlight with treesitter
29+
vim.api.nvim_buf_clear_namespace(bufnr, require('blink.cmp.config').highlight.ns, 0, -1)
30+
31+
if #detail_lines > 0 then docs.highlight_with_treesitter(bufnr, vim.bo.filetype, 0, #detail_lines) end
32+
33+
-- Only add the separator if there are documentation lines (otherwise only display the detail)
34+
if #detail_lines > 0 and #doc_lines > 0 then
35+
vim.api.nvim_buf_set_extmark(bufnr, require('blink.cmp.config').highlight.ns, #detail_lines, 0, {
36+
virt_text = { { string.rep('', max_width) } },
37+
virt_text_pos = 'overlay',
38+
hl_eol = true,
39+
hl_group = 'BlinkCmpDocDetail',
40+
})
41+
end
42+
43+
if #doc_lines > 0 then
44+
local start = #detail_lines + (#detail_lines > 0 and 1 or 0)
45+
docs.highlight_with_treesitter(bufnr, 'markdown', start, start + #doc_lines)
46+
end
47+
end
48+
49+
--- Highlights the given range with treesitter with the given filetype
50+
--- @param bufnr number
51+
--- @param filetype string
52+
--- @param start_line number
53+
--- @param end_line number
54+
--- TODO: fallback to regex highlighting if treesitter fails
55+
function docs.highlight_with_treesitter(bufnr, filetype, start_line, end_line)
56+
local Range = require('vim.treesitter._range')
57+
58+
local root_lang = vim.treesitter.language.get_lang(filetype)
59+
if root_lang == nil then return end
60+
61+
local success, trees = pcall(vim.treesitter.get_parser, bufnr, root_lang)
62+
if not success or not trees then return end
63+
64+
trees:parse({ start_line, end_line })
65+
66+
trees:for_each_tree(function(tree, tstree)
67+
local lang = tstree:lang()
68+
local highlighter_query = vim.treesitter.query.get(lang, 'highlights')
69+
if not highlighter_query then return end
70+
71+
local root_node = tree:root()
72+
local _, _, root_end_row, _ = root_node:range()
73+
74+
local iter = highlighter_query:iter_captures(tree:root(), bufnr, start_line, end_line)
75+
local line = start_line
76+
while line < end_line do
77+
local capture, node, metadata, _ = iter(line)
78+
if capture == nil then break end
79+
80+
local range = { root_end_row + 1, 0, root_end_row + 1, 0 }
81+
if node then range = vim.treesitter.get_range(node, bufnr, metadata and metadata[capture]) end
82+
local start_row, start_col, end_row, end_col = Range.unpack4(range)
83+
84+
if capture then
85+
local name = highlighter_query.captures[capture]
86+
local hl = 0
87+
if not vim.startswith(name, '_') then hl = vim.api.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) end
88+
89+
-- The "priority" attribute can be set at the pattern level or on a particular capture
90+
local priority = (
91+
tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
92+
or vim.highlight.priorities.treesitter
93+
)
94+
95+
-- The "conceal" attribute can be set at the pattern level or on a particular capture
96+
local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
97+
98+
if hl and end_row >= line then
99+
vim.api.nvim_buf_set_extmark(bufnr, require('blink.cmp.config').highlight.ns, start_row, start_col, {
100+
end_line = end_row,
101+
end_col = end_col,
102+
hl_group = hl,
103+
priority = priority,
104+
conceal = conceal,
105+
})
106+
end
107+
end
108+
109+
if start_row > line then line = start_row end
110+
end
111+
end)
112+
end
113+
114+
--- Combines adjacent paragraph lines together
115+
--- @param lines string[]
116+
--- @return string[]
117+
--- TODO: Likely buggy
118+
function docs.combine_markdown_lines(lines)
119+
local combined_lines = {}
120+
121+
local special_starting_chars = { '#', '>', '-', '|' }
122+
local in_code_block = false
123+
local prev_is_special = false
124+
for _, line in ipairs(lines) do
125+
if line:match('^%s*```') then in_code_block = not in_code_block end
126+
127+
local is_special = line:match('^%s*[' .. table.concat(special_starting_chars) .. ']') or line:match('^%s*%d\\.$')
128+
local is_empty = line:match('^%s*$')
129+
local has_linebreak = line:match('%s%s$')
130+
131+
if #combined_lines == 0 or in_code_block or is_special or prev_is_special or is_empty or has_linebreak then
132+
table.insert(combined_lines, line)
133+
elseif line:match('^%s*$') then
134+
table.insert(combined_lines, '')
135+
else
136+
combined_lines[#combined_lines] = combined_lines[#combined_lines] .. '' .. line
137+
end
138+
139+
prev_is_special = is_special
140+
end
141+
142+
return combined_lines
143+
end
144+
145+
function docs.split_lines(text)
146+
local lines = {}
147+
for s in text:gmatch('[^\r\n]+') do
148+
table.insert(lines, s)
149+
end
150+
return lines
151+
end
152+
153+
return docs

lua/blink/cmp/windows/lib/init.lua

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ function win:get_buf()
4242
self.buf = vim.api.nvim_create_buf(false, true)
4343
vim.api.nvim_set_option_value('tabstop', 1, { buf = self.buf }) -- prevents tab widths from being unpredictable
4444
vim.api.nvim_set_option_value('filetype', self.config.filetype, { buf = self.buf })
45-
vim.treesitter.stop(self.buf)
4645
end
4746
return self.buf
4847
end

0 commit comments

Comments
 (0)