diff --git a/runtime/colors/vim.lua b/runtime/colors/vim.lua index 6e9e831955..510059a30c 100644 --- a/runtime/colors/vim.lua +++ b/runtime/colors/vim.lua @@ -62,6 +62,8 @@ hi('PmenuMatchSel', { link = 'PmenuSel' }) hi('PmenuExtra', { link = 'Pmenu' }) hi('PmenuExtraSel', { link = 'PmenuSel' }) hi('ComplMatchIns', {}) +hi('ComplHint', { link = 'NonText' }) +hi('ComplHintMore', { link = 'MoreMsg' }) hi('Substitute', { link = 'Search' }) hi('Whitespace', { link = 'NonText' }) hi('MsgSeparator', { link = 'StatusLine' }) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 3c902819a9..c69cb1515e 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -332,6 +332,7 @@ They are also listed below. - `'textDocument/formatting'` - `'textDocument/hover'` - `'textDocument/inlayHint'` +- `'textDocument/inlineCompletion'` - `'textDocument/publishDiagnostics'` - `'textDocument/rangeFormatting'` - `'textDocument/rename'` @@ -2219,6 +2220,73 @@ is_enabled({filter}) *vim.lsp.inlay_hint.is_enabled()* (`boolean`) +============================================================================== +Lua module: vim.lsp.inline_completion *lsp-inline_completion* + +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. + + To "toggle", pass the inverse of `is_enabled()`: >lua + vim.lsp.inline_completion.enable(not vim.lsp.inline_completion.is_enabled()) +< + + Parameters: ~ + • {enable} (`boolean?`) true/nil to enable, false to disable + • {filter} (`table?`) Optional filters |kwargs|, + • {bufnr}? (`integer`, default: all) Buffer number, or 0 for + current buffer, or nil for all. + • {client_id}? (`integer`, default: all) Client ID, or nil + for all. + +get({opts}) *vim.lsp.inline_completion.get()* + Apply the currently displayed completion candidate to the buffer. + + It returns false when no candidate can be applied, 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 + return '' + end + end, { + expr = true, + replace_keycodes = true, + desc = 'Get the current inline completion', + }) +< + + Parameters: ~ + • {opts} (`table?`) A table with the following fields: + • {bufnr}? (`integer`, default: 0) Buffer handle, or 0 for + current. + + Return: ~ + (`boolean`) `true` if a completion was applied, else `false`. + +is_enabled({filter}) *vim.lsp.inline_completion.is_enabled()* + Query whether inline completion is enabled in the {filter}ed scope + + Parameters: ~ + • {filter} (`table?`) Optional filters |kwargs|, + • {bufnr}? (`integer`, default: all) Buffer number, or 0 for + current buffer, or nil for all. + • {client_id}? (`integer`, default: all) Client ID, or nil + for all. + +select({opts}) *vim.lsp.inline_completion.select()* + Switch between available inline completion candidates. + + Parameters: ~ + • {opts} (`table?`) A table with the following fields: + • {bufnr}? (`integer`) (default: current buffer) + • {count}? (`integer`, default: v:count1) The number of + candidates to move by. A positive integer moves forward by + {count} candidates, while a negative integer moves backward + by {count} candidates. + • {wrap}? (`boolean`, default: `true`) Whether to loop around + file or not. Similar to 'wrapscan'. + + ============================================================================== Lua module: vim.lsp.linked_editing_range *lsp-linked_editing_range* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 76a31c6baa..13774a8da9 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -231,6 +231,8 @@ LSP • Support for related documents in pull diagnostics: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#relatedFullDocumentDiagnosticReport • |vim.lsp.buf.signature_help()| supports "noActiveParameterSupport". +• Support for `textDocument/inlineCompletion` |lsp-inline_completion| + https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_inlineCompletion LUA diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt index 7fa97f1d66..658dba3ecb 100644 --- a/runtime/doc/syntax.txt +++ b/runtime/doc/syntax.txt @@ -5350,6 +5350,10 @@ PmenuMatchSel Popup menu: Matched text in selected item. Combined with |hl-PmenuMatch| and |hl-PmenuSel|. *hl-ComplMatchIns* ComplMatchIns Matched text of the currently inserted completion. + *hl-ComplHint* +ComplHint Virtual text of the currently selected completion. + *hl-ComplHintMore* +ComplHintMore The additional information of the virtual text. *hl-Question* Question |hit-enter| prompt and yes/no questions. *hl-QuickFixLine* diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index d6943ae36d..74280efc48 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -16,6 +16,7 @@ local lsp = vim._defer_require('vim.lsp', { document_color = ..., --- @module 'vim.lsp.document_color' handlers = ..., --- @module 'vim.lsp.handlers' inlay_hint = ..., --- @module 'vim.lsp.inlay_hint' + inline_completion = ..., --- @module 'vim.lsp.inline_completion' linked_editing_range = ..., --- @module 'vim.lsp.linked_editing_range' log = ..., --- @module 'vim.lsp.log' protocol = ..., --- @module 'vim.lsp.protocol' diff --git a/runtime/lua/vim/lsp/_capability.lua b/runtime/lua/vim/lsp/_capability.lua index e3aeac8d43..243ed6f24f 100644 --- a/runtime/lua/vim/lsp/_capability.lua +++ b/runtime/lua/vim/lsp/_capability.lua @@ -4,6 +4,7 @@ local api = vim.api ---| 'semantic_tokens' ---| 'folding_range' ---| 'linked_editing_range' +---| 'inline_completion' --- Tracks all supported capabilities, all of which derive from `vim.lsp.Capability`. --- Returns capability *prototypes*, not their instances. diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 75ac0751bd..c23115fe18 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -514,6 +514,7 @@ function Client:initialize() -- HACK: Capability modules must be loaded require('vim.lsp.semantic_tokens') require('vim.lsp._folding_range') + require('vim.lsp.inline_completion') local init_params = { -- The process Id of the parent process that started the server. Is null if @@ -607,6 +608,7 @@ local static_registration_capabilities = { [ms.textDocument_foldingRange] = 'foldingRangeProvider', [ms.textDocument_implementation] = 'implementationProvider', [ms.textDocument_inlayHint] = 'inlayHintProvider', + [ms.textDocument_inlineCompletion] = 'inlineCompletionProvider', [ms.textDocument_inlineValue] = 'inlineValueProvider', [ms.textDocument_linkedEditingRange] = 'linkedEditingRangeProvider', [ms.textDocument_moniker] = 'monikerProvider', diff --git a/runtime/lua/vim/lsp/inline_completion.lua b/runtime/lua/vim/lsp/inline_completion.lua new file mode 100644 index 0000000000..58d12bfd27 --- /dev/null +++ b/runtime/lua/vim/lsp/inline_completion.lua @@ -0,0 +1,435 @@ +local util = require('vim.lsp.util') +local log = require('vim.lsp.log') +local protocol = require('vim.lsp.protocol') +local ms = require('vim.lsp.protocol').Methods +local grammar = require('vim.lsp._snippet_grammar') +local api = vim.api + +local Capability = require('vim.lsp._capability') + +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. +---@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 range? vim.Range Which range it be applied. +---@field command? lsp.Command Corresponding server command. + +---@class (private) vim.lsp.inline_completion.ClientState +---@field items? lsp.InlineCompletionItem[] + +---@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 client_state table +local Completor = { + name = 'inline_completion', + method = ms.textDocument_inlineCompletion, + active = {}, +} +Completor.__index = Completor +setmetatable(Completor, Capability) +Capability.all[Completor.name] = Completor + +---@package +---@param bufnr integer +---@return vim.lsp.inline_completion.Completor +function Completor:new(bufnr) + self = Capability.new(self, bufnr) + self.client_state = {} + api.nvim_create_autocmd({ 'InsertEnter', 'CursorMovedI', 'CursorHoldI' }, { + group = self.augroup, + callback = function() + self:automatic_request() + end, + }) + api.nvim_create_autocmd({ 'InsertLeave' }, { + group = self.augroup, + callback = function() + self:abort() + end, + }) + return self +end + +---@package +function Completor:destroy() + api.nvim_buf_clear_namespace(self.bufnr, namespace, 0, -1) + api.nvim_del_augroup_by_id(self.augroup) + self.active[self.bufnr] = nil +end + +--- Longest common prefix +--- +---@param a string +---@param b string +---@return integer index where the common prefix ends, exclusive +local function lcp(a, b) + local i, la, lb = 1, #a, #b + while i <= la and i <= lb and a:sub(i, i) == b:sub(i, i) do + i = i + 1 + end + return i +end + +--- `lsp.Handler` for `textDocument/inlineCompletion`. +--- +---@package +---@param err? lsp.ResponseError +---@param result? lsp.InlineCompletionItem[]|lsp.InlineCompletionList +---@param ctx lsp.HandlerContext +function Completor:handler(err, result, ctx) + if err then + log.error('inlinecompletion', err) + return + end + if not result then + return + end + + local items = result.items or result + self.client_state[ctx.client_id].items = items + self:select(1) +end + +---@package +function Completor:count_items() + local n = 0 + for _, state in pairs(self.client_state) do + local items = state.items + if items then + n = n + #items + end + end + return n +end + +---@package +---@param i integer +---@return integer?, lsp.InlineCompletionItem? +function Completor:get_item(i) + local n = self:count_items() + i = i % (n + 1) + ---@type integer[] + local client_ids = vim.tbl_keys(self.client_state) + table.sort(client_ids) + for _, client_id in ipairs(client_ids) do + local items = self.client_state[client_id].items + if items then + if i > #items then + i = i - #items + else + return client_id, items[i] + end + end + end +end + +--- Select the {index}-th completion item. +--- +---@package +---@param index integer +---@param show_index? boolean +function Completor:select(index, show_index) + self.current = nil + local client_id, item = self:get_item(index) + if not client_id or not item then + self:hide() + return + end + + 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, + client_id = client_id, + insert_text = item.insertText, + range = range, + filter_text = item.filterText, + command = item.command, + } + + local hint = show_index and (' (%d/%d)'):format(index, self:count_items()) or nil + self:show(hint) +end + +--- Show or update the current completion item. +--- +---@package +---@param hint? string +function Completor:show(hint) + self:hide() + local current = self.current + if not current then + return + end + + local insert_text = current.insert_text + local text = type(insert_text) == 'string' and insert_text + or tostring(grammar.parse(insert_text.value)) + local lines = {} ---@type [string, string][][] + for s in vim.gsplit(text, '\n', { plain = true }) do + table.insert(lines, { { s, 'ComplHint' } }) + end + if hint then + table.insert(lines[#lines], { hint, 'ComplHintMore' }) + end + + -- The first line of the text to be inserted + -- usually contains characters entered by the user, + -- which should be skipped before displaying the virtual text. + local pos = current.range and current.range.start:to_extmark() + or vim.pos.cursor(api.nvim_win_get_cursor(vim.fn.bufwinid(self.bufnr))):to_extmark() + local row, col = unpack(pos) + local virt_text = lines[1] + local skip = + lcp(api.nvim_buf_get_lines(self.bufnr, row, row + 1, true)[1]:sub(col + 1), virt_text[1][1]) + local winid = api.nvim_get_current_win() + -- At least, characters before the cursor should be skipped. + if api.nvim_win_get_buf(winid) == self.bufnr then + local cursor_row, cursor_col = + unpack(vim.pos.cursor(api.nvim_win_get_cursor(winid)):to_extmark()) + if row == cursor_row then + skip = math.max(skip, cursor_col - col + 1) + end + end + virt_text[1][1] = virt_text[1][1]:sub(skip) + col = col + skip - 1 + + local virt_lines = { unpack(lines, 2) } + api.nvim_buf_set_extmark(self.bufnr, namespace, row, col, { + virt_text = virt_text, + virt_lines = virt_lines, + virt_text_pos = current.range and 'overlay' or 'inline', + hl_mode = 'combine', + }) +end + +--- Hide the current completion item. +--- +---@package +function Completor:hide() + api.nvim_buf_clear_namespace(self.bufnr, namespace, 0, -1) +end + +---@package +---@param kind lsp.InlineCompletionTriggerKind +function Completor:request(kind) + for client_id in pairs(self.client_state) do + local client = assert(vim.lsp.get_client_by_id(client_id)) + ---@type lsp.InlineCompletionContext + local context = { triggerKind = kind } + if + kind == protocol.InlineCompletionTriggerKind.Invoked and api.nvim_get_mode().mode:match('^v') + then + context.selectedCompletionInfo = { + range = util.make_given_range_params(nil, nil, self.bufnr, client.offset_encoding).range, + text = table.concat(vim.fn.getregion(vim.fn.getpos("'<"), vim.fn.getpos("'>")), '\n'), + } + end + + ---@type lsp.InlineCompletionParams + local params = { + textDocument = util.make_text_document_params(self.bufnr), + position = util.make_position_params(0, client.offset_encoding).position, + context = context, + } + client:request(ms.textDocument_inlineCompletion, params, function(...) + self:handler(...) + end) + end +end + +---@private +function Completor:reset_timer() + local timer = self.timer + if timer then + self.timer = nil + if not timer:is_closing() then + timer:stop() + timer:close() + end + end +end + +--- Automatically request with debouncing, used as callbacks in autocmd events. +--- +---@package +function Completor:automatic_request() + self:show() + self:reset_timer() + self.timer = vim.defer_fn(function() + self:request(protocol.InlineCompletionTriggerKind.Automatic) + end, 200) +end + +--- Abort the current completion item and pending requests. +--- +---@package +function Completor:abort() + util._cancel_requests({ + bufnr = self.bufnr, + method = ms.textDocument_inlineCompletion, + type = 'pending', + }) + self:hide() + self.current = nil +end + +--- Apply 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 + if type(insert_text) == 'string' then + local range = current.range + if range then + local lines = vim.split(insert_text, '\n') + api.nvim_buf_set_text( + self.bufnr, + range.start.row, + range.start.col, + range.end_.row, + range.end_.col, + lines + ) + local pos = current.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], + }) + else + api.nvim_paste(insert_text, false, 0) + end + elseif insert_text.kind == 'snippet' then + vim.snippet.expand(insert_text.value) + 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 }) + end +end + +--- Query whether inline completion is enabled in the {filter}ed scope +---@param filter? vim.lsp.capability.enable.Filter +function M.is_enabled(filter) + return vim.lsp._capability.is_enabled('inline_completion', filter) +end + +--- Enables or disables inline completion for the {filter}ed scope, +--- inline completion will automatically be refreshed when you are in insert mode. +--- +--- To "toggle", pass the inverse of `is_enabled()`: +--- +--- ```lua +--- vim.lsp.inline_completion.enable(not vim.lsp.inline_completion.is_enabled()) +--- ``` +--- +---@param enable? boolean true/nil to enable, false to disable +---@param filter? vim.lsp.capability.enable.Filter +function M.enable(enable, filter) + vim.lsp._capability.enable('inline_completion', enable, filter) +end + +---@class vim.lsp.inline_completion.select.Opts +---@inlinedoc +--- +--- (default: current buffer) +---@field bufnr? integer +--- +--- The number of candidates to move by. +--- A positive integer moves forward by {count} candidates, +--- while a negative integer moves backward by {count} candidates. +--- (default: v:count1) +---@field count? integer +--- +--- Whether to loop around file or not. Similar to 'wrapscan'. +--- (default: `true`) +---@field wrap? boolean + +--- Switch between available inline completion candidates. +--- +---@param opts? vim.lsp.inline_completion.select.Opts +function M.select(opts) + vim.validate('opts', opts, 'table', true) + opts = opts or {} + local bufnr = vim._resolve_bufnr(opts.bufnr) + local completor = Completor.active[bufnr] + if not completor then + return + end + + local count = opts.count or vim.v.count1 + local wrap = opts.wrap or true + + local current = completor.current + if not current then + return + end + + local n = completor:count_items() + local index = current.index + count + if wrap then + index = (index - 1) % n + 1 + else + index = math.max(1, math.min(index, n)) + end + completor:select(index, true) +end + +---@class vim.lsp.inline_completion.get.Opts +---@inlinedoc +--- +--- 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, +--- 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 +--- return '' +--- end +--- end, { +--- expr = true, +--- replace_keycodes = true, +--- desc = 'Get the current inline completion', +--- }) +--- ```` +---@param opts? vim.lsp.inline_completion.get.Opts +---@return boolean `true` if a completion was applied, else `false`. +function M.get(opts) + vim.validate('opts', opts, 'table', true) + opts = opts or {} + + local bufnr = vim._resolve_bufnr(opts.bufnr) + 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() + end) + return true + end + + return false +end + +return M diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 30b993c1cc..f724d6fc66 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -312,6 +312,14 @@ local constants = { -- also be triggered when file content changes. Automatic = 2, }, + InlineCompletionTriggerKind = { + -- Completion was triggered explicitly by a user gesture. + -- Return multiple completion items to enable cycling through them. + Invoked = 1, + -- Completion was triggered automatically while editing. + -- It is sufficient to return a single completion item in this case. + Automatic = 2, + }, } --- Protocol for the Microsoft Language Server Protocol (mslsp) @@ -503,6 +511,9 @@ function protocol.make_client_capabilities() implementation = { linkSupport = true, }, + inlineCompletion = { + dynamicRegistration = false, + }, typeDefinition = { linkSupport = true, }, diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua index c2692793ca..858eb4beab 100755 --- a/src/gen/gen_vimdoc.lua +++ b/src/gen/gen_vimdoc.lua @@ -283,6 +283,7 @@ local config = { 'folding_range.lua', 'handlers.lua', 'inlay_hint.lua', + 'inline_completion.lua', 'linked_editing_range.lua', 'log.lua', 'rpc.lua', diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index fb3dd0a49a..dc76240ed3 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -175,6 +175,8 @@ static const char *highlight_init_both[] = { "default link PmenuKindSel PmenuSel", "default link PmenuSbar Pmenu", "default link ComplMatchIns NONE", + "default link ComplHint NonText", + "default link ComplHintMore MoreMsg", "default link Substitute Search", "default link StatusLineTerm StatusLine", "default link StatusLineTermNC StatusLineNC", diff --git a/test/functional/plugin/lsp/inline_completion_spec.lua b/test/functional/plugin/lsp/inline_completion_spec.lua new file mode 100644 index 0000000000..7245181f2d --- /dev/null +++ b/test/functional/plugin/lsp/inline_completion_spec.lua @@ -0,0 +1,234 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local t_lsp = require('test.functional.plugin.lsp.testutil') +local Screen = require('test.functional.ui.screen') + +local dedent = t.dedent + +local api = n.api +local exec_lua = n.exec_lua +local insert = n.insert +local feed = n.feed + +local clear_notrace = t_lsp.clear_notrace +local create_server_definition = t_lsp.create_server_definition + +describe('vim.lsp.inline_completion', function() + local text = dedent([[ + function fibonacci() + ]]) + + local grid_without_candidates = dedent([[ + function fibonacci() | + ^ | + {1:~ }|*11 + | + ]]) + + local grid_with_candidates = dedent([[ + function fibonacci({1:n) {} | + {1: if (n <= 0) return 0;} | + {1: if (n === 1) return 1;} | + | + {1: let a = 0, b = 1, c;} | + {1: for (let i = 2; i <= n; i++) {} | + {1: c = a + b;} | + {1: a = b;} | + {1: b = c;} | + {1: }} | + {1: return b;} | + {1:}} | + ^ | + {3:-- INSERT --} | + ]]) + + local grid_applied_candidates = 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; | + ^} | + |*2 + ]]) + + --- @type test.functional.ui.screen + local screen + + --- @type integer + local client_id + + before_each(function() + clear_notrace() + exec_lua(create_server_definition) + + screen = Screen.new() + screen:set_default_attr_ids({ + [1] = { bold = true, foreground = Screen.colors.Blue1 }, + [2] = { bold = true, foreground = Screen.colors.SeaGreen4 }, + [3] = { bold = true }, + }) + + client_id = exec_lua(function() + _G.server = _G._create_server({ + capabilities = { + inlineCompletionProvider = true, + }, + handlers = { + ['textDocument/inlineCompletion'] = function(_, _, callback) + callback(nil, { + items = { + { + command = { + command = 'dummy', + title = 'Completion Accepted', + }, + insertText = 'function fibonacci(n) {\n if (n <= 0) return 0;\n if (n === 1) return 1;\n\n let a = 0, b = 1, c;\n for (let i = 2; i <= n; i++) {\n c = a + b;\n a = b;\n b = c;\n }\n return b;\n}', + range = { + ['end'] = { + character = 20, + line = 0, + }, + start = { + character = 0, + line = 0, + }, + }, + }, + { + command = { + command = 'dummy', + title = 'Completion Accepted', + }, + insertText = 'function fibonacci(n) {\n if (n <= 0) return 0;\n if (n === 1) return 1;\n\n let a = 0, b = 1, c;\n for (let i = 2; i <= n; i++) {\n c = a + b;\n a = b;\n b = c;\n }\n return c;\n}', + range = { + ['end'] = { + character = 20, + line = 0, + }, + start = { + character = 0, + line = 0, + }, + }, + }, + { + command = { + command = 'dummy', + title = 'Completion Accepted', + }, + insertText = 'function fibonacci(n) {\n if (n < 0) {\n throw new Error("Input must be a non-negative integer.");\n }\n if (n === 0) return 0;\n if (n === 1) return 1;\n\n let a = 0, b = 1, c;\n for (let i = 2; i <= n; i++) {\n c = a + b;\n a = b;\n b = c;\n }\n return b;\n}', + range = { + ['end'] = { + character = 20, + line = 0, + }, + start = { + character = 0, + line = 0, + }, + }, + }, + }, + }) + end, + }, + }) + + return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) + + exec_lua(function() + local client = assert(vim.lsp.get_client_by_id(client_id)) + _G.called = false + client.commands.dummy = function() + _G.called = true + end + end) + + insert(text) + feed('$') + exec_lua(function() + vim.lsp.inline_completion.enable() + end) + end) + + after_each(function() + api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) + end) + + describe('enable()', function() + it('requests or abort when entered/left insert mode', function() + screen:expect({ grid = grid_without_candidates }) + feed('i') + screen:expect({ grid = grid_with_candidates }) + feed('') + screen:expect({ grid = grid_without_candidates }) + end) + end) + + describe('get()', function() + it('applies the current candidate', function() + feed('i') + screen:expect({ grid = grid_with_candidates }) + exec_lua(function() + vim.lsp.inline_completion.get() + end) + feed('') + screen:expect({ grid = grid_applied_candidates }) + end) + end) + + describe('select()', function() + it('selects the next candidate', function() + feed('i') + screen:expect({ grid = grid_with_candidates }) + + exec_lua(function() + vim.lsp.inline_completion.select() + end) + + screen:expect([[ + function fibonacci({1:n) {} | + {1: if (n <= 0) return 0;} | + {1: if (n === 1) return 1;} | + | + {1: let a = 0, b = 1, c;} | + {1: for (let i = 2; i <= n; i++) {} | + {1: c = a + b;} | + {1: a = b;} | + {1: b = c;} | + {1: }} | + {1: return c;} | + {1:}}{2: (2/3)} | + ^ | + {3:-- INSERT --} | + ]]) + exec_lua(function() + vim.lsp.inline_completion.get() + end) + feed('') + screen:expect([[ + 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 c; | + ^} | + |*2 + ]]) + end) + end) +end)