Repository: neovim/neovim
File: runtime/lua/vim/lsp/completion.lua (Lua)

Code before:
        abbr_hlgroup = hl_group,
        user_data = {
          nvim = {
            lsp = {
              completion_item = item,
              client_id = client_id,
            },
          },
        },
        _fuzzy_score = score,
      }
      if user_convert then
        completion_item = vim.tbl_extend('keep', user_convert(item), completion_item)
      end
      table.insert(candidates, completion_item)
    end
  end
  if not user_cmp then
    ---@diagnostic disable-next-line: no-unknown
    table.sort(candidates, function(a, b)
      if use_fuzzy and prefix ~= '' then
        local score_a = a._fuzzy_score or 0
        local score_b = b._fuzzy_score or 0
        if score_a ~= score_b then
          return score_a > score_b
        end
      end
      ---@type lsp.CompletionItem
      local itema = a.user_data.nvim.lsp.completion_item
      ---@type lsp.CompletionItem
      local itemb = b.user_data.nvim.lsp.completion_item
      return (itema.sortText or itema.label) < (itemb.sortText or itemb.label)
    end)
  end
  return candidates
end

--- @param lnum integer 0-indexed
--- @param line string
--- @param items lsp.CompletionItem[]
--- @param encoding 'utf-8'|'utf-16'|'utf-32'
--- @return integer?
local function adjust_start_col(lnum, line, items, encoding)
  local min_start_char = nil
  for _, item in pairs(items) do
    if item.textEdit then
      if item.textEdit.range and item.textEdit.range.start.line == lnum then
        if min_start_char and min_start_char ~= item.textEdit.range.start.character then
          return nil
        end
        min_start_char = item.textEdit.range.start.character

Review comment (bug):
Isn't this strictly speaking going against the [spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion)?

> to achieve consistency across languages and to honor different clients usually the client is responsible for filtering and sorting. This has also the advantage that client can experiment with different filter and sorting models. However servers can enforce different behavior by setting a filterText / sortText

> Completion Item with text edits: in this mode the server tells the client that it actually knows what it is doing. If you create a completion item with a text edit at the current cursor position no word guessing takes place and no automatic filtering (like with an insertText) should happen. This mode can be combined with a sort text and filter text to customize two things. If the text edit is a replace edit then the range denotes the word used for filtering. If the replace changes the text it most likely makes sense to specify a filter text to be used.

Based on that I'd think it should use  `sortText -> score -> label`. But judging from your test cases the server actually provides a `textEdit` and `sortText` - which seems like the server is drunk and provides a bad ordering?

No strong opinion. But maybe worth double checking if the server isn't doing something wrong.

Otherwise - if you extend it to also honor the `nosort` `completeopt` it should be good, given that we'd then have an opt-out and users could configure either behavior.
And maybe worth pulling the `if ...` out of the inner sort loop and create a different `compare_fn` depending on `use_fuzzy` & `prefix`.


Code after:
        user_data = {
          nvim = {
            lsp = {
              completion_item = item,
              client_id = client_id,
            },
          },
        },
        _fuzzy_score = score,
      }
      if user_convert then
        completion_item = vim.tbl_extend('keep', user_convert(item), completion_item)
      end
      table.insert(candidates, completion_item)
    end
  end

  if not user_cmp then
    local compare_by_sortText_and_label = function(a, b)
      ---@type lsp.CompletionItem
      local itema = a.user_data.nvim.lsp.completion_item
      ---@type lsp.CompletionItem
      local itemb = b.user_data.nvim.lsp.completion_item
      return (itema.sortText or itema.label) < (itemb.sortText or itemb.label)
    end

    local use_fuzzy_sort = vim.o.completeopt:find('fuzzy') ~= nil
      and vim.o.completeopt:find('nosort') == nil
      and not result.isIncomplete
      and #prefix > 0

    local compare_fn = use_fuzzy_sort
        and function(a, b)
          local score_a = a._fuzzy_score or 0
          local score_b = b._fuzzy_score or 0
          if score_a ~= score_b then
            return score_a > score_b
          end
          return compare_by_sortText_and_label(a, b)
        end
      or compare_by_sortText_and_label

    table.sort(candidates, compare_fn)
  end
  return candidates
end

--- @param lnum integer 0-indexed
--- @param line string
--- @param items lsp.CompletionItem[]
--- @param encoding 'utf-8'|'utf-16'|'utf-32'