perf: improve for source providing huge list of items (#1980)

* perf: avoid creating closure in cache.ensure and drop some cached getters

This mainly addresses the perf issue on large amount of calls to
`entry.new`. Previously every `cache.ensure` calls in the code path of
it creates an anonymous function, and it seems that luajit just could
not inline it. Function creation is not expensive in luajit, but that
overhead is noticeable if every `cache.ensure` call creates a function.
The first improvemnt is to solidate the cache callback and attach it to
the metatable of `entry`. This ensures that every created entry instance
share the same cache callback and no new functions will be frequently created,
reduces the ram usage and GC overhead.

To improve it further, some frequently accessed fields of entry like
`completion_item` and `offset` is refactored to use simple table access
instead of getter pattern. The current cached getter is implemented
using `cache.ensure`, which introduces two more levels of function calls
on each access: `cache.key` and `cache.get`. The overhead is okay if but
noticeable if entries amount is quite large: you need to call 4 functions on
a simple `completion_item` field access for each item.

All of the changes done in the commit is just constant time
optimization. But the different is huge if tested with LS providing
large amount of entries like tailwindcss.

* perf: delay fuzzy match on displayed vim item

`entry.get_vim_item` is a very heavy call, especially when user do
complex stuff on item formatting. Delay its call to window displaying to
let `performance.max_view_entries` applied to it.

* remove unneeded fill_defaults

* update gha

---------

Co-authored-by: hrsh7th <629908+hrsh7th@users.noreply.github.com>
This commit is contained in:
yioneko
2024-10-20 12:28:20 +08:00
committed by GitHub
parent ae644feb7b
commit 1a1d7ecb73
12 changed files with 468 additions and 408 deletions

View File

@@ -23,9 +23,9 @@ jobs:
neovim: true neovim: true
- name: Setup lua - name: Setup lua
uses: leafo/gh-actions-lua@v8 uses: leafo/gh-actions-lua@v10
with: with:
luaVersion: "luajit-2.1.0-beta3" luaVersion: "luajit-openresty"
- name: Setup luarocks - name: Setup luarocks
uses: leafo/gh-actions-luarocks@v4 uses: leafo/gh-actions-luarocks@v4

View File

@@ -17,7 +17,7 @@ local compare = {}
---offset: Entries with smaller offset will be ranked higher. ---offset: Entries with smaller offset will be ranked higher.
---@type cmp.ComparatorFunction ---@type cmp.ComparatorFunction
compare.offset = function(entry1, entry2) compare.offset = function(entry1, entry2)
local diff = entry1:get_offset() - entry2:get_offset() local diff = entry1.offset - entry2.offset
if diff < 0 then if diff < 0 then
return true return true
elseif diff > 0 then elseif diff > 0 then
@@ -180,8 +180,8 @@ compare.locality = setmetatable({
}, { }, {
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
__call = function(self, entry1, entry2) __call = function(self, entry1, entry2)
local local1 = self.locality_map[entry1:get_word()] local local1 = self.locality_map[entry1.word]
local local2 = self.locality_map[entry2:get_word()] local local2 = self.locality_map[entry2.word]
if local1 ~= local2 then if local1 ~= local2 then
if local1 == nil then if local1 == nil then
return false return false
@@ -255,8 +255,8 @@ compare.scopes = setmetatable({
}, { }, {
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
__call = function(self, entry1, entry2) __call = function(self, entry1, entry2)
local local1 = self.scopes_map[entry1:get_word()] local local1 = self.scopes_map[entry1.word]
local local2 = self.scopes_map[entry2:get_word()] local local2 = self.scopes_map[entry2.word]
if local1 ~= local2 then if local1 ~= local2 then
if local1 == nil then if local1 == nil then
return false return false

View File

@@ -124,7 +124,7 @@ core.on_keymap = function(self, keys, fallback)
commit_character = chars, commit_character = chars,
}, function() }, function()
local ctx = self:get_context() local ctx = self:get_context()
local word = e:get_word() local word = e.word
if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then
fallback() fallback()
else else
@@ -358,7 +358,7 @@ core.confirm = function(self, e, option, callback)
end end
e.confirmed = true e.confirmed = true
debug.log('entry.confirm', e:get_completion_item()) debug.log('entry.confirm', e.completion_item)
async.sync(function(done) async.sync(function(done)
e:resolve(done) e:resolve(done)
@@ -374,8 +374,8 @@ core.confirm = function(self, e, option, callback)
-- Emulate `<C-y>` behavior to save `.` register. -- Emulate `<C-y>` behavior to save `.` register.
local ctx = context.new() local ctx = context.new()
local keys = {} local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset()))) table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e.offset)))
table.insert(keys, e:get_word()) table.insert(keys, e.word)
table.insert(keys, keymap.undobreak()) table.insert(keys, keymap.undobreak())
feedkeys.call(table.concat(keys, ''), 'in') feedkeys.call(table.concat(keys, ''), 'in')
end) end)
@@ -384,15 +384,15 @@ core.confirm = function(self, e, option, callback)
local ctx = context.new() local ctx = context.new()
if api.is_cmdline_mode() then if api.is_cmdline_mode() then
local keys = {} local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset()))) table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e.offset)))
table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset())) table.insert(keys, string.sub(e.context.cursor_before_line, e.offset))
feedkeys.call(table.concat(keys, ''), 'in') feedkeys.call(table.concat(keys, ''), 'in')
else else
vim.cmd([[silent! undojoin]]) vim.cmd([[silent! undojoin]])
-- This logic must be used nvim_buf_set_text. -- This logic must be used nvim_buf_set_text.
-- If not used, the snippet engine's placeholder wil be broken. -- If not used, the snippet engine's placeholder wil be broken.
vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, { vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e.offset - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, {
e.context.cursor_before_line:sub(e:get_offset()), e.context.cursor_before_line:sub(e.offset),
}) })
vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 }) vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 })
end end
@@ -400,10 +400,10 @@ core.confirm = function(self, e, option, callback)
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
-- Apply additionalTextEdits. -- Apply additionalTextEdits.
local ctx = context.new() local ctx = context.new()
if #(e:get_completion_item().additionalTextEdits or {}) == 0 then if #(e.completion_item.additionalTextEdits or {}) == 0 then
e:resolve(function() e:resolve(function()
local new = context.new() local new = context.new()
local text_edits = e:get_completion_item().additionalTextEdits or {} local text_edits = e.completion_item.additionalTextEdits or {}
if #text_edits == 0 then if #text_edits == 0 then
return return
end end
@@ -428,12 +428,12 @@ core.confirm = function(self, e, option, callback)
end) end)
else else
vim.cmd([[silent! undojoin]]) vim.cmd([[silent! undojoin]])
vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind()) vim.lsp.util.apply_text_edits(e.completion_item.additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind())
end end
end) end)
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
local ctx = context.new() local ctx = context.new()
local completion_item = misc.copy(e:get_completion_item()) local completion_item = misc.copy(e.completion_item)
if not completion_item.textEdit then if not completion_item.textEdit then
completion_item.textEdit = {} completion_item.textEdit = {}
local insertText = completion_item.insertText local insertText = completion_item.insertText
@@ -444,9 +444,9 @@ core.confirm = function(self, e, option, callback)
end end
local behavior = option.behavior or config.get().confirmation.default_behavior local behavior = option.behavior or config.get().confirmation.default_behavior
if behavior == types.cmp.ConfirmBehavior.Replace then if behavior == types.cmp.ConfirmBehavior.Replace then
completion_item.textEdit.range = e:get_replace_range() completion_item.textEdit.range = e.replace_range
else else
completion_item.textEdit.range = e:get_insert_range() completion_item.textEdit.range = e.insert_range
end end
local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1)) local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1))
@@ -460,15 +460,15 @@ core.confirm = function(self, e, option, callback)
if false then if false then
--To use complex expansion debug. --To use complex expansion debug.
vim.print({ -- luacheck: ignore vim.print({ -- luacheck: ignore
item = e:get_completion_item(), item = e.completion_item,
diff_before = diff_before, diff_before = diff_before,
diff_after = diff_after, diff_after = diff_after,
new_text = new_text, new_text = new_text,
text_edit_new_text = completion_item.textEdit.newText, text_edit_new_text = completion_item.textEdit.newText,
range_start = completion_item.textEdit.range.start.character, range_start = completion_item.textEdit.range.start.character,
range_end = completion_item.textEdit.range['end'].character, range_end = completion_item.textEdit.range['end'].character,
original_range_start = e:get_completion_item().textEdit.range.start.character, original_range_start = e.completion_item.textEdit.range.start.character,
original_range_end = e:get_completion_item().textEdit.range['end'].character, original_range_end = e.completion_item.textEdit.range['end'].character,
cursor_line = ctx.cursor_line, cursor_line = ctx.cursor_line,
cursor_col0 = ctx.cursor.col - 1, cursor_col0 = ctx.cursor.col - 1,
}) })

View File

@@ -25,7 +25,14 @@ local matcher = require('cmp.matcher')
---@field public resolved_callbacks fun()[] ---@field public resolved_callbacks fun()[]
---@field public resolving boolean ---@field public resolving boolean
---@field public confirmed boolean ---@field public confirmed boolean
---@field public insert_range lsp.Range
---@field public replace_range lsp.Range
---@field public offset integer
---@field public word string
---@field public filter_text string
---@field private match_view_args_ret {input:string, word:string, option:cmp.MatchingConfig, matches:table[]}
local entry = {} local entry = {}
entry.__index = entry
---Create new entry ---Create new entry
---@param ctx cmp.Context ---@param ctx cmp.Context
@@ -34,7 +41,7 @@ local entry = {}
---@param item_defaults? lsp.internal.CompletionItemDefaults ---@param item_defaults? lsp.internal.CompletionItemDefaults
---@return cmp.Entry ---@return cmp.Entry
entry.new = function(ctx, source, completion_item, item_defaults) entry.new = function(ctx, source, completion_item, item_defaults)
local self = setmetatable({}, { __index = entry }) local self = setmetatable({}, entry)
self.id = misc.id('entry.new') self.id = misc.id('entry.new')
self.cache = cache.new() self.cache = cache.new()
self.match_cache = cache.new() self.match_cache = cache.new()
@@ -43,159 +50,211 @@ entry.new = function(ctx, source, completion_item, item_defaults)
self.matches = {} self.matches = {}
self.context = ctx self.context = ctx
self.source = source self.source = source
self.offset = source.request_offset
self.source_offset = source.request_offset self.source_offset = source.request_offset
self.source_insert_range = source:get_default_insert_range() self.source_insert_range = source.default_insert_range
self.source_replace_range = source:get_default_replace_range() self.source_replace_range = source.default_replace_range
self.completion_item = self:fill_defaults(completion_item, item_defaults)
self.item_defaults = item_defaults self.item_defaults = item_defaults
self.resolved_completion_item = nil self.resolved_completion_item = nil
self.resolved_callbacks = {} self.resolved_callbacks = {}
self.resolving = false self.resolving = false
self.confirmed = false self.confirmed = false
self:_set_completion_item(completion_item)
return self return self
end end
---Make offset value ---@package
---@return integer entry._set_completion_item = function(self, completion_item)
if not self.completion_item then
self.completion_item = self:fill_defaults(completion_item, self.item_defaults)
else
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts#L588
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/base/common/objects.ts#L89
-- @see https://github.com/microsoft/vscode/blob/a00f2e64f4fa9a1f774875562e1e9697d7138ed3/src/vs/editor/contrib/suggest/browser/suggest.ts#L147
for k, v in pairs(completion_item) do
self.completion_item[k] = v or self.completion_item[k]
end
end
local item = self.completion_item
---Create filter text
self.filter_text = item.filterText or str.trim(item.label)
-- TODO: the order below is important
if item.textEdit then
self.insert_range = self:convert_range_encoding(item.textEdit.insert or item.textEdit.range)
self.replace_range = self:convert_range_encoding(item.textEdit.replace or item.textEdit.range)
end
self.word = self:_get_word()
self.offset = self:_get_offset()
if not self.insert_range then
self.insert_range = {
start = {
line = self.context.cursor.row - 1,
character = self.offset - 1,
},
['end'] = self.source_insert_range['end'],
}
end
if not self.replace_range or ((self.context.cursor.col - 1) == self.replace_range['end'].character) then
self.replace_range = {
start = {
line = self.source_replace_range.start.line,
character = self.offset - 1,
},
['end'] = self.source_replace_range['end'],
}
end
end
---@deprecated use entry.offset instead
entry.get_offset = function(self) entry.get_offset = function(self)
return self.cache:ensure('get_offset', function() return self.offset
local offset = self.source_offset end
if self:get_completion_item().textEdit then
local range = self:get_insert_range() ---Make offset value
if range then ---@package
offset = self.context.cache:ensure('entry:' .. 'get_offset:' .. tostring(range.start.character), function() ---@return integer
local start = math.min(range.start.character + 1, offset) entry._get_offset = function(self)
for idx = start, self.source_offset do local offset = self.source_offset
local byte = string.byte(self.context.cursor_line, idx) if self.completion_item.textEdit then
if byte == nil or not char.is_white(byte) then local range = self.insert_range
return idx if range then
end local start = math.min(range.start.character + 1, offset)
end for idx = start, self.source_offset do
return offset local byte = string.byte(self.context.cursor_line, idx)
end) if byte == nil or not char.is_white(byte) then
return idx
end
end end
else return offset
-- NOTE end
-- The VSCode does not implement this but it's useful if the server does not care about word patterns. else
-- We should care about this performance. -- NOTE
local word = self:get_word() -- The VSCode does not implement this but it's useful if the server does not care about word patterns.
for idx = self.source_offset - 1, self.source_offset - #word, -1 do -- We should care about this performance.
if char.is_semantic_index(self.context.cursor_line, idx) then local word = self.word
local c = string.byte(self.context.cursor_line, idx) for idx = self.source_offset - 1, self.source_offset - #word, -1 do
if char.is_white(c) then if char.is_semantic_index(self.context.cursor_line, idx) then
local c = string.byte(self.context.cursor_line, idx)
if char.is_white(c) then
break
end
local match = true
for i = 1, self.source_offset - idx do
local c1 = string.byte(word, i)
local c2 = string.byte(self.context.cursor_line, idx + i - 1)
if not c1 or not c2 or c1 ~= c2 then
match = false
break break
end end
local match = true end
for i = 1, self.source_offset - idx do if match then
local c1 = string.byte(word, i) offset = math.min(offset, idx)
local c2 = string.byte(self.context.cursor_line, idx + i - 1)
if not c1 or not c2 or c1 ~= c2 then
match = false
break
end
end
if match then
offset = math.min(offset, idx)
end
end end
end end
end end
return offset end
end) return offset
end
---@deprecated use entry.word instead
entry.get_word = function(self)
return self.word
end end
---Create word for vim.CompletedItem ---Create word for vim.CompletedItem
---NOTE: This method doesn't clear the cache after completionItem/resolve. ---NOTE: This method doesn't clear the cache after completionItem/resolve.
---@package
---@return string ---@return string
entry.get_word = function(self) entry._get_word = function(self)
return self.cache:ensure('get_word', function() --NOTE: This is nvim-cmp specific implementation.
--NOTE: This is nvim-cmp specific implementation. local completion_item = self.completion_item
if self:get_completion_item().word then if completion_item.word then
return self:get_completion_item().word return completion_item.word
end end
local word local word
if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then if completion_item.textEdit and not misc.empty(completion_item.textEdit.newText) then
word = str.trim(self:get_completion_item().textEdit.newText) word = str.trim(completion_item.textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = tostring(snippet.parse(word)) word = tostring(snippet.parse(word))
end
local overwrite = self:get_overwrite()
if 0 < overwrite[2] or self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0)
end
elseif not misc.empty(self:get_completion_item().insertText) then
word = str.trim(self:get_completion_item().insertText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(tostring(snippet.parse(word)))
end
else
word = str.trim(self:get_completion_item().label)
end end
return str.oneline(word) local overwrite = self:get_overwrite()
end) --[[@as string]] if 0 < overwrite[2] or completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0)
end
elseif not misc.empty(completion_item.insertText) then
word = str.trim(completion_item.insertText)
if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(tostring(snippet.parse(word)))
end
else
word = str.trim(completion_item.label)
end
return str.oneline(word)
end end
---Get overwrite information ---Get overwrite information
---@return integer[] ---@return integer[]
entry.get_overwrite = function(self) entry.get_overwrite = function(self)
return self.cache:ensure('get_overwrite', function() return self.cache:ensure('get_overwrite', entry._get_overwrite, self)
if self:get_completion_item().textEdit then
local range = self:get_insert_range()
if range then
return self.context.cache:ensure('entry:' .. 'get_overwrite:' .. tostring(range.start.character) .. ':' .. tostring(range['end'].character), function()
local vim_start = range.start.character + 1
local vim_end = range['end'].character + 1
local before = self.context.cursor.col - vim_start
local after = vim_end - self.context.cursor.col
return { before, after }
end)
end
end
return { 0, 0 }
end)
end end
---Create filter text ---@package
---@return string entry._get_overwrite = function(self)
entry.get_filter_text = function(self) if self.completion_item.textEdit then
return self.cache:ensure('get_filter_text', function() local range = self.insert_range
local word if range then
if self:get_completion_item().filterText then local vim_start = range.start.character + 1
word = self:get_completion_item().filterText local vim_end = range['end'].character + 1
else local before = self.context.cursor.col - vim_start
word = str.trim(self:get_completion_item().label) local after = vim_end - self.context.cursor.col
return { before, after }
end end
return word end
end) return { 0, 0 }
end
---@package
entry.get_filter_text = function(self)
return self.filter_text
end end
---Get LSP's insert text ---Get LSP's insert text
---@return string ---@return string
entry.get_insert_text = function(self) entry.get_insert_text = function(self)
return self.cache:ensure('get_insert_text', function() return self.cache:ensure('get_insert_text', entry._get_insert_text, self)
local word end
if self:get_completion_item().textEdit then
word = str.trim(self:get_completion_item().textEdit.newText) ---@package
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then entry._get_insert_text = function(self)
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') local completion_item = self.completion_item
end local word
elseif self:get_completion_item().insertText then if completion_item.textEdit then
word = str.trim(self:get_completion_item().insertText) word = str.trim(completion_item.textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end
else
word = str.trim(self:get_completion_item().label)
end end
return word elseif completion_item.insertText then
end) word = str.trim(completion_item.insertText)
if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end
else
word = str.trim(completion_item.label)
end
return word
end end
---Return the item is deprecated or not. ---Return the item is deprecated or not.
---@return boolean ---@return boolean
entry.is_deprecated = function(self) entry.is_deprecated = function(self)
return self:get_completion_item().deprecated or vim.tbl_contains(self:get_completion_item().tags or {}, types.lsp.CompletionItemTag.Deprecated) return self.completion_item.deprecated or vim.tbl_contains(self.completion_item.tags or {}, types.lsp.CompletionItemTag.Deprecated)
end end
---Return view information. ---Return view information.
@@ -204,161 +263,127 @@ end
---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } } ---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } }
entry.get_view = function(self, suggest_offset, entries_buf) entry.get_view = function(self, suggest_offset, entries_buf)
local item = self:get_vim_item(suggest_offset) local item = self:get_vim_item(suggest_offset)
return self.cache:ensure('get_view:' .. tostring(entries_buf), function() return self.cache:ensure('get_view:' .. tostring(entries_buf), entry._get_view, self, item, entries_buf)
local view = {} end
-- The result of vim.fn.strdisplaywidth depends on which buffer it was
-- called in because it reads the values of the option 'tabstop' when ---@package
-- rendering <Tab> characters. entry._get_view = function(self, item, entries_buf)
vim.api.nvim_buf_call(entries_buf, function() local view = {}
view.abbr = {} -- The result of vim.fn.strdisplaywidth depends on which buffer it was
view.abbr.text = item.abbr or '' -- called in because it reads the values of the option 'tabstop' when
view.abbr.bytes = #view.abbr.text -- rendering <Tab> characters.
view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text) vim.api.nvim_buf_call(entries_buf, function()
view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr') view.abbr = {}
view.kind = {} view.abbr.text = item.abbr or ''
view.kind.text = item.kind or '' view.abbr.bytes = #view.abbr.text
view.kind.bytes = #view.kind.text view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text)
view.kind.width = vim.fn.strdisplaywidth(view.kind.text) view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr')
view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or '')) view.kind = {}
view.menu = {} view.kind.text = item.kind or ''
view.menu.text = item.menu or '' view.kind.bytes = #view.kind.text
view.menu.bytes = #view.menu.text view.kind.width = vim.fn.strdisplaywidth(view.kind.text)
view.menu.width = vim.fn.strdisplaywidth(view.menu.text) view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or ''))
view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu' view.menu = {}
view.dup = item.dup view.menu.text = item.menu or ''
end) view.menu.bytes = #view.menu.text
return view view.menu.width = vim.fn.strdisplaywidth(view.menu.text)
view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu'
view.dup = item.dup
end) end)
return view
end end
---Make vim.CompletedItem ---Make vim.CompletedItem
---@param suggest_offset integer ---@param suggest_offset integer
---@return vim.CompletedItem ---@return vim.CompletedItem
entry.get_vim_item = function(self, suggest_offset) entry.get_vim_item = function(self, suggest_offset)
return self.cache:ensure('get_vim_item:' .. tostring(suggest_offset), function() return self.cache:ensure('get_vim_item:' .. tostring(suggest_offset), entry._get_vim_item, self, suggest_offset)
local completion_item = self:get_completion_item() end
local word = self:get_word()
local abbr = str.oneline(completion_item.label)
-- ~ indicator ---@package
local is_expandable = false entry._get_vim_item = function(self, suggest_offset)
local expandable_indicator = config.get().formatting.expandable_indicator local completion_item = self.completion_item
if #(completion_item.additionalTextEdits or {}) > 0 then local word = self.word
is_expandable = true local abbr = str.oneline(completion_item.label)
elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
is_expandable = self:get_insert_text() ~= word
elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
is_expandable = true
end
if expandable_indicator and is_expandable then
abbr = abbr .. '~'
end
-- append delta text -- ~ indicator
if suggest_offset < self:get_offset() then local is_expandable = false
word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word local expandable_indicator = config.get().formatting.expandable_indicator
end if #(completion_item.additionalTextEdits or {}) > 0 then
is_expandable = true
elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
is_expandable = self:get_insert_text() ~= word
elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
is_expandable = true
end
if expandable_indicator and is_expandable then
abbr = abbr .. '~'
end
-- labelDetails. -- append delta text
local menu = nil if suggest_offset < self.offset then
if completion_item.labelDetails then word = string.sub(self.context.cursor_before_line, suggest_offset, self.offset - 1) .. word
menu = '' end
if completion_item.labelDetails.detail then
menu = menu .. completion_item.labelDetails.detail -- labelDetails.
end local menu = nil
if completion_item.labelDetails.description then if completion_item.labelDetails then
menu = menu .. completion_item.labelDetails.description menu = ''
if completion_item.labelDetails.detail then
menu = menu .. completion_item.labelDetails.detail
end
if completion_item.labelDetails.description then
menu = menu .. completion_item.labelDetails.description
end
end
-- remove duplicated string.
if self.offset ~= self.context.cursor.col then
for i = 1, #word do
if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
word = string.sub(word, 1, i - 1)
break
end end
end end
end
-- remove duplicated string. local cmp_opts = completion_item.cmp or {}
if self:get_offset() ~= self.context.cursor.col then
for i = 1, #word do
if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
word = string.sub(word, 1, i - 1)
break
end
end
end
local cmp_opts = self:get_completion_item().cmp or {} local vim_item = {
word = word,
abbr = abbr,
kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
kind_hl_group = cmp_opts.kind_hl_group,
menu = menu,
dup = completion_item.dup or 1,
}
if config.get().formatting.format then
vim_item = config.get().formatting.format(self, vim_item)
end
vim_item.word = str.oneline(vim_item.word or '')
vim_item.abbr = str.oneline(vim_item.abbr or '')
vim_item.kind = str.oneline(vim_item.kind or '')
vim_item.menu = str.oneline(vim_item.menu or '')
vim_item.equal = 1
vim_item.empty = 1
local vim_item = { return vim_item
word = word,
abbr = abbr,
kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
kind_hl_group = cmp_opts.kind_hl_group,
menu = menu,
dup = self:get_completion_item().dup or 1,
}
if config.get().formatting.format then
vim_item = config.get().formatting.format(self, vim_item)
end
vim_item.word = str.oneline(vim_item.word or '')
vim_item.abbr = str.oneline(vim_item.abbr or '')
vim_item.kind = str.oneline(vim_item.kind or '')
vim_item.menu = str.oneline(vim_item.menu or '')
vim_item.equal = 1
vim_item.empty = 1
return vim_item
end)
end end
---Get commit characters ---Get commit characters
---@return string[] ---@return string[]
entry.get_commit_characters = function(self) entry.get_commit_characters = function(self)
return self:get_completion_item().commitCharacters or {} return self.completion_item.commitCharacters or {}
end end
---Return insert range ---@deprecated use entry.insert_range instead
---@return lsp.Range|nil
entry.get_insert_range = function(self) entry.get_insert_range = function(self)
local insert_range return self.insert_range
if self:get_completion_item().textEdit then
if self:get_completion_item().textEdit.insert then
insert_range = self:get_completion_item().textEdit.insert
else
insert_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
end
insert_range = self:convert_range_encoding(insert_range)
else
insert_range = {
start = {
line = self.context.cursor.row - 1,
character = self:get_offset() - 1,
},
['end'] = self.source_insert_range['end'],
}
end
return insert_range
end end
---Return replace range ---@deprecated use entry.replace_range instead
---@return lsp.Range|nil
entry.get_replace_range = function(self) entry.get_replace_range = function(self)
return self.cache:ensure('get_replace_range', function() return self.replace_range
local replace_range
if self:get_completion_item().textEdit then
if self:get_completion_item().textEdit.replace then
replace_range = self:get_completion_item().textEdit.replace
else
replace_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
end
replace_range = self:convert_range_encoding(replace_range)
end
if not replace_range or ((self.context.cursor.col - 1) == replace_range['end'].character) then
replace_range = {
start = {
line = self.source_replace_range.start.line,
character = self:get_offset() - 1,
},
['end'] = self.source_replace_range['end'],
}
end
return replace_range
end)
end end
---Match line. ---Match line.
@@ -366,82 +391,105 @@ end
---@param matching_config cmp.MatchingConfig ---@param matching_config cmp.MatchingConfig
---@return { score: integer, matches: table[] } ---@return { score: integer, matches: table[] }
entry.match = function(self, input, matching_config) entry.match = function(self, input, matching_config)
return self.match_cache:ensure(input .. ':' .. (self.resolved_completion_item and '1' or '0' .. ':') .. (matching_config.disallow_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_matching and '1' or '0') .. ':' .. (matching_config.disallow_prefix_unmatching and '1' or '0') .. ':' .. (matching_config.disallow_symbol_nonprefix_matching and '1' or '0'), function() -- https://www.lua.org/pil/11.6.html
local option = { -- do not use '..' to allocate multiple strings
disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching, local cache_key = string.format('%s:%d:%d:%d:%d:%d:%d', input, self.resolved_completion_item and 1 or 0, matching_config.disallow_fuzzy_matching and 1 or 0, matching_config.disallow_partial_matching and 1 or 0, matching_config.disallow_prefix_unmatching and 1 or 0, matching_config.disallow_partial_fuzzy_matching and 1 or 0, matching_config.disallow_symbol_nonprefix_matching and 1 or 0)
disallow_partial_fuzzy_matching = matching_config.disallow_partial_fuzzy_matching, local matched = self.match_cache:get(cache_key)
disallow_partial_matching = matching_config.disallow_partial_matching, if matched then
disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching, if self.match_view_args_ret and self.match_view_args_ret.input ~= input then
disallow_symbol_nonprefix_matching = matching_config.disallow_symbol_nonprefix_matching, self.match_view_args_ret.input = input
synonyms = { self.match_view_args_ret.word = matched._word
self:get_word(), self.match_view_args_ret.matches = matched.matches
self:get_completion_item().label, end
}, return matched
} end
matched = self:_match(input, matching_config)
self.match_cache:set(cache_key, matched)
return matched
end
local score, matches, filter_text, _ ---@package
local checked = {} ---@type table<string, boolean> entry._match = function(self, input, matching_config)
local completion_item = self.completion_item
local option = {
disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching,
disallow_partial_fuzzy_matching = matching_config.disallow_partial_fuzzy_matching,
disallow_partial_matching = matching_config.disallow_partial_matching,
disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching,
disallow_symbol_nonprefix_matching = matching_config.disallow_symbol_nonprefix_matching,
synonyms = {
self.word,
self.completion_item.label,
},
}
filter_text = self:get_filter_text() local score, matches, filter_text
checked[filter_text] = true local checked = {} ---@type table<string, boolean>
score, matches = matcher.match(input, filter_text, option)
-- Support the language server that doesn't respect VSCode's behaviors. filter_text = self.filter_text
if score == 0 then checked[filter_text] = true
if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then score, matches = matcher.match(input, filter_text, option)
local diff = self.source_offset - self:get_offset()
if diff > 0 then -- Support the language server that doesn't respect VSCode's behaviors.
local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff) if score == 0 then
local accept = nil if completion_item.textEdit and not misc.empty(completion_item.textEdit.newText) then
accept = accept or string.match(prefix, '^[^%a]+$') local diff = self.source_offset - self.offset
accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true) if diff > 0 then
if accept then local prefix = string.sub(self.context.cursor_line, self.offset, self.offset + diff)
filter_text = prefix .. self:get_filter_text() local accept = nil
if not checked[filter_text] then accept = accept or string.match(prefix, '^[^%a]+$')
checked[filter_text] = true accept = accept or string.find(completion_item.textEdit.newText, prefix, 1, true)
score, matches = matcher.match(input, filter_text, option) if accept then
end filter_text = prefix .. filter_text
if not checked[filter_text] then
checked[filter_text] = true
score, matches = matcher.match(input, filter_text, option)
end end
end end
end end
end end
end
-- Fix highlight if filterText is not the same to vim_item.abbr. -- Fix highlight if filterText is not the same to vim_item.abbr.
if score > 0 then if score > 0 then
local vim_item = self:get_vim_item(self.source_offset) self.match_view_args_ret = {
filter_text = vim_item.abbr or vim_item.word input = input,
if not checked[filter_text] then word = filter_text,
local diff = self.source_offset - self:get_offset() option = option,
_, matches = matcher.match(input:sub(1 + diff), filter_text, option) matches = matches,
end }
end end
return { score = score, matches = matches } return { score = score, matches = matches, _word = filter_text }
end)
end end
---Get resolved completion item if possible. ---@param view string
---@return lsp.CompletionItem entry.get_view_matches = function(self, view)
entry.get_completion_item = function(self) if self.match_view_args_ret then
return self.cache:ensure('get_completion_item', function() if self.match_view_args_ret.word == view then
if self.resolved_completion_item then return self.match_view_args_ret.matches
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts#L588
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/base/common/objects.ts#L89
-- @see https://github.com/microsoft/vscode/blob/a00f2e64f4fa9a1f774875562e1e9697d7138ed3/src/vs/editor/contrib/suggest/browser/suggest.ts#L147
local completion_item = misc.copy(self.completion_item)
for k, v in pairs(self.resolved_completion_item) do
completion_item[k] = v or completion_item[k]
end
return completion_item
end end
return self.completion_item self.match_view_args_ret.word = view
end) local input = self.match_view_args_ret.input
local diff = self.source_offset - self.offset
if diff > 0 then
input = input:sub(1 + diff)
end
local _, matches = matcher.match(input, view, self.match_view_args_ret.option)
self.match_view_args_ret.matches = matches
return matches
end
end
---@deprecated use entry.completion_item instead
entry.get_completion_item = function(self)
return self.completion_item
end end
---Create documentation ---Create documentation
---@return string[] ---@return string[]
entry.get_documentation = function(self) entry.get_documentation = function(self)
local item = self:get_completion_item() local item = self.completion_item
local documents = {} local documents = {}
@@ -483,13 +531,13 @@ end
---Get completion item kind ---Get completion item kind
---@return lsp.CompletionItemKind ---@return lsp.CompletionItemKind
entry.get_kind = function(self) entry.get_kind = function(self)
return self:get_completion_item().kind or types.lsp.CompletionItemKind.Text return self.completion_item.kind or types.lsp.CompletionItemKind.Text
end end
---Execute completion item's command. ---Execute completion item's command.
---@param callback fun() ---@param callback fun()
entry.execute = function(self, callback) entry.execute = function(self, callback)
self.source:execute(self:get_completion_item(), callback) self.source:execute(self.completion_item, callback)
end end
---Resolve completion item. ---Resolve completion item.
@@ -507,7 +555,8 @@ entry.resolve = function(self, callback)
if not completion_item then if not completion_item then
return return
end end
self.resolved_completion_item = self:fill_defaults(completion_item, self.item_defaults) self:_set_completion_item(completion_item)
self.resolved_completion_item = self.completion_item
self.cache:clear() self.cache:clear()
for _, c in ipairs(self.resolved_callbacks) do for _, c in ipairs(self.resolved_callbacks) do
c() c()
@@ -560,13 +609,18 @@ end
---Convert the oneline range encoding. ---Convert the oneline range encoding.
entry.convert_range_encoding = function(self, range) entry.convert_range_encoding = function(self, range)
local from_encoding = self.source:get_position_encoding_kind() local from_encoding = self.source.position_encoding
return self.context.cache:ensure('entry.convert_range_encoding:' .. range.start.character .. ':' .. range['end'].character .. ':' .. from_encoding, function() local cache_key = string.format('entry.convert_range_encoding:%d:%d:%s', range.start.character, range['end'].character, from_encoding)
return { local res = self.context.cache:get(cache_key)
start = types.lsp.Position.to_utf8(self.context.cursor_line, range.start, from_encoding), if res then
['end'] = types.lsp.Position.to_utf8(self.context.cursor_line, range['end'], from_encoding), return res
} end
end) res = {
start = types.lsp.Position.to_utf8(self.context.cursor_line, range.start, from_encoding),
['end'] = types.lsp.Position.to_utf8(self.context.cursor_line, range['end'], from_encoding),
}
self.context.cache:set(cache_key, res)
return res
end end
---Return true if the entry is invalid. ---Return true if the entry is invalid.

View File

@@ -11,8 +11,8 @@ describe('entry', function()
local e = entry.new(state.manual(), state.source(), { local e = entry.new(state.manual(), state.source(), {
label = '@', label = '@',
}) })
assert.are.equal(e:get_offset(), 3) assert.are.equal(e.offset, 3)
assert.are.equal(e:get_vim_item(e:get_offset()).word, '@') assert.are.equal(e:get_vim_item(e.offset).word, '@')
end) end)
it('word length (no fix)', function() it('word length (no fix)', function()
@@ -21,8 +21,8 @@ describe('entry', function()
local e = entry.new(state.manual(), state.source(), { local e = entry.new(state.manual(), state.source(), {
label = 'b', label = 'b',
}) })
assert.are.equal(e:get_offset(), 5) assert.are.equal(e.offset, 5)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b') assert.are.equal(e:get_vim_item(e.offset).word, 'b')
end) end)
it('word length (fix)', function() it('word length (fix)', function()
@@ -31,8 +31,8 @@ describe('entry', function()
local e = entry.new(state.manual(), state.source(), { local e = entry.new(state.manual(), state.source(), {
label = 'b.', label = 'b.',
}) })
assert.are.equal(e:get_offset(), 3) assert.are.equal(e.offset, 3)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b.') assert.are.equal(e:get_vim_item(e.offset).word, 'b.')
end) end)
it('semantic index (no fix)', function() it('semantic index (no fix)', function()
@@ -41,8 +41,8 @@ describe('entry', function()
local e = entry.new(state.manual(), state.source(), { local e = entry.new(state.manual(), state.source(), {
label = 'c.', label = 'c.',
}) })
assert.are.equal(e:get_offset(), 6) assert.are.equal(e.offset, 6)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'c.') assert.are.equal(e:get_vim_item(e.offset).word, 'c.')
end) end)
it('semantic index (fix)', function() it('semantic index (fix)', function()
@@ -51,8 +51,8 @@ describe('entry', function()
local e = entry.new(state.manual(), state.source(), { local e = entry.new(state.manual(), state.source(), {
label = 'bc.', label = 'bc.',
}) })
assert.are.equal(e:get_offset(), 3) assert.are.equal(e.offset, 3)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'bc.') assert.are.equal(e:get_vim_item(e.offset).word, 'bc.')
end) end)
it('[vscode-html-language-server] 1', function() it('[vscode-html-language-server] 1', function()
@@ -74,8 +74,8 @@ describe('entry', function()
newText = ' </div', newText = ' </div',
}, },
}) })
assert.are.equal(e:get_offset(), 5) assert.are.equal(e.offset, 5)
assert.are.equal(e:get_vim_item(e:get_offset()).word, '</div') assert.are.equal(e:get_vim_item(e.offset).word, '</div')
end) end)
it('[clangd] 1', function() it('[clangd] 1', function()
@@ -101,7 +101,7 @@ describe('entry', function()
}, },
}) })
assert.are.equal(e:get_vim_item(4).word, '->foo') assert.are.equal(e:get_vim_item(4).word, '->foo')
assert.are.equal(e:get_filter_text(), 'foo') assert.are.equal(e.filter_text, 'foo')
end) end)
it('[typescript-language-server] 1', function() it('[typescript-language-server] 1', function()
@@ -112,7 +112,7 @@ describe('entry', function()
}) })
-- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate. -- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate.
assert.are.equal(e:get_vim_item(18).word, '.catch') assert.are.equal(e:get_vim_item(18).word, '.catch')
assert.are.equal(e:get_filter_text(), 'catch') assert.are.equal(e.filter_text, 'catch')
end) end)
it('[typescript-language-server] 2', function() it('[typescript-language-server] 2', function()
@@ -136,7 +136,7 @@ describe('entry', function()
}, },
}) })
assert.are.equal(e:get_vim_item(18).word, '[Symbol]') assert.are.equal(e:get_vim_item(18).word, '[Symbol]')
assert.are.equal(e:get_filter_text(), '.Symbol') assert.are.equal(e.filter_text, '.Symbol')
end) end)
it('[lua-language-server] 1', function() it('[lua-language-server] 1', function()
@@ -163,7 +163,7 @@ describe('entry', function()
}, },
}) })
assert.are.equal(e:get_vim_item(19).word, 'cmp.config') assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
assert.are.equal(e:get_filter_text(), 'cmp.config') assert.are.equal(e.filter_text, 'cmp.config')
-- press ' -- press '
state.input("'") state.input("'")
@@ -185,7 +185,7 @@ describe('entry', function()
}, },
}) })
assert.are.equal(e:get_vim_item(19).word, 'cmp.config') assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
assert.are.equal(e:get_filter_text(), 'cmp.config') assert.are.equal(e.filter_text, 'cmp.config')
end) end)
it('[lua-language-server] 2', function() it('[lua-language-server] 2', function()
@@ -212,7 +212,7 @@ describe('entry', function()
}, },
}) })
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
assert.are.equal(e:get_filter_text(), 'lua.cmp.config') assert.are.equal(e.filter_text, 'lua.cmp.config')
-- press ' -- press '
state.input("'") state.input("'")
@@ -234,7 +234,7 @@ describe('entry', function()
}, },
}) })
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
assert.are.equal(e:get_filter_text(), 'lua.cmp.config') assert.are.equal(e.filter_text, 'lua.cmp.config')
end) end)
it('[intelephense] 1', function() it('[intelephense] 1', function()
@@ -260,8 +260,8 @@ describe('entry', function()
}, },
}, },
}) })
assert.are.equal(e:get_vim_item(e:get_offset()).word, '$this') assert.are.equal(e:get_vim_item(e.offset).word, '$this')
assert.are.equal(e:get_filter_text(), '$this') assert.are.equal(e.filter_text, '$this')
end) end)
it('[odin-language-server] 1', function() it('[odin-language-server] 1', function()
@@ -285,7 +285,7 @@ describe('entry', function()
label = 'string', label = 'string',
tags = {}, tags = {},
}) })
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string') assert.are.equal(e:get_vim_item(e.offset).word, 'string')
end) end)
it('[#47] word should not contain \\n character', function() it('[#47] word should not contain \\n character', function()
@@ -299,8 +299,8 @@ describe('entry', function()
insertTextFormat = 1, insertTextFormat = 1,
insertText = '__init__(self) -> None:\n pass', insertText = '__init__(self) -> None:\n pass',
}) })
assert.are.equal(e:get_vim_item(e:get_offset()).word, '__init__(self) -> None:') assert.are.equal(e:get_vim_item(e.offset).word, '__init__(self) -> None:')
assert.are.equal(e:get_filter_text(), '__init__') assert.are.equal(e.filter_text, '__init__')
end) end)
-- I can't understand this test case... -- I can't understand this test case...
@@ -360,7 +360,7 @@ describe('entry', function()
}, },
}, },
}) })
assert.are.equal(e:get_offset(), 12) assert.are.equal(e.offset, 12)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'getPath()') assert.are.equal(e:get_vim_item(e.offset).word, 'getPath()')
end) end)
end) end)

View File

@@ -24,6 +24,9 @@ local char = require('cmp.utils.char')
---@field public completion_context lsp.CompletionContext|nil ---@field public completion_context lsp.CompletionContext|nil
---@field public status cmp.SourceStatus ---@field public status cmp.SourceStatus
---@field public complete_dedup function ---@field public complete_dedup function
---@field public default_replace_range lsp.Range
---@field public default_insert_range lsp.Range
---@field public position_encoding lsp.PositionEncodingKind
local source = {} local source = {}
---@alias cmp.SourceStatus 1 | 2 | 3 ---@alias cmp.SourceStatus 1 | 2 | 3
@@ -41,6 +44,7 @@ source.new = function(name, s)
self.cache = cache.new() self.cache = cache.new()
self.complete_dedup = async.dedup() self.complete_dedup = async.dedup()
self.revision = 0 self.revision = 0
self.position_encoding = self:get_position_encoding_kind()
self:reset() self:reset()
return self return self
end end
@@ -108,7 +112,7 @@ source.get_entries = function(self, ctx)
local entries = {} local entries = {}
local matching_config = self:get_matching_config() local matching_config = self:get_matching_config()
for _, e in ipairs(target_entries) do for _, e in ipairs(target_entries) do
local o = e:get_offset() local o = e.offset
if not inputs[o] then if not inputs[o] then
inputs[o] = string.sub(ctx.cursor_before_line, o) inputs[o] = string.sub(ctx.cursor_before_line, o)
end end
@@ -118,7 +122,7 @@ source.get_entries = function(self, ctx)
e.exact = false e.exact = false
if e.score >= 1 then if e.score >= 1 then
e.matches = match.matches e.matches = match.matches
e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o] e.exact = e.filter_text == inputs[o] or e.word == inputs[o]
if entry_filter(e, ctx) then if entry_filter(e, ctx) then
entries[#entries + 1] = e entries[#entries + 1] = e
@@ -138,46 +142,46 @@ source.get_entries = function(self, ctx)
end end
---Get default insert range (UTF8 byte index). ---Get default insert range (UTF8 byte index).
---@package
---@return lsp.Range ---@return lsp.Range
source.get_default_insert_range = function(self) source._get_default_insert_range = function(self)
if not self.context then return {
error('context is not initialized yet.') start = {
end line = self.context.cursor.row - 1,
character = self.offset - 1,
return self.cache:ensure({ 'get_default_insert_range', tostring(self.revision) }, function() },
return { ['end'] = {
start = { line = self.context.cursor.row - 1,
line = self.context.cursor.row - 1, character = self.context.cursor.col - 1,
character = self.offset - 1, },
}, }
['end'] = {
line = self.context.cursor.row - 1,
character = self.context.cursor.col - 1,
},
}
end)
end end
---Get default replace range (UTF8 byte index). ---Get default replace range (UTF8 byte index).
---@package
---@return lsp.Range ---@return lsp.Range
source.get_default_replace_range = function(self) source._get_default_replace_range = function(self)
if not self.context then local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset))
error('context is not initialized yet.') return {
end start = {
line = self.context.cursor.row - 1,
character = self.offset,
},
['end'] = {
line = self.context.cursor.row - 1,
character = (e and self.offset + e - 2 or self.context.cursor.col - 1),
},
}
end
return self.cache:ensure({ 'get_default_replace_range', tostring(self.revision) }, function() ---@deprecated use source.default_insert_range instead
local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset)) source.get_default_insert_range = function(self)
return { return self.default_insert_range
start = { end
line = self.context.cursor.row - 1,
character = self.offset, ---@deprecated use source.default_replace_range instead
}, source.get_default_replae_range = function(self)
['end'] = { return self.default_replace_range
line = self.context.cursor.row - 1,
character = (e and self.offset + e - 2 or self.context.cursor.col - 1),
},
}
end)
end end
---Return source name. ---Return source name.
@@ -322,6 +326,9 @@ source.complete = function(self, ctx, callback)
self.offset = offset self.offset = offset
self.request_offset = offset self.request_offset = offset
self.context = ctx self.context = ctx
self.default_replace_range = self:_get_default_replace_range()
self.default_insert_range = self:_get_default_insert_range()
self.position_encoding = self:get_position_encoding_kind()
self.completion_context = completion_context self.completion_context = completion_context
self.source:complete( self.source:complete(
vim.tbl_extend('keep', misc.copy(self:get_source_config()), { vim.tbl_extend('keep', misc.copy(self:get_source_config()), {
@@ -346,11 +353,11 @@ source.complete = function(self, ctx, callback)
self.status = source.SourceStatus.COMPLETED self.status = source.SourceStatus.COMPLETED
self.entries = {} self.entries = {}
for _, item in ipairs(response.items or response) do for _, item in ipairs(response.items or response) do
if (item or {}).label then if item.label then
local e = entry.new(ctx, self, item, response.itemDefaults) local e = entry.new(ctx, self, item, response.itemDefaults)
if not e:is_invalid() then if not e:is_invalid() then
table.insert(self.entries, e) table.insert(self.entries, e)
self.offset = math.min(self.offset, e:get_offset()) self.offset = math.min(self.offset, e.offset)
end end
end end
end end

View File

@@ -65,9 +65,8 @@ lsp.Position = {
return position return position
end end
local ok, byteindex = pcall(function() local ok, byteindex = pcall(vim.str_byteindex,
return vim.str_byteindex(text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16) text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16)
end)
if not ok then if not ok then
return position return position
end end

View File

@@ -30,12 +30,12 @@ end
---Ensure value by callback ---Ensure value by callback
---@generic T ---@generic T
---@param key string|string[] ---@param key string|string[]
---@param callback fun(): T ---@param callback fun(...): T
---@return T ---@return T
cache.ensure = function(self, key, callback) cache.ensure = function(self, key, callback, ...)
local value = self:get(key) local value = self:get(key)
if value == nil then if value == nil then
local v = callback() local v = callback(...)
self:set(key, v) self:set(key, v)
return v return v
end end

View File

@@ -99,7 +99,7 @@ view.open = function(self, ctx, sources)
for _, e in ipairs(s:get_entries(ctx)) do for _, e in ipairs(s:get_entries(ctx)) do
e.score = e.score + priority e.score = e.score + priority
table.insert(group_entries, e) table.insert(group_entries, e)
offset = math.min(offset, e:get_offset()) offset = math.min(offset, e.offset)
end end
end end
end end

View File

@@ -80,7 +80,7 @@ custom_entries_view.new = function()
o = o + v[field].bytes + (self.column_width[field] - v[field].width) + 1 o = o + v[field].bytes + (self.column_width[field] - v[field].width) + 1
end end
for _, m in ipairs(e.matches or {}) do for _, m in ipairs(e:get_view_matches(v.abbr.text) or {}) do
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, a + m.word_match_start - 1, { vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, a + m.word_match_start - 1, {
end_line = i, end_line = i,
end_col = a + m.word_match_end, end_col = a + m.word_match_end,

View File

@@ -89,7 +89,7 @@ ghost_text_view.text_gen = function(self, line, cursor_col)
word = tostring(snippet.parse(word)) word = tostring(snippet.parse(word))
end end
local word_clen = vim.str_utfindex(word) local word_clen = vim.str_utfindex(word)
local cword = string.sub(line, self.entry:get_offset(), cursor_col) local cword = string.sub(line, self.entry.offset, cursor_col)
local cword_clen = vim.str_utfindex(cword) local cword_clen = vim.str_utfindex(cword)
-- Number of characters from entry text (word) to be displayed as ghost thext -- Number of characters from entry text (word) to be displayed as ghost thext
local nchars = word_clen - cword_clen local nchars = word_clen - cword_clen

View File

@@ -74,7 +74,7 @@ wildmenu_entries_view.new = function()
}) })
end end
for _, m in ipairs(e.matches or {}) do for _, m in ipairs(e:get_view_matches(view.abbr.text) or {}) do
vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i] + m.word_match_start - 1, { vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i] + m.word_match_start - 1, {
end_line = 0, end_line = 0,
end_col = self.offsets[i] + m.word_match_end, end_col = self.offsets[i] + m.word_match_end,