Commit Graph

659 Commits

Author SHA1 Message Date
Evgeni Chasnovski
f492f62c3d fix(pack): rename confirmation buffer to again use nvim-pack:// scheme
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>`.
2025-11-17 12:47:29 +02:00
Evgeni Chasnovski
b151aa761f feat(pack)!: synchronize lockfile with installed plugins when reading it
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
2025-11-17 12:47:29 +02:00
Evgeni Chasnovski
c3ac329c7a fix(pack)!: ensure plugin is fully absent if not fully installed
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.
2025-11-17 12:47:29 +02:00
Evgeni Chasnovski
9e2599df05 fix(pack)!: adjust install confirm (no error on "No", show names)
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).
2025-11-17 12:46:22 +02:00
tao
654303079b feat(lsp): skip invalid header lines #36402
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)
2025-11-16 17:23:52 -08:00
Evgeni Chasnovski
2e533e364e feat(pack): update add() to handle source change for installed plugin
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.
2025-11-16 22:19:10 +02:00
Evgeni Chasnovski
a39171f532 fix(pack)!: make default opts.load in add() to work inside 'plugin/'
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`.
2025-11-16 22:19:10 +02:00
Justin M. Keyes
72110da567 Merge #36468 from echasnovski/pack-consistency
`vim.pack` consistency improvements: full hashes, state->revision, buffer URI name
2025-11-13 02:00:05 -05:00
Toby She
4dd9137215 fix(lsp): reuse_win prioritizes windows/tabs currently displayed #36486
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
2025-11-12 20:43:25 -08:00
Olivia Kinnear
7c9b865bdd feat(lsp): deprecate vim.lsp.stop_client (#36459)
* feat(lsp): deprecate `vim.lsp.stop_client`

* fix(tests): fix nil variable in diagnostic_spec.lua
2025-11-10 18:27:13 -08:00
Yochem van Rosmalen
9bdb011a50 refactor(spellfile): config() interface, docs #36481
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|.
2025-11-09 21:51:39 -08:00
Riley Bruins
c2c5a0297e 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.
2025-11-09 17:49:25 -08:00
Evgeni Chasnovski
eff01b7620 fix(pack): use more correct URI for confirmation buffer name
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.
2025-11-06 20:13:17 +02:00
Evgeni Chasnovski
ee3239fcb6 fix(pack): consistently use "revision" instead of "state"
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".
2025-11-06 19:54:57 +02:00
Evgeni Chasnovski
f3f5095630 fix(pack): use full hashes in lockfile and revision description
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).
2025-11-06 18:42:36 +02:00
Eduardo Cruz Guedes
5e039c8e97 fix(tutor): remove hyperlinks, simplify non-interactive examples #36307
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.
2025-10-27 10:24:00 -07:00
tao
a768d0a95b fix(lsp): stop repeatedly resuming dead coroutine #35743
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.
2025-10-27 10:03:45 -07:00
Justin M. Keyes
4d501c93bb refactor(spell): cleanup
- prefer `stdpath(data)/site/spell` instead of looking for random dirs in 'runtimepath'.
- drop unused functions `choose_directory`, `setup`, etc.
2025-10-26 01:28:37 +02:00
Evgeni Chasnovski
16a6559ec6 fix(pack)!: do not trigger PackChanged[Pre] kind=update during install
Problem: `PackChanged[Pre]` events with `kind=update` are triggered both
  during plugin's initial installation and after already installed
  plugin was updated.

  It was a deliberate decision to allow writing only a single update
  hook to act as a dedicated "build" entry point (like execute `make` or
  `cargo build —release`). This mimics how other plugin managers have a
  single "build" command.

  This was a result of 'mini.deps' experience with the different
  approach: "update" hooks are not run during install. This proved to be
  confusing as it requires to write two hooks. But also the reason might
  be that 'mini.deps' names it "checkout" hook instead of "update".

  However, the `vim.pack` event approach makes it lower cost to handle
  separate "update" and "install" events. Something like
  `if ev.data.kind == 'install' or ev.data.kind == 'update' then`
  instead of two autocommands.
  Plus this makes clearer separation of events.

Solution: do not trigger `PackChanged[Pre] kind=update` event during
  install.
2025-10-23 20:28:09 +03:00
Evgeni Chasnovski
a9db6ec6fa feat(pack): add active field to PackChanged event data
Problem: Inside `PackChanged[Pre]` callbacks it might be useful to tell
  if the affected plugin is active or not. It is already possible via
    extra `vim.pack.get({ 'plug-name' })[1].active`, but it is not quite
    user-friendly for something that might be needed frequently in real
    world use cases.

Solution: Supply extra `active` event data field.
2025-10-23 17:50:03 +03:00
Evgeni Chasnovski
c732133efe test(pack): refactor 'pack_spec.lua' with better code
Problem: 'pack_spec.lua' test file's code can be improved.

Solution: Refactor some aspects. In particular:
  - Rewrite `find_in_log` for finding event data in event log into a
    generator function that from log list returns a finder function.
    This allows it to take less arguments and be more concise.

  - Consistently use `local function f()` instead of
    `local f = function()`.

  - Prefer to use `fn.readblob()` instead of `fn.readfile()` to assert
    text from a file.

  - Use `([[...]]):format()` approach to testing file content (instead
    of array of strings). Should improve readability.

  - Universally prefer using "assert" to mean "check if certain
    expectation about the process holds up" (instead of occasional
    "validate").
2025-10-23 17:38:35 +03:00
Till Bungert
b67eff38fe fix(lsp): deduplicate completion items #36166
The current implementation has a race condition where items are appended
to the completion list twice when a second completion runs while the
first is still going. This hotfix just deduplicates the entire list.

Co-authored-by: Tomasz N <przepompownia@users.noreply.github.com>
2025-10-20 15:45:37 -07:00
Mathias Fußenegger
cdc3702f8d revert "fix(lsp): _get_workspace_folders does not handle root_dir() function" #36183
This reverts commit 97ab24b9c7.

See discussion in https://github.com/neovim/neovim/pull/36071
2025-10-14 19:26:01 -07:00
Justin M. Keyes
bf4710d8c3 fix(lsp): "attempt to index nil config" #36189
Problem:
If a client doesn't have a config then an error may be thrown.
Probably caused by: 2f78ff816b

    Lua callback: …/lsp.lua:442: attempt to index local 'config' (a nil value)
    stack traceback:
            …/lsp.lua:442: in function 'can_start'
            …/lsp.lua:479: in function 'lsp_enable_callback'
            …/lsp.lua:566: in function <…/lsp.lua:565>

Solution:
Not all clients necessarily have configs.
- Handle `config=nil` in `can_start`.
- If user "enables" an invalid name that happens to match a *client*
  name, don't auto-detach the client.
2025-10-15 00:43:25 +00:00
Tomas Slusny
c6113da5a9 fix(difftool): fully resolve symlinks when comparing paths #36147
Fixes issue on mac where it was constantly reloading buffers as paths
were not being normalized and resolved correctly (in relation to buffer
name).

Quickfix entry:
/var/folders/pt/2s7dzyw12v36tsslrghfgpkr0000gn/T/git-difftool.m95lj8/right/app.vue

Buffer name:
/private/var/folders/pt/2s7dzyw12v36tsslrghfgpkr0000gn/T/git-difftool.m95lj8/right/app.vue

/var was synlinked to /private/var and this was not being properly
handled.

Also added lazy redraw to avoid too many redraws when this happens in
future and added test for symlink handling.

Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
2025-10-12 18:25:14 +00:00
Tomas Slusny
c9b74f8b7e fix(difftool): ensure split layout, use systemlist #36145
- Always open the right window to the right, regardless of 'splitright'
  setting, ensuring the left window is always leftmost.
- Use `vim.fn.systemlist` for diffr output to avoid manual splitting.
- Add a test to verify window layout consistency with 'splitright' and
  'nosplitright' options.
- Escape quotes in git difftool example properly.

Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
2025-10-12 05:11:27 +00:00
Vlad
2ea7333f64 test(plugin/pack_spec): handle pcall path truncation #36143
Problem

In `vim.pack` the source of any errors is included in the `update`
buffer from lua's `pcall` method. Since the full path is known it is
replaced in the unit test by the string `VIM_PACK_RUNTIME`. The issue is
that `pcall` does not necessarily include the full path, it instead uses
the `lua_Debug` `short_src` value which can be truncated. This means
depending on where you've cloned the repo locally the test can fail.

Solution

Change the replacement pattern for the traceback to be more generic and
handle any path prefix, not just the value of `vim.env.VIMRUNTIME`.
2025-10-12 03:23:19 +00:00
Tomas Slusny
fec02ae8e4 feat(plugins): nvim.difftool can compare directories #35448
Problem:
Built-in diff mode (nvim -d) does not support directory diffing
as required by git difftool -d. This makes it difficult to compare
entire directories, detect renames, and navigate changes efficiently.

Solution:
Add a DiffTool plugin and command that enables side-by-side diffing of
files and directories in Neovim. The plugin supports rename detection,
highlights changes in the quickfix list, and provides a user command for
easy invocation. This allows proper integration with git difftool -d for
directory comparison.

Example git config:

```ini
[diff]
    tool = nvim_difftool

[difftool "nvim_difftool"]
    cmd = nvim -c "packadd nvim.difftool" -c "DiffTool $LOCAL $REMOTE"
```

Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
Co-authored-by: Phạm Bình An <111893501+brianhuster@users.noreply.github.com>
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
2025-10-11 19:24:39 -07:00
atusy
97ab24b9c7 fix(lsp): _get_workspace_folders does not handle root_dir() function #36071
* fix(lsp): type of root_dir should be annotated with string|fun|nil
* feat(lsp): support root_dir as function in _get_workspace_folders
* feat(lsp): let checkhealth support root_dir() function

Examples:

    vim.lsp: Active Clients ~
    - lua_ls (id: 1)
      - Version: <Unknown>
      - Root directories:
          ~/foo/bar
          ~/dev/neovim

Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
2025-10-11 16:01:05 -07:00
Vlad
f0b9232ad8 test(plugin/shada_spec): always use UTC formatted date (#36128)
Problem

Similar to https://github.com/neovim/neovim/pull/33257, but for the date
component. When timezone is behind UTC the epoch date of individual
components mismatch causing the test to fail.

Solution

The `epoch` variable is created using `os.date` with a `!`, which means
format the date in UTC instead of local timezone:
https://www.lua.org/manual/5.1/manual.html#pdf-os.date.

Since the individual components like year, month, and day are expected
to match `epoch` they should all be created using a `!` as well.
2025-10-11 06:03:02 +00:00
phanium
9c89212de1 fix(undotree): sync scroll pos with undo #36117
Problem: when undo in buffer, undotree window is not updated to
position of correct node

Solution: schedule nvim_win_set_cursor
2025-10-10 07:07:01 -07:00
Mickaël RAYBAUD-ROIG
8e740db70c fix(vim.pack): skip checkout on bad version #36038
Refs: #36037
2025-10-09 13:40:26 -07:00
altermo
9e1d3f4870 feat(runtime): undotree #35627
Problem
No builtin way to visualize and navigate the undo-tree.

Solution
Include an "opt" plugin.
2025-10-07 14:32:22 -07:00
Evgeni Chasnovski
3b860653ca fix(pack): handle lockfile in case of install errors #36064
Problem: If plugin was intended to install but there were errors (like
  if there is a typo in `src`), lockfile still includes its entry.
  This leads to all source of problems (like not correct `get()` output,
  not working `update()`, etc.).

Solution: Explicitly account for plugins that were not installed.
  Alternative solution might be to write a safe
  `lock_set(plug, field, value)` wrapper (which sets field for a correct
  `plugins` entry in the lockfile Lua table) and use it after install
  to detect the change in `version`. However, this always requires
  an extra pass through plugins on every startup, which is suboptimal.
  Optimizing for the "happy path" should be a priority in `add()`.
2025-10-07 12:24:23 -07:00
Evgeni Chasnovski
98e3a571dd feat(pack): add code actions in confirmation buffer
Problem: No way to granularly operate on plugins when inside
confirmation buffer.

Solution: Implement code actions for in-process LSP that act on "plugin
at cursor":
  - Update (if has updates).
  - Skip updating (if has updates).
  - Delete.

  Activate via default `gra` or `vim.lsp.buf.code_action()`.
2025-10-05 19:20:06 +03:00
Evgeni Chasnovski
2728b4efe0 feat(pack): add [[ and ]] mappings in confirmation buffer
Problem: No easy/robust way to jump between plugin sections.

Solution: Add `[[` and `]]` mappings.
2025-10-05 19:20:05 +03:00
Maria Solano
b938638d2d fix(lsp): deprecate vim.lsp.protocol.Methods (#35998) 2025-10-04 21:09:13 -07:00
Evgeni Chasnovski
dc8235c48c feat(pack): prefer using revision from lockfile during install
Problem: Installing plugin always pulls latest `version` changes
  (usually from the default branch or "latest version tag"). It is more
  robust to prefer initial installation to use the latest recorded
  (i.e. "working") revision.

Solution: Prefer using revision from the lockfile (if present) during
  install. The extra `update()` will pull the latest changes.
2025-10-04 16:15:54 +03:00
Evgeni Chasnovski
cfbc03a954 feat(pack)!: make update() include not active plugins by default
Problem: Running `update()` by default doesn't include not active
  plugins, because there was no way to get relevant `version` to get
  updates from. This might be a problem in presence of lazy loaded
  plugins, i.e. ones that can be "not *yet* active" but still needed to
  be updated.

Solution: Include not active plugins by default since their `version` is
  tracked via lockfile.
2025-10-04 16:13:39 +03:00
Evgeni Chasnovski
a31f63f661 feat(pack)!: update get() to return revision regardless of opts.info
Problem: The revision data is returned behind `opts.info` flag because
  it required extra Git calls. With lockfile it is not the case.

Solution: Use lockfile to always set `rev` field in output of `get()`.
2025-10-04 16:13:38 +03:00
Evgeni Chasnovski
2c0b70e559 fix(pack)!: use lockfile in get() for data about non-active plugins
Problem: `get()` doesn't return `spec.version` about not-yet-active
  plugins (because there was no way to know that without `add()`).

Solution: Use lockfile data to set `spec.version` of non-active plugins.
2025-10-04 16:13:36 +03:00
Evgeni Chasnovski
d7db552394 feat(pack): add initial lockfile tracking
Problem: Some use cases require or benefit from persistent on disk
  storage of plugin data (a.k.a. "lockfile"):

  1. Allow `update()` to act on not-yet-active plugins. Currently if
    `add()` is not yet called, then plugin's version is unknown
    and `update()` can't decide where to look for changes.

  2. Efficiently know plugin's dependencies without having to read
    'pkg.json' files on every load for every plugin. This is for the
    future, after there is `packspec` support (or other declaration of
    dependencies on plugin's side).

  3. Allow initial install to check out the exact latest "working" state
    for a reproducible setup. Currently it pulls the latest available
    `version.`

  4. Ensure that all declared plugins are installed, even if lazy loaded.
    So that later `add()` does not trigger auto-install (when there
    might be no Internet connection, for example) and there is no issues
    with knowing which plugins are used in the config (so even never
    loaded rare plugins are still installed and can be updated).

  5. Allow `add()` to detect if plugin's spec has changed between
    Nvim sessions and act accordingly. I.e. either set new `src` as
    origin or enforce `version.` This is not critical and can be done
    during `update()`, but it might be nice to have.

Solution: Add lockfile in JSON format that tracks (adds, updtes,
  removes) necessary data for described use cases. Here are the required
  data that enables each point:

    1. `name` -> `version` map.
    2. `name` -> `dependencies` map.
    3. `name` -> `rev` map. Probably also requires `name` -> `src` map
       to ensure that commit comes from correct origin.
    4. `name` -> `src` map. It would be good to also track the order,
       but that might be too many complications and redundant together
       with point 2.
    5. Map from `name` to all relevant spec fields. I.e. `name` -> `src`
       and `name` -> `version` for now. Storing data might be too much,
       but can be discussed, of course.

  This commit only adds lockfile tracking without implementing actual
  use cases. It is stored in user's config directory and is suggested to
  be tracked via version control.

  Example of a lockfile:

  ```json
  {
    # Extra nesting to more future proof.
    "plugins": {
      "plug-a": {
        "ref": "abcdef1"
        "src": "https://github.com/user/plug-a",
        # No `version` means it was `nil` (infer default branch later)
      },
      "plug-b": {
        "dependencies": ["plugin-a", "plug-c"],
        "src": "https://github.com/user/plug-b",
        "ref": "bcdefg2",
        # Enclose string `version` in quotes
        "version": "'dev'"
      },
      "plug-c": {
        "src": "https://github.com/user/plug-c",
        "ref": "cdefgh3",
        # Store `vim.version.Range` via its `tostring()` output
        "version": ">=0.0.0",
      }
    }
  }
  ```
2025-10-04 16:13:32 +03:00
zeertzjq
eb7c12d3e3 test: remove a few more redundant clear() calls (#35903) 2025-09-24 14:25:54 +08:00
zeertzjq
a62fb57e58 test: don't call clear() in both before_each() and after_each() (#35901) 2025-09-24 11:41:43 +08:00
zeertzjq
777dafdc46 test: reduce some clear() calls
Use only a single clear() call in some test/functional/vimscript/ test
files whose test cases have very little side effect.

A downside of using a single clear() is that if a crash happens in one
test case, all following test cases in the same file will also fail, but
these functionalities and tests don't change very often.
2025-09-22 14:03:42 +08:00
Tomasz N
e69b81ad94 fix(lsp): treat 2-triggers-at-once as "last char wins" #35435
Problem:
If there are 2 language servers with different trigger chars (`-` and
`>`), and a keymap inputs both simultaneously (`->`), then `>` doesn't
trigger. We get completion items from server1 only.

This happens because the `completion_timer` for the `-` trigger is still
pending.

Solution:
If the next character arrived enough quickly (< 25 ms), replace the
existing deferred autotrigger with a new one that matches this later
character.
2025-09-17 09:07:45 -07:00
zeertzjq
8d5476691c test(lsp): make async format test work properly (#35794)
Overriding vim.lsp.handlers['textDocument/formatting'] doesn't work here
because fake_lsp_server_setup() uses a table with __index to specify
client handlers, which takes priority over vim.lsp.handlers[], and as a
result the overridden handler is never called, and the test ends before
the vim.wait() even finishes.

Instead, set a global variable from the handler that is actually reached
(by vim.rpcrequest() from client handler), and avoid stopping the event
loop too early.
2025-09-17 08:21:11 +08:00
zeertzjq
2debe2f30a fix(lsp): avoid automatic request after leaving insert mode (#35767)
This also fixes the following warning in tests with ASAN or TSAN:

    -------- Running tests from test/functional/plugin/lsp/inline_completion_spec.lua
    RUN      T4604 vim.lsp.inline_completion enable() requests or abort when entered/left insert mode: 225.00 ms OK
    RUN      T4605 vim.lsp.inline_completion get() applies the current candidate: 212.00 ms OK
    nvim took 2013 milliseconds to exit after last test
    This indicates a likely problem with the test even if it passed!

    RUN      T4606 vim.lsp.inline_completion get() accepts on_accept callback: 212.00 ms OK
    RUN      T4607 vim.lsp.inline_completion select() selects the next candidate: 220.00 ms OK
    -------- 4 tests from test/functional/plugin/lsp/inline_completion_spec.lua (3437.00 ms total)

    -------- Running tests from test/functional/plugin/lsp/linked_editing_range_spec.lua
    nvim took 2011 milliseconds to exit after last test
    This indicates a likely problem with the test even if it passed!
2025-09-15 14:41:29 +08:00
Justin M. Keyes
2f78ff816b fix(lsp): misleading logs in non-applicable filetypes #35749
Problem:
LSP logs show misleading "cannot start" messages when editing a filetype
NOT listed in the `config.filetypes` field.

    [ERROR][2025-09-13 18:55:56] …/runtime//lua/vim/lsp/log.lua:151
    "cannot start cssls due to config error: …/runtime//lua/vim/lsp.lua:423:
    cmd: expected expected function or table with executable command,
    got table: 0x0104701b18. Info: vscode-css-language-server is not executable"

Solution:
- `can_start`: check `config.filetypes` before checking the rest of the
  config.
2025-09-13 18:51:06 -07:00
Evgeni Chasnovski
a41703d107 feat(pack): confirm "Always" to install all plugins #35733
Problem: First clean start with config containing multiple
  `vim.pack.add()` calls requires to explicitly confirm each one.
  Although usually a rare occurrence, it still might be tedious.

Solution: Add a third choice during installation confirmation that
  approves current and all next installs within current session. It is
  reset after session restart.
2025-09-13 13:32:09 -07:00