Problem: due to totally async install/update/checkout there is no
guaranteed order of `PackChanged{Pre,}` events across different
plugins. This might lead to conflicts when callback for some "main"
plugin relies on features from "dependency" plugin: i.e. callback for
"main" plugin can trigger before installing/updating "dependency"
plugin. The installation order can be enforced by separate
vim.pack.add() calls, but update/checkout can not.
Solution: Trigger events in bulk independently of async execution:
- `PackChangedPre` before any action for all input plugins in order
they are supplied. It will also trigger even if an action will fail.
- `PackChanged` after all actions finished for all sucessfully
affected plugins in order they are supplied.
This also comes with a couple of side effect changes:
- `PackChangedPre kind=delete` is now also triggered even if the
delete won't be done. This makes it more aligned with `kind=install`
and `kind=update`.
- Force update (`:packupdate!`) and "udpate LSP action" now do two
async steps: download/compute updates and apply them. This also
results in two progress reports.
This is mostly a by-product of the implementation (there has to be
a pre-computation of target revision for all plugins before doing
`PackChangedPre` in bulk before possibly applying an update), but I
kind of like it more this way as it is more explicit of what's going
on. If absolutely not acceptable, there might be some hacks to
mitigate it at least in code action, but I'd keep it like this.
Problem:
dir.lua leaves previously-navigated directory buffers around.
This is fine by default, but users need a simple way to opt out.
Solution:
1. Respect `set hidden` (via `'bufhidden'`) as one way to make
previously-navigated dir buffers from showing up.
2. Document a one-liner to hide these buffers
Problem: plugin/shada.lua does not respect the `+` flag of 'cpoptions'
when writing a *.shada buffer into a different file.
Solution: Check the presence of `+` in 'cpoptions' and whether the
buffer is written to a different file in BufWriteCmd for
*.shada before resetting the 'modified' option of the buffer.
This little defect appeared after plugin/shada.vim was rewritten into
Lua in #34725.
Problem:
Diagnostic tracking used a separate bufstates table and manual
LspDetach/LspNotify autocmd management via _enable()/_refresh(),
duplicating the lifecycle logic already provided by the Capability
framework. This caused inconsistencies in how client attach/detach and
buffer teardown were handled compared to other LSP features.
Solution:
Replace the ad-hoc bufstate tracking and _enable/_refresh pattern in
vim.lsp.diagnostic with a proper Diagnostics subclass of Capability.
This cleans up a few random places in the main lsp module and client
module that were poking the diagnostics. It also fixes some pre-existing
bugs and inconsistencies that were discovered:
- Refresh diagnostics immediately on attach instead of lazily by the
first didOpen/didChange notification
- Fix Capability.active lookup in M.enable() to key by it_bufnr instead
of the filter bufnr
- Set lsp defaults before calling the _text_document_did_open_handler in
Client:on_attach() so defaults are there before any lsp notification
occurs
- Log (and return early) on any error from a diagnostic request result
instead of only returning early for server cancelled errors
Problem:
cmd given as string[] always starts using Nvim's CWD, which is arbitrary.
Solution:
If cmd_cwd is not given, use root_dir as CWD.
BREAKING CHANGE: LSP commands given as string arrays now use `root_dir` as
the process working directory when `cmd_cwd` is unset.
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
Problem:
`:edit <dir>` and `nvim <dir>` currently rely on netrw to show local directory
contents.
Solution:
- Provide `filetype=directory`.
- Introduce dir.lua, a small plugin that provides directory listing, opening
items, parent navigation, and refresh.
- `netrw` remains available for `:Explore`, remote paths, archives, and file
operations. To continue
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
Some servers register `workspace/didChangeWatchedFiles` watchers for URI
schemes that cannot be watched locally. Skipping the unsupported glob
and keep the rest of the registration batch active.
Problem: LspNotify autocmds were not being triggered for didChange
requests when being used during undo/redo (and possibly other) actions.
autocmds are blocked when calling on_lines() callbacks while doing the
undo/redo action.
Solution: Defer firing the autocmd until after the action is complete.
This is closer to what existed before, but now there's a check in the
deferred function to only fire the autocmd if the client is still
active and the buffer is still attached, if applicable.
Problem: LspNotify never passed a buffer when executing the autocmds, so
buffer-local LspNotify autocmd subscriptions didn't have the correct buf
in the event metadata. It was also wrapped in a schedule() so the actual
autocmd was delayed until after the event loop.
This could result in the wrong buffer receiving the notification if
multiple LspNotify autocmds with buffer filters were added. Only the
"latest" one would actually receive non-buffer-filtered autocmds, not
the matching one. It also caused listeners to receive the notification
"out of sync" with when the notification is actually sent. If a buffer
is being deleted (which fires a textDocument/didClose notification), the
notification is scheduled and fired after the buffer is already gone.
Solution: For LSP notifications that pertain to a particular buffer, set
it when executing the LspNotify autocmds so the callback functions that
are filtered on that buffer will get the correct notifications and the
metadata buf field will be correct. Additionally, there is no need to
wrap the LspNotify callback in vim.schedule when it can be called inline
when the notification to the rpc server is fired.
This is tested by removing now-unnecessary autocmds from semantic tokens
(InsertEnter and BufWinEnter should no longer be necessary now that
requests are fired by LspNotify). Without this fix, simply modifying a
buffer doesn't actually trigger LspNotify correctly, and the test for
that fails.
Problem: When multiline semantic token support was introduced, the loop
that finds the end line for a particular token didn't sanitize the token
length sent back by the LSP server. If the server returned an overflowed
length (near uint32 max), neovim would burn cpu and loop for an
extremely long time while trying to find the "end line" represented by
the massively large token, causing neovim to seemingly hang.
Solution: Stop looping once the calculated end_line reaches the actual
last line of the buffer.
Fixes#36257
Problem:
an empty `{ isIncomplete = true, items = {} }` ends completion
instead of requerying.
Solution:
keep isIncomplete on empty lists and retrigger on keypress while incomplete.
Reset on <C-e> so it doesn't immediately re-query.
Problem: The refactor to use Capability left around some cruft and
semi-broken configuration for debounce.
Solution: Clean up now-unnecessary helper methods and simplify
deprecated ones to pass through to the non-deprecated ones. `debounce`
now defaults to 200 for all buffers but is overridable via the
deprecated start() method, which continues to take the max value
specified for any client attached to the buffer.
If we wish to expose changing the debounce in a non-deprecated way, we
will need to consider a "configuration" function, or even a bespoke
method to set the debounce time on the main metaclass (or provide
options to override for a particular buffer). General configuration of
specific LSP features is an as-of-yet unsolved problem.
Problem: The semantic token module is using its own debounce timer for
the buffer on_lines event. If its internal debounce is shorter than the
changetracking module's debounce, it's possible for semantic token
requests to fire for changed buffers before the textDocument/didChange
notification is sent to the server.
Solution: Trigger semantic token requests from the LspNotify autocmd
when the method is the didChange or didOpen notifications, which
enforces a strict happens-before relationship for the sync change
notification followed by a semantic token request.
Note: There is still an internal debounce mechanism in the semantic
token module to handle other debouncing needs specific to its
functionality, such as debouncing server refresh notifications and
handling WinScrolled events when using range requests.
Problem:
After 400f247397 the undotree test became flaky.
It seems to be caused by some kind of race condition where an entry in the undotree is not added when a change in the buffer happens (adding a bunch of sleep around buffer-change operations seems to fix the issue).
Solution:
Disable autoread plugin in undotree test.
Problem:
Listing individual "skipped" notices for each non-actionable or
NVIM_TEST_INTEG test is noisy and makes the "skipped" list overwhelming
and less meaningful.
Solution:
- Mark non-actionable tests with "N/A".
- Report NVIM_TEST_INTEG tests as a separate 1-line summary.
Problem:
A text edit positioned entirely past the last buffer line, with
newText ending in a newline, leaves a stray blank line: the
past-the-end path appends the trailing empty fragment produced by
vim.split() and does not set has_eol_text_edit, so the end-of-buffer
cleanup is skipped. Formatting servers emit such edits whenever
formatting moves text to the end of a document.
Regression from ec94014cd1 (#20137), which split the past-the-end
fast path off the clamp path that sets the flag.
Solution:
Set has_eol_text_edit in the past-the-end path, like the adjacent
path that clamps end_row.
Problem:
When a server sends workspace/codeLens/refresh while an automatic codelens
request is already scheduled, Nvim ignores the server refresh. This can leave
rendered codelens text stale until another buffer edit triggers a new request.
Solution:
Cancel the pending automatic request and send the server-requested refresh
immediately. This preserves request coalescing while giving explicit server
refreshes priority.
Problem:
PR #38340 prevented messages we receive with id:null from being
incorrectly classified as notifications, but caused us to ignore all
messages with id:null, including requests.
Solution:
Handle requests with id:null. When we receive a request, we only need to
respond based on the `method` and `param`.
(The original so-called `notification_received` in the test was actually
semantically `request_or_notification_received`.)
Problem:
The logic for generating the complete item `info` is spread across
`_lsp_to_complete_items`, the `CompleteChanged` event handler, and
`CompletionResolver:request`. This has previously caused the `info`
shown for resolved (via `completionItem/resolve`) and unresolved items
to differ.
Solution:
Centralise the logic in a new `complete_item_info` function which is now
solely responsible for determining:
1. The `info` to show.
2. The markup kind of the `info`.
3. Whether the `info` is complete.
This simplifies the interaction between the 3 functions mentioned in the
problem:
- `_lsp_to_complete_items` calls `complete_item_info` and passes along
the markup kind and whether the item needs resolving via the complete
item's `user_data`.
- The `CompleteChanged` consumes the markup kind and whether the
item needs resolving from the complete item's `user_data`.
- `CompletionResolver:request`, like `_lsp_to_complete_items` calls
`complete_item_info` again and updates the current `info` if it's
changed.
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:
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 currently no convenient way to programmatically check
for new updates from plugin source. Running `vim.pack.update()` is one
approach, but it opens a confirmation buffer that requires a manual
action to close.
Solution: Add `opts.offline` to `vim.pack.get()` that will first fetch
new updates from plugin source before computing the output.
Problem: No convenient way to programmatically get the revision that
would be checked out after `vim.pack.update()` (with `offline=true`).
Doing this manually requires resolving `spec.version` which is not
trivial. This data can be useful for custom reporting of pending
updates or third party confirmation step.
Solution: Make `get()` include a new field for the revision that points
at the state after applying pending update. This is also the same as
the revision of resolved `spec.version`.
Problem: `vim.pack.get()` always uses lockfile as the source for the
`rev` field. This is fast, but may be misleading in case of
a corrupted lockfile.
Solution: Compute revision from Git repo on disk if `info=true`
(default), use lockfile otherwise. This does increase execution time
(as a result of one extra `git ...` call for every plugin), but
`info=true` is already designed to be informative and not necessarily
fast.
Define `:packupdate` and `:packdel` as separate commands instead of a
unified `:pack {subcommand}` because the semantics between the two
commands vary differently enough that it doesn't make sense to combine
them. Additionally, `:pack! update/del` looks bad.
Problem:
Using the `DiffTool` plugin (e.g. through `nvim -d ...` or `:DiffTool
<file1> <file2>` fails if a space is in one of the paths. This occurs
because the `diff` wraps the paths with quotes (`'`) if space
characters are present, which the line diff regex fails to parse.
Solution:
Update regex to handle quoted paths by matching the string within the
quotes, if it exists.
Problem: Trying to execute code action on an active plugin without
updates leads to nothing. It is more useful if code actions "do
something" on a bigger portion of the confirm buffer.
Solution: Suggest "delete" code action even for active plugins. Trying
to execute it will first show a confirmation buffer with relevant
warning of why this might be not a good idea. Confirming will delete
a plugin.
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:
`EBUSY` during cleanup:
Windows CI can intermittently fail `pack_spec.lua` with `EBUSY` while removing
`site/pack/core/opt/plugindirs`.
This can happen because:
- the test Nvim session may still be alive when `after_each()` removes the pack
directory
- Windows does not allow removing a directory while another process still has an
open handle below it
- startup-time `vim.pack.add()` performs a real `git clone`, so process and file
handle release timing can vary on slower runners
Startup timeout:
The startup tests can also fail before cleanup because they wait for `_G.done`
with a fixed timeout. That timeout includes the time needed for startup to run
`vim.pack.add()` and finish the local clone.
Solution:
Close before cleanup:
Capture the pack, lockfile, and log paths while the test Nvim session is still
available, then call `n.check_close()` before removing the pack directory.
Extend Windows startup wait:
Increase the `_G.done` retry budget only on Windows so startup-time
`vim.pack.add()` has more time to finish on slower CI runners.
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
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:
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: There are many Git forges each with a different way of
constructing permanent links to like commits and tags.
Solution: Add a private utility function that computes these special
links on the best effort basis.
Problem: In `vim.pack.update()` confirmation buffer it might be useful
to be able to use `gx` (open link at cursor) when cursor is on
something like commit or tag.
Solution: Add `textDocument/documentLink` method support for the
in-process LSP. This may be used by LSP clients and makes `gx`
automatically work.
The shortcoming is that this requires tracking how to construct a URL
from source and commit/tag. Currently only GitHub hosted repositories
are supported.
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.