feat(lsp): vim.lsp.inline_completion on_accept #35507

This commit is contained in:
Yi Ming
2025-09-01 23:46:29 +08:00
committed by GitHub
parent 06df337617
commit 6888f65be1
3 changed files with 107 additions and 27 deletions

View File

@@ -2229,6 +2229,16 @@ is_enabled({filter}) *vim.lsp.inlay_hint.is_enabled()*
============================================================================== ==============================================================================
Lua module: vim.lsp.inline_completion *lsp-inline_completion* 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()* enable({enable}, {filter}) *vim.lsp.inline_completion.enable()*
Enables or disables inline completion for the {filter}ed scope, inline Enables or disables inline completion for the {filter}ed scope, inline
completion will automatically be refreshed when you are in insert mode. 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. for all.
get({opts}) *vim.lsp.inline_completion.get()* 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 return value to implement a fallback: >lua
vim.keymap.set('i', '<Tab>', function() vim.keymap.set('i', '<Tab>', function()
if not vim.lsp.inline_completion.get() then 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: • {opts} (`table?`) A table with the following fields:
• {bufnr}? (`integer`, default: 0) Buffer handle, or 0 for • {bufnr}? (`integer`, default: 0) Buffer handle, or 0 for
current. 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: ~ Return: ~
(`boolean`) `true` if a completion was applied, else `false`. (`boolean`) `true` if a completion was applied, else `false`.

View File

@@ -11,11 +11,11 @@ local M = {}
local namespace = api.nvim_create_namespace('nvim.lsp.inline_completion') local namespace = api.nvim_create_namespace('nvim.lsp.inline_completion')
---@class (private) vim.lsp.inline_completion.CurrentItem ---@class vim.lsp.inline_completion.Item
---@field index integer The index among all items form all clients. ---@field _index integer The index among all items form all clients.
---@field client_id integer Client ID ---@field client_id integer Client ID
---@field insert_text string|lsp.StringValue The text to be inserted, can be a snippet. ---@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 range? vim.Range Which range it be applied.
---@field command? lsp.Command Corresponding server command. ---@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 ---@class (private) vim.lsp.inline_completion.Completor : vim.lsp.Capability
---@field active table<integer, vim.lsp.inline_completion.Completor?> ---@field active table<integer, vim.lsp.inline_completion.Completor?>
---@field timer? uv.uv_timer_t Timer for debouncing automatic requests ---@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> ---@field client_state table<integer, vim.lsp.inline_completion.ClientState>
local Completor = { local Completor = {
name = 'inline_completion', 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 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) local range = item.range and vim.range.lsp(self.bufnr, item.range, client.offset_encoding)
self.current = { self.current = {
index = index, _index = index,
client_id = client_id, client_id = client_id,
insert_text = item.insertText, insert_text = item.insertText,
range = range, range = range,
filter_text = item.filterText, _filter_text = item.filterText,
command = item.command, command = item.command,
} }
@@ -281,19 +281,14 @@ function Completor:abort()
self.current = nil self.current = nil
end end
--- Apply the current completion item to the buffer. --- Accept the current completion item to the buffer.
--- ---
---@package ---@package
function Completor:apply() ---@param item vim.lsp.inline_completion.Item
local current = self.current function Completor:accept(item)
self:abort() local insert_text = item.insert_text
if not current then
return
end
local insert_text = current.insert_text
if type(insert_text) == 'string' then if type(insert_text) == 'string' then
local range = current.range local range = item.range
if range then if range then
local lines = vim.split(insert_text, '\n') local lines = vim.split(insert_text, '\n')
api.nvim_buf_set_text( api.nvim_buf_set_text(
@@ -304,7 +299,7 @@ function Completor:apply()
range.end_.col, range.end_.col,
lines lines
) )
local pos = range.start:to_cursor() local pos = item.range.start:to_cursor()
api.nvim_win_set_cursor(vim.fn.bufwinid(self.bufnr), { api.nvim_win_set_cursor(vim.fn.bufwinid(self.bufnr), {
pos[1] + #lines - 1, pos[1] + #lines - 1,
(#lines == 1 and pos[2] or 0) + #lines[#lines], (#lines == 1 and pos[2] or 0) + #lines[#lines],
@@ -317,9 +312,9 @@ function Completor:apply()
end end
-- Execute the command *after* inserting this completion. -- Execute the command *after* inserting this completion.
if current.command then if item.command then
local client = assert(vim.lsp.get_client_by_id(current.client_id)) local client = assert(vim.lsp.get_client_by_id(item.client_id))
client:exec_cmd(current.command, { bufnr = self.bufnr }) client:exec_cmd(item.command, { bufnr = self.bufnr })
end end
end end
@@ -381,7 +376,7 @@ function M.select(opts)
end end
local n = completor:count_items() local n = completor:count_items()
local index = current.index + count local index = current._index + count
if wrap then if wrap then
index = (index - 1) % n + 1 index = (index - 1) % n + 1
else else
@@ -396,10 +391,15 @@ end
--- Buffer handle, or 0 for current. --- Buffer handle, or 0 for current.
--- (default: 0) --- (default: 0)
---@field bufnr? integer ---@field bufnr? integer
--- Apply the currently displayed completion candidate to the buffer.
--- ---
--- It returns false when no candidate can be applied, --- 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)
--- Accept the currently displayed completion candidate to the buffer.
---
--- It returns false when no candidate can be accepted,
--- so you can use the return value to implement a fallback: --- so you can use the return value to implement a fallback:
--- ---
--- ```lua --- ```lua
@@ -420,11 +420,23 @@ function M.get(opts)
opts = opts or {} opts = opts or {}
local bufnr = vim._resolve_bufnr(opts.bufnr) local bufnr = vim._resolve_bufnr(opts.bufnr)
local on_accept = opts.on_accept
local completor = Completor.active[bufnr] local completor = Completor.active[bufnr]
if completor and completor.current then if completor and completor.current then
-- Schedule apply to allow `get()` can be mapped with `<expr>`. -- Schedule apply to allow `get()` can be mapped with `<expr>`.
vim.schedule(function() 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) end)
return true return true
end end

View File

@@ -4,6 +4,7 @@ local t_lsp = require('test.functional.plugin.lsp.testutil')
local Screen = require('test.functional.ui.screen') local Screen = require('test.functional.ui.screen')
local dedent = t.dedent local dedent = t.dedent
local eq = t.eq
local api = n.api local api = n.api
local exec_lua = n.exec_lua local exec_lua = n.exec_lua
@@ -183,6 +184,59 @@ describe('vim.lsp.inline_completion', function()
feed('<Esc>') feed('<Esc>')
screen:expect({ grid = grid_applied_candidates }) screen:expect({ grid = grid_applied_candidates })
end) 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) end)
describe('select()', function() describe('select()', function()