Merge #33972 feat(lsp): textDocument/inlineCompletion

This commit is contained in:
Justin M. Keyes
2025-08-24 22:17:34 -04:00
committed by GitHub
14 changed files with 840 additions and 12 deletions

View File

@@ -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' })

View File

@@ -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', '<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*

View File

@@ -3984,19 +3984,37 @@ by |vim.Pos| objects.
as format conversions.
Fields: ~
• {row} (`integer`) 0-based byte index.
• {col} (`integer`) 0-based byte index.
• {buf}? (`integer`) Optional buffer handle.
• {row} (`integer`) 0-based byte index.
• {col} (`integer`) 0-based byte index.
• {buf}? (`integer`) Optional buffer handle.
When specified, it indicates that this position belongs to a
specific buffer. This field is required when performing
position conversions.
• {to_lsp} (`fun(pos: vim.Pos, position_encoding: lsp.PositionEncodingKind)`)
See |Pos:to_lsp()|.
• {lsp} (`fun(buf: integer, pos: lsp.Position, position_encoding: lsp.PositionEncodingKind)`)
See |Pos:lsp()|.
When specified, it indicates that this position belongs
to a specific buffer. This field is required when
performing position conversions.
• {to_lsp} (`fun(pos: vim.Pos, position_encoding: lsp.PositionEncodingKind)`)
See |Pos:to_lsp()|.
• {lsp} (`fun(buf: integer, pos: lsp.Position, position_encoding: lsp.PositionEncodingKind)`)
See |Pos:lsp()|.
• {to_cursor} (`fun(pos: vim.Pos): [integer, integer]`) See
|Pos:to_cursor()|.
• {cursor} (`fun(pos: [integer, integer])`) See |Pos:cursor()|.
• {to_extmark} (`fun(pos: vim.Pos): [integer, integer]`) See
|Pos:to_extmark()|.
• {extmark} (`fun(pos: [integer, integer])`) See |Pos:extmark()|.
Pos:cursor({pos}) *Pos:cursor()*
Creates a new |vim.Pos| from cursor position.
Parameters: ~
• {pos} (`[integer, integer]`)
Pos:extmark({pos}) *Pos:extmark()*
Creates a new |vim.Pos| from extmark position.
Parameters: ~
• {pos} (`[integer, integer]`)
Pos:lsp({buf}, {pos}, {position_encoding}) *Pos:lsp()*
Creates a new |vim.Pos| from `lsp.Position`.
@@ -4016,6 +4034,24 @@ Pos:lsp({buf}, {pos}, {position_encoding}) *Pos:lsp()*
• {pos} (`lsp.Position`)
• {position_encoding} (`lsp.PositionEncodingKind`)
Pos:to_cursor({pos}) *Pos:to_cursor()*
Converts |vim.Pos| to cursor position.
Parameters: ~
• {pos} (`vim.Pos`) See |vim.Pos|.
Return: ~
(`[integer, integer]`)
Pos:to_extmark({pos}) *Pos:to_extmark()*
Converts |vim.Pos| to extmark position.
Parameters: ~
• {pos} (`vim.Pos`) See |vim.Pos|.
Return: ~
(`[integer, integer]`)
Pos:to_lsp({pos}, {position_encoding}) *Pos:to_lsp()*
Converts |vim.Pos| to `lsp.Position`.

View File

@@ -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

View File

@@ -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*

View File

@@ -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'

View File

@@ -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.

View File

@@ -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',

View File

@@ -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

View File

@@ -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,
},

View File

@@ -52,7 +52,7 @@ Pos.__index = Pos
---@package
---@param row integer
---@param col integer
---@param opts vim.Pos.Optional
---@param opts? vim.Pos.Optional
function Pos.new(row, col, opts)
validate('row', row, 'number')
validate('col', col, 'number')
@@ -168,12 +168,41 @@ function Pos.lsp(buf, pos, position_encoding)
-- When on the first character,
-- we can ignore the difference between byte and character.
if col > 0 then
col = vim.str_byteindex(get_line(buf, row), position_encoding, col)
-- `strict_indexing` is disabled, because LSP responses are asynchronous,
-- and the buffer content may have changed, causing out-of-bounds errors.
col = vim.str_byteindex(get_line(buf, row), position_encoding, col, false)
end
return Pos.new(row, col, { buf = buf })
end
--- Converts |vim.Pos| to cursor position.
---@param pos vim.Pos
---@return [integer, integer]
function Pos.to_cursor(pos)
return { pos.row + 1, pos.col }
end
--- Creates a new |vim.Pos| from cursor position.
---@param pos [integer, integer]
function Pos.cursor(pos)
return Pos.new(pos[1] - 1, pos[2])
end
--- Converts |vim.Pos| to extmark position.
---@param pos vim.Pos
---@return [integer, integer]
function Pos.to_extmark(pos)
return { pos.row, pos.col }
end
--- Creates a new |vim.Pos| from extmark position.
---@param pos [integer, integer]
function Pos.extmark(pos)
local row, col = unpack(pos)
return Pos.new(row, col)
end
-- Overload `Range.new` to allow calling this module as a function.
setmetatable(Pos, {
__call = function(_, ...)

View File

@@ -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',

View File

@@ -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",

View File

@@ -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)