I am using pyright LSP with the neovim (0.10.0-dev), and am seeing any leading spaces in docstrings replaced by  . For example, see the attached image:

enter image description here

As far as I can tell it seems very similar to this issue: https://github.com/neovim/nvim-lspconfig/issues/936 . Unfortunately, from that issue I can't determine how the problem was rectified. I'm already using nvim-cmp (the successor to nvim-compe), so am assuming that this isn't the problem.

I am using mason, and mason-lspconfig, and am initialising pyright as follows. (And calling setup with purely default arguments gives the same result.)

require("lspconfig").pyright.setup {
   settings = {
      pyright = {
         -- Use Ruff for import organisation
         disableOrganizeImports = true,

So far I've tried, without success, to

  • find a way to tell pyright LSP to return plaintext rather than HTML format
  • find a way to intercept the string that neovim receives from the LSP, and use a lua function to replace " " with " " before it gets rendered (which would be VERY hacky but better than nothing!)

Does someone know any likely causes of this issue?

1 Answer 1


I think this is the quirky behavior of pyright. The docstring parsing of pyright is not that good. When Neovim used markdown_inline and markdown as their default hover parser, the hover result got even worse from pyright. Other languages works quite well, though.

The following code is how I solve this. My config.

local util = require("vim.lsp.util")
-- The function that replace those quirky html symbols.
local function split_lines(value)
  value = string.gsub(value, " ", " ")
  value = string.gsub(value, ">", ">")
  value = string.gsub(value, "&lt;", "<")
  value = string.gsub(value, "\\", "")
  value = string.gsub(value, "```python", "")
  value = string.gsub(value, "```", "")
  return vim.split(value, "\n", { plain = true, trimempty = true })

-- The function name is the same as what you found in the neovim repo.
-- I just remove those unused codes.
-- Actually, this function doesn't "convert input to markdown".
-- I just keep the function name the same for reference.
local function convert_input_to_markdown_lines(input, contents)
  contents = contents or {}
  assert(type(input) == "table", "Expected a table for LSP input")
  if input.kind then
    local value = input.value or ""
    vim.list_extend(contents, split_lines(value))
  if (contents[1] == "" or contents[1] == nil) and #contents == 1 then
    return {}
  return contents

-- The overwritten hover function that pyright uses.
-- Note that other language server can use the default one.
local function hover(_, result, ctx, config)
  config = config or {}
  config.focus_id = ctx.method
  if vim.api.nvim_get_current_buf() ~= ctx.bufnr then
    -- Ignore result since buffer changed. This happens for slow language servers.
  if not (result and result.contents) then
    if config.silent ~= true then
      vim.notify("No information available")
  local contents ---@type string[]
  contents = convert_input_to_markdown_lines(result.contents)
  if vim.tbl_isempty(contents) then
    if config.silent ~= true then
      vim.notify("No information available")
  -- Notice here. The "plaintext" string was originally "markdown".
  -- The reason of using "plaintext" instead of "markdown" is becasue
  -- of the bracket characters ([]). Markdown will hide single bracket,
  -- so when your docstrings consist of numpy or pytorch or python list,
  -- you will get garbadge hover results.
  -- The bad side of "plaintext" is that you never get syntax highlighting.
  -- I personally don't care about this.
  return util.open_floating_preview(contents, "plaintext", config)
  handlers = {
    -- I use ruff for diagnostics. So I disalbe in pyright.
    ["textDocument/publishDiagnostics"] = function() end,
    -- The actual hover function assignment.
    ["textDocument/hover"] = vim.lsp.with(hover, {
      border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" },
      title = " |・ω・) ? ",
      max_width = 120,
      zindex = 500,
  -- Nothing related to hover. You can ignore them.
  settings = {
    pyright = {
      disableOrganizeImports = false,
    python = {
      analysis = {
        ignore = { "*" },
        logLevel = "Information",
        autoImportCompletions = true,
        autoSearchPaths = true,
        diagnosticMode = "off",
        typeCheckingMode = "off",
        useLibraryCodeForTypes = false,

Here's the screenshot of the results: enter image description here enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.