Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/configuration/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ keymap = {
- `select_prev`: Selects the previous item, cycling to the bottom of the list if at the top, if `completion.list.cycle.from_top == true`
- Optionally control the `auto_insert` property of `completion.list.selection`: `function(cmp) cmp.select_prev({ auto_insert = false }) end`
- Optionally, run when ghost text is visible, instead of only when the menu is visible: `function(cmp) cmp.select_prev({ on_ghost_text = true })`
- Optionally, jump to the item whose specified property differs from the current one: `function(cmp) cmp.select_prev({ jump_by = 'source_id' })`
- `select_next`: Selects the next item, cycling to the top of the list if at the bottom, if `completion.list.cycle.from_bottom == true`
- Optionally control the `auto_insert` property of `completion.list.selection`: `function(cmp) cmp.select_next({ auto_insert = false }) end`
- Optionally, run when ghost text is visible, instead of only when the menu is visible: `function(cmp) cmp.select_next({ on_ghost_text = true })`
- Optionally, jump to the item whose specified property differs from the current one: `function(cmp) cmp.select_next({ jump_by = 'source_id' })`
- `insert_prev`: Inserts the previous item (`auto_insert`), cycling to the bottom of the list if at the top, if `completion.list.cycle.from_top == true`. This will trigger completions if none are available, unlike `select_prev` which would fallback to the next keymap in this case.
- `insert_next`: Inserts the next item (`auto_insert`), cycling to the top of the list if at the bottom, if `completion.list.cycle.from_bottom == true`. This will trigger completions if none are available, unlike `select_next` which would fallback to the next keymap in this case.
- `show_documentation`: Shows the documentation for the currently selected item
Expand Down
98 changes: 86 additions & 12 deletions lua/blink/cmp/completion/list.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,33 @@
--- @field get_selected_item fun(): blink.cmp.CompletionItem?
--- @field get_selection_mode fun(context: blink.cmp.Context): { preselect: boolean, auto_insert: boolean }
--- @field get_item_idx_in_list fun(item?: blink.cmp.CompletionItem): number?
--- @field select fun(idx?: number, opts?: { auto_insert?: boolean, undo_preview?: boolean, is_explicit_selection?: boolean })
--- @field select_next fun(opts?: blink.cmp.CompletionListSelectOpts)
--- @field select_prev fun(opts?: blink.cmp.CompletionListSelectOpts)
--- @field select fun(idx?: number, opts?: { auto_insert?: boolean, undo_preview?: boolean, is_explicit_selection?: boolean }): boolean
--- @field select_next fun(opts?: blink.cmp.CompletionListSelectOpts): boolean
--- @field select_prev fun(opts?: blink.cmp.CompletionListSelectOpts): boolean
--- @field can_select fun(opts?: blink.cmp.CompletionListSelectOpts): boolean
--- @field jump_by fun(dir: number, opts?: blink.cmp.CompletionListSelectOpts): boolean
---
--- @field undo_preview fun()
--- @field apply_preview fun(item: blink.cmp.CompletionItem)
--- @field accept fun(opts?: blink.cmp.CompletionListAcceptOpts): boolean Applies the currently selected item, returning true if it succeeded

--- @class blink.cmp.CompletionListSelectOpts
--- @field count? number The number of items to jump by, defaults to 1
--- @field jump_by? blink.cmp.CompletionListJumpBy Jump to the item whose specified property differs from the current one.
--- @field auto_insert? boolean Insert the completion item automatically when selecting it
--- @field on_ghost_text? boolean Run when ghost text is visible, instead of only when the menu is visible

--- @alias blink.cmp.CompletionListJumpBy
--- | 'client_id'
--- | 'client_name'
--- | 'deprecated'
--- | 'exact'
--- | 'kind'
--- | 'score'
--- | 'score_offset'
--- | 'source_id'
--- | 'source_name'

--- @class blink.cmp.CompletionListSelectAndAcceptOpts
--- @field callback? fun() Called after the item is accepted

Expand Down Expand Up @@ -177,10 +191,11 @@ function list.select(idx, opts)
list.is_explicitly_selected = opts.is_explicit_selection == nil and true or opts.is_explicit_selection
list.selected_item_idx = idx
list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context })
return true
end

function list.select_next(opts)
if #list.items == 0 or list.context == nil then return end
if #list.items == 0 or list.context == nil then return false end

-- haven't selected anything yet, select the first item, if cycling enabled
if list.selected_item_idx == nil then return list.select(1, opts) end
Expand All @@ -192,23 +207,25 @@ function list.select_next(opts)
if not select_mode.preselect or select_mode.auto_insert then return list.select(nil, opts) end

-- cycling around has been disabled, ignore
if not list.config.cycle.from_bottom then return end
if not list.config.cycle.from_bottom then return false end

-- otherwise, we cycle around
return list.select(1, opts)
end

-- typical case, select the next item
if opts and opts.jump_by then return list.jump_by(1, opts) end

-- fallback, select the next item
local count = opts and opts.count or 1
list.select(math.min(list.selected_item_idx + count, #list.items), opts)
return list.select(math.min(list.selected_item_idx + count, #list.items), opts)
end

function list.select_prev(opts)
if #list.items == 0 or list.context == nil then return end
if #list.items == 0 or list.context == nil then return false end

-- haven't selected anything yet, select the last item, if cycling enabled
if list.selected_item_idx == nil then
if not list.config.cycle.from_top then return end
if not list.config.cycle.from_top then return false end

return list.select(#list.items, opts)
end
Expand All @@ -220,15 +237,72 @@ function list.select_prev(opts)
if not select_mode.preselect or select_mode.auto_insert then return list.select(nil, opts) end

-- cycling around has been disabled, ignore
if not list.config.cycle.from_top then return end
if not list.config.cycle.from_top then return false end

-- otherwise, we cycle around
return list.select(#list.items, opts)
end

-- typical case, select the previous item
if opts and opts.jump_by then return list.jump_by(-1, opts) end

-- fallback, select the previous item
local count = opts and opts.count or 1
list.select(math.max(list.selected_item_idx - count, 1), opts)
return list.select(math.max(list.selected_item_idx - count, 1), opts)
end

--- Check if we're able to perform a selection.
--- We need this because `select_next` and `select_prev` are scheduled, but we need to know whether
--- to fallback beforehand.
--- @param opts blink.cmp.CompletionListSelectOpts
--- @return boolean
function list.can_select(opts)
local cmp = require('blink.cmp')
local on_ghost_text = opts and opts.on_ghost_text
if not cmp.is_menu_visible() and (not on_ghost_text or not cmp.is_ghost_text_visible()) then return false end

if not opts or not opts.jump_by then return true end

if list.selected_item_idx then
local current = list.items[list.selected_item_idx][opts.jump_by]
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(current)) then return false end
end

return true
end

--- Jump to the item whose specified property differs from the current one. Supports cycling.
--- @param dir integer direction - 1 for next, -1 for previous
--- @param opts blink.cmp.CompletionListSelectOpts
--- @return boolean
function list.jump_by(dir, opts)
opts = opts or {}
assert(vim.tbl_contains({ 'string', 'function' }, type(opts.jump_by)), 'jump_by must be a string or function')

if not list.items or #list.items == 0 or not list.selected_item_idx then return false end

local current = list.items[list.selected_item_idx][opts.jump_by]
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(current)) then return false end

local function try_jump(start_idx, end_idx, step)
for i = start_idx, end_idx, step do
if list.items[i][opts.jump_by] ~= current then return list.select(i, opts) end
end
return false
end

if dir == 1 then
if try_jump(list.selected_item_idx + 1, #list.items, 1) then return true end
if list.config and list.config.cycle and list.config.cycle.from_bottom then
return try_jump(1, list.selected_item_idx - 1, 1)
end
elseif dir == -1 then
if try_jump(list.selected_item_idx - 1, 1, -1) then return true end
if list.config and list.config.cycle and list.config.cycle.from_top then
return try_jump(#list.items, list.selected_item_idx + 1, -1)
end
end

return false
end

---------- Preview ----------
Expand Down
10 changes: 6 additions & 4 deletions lua/blink/cmp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,15 @@ end
--- Select the previous completion item
--- @param opts? blink.cmp.CompletionListSelectOpts
function cmp.select_prev(opts)
local on_ghost_text = opts and opts.on_ghost_text
if not cmp.is_menu_visible() and (not on_ghost_text or not cmp.is_ghost_text_visible()) then return end
if not require('blink.cmp.completion.list').can_select(opts) then return end
vim.schedule(function() require('blink.cmp.completion.list').select_prev(opts) end)
return true
end

--- Select the next completion item
--- @param opts? blink.cmp.CompletionListSelectOpts
function cmp.select_next(opts)
local on_ghost_text = opts and opts.on_ghost_text
if not cmp.is_menu_visible() and (not on_ghost_text or not cmp.is_ghost_text_visible()) then return end
if not require('blink.cmp.completion.list').can_select(opts) then return end
vim.schedule(function() require('blink.cmp.completion.list').select_next(opts) end)
return true
end
Expand All @@ -218,6 +216,8 @@ end
--- This will trigger completions if none are available, unlike `select_next` which would fallback to the next keymap in this case.
function cmp.insert_next()
if not cmp.is_active() then return cmp.show_and_insert() end
if not require('blink.cmp.completion.list').can_select({ auto_insert = true }) then return end

vim.schedule(function() require('blink.cmp.completion.list').select_next({ auto_insert = true }) end)
return true
end
Expand All @@ -226,6 +226,8 @@ end
--- This will trigger completions if none are available, unlike `select_prev` which would fallback to the next keymap in this case.
function cmp.insert_prev()
if not cmp.is_active() then return cmp.show_and_insert() end
if not require('blink.cmp.completion.list').can_select({ auto_insert = true }) then return end

vim.schedule(function() require('blink.cmp.completion.list').select_prev({ auto_insert = true }) end)
return true
end
Expand Down
Loading