Skip to content

Commit a67adaf

Browse files
committed
feat: rework window positioning
closes #45 closes #194
1 parent 88f1c20 commit a67adaf

File tree

7 files changed

+309
-168
lines changed

7 files changed

+309
-168
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,10 @@ MiniDeps.add({
409409
max_height = 10,
410410
border = 'padded',
411411
winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder',
412+
413+
-- which directions to show the window,
414+
-- falling back to the next direction when there's not enough space
415+
direction_priority = { 'n', 's' },
412416
},
413417
ghost_text = {
414418
enabled = false,

lua/blink/cmp/config.lua

+6-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
--- @field selection? "preselect" | "manual" | "auto_insert"
118118
--- @field winhighlight? string
119119
--- @field scrolloff? number
120-
--- @field draw? 'simple' | 'reversed' | 'minimal' | function(blink.cmp.CompletionRenderContext): blink.cmp.Component[]
120+
--- @field draw? 'simple' | 'reversed' | 'minimal' | blink.cmp.CompletionDrawFn
121121
--- @field cycle? blink.cmp.AutocompleteConfig.CycleConfig
122122

123123
--- @class blink.cmp.AutocompleteConfig.CycleConfig
@@ -148,6 +148,7 @@
148148
--- @field max_height? number
149149
--- @field border? blink.cmp.WindowBorder
150150
--- @field winhighlight? string
151+
--- @field direction_priority? ("n" | "s")[]
151152

152153
--- @class GhostTextConfig
153154
--- @field enabled? boolean
@@ -391,6 +392,10 @@ local config = {
391392
max_height = 10,
392393
border = 'padded',
393394
winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder',
395+
396+
-- which directions to show the window,
397+
-- falling back to the next direction when there's not enough space
398+
direction_priority = { 'n', 's' },
394399
},
395400
ghost_text = {
396401
enabled = false,

lua/blink/cmp/trigger/completion.lua

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ function trigger.activate_autocmds()
116116
})
117117

118118
-- definitely leaving the context
119+
-- TODO: handle leaving snippet mode
119120
vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufLeave' }, { callback = trigger.hide })
120121

121122
-- manually hide when exiting insert mode with ctrl+c, since it doesn't trigger InsertLeave

lua/blink/cmp/windows/autocomplete.lua

+84-66
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,67 @@
66
--- @field icon_gap string
77
--- @field deprecated boolean
88

9+
--- @alias blink.cmp.CompletionDrawFn fun(ctx: blink.cmp.CompletionRenderContext): blink.cmp.Component[]
10+
11+
--- @class blink.cmp.CompletionWindowEventTargets
12+
--- @field on_open table<fun()>
13+
--- @field on_close table<fun()>
14+
--- @field on_position_update table<fun()>
15+
--- @field on_select table<fun(item: blink.cmp.CompletionItem?, context: blink.cmp.Context)>
16+
17+
--- @class blink.cmp.CompletionWindow
18+
--- @field win blink.cmp.Window
19+
--- @field items blink.cmp.CompletionItem[]
20+
--- @field rendered_items? blink.cmp.RenderedComponentTree[]
21+
--- @field has_selected? boolean
22+
--- @field auto_show boolean
23+
--- @field context blink.cmp.Context?
24+
--- @field event_targets blink.cmp.CompletionWindowEventTargets
25+
---
26+
--- @field setup fun(): blink.cmp.CompletionWindow
27+
---
28+
--- @field open_with_items fun(context: blink.cmp.CompletionRenderContext, items: blink.cmp.CompletionItem[])
29+
--- @field open fun()
30+
--- @field close fun()
31+
--- @field listen_on_open fun(callback: fun())
32+
--- @field listen_on_close fun(callback: fun())
33+
---
34+
--- @field update_position fun(context: blink.cmp.Context)
35+
--- @field listen_on_position_update fun(callback: fun())
36+
---
37+
--- @field accept fun(): boolean?
38+
---
39+
--- @field select fun(line: number, skip_auto_insert?: boolean)
40+
--- @field select_next fun(opts?: { skip_auto_insert?: boolean })
41+
--- @field select_prev fun(opts?: { skip_auto_insert?: boolean })
42+
--- @field get_selected_item fun(): blink.cmp.CompletionItem?
43+
--- @field set_has_selected fun(selected: boolean)
44+
--- @field listen_on_select fun(callback: fun(item: blink.cmp.CompletionItem?, context: blink.cmp.Context))
45+
--- @field emit_on_select fun(item: blink.cmp.CompletionItem?, context: blink.cmp.Context)
46+
---
47+
--- @field draw fun()
48+
--- @field get_draw_fn fun(): blink.cmp.CompletionDrawFn
49+
--- @field draw_item_simple blink.cmp.CompletionDrawFn
50+
--- @field draw_item_reversed blink.cmp.CompletionDrawFn
51+
--- @field draw_item_minimal blink.cmp.CompletionDrawFn
52+
953
local config = require('blink.cmp.config')
1054
local renderer = require('blink.cmp.windows.lib.render')
1155
local text_edits_lib = require('blink.cmp.accept.text-edits')
1256
local autocmp_config = config.windows.autocomplete
57+
58+
--- @type blink.cmp.CompletionWindow
59+
--- @diagnostic disable-next-line: missing-fields
1360
local autocomplete = {
14-
---@type blink.cmp.CompletionItem[]
1561
items = {},
1662
has_selected = nil,
1763
-- hack: ideally this doesn't get mutated by the public API
1864
auto_show = autocmp_config.auto_show,
19-
---@type blink.cmp.Context?
2065
context = nil,
2166
event_targets = {
2267
on_position_update = {},
23-
--- @type table<fun(item: blink.cmp.CompletionItem?, context: blink.cmp.Context)>
2468
on_select = {},
25-
--- @type table<fun()>
2669
on_close = {},
27-
--- @type table<fun()>
2870
on_open = {},
2971
},
3072
}
@@ -55,8 +97,7 @@ function autocomplete.setup()
5597

5698
vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, {
5799
callback = function()
58-
if autocomplete.context == nil then return end
59-
autocomplete.update_position(autocomplete.context)
100+
if autocomplete.context ~= nil then autocomplete.update_position(autocomplete.context) end
60101
end,
61102
})
62103

@@ -106,7 +147,7 @@ function autocomplete.open_with_items(context, items)
106147
-- todo: some logic to maintain the selection if the user moved the cursor?
107148
vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { 1, 0 })
108149

109-
autocomplete.on_select_callbacks(autocomplete.get_selected_item(), context)
150+
autocomplete.emit_on_select(autocomplete.get_selected_item(), context)
110151
end
111152

112153
function autocomplete.open()
@@ -125,17 +166,13 @@ function autocomplete.close()
125166
vim.iter(autocomplete.event_targets.on_close):each(function(callback) callback() end)
126167
end
127168

128-
--- Add a listener for when the autocomplete window closes
129-
--- @param callback fun()
130-
function autocomplete.listen_on_close(callback) table.insert(autocomplete.event_targets.on_close, callback) end
131-
132169
--- Add a listener for when the autocomplete window opens
133170
--- This is useful for hiding GitHub Copilot ghost text and similar functionality.
134-
---
135-
--- @param callback fun()
136171
function autocomplete.listen_on_open(callback) table.insert(autocomplete.event_targets.on_open, callback) end
137172

138-
--- @param context blink.cmp.Context
173+
--- Add a listener for when the autocomplete window closes
174+
function autocomplete.listen_on_close(callback) table.insert(autocomplete.event_targets.on_close, callback) end
175+
139176
--- TODO: Don't switch directions if the context is the same
140177
function autocomplete.update_position(context)
141178
local win = autocomplete.win
@@ -144,34 +181,22 @@ function autocomplete.update_position(context)
144181

145182
win:update_size()
146183

147-
local height = win:get_height()
148-
local cursor_screen_position = win.get_cursor_screen_position()
184+
local border_size = win:get_border_size()
185+
local pos = win:get_vertical_direction_and_height(autocmp_config.direction_priority)
149186

150-
local cursor = vim.api.nvim_win_get_cursor(0)
151-
local cursor_col = cursor[2]
187+
-- couldn't find anywhere to place the window
188+
if not pos then
189+
win:close()
190+
return
191+
end
152192

153193
-- place the window at the start col of the current text we're fuzzy matching against
154194
-- so the window doesnt move around as we type
155-
local col = context.bounds.start_col - cursor_col - (context.bounds.start_col == 0 and 0 or 1)
156-
157-
-- detect if there's space above/below the cursor
158-
-- todo: should pick the largest space if both are false and limit height of the window
159-
local is_space_below = cursor_screen_position.distance_from_bottom > height
160-
local is_space_above = cursor_screen_position.distance_from_top > height
161-
162-
-- default to the user's preference but attempt to use the other options
163-
local row = autocmp_config.direction_priority[1] == 's' and 1 or -height
164-
for _, direction in ipairs(autocmp_config.direction_priority) do
165-
if direction == 's' and is_space_below then
166-
row = 1
167-
break
168-
elseif direction == 'n' and is_space_above then
169-
row = -height
170-
break
171-
end
172-
end
173-
195+
local cursor_col = vim.api.nvim_win_get_cursor(0)[2]
196+
local col = context.bounds.start_col - cursor_col - (context.bounds.length == 0 and 0 or 1)
197+
local row = pos.direction == 's' and 1 or -pos.height - border_size.vertical
174198
vim.api.nvim_win_set_config(winnr, { relative = 'cursor', row = row, col = col })
199+
vim.api.nvim_win_set_height(winnr, pos.height)
175200

176201
for _, callback in ipairs(autocomplete.event_targets.on_position_update) do
177202
callback()
@@ -198,8 +223,6 @@ function autocomplete.accept()
198223
return true
199224
end
200225

201-
--- @param line number
202-
--- @param skip_auto_insert? boolean
203226
function autocomplete.select(line, skip_auto_insert)
204227
autocomplete.set_has_selected(true)
205228
vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { line, 0 })
@@ -216,10 +239,9 @@ function autocomplete.select(line, skip_auto_insert)
216239
end)
217240
end
218241

219-
autocomplete.on_select_callbacks(selected_item, autocomplete.context)
242+
autocomplete.emit_on_select(selected_item, autocomplete.context)
220243
end
221244

222-
--- @params opts? { skip_auto_insert?: boolean }
223245
function autocomplete.select_next(opts)
224246
if not autocomplete.win:is_open() then return end
225247

@@ -241,7 +263,6 @@ function autocomplete.select_next(opts)
241263
autocomplete.select(line, opts and opts.skip_auto_insert)
242264
end
243265

244-
--- @params opts? { skip_auto_insert?: boolean }
245266
function autocomplete.select_prev(opts)
246267
if not autocomplete.win:is_open() then return end
247268

@@ -259,24 +280,29 @@ function autocomplete.select_prev(opts)
259280
autocomplete.select(line, opts and opts.skip_auto_insert)
260281
end
261282

283+
function autocomplete.get_selected_item()
284+
if not autocomplete.win:is_open() then return end
285+
if not autocomplete.has_selected then return end
286+
local line = vim.api.nvim_win_get_cursor(autocomplete.win:get_win())[1]
287+
return autocomplete.items[line]
288+
end
289+
290+
function autocomplete.set_has_selected(selected)
291+
if not autocomplete.win:is_open() then return end
292+
autocomplete.has_selected = selected
293+
autocomplete.win:set_option_value('cursorline', selected)
294+
end
295+
262296
function autocomplete.listen_on_select(callback) table.insert(autocomplete.event_targets.on_select, callback) end
263297

264-
--- @param item? blink.cmp.CompletionItem
265-
--- @param context blink.cmp.Context
266-
function autocomplete.on_select_callbacks(item, context)
298+
function autocomplete.emit_on_select(item, context)
267299
for _, callback in ipairs(autocomplete.event_targets.on_select) do
268300
callback(item, context)
269301
end
270302
end
271303

272304
---------- Rendering ----------
273305

274-
function autocomplete.set_has_selected(selected)
275-
if not autocomplete.win:is_open() then return end
276-
autocomplete.has_selected = selected
277-
autocomplete.win:set_option_values('cursorline', selected)
278-
end
279-
280306
function autocomplete.draw()
281307
local draw_fn = autocomplete.get_draw_fn()
282308
local icon_gap = config.nerd_font_variant == 'mono' and ' ' or ' '
@@ -318,18 +344,18 @@ function autocomplete.get_draw_fn()
318344
if type(autocmp_config.draw) == 'function' then
319345
return autocmp_config.draw
320346
elseif autocmp_config.draw == 'simple' then
321-
return autocomplete.render_item_simple
347+
return autocomplete.draw_item_simple
322348
elseif autocmp_config.draw == 'reversed' then
323-
return autocomplete.render_item_reversed
349+
return autocomplete.draw_item_reversed
324350
elseif autocmp_config.draw == 'minimal' then
325-
return autocomplete.render_item_minimal
351+
return autocomplete.draw_item_minimal
326352
end
327353
error('Invalid autocomplete window draw config')
328354
end
329355

330356
--- @param ctx blink.cmp.CompletionRenderContext
331357
--- @return blink.cmp.Component[]
332-
function autocomplete.render_item_simple(ctx)
358+
function autocomplete.draw_item_simple(ctx)
333359
return {
334360
' ',
335361
{ ctx.kind_icon, ctx.icon_gap, hl_group = 'BlinkCmpKind' .. ctx.kind },
@@ -347,7 +373,7 @@ end
347373

348374
--- @param ctx blink.cmp.CompletionRenderContext
349375
--- @return blink.cmp.Component[]
350-
function autocomplete.render_item_reversed(ctx)
376+
function autocomplete.draw_item_reversed(ctx)
351377
return {
352378
' ',
353379
{
@@ -365,7 +391,7 @@ end
365391

366392
--- @param ctx blink.cmp.CompletionRenderContext
367393
--- @return blink.cmp.Component[]
368-
function autocomplete.render_item_minimal(ctx)
394+
function autocomplete.draw_item_minimal(ctx)
369395
return {
370396
' ',
371397
{
@@ -381,12 +407,4 @@ function autocomplete.render_item_minimal(ctx)
381407
}
382408
end
383409

384-
---@return blink.cmp.CompletionItem?
385-
function autocomplete.get_selected_item()
386-
if not autocomplete.win:is_open() then return end
387-
if not autocomplete.has_selected then return end
388-
local line = vim.api.nvim_win_get_cursor(autocomplete.win:get_win())[1]
389-
return autocomplete.items[line]
390-
end
391-
392410
return autocomplete

0 commit comments

Comments
 (0)