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

Code before:
    server_start_boundary = curstartbyte
  elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
    server_start_boundary = client_start_boundary
  end
  local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
  local matches = M._lsp_to_complete_items(result, prefix, client_id)

  if server_start_boundary then
    for _, match in ipairs(matches) do
      --- @type lsp.CompletionItem
      local item = match.user_data.nvim.lsp.completion_item
      if item.textEdit then
        --- @type integer?
        local item_start_char
        if item.textEdit.range and item.textEdit.range.start.line == lnum then
          item_start_char = item.textEdit.range.start.character
        elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then
          item_start_char = item.textEdit.insert.start.character
        end

        if item_start_char then
          local item_start_byte = vim.str_byteindex(line, encoding, item_start_char, false)
          if item_start_byte > server_start_boundary then
            local missing_prefix = line:sub(server_start_boundary + 1, item_start_byte)
            --- @type string
            match.word = missing_prefix .. match.word
          end
        end
      end
    end
  end
  return matches, server_start_boundary
end

-- NOTE: The reason we don't use `lsp.buf_request_all` here is because we want to filter the clients
-- that received the request based on the trigger characters.
--- @param clients table<integer, vim.lsp.Client> # keys != client_id
--- @param bufnr integer
--- @param win integer
--- @param ctx? lsp.CompletionContext
--- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>)
--- @return function # Cancellation function
local function request(clients, bufnr, win, ctx, callback)
  local responses = {} --- @type table<integer, { err: lsp.ResponseError, result: any }>
  local request_ids = {} --- @type table<integer, integer>
  local remaining_requests = vim.tbl_count(clients)

  for _, client in pairs(clients) do
    local client_id = client.id
    local params = lsp.util.make_position_params(win, client.offset_encoding)
    --- @cast params lsp.CompletionParams

Review comment (suggestion):
> And yes, my implementation requires 2 iterations, but as @mfussenegger said, this is necessary to handle mixed start positions.

Given the current `vim.fn.complete(start_col)` mechanism 2 iterations are the minimum we need: 1 full iteration to determine the min/max boundaries, a second iteration to do the conversion to matches.

But the implementation here currently has one extra iteration - it modifies `match.word` in a separate third iteration. @glepnir is right that adding the `missing_prefix` could be integrated into `to_complete_items` 

Otherwise the change here looks good to me - I like that the scope is narrower and limited to fixing the mixed textEdit start location issue.


Code after:
--- @param encoding 'utf-8'|'utf-16'|'utf-32'
--- @return table[] matches
--- @return integer? server_start_boundary
function M._convert_results(
  line,
  lnum,
  cursor_col,
  client_id,
  client_start_boundary,
  server_start_boundary,
  result,
  encoding
)
  -- Completion response items may be relative to a position different than `client_start_boundary`.
  -- Concrete example, with lua-language-server:
  --
  -- require('plenary.asy|
  --         ▲       ▲   ▲
  --         │       │   └── cursor_pos:                     20
  --         │       └────── client_start_boundary:          17
  --         └────────────── textEdit.range.start.character: 9
  --                                 .newText = 'plenary.async'
  --                  ^^^
  --                  prefix (We'd remove everything not starting with `asy`,
  --                  so we'd eliminate the `plenary.async` result
  --
  -- `adjust_start_col` is used to prefer the language server boundary.
  --
  local candidates = get_items(result)
  local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
  if server_start_boundary == nil then
    server_start_boundary = curstartbyte
  elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
    server_start_boundary = client_start_boundary
  end
  local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
  local matches =
    M._lsp_to_complete_items(result, prefix, client_id, server_start_boundary, line, lnum, encoding)

  return matches, server_start_boundary
end

-- NOTE: The reason we don't use `lsp.buf_request_all` here is because we want to filter the clients
-- that received the request based on the trigger characters.
--- @param clients table<integer, vim.lsp.Client> # keys != client_id
--- @param bufnr integer
--- @param win integer
--- @param ctx? lsp.CompletionContext
--- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>)
--- @return function # Cancellation function
local function request(clients, bufnr, win, ctx, callback)