blink.cmp icon indicating copy to clipboard operation
blink.cmp copied to clipboard

Elixir-ls snippet completion removes prefix character

Open rlbaker opened this issue 2 months ago • 3 comments

Make sure you have done the following

  • [x] Updated to the latest version of blink.cmp
  • [x] Searched for existing issues and documentation (try <C-k> on https://cmp.saghen.dev)

Bug Description

First off, thanks for building and maintaining blink.cmp, it's one of the best completion plugins I've used to date. Setup was easy, great performance, and sane defaults. Many thanks for the work you've put into it.

I encountered a (very) small bug when attempting to complete + accept an lsp-sourced snippet.

While using elixir-ls, a prefix character (@ in this case) is being removed when selecting and/or accepting the snippet.

May be related to the resolved https://github.com/Saghen/blink.cmp/issues/1732 issue, but is still occurring for this scenario. Interestingly, when selecting the non-snippet version the @ is not removed, which may be what that issue resolved?

This capture should illustrate the issue hopefully:

Image

ctx objects

I've captured the ctx object from the draw callbacks for the @doc attribute and @doc snippets associated with these completions, in hopes that it will help with debugging the issue.

@doc attribute

{
  deprecated = false,
  icon_gap = "",
  idx = 1,
  item = {
    client_id = 1,
    client_name = "elixirls",
    cursor_column = 4,
    detail = "module attribute",
    documentation = {
      kind = "markdown",
      value = "@doc\nProvides documentation for a function/macro/callback."
    },
    exact = false,
    filterText = "doc",
    insertText = "doc",
    insertTextFormat = 2,
    kind = 20,
    label = "@doc",
    score = 29,
    score_offset = 0,
    sortText = "00000006",
    source_id = "lsp",
    source_name = "LSP"
  },
  kind = "EnumMember",
  kind_hl = "BlinkCmpKindEnumMember",
  kind_icon = "󰦨",
  label = "@doc",
  label_description = "",
  label_detail = "",
  label_matched_indices = { 0, 1 },
  self = { ... },
  source_id = "lsp",
  source_name = "LSP"
}

@doc snippet

{
  deprecated = false,
  idx = 5,
  item = {
    client_id = 1,
    client_name = "elixirls",
    cursor_column = 4,
    detail = "module attribute snippet",
    documentation = {
      kind = "markdown",
      value = "Documents a function/macro/callback"
    },
    exact = false,
    filterText = "doc",
    insertText = 'doc """\n$0\n"""',
    insertTextFormat = 2,
    kind = 15,
    label = '@doc """"""',
    score = 27,
    score_offset = 0,
    sortText = "00000000",
    source_id = "lsp",
    source_name = "LSP"
  },
  kind = "Snippet",
  kind_hl = "BlinkCmpKindSnippet",
  kind_icon = "󱄽",
  label = '@doc """"""~',
  label_description = "",
  label_detail = "",
  label_matched_indices = { 0, 1 },
  self = { ... },
  source_id = "lsp",
  source_name = "LSP"
}

In an attempt to rule out elixir-ls being the culprit, I tested with base neovim lsp completion select + accept and it seems to function normally that way. I'm sure that's not concrete evidence to fully rule it out, so I'm happy to verify further though whatever means would be helpful.

Let me know if there's any other information that would be useful provide, and thanks again for this plugin!

If others are looking for a janky short-term fix, it is possible to work around this by prepending the @ back into the ctx.item.insertText in one of the completion.menu.draw callbacks.

Relevant configuration

{
  'saghen/blink.cmp',
  version = '1.*',
  opts = {
    completion = {
      list = { selection = { preselect = false } },
      documentation = { auto_show = true },
    }
  }
}

neovim version

NVIM v0.11.4 - Build type: Release - LuaJIT 2.1.1753364724

blink.cmp version

1.7

rlbaker avatar Oct 10 '25 20:10 rlbaker

Glad to hear you enjoy blink.cmp!

To add a bit to your already detailed analysis, elixir-ls does not support textEdit, blink.cmp has to guess what the completion should be from the provided insertText. However, as you noticed, the insertText omits the @. The resolved issues #1340 (Lua) and #1732 (Rust) actually dealt with the opposite problem ; snippets starting with symbols were not getting replaced. It seems elixir-ls the black sheep here, so this kind of workaround, adding the @ back, seems necessary.

Anyway, I’ll let @Saghen have the final say, he's got more insight on this than I do.

soifou avatar Oct 11 '25 00:10 soifou

Ah interesting, thanks for the context.

Given the elixir ecosystem is heading towards using expert as the official LSP, it might not be worth handling this special elixir-ls case on the blink.cmp side anyways.

I appreciate folks looking into it and the info you shared. If a fix does end up getting implemented, I'm happy to help verify though.

rlbaker avatar Oct 12 '25 23:10 rlbaker

Hi,

I also noticed this issue with erlang and erlang_ls and the - prefix when adding module attributes, for example:

-module().

-behaviour(gen_server).

I think this falls into the same category. I inspected the completion items returned by the LSP and captured the following:

{
  is_incomplete_backward = true,
  is_incomplete_forward = false,
  items = {{
    client_id = 1,
    client_name = "erlangls",
    cursor_column = 1,
    insertText = "behaviour(${1:Behaviour}).",
    insertTextFormat = 2,
    kind = 15,
    label = "-behaviour()."
  },
  ...
  }
}

It seems the LSP returns the insertText and assumes it will be inserted at cursor_column.

I noticed some workarounds/hacks for completions in other LSPs, and I ended up writing one for erlang_ls. My assumption is that a similar approach could be applied to elixir-ls as well.

Here’s the comparison branch with my changes: https://github.com/Saghen/blink.cmp/compare/main...guisaez:blink.cmp:main

@soifou I’m not sure if it’s worth opening a PR for this.

Every other aspect works great! Thanks for mantaining blink.cmp

guisaez avatar Oct 16 '25 16:10 guisaez