Skip to content

Commit

Permalink
feat!: support preselect with auto_insert, set as default
Browse files Browse the repository at this point in the history
Closes #668
  • Loading branch information
Saghen committed Jan 6, 2025
1 parent 047444a commit 8126d0e
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 92 deletions.
47 changes: 36 additions & 11 deletions docs/configuration/completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,33 +57,58 @@ TODO: Find a case where this actually fires : )

## List <a href="./reference#completion-list"><Badge type="info" text="Go to default configuration" /></a>

Manages the completion list and its behavior when selecting items. The most commonly changed option is `completion.list.selection`, which controls whether the list will automatically select the first item in the list, and whether selection shows a preview:

To control the selection behavior per mode, pass a function to `completion.list.selection` that returns the selection mode:
Manages the completion list and its behavior when selecting items. The most commonly changed option is `selection.preselect/auto_insert`, which controls whether the list will automatically select the first item in the list, and whether a "preview" will be inserted on selection.

:::tabs
== Preselect, Auto Insert (default)
```lua
completion.list.selection = 'preselect'
-- or
completion.list.selection = function(ctx)
return ctx.mode == 'cmdline' and 'auto_insert' or 'preselect'
end
completion.list.selection = { preselect = true, auto_insert = true }
```
Selects the first item automatically, and inserts a preview of the item on selection. The `cancel` keymap (default `<C-e>`) will close the menu and undo the preview.

<video src="https://github.com/user-attachments/assets/ef295526-8332-4ad0-9a2a-e2f6484081b2" muted autoplay loop />

:::tabs
== Preselect
```lua
completion.list.selection = { preselect = true, auto_insert = false }
```
Selects the first item automatically

<img src="https://github.com/user-attachments/assets/69079ced-43f1-437e-8a45-3cb13f841d61" />
== Manual
```lua
completion.list.selection = { preselect = false, auto_insert = false }
```

No item will be selected by default. You may use the `select_and_accept` keymap command to select the first item and accept it when there's no selection. The `accept` keymap command, on the other hand, will only trigger if an item is selected.

<video src="https://github.com/user-attachments/assets/09cd9b4b-18b3-456b-bb0a-074ae54e9d77" muted autoplay loop />
== Auto Insert
No item will be selected by default, and selecting an item will insert a "preview" of the item automatically. You may use the `select_and_accept` keymap command to select the first item and accept it when there's no selection. The `accept` keymap command, on the other hand, will only trigger if an item is selected.
== Manual, Auto Insert
```lua
completion.list.selection = { preselect = false, auto_insert = true }
```

Selecting an item will insert a "preview" of the item automatically. You may use the `select_and_accept` keymap command to select the first item and accept it when there's no selection. The `accept` keymap command will only trigger if an item is selected. The `cancel` keymap (default `<C-e>`) will close the menu and undo the preview.

<video src="https://github.com/user-attachments/assets/4658b61d-1b95-404a-b6b5-3a4afbfb8112" muted autoplay loop />
:::

To control the selection behavior per mode, pass a function to `selection.preselect/auto_insert`:

```lua
completion.list.selection = {
preselect = true,
auto_insert = true,

-- or a function
preselect = function(ctx)
return ctx.mode ~= 'cmdline' and not require('blink.cmp').snippet_active({ direction = 1 })
end,
-- auto_insert = function(ctx) return ctx.mode ~= 'cmdline' end,
}
```


## Accept <a href="./reference#completion-accept"><Badge type="info" text="Go to default configuration" /></a>

Manages the behavior when accepting an item in the completion menu.
Expand Down
8 changes: 4 additions & 4 deletions docs/configuration/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ For more common configurations, see the [recipes](../recipes.md).
-- NOTE: some LSPs may add auto brackets themselves anyway
accept = { auto_brackets = { enabled = false }, },

-- Insert completion item on selection, don't select by default
list = { selection = 'auto_insert' },
-- or set per mode
list = { selection = function(ctx) return ctx.mode == 'cmdline' and 'auto_insert' or 'preselect' end },
-- Don't select by default, auto insert on selection
list = { selection = { preselect = false, auto_insert = true } },
-- or set either per mode via a function
list = { selection = { preselect = function(ctx) return ctx.mode ~= 'cmdline' end } },

menu = {
-- Don't automatically show the completion menu
Expand Down
8 changes: 5 additions & 3 deletions docs/configuration/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ keymap = {
- `show`: Shows the completion menu
- Optionally use `function(cmp) cmp.show({ providers = { 'snippets' } }) end` to show with a specific list of providers
- `hide`: Hides the completion menu
- `cancel`: Reverts `completion.list.selection = 'auto_insert'` and hides the completion menu
- `cancel`: Reverts `completion.list.selection.auto_insert` and hides the completion menu
- `accept`: Accepts the currently selected item
- Optionally pass an index to select a specific item in the list: `function(cmp) cmp.accept({ index = 1 }) end`
- Optionally pass a `callback` to run after the item is accepted: `function(cmp) cmp.accept({ callback = function() vim.api.nvim_feedkeys('\n', 'n', true) end }) end`
- `select_and_accept`: Accepts the currently selected item, or the first item if none are selected
- `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`
- `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`
- `show_documentation`: Shows the documentation for the currently selected item
- `hide_documentation`: Hides the documentation
- `scroll_documentation_up`: Scrolls the documentation up by 4 lines
Expand Down Expand Up @@ -96,7 +98,7 @@ Set the preset to `none` to disable the presets

### `super-tab`

You may want to use `completion.list.selection = "manual" | "auto_insert"`. You can also set it per mode: https://cmp.saghen.dev/configuration/completion.html#list
You may want to set `completion.trigger.show_in_snippet = false` or use `completion.list.selection.preselect = function(ctx) return not require('blink.cmp').snippet_active({ direction = 1 }) end`. See more info in: https://cmp.saghen.dev/configuration/completion.html#list

```lua
['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' },
Expand All @@ -123,7 +125,7 @@ You may want to use `completion.list.selection = "manual" | "auto_insert"`. You

### `enter`

You may want to set `completion.list.selection = "manual" | "auto_insert"`. You can also set it per mode: https://cmp.saghen.dev/configuration/completion.html#list
You may want to set `completion.list.selection.preselect = false`. See more info in: https://cmp.saghen.dev/configuration/completion.html#list

```lua
['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' },
Expand Down
24 changes: 12 additions & 12 deletions docs/configuration/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,18 @@ completion.list = {
-- Maximum number of items to display
max_items = 200,

-- Controls if completion items will be selected automatically,
-- and whether selection automatically inserts
selection = 'preselect',
-- selection = function(ctx) return ctx.mode == 'cmdline' and 'auto_insert' or 'preselect' end,

-- Controls how the completion items are selected
-- 'preselect' will automatically select the first item in the completion list
-- 'manual' will not select any item by default
-- 'auto_insert' will not select any item by default, and insert the completion items automatically when selecting them
--
-- You may want to bind a key to the `cancel` command, which will undo the selection
-- when using 'auto_insert'
selection = {
-- When `true`, will automatically select the first item in the completion list
preselect = true,
-- preselect = function(ctx) return ctx.mode ~= 'cmdline' end,

-- When `true`, inserts the completion item automatically when selecting it
-- You may want to bind a key to the `cancel` command (default <C-e>) when using this option,
-- which will both undo the selection and hide the completion menu
auto_insert = true,
-- auto_insert = function(ctx) return ctx.mode ~= 'cmdline' end
},

cycle = {
-- When `true`, calling `select_next` at the *bottom* of the completion list
-- will select the *first* completion item.
Expand Down
7 changes: 4 additions & 3 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ signature = { window = { border = 'single' } },
```lua
completion = {
list = {
selection = function(ctx)
return ctx.mode == 'cmdline' and 'auto_insert' or 'preselect'
end
selection = {
preselect = function(ctx) return ctx.mode ~= 'cmdline' end,
auto_insert = function(ctx) return ctx.mode ~= 'cmdline' end
}
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion lua/blink/cmp/completion/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function completion.setup()
-- run 'resolve' on the item ahead of time to avoid delays
-- when accepting the item or showing documentation
list.select_emitter:on(function(event)
-- when selection == 'manual' | 'auto_insert', we still want to prefetch the first item
-- when selection.preselect == false, we still want to prefetch the first item
local item = event.item or list.items[1]
if item == nil then return end
require('blink.cmp.completion.prefetch')(event.context, event.item)
Expand Down
83 changes: 47 additions & 36 deletions lua/blink/cmp/completion/list.lua
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
--- Manages most of the state for the completion list such that downstream consumers can be mostly stateless
--- @class (exact) blink.cmp.CompletionList
--- @field config blink.cmp.CompletionListConfig
--- @field context? blink.cmp.Context
--- @field items blink.cmp.CompletionItem[]
--- @field selected_item_idx? number
--- @field preview_undo? { text_edit: lsp.TextEdit, cursor: integer[]?}
--- @field show_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListShowEvent>
--- @field hide_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListHideEvent>
--- @field select_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListSelectEvent>
--- @field accept_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListAcceptEvent>
---
--- @field context? blink.cmp.Context
--- @field items blink.cmp.CompletionItem[]
--- @field selected_item_idx? number
--- @field preview_undo? { text_edit: lsp.TextEdit, cursor: integer[]?}
---
--- @field show fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>)
--- @field fuzzy fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>): blink.cmp.CompletionItem[]
--- @field hide fun()
---
--- @field get_selected_item fun(): blink.cmp.CompletionItem?
--- @field get_selection_mode fun(context: blink.cmp.Context): blink.cmp.CompletionListSelection
--- @field select fun(idx?: number, opts?: { undo_preview?: boolean, is_explicit_selection?: boolean })
--- @field select_next fun()
--- @field select_prev fun()
--- @field get_item_idx_in_list fun(item?: blink.cmp.CompletionItem): number
--- @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 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 auto_insert? boolean When `true`, inserts the completion item automatically when selecting it

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

Expand Down Expand Up @@ -58,7 +62,6 @@ local list = {
config = require('blink.cmp.config').completion.list,
context = nil,
items = {},
selection_mode = nil,
is_explicitly_selected = false,
preview_undo = nil,
}
Expand All @@ -84,7 +87,6 @@ function list.show(context, items_by_source)
-- update the context/list and emit
list.context = context
list.items = list.fuzzy(context, items_by_source)
list.selection_mode = list.get_selection_mode(list.context)

if #list.items == 0 then
list.hide_emitter:emit({ context = context })
Expand All @@ -95,13 +97,13 @@ function list.show(context, items_by_source)
-- maintain the selection if the user selected an item
local previous_item_idx = list.get_item_idx_in_list(previous_selected_item)
if list.is_explicitly_selected and previous_item_idx ~= nil and previous_item_idx <= 10 then
list.select(previous_item_idx, { undo_preview = false })
list.select(previous_item_idx, { auto_insert = false, undo_preview = false })

-- otherwise, use the default selection
else
list.select(
list.selection_mode == 'preselect' and 1 or nil,
{ undo_preview = false, is_explicit_selection = false }
list.get_selection_mode(list.context).preselect and 1 or nil,
{ auto_insert = false, undo_preview = false, is_explicit_selection = false }
)
end
end
Expand Down Expand Up @@ -130,19 +132,33 @@ function list.get_selected_item() return list.items[list.selected_item_idx] end

function list.get_selection_mode(context)
assert(context ~= nil, 'Context must be set before getting selection mode')
if type(list.config.selection) == 'function' then return list.config.selection(context) end
--- @diagnostic disable-next-line: return-type-mismatch
return list.config.selection

local preselect = list.config.selection.preselect
if type(preselect) == 'function' then preselect = preselect(context) end
--- @cast preselect boolean

local auto_insert = list.config.selection.auto_insert
if type(auto_insert) == 'function' then auto_insert = auto_insert(context) end
--- @cast auto_insert boolean

return { preselect = preselect, auto_insert = auto_insert }
end

function list.get_item_idx_in_list(item)
if item == nil then return end
return require('blink.cmp.lib.utils').find_idx(list.items, function(i) return i.label == item.label end)
end

function list.select(idx, opts)
opts = opts or {}
local item = list.items[idx]

local auto_insert = opts.auto_insert
if auto_insert == nil then auto_insert = list.get_selection_mode(list.context).auto_insert end

require('blink.cmp.completion.trigger').suppress_events_for_callback(function()
-- default to undoing the preview
if opts.undo_preview ~= false then list.undo_preview() end
if list.selection_mode == 'auto_insert' and item then list.apply_preview(item) end
if auto_insert and item ~= nil then list.apply_preview(item) end
end)

--- @diagnostic disable-next-line: assign-type-mismatch
Expand All @@ -151,53 +167,48 @@ function list.select(idx, opts)
list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context })
end

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

-- haven't selected anything yet, select the first item
if list.selected_item_idx == nil then return list.select(1) end
if list.selected_item_idx == nil then return list.select(1, opts) end

-- end of the list
if list.selected_item_idx == #list.items then
-- cycling around has been disabled, ignore
if not list.config.cycle.from_bottom then return end

-- preselect is not enabled, we go back to no selection
if list.selection_mode ~= 'preselect' then return list.select(nil) end
if not list.get_selection_mode(list.context).preselect then return list.select(nil, opts) end

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

-- typical case, select the next item
list.select(list.selected_item_idx + 1)
list.select(list.selected_item_idx + 1, opts)
end

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

-- haven't selected anything yet, select the last item
if list.selected_item_idx == nil then return list.select(#list.items) end
if list.selected_item_idx == nil then return list.select(#list.items, opts) end

-- start of the list
if list.selected_item_idx == 1 then
-- cycling around has been disabled, ignore
if not list.config.cycle.from_top then return end

-- auto_insert is enabled, we go back to no selection
if list.selection_mode == 'auto_insert' then return list.select(nil) end
if list.get_selection_mode(list.context).auto_insert then return list.select(nil, opts) end

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

-- typical case, select the previous item
list.select(list.selected_item_idx - 1)
end

function list.get_item_idx_in_list(item)
if item == nil then return end
return require('blink.cmp.lib.utils').find_idx(list.items, function(i) return i.label == item.label end)
list.select(list.selected_item_idx - 1, opts)
end

---------- Preview ----------
Expand Down
Loading

0 comments on commit 8126d0e

Please sign in to comment.