Skip to content

Commit 9f0cc38

Browse files
committed
feat(keymap): add jump_by option to navigate by item property
Introduce `jump_by` option for `select_next` and `select_prev` commands to jump to the item whose specified property differs from the current one. Support primitive properties (`string`, `number`, `boolean`) of a completion item, including: - client_id - client_name - deprecated - exact - kind - score - score_offset - source_id - source_name Honor cycling, set by `completion.list.cycle.from_[top|bottom]`. Falls back to `count` when no match is found. Example, jump to the next/previous `source_id`: ```lua opts.keymaps = { -- ... ['<C-f>'] = { function(cmp) return cmp.select_next({ jump_by = 'source_id' }) end, 'fallback', }, ['<C-b>'] = { function(cmp) return cmp.select_prev({ jump_by = 'source_id' }) end, 'fallback', }, }, ``` Closes #1890
1 parent dc68824 commit 9f0cc38

File tree

2 files changed

+61
-2
lines changed

2 files changed

+61
-2
lines changed

doc/configuration/keymap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ keymap = {
5757
- `select_prev`: Selects the previous item, cycling to the bottom of the list if at the top, if `completion.list.cycle.from_top == true`
5858
- Optionally control the `auto_insert` property of `completion.list.selection`: `function(cmp) cmp.select_prev({ auto_insert = false }) end`
5959
- Optionally, run when ghost text is visible, instead of only when the menu is visible: `function(cmp) cmp.select_prev({ on_ghost_text = true })`
60+
- Optionally, jump to the item whose specified property differs from the current one. Fallbacks to `count` when no such item is found: `function(cmp) cmp.select_prev({ jump_by = 'source_id' })`
6061
- `select_next`: Selects the next item, cycling to the top of the list if at the bottom, if `completion.list.cycle.from_bottom == true`
6162
- Optionally control the `auto_insert` property of `completion.list.selection`: `function(cmp) cmp.select_next({ auto_insert = false }) end`
6263
- Optionally, run when ghost text is visible, instead of only when the menu is visible: `function(cmp) cmp.select_next({ on_ghost_text = true })`
64+
- Optionally, jump to the item whose specified property differs from the current one. Fallbacks to `count` when no such item is found: `function(cmp) cmp.select_next({ jump_by = 'source_id' })`
6365
- `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.
6466
- `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.
6567
- `show_documentation`: Shows the documentation for the currently selected item

lua/blink/cmp/completion/list.lua

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,29 @@
2121
--- @field select fun(idx?: number, opts?: { auto_insert?: boolean, undo_preview?: boolean, is_explicit_selection?: boolean })
2222
--- @field select_next fun(opts?: blink.cmp.CompletionListSelectOpts)
2323
--- @field select_prev fun(opts?: blink.cmp.CompletionListSelectOpts)
24+
--- @field jump_by fun(dir: number, opts?: blink.cmp.CompletionListSelectOpts): boolean
2425
---
2526
--- @field undo_preview fun()
2627
--- @field apply_preview fun(item: blink.cmp.CompletionItem)
2728
--- @field accept fun(opts?: blink.cmp.CompletionListAcceptOpts): boolean Applies the currently selected item, returning true if it succeeded
2829

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

36+
--- @alias blink.cmp.CompletionListJumpBy
37+
--- | 'client_id'
38+
--- | 'client_name'
39+
--- | 'deprecated'
40+
--- | 'exact'
41+
--- | 'kind'
42+
--- | 'score'
43+
--- | 'score_offset'
44+
--- | 'source_id'
45+
--- | 'source_name'
46+
3447
--- @class blink.cmp.CompletionListSelectAndAcceptOpts
3548
--- @field callback? fun() Called after the item is accepted
3649

@@ -198,7 +211,10 @@ function list.select_next(opts)
198211
return list.select(1, opts)
199212
end
200213

201-
-- typical case, select the next item
214+
-- try to jump to the item whose specified property differs from the current one
215+
if list.jump_by(1, opts) then return end
216+
217+
-- fallback, select the next item
202218
local count = opts and opts.count or 1
203219
list.select(math.min(list.selected_item_idx + count, #list.items), opts)
204220
end
@@ -226,11 +242,52 @@ function list.select_prev(opts)
226242
return list.select(#list.items, opts)
227243
end
228244

229-
-- typical case, select the previous item
245+
-- try to jump to the item whose specified property differs from the current one
246+
if list.jump_by(-1, opts) then return end
247+
248+
-- fallback, select the previous item
230249
local count = opts and opts.count or 1
231250
list.select(math.max(list.selected_item_idx - count, 1), opts)
232251
end
233252

253+
--- Jump to the item whose specified property differs from the current one. Supports cycling.
254+
--- @param dir integer direction - 1 for next, -1 for previous
255+
--- @param opts blink.cmp.CompletionListSelectOpts
256+
--- @return boolean
257+
function list.jump_by(dir, opts)
258+
opts = opts or {}
259+
260+
if not list.items or #list.items == 0 or not list.selected_item_idx then return false end
261+
if type(opts.jump_by) ~= 'string' then return false end
262+
263+
local current = list.items[list.selected_item_idx][opts.jump_by]
264+
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(current)) then return false end
265+
266+
local function try_jump(start_idx, end_idx, step)
267+
for i = start_idx, end_idx, step do
268+
if list.items[i][opts.jump_by] ~= current then
269+
list.select(i, opts)
270+
return true
271+
end
272+
end
273+
return false
274+
end
275+
276+
if dir == 1 then
277+
if try_jump(list.selected_item_idx + 1, #list.items, 1) then return true end
278+
if list.config and list.config.cycle and list.config.cycle.from_bottom then
279+
return try_jump(1, list.selected_item_idx - 1, 1)
280+
end
281+
elseif dir == -1 then
282+
if try_jump(list.selected_item_idx - 1, 1, -1) then return true end
283+
if list.config and list.config.cycle and list.config.cycle.from_top then
284+
return try_jump(#list.items, list.selected_item_idx + 1, -1)
285+
end
286+
end
287+
288+
return false
289+
end
290+
234291
---------- Preview ----------
235292

236293
function list.undo_preview()

0 commit comments

Comments
 (0)