Skip to content

Commit 3e55028

Browse files
committed
feat: custom drawing support
1 parent cf9e4aa commit 3e55028

File tree

5 files changed

+214
-50
lines changed

5 files changed

+214
-50
lines changed

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,24 @@ For LazyVim/distro users, you can disable nvim-cmp via:
155155
max_width = 60,
156156
max_height = 10,
157157
border = 'none',
158+
winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None',
159+
-- keep the cursor X lines away from the top/bottom of the window
160+
scrolloff = 2,
158161
-- which directions to show the window,
159162
-- falling back to the next direction when there's not enough space
160163
direction_priority = { 's', 'n' },
164+
-- Controls how the completion items are rendered on the popup window
165+
-- 'simple' will render the item's kind icon the left alongside the label
166+
-- 'reversed' will render the label on the left and the kind icon + name on the right
167+
-- 'function(blink.cmp.CompletionRenderContext): blink.cmp.Component[]' for custom rendering
168+
draw = 'simple',
161169
},
162170
documentation = {
163171
min_width = 10,
164172
max_width = 60,
165173
max_height = 20,
166174
border = 'padded',
175+
winhighlight = 'Normal:BlinkCmpDoc,FloatBorder:BlinkCmpDocBorder,CursorLine:BlinkCmpDocCursorLine,Search:None',
167176
-- which directions to show the documentation window,
168177
-- for each of the possible autocomplete window directions,
169178
-- falling back to the next direction when there's not enough space
@@ -180,6 +189,7 @@ For LazyVim/distro users, you can disable nvim-cmp via:
180189
max_width = 100,
181190
max_height = 10,
182191
border = 'padded',
192+
winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder',
183193
},
184194
},
185195

@@ -260,10 +270,11 @@ The plugin use a 4 stage pipeline: trigger -> sources -> fuzzy -> render
260270
- Boosts completion item score via frecency *and* proximity bonus. nvim-cmp only boosts score via proximity bonus and optionally by recency
261271
- Typo-resistant fuzzy matching unlike nvim-cmp's fzf-style fuzzy matching
262272
- Core sources (buffer, snippets, path, lsp) are built-in versus nvim-cmp's exclusively external sources
273+
- Built-in auto bracket and signature help support
263274

264275
### Planned missing features
265276

266-
- Less customizable across the board wrt trigger, sources, sorting, filtering, and rendering
277+
- Less customizable with regards to trigger, sources, sorting, filtering
267278
- Significantly less testing and documentation
268279
- Ghost text
269280
- Matched character highlighting

lua/blink/cmp/config.lua

+7
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
--- @field preselect boolean
8585
--- @field winhighlight string
8686
--- @field scrolloff number
87+
--- @field draw 'simple' | 'reversed' | function(blink.cmp.CompletionRenderContext): blink.cmp.Component[]
8788

8889
--- @class blink.cmp.DocumentationDirectionPriorityConfig
8990
--- @field autocomplete_north ("n" | "s" | "e" | "w")[]
@@ -213,6 +214,7 @@ local config = {
213214
max_height = 10,
214215
border = 'none',
215216
winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None',
217+
-- keep the cursor X lines away from the top/bottom of the window
216218
scrolloff = 2,
217219
-- todo: implement
218220
order = 'top_down',
@@ -221,6 +223,11 @@ local config = {
221223
direction_priority = { 's', 'n' },
222224
-- todo: implement
223225
preselect = true,
226+
-- Controls how the completion items are rendered on the popup window
227+
-- 'simple' will render the item's kind icon the left alongside the label
228+
-- 'reversed' will render the label on the left and the kind icon + name on the right
229+
-- 'function(blink.cmp.CompletionRenderContext): blink.cmp.Component[]' for custom rendering
230+
draw = 'simple',
224231
},
225232
documentation = {
226233
min_width = 10,

lua/blink/cmp/windows/autocomplete.lua

+59-49
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
--- @class blink.cmp.CompletionRenderContext
2+
--- @field item blink.cmp.CompletionItem
3+
--- @field kind string
4+
--- @field kind_icon string
5+
--- @field icon_gap string
6+
17
-- todo: track cursor position
28
local config = require('blink.cmp.config')
9+
local renderer = require('blink.cmp.windows.lib.render')
310
local autocmp_config = config.windows.autocomplete
411
local autocomplete = {
512
items = {},
@@ -30,32 +37,9 @@ function autocomplete.setup()
3037
return autocomplete.win:get_win() == winnr and bufnr == autocomplete.win:get_buf()
3138
end,
3239
on_line = function(_, _, bufnr, line_number)
33-
local item = autocomplete.items[line_number + 1]
34-
if item == nil then return end
35-
local line_text = vim.api.nvim_buf_get_lines(bufnr, line_number, line_number + 1, false)[1]
36-
37-
local kind = vim.lsp.protocol.CompletionItemKind[item.kind] or 'Unknown'
38-
local kind_hl = 'BlinkCmpKind' .. kind
39-
40-
-- todo: handle .labelDetails and others
41-
vim.api.nvim_buf_set_extmark(bufnr, config.highlight.ns, line_number, 0, {
42-
end_col = 4,
43-
hl_group = kind_hl,
44-
hl_mode = 'combine',
45-
hl_eol = true,
46-
ephemeral = true,
47-
})
48-
49-
-- todo: use vim.lsp.protocol.CompletionItemTag
50-
if item.deprecated or (item.tags and vim.tbl_contains(item.tags, 1)) then
51-
-- todo: why 7?
52-
vim.api.nvim_buf_set_extmark(bufnr, config.highlight.ns, line_number, 7, {
53-
end_col = #line_text - 1,
54-
hl_group = 'BlinkCmpLabelDeprecated',
55-
hl_mode = 'combine',
56-
ephemeral = true,
57-
})
58-
end
40+
local rendered_item = autocomplete.rendered_items[line_number + 1]
41+
if rendered_item == nil then return end
42+
renderer.draw_highlights(rendered_item, bufnr, config.highlight.ns, line_number)
5943
end,
6044
})
6145

@@ -174,42 +158,68 @@ function autocomplete.listen_on_select(callback) autocomplete.event_targets.on_s
174158
---------- Rendering ----------
175159

176160
function autocomplete.draw()
177-
local max_line_width = 0
161+
local draw_fn = autocomplete.get_draw_fn()
162+
local icon_gap = config.nerd_font_variant == 'mono' and ' ' or ' '
163+
local arr_of_components = {}
178164
for _, item in ipairs(autocomplete.items) do
179-
max_line_width = math.max(max_line_width, autocomplete.get_item_max_length(item))
165+
local kind = vim.lsp.protocol.CompletionItemKind[item.kind] or 'Unknown'
166+
local kind_icon = config.kind_icons[kind] or config.kind_icons.Field
167+
168+
table.insert(
169+
arr_of_components,
170+
draw_fn({
171+
item = item,
172+
kind = kind,
173+
kind_icon = kind_icon,
174+
icon_gap = icon_gap,
175+
})
176+
)
180177
end
181-
local target_width =
182-
math.max(math.min(max_line_width, autocomplete.win.config.max_width), autocomplete.win.config.min_width)
183178

184-
local lines = {}
185-
for _, item in ipairs(autocomplete.items) do
186-
table.insert(lines, autocomplete.draw_item(item, target_width))
187-
end
179+
local max_line_length = renderer.get_max_length(arr_of_components)
180+
autocomplete.rendered_items = vim.tbl_map(
181+
function(component) return renderer.render(component, max_line_length) end,
182+
arr_of_components
183+
)
188184

185+
local lines = vim.tbl_map(function(rendered) return rendered.text end, autocomplete.rendered_items)
189186
local bufnr = autocomplete.win:get_buf()
190187
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
191188
vim.api.nvim_set_option_value('modified', false, { buf = bufnr })
192189
end
193190

194-
function autocomplete.get_item_max_length(item)
195-
local icon_width = 4
196-
local label_width = vim.api.nvim_strwidth(autocomplete.get_label(item))
197-
return icon_width + label_width
191+
function autocomplete.get_draw_fn()
192+
if type(autocmp_config.draw) == 'function' then
193+
return autocmp_config.draw
194+
elseif autocmp_config.draw == 'simple' then
195+
return autocomplete.render_item_simple
196+
elseif autocmp_config.draw == 'reversed' then
197+
return autocomplete.render_item_reversed
198+
end
199+
error('Invalid autocomplete window draw config')
198200
end
199201

200-
function autocomplete.draw_item(item, max_length)
201-
local kind = vim.lsp.protocol.CompletionItemKind[item.kind] or 'Unknown'
202-
local kind_icon = config.kind_icons[kind] or config.kind_icons.Field
203-
204-
-- get line text
205-
local label = autocomplete.get_label(item)
206-
local other_content_length = 5
207-
local abbr = string.sub(label, 1, max_length - other_content_length)
208-
209-
return string.format(' %s%s%s ', kind_icon, config.nerd_font_variant == 'mono' and ' ' or ' ', abbr)
202+
--- @param ctx blink.cmp.CompletionRenderContext
203+
--- @return blink.cmp.Component[]
204+
function autocomplete.render_item_simple(ctx)
205+
return {
206+
{ ' ', ctx.kind_icon, ctx.icon_gap, hl_group = 'BlinkCmpKind' .. ctx.kind },
207+
{ ctx.item.label, fill = true, hl_group = ctx.item.deprecated and 'BlinkCmpLabelDeprecated' or 'BlinkCmpLabel' },
208+
}
210209
end
211210

212-
function autocomplete.get_label(item) return item.label end
211+
--- @param ctx blink.cmp.CompletionRenderContext
212+
--- @return blink.cmp.Component[]
213+
function autocomplete.render_item_reversed(ctx)
214+
return {
215+
{
216+
' ' .. ctx.item.label,
217+
fill = true,
218+
hl_group = ctx.item.deprecated and 'BlinkCmpLabelDeprecated' or 'BlinkCmpLabel',
219+
},
220+
{ ' ', ctx.kind_icon, ctx.icon_gap, ctx.kind .. ' ', hl_group = 'BlinkCmpKind' .. ctx.kind },
221+
}
222+
end
213223

214224
function autocomplete.get_selected_item()
215225
if not autocomplete.win:is_open() then return end
File renamed without changes.

lua/blink/cmp/windows/lib/render.lua

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
--- @class blink.cmp.Component
2+
--- @field [number] blink.cmp.Component | string
3+
--- @field fill boolean | nil
4+
--- @field hl_group string | nil
5+
--- @field hl_params table | nil
6+
7+
--- @class blink.cmp.RenderedComponentTree
8+
--- @field text string
9+
--- @field highlights { start: number, stop: number, group: string | nil, params: table | nil }[]
10+
11+
local renderer = {}
12+
13+
--- Draws the highlights for the rendered component tree
14+
--- as ephemeral extmarks
15+
--- @param rendered blink.cmp.RenderedComponentTree
16+
function renderer.draw_highlights(rendered, bufnr, ns, line_number)
17+
for _, highlight in ipairs(rendered.highlights) do
18+
vim.api.nvim_buf_set_extmark(bufnr, ns, line_number, highlight.start - 1, {
19+
end_col = highlight.stop - 1,
20+
hl_group = highlight.group,
21+
hl_mode = 'combine',
22+
hl_eol = true,
23+
ephemeral = true,
24+
})
25+
end
26+
end
27+
28+
--- @param components blink.cmp.Component[]
29+
--- @param length number
30+
--- @return blink.cmp.RenderedComponentTree
31+
function renderer.render(components, length)
32+
local left_of_fill = {}
33+
local right_of_fill = {}
34+
local fill = nil
35+
for _, component in ipairs(components) do
36+
if component.fill then
37+
fill = component
38+
else
39+
table.insert(fill and right_of_fill or left_of_fill, component)
40+
end
41+
end
42+
43+
local left_rendered = renderer.render_components(left_of_fill)
44+
local fill_rendered = renderer.render_components({ fill })
45+
local right_rendered = renderer.render_components(right_of_fill)
46+
47+
-- expanad/truncate the fill component to the width
48+
fill_rendered.text = fill_rendered.text
49+
.. string.rep(' ', length - vim.api.nvim_strwidth(left_rendered.text .. fill_rendered.text .. right_rendered.text))
50+
fill_rendered.text = fill_rendered.text:sub(
51+
1,
52+
length - vim.api.nvim_strwidth(left_rendered.text) - vim.api.nvim_strwidth(right_rendered.text)
53+
)
54+
55+
renderer.add_offset_to_rendered_component(fill_rendered, left_rendered.text:len())
56+
renderer.add_offset_to_rendered_component(right_rendered, left_rendered.text:len() + fill_rendered.text:len())
57+
58+
local highlights = {}
59+
vim.list_extend(highlights, left_rendered.highlights)
60+
vim.list_extend(highlights, fill_rendered.highlights)
61+
vim.list_extend(highlights, right_rendered.highlights)
62+
63+
return {
64+
text = left_rendered.text .. fill_rendered.text .. right_rendered.text,
65+
highlights = highlights,
66+
}
67+
end
68+
69+
--- @param components (blink.cmp.Component | string)[]
70+
--- @return blink.cmp.RenderedComponentTree
71+
function renderer.render_components(components)
72+
local text = ''
73+
local offset = 0
74+
local highlights = {}
75+
76+
for _, component in ipairs(components) do
77+
if type(component) == 'string' then
78+
text = text .. component
79+
offset = offset + #component
80+
else
81+
local rendered = renderer.render_components(component)
82+
83+
renderer.add_offset_to_rendered_component(rendered, offset)
84+
table.insert(highlights, {
85+
start = offset + 1,
86+
stop = offset + #rendered.text + 1,
87+
group = component.hl_group,
88+
params = component.hl_params,
89+
})
90+
vim.list_extend(highlights, rendered.highlights)
91+
92+
text = text .. rendered.text
93+
offset = offset + #rendered.text
94+
end
95+
end
96+
97+
return { text = text, highlights = highlights }
98+
end
99+
100+
--- @param components blink.cmp.Component[]
101+
--- @return number
102+
function renderer.get_length(components)
103+
local length = 0
104+
for _, component in ipairs(components) do
105+
if type(component) == 'string' then
106+
length = length + #component
107+
else
108+
length = length + renderer.get_length(component)
109+
end
110+
end
111+
return length
112+
end
113+
114+
--- @param arr_of_components blink.cmp.Component[][]
115+
--- @return number
116+
function renderer.get_max_length(arr_of_components)
117+
local max_length = 0
118+
for _, components in ipairs(arr_of_components) do
119+
local length = renderer.get_length(components)
120+
if length > max_length then max_length = length end
121+
end
122+
return max_length
123+
end
124+
125+
--- @param component blink.cmp.RenderedComponentTree
126+
--- @param offset number
127+
--- @return blink.cmp.RenderedComponentTree
128+
function renderer.add_offset_to_rendered_component(component, offset)
129+
for _, highlight in ipairs(component.highlights) do
130+
highlight.start = highlight.start + offset
131+
highlight.stop = highlight.stop + offset
132+
end
133+
return component
134+
end
135+
136+
return renderer

0 commit comments

Comments
 (0)