feat(lsp): apply additionalTextEdits on completion accept
This commit is contained in:
+4
-1
@@ -103,7 +103,10 @@ vim.cmd.aunmenu({ "PopUp.How-to\\ disable\\ mouse" })
|
|||||||
vim.keymap.set("i", "<C-f>", "<Right>")
|
vim.keymap.set("i", "<C-f>", "<Right>")
|
||||||
vim.keymap.set("i", "<C-b>", "<Left>")
|
vim.keymap.set("i", "<C-b>", "<Left>")
|
||||||
vim.keymap.set("i", "<C-a>", "<C-o>^")
|
vim.keymap.set("i", "<C-a>", "<C-o>^")
|
||||||
vim.keymap.set("i", "<C-e>", "<C-o>$")
|
vim.keymap.set("i", "<C-e>", function()
|
||||||
|
-- Fall through to Vim's default (close popup + revert) during completion.
|
||||||
|
return vim.fn.pumvisible() ~= 0 and "<C-e>" or "<C-o>$"
|
||||||
|
end, { expr = true, replace_keycodes = true })
|
||||||
-- vim.keymap.set('i', '<C-d>', '<C-o>x') -- Overrides de-indent
|
-- vim.keymap.set('i', '<C-d>', '<C-o>x') -- Overrides de-indent
|
||||||
vim.keymap.set("i", "<M-f>", "<C-o>w")
|
vim.keymap.set("i", "<M-f>", "<C-o>w")
|
||||||
vim.keymap.set("i", "<M-b>", "<C-o>b")
|
vim.keymap.set("i", "<M-b>", "<C-o>b")
|
||||||
|
|||||||
@@ -94,6 +94,79 @@ function M.setup()
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
-- Apply the LSP item's additionalTextEdits (auto-imports, etc.) and
|
||||||
|
-- run its post-accept command whenever an item's text was committed to
|
||||||
|
-- the buffer. This covers both explicit accept (<C-y>) and
|
||||||
|
-- acceptance-by-continuing-to-type, since Vim keeps the inserted text
|
||||||
|
-- in both cases. Only <C-e>/discard leaves v:completed_item empty.
|
||||||
|
vim.api.nvim_create_autocmd("CompleteDone", {
|
||||||
|
group = GROUP,
|
||||||
|
callback = function(ev)
|
||||||
|
local completed = vim.v.completed_item or {}
|
||||||
|
local original = vim.tbl_get(
|
||||||
|
completed,
|
||||||
|
"user_data",
|
||||||
|
"nvim",
|
||||||
|
"lsp",
|
||||||
|
"completion_item"
|
||||||
|
)
|
||||||
|
if not original then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local client_id =
|
||||||
|
vim.tbl_get(completed, "user_data", "nvim", "lsp", "client_id")
|
||||||
|
local client = client_id and vim.lsp.get_client_by_id(client_id)
|
||||||
|
if not client then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply(item)
|
||||||
|
if item.additionalTextEdits then
|
||||||
|
vim.lsp.util.apply_text_edits(
|
||||||
|
item.additionalTextEdits,
|
||||||
|
ev.buf,
|
||||||
|
client.offset_encoding
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if item.command then
|
||||||
|
client:request("workspace/executeCommand", {
|
||||||
|
command = item.command.command,
|
||||||
|
arguments = item.command.arguments,
|
||||||
|
}, nil, ev.buf)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Prefer the resolved item when we have it (servers like
|
||||||
|
-- rust-analyzer only provide additionalTextEdits in resolve
|
||||||
|
-- responses). If we haven't cached one yet and the original is
|
||||||
|
-- missing edits, resolve on the fly.
|
||||||
|
local cached = popup:resolved_for(completed.word)
|
||||||
|
if cached then
|
||||||
|
apply(cached)
|
||||||
|
elseif
|
||||||
|
client:supports_method(
|
||||||
|
vim.lsp.protocol.Methods.completionItem_resolve
|
||||||
|
)
|
||||||
|
and not original.additionalTextEdits
|
||||||
|
and not original.command
|
||||||
|
then
|
||||||
|
client:request(
|
||||||
|
vim.lsp.protocol.Methods.completionItem_resolve,
|
||||||
|
original,
|
||||||
|
function(err, resolved)
|
||||||
|
if err or not resolved then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
apply(resolved)
|
||||||
|
end,
|
||||||
|
ev.buf
|
||||||
|
)
|
||||||
|
else
|
||||||
|
apply(original)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
---@param key string
|
---@param key string
|
||||||
---@param action fun()
|
---@param action fun()
|
||||||
local function scroll_map(key, action)
|
local function scroll_map(key, action)
|
||||||
|
|||||||
@@ -22,42 +22,23 @@ local function fence(ft, text)
|
|||||||
return string.format("```%s\n%s\n```", ft, text)
|
return string.format("```%s\n%s\n```", ft, text)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param item lsp.CompletionItem
|
|
||||||
---@return string
|
|
||||||
local function signature_of(item)
|
|
||||||
return item.detail or vim.tbl_get(item, "labelDetails", "description") or ""
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param item lsp.CompletionItem
|
---@param item lsp.CompletionItem
|
||||||
---@param ft string
|
---@param ft string
|
||||||
---@return string? content
|
---@return string? content
|
||||||
---@return integer? width
|
---@return integer? width
|
||||||
local function build_content(item, ft)
|
local function build_content(item, ft)
|
||||||
local signature = signature_of(item)
|
local signature = item.detail
|
||||||
|
or vim.tbl_get(item, "labelDetails", "description")
|
||||||
local doc = item.documentation
|
local doc = item.documentation
|
||||||
if type(doc) == "table" then
|
if type(doc) == "table" then
|
||||||
doc = doc.value
|
doc = doc.value
|
||||||
end
|
end
|
||||||
doc = doc or ""
|
|
||||||
|
|
||||||
local code_parts = {}
|
|
||||||
if item.additionalTextEdits then
|
|
||||||
for _, edit in ipairs(item.additionalTextEdits) do
|
|
||||||
local text = (edit.newText or ""):gsub("%s+$", "")
|
|
||||||
if text ~= "" then
|
|
||||||
table.insert(code_parts, fence(ft, text))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if signature ~= "" then
|
|
||||||
table.insert(code_parts, fence(ft, signature))
|
|
||||||
end
|
|
||||||
|
|
||||||
local sections = {}
|
local sections = {}
|
||||||
if #code_parts > 0 then
|
if signature then
|
||||||
table.insert(sections, table.concat(code_parts, "\n\n"))
|
table.insert(sections, fence(ft, signature))
|
||||||
end
|
end
|
||||||
if doc ~= "" then
|
if doc then
|
||||||
table.insert(sections, doc)
|
table.insert(sections, doc)
|
||||||
end
|
end
|
||||||
if #sections == 0 then
|
if #sections == 0 then
|
||||||
@@ -78,6 +59,7 @@ end
|
|||||||
---@field private winid integer?
|
---@field private winid integer?
|
||||||
---@field private bufnr integer?
|
---@field private bufnr integer?
|
||||||
---@field private pending ow.lsp.completion.PendingResolve?
|
---@field private pending ow.lsp.completion.PendingResolve?
|
||||||
|
---@field private resolved { word: string, item: lsp.CompletionItem }?
|
||||||
local Popup = {}
|
local Popup = {}
|
||||||
Popup.__index = Popup
|
Popup.__index = Popup
|
||||||
|
|
||||||
@@ -87,9 +69,23 @@ function Popup.new()
|
|||||||
winid = nil,
|
winid = nil,
|
||||||
bufnr = nil,
|
bufnr = nil,
|
||||||
pending = nil,
|
pending = nil,
|
||||||
|
resolved = nil,
|
||||||
}, Popup)
|
}, Popup)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Most recently resolved completion item for the given selection `word`,
|
||||||
|
--- or `nil` if we don't have one cached. Servers like rust-analyzer fill in
|
||||||
|
--- `additionalTextEdits` only in the resolve response, so on accept we want
|
||||||
|
--- the resolved version rather than the original one in `user_data`.
|
||||||
|
---@param word string
|
||||||
|
---@return lsp.CompletionItem?
|
||||||
|
function Popup:resolved_for(word)
|
||||||
|
if self.resolved and self.resolved.word == word then
|
||||||
|
return self.resolved.item
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function Popup:is_visible()
|
function Popup:is_visible()
|
||||||
return self.winid ~= nil and vim.api.nvim_win_is_valid(self.winid)
|
return self.winid ~= nil and vim.api.nvim_win_is_valid(self.winid)
|
||||||
@@ -127,6 +123,7 @@ end
|
|||||||
---@param buf integer
|
---@param buf integer
|
||||||
function Popup:dispatch_resolve(client, item, ft, pum, word, buf)
|
function Popup:dispatch_resolve(client, item, ft, pum, word, buf)
|
||||||
self:cancel_pending()
|
self:cancel_pending()
|
||||||
|
self.resolved = nil
|
||||||
local _, request_id = client:request(
|
local _, request_id = client:request(
|
||||||
vim.lsp.protocol.Methods.completionItem_resolve,
|
vim.lsp.protocol.Methods.completionItem_resolve,
|
||||||
item,
|
item,
|
||||||
@@ -139,6 +136,7 @@ function Popup:dispatch_resolve(client, item, ft, pum, word, buf)
|
|||||||
if (vim.tbl_get(cur, "completed", "word") or "") ~= word then
|
if (vim.tbl_get(cur, "completed", "word") or "") ~= word then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
self.resolved = { word = word, item = result }
|
||||||
self:show(result, ft, pum)
|
self:show(result, ft, pum)
|
||||||
end,
|
end,
|
||||||
buf
|
buf
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ local function build_items(responses, base)
|
|||||||
result[#result + 1] = {
|
result[#result + 1] = {
|
||||||
word = item_word(item),
|
word = item_word(item),
|
||||||
abbr = item.label:match("[^(]+") or item.label,
|
abbr = item.label:match("[^(]+") or item.label,
|
||||||
menu = "",
|
menu = vim.tbl_get(item, "labelDetails", "detail") or "",
|
||||||
kind = kind_icon,
|
kind = kind_icon,
|
||||||
kind_hlgroup = kind_hl,
|
kind_hlgroup = kind_hl,
|
||||||
-- non-empty so our CompleteChanged handler triggers resolve
|
-- non-empty so our CompleteChanged handler triggers resolve
|
||||||
|
|||||||
Reference in New Issue
Block a user