diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index f2b6693c21..ac77b3bf75 100644 --- 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', '', 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 index 1d9be0bcfa..64bf2a1cb8 100644 --- 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 ---@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 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 - ---- 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: --- --- ```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 ``. 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 index 7245181f2d..bbf0ec2102 100644 --- 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('') 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('') + 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()