diff --git a/doc/configuration/keymap.md b/doc/configuration/keymap.md index db672247..b255bbbd 100644 --- a/doc/configuration/keymap.md +++ b/doc/configuration/keymap.md @@ -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 diff --git a/lua/blink/cmp/completion/list.lua b/lua/blink/cmp/completion/list.lua index ce0c9fa5..79c777c1 100644 --- a/lua/blink/cmp/completion/list.lua +++ b/lua/blink/cmp/completion/list.lua @@ -18,9 +18,11 @@ --- @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) @@ -28,9 +30,21 @@ --- @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 @@ -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 @@ -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 and list.jump_by(1, opts) then return true 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 @@ -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 and list.jump_by(-1, opts) then return true 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 ---------- diff --git a/lua/blink/cmp/init.lua b/lua/blink/cmp/init.lua index 617e6ef6..f15e4513 100644 --- a/lua/blink/cmp/init.lua +++ b/lua/blink/cmp/init.lua @@ -199,8 +199,7 @@ 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 @@ -208,8 +207,7 @@ 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 @@ -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 @@ -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