commit 0e70aa0e8687014b5902d6de1208b2950ef988c4
parent 40f5115ac43088fb6183a860d5db4ccd206e9bcc
Author: Yi Ming <ofseed@foxmail.com>
Date: Wed, 20 Aug 2025 13:54:20 +0800
feat(lsp): support `textDocument/inlineCompletion`
Diffstat:
12 files changed, 763 insertions(+), 0 deletions(-)
diff --git 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
@@ -332,6 +332,7 @@ They are also listed below.
- `'textDocument/formatting'`
- `'textDocument/hover'`
- `'textDocument/inlayHint'`
+- `'textDocument/inlineCompletion'`
- `'textDocument/publishDiagnostics'`
- `'textDocument/rangeFormatting'`
- `'textDocument/rename'`
@@ -2220,6 +2221,73 @@ is_enabled({filter}) *vim.lsp.inlay_hint.is_enabled()*
==============================================================================
+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', '<Tab>', function()
+ if not vim.lsp.inline_completion.get() then
+ return '<Tab>'
+ 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*
The `vim.lsp.linked_editing_range` module enables "linked editing" via a
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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<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 client_state table<integer, vim.lsp.inline_completion.ClientState>
+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', '<Tab>', function()
+--- if not vim.lsp.inline_completion.get() then
+--- return '<Tab>'
+--- 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 `<expr>`.
+ 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
@@ -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
@@ -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
@@ -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
@@ -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('<Esc>')
+ 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('<Esc>')
+ 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('<Esc>')
+ 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)