feat(lsp): add snippet support and refactor completion into Item class

This commit is contained in:
2026-04-17 00:03:03 +02:00
parent 5ecec7cc6c
commit 47f36adc21
9 changed files with 213 additions and 144 deletions
+32 -57
View File
@@ -1,23 +1,24 @@
---@class ow.lsp.CompletionResponse : ow.lsp.Response
---@field result lsp.CompletionItem[] | lsp.CompletionList
local Item = require("lsp.completion.item")
local kind = require("lsp.kind")
local util = require("util")
local REQUEST_DEBOUNCE_MS = 100
--- Normalize an `itemDefaults.editRange` to a plain Range, since it may be
--- either `Range` or `{ insert: Range, replace: Range }`.
---@param edit_range table?
---@return table?
local function default_range(edit_range)
---@param item_defaults? lsp.CompletionItemDefaults
---@return lsp.Range?
local function default_range(item_defaults)
local edit_range = item_defaults and item_defaults.editRange
if type(edit_range) ~= "table" then
return nil
end
return edit_range.insert or (edit_range.start and edit_range) or nil
return edit_range.insert or (edit_range.start and edit_range)
end
--- Normalize an item's `textEdit`, which may be `TextEdit` (has `range`) or
--- `InsertReplaceEdit` (has `insert`/`replace`).
---@param item lsp.CompletionItem
---@return table?
---@return lsp.Range?
local function item_range(item)
local te = item.textEdit
if type(te) ~= "table" then
@@ -26,18 +27,18 @@ local function item_range(item)
return te.range or te.insert
end
---@param response table LSP response entry (as returned by buf_request_all)
---@return integer? character 0-indexed UTF-16 character where edit starts
---@param response ow.lsp.CompletionResponse
---@return integer? character 0-indexed UTF-16 character where edit starts
local function response_edit_start(response)
if not response.result or response.err or response.error then
if not response.result or response.err then
return nil
end
local defaults =
default_range(vim.tbl_get(response.result, "itemDefaults", "editRange"))
local result = response.result
local defaults = default_range(result.items and result.itemDefaults)
if defaults then
return defaults.start.character
end
local items = response.result.items or response.result
local items = result.items or result
if type(items) ~= "table" then
return nil
end
@@ -50,23 +51,13 @@ local function response_edit_start(response)
return nil
end
---@param item lsp.CompletionItem
---@return string
local function item_word(item)
local edit = item.textEdit
if type(edit) == "table" and edit.newText then
return edit.newText
end
return item.insertText or item.label
end
---@param items lsp.CompletionItem[]
---@param defaults? table
---@param defaults? lsp.CompletionItemDefaults
local function apply_defaults(items, defaults)
if type(defaults) ~= "table" then
return
end
local range = default_range(defaults.editRange)
local range = default_range(defaults)
for _, item in ipairs(items) do
item.commitCharacters = item.commitCharacters
or defaults.commitCharacters
@@ -83,15 +74,13 @@ local function apply_defaults(items, defaults)
end
end
--- Transform raw LSP completion responses into Vim |complete()| items,
--- filtered by `base` and sorted by sortText/label.
---@param responses table map of client_id -> response from buf_request_all
---@param responses table<integer, ow.lsp.CompletionResponse>
---@param base string
---@return table[]
---@return vim.v.completed_item[]
local function build_items(responses, base)
local entries = {}
for client_id, response in pairs(responses) do
if response.result and not (response.err or response.error) then
if response.result and not response.err then
local raw = response.result.items or response.result
if type(raw) == "table" then
apply_defaults(raw, response.result.itemDefaults)
@@ -116,14 +105,15 @@ local function build_items(responses, base)
return ka < kb
end)
---@type vim.v.completed_item[]
local result = {}
for _, entry in ipairs(entries) do
local item = entry.item
local kind_icon, kind_hl = kind.get(item.kind)
local item = Item.from_lsp(entry.item, entry.client_id)
local kind_icon, kind_hl = kind.get(entry.item.kind)
result[#result + 1] = {
word = item_word(item),
abbr = item.label:match("[^(]+") or item.label,
menu = vim.tbl_get(item, "labelDetails", "detail") or "",
word = item.word,
abbr = item.abbr,
menu = item.menu,
kind = kind_icon,
kind_hlgroup = kind_hl,
-- non-empty so our CompleteChanged handler triggers resolve
@@ -131,14 +121,7 @@ local function build_items(responses, base)
icase = 1,
dup = 1,
empty = 1,
user_data = {
nvim = {
lsp = {
completion_item = item,
client_id = entry.client_id,
},
},
},
user_data = { ow = { item = item } },
}
end
return result
@@ -182,7 +165,7 @@ end
---@class ow.lsp.completion.Request
---@field private generation integer
---@field private phase ('awaiting' | 'ready' | 'done')?
---@field private result table?
---@field private result table<integer, ow.lsp.CompletionResponse>?
---@field private cancel function?
local Request = {}
Request.__index = Request
@@ -207,7 +190,6 @@ function Request:is_ready()
return self.phase == "ready"
end
--- Cancel any in-flight LSP request and clear cached results.
function Request:discard()
if self.cancel then
pcall(self.cancel)
@@ -219,7 +201,7 @@ function Request:discard()
end
---@param trigger_kind integer
---@param trigger_char string?
---@param trigger_char? string
function Request:dispatch(trigger_kind, trigger_char)
self:discard()
self.phase = "awaiting"
@@ -246,8 +228,6 @@ function Request:dispatch(trigger_kind, trigger_char)
)
end
--- Byte column where the cached LSP edits start, for |complete-functions|
--- `findstart == 1`. Falls back to the regex end of the word under cursor.
---@return integer
function Request:start_col()
for _, response in pairs(self.result or {}) do
@@ -261,11 +241,8 @@ function Request:start_col()
return vim.fn.match(line:sub(1, cursor_col), "\\k*$")
end
--- Build the Vim |complete()| items from the cached LSP results filtered by
--- `base`, and transition this request to the "done" phase. Subsequent
--- completion triggers will dispatch a fresh request.
---@param base string
---@return table[]
---@return vim.v.completed_item[]
function Request:consume(base)
local items = build_items(self.result or {}, base)
self.phase = "done"
@@ -310,11 +287,9 @@ end
-- `v:lua.ow_lsp_completion.omnifunc` from on_attach's `vim.bo.omnifunc`.
_G.ow_lsp_completion = _G.ow_lsp_completion or {}
--- Returns cached items without re-requesting, which is what lets Vim filter
--- the popup in-place on backspace.
---@param findstart 0 | 1
---@param base string
---@return integer | table
---@return integer | vim.v.completed_item[]
function _G.ow_lsp_completion.omnifunc(findstart, base)
if not buffer_has_completion_client() or request:is_awaiting() then
return findstart == 1 and -3 or {}