feat(lsp): add snippet support and refactor completion into Item class
This commit is contained in:
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user