fix(lsp): incorrect rendering of markdown codeblock #40861

Problem:
LSP hover erroneously drops blank lines before a 4-space-indented
codeblock, which is not valid Markdown. This causes incorrect parsing
and wrong display.

Solution:
Fix `split_lines` so that it doesn't drop the blank line just before
a 4-space-indented codeblock.

fix https://github.com/neovim/neovim/issues/40860

(cherry picked from commit 317c5ddda6)
This commit is contained in:
Justin M. Keyes
2026-07-20 10:03:11 -04:00
committed by github-actions[bot]
parent 37d7b11bc1
commit ba2ea67a1f
2 changed files with 21 additions and 4 deletions

View File

@@ -92,18 +92,29 @@ end
---
--- @param s string Multiline string
--- @param no_blank boolean? Drop blank lines for each @param/@return (except one empty line
--- separating each). Workaround for https://github.com/LuaLS/lua-language-server/issues/2333
--- separating each, and before a codeblock).
--- Workaround for https://github.com/LuaLS/lua-language-server/issues/2333
local function split_lines(s, no_blank)
s = string.gsub(s, '\r\n?', '\n')
local raw = vim.split(s, '\n', { plain = true, trimempty = true })
--- @return boolean # true if line begins a (4-space-indented) codeblock. #40860
local function codeblock(l)
return l:find('^ ') or l:find('^\t')
end
local lines = {}
local in_desc = true -- Main description block, before seeing any @foo.
for line in vim.gsplit(s, '\n', { plain = true, trimempty = true }) do
for i, line in ipairs(raw) do
local start_annotation = not not line:find('^ ?%@.?[pr]')
in_desc = (not start_annotation) and in_desc or false
if start_annotation and no_blank and not (lines[#lines] or ''):find('^%s*$') then
table.insert(lines, '') -- Separate each @foo with a blank line.
end
if in_desc or not no_blank or not line:find('^%s*$') then
local blank = line:find('^%s*$')
local keep_blank = blank and codeblock(raw[i + 1] or '') -- Keep if it precedes a codeblock. #40860
if in_desc or not no_blank or not blank or keep_blank then
table.insert(lines, line)
end
end

View File

@@ -1209,7 +1209,7 @@ describe('vim.lsp.util', function()
local r = exec_lua(function()
local hover_data = {
kind = 'markdown',
value = '```lua\nfunction vim.api.nvim_buf_attach(buffer: integer, send_buffer: boolean, opts: vim.api.keyset.buf_attach)\n -> boolean\n```\n\n---\n\n Activates buffer-update events. Example:\n\n\n\n ```lua\n events = {}\n vim.api.nvim_buf_attach(0, false, {\n on_lines = function(...)\n table.insert(events, {...})\n end,\n })\n ```\n\n\n @see `nvim_buf_detach()`\n @see `api-buffer-updates-lua`\n@*param* `buffer` — Buffer handle, or 0 for current buffer\n\n\n\n@*param* `send_buffer` — True if whole buffer.\n Else the first notification will be `nvim_buf_changedtick_event`.\n\n\n@*param* `opts` — Optional parameters.\n\n - on_lines: Lua callback. Args:\n - the string "lines"\n - buffer handle\n - b:changedtick\n@*return* — False if foo;\n\n otherwise True.\n\n@see foo\n@see bar\n\n',
value = '```lua\nfunction vim.api.nvim_buf_attach(buffer: integer, send_buffer: boolean, opts: vim.api.keyset.buf_attach)\n -> boolean\n```\n\n---\n\n Activates buffer-update events. Example:\n\n\n\n ```lua\n events = {}\n vim.api.nvim_buf_attach(0, false, {\n on_lines = function(...)\n table.insert(events, {...})\n end,\n })\n ```\n\n\n @see `nvim_buf_detach()`\n @see `api-buffer-updates-lua`\n@*param* `buffer` — Buffer handle, or 0 for current buffer\n\n\n\n@*param* `send_buffer` — True if whole buffer.\n Else the first notification will be `nvim_buf_changedtick_event`.\n\n\n@*param* `opts` — Optional parameters.\n\n - on_lines: Lua callback. Args:\n - the string "lines"\n - buffer handle\n - b:changedtick\n@*return* — False if foo;\n\n otherwise True.\n\n@see foo\n@see bar\n\nExample:\n\n iex> params = %{title: "", topics: []}\n\n iex> changeset.changes[:topics]\n',
}
return vim.lsp.util.convert_input_to_markdown_lines(hover_data)
end)
@@ -1256,6 +1256,12 @@ describe('vim.lsp.util', function()
' otherwise True.',
'@see foo',
'@see bar',
-- Blank line preceding a 4-space-indented codeblock is required, per Markdown spec. #40860
'Example:',
'',
' iex> params = %{title: "", topics: []}',
'',
' iex> changeset.changes[:topics]',
}
eq(expected, r)
end)