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

Code before:

  if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then
    return vim.startswith(value:lower(), prefix:lower())
  end
  return vim.startswith(value, prefix)
end

--- Compare two completion items according priority: sortText > label > kind
---
--- @param a table completion item with user_data
--- @param b table completion item with user_data
--- @return boolean
local function compare_completion_items(a, b)
  ---@type lsp.CompletionItem
  local item_a = vim.tbl_get(a, 'user_data', 'nvim', 'lsp', 'completion_item')
  ---@type lsp.CompletionItem
  local item_b = vim.tbl_get(b, 'user_data', 'nvim', 'lsp', 'completion_item')
  if not item_a then
    return false
  end
  if not item_b then
    return true
  end

  -- 1. Compare by sortText
  if item_a.sortText and item_b.sortText then
    local sort_a_lower = item_a.sortText:lower()
    local sort_b_lower = item_b.sortText:lower()
    if sort_a_lower ~= sort_b_lower then
      return sort_a_lower < sort_b_lower
    end
  end

  -- 2. Compare by label
  local label_a = type(item_a.label) == 'string' and item_a.label or (item_a.label and item_a.label)
  local label_b = type(item_b.label) == 'string' and item_b.label or (item_b.label and item_b.label)

  if not label_a then
    return false
  end
  if not label_b then
    return true
  end
  if label_a ~= label_b then
    return label_a < label_b
  end

  -- 3. Compare by kind
  return (item_a.kind or 0) < (item_b.kind or 0)
end


Review comment (security):
> Implement proper comparison: sortText (case-insensitive, only when both present) > label > kind

Could you please clarify why `sortText` should only be compared when both items have it?

The spec says: 
```
/**
 * A string that should be used when comparing this item
 * with other items. When omitted, the label is used
 * as the sort text for this item.
 */
sortText?: string;
```

The phrase "when comparing this item with other items" seems to imply that the comparison value for the other item may not necessarily be `sortText`. The field priorities apply within a single item, not across items.

Code after:

  if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then
    return vim.startswith(value:lower(), prefix:lower())
  end
  return vim.startswith(value, prefix)
end

--- Turns the result of a `textDocument/completion` request into vim-compatible
--- |complete-items|.
---
--- @param result vim.lsp.CompletionResult Result of `textDocument/completion`
--- @param prefix string prefix to filter the completion items
--- @param client_id integer? Client ID
--- @return table[]
--- @see complete-items
function M._lsp_to_complete_items(result, prefix, client_id)
  local items = get_items(result)
  if vim.tbl_isempty(items) then
    return {}
  end

  ---@type fun(item: lsp.CompletionItem):boolean
  local matches
  if not prefix:find('%w') then
    matches = function(_)
      return true
    end
  else
    ---@param item lsp.CompletionItem
    matches = function(item)
      if item.filterText then
        return match_item_by_value(item.filterText, prefix)
      end

      if item.textEdit then
        -- server took care of filtering
        return true
      end

      return match_item_by_value(item.label, prefix)
    end
  end

  local candidates = {}
  local bufnr = api.nvim_get_current_buf()
  local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
  local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp')
  for _, item in ipairs(items) do
    if matches(item) then
      local word = get_completion_word(item, prefix, match_item_by_value)
      local hl_group = ''