Work on #37166
- Dynamic Registration Tracking via Provider
- Supports_Method
- Multiple Registrations
- RegistrationOptions may dictate support for a method
Problem: Deleting active plugins can lead to a situation when it is
reinstalled after restart.
Solution: Suggest "delete" code action only for not active plugins.
Whether a plugin is not active is visible by a hint in its header.
Problem: After `vim.pack.update()` it is not clear if plugin is active
or not. This can be useful to detect cases when plugin was removed
from 'init.lua' but there was no `vim.pack.del()`.
Solution: Add ` (not active)` suffix with distinctive highlighting to
header of plugins that are not active.
It will also be shown in in-process LSP document symbols to have quick
reference about which plugins are not active.
Problem: Using `vim.pack.del()` to delete active plugin can lead to
a situation when this plugin is reinstalled after restart. Removing
plugin from 'init.lua' is documented, but can be missed.
Solution: Make `del()` only remove non-active plugins by default and
throw an informative error if there is an active plugin.
Add a way to force delete any plugin by adding `opts.force`. This also
makes `del()` signature be the same as other functions, which is nice.
Problem: There is now way to run `update()` without Internet connection
while there are some workflows that do not require it. Like "switch
plugin version" and "revert latest update".
Solution: Add `opts.offline` to `update()`. This also allows now to
treat `vim.pack.update(nil, { offline = true })` as a way to
interactively explore currently installed plugins.
Problem: There are two fairly common workflows that involve lockfile and
can be made more straightforward in `vim.pack`:
1. Revert latest update. Like if it introduced unwanted behavior.
2. Update to a revision in the lockfile. Like if already updated
on another machine, verified that everything works, `git add` +
`git commit` + `git push` the config, and want to have the same
plugin states on current machine.
Solution: Make `update` allow `opts.target`. By default it uses
`version` from a plugin specification (like a regular "get new changes
from source" workflow). But it also allows `"lockfile"` value to
indicate that target revision after update should be taken from the
current lockfile verbatim.
With this, the workflows are:
1. Revert (somehow) to the lockfile before the update, restart, and
`vim.pack.update({ 'plugin' }, { target = 'lockfile' })`. If Git
tracked, revert with `git checkout HEAD -- nvim-pack-lock.json`.
For non-VCS tracked lockfile, the revisions can be taken from the
log file. It would be nicer if `update()` would backup a lockfile
before doing an update, but that might require discussions.
2. `git pull` + `:restart` +
`vim.pack.update(nil, { target = 'lockfile' })`.
The only caveats are for new and deleted plugins:
- New plugins (not present locally but present in the lockfile)
will be installed at lockfile revision during restart.
- Deleted plugins (present locally but not present in the
lockfile) will still be present: both locally *and* in the
lockfile. They can be located by
`git diff -- nvim-pack-lock.json` and require manual
`vim.pack.del({ 'old-plugin1', 'old-plugin2' })`.
Problem: Changing `src` of already installed plugin currently takes
effect immediately inside `vim.pack.add()` and acts as "delete and
later fresh install". Although more robust, this might lead to
unintentional data loss (since plugin is deleted) if the plugin was
manually modified or the new source is not valid.
Also this introduces unnecessary differentiation between "change
`version`" and "change `src`" of already installed plugin.
Solution: Require an explicit `vim.pack.update()` to change plugin's
source. It is done by conditionally changing `origin` remote of the
Git repo. The effect does not require update confirmation in order to
have new changes fetched from the new `src` right away.
If in the future there are more types of plugins supported (i.e. not
only Git repos), also do extra work (like delete + install) during
`vim.pack.update()`.
Problem:
The q keymap is already set in open_floating_preview, so maparg('q') is not empty.
Solution:
Add a health.style check before setting the q keymap.
Problem: When fuzzy is enabled and the prefix is not empty,
items are not sorted by fuzzy score before calling fn.complete.
Solution: Use matchfuzzypos to get the scores and sort the items
by fuzzy score before calling fn.complete.
Refactor capability checks in Client:_supports_registration and
Client:supports_method to properly handle dynamicRegistration and unknown
methods. Now, dynamic capabilities are checked before assuming support for
unknown methods, ensuring more accurate LSP feature detection.
Problem:
If the last visible line in a window is not fully displayed, this line
may not get injection highlighting. This happens because line('w$')
actually means the last *completely displayed* line.
Solution:
Use line('w$') + 1 for the botline.
This reverts 4244a96774
"test: fix failing lsp/utils_spec #36609",
which changed the test based on the wrong behavior.
Problem:
Nvim supports `textDocument/semanticTokens/full` and `…/full/delta`
already, but most servers don't support `…/full/delta` and Nvim will try
to request and process full semantic tokens response on every buffer
change. Even though the request is debounced, there is noticeable lag if
the token response is large (in a big file).
Solution:
Support `textDocument/semanticTokens/range`, which requests semantic
tokens for visible screen only.
Problem:
With the typescript LSes typescript-language-server and vtsls,
omnicompletion on partial tokens for certain types, such as array
methods, and functions that are attached as attributes to other
functions, either results in no entries populated in the completion menu
(typescript-language-server), or an unfiltered completion menu with all
array methods included, even if they don't share the same prefix as the
partial token being completed (vtsls).
Solution:
Enable insertReplaceSupport and uses the insert portion of the lsp
completion response in adjust_start_col if it's included in the
response.
Completion results are still filtered client side.
Problem: No way to customize completion order across multiple servers.
Solution: Add `cmp` function to `vim.lsp.completion.enable()` options
for custom sorting logic.
Problem:
Users often jump and navigate through LSP windows to yank text.
Concealed markdown can make navigation through hyperlinks and code
blocks more difficult.
Solution:
Change 'concealcursor' from 'n' to '' to preserve clean display
while improving navigation and selection of the LSP response.
Closes#36537
Problem: `nvim://` scheme feels more like a generalized interface that
may be requested externally, and it acts like CLI args (roughly).
This is how `vscode://` works.
Anything that behaves like an "app" or a "protocol" deserves its own
scheme. For such Nvim-owned things they will be called `nvim-xx://`.
Solution: Use `nvim-pack://confirm#<bufnr>` template for confirmation
buffer name instead of `nvim://pack-confirm#<bufnr>`.
Problem: Lockfile can become out of sync with what is actually installed
on disk when user performs (somewhat reasonable) manual actions like:
- Delete lockfile and expect it to regenerate.
- Delete plugin directory without `vim.pack.del()`.
- Manually edit lock data in a bad way.
Solution: Synchronize lockfile data with installed plugins on every
lockfile read. In particular:
1. Install immediately all missing plugins with valid lock data.
This helps with "manually delete plugin directory" case by
prompting user to figure out how to properly delete a plugin.
2. Repair lock data for properly installed plugins.
This helps with "manually deleted lockfile", "manually edited
lockfile in an unexpected way", "installation terminated due to
timeout" cases.
3. Remove unrepairable corrupted lock data and their plugins. This
includes bad lock data for missing plugins and any lock data
for corrupted plugins (right now this only means that plugin
path is not a directory, but can be built upon).
Step 1 also improves usability in case there are lazy loaded plugins
that are rarely loaded (like on `FileType` event, for example):
- Previously starting with config+lockfile on a new machine only
installs rare `vim.pack.add()` plugin after it is called (while
an entry in lockfile would still be present). This could be
problematic if there is no Internet connection, for example.
- Now all plugins from the lockfile are installed before actually
executing the first `vim.pack.add()` call in 'init.lua'. And later
they are only loaded on a rare `vim.pack.add()` call.
---
Synchronizing lockfile on its every read makes it work more robustly
if other `vim.pack` functions are called without any `vim.pack.add()`.
---
Performance for a regular startup (good lockfile, everything is
installed) is not affected and usually even increased. The bottleneck
in this area is figuring out which plugins need to be installed.
Previously the check was done by `vim.uv.fs_stat()` for every plugin
in `vim.pack.add()`. Now it is replaced with a single `vim.fs.dir()`
traversal during lockfile sync while later using lockfile data to
figure out if plugin needs to be installed.
The single `vim.fs.dir` approach scales better than `vim.uv.fs_stat`,
but might be less performant if there are many plugins that will be
not loaded via `vim.pack.add()` during startup.
Rough estimate of how long the same steps (read lockfile and normalize
plugin array) take with a single `vim.pack.add()` filled with 43
plugins benchmarking:
- Before commit: ~700 ms
- After commit: ~550 ms
Problem: Currently it is possible to have plugin in a "partial install"
state when `git clone` was successfull but `git checkout` was not.
This was done to not checkout default branch by default in these
situations (for security reasons).
The problem is that it adds complexity when both dealing with lockfile
(plugin's `rev` might be `nil`) and in how `src` and `version` are
treated (wrong `src` - no plugin on disk; wrong `version` - "partial"
plugin on disk).
Solution: Treat plugin as "installed" if both `git clone` and
`git checkout` are successful, while ensuring that not installed
plugins are not on disk and in lockfile.
This also means that if in 'init.lua' there is a `vim.pack.add()` with
bad `version`, for first install there will be an informative error
about it BUT next session will also try to install it. The solution is
the same - adjust `version` beforehand.
Problem: Installation confirmation has several usability issues:
- Choosing "No" results in a `vim.pack.add()` error. This was by
design to ensure that all later code that *might* reference
presumably installed plugin will not get executed. However, this
is often too restrictive since there might be no such code (like
if plugin's effects are automated in its 'plugin/' directory).
Instead the potential code using not installed plugin will throw
an error.
No error on "No" will also be useful for planned lockfile repair.
- List of soon-to-be-installed plugins doesn't mention plugin names.
This might be confusing if plugins are installed under different
name.
Solution: Silently drop installation step if user chose "No" and show
plugin names in confirmation text (together with their pretty aligned
sources).
Problem:
Some servers write log to stdout and there's no way to avoid it.
See https://github.com/neovim/neovim/pull/35743#pullrequestreview-3379705828
Solution:
We can extract `content-length` field byte by byte and skip invalid
lines via a simple state machine (name/colon/value/invalid), with minimal
performance impact.
I chose byte parsing here instead of pattern. Although it's a bit more complex,
it provides more stable performance and allows for more accurate error info when
needed.
Here is a bench result and script:
parse header1 by pattern: 59.52377ms 45
parse header1 by byte: 7.531128ms 45
parse header2 by pattern: 26.06936ms 45
parse header2 by byte: 5.235724ms 45
parse header3 by pattern: 9.348495ms 45
parse header3 by byte: 3.452389ms 45
parse header4 by pattern: 9.73156ms 45
parse header4 by byte: 3.638386ms 45
Script:
```lua
local strbuffer = require('string.buffer')
--- @param header string
local function get_content_length(header)
for line in header:gmatch('(.-)\r?\n') do
if line == '' then
break
end
local key, value = line:match('^%s*(%S+)%s*:%s*(%d+)%s*$')
if key and key:lower() == 'content-length' then
return assert(tonumber(value))
end
end
error('Content-Length not found in header: ' .. header)
end
--- @param header string
local function get_content_length_by_byte(header)
local state = 'name'
local i, len = 1, #header
local j, name = 1, 'content-length'
local buf = strbuffer.new()
local digit = true
while i <= len do
local c = header:byte(i)
if state == 'name' then
if c >= 65 and c <= 90 then -- lower case
c = c + 32
end
if (c == 32 or c == 9) and j == 1 then
-- skip OWS for compatibility only
elseif c == name:byte(j) then
j = j + 1
elseif c == 58 and j == 15 then
state = 'colon'
else
state = 'invalid'
end
elseif state == 'colon' then
if c ~= 32 and c ~= 9 then -- skip OWS normally
state = 'value'
i = i - 1
end
elseif state == 'value' then
if c == 13 and header:byte(i + 1) == 10 then -- must end with \r\n
local value = buf:get()
return assert(digit and tonumber(value), 'value of Content-Length is not number: ' .. value)
else
buf:put(string.char(c))
end
if c < 48 and c ~= 32 and c ~= 9 or c > 57 then
digit = false
end
elseif state == 'invalid' then
if c == 10 then -- reset for next line
state, j = 'name', 1
end
end
i = i + 1
end
error('Content-Length not found in header: ' .. header)
end
--- @param fn fun(header: string): number
local function bench(label, header, fn, count)
local start = vim.uv.hrtime()
local value --- @type number
for _ = 1, count do
value = fn(header)
end
local elapsed = (vim.uv.hrtime() - start) / 1e6
print(label .. ':', elapsed .. 'ms', value)
end
-- header starting with log lines
local header1 =
'WARN: no common words file defined for Khmer - this language might not be correctly auto-detected\nWARN: no common words file defined for Japanese - this language might not be correctly auto-detected\nContent-Length: 45 \r\n\r\n'
-- header starting with content-type
local header2 = 'Content-Type: application/json-rpc; charset=utf-8\r\nContent-Length: 45 \r\n'
-- regular header
local header3 = ' Content-Length: 45\r\n'
-- regular header ending with content-type
local header4 = ' Content-Length: 45 \r\nContent-Type: application/json-rpc; charset=utf-8\r\n'
local count = 10000
collectgarbage('collect')
bench('parse header1 by pattern', header1, get_content_length, count)
collectgarbage('collect')
bench('parse header1 by byte', header1, get_content_length_by_byte, count)
collectgarbage('collect')
bench('parse header2 by pattern', header2, get_content_length, count)
collectgarbage('collect')
bench('parse header2 by byte', header2, get_content_length_by_byte, count)
collectgarbage('collect')
bench('parse header3 by pattern', header3, get_content_length, count)
collectgarbage('collect')
bench('parse header3 by byte', header3, get_content_length_by_byte, count)
collectgarbage('collect')
bench('parse header4 by pattern', header4, get_content_length, count)
collectgarbage('collect')
bench('parse header4 by byte', header4, get_content_length_by_byte, count)
```
Also, I removed an outdated test
accd392f4d/test/functional/plugin/lsp_spec.lua (L1950)
and tweaked the boilerplate in two other tests for reusability while keeping the final assertions the same.
accd392f4d/test/functional/plugin/lsp_spec.lua (L5704)accd392f4d/test/functional/plugin/lsp_spec.lua (L5721)
Problem: Changing `src` of an existing plugin cleanly requires manual
`vim.pack.del()` prior to executing `vim.pack.add()` with a new `src`.
Solution: Autodetect `src` change for an existing plugin (by comparing
against lockfile data). If different - properly delete immediately and
treat this as new plugin installation.
Alternative solution might be to update `origin` remote in the
installed plugin after calling `vim.pack.update()`. Although, doable,
this 1) requires more code; and 2) works only for Git plugins (which
might be not the only type of plugins in the future). Automatic
"delete and clean install" feels more robust.
Problem: Plain `vim.pack.add()` calls (with default `opts.load`) does
not fully work if called inside 'plugin/' runtime directory. In
particular, 'plugin/' files of newly added plugins are not sourced.
This is because `opts.load` is `false` during the whole startup, which
means `:packadd!` is used (modify 'runtimepath' but not force source
newly added 'plugin/' files).
This use case is common due to users organizing their config as
separate files in '~/.config/nvim/plugin/'.
Solution: Use newly added `v:vim_did_init` to decide default `opts.load`
value instead of `v:vim_did_enter`.
Problem: reuse_win will always jump to the first window containing the
target buffer rather even if the buffer is displayed in the current
window/tab
Solution: check to see if the buffer is already displayed in the
current window or any window of the current buffer
Problem:
- Exposing the raw config as table is a pattern not seen anywhere else
in the Nvim codebase.
- Old spellfile.vim docs still available, no new documentation
Solution:
- Exposing a `config()` function that both acts as "getter" and "setter"
is a much more common idiom (e.g. vim.lsp, vim.diagnostic).
- Add new documentation and link old docs to |spellfile.lua| instead of
|spellfile.vim|.
* 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.
Problem: Confirmation buffer is named with `nvim-pack://` as scheme
prefix and uses buffer id (needed for in-process LSP) as one an entry
in the "hierarchical part".
Solution: Use `nvim://pack-confirm#<buf>` format with a more ubiquitous
`nvim://` prefix and buffer id at the end as the optional fragment.
Problem: In some areas plugin's revision is named "state". This might be
confusing for the users.
Solution: Consistently use "revision" to indicate "plugin's state on
disk".
Problem: Using abbreviated version of commit hashes might be unreliable
in the long term (although highly unlikely).
Solution: Use full hashes in lockfile and revision description (in
confirmation buffer and log). Keep abbreviated hashes when displaying
update changes (for brevity).
1. Every hyperlink-like element was replaced by `"$1"` (where $1 is the original string showed in the hyperlink);
2. Arrows `--->` were used in lines containing practice examples when no editing text is involved;
3. Context on interactivity was minimally adapted when strictly needed, not to disrupt the original tutor's intent;
4. Tests regarding the tutor file refactored to ensure the new syntax is not flagged as an error.
Problem:
Error extracting content-length causes all future coroutine resumes to
fail.
Solution:
Replace coroutine.wrap with coroutine.create in create_read_loop
so that we can check its status and catch any errors, allowing us to
stop the lsp client and avoid repeatedly resuming the dead coroutine.