fix(lsp): don't overlay insertion-style inline completions (#36477)

* feat(lua): `Range:is_empty()` to check vim.range emptiness

* fix(lsp): don't overlay insertion-style inline completions

**Problem:** Some servers commonly respond with an empty inline
completion range which acts as a position where text should be inserted.
However, the inline completion module assumes that all responses with a
range are deletions + insertions that thus require an `overlay` display
style. This causes an incorrect preview, because the virtual text should
have the `inline` display style (to reflect that this is purely an
insertion).

**Solution:** Only use `overlay` for non-empty replacement ranges.
This commit is contained in:
Riley Bruins
2025-11-09 17:49:25 -08:00
committed by GitHub
parent c6dad6e9df
commit c2c5a0297e
5 changed files with 50 additions and 1 deletions

View File

@@ -4136,6 +4136,7 @@ Provides operations to compare, calculate, and convert ranges represented by
Fields: ~ Fields: ~
• {start} (`vim.Pos`) Start position. • {start} (`vim.Pos`) Start position.
• {end_} (`vim.Pos`) End position, exclusive. • {end_} (`vim.Pos`) End position, exclusive.
• {is_empty} (`fun(self: vim.Range): boolean`) See |Range:is_empty()|.
• {has} (`fun(outer: vim.Range, inner: vim.Range): boolean`) See • {has} (`fun(outer: vim.Range, inner: vim.Range): boolean`) See
|Range:has()|. |Range:has()|.
• {intersect} (`fun(r1: vim.Range, r2: vim.Range): vim.Range?`) See • {intersect} (`fun(r1: vim.Range, r2: vim.Range): vim.Range?`) See
@@ -4167,6 +4168,12 @@ Range:intersect({r1}, {r2}) *Range:intersect()*
(`vim.Range?`) range that is present inside both `r1` and `r2`. `nil` (`vim.Range?`) range that is present inside both `r1` and `r2`. `nil`
if such range does not exist. See |vim.Range|. if such range does not exist. See |vim.Range|.
Range:is_empty() *Range:is_empty()*
Checks whether the given range is empty; i.e., start >= end.
Return: ~
(`boolean`) `true` if the given range is empty
Range:lsp({buf}, {range}, {position_encoding}) *Range:lsp()* Range:lsp({buf}, {range}, {position_encoding}) *Range:lsp()*
Creates a new |vim.Range| from `lsp.Range`. Creates a new |vim.Range| from `lsp.Range`.

View File

@@ -280,6 +280,7 @@ LUA
• Experimental `vim.pos` and `vim.range` for Position/Range abstraction. • Experimental `vim.pos` and `vim.range` for Position/Range abstraction.
• |vim.json.encode()| has an `indent` option for pretty-formatting. • |vim.json.encode()| has an `indent` option for pretty-formatting.
• |vim.json.encode()| has an `sort_keys` option. • |vim.json.encode()| has an `sort_keys` option.
• |Range:is_empty()| to check if a |vim.Range| is empty.
OPTIONS OPTIONS

View File

@@ -248,7 +248,7 @@ function Completor:show(hint)
api.nvim_buf_set_extmark(self.bufnr, namespace, row, col, { api.nvim_buf_set_extmark(self.bufnr, namespace, row, col, {
virt_text = virt_text, virt_text = virt_text,
virt_lines = virt_lines, virt_lines = virt_lines,
virt_text_pos = current.range and 'overlay' or 'inline', virt_text_pos = (current.range and not current.range:is_empty() and 'overlay') or 'inline',
hl_mode = 'combine', hl_mode = 'combine',
}) })
end end

View File

@@ -99,6 +99,13 @@ function Range.__eq(r1, r2)
return r1.start == r2.start and r1.end_ == r2.end_ return r1.start == r2.start and r1.end_ == r2.end_
end end
--- Checks whether the given range is empty; i.e., start >= end.
---
---@return boolean `true` if the given range is empty
function Range:is_empty()
return self.start >= self.end_
end
--- Checks whether {outer} range contains {inner} range. --- Checks whether {outer} range contains {inner} range.
--- ---
---@param outer vim.Range ---@param outer vim.Range

View File

@@ -83,6 +83,27 @@ describe('vim.lsp.inline_completion', function()
}, },
handlers = { handlers = {
['textDocument/inlineCompletion'] = function(_, _, callback) ['textDocument/inlineCompletion'] = function(_, _, callback)
if _G.empty then
callback(nil, {
items = {
{
insertText = 'foobar',
range = {
start = {
line = 0,
character = 19,
},
['end'] = {
line = 0,
character = 19,
},
},
},
},
})
return
end
callback(nil, { callback(nil, {
items = { items = {
{ {
@@ -198,6 +219,19 @@ describe('vim.lsp.inline_completion', function()
screen:expect({ grid = grid_applied_candidates }) screen:expect({ grid = grid_applied_candidates })
end) end)
it('correctly displays with absent/empty range', function()
exec_lua(function()
_G.empty = true
end)
feed('I')
screen:expect([[
function fibonacci({1:foobar}) |
^ |
{1:~ }|*11
{3:-- INSERT --} |
]])
end)
it('accepts on_accept callback', function() it('accepts on_accept callback', function()
feed('i') feed('i')
screen:expect({ grid = grid_with_candidates }) screen:expect({ grid = grid_with_candidates })