Skip to content

LSP hover provider drops content when result.contents is a single MarkupContent object #21

Description

@lukeorland

Description

hover.lua's content extraction logic silently drops the actual hover text when an LSP server returns result.contents as a single MarkupContent object ({kind = "markdown", value = "..."}) rather than an array of MarkedString. This is a very common shape — e.g. pyright returns exactly this for stdlib symbols.

https://github.com/patrickpichler/hovercraft.nvim/blob/main/lua/hovercraft/provider/lsp/hover.lua#L86-L93

local res = result.contents

if type(res) == "table" and #res > 0 then
  vim.list_extend(contents, res)
elseif (type(res) == "table" and res.value) or type(res) then
  contents[#contents + 1] = res
end

The bug is in the second branch's condition: type(res) on a Lua table always returns the string "table", which is truthy. This means (type(res) == "table" and res.value) or type(res) is unconditionally true whenever res is a table, regardless of whether res.value exists. As a result, contents[#contents + 1] = res pushes the whole table (e.g. {kind="markdown", value="```python\n...\n```"}) as a single "content line," rather than res.value (the actual markdown text).

Downstream in ui.lua, vim.lsp.util.convert_input_to_markdown_lines expects array items to be strings or MarkedString-shaped ({language=, value=}) — not MarkupContent-shaped ({kind=, value=}) — so the real text is silently dropped. The popup ends up rendering nothing but whatever fallback/footer UI hovercraft draws (in my case, just a language icon with no code/text beneath it).

Steps to reproduce

  1. Use pyright (or any server returning a single non-array MarkupContent) as the LSP client
  2. Hover (K) over a stdlib symbol with a minimal hover response, e.g. time.monotonic
  3. Confirm via raw LSP request that the server is returning content:
    vim.lsp.buf_request(0, 'textDocument/hover', vim.lsp.util.make_position_params(0, 'utf-16'), function(err, result)
      vim.print(result)
    end)
    This returns:
    {
      contents = {
        kind = "markdown",
        value = "```python\n(function) def monotonic() -> float\n```"
      },
      ...
    }
  4. Observe hovercraft's popup shows no text for this hover, despite the LSP response containing content.

Expected

The signature/docstring text should render in the popup.

Suggested fix

local res = result.contents

if type(res) == "table" and #res > 0 then
  vim.list_extend(contents, res)
elseif type(res) == "table" and res.value then
  vim.list_extend(contents, vim.split(res.value, '\n', { trimempty = true }))
elseif type(res) == "string" then
  contents[#contents + 1] = res
end

This correctly extracts res.value and splits it into lines instead of pushing the raw table. Verified locally that this fixes the missing-content issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions