mirror of
https://github.com/neovim/neovim.git
synced 2025-09-05 19:08:15 +00:00
Merge #33972 feat(lsp): textDocument/inlineCompletion
This commit is contained in:
@@ -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' })
|
||||
|
@@ -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*
|
||||
|
||||
|
@@ -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`.
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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*
|
||||
|
@@ -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'
|
||||
|
@@ -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.
|
||||
|
@@ -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',
|
||||
|
435
runtime/lua/vim/lsp/inline_completion.lua
Normal file
435
runtime/lua/vim/lsp/inline_completion.lua
Normal 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
|
@@ -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,
|
||||
},
|
||||
|
@@ -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(_, ...)
|
||||
|
@@ -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',
|
||||
|
@@ -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",
|
||||
|
234
test/functional/plugin/lsp/inline_completion_spec.lua
Normal file
234
test/functional/plugin/lsp/inline_completion_spec.lua
Normal 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)
|
Reference in New Issue
Block a user