commit 6888f65be1772f34a35a324c7f3d817e486fc0ab
parent 06df3376170f407b40af8b73b872da7b0f2a98f0
Author: Yi Ming <ofseed@foxmail.com>
Date: Mon, 1 Sep 2025 23:46:29 +0800
feat(lsp): vim.lsp.inline_completion on_accept #35507
Diffstat:
3 files changed, 106 insertions(+), 26 deletions(-)
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
@@ -2229,6 +2229,16 @@ is_enabled({filter}) *vim.lsp.inlay_hint.is_enabled()*
==============================================================================
Lua module: vim.lsp.inline_completion *lsp-inline_completion*
+*vim.lsp.inline_completion.Item*
+
+ Fields: ~
+ • {client_id} (`integer`) Client ID
+ • {insert_text} (`string|lsp.StringValue`) The text to be inserted, can
+ be a snippet.
+ • {range}? (`vim.Range`) Which range it be applied.
+ • {command}? (`lsp.Command`) Corresponding server command.
+
+
enable({enable}, {filter}) *vim.lsp.inline_completion.enable()*
Enables or disables inline completion for the {filter}ed scope, inline
completion will automatically be refreshed when you are in insert mode.
@@ -2246,9 +2256,9 @@ enable({enable}, {filter}) *vim.lsp.inline_completion.enable()*
for all.
get({opts}) *vim.lsp.inline_completion.get()*
- Apply the currently displayed completion candidate to the buffer.
+ Accept the currently displayed completion candidate to the buffer.
- It returns false when no candidate can be applied, so you can use the
+ It returns false when no candidate can be accepted, so you can use the
return value to implement a fallback: >lua
vim.keymap.set('i', '<Tab>', function()
if not vim.lsp.inline_completion.get() then
@@ -2265,6 +2275,10 @@ get({opts}) *vim.lsp.inline_completion.get()*
• {opts} (`table?`) A table with the following fields:
• {bufnr}? (`integer`, default: 0) Buffer handle, or 0 for
current.
+ • {on_accept}? (`fun(item: vim.lsp.inline_completion.Item)`)
+ Accept handler, called with the accepted item. If not
+ provided, the default handler is used, which applies changes
+ to the buffer based on the completion item.
Return: ~
(`boolean`) `true` if a completion was applied, else `false`.
diff --git a/runtime/lua/vim/lsp/inline_completion.lua b/runtime/lua/vim/lsp/inline_completion.lua
@@ -11,11 +11,11 @@ local M = {}
local namespace = api.nvim_create_namespace('nvim.lsp.inline_completion')
----@class (private) vim.lsp.inline_completion.CurrentItem
----@field index integer The index among all items form all clients.
+---@class vim.lsp.inline_completion.Item
+---@field _index integer The index among all items form all clients.
---@field client_id integer Client ID
---@field insert_text string|lsp.StringValue The text to be inserted, can be a snippet.
----@field filter_text? string
+---@field _filter_text? string
---@field range? vim.Range Which range it be applied.
---@field command? lsp.Command Corresponding server command.
@@ -25,7 +25,7 @@ local namespace = api.nvim_create_namespace('nvim.lsp.inline_completion')
---@class (private) vim.lsp.inline_completion.Completor : vim.lsp.Capability
---@field active table<integer, vim.lsp.inline_completion.Completor?>
---@field timer? uv.uv_timer_t Timer for debouncing automatic requests
----@field current? vim.lsp.inline_completion.CurrentItem Currently selected item
+---@field current? vim.lsp.inline_completion.Item Currently selected item
---@field client_state table<integer, vim.lsp.inline_completion.ClientState>
local Completor = {
name = 'inline_completion',
@@ -146,11 +146,11 @@ function Completor:select(index, show_index)
local client = assert(vim.lsp.get_client_by_id(client_id))
local range = item.range and vim.range.lsp(self.bufnr, item.range, client.offset_encoding)
self.current = {
- index = index,
+ _index = index,
client_id = client_id,
insert_text = item.insertText,
range = range,
- filter_text = item.filterText,
+ _filter_text = item.filterText,
command = item.command,
}
@@ -281,19 +281,14 @@ function Completor:abort()
self.current = nil
end
---- Apply the current completion item to the buffer.
+--- Accept the current completion item to the buffer.
---
---@package
-function Completor:apply()
- local current = self.current
- self:abort()
- if not current then
- return
- end
-
- local insert_text = current.insert_text
+---@param item vim.lsp.inline_completion.Item
+function Completor:accept(item)
+ local insert_text = item.insert_text
if type(insert_text) == 'string' then
- local range = current.range
+ local range = item.range
if range then
local lines = vim.split(insert_text, '\n')
api.nvim_buf_set_text(
@@ -304,7 +299,7 @@ function Completor:apply()
range.end_.col,
lines
)
- local pos = range.start:to_cursor()
+ local pos = item.range.start:to_cursor()
api.nvim_win_set_cursor(vim.fn.bufwinid(self.bufnr), {
pos[1] + #lines - 1,
(#lines == 1 and pos[2] or 0) + #lines[#lines],
@@ -317,9 +312,9 @@ function Completor:apply()
end
-- Execute the command *after* inserting this completion.
- if current.command then
- local client = assert(vim.lsp.get_client_by_id(current.client_id))
- client:exec_cmd(current.command, { bufnr = self.bufnr })
+ if item.command then
+ local client = assert(vim.lsp.get_client_by_id(item.client_id))
+ client:exec_cmd(item.command, { bufnr = self.bufnr })
end
end
@@ -381,7 +376,7 @@ function M.select(opts)
end
local n = completor:count_items()
- local index = current.index + count
+ local index = current._index + count
if wrap then
index = (index - 1) % n + 1
else
@@ -396,10 +391,15 @@ end
--- Buffer handle, or 0 for current.
--- (default: 0)
---@field bufnr? integer
+---
+--- Accept handler, called with the accepted item.
+--- If not provided, the default handler is used,
+--- which applies changes to the buffer based on the completion item.
+---@field on_accept? fun(item: vim.lsp.inline_completion.Item)
---- Apply the currently displayed completion candidate to the buffer.
+--- Accept the currently displayed completion candidate to the buffer.
---
---- It returns false when no candidate can be applied,
+--- It returns false when no candidate can be accepted,
--- so you can use the return value to implement a fallback:
---
--- ```lua
@@ -420,11 +420,23 @@ function M.get(opts)
opts = opts or {}
local bufnr = vim._resolve_bufnr(opts.bufnr)
+ local on_accept = opts.on_accept
+
local completor = Completor.active[bufnr]
if completor and completor.current then
-- Schedule apply to allow `get()` can be mapped with `<expr>`.
vim.schedule(function()
- completor:apply()
+ local item = completor.current
+ completor:abort()
+ if not item then
+ return
+ end
+
+ if on_accept then
+ on_accept(item)
+ else
+ completor:accept(item)
+ end
end)
return true
end
diff --git a/test/functional/plugin/lsp/inline_completion_spec.lua b/test/functional/plugin/lsp/inline_completion_spec.lua
@@ -4,6 +4,7 @@ local t_lsp = require('test.functional.plugin.lsp.testutil')
local Screen = require('test.functional.ui.screen')
local dedent = t.dedent
+local eq = t.eq
local api = n.api
local exec_lua = n.exec_lua
@@ -183,6 +184,59 @@ describe('vim.lsp.inline_completion', function()
feed('<Esc>')
screen:expect({ grid = grid_applied_candidates })
end)
+
+ it('accepts on_accept callback', function()
+ feed('i')
+ screen:expect({ grid = grid_with_candidates })
+ local result = exec_lua(function()
+ ---@type vim.lsp.inline_completion.Item
+ local result
+ vim.lsp.inline_completion.get({
+ on_accept = function(item)
+ result = item
+ end,
+ })
+ vim.wait(1000, function()
+ return result ~= nil
+ end) -- Wait for async callback.
+ return result
+ end)
+ feed('<Esc>')
+ screen:expect({ grid = grid_without_candidates })
+ eq({
+ _index = 1,
+ client_id = 1,
+ command = {
+ command = 'dummy',
+ title = 'Completion Accepted',
+ },
+ insert_text = dedent([[
+ function fibonacci(n) {
+ if (n <= 0) return 0;
+ if (n === 1) return 1;
+
+ let a = 0, b = 1, c;
+ for (let i = 2; i <= n; i++) {
+ c = a + b;
+ a = b;
+ b = c;
+ }
+ return b;
+ }]]),
+ range = {
+ end_ = {
+ buf = 1,
+ col = 20,
+ row = 0,
+ },
+ start = {
+ buf = 1,
+ col = 0,
+ row = 0,
+ },
+ },
+ }, result)
+ end)
end)
describe('select()', function()