<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rneovim.git/test/functional/plugin/lsp/testutil.lua, branch master</title>
<subtitle>Neovim fork with Rahm's personal hacks.</subtitle>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/'/>
<entry>
<title>feat(lsp): fallback to textDocument/formatting from vim.lsp.formatexpr #40079</title>
<updated>2026-08-18T09:16:16+00:00</updated>
<author>
<name>Marcus Caisey</name>
<email>marcus@teckna.com</email>
</author>
<published>2026-08-18T09:16:16+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=d9b4fb1273fd4649593926451121d416154738b1'/>
<id>d9b4fb1273fd4649593926451121d416154738b1</id>
<content type='text'>
Problem:
With the addition of the `:help al` text object, you can now easily
format the whole buffer with `gqal`. However, `vim.lsp.formatexpr` only
uses `textDocument/rangeFormatting` which some language servers (like
gopls) don't support.

Solution:
- Fall back to `textDocument/formatting` if the whole buffer is being formatted
  and the server doesn't support `textDocument/rangeFormatting`.
  - In theory, these two methods should return the same response if the whole
    buffer is being formatted, but I preserved the existing behaviour of
    prioritising `textDocument/rangeFormatting` in case that does not hold (i.e.
    LS bug).
- Also: `vim.lsp.formatexpr` had no tests at all, so actually add tests for it.</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Problem:
With the addition of the `:help al` text object, you can now easily
format the whole buffer with `gqal`. However, `vim.lsp.formatexpr` only
uses `textDocument/rangeFormatting` which some language servers (like
gopls) don't support.

Solution:
- Fall back to `textDocument/formatting` if the whole buffer is being formatted
  and the server doesn't support `textDocument/rangeFormatting`.
  - In theory, these two methods should return the same response if the whole
    buffer is being formatted, but I preserved the existing behaviour of
    prioritising `textDocument/rangeFormatting` in case that does not hold (i.e.
    LS bug).
- Also: `vim.lsp.formatexpr` had no tests at all, so actually add tests for it.</pre>
</div>
</content>
</entry>
<entry>
<title>feat(lsp): use LspNotify for semantic tokens #40224</title>
<updated>2026-06-14T15:10:59+00:00</updated>
<author>
<name>jdrouhard</name>
<email>john@drouhard.dev</email>
</author>
<published>2026-06-14T15:10:59+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=3d6393540e45f993b2e443387ade6a0db52a2ae5'/>
<id>3d6393540e45f993b2e443387ade6a0db52a2ae5</id>
<content type='text'>
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.</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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.</pre>
</div>
</content>
</entry>
<entry>
<title>test(lsp): fix fake LSP server timeout not working (#37970)</title>
<updated>2026-02-19T18:38:14+00:00</updated>
<author>
<name>zeertzjq</name>
<email>zeertzjq@outlook.com</email>
</author>
<published>2026-02-19T18:38:14+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=dec3c6fa346b298e7feb72398f9fdd5d59c11444'/>
<id>dec3c6fa346b298e7feb72398f9fdd5d59c11444</id>
<content type='text'>
Problem:  Fake LSP server does not timeout or respond to SIGTERM as it
          does not run the event loop.
Solution: Instead of io.read(), use stdioopen()'s on_stdin callback to
          accumulate input and use vim.wait() to wait for input.

Also, in the test suite, don't stop a session when it's not running, as
calling uv.stop() outside uv.run() will instead cause the next uv.run()
to stop immediately, which cancels the next RPC request.</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Problem:  Fake LSP server does not timeout or respond to SIGTERM as it
          does not run the event loop.
Solution: Instead of io.read(), use stdioopen()'s on_stdin callback to
          accumulate input and use vim.wait() to wait for input.

Also, in the test suite, don't stop a session when it's not running, as
calling uv.stop() outside uv.run() will instead cause the next uv.run()
to stop immediately, which cancels the next RPC request.</pre>
</div>
</content>
</entry>
<entry>
<title>feat(lsp): skip invalid header lines #36402</title>
<updated>2025-11-17T01:23:52+00:00</updated>
<author>
<name>tao</name>
<email>2471314@gmail.com</email>
</author>
<published>2025-11-17T01:23:52+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=654303079baa9641a6fb50787e7f525ed64e6f40'/>
<id>654303079baa9641a6fb50787e7f525ed64e6f40</id>
<content type='text'>
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 &lt;= len do
    local c = header:byte(i)
    if state == 'name' then
      if c &gt;= 65 and c &lt;= 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 &lt; 48 and c ~= 32 and c ~= 9 or c &gt; 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
https://github.com/neovim/neovim/blob/accd392f4d14a114e378f84dc15cb24bc34a370a/test/functional/plugin/lsp_spec.lua#L1950
and tweaked the boilerplate in two other tests for reusability while keeping the final assertions the same.
https://github.com/neovim/neovim/blob/accd392f4d14a114e378f84dc15cb24bc34a370a/test/functional/plugin/lsp_spec.lua#L5704
https://github.com/neovim/neovim/blob/accd392f4d14a114e378f84dc15cb24bc34a370a/test/functional/plugin/lsp_spec.lua#L5721</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;= len do
    local c = header:byte(i)
    if state == 'name' then
      if c &gt;= 65 and c &lt;= 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 &lt; 48 and c ~= 32 and c ~= 9 or c &gt; 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
https://github.com/neovim/neovim/blob/accd392f4d14a114e378f84dc15cb24bc34a370a/test/functional/plugin/lsp_spec.lua#L1950
and tweaked the boilerplate in two other tests for reusability while keeping the final assertions the same.
https://github.com/neovim/neovim/blob/accd392f4d14a114e378f84dc15cb24bc34a370a/test/functional/plugin/lsp_spec.lua#L5704
https://github.com/neovim/neovim/blob/accd392f4d14a114e378f84dc15cb24bc34a370a/test/functional/plugin/lsp_spec.lua#L5721</pre>
</div>
</content>
</entry>
<entry>
<title>test(lsp): make async format test work properly (#35794)</title>
<updated>2025-09-17T00:21:11+00:00</updated>
<author>
<name>zeertzjq</name>
<email>zeertzjq@outlook.com</email>
</author>
<published>2025-09-17T00:21:11+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=8d5476691cd177ee2166bc8f4c64679292cc7559'/>
<id>8d5476691cd177ee2166bc8f4c64679292cc7559</id>
<content type='text'>
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.</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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.</pre>
</div>
</content>
</entry>
<entry>
<title>feat(lsp): pass resolved config to cmd() #34550</title>
<updated>2025-06-18T11:52:17+00:00</updated>
<author>
<name>Julian Visser</name>
<email>12615757+justmejulian@users.noreply.github.com</email>
</author>
<published>2025-06-18T11:52:17+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=32f30c4874a0fdfbad427b0f4b699638c53f0ccd'/>
<id>32f30c4874a0fdfbad427b0f4b699638c53f0ccd</id>
<content type='text'>
Problem:
In LSP configs, the function form of `cmd()` cannot easily get the
resolved root dir (workspace). One of the main use-cases of a dynamic
`cmd()` is to be able to start a new server  whose binary may be located
*in the workspace* ([example](https://github.com/neovim/nvim-lspconfig/pull/3912)).

Compare `reuse_client()`, which also receives the resolved config.

Solution:
Pass the resolved config to `cmd()`.

Co-authored-by: Justin M. Keyes &lt;justinkz@gmail.com&gt;</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Problem:
In LSP configs, the function form of `cmd()` cannot easily get the
resolved root dir (workspace). One of the main use-cases of a dynamic
`cmd()` is to be able to start a new server  whose binary may be located
*in the workspace* ([example](https://github.com/neovim/nvim-lspconfig/pull/3912)).

Compare `reuse_client()`, which also receives the resolved config.

Solution:
Pass the resolved config to `cmd()`.

Co-authored-by: Justin M. Keyes &lt;justinkz@gmail.com&gt;</pre>
</div>
</content>
</entry>
<entry>
<title>feat(lsp): deprecate non-method client functions</title>
<updated>2024-11-20T08:51:45+00:00</updated>
<author>
<name>Lewis Russell</name>
<email>lewis6991@gmail.com</email>
</author>
<published>2024-11-14T11:53:20+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=454ae672aad172a299dcff7c33c5e61a3b631c90'/>
<id>454ae672aad172a299dcff7c33c5e61a3b631c90</id>
<content type='text'>
Deprecated:
- `client.request()` -&gt; `client:request()`
- `client.request_sync()` -&gt; `client:request_sync()`
- `client.notify()` -&gt; `client:notify()`
- `client.cancel_request()` -&gt; `client:cancel_request()`
- `client.stop()` -&gt; `client:stop()`
- `client.is_stopped()` `client:is_stopped()`
- `client.supports_method()` -&gt; `client:supports_method()`
- `client.on_attach()` -&gt; `client:on_attach()`

Fixed docgen to link class fields to the full function doc.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Deprecated:
- `client.request()` -&gt; `client:request()`
- `client.request_sync()` -&gt; `client:request_sync()`
- `client.notify()` -&gt; `client:notify()`
- `client.cancel_request()` -&gt; `client:cancel_request()`
- `client.stop()` -&gt; `client:stop()`
- `client.is_stopped()` `client:is_stopped()`
- `client.supports_method()` -&gt; `client:supports_method()`
- `client.on_attach()` -&gt; `client:on_attach()`

Fixed docgen to link class fields to the full function doc.
</pre>
</div>
</content>
</entry>
<entry>
<title>test: support upvalues in exec_lua</title>
<updated>2024-09-21T15:04:09+00:00</updated>
<author>
<name>Lewis Russell</name>
<email>lewis6991@gmail.com</email>
</author>
<published>2024-08-11T08:27:48+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=e5c174421df3872df0dd3a676609d1e74dfef6a9'/>
<id>e5c174421df3872df0dd3a676609d1e74dfef6a9</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>feat(lsp): support hostname in rpc.connect #30238</title>
<updated>2024-09-03T15:10:39+00:00</updated>
<author>
<name>Tristan Knight</name>
<email>admin@snappeh.com</email>
</author>
<published>2024-09-03T15:10:39+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=45e76acaa053a077cab49b6606536d3f2646f033'/>
<id>45e76acaa053a077cab49b6606536d3f2646f033</id>
<content type='text'>
Updated the `rpc.connect` function to support connecting to LSP servers
using hostnames, not just IP addresses. This change includes updates to
the documentation and additional test cases to verify the new
functionality.

- Modified `connect` function to resolve hostnames.
- Updated documentation to reflect the change.
- Added test case for connecting using hostname.

Added a TCP echo server utility function to the LSP test suite. This
server echoes the first message it receives and is used in tests to
verify LSP server connections via both IP address and hostname.
Refactored existing tests to use the new utility function.</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Updated the `rpc.connect` function to support connecting to LSP servers
using hostnames, not just IP addresses. This change includes updates to
the documentation and additional test cases to verify the new
functionality.

- Modified `connect` function to resolve hostnames.
- Updated documentation to reflect the change.
- Added test case for connecting using hostname.

Added a TCP echo server utility function to the LSP test suite. This
server echoes the first message it receives and is used in tests to
verify LSP server connections via both IP address and hostname.
Refactored existing tests to use the new utility function.</pre>
</div>
</content>
</entry>
<entry>
<title>test(lsp): refactor and tidy</title>
<updated>2024-08-11T14:14:14+00:00</updated>
<author>
<name>Lewis Russell</name>
<email>lewis6991@gmail.com</email>
</author>
<published>2024-08-11T10:58:15+00:00</published>
<link rel='alternate' type='text/html' href='http://git.josher.dev/rneovim.git/commit/?id=9b5ab66678f8efa1f5fe89205839fe053fe5efaf'/>
<id>9b5ab66678f8efa1f5fe89205839fe053fe5efaf</id>
<content type='text'>
- Merge all the top level 'LSP' describe blocks
- Refactor text edit tests
- Fix typing errors
- Add linebreaks between tests
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- Merge all the top level 'LSP' describe blocks
- Refactor text edit tests
- Fix typing errors
- Add linebreaks between tests
</pre>
</div>
</content>
</entry>
</feed>
