Skip to content

Commit 111a0c2

Browse files
soifouSaghen
andcommitted
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]`. Example: jump to the next/previous `source_id`, if not possible select the next/previous item: ```lua opts.keymap = { -- ... ['<C-f>'] = { function(cmp) return cmp.select_next({ jump_by = 'source_id' }) end, 'select_next', }, ['<C-b>'] = { function(cmp) return cmp.select_prev({ jump_by = 'source_id' }) end, 'select_prev', }, }, ``` Closes #1890 --------- Co-authored-by: Liam Dyer <[email protected]>
1 parent dc68824 commit 111a0c2

File tree

3 files changed

+94
-16
lines changed

3 files changed

+94
-16
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: `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: `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: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,33 @@
1818
--- @field get_selected_item fun(): blink.cmp.CompletionItem?
1919
--- @field get_selection_mode fun(context: blink.cmp.Context): { preselect: boolean, auto_insert: boolean }
2020
--- @field get_item_idx_in_list fun(item?: blink.cmp.CompletionItem): number?
21-
--- @field select fun(idx?: number, opts?: { auto_insert?: boolean, undo_preview?: boolean, is_explicit_selection?: boolean })
22-
--- @field select_next fun(opts?: blink.cmp.CompletionListSelectOpts)
23-
--- @field select_prev fun(opts?: blink.cmp.CompletionListSelectOpts)
21+
--- @field select fun(idx?: number, opts?: { auto_insert?: boolean, undo_preview?: boolean, is_explicit_selection?: boolean }): boolean
22+
--- @field select_next fun(opts?: blink.cmp.CompletionListSelectOpts): boolean
23+
--- @field select_prev fun(opts?: blink.cmp.CompletionListSelectOpts): boolean
24+
--- @field can_select fun(opts?: blink.cmp.CompletionListSelectOpts): boolean
25+
--- @field jump_by fun(dir: number, opts?: blink.cmp.CompletionListSelectOpts): boolean
2426
---
2527
--- @field undo_preview fun()
2628
--- @field apply_preview fun(item: blink.cmp.CompletionItem)
2729
--- @field accept fun(opts?: blink.cmp.CompletionListAcceptOpts): boolean Applies the currently selected item, returning true if it succeeded
2830

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

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

@@ -177,10 +191,11 @@ function list.select(idx, opts)
177191
list.is_explicitly_selected = opts.is_explicit_selection == nil and true or opts.is_explicit_selection
178192
list.selected_item_idx = idx
179193
list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context })
194+
return true
180195
end
181196

182197
function list.select_next(opts)
183-
if #list.items == 0 or list.context == nil then return end
198+
if #list.items == 0 or list.context == nil then return false end
184199

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

194209
-- cycling around has been disabled, ignore
195-
if not list.config.cycle.from_bottom then return end
210+
if not list.config.cycle.from_bottom then return false end
196211

197212
-- otherwise, we cycle around
198213
return list.select(1, opts)
199214
end
200215

201-
-- typical case, select the next item
216+
if opts and opts.jump_by and list.jump_by(1, opts) then return true end
217+
218+
-- fallback, select the next item
202219
local count = opts and opts.count or 1
203-
list.select(math.min(list.selected_item_idx + count, #list.items), opts)
220+
return list.select(math.min(list.selected_item_idx + count, #list.items), opts)
204221
end
205222

206223
function list.select_prev(opts)
207-
if #list.items == 0 or list.context == nil then return end
224+
if #list.items == 0 or list.context == nil then return false end
208225

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

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

222239
-- cycling around has been disabled, ignore
223-
if not list.config.cycle.from_top then return end
240+
if not list.config.cycle.from_top then return false end
224241

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

229-
-- typical case, select the previous item
246+
if opts and opts.jump_by and list.jump_by(-1, opts) then return true end
247+
248+
-- fallback, select the previous item
230249
local count = opts and opts.count or 1
231-
list.select(math.max(list.selected_item_idx - count, 1), opts)
250+
return list.select(math.max(list.selected_item_idx - count, 1), opts)
251+
end
252+
253+
--- Check if we're able to perform a selection.
254+
--- We need this because `select_next` and `select_prev` are scheduled, but we need to know whether
255+
--- to fallback beforehand.
256+
--- @param opts blink.cmp.CompletionListSelectOpts
257+
--- @return boolean
258+
function list.can_select(opts)
259+
local cmp = require('blink.cmp')
260+
local on_ghost_text = opts and opts.on_ghost_text
261+
if not cmp.is_menu_visible() and (not on_ghost_text or not cmp.is_ghost_text_visible()) then return false end
262+
263+
if not opts or not opts.jump_by then return true end
264+
265+
if list.selected_item_idx then
266+
local current = list.items[list.selected_item_idx][opts.jump_by]
267+
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(current)) then return false end
268+
end
269+
270+
return true
271+
end
272+
273+
--- Jump to the item whose specified property differs from the current one. Supports cycling.
274+
--- @param dir integer direction - 1 for next, -1 for previous
275+
--- @param opts blink.cmp.CompletionListSelectOpts
276+
--- @return boolean
277+
function list.jump_by(dir, opts)
278+
opts = opts or {}
279+
assert(vim.tbl_contains({ 'string', 'function' }, type(opts.jump_by)), 'jump_by must be a string or function')
280+
281+
if not list.items or #list.items == 0 or not list.selected_item_idx then return false end
282+
283+
local current = list.items[list.selected_item_idx][opts.jump_by]
284+
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(current)) then return false end
285+
286+
local function try_jump(start_idx, end_idx, step)
287+
for i = start_idx, end_idx, step do
288+
if list.items[i][opts.jump_by] ~= current then return list.select(i, opts) end
289+
end
290+
return false
291+
end
292+
293+
if dir == 1 then
294+
if try_jump(list.selected_item_idx + 1, #list.items, 1) then return true end
295+
if list.config and list.config.cycle and list.config.cycle.from_bottom then
296+
return try_jump(1, list.selected_item_idx - 1, 1)
297+
end
298+
elseif dir == -1 then
299+
if try_jump(list.selected_item_idx - 1, 1, -1) then return true end
300+
if list.config and list.config.cycle and list.config.cycle.from_top then
301+
return try_jump(#list.items, list.selected_item_idx + 1, -1)
302+
end
303+
end
304+
305+
return false
232306
end
233307

234308
---------- Preview ----------

lua/blink/cmp/init.lua

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,15 @@ end
199199
--- Select the previous completion item
200200
--- @param opts? blink.cmp.CompletionListSelectOpts
201201
function cmp.select_prev(opts)
202-
local on_ghost_text = opts and opts.on_ghost_text
203-
if not cmp.is_menu_visible() and (not on_ghost_text or not cmp.is_ghost_text_visible()) then return end
202+
if not require('blink.cmp.completion.list').can_select(opts) then return end
204203
vim.schedule(function() require('blink.cmp.completion.list').select_prev(opts) end)
205204
return true
206205
end
207206

208207
--- Select the next completion item
209208
--- @param opts? blink.cmp.CompletionListSelectOpts
210209
function cmp.select_next(opts)
211-
local on_ghost_text = opts and opts.on_ghost_text
212-
if not cmp.is_menu_visible() and (not on_ghost_text or not cmp.is_ghost_text_visible()) then return end
210+
if not require('blink.cmp.completion.list').can_select(opts) then return end
213211
vim.schedule(function() require('blink.cmp.completion.list').select_next(opts) end)
214212
return true
215213
end
@@ -218,6 +216,8 @@ end
218216
--- This will trigger completions if none are available, unlike `select_next` which would fallback to the next keymap in this case.
219217
function cmp.insert_next()
220218
if not cmp.is_active() then return cmp.show_and_insert() end
219+
if not require('blink.cmp.completion.list').can_select({ auto_insert = true }) then return end
220+
221221
vim.schedule(function() require('blink.cmp.completion.list').select_next({ auto_insert = true }) end)
222222
return true
223223
end
@@ -226,6 +226,8 @@ end
226226
--- This will trigger completions if none are available, unlike `select_prev` which would fallback to the next keymap in this case.
227227
function cmp.insert_prev()
228228
if not cmp.is_active() then return cmp.show_and_insert() end
229+
if not require('blink.cmp.completion.list').can_select({ auto_insert = true }) then return end
230+
229231
vim.schedule(function() require('blink.cmp.completion.list').select_prev({ auto_insert = true }) end)
230232
return true
231233
end

0 commit comments

Comments
 (0)