Problem:
`CompletionItem.detail` is only shown in the info popup if the server
supports `completionItem/resolve`.
Solution:
If the server doesn't support `completionItem/resolve`, prepend the
complete item `info` with `CompletionItem.detail` in a fenced codeblock,
same as we do when the server supports `completionItem/resolve`.
To ensure that completion items are displayed in the same way,
regardless of whether the server supports `completionItem/resolve`, i've
extracted out the test logic from the `selecting an item triggers
completionItem/resolve + (snippet) preview` case so that we can run the
same tests against a server which supports `completionItem/resolve` and
one which doesn't. Hopefully this should prevent the two behaviours
diverging again.
Problem:
If `CompletionItem.documentation` is populated but `detail` is not, then
`detail` is not resolved.
Solution:
Ensure that we resolve a `CompletionItem` if either `detail` or
`documentation` are not populated.
I've also removed `detail` from the popup menu since otherwise it will
be populated in both the popup menu and the info popup after the
`CompletionItem` has been resolved. I think the info popup is the best
place for it anyway as when there is a completion item with a long popup
menu entry (when `detail` is a medium/long function signature for
instance), the whole popup menu gets widened and this steals horizontal
space that could be used to display the `documentation`. Now with
`detail` and `documentation` in the info popup, they share the same
horizontal space. This also aligns with how VSCode, nvim-cmp, blink.cmp,
and mini.nvim display `detail`.
Problem:
When a resolved `CompletionItem` with kind `Snippet` populates
`textEdit` instead of `insertText`, the contents are not previewed.
Solution:
Generate the snippet preview from `textEdit.newText` as well.
Problem:
`nvim_create_autocmd` is too verbose and its `callback` requires extra
"nesting".
Solution:
Introduce `nvim_on`. Start using it internally. Then we can get a feel
for how it should look before making it public.
Problem:
A benefit of the old `:LspInfo` was that it showed info related to the
"current buffer" a bit more clearly.
Solution:
Highlight the "current buffer" in the LSP checkhealth report.
Problem:
filewatcher and position-encoding info is reported after the big list of
client details.
Solution:
- report the info in the top-level, not in dedicated subsections.
Problem:
- To share logic, creating a `vim.Range` currently creates two `vim.Pos` values
as intermediates, which causes unnecessary table allocations.
- `pos.lua` and `range.lua` contain some overlapping logic.
Solution:
Add `vim.pos._util`, a module for handling
positions represented directly by `row` and `col`.
Problem:
- A window can only have one cursor, ranges selected by the cursor are typically
obtained by marks like ">" and "<", instead of calling get_cursor() twice.
- `vim.Range` is described as end-exclusive,
but the current `range.cursor()`/`range:to_cursor()` are end-inclusive.
- Conversion between `vim.Range` and mark-indexed range can be done by
`range.mark()`/`range:to_mark()`
Solution:
Remove `range.cursor()` and `range:to_cursor()`,
Problem:
After 767fbd8, typing trigger chars would open completion but the
chars were removed.
Solution:
Use filterText fallback so selected item respects typed trigger chars.
Problem:
PlainText completion items used `textEdit.newText` or `insertText` as
the completion word even when they did not match the typed prefix. This
could break popup completion behavior like 'completeopt+=longest'.
Solution:
Fall back to `filterText` when `newText` or `insertText` does not match
the typed prefix.
Problem: There is no way for a `vim.ui.input` caller to indicate for
which scope the input is. As in "This input is for something at cursor
scope". This information can be useful for `vim.ui.input`
implementation to tweak its behavior and presentation:
- Show different floating window depending on the scope. For example:
- Near cursor for "cursor" scope.
- At line start for "line" scope.
- In window corner for "buffer" and "window" scopes.
- In whole editor corner for "tabpage", "editor", "project" scopes.
- Navigate through history only for inputs with the same scope.
Solution: Document new `opts.scope` for `vim.ui.input`. Use it in the
codebase.
Problem:
`make_position_params`/`get_line_byte_from_position`/`make_line_range_params`
are private functions and their functionality can be replaced by `vim.pos` now.
Solution:
Remove them, use `vim.pos` instead.
Problem:
Currently, it only supports sending requests using the current cursor position as a parameter.
Solution:
Support sending requests using arbitrary positions.
Problem:
Linter missed backtick and double-quote keynames in the quasi-keyset of
the `nvim_create_user_command` docstring.
Solution:
Update the linter to check backtick-surrounded and quote-surrounded key
names.
Problem:
Nested workspace capabilities like workspace.fileOperations.didCreate and
workspace.textDocumentContent are not handled consistently for dynamic and
static registration provider lookup.
Solution:
Generate explicit registration-provider mappings from the LSP metadata and use
them when registering and querying capabilities. Add coverage for dynamic and
static nested workspace registrations.
Problem:
LSP clients previously did not handle dynamic registration for off-spec methods
Solution:
Update the client logic to assume support for dynamic registration when
the method is unknown. Adjust the registration provider fallback and
enhance tests to verify correct behaviour for unknown methods and their
registration options. This improves compatibility with servers using
custom dynamic registrations.
AI-assisted: OpenCode
docs: update instructions for debugging LSP
Previously, it was suggested to set:
vim.lsp.log.set_format_func(vim.inspect)
This made sense before f72c13341a, when
`format_func` was called once per argument being logged, but since that
commit it's called with the log level followed by the other args, so the
suggested setting would call `vim.inspect(log_level, ....)` which would
just print the human readable name of the current log level and no other
details, for example with this set I saw in my logs:
"DEBUG""DEBUG""DEBUG""DEBUG"
Instead just rely on the default formatter, which will:
> ... log the level, date, source and line number of the
caller, followed by the arguments.
Problem:
If a buffer's filetype changes after the LSP client has already
attached (e.g. from json to jsonc via a modeline), but the client
supports both filetypes, it stays attached. It does not notify the
server of the new languageId, causing the server to incorrectly process
the file using the old languageId.
Solution:
Save the languageId used during textDocument/didOpen, and send
textDocument/didClose + textDocument/didOpen when buffer's languageId
changed.
Lsp spec:
0003fb53f1/_specifications/lsp/3.18/textDocument/didOpen.md (L5)
> If the language id of a document changes, the client
> needs to send a textDocument/didClose to the server followed by a
> textDocument/didOpen with the new language id if the server handles
> the new language id as well.
AI-assisted: Gemini 3.1 Pro
Problem:
The threshold to consider a color as bright is too high, and for some colors the
foreground is set to white when it should be black.
Solution:
Change the threshold to 0.179. This value is taken from pastel-textcolor.
Problem:
Error when querying document symbols using python-lsp-server:
lsp/util.lua:1955: attempt to concatenate field 'containerName' (a userdata value)
Solution:
Check for `vim.NIL`.
Problem: With `vim.g.health = { style = 'float' }`, running
`:checkhealth` from a `:help` buffer placed the float in the
top-left corner instead of centered.
make_floating_popup_options() picks the NW/NE/SW/SE anchor
and the available height from cursor-relative metrics
(winline(), wincol(), winheight()). When the caller passes
relative='editor', those metrics are meaningless, so the
function could flip to an 'E' anchor and clamp the float
off-screen.
Solution: When relative='editor', treat the whole editor area as
available space (lines_above=0, lines_below=&lines,
wincol=0). This makes the NW anchor the natural choice and
keeps the float position stable regardless of where the
cursor is in the current window.
AI-assisted: Claude Code
Problem:
Since `foldclose` is async, it must wait for the request to return before actually executing, at which point the original window may no longer be valid.
Solution:
Check whether the window is valid before actually performing `foldclose`.
Problem:
The LSP client incorrectly checks for server capabilities when determining
support for self-mapped methods (e.g., 'shutdown'), which do not have
corresponding capabilities in the server's response. This leads to false
negatives when checking if such methods are supported.
This was handled correctly for dynamic registrations, but not for static.
Methods such as 'shutdown', do not have a related server capability and should
be assumed to be supported.
Solution:
Update the `supports_method` logic to always return true for self-mapped
methods.
Problem:
LSP spec allows response message to have a null request-id.
This may happen when for example client sends unparseable request.
https://github.com/microsoft/language-server-protocol/issues/196
Solution:
Guard the server response branches against id=vim.NIL (json null),
and handle error responses with null id by logging a warning
and dispatching on error.
Problem:
CI (ubuntu asan, ubuntu tsan, windows) reports `uv_loop_close()
hang?` from the two new null-id response tests. The leaked
handle is the server-side accepted TCP socket created inside
`server:listen` callback. The tests closed only the listener
but not the accepted socket, so libuv could not finish shutting
down the loop and each test session took ~2s extra to exit.
Solution:
Hoist the accepted socket to the outer `exec_lua` scope and
close it at teardown before closing the listener. The close
runs synchronously inside `exec_lua`, so the loop has time to
dispose the handle before the session exits.
* test(lsp): close accepted socket on read-loop exit/error
Match the precedent in the handler test ("handler can return
false as response") and the shared `_create_tcp_server` helper
in `test/functional/plugin/lsp/testutil.lua`: close the
accepted socket from inside the `create_read_loop` exit/error
callbacks. The teardown close added in the previous commit
remains as belt-and-suspenders, so the socket is disposed
whether the server goes away first or the client does.
Problem:
Use vim.lsp.util.apply_text_edits to re-apply the same textedit causes
an incorrect edit, because apply_text_edits silently modifies the
parameter.
Solution:
- Avoid changing `text_edit._index`.
- Document this fun feature.
Helped-by: Riley Bruins <ribru17@hotmail.com>
Helped-by: Yi Ming <ofseed@foxmail.com>
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
Problem:
The fromRanges field of the result of callHierarchy/outgoingCalls is
documented as being relative to the caller. Using
vim.lsp.buf.outgoing_calls() opened the qflist with an entry with the
callee's filename, but the caller's line number.
Solution:
Open the qflist with the callers file (the bufnr from the request),
rather than the callees (the uri from the resulting CallHierarchyItem)
Problem:
Cursor-position `vim.lsp.buf.code_action()` requests include all diagnostics on the current line, so unrelated same-line diagnostics affect the returned actions.
Solution:
Filter same-line diagnostics to the cursor position for cursor-position requests.
benchmark: https://gist.github.com/ofseed/6224529d77c016c36f7ab2f977059848
local rounds = tonumber(arg[1]) or 1000
local count = tonumber(arg[2]) or 1000
-- Load the table.clear function.
local clear = require("table.clear")
local function fill(t, n)
for i = 1, n do
t[i] = i
end
end
local function bench_reassign(n_rounds, n_items)
local t = {}
local start = os.clock()
for _ = 1, n_rounds do
t = {}
collectgarbage("collect")
fill(t, n_items)
end
return os.clock() - start
end
local function bench_reassign_no_gc(n_rounds, n_items)
local t = {}
local start = os.clock()
for _ = 1, n_rounds do
t = {}
fill(t, n_items)
end
return os.clock() - start
end
local function bench_clear(n_rounds, n_items)
local t = {}
local start = os.clock()
for _ = 1, n_rounds do
clear(t)
fill(t, n_items)
end
return os.clock() - start
end
-- Warm up LuaJIT before the real benchmark.
do
local t = {}
for _ = 1, 2000 do
clear(t)
fill(t, count)
end
end
collectgarbage("collect")
local reassign_time = bench_reassign(rounds, count)
collectgarbage("collect")
local reassign_no_gc_time = bench_reassign_no_gc(rounds, count)
collectgarbage("collect")
local clear_time = bench_clear(rounds, count)
print(string.format("rounds=%d count=%d", rounds, count))
print(string.format("t = {} + GC : %.6f s", reassign_time))
print(string.format("t = {} : %.6f s", reassign_no_gc_time))
print(string.format("table.clear : %.6f s", clear_time))
print(string.format("vs + GC : %.2fx", reassign_time / clear_time))
print(string.format("vs no GC : %.2fx", reassign_no_gc_time / clear_time))
benchmark result:
rounds=1000 count=1000
t = {} + GC : 0.022469 s
t = {} : 0.002570 s
table.clear : 0.000387 s
vs + GC : 58.06x
vs no GC : 6.64x
`count` is how many items the table has, and `round` is how many rounds we fill
the table, clear, and then refill it. `table = {}` is clear the table by
resigning a new empty one, because this script does not run persistently like
nvim so GC is not triggered, so I added another extreme control group that
manually triggers GC.
Problem:
Codelens virtual lines remain on stale rows after an external file
change and buffer reload.
Solution:
Clear codelens extmarks and cached row/version state in `on_reload`
before requesting fresh code lenses.
Problem: completionItem/resolve response's `detail` field is silently
dropped. Only `documentation` is shown in the popup.
Solution: Prepend `detail` as a fenced code block before `documentation`
in the info popup, skipping if documentation already contains it.