Skip to content

Invalid snippet syntax causes failures in neovim text editor #4363

@rmullin7286

Description

@rmullin7286

Your environment

Which OS do you use?

MacOS and Arch Linux, the issue appears on both.

Which version of GHC do you use and how did you install it?

GHC 9.10.1 using ghcup

How is your project built (alternative: link to the project)?
My main reproduction case is a simple haskell script with embedded cabal metadata:

#!/usr/bin/env cabal
{- cabal:
    build-depends: turtle, base
-}

main :: IO ()
main = putStrLn "foo"

However, the issue also appears in full cabal projects.

Which LSP client (editor/plugin) do you use?

neovim 0.10.0 using the haskell-tools plugin, lsp-config and nvim-cmp.
My configuration can be found here.

Which version of HLS do you use and how did you install it?

HLS 2.9.0.1 using ghcup

Have you configured HLS in any way (especially: a hie.yaml file)?
No

Steps to reproduce

  1. Create a new file, test.hs
  2. Start typing the code provided in the above example.

Expected behaviour

Autocompletion should work correctly

Actual behaviour

At some point, every time a character is typed, the following error message appears:

Error executing vim.schedule lua callback: ...local/share/nvim/lazy/nvim-cmp/lua/cmp/utils/snippet.lua:422
: snippet parsing failed
stack traceback:
        [C]: in function 'error'
        ...local/share/nvim/lazy/nvim-cmp/lua/cmp/utils/snippet.lua:422: in function 'parse'
        ...s/ryan/.local/share/nvim/lazy/nvim-cmp/lua/cmp/entry.lua:130: in function 'callback'
        .../.local/share/nvim/lazy/nvim-cmp/lua/cmp/utils/cache.lua:38: in function 'get_word'
        ...s/ryan/.local/share/nvim/lazy/nvim-cmp/lua/cmp/entry.lua:81: in function 'callback'
        .../.local/share/nvim/lazy/nvim-cmp/lua/cmp/utils/cache.lua:38: in function 'get_offset'
        .../ryan/.local/share/nvim/lazy/nvim-cmp/lua/cmp/source.lua:353: in function ''
        vim/_editor.lua: in function <vim/_editor.lua:0>

Debug information

I've spent a bit of time diving into this issue and I think I understand what the root cause is. There's a few components at work:

  1. nvim-cmp has recently updated their codebase to perform validation on code snippets: see this commit
  2. I modified the error message of nvim-cmp to see what the snippet was that it was failing to parse. The snippet it showed was $!
  3. Given that $! is a standard operator in Haskell, I began to suspect that the issue is actually coming from HLS
  4. Sure enough, looking at the debug logs for the lsp, I see this, in the response from method "textDocument/completion". I've only copied the relevant bit:
 itemName = { "GHC.Internal.Base", "ghc-internal", "v", "$!" },            itemNeedsType = true          }        },        
detail = "from Prelude",        documentation = {          kind = "markdown",          value = "*Imported from 'Prelude'*\n"        },
 insertText = "$!",        insertTextFormat = 2,        kind = 3,        label = "$!",        sortText = "01"      }, {        data = { 
resolvePlugin = "ghcide-completions",          resolveURI = "file:///Users/ryan/test3.hs",          resolveValue = {            itemFile =
"file:///Users/ryan/test3.hs",            itemName = { "GHC.Internal.Base", "ghc-internal", "v", "." },            itemNeedsType = true          }
},        detail = "from Prelude",        documentation = {          kind = "markdown",          value = "*Imported from 'Prelude'*\n"        }, 

So HLS is responding with a completion item of kind Snippet, and with an insertText of "$!".
5. Here's the documentation for the snippet syntax specification from vscode, which nvim-cmp also uses. Specifically, the section describing how $ characters must be escaped using \, otherwise they will be interpreted as a placeholder variable: link.
6. Looking at the HLS source code, specifically ghcide/src/Development/IDE/Plugin/Completions/Logic.hs line 210:

  let ci = CompletionItem
                 {_label = label,
                  _kind = kind,
                  _tags = Nothing,
                  _detail =
                      case (typeText, provenance) of
                          (Just t,_) | not(T.null t) -> Just $ ":: " <> t
                          (_, ImportedFrom mod)      -> Just $ "from " <> mod
                          (_, DefinedIn mod)         -> Just $ "from " <> mod
                          _                          -> Nothing,
                  _documentation = documentation,
                  _deprecated = Nothing,
                  _preselect = Nothing,
                  _sortText = Nothing,
                  _filterText = Nothing,
                  _insertText = Just insertText,
                  _insertTextFormat = Just InsertTextFormat_Snippet,
                  _insertTextMode = Nothing,
                  _textEdit = Nothing,
                  _additionalTextEdits = Nothing,
                  _commitCharacters = Nothing,
                  _command = mbCommand,
                  _data_ = toJSON <$> fmap (CompletionResolveData uri (isNothing typeText)) nameDetails,
                  _labelDetails = Nothing,
                  _textEditText = Nothing}

It appears that the language server hardcodes every response to be of kind Snippet, and does not use PlainText responses, except in the case that snippets are disabled or the symbol being types is infix (e.g. foo `on` bar).

  1. All put together, since $ is a valid character in Haskell but is not handled or escaped by HLS, it responds with invalid snippets, and nvim-cmp correctly fails.

There's two potential solutions here:

  1. Continue returning all responses as snippets, but ensure that all $ characters outside of actual snippet placeholders are escaped as \$
  2. Tag non-snippet items as _insertTextFormat = InsertTextFormat_PlainText. Save InsertTextFormat_Snippet for actual snippet items.

Currently the only workaround is to disable snippets in the language server, or role back to a version of nvim-cmp that doesn't perform validation, otherwise the functionality will be broken. The following code in init.lua should work as a temporary fix:

vim.g.haskell_tools = {
    hls = {
        settings = {
            plugin = {
                ['ghcide-completions'] = {
                    config = {
                        snippetsOn = false,
                        autoExtendOn = true
                    }
                }
            }
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: ghcidetype: bugSomething isn't right: doesn't work as intended, documentation is missing/outdated, etc..

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions