From e820335208a6a46e97202592a3694d8e01b923bc Mon Sep 17 00:00:00 2001 From: hrsh7th <629908+hrsh7th@users.noreply.github.com> Date: Wed, 16 Nov 2022 01:27:15 +0900 Subject: [PATCH] LSP 3.17 (#1306) * 3.17 * PositionEncodingKind * Implement PositionEncodingKind * Remove luarc.json --- autoload/cmp.vim | 13 +++- doc/cmp.txt | 7 +++ lua/cmp/core.lua | 55 +++++++++------- lua/cmp/core_spec.lua | 55 +++++++++++++++- lua/cmp/entry.lua | 125 +++++++++++++++++++++++++++--------- lua/cmp/source.lua | 42 ++++++++----- lua/cmp/types/lsp.lua | 133 +++++++++++++++++++++++++++++++-------- lua/cmp/types/vim.lua | 2 +- lua/cmp/utils/keymap.lua | 15 +++++ 9 files changed, 353 insertions(+), 94 deletions(-) diff --git a/autoload/cmp.vim b/autoload/cmp.vim index 43b8cc2..a331d07 100644 --- a/autoload/cmp.vim +++ b/autoload/cmp.vim @@ -6,7 +6,16 @@ let s:sources = {} " function! cmp#register_source(name, source) abort let l:methods = [] - for l:method in ['is_available', 'get_debug_name', 'get_trigger_characters', 'get_keyword_pattern', 'complete', 'execute', 'resolve'] + for l:method in [ + \ 'is_available', + \ 'get_debug_name', + \ 'get_position_encoding_kind', + \ 'get_trigger_characters', + \ 'get_keyword_pattern', + \ 'complete', + \ 'execute', + \ 'resolve' + \ ] if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func call add(l:methods, l:method) endif @@ -39,6 +48,8 @@ function! cmp#_method(bridge_id, method, args) abort return l:source[a:method]() elseif a:method ==# 'get_debug_name' return l:source[a:method]() + elseif a:method ==# 'get_position_encoding_kind' + return l:source[a:method](a:args[0]) elseif a:method ==# 'get_keyword_pattern' return l:source[a:method](a:args[0]) elseif a:method ==# 'get_trigger_characters' diff --git a/doc/cmp.txt b/doc/cmp.txt index a321d2d..228ece9 100644 --- a/doc/cmp.txt +++ b/doc/cmp.txt @@ -735,6 +735,13 @@ Here is an example on how to create a custom source: return 'debug name' end + ---Return LSP's PositionEncodingKind. + ---@NOTE: If this method is ommited, the default value will be `utf-16`. + ---@return lsp.PositionEncodingKind + function source:get_position_encoding_kind() + return 'utf-16' + end + ---Return the keyword pattern for triggering completion (optional). ---If this is ommited, nvim-cmp will use a default keyword pattern. See |cmp-config.completion.keyword_pattern|. ---@return string diff --git a/lua/cmp/core.lua b/lua/cmp/core.lua index 4d1298a..cac9267 100644 --- a/lua/cmp/core.lua +++ b/lua/cmp/core.lua @@ -344,7 +344,10 @@ end, config.get().performance.throttle) ---@param callback function core.confirm = function(self, e, option, callback) if not (e and not e.confirmed) then - return callback() + if callback then + callback() + end + return end e.confirmed = true @@ -357,9 +360,10 @@ core.confirm = function(self, e, option, callback) feedkeys.call(keymap.indentkeys(), 'n') feedkeys.call('', 'n', function() + -- Emulate `` behavior to save `.` register. local ctx = context.new() local keys = {} - table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset()))) + table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset()))) table.insert(keys, e:get_word()) table.insert(keys, keymap.undobreak()) feedkeys.call(table.concat(keys, ''), 'in') @@ -368,7 +372,7 @@ core.confirm = function(self, e, option, callback) local ctx = context.new() if api.is_cmdline_mode() then local keys = {} - table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset()))) + table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset()))) table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset())) feedkeys.call(table.concat(keys, ''), 'in') else @@ -405,11 +409,15 @@ core.confirm = function(self, e, option, callback) return end vim.cmd([[silent! undojoin]]) - vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, 'utf-16') + vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, e.source:get_position_encoding_kind()) end) else vim.cmd([[silent! undojoin]]) - vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, 'utf-16') + vim.lsp.util.apply_text_edits( + e:get_completion_item().additionalTextEdits, + ctx.bufnr, + e.source:get_position_encoding_kind() + ) end end) feedkeys.call('', 'n', function() @@ -426,30 +434,33 @@ core.confirm = function(self, e, option, callback) completion_item.textEdit.range = e:get_insert_range() end - local diff_before = math.max(0, e.context.cursor.character - completion_item.textEdit.range.start.character) - local diff_after = math.max(0, completion_item.textEdit.range['end'].character - e.context.cursor.character) + local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1)) + local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col) local new_text = completion_item.textEdit.newText if api.is_insert_mode() then - local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet completion_item.textEdit.range.start.line = ctx.cursor.line - completion_item.textEdit.range.start.character = ctx.cursor.character - diff_before + completion_item.textEdit.range.start.character = (e.context.cursor.col - 1) - diff_before completion_item.textEdit.range['end'].line = ctx.cursor.line - completion_item.textEdit.range['end'].character = ctx.cursor.character + diff_after + completion_item.textEdit.range['end'].character = (ctx.cursor.col - 1) + diff_after + + local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet if is_snippet then completion_item.textEdit.newText = '' end - vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-16') + vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-8') + local texts = vim.split(completion_item.textEdit.newText, '\n') - local position = completion_item.textEdit.range.start - position.line = position.line + (#texts - 1) - if #texts == 1 then - position.character = position.character + misc.to_utfindex(texts[1]) - else - position.character = misc.to_utfindex(texts[#texts]) - end - local pos = types.lsp.Position.to_vim(0, position) - vim.api.nvim_win_set_cursor(0, { pos.row, pos.col - 1 }) + vim.api.nvim_win_set_cursor(0, { + completion_item.textEdit.range.start.line + #texts, + ( + #texts == 1 and ( + completion_item.textEdit.range.start.character + #texts[1] + ) or ( + #texts[#texts] + ) + ) + }) if is_snippet then config.get().snippet.expand({ body = new_text, @@ -458,8 +469,8 @@ core.confirm = function(self, e, option, callback) end else local keys = {} - table.insert(keys, string.rep(keymap.t(''), diff_before)) - table.insert(keys, string.rep(keymap.t(''), diff_after)) + table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1))) + table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character))) table.insert(keys, new_text) feedkeys.call(table.concat(keys, ''), 'in') end diff --git a/lua/cmp/core_spec.lua b/lua/cmp/core_spec.lua index c37090e..af98199 100644 --- a/lua/cmp/core_spec.lua +++ b/lua/cmp/core_spec.lua @@ -8,9 +8,19 @@ local api = require('cmp.utils.api') describe('cmp.core', function() describe('confirm', function() - local confirm = function(request, filter, completion_item) + ---@param request string + ---@param filter string + ---@param completion_item lsp.CompletionItem + ---@param option? { position_encoding_kind: lsp.PositionEncodingKind } + ---@return table + local confirm = function(request, filter, completion_item, option) + option = option or {} + local c = core.new() local s = source.new('spec', { + get_position_encoding_kind = function() + return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16 + end, complete = function(_, _, callback) callback({ completion_item }) end, @@ -23,7 +33,8 @@ describe('cmp.core', function() end) end) feedkeys.call(filter, 'n', function() - c:confirm(c.sources[s.id].entries[1], {}) + c:confirm(c.sources[s.id].entries[1], {}, function() + end) end) local state = {} feedkeys.call('', 'x', function() @@ -111,6 +122,46 @@ describe('cmp.core', function() assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) assert.are.same(state.cursor, { 2, 2 }) end) + + local char = '🗿' + for _, case in ipairs({ + { + encoding = types.lsp.PositionEncodingKind.UTF8, + char_size = #char + }, + { + encoding = types.lsp.PositionEncodingKind.UTF16, + char_size = select(2, vim.str_utfindex(char)) + }, + { + encoding = types.lsp.PositionEncodingKind.UTF32, + char_size = select(1, vim.str_utfindex(char)) + }, + }) do + it('textEdit & multibyte: ' .. case.encoding , function() + local state = confirm(keymap.t('i%s:%s%s:%s'):format(char, char, char, char), char, { + label = char .. char .. char, + textEdit = { + range = { + start = { + line = 0, + character = case.char_size + #':' + }, + ['end'] = { + line = 0, + character = case.char_size + #':' + case.char_size + case.char_size + }, + }, + newText = char .. char .. char .. char .. char, + } + }, { + position_encoding_kind = case.encoding + }) + vim.pretty_print({ state = state, case = case }) + assert.are.same(state.buffer, { ('%s:%s%s%s%s%s:%s'):format(char, char, char, char, char, char, char) }) + assert.are.same(state.cursor, { 1, #('%s:%s%s%s%s%s'):format(char, char, char, char, char, char) }) + end) + end end) describe('cmdline-mode', function() diff --git a/lua/cmp/entry.lua b/lua/cmp/entry.lua index 756dce7..833c6a7 100644 --- a/lua/cmp/entry.lua +++ b/lua/cmp/entry.lua @@ -29,8 +29,9 @@ local entry = {} ---@param ctx cmp.Context ---@param source cmp.Source ---@param completion_item lsp.CompletionItem +---@param item_defaults? lsp.internal.CompletionItemDefaults ---@return cmp.Entry -entry.new = function(ctx, source, completion_item) +entry.new = function(ctx, source, completion_item, item_defaults) local self = setmetatable({}, { __index = entry }) self.id = misc.id('entry.new') self.cache = cache.new() @@ -43,7 +44,7 @@ entry.new = function(ctx, source, completion_item) self.source_offset = source.request_offset self.source_insert_range = source:get_default_insert_range() self.source_replace_range = source:get_default_replace_range() - self.completion_item = completion_item + self.completion_item = self:fill_defaults(completion_item, item_defaults) self.resolved_completion_item = nil self.resolved_callbacks = {} self.resolving = false @@ -57,11 +58,10 @@ entry.get_offset = function(self) return self.cache:ensure('get_offset', function() local offset = self.source_offset if misc.safe(self:get_completion_item().textEdit) then - local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range) + local range = self:get_insert_range() if range then - offset = self.context.cache:ensure({ 'entry', 'get_offset', range.start.character }, function() - local c = misc.to_vimindex(self.context.cursor_line, range.start.character) - for idx = c, self.source_offset do + offset = self.context.cache:ensure({ 'entry', 'get_offset', tostring(range.start.character) }, function() + for idx = range.start.character + 1, self.source_offset do if not char.is_white(string.byte(self.context.cursor_line, idx)) then return idx end @@ -128,7 +128,7 @@ entry.get_word = function(self) word = str.trim(self:get_completion_item().label) end return str.oneline(word) - end) + end) --[[@as string]] end ---Get overwrite information @@ -136,15 +136,17 @@ end entry.get_overwrite = function(self) return self.cache:ensure('get_overwrite', function() if misc.safe(self:get_completion_item().textEdit) then - local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range) + local range = self:get_insert_range() if range then - return self.context.cache:ensure({ 'entry', 'get_overwrite', range.start.character, range['end'].character }, function() - local s = misc.to_vimindex(self.context.cursor_line, range.start.character) - local e = misc.to_vimindex(self.context.cursor_line, range['end'].character) - local before = self.context.cursor.col - s - local after = e - self.context.cursor.col - return { before, after } - end) + 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 } @@ -190,7 +192,8 @@ end ---Return the item is deprecated or not. ---@return boolean 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:get_completion_item().deprecated or + vim.tbl_contains(self:get_completion_item().tags or {}, types.lsp.CompletionItemTag.Deprecated) end ---Return view information. @@ -199,7 +202,7 @@ 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 } } entry.get_view = function(self, suggest_offset, entries_buf) local item = self:get_vim_item(suggest_offset) - return self.cache:ensure({ 'get_view', entries_buf }, function() + return self.cache:ensure({ 'get_view', tostring(entries_buf) }, function() local view = {} -- The result of vim.fn.strdisplaywidth depends on which buffer it was -- called in because it reads the values of the option 'tabstop' when @@ -214,7 +217,9 @@ entry.get_view = function(self, suggest_offset, entries_buf) view.kind.text = item.kind or '' view.kind.bytes = #view.kind.text view.kind.width = vim.fn.strdisplaywidth(view.kind.text) - view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or '')) + view.kind.hl_group = item.kind_hl_group or + ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or '') + ) view.menu = {} view.menu.text = item.menu or '' view.menu.bytes = #view.menu.text @@ -230,7 +235,7 @@ end ---@param suggest_offset integer ---@return vim.CompletedItem entry.get_vim_item = function(self, suggest_offset) - return self.cache:ensure({ 'get_vim_item', suggest_offset }, function() + return self.cache:ensure({ 'get_vim_item', tostring(suggest_offset) }, function() local completion_item = self:get_completion_item() local word = self:get_word() local abbr = str.oneline(completion_item.label) @@ -314,13 +319,17 @@ entry.get_insert_range = function(self) if misc.safe(self:get_completion_item().textEdit.insert) then insert_range = self:get_completion_item().textEdit.insert else - insert_range = self:get_completion_item().textEdit.range + insert_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]] end + insert_range = { + start = self:convert_position_encoding(insert_range.start), + ['end'] = self:convert_position_encoding(insert_range['end']), + } else insert_range = { start = { line = self.context.cursor.row - 1, - character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_insert_range.start.character), + character = self:get_offset() - 1, }, ['end'] = self.source_insert_range['end'], } @@ -337,15 +346,19 @@ entry.get_replace_range = function(self) if misc.safe(self:get_completion_item().textEdit.replace) then replace_range = self:get_completion_item().textEdit.replace else - replace_range = self:get_completion_item().textEdit.range + replace_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]] end + replace_range = { + start = self:convert_position_encoding(replace_range.start), + ['end'] = self:convert_position_encoding(replace_range['end']), + } end - if not replace_range or (self.context.cursor.character == replace_range['end'].character) then + 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 = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_replace_range.start.character), + character = self:get_offset() - 1 }, ['end'] = self.source_replace_range['end'], } @@ -361,10 +374,10 @@ end 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_matching and 1 or 0, - matching_config.disallow_prefix_unmatching and 1 or 0, + 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', }, function() local option = { disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching, @@ -492,4 +505,60 @@ entry.resolve = function(self, callback) end end +---@param completion_item lsp.CompletionItem +---@param defaults? lsp.internal.CompletionItemDefaults +---@return lsp.CompletionItem +entry.fill_defaults = function(_, completion_item, defaults) + defaults = defaults or {} + + if defaults.data then + completion_item.data = completion_item.data or defaults.data + end + + if defaults.commitCharacters then + completion_item.commitCharacters = completion_item.commitCharacters or defaults.commitCharacters + end + + if defaults.insertTextFormat then + completion_item.insertTextFormat = completion_item.insertTextFormat or defaults.insertTextFormat + end + + if defaults.insertTextMode then + completion_item.insertTextMode = completion_item.insertTextMode or defaults.insertTextMode + end + + if defaults.editRange then + if not completion_item.textEdit then + if defaults.editRange.insert then + completion_item.textEdit = { + insert = defaults.editRange.insert, + replace = defaults.editRange.replace, + newText = completion_item.textEditText or completion_item.label, + } + else + completion_item.textEdit = { + range = defaults.editRange --[[@as lsp.Range]] , + newText = completion_item.textEditText or completion_item.label, + } + end + end + end + + return completion_item +end + +---Convert the oneline range encoding. +entry.convert_position_encoding = function(self, position) + local from_encoding = self.source:get_position_encoding_kind() + return self.context.cache:ensure('entry.convert_position_encoding.' .. position.character .. '.' .. from_encoding, + function() + return types.lsp.Position.to_utf8( + self.context.cursor_line, + position, + from_encoding + ) + end + ) +end + return entry diff --git a/lua/cmp/source.lua b/lua/cmp/source.lua index f6b448e..4d7ee94 100644 --- a/lua/cmp/source.lua +++ b/lua/cmp/source.lua @@ -46,7 +46,6 @@ source.new = function(name, s) end ---Reset current completion state ----@return boolean source.reset = function(self) self.cache:clear() self.revision = self.revision + 1 @@ -124,7 +123,7 @@ source.get_entries = function(self, ctx) end end end - self.cache:set({ 'get_entries', self.revision, ctx.cursor_before_line }, entries) + self.cache:set({ 'get_entries', tostring(self.revision), ctx.cursor_before_line }, entries) local max_item_count = self:get_source_config().max_item_count or 200 local limited_entries = {} @@ -137,44 +136,44 @@ source.get_entries = function(self, ctx) return limited_entries end ----Get default insert range +---Get default insert range (UTF8 byte index). ---@return lsp.Range source.get_default_insert_range = function(self) if not self.context then error('context is not initialized yet.') end - return self.cache:ensure({ 'get_default_insert_range', self.revision }, function() + return self.cache:ensure({ 'get_default_insert_range', tostring(self.revision) }, function() return { start = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, self.offset), + character = self.offset - 1, }, ['end'] = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, self.context.cursor.col), + character = self.context.cursor.col - 1, }, } end) end ----Get default replace range +---Get default replace range (UTF8 byte index). ---@return lsp.Range source.get_default_replace_range = function(self) if not self.context then error('context is not initialized yet.') end - return self.cache:ensure({ 'get_default_replace_range', self.revision }, function() + return self.cache:ensure({ 'get_default_replace_range', tostring(self.revision) }, function() local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset)) return { start = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, self.offset), + character = self.offset, }, ['end'] = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, e and self.offset + e - 1 or self.context.cursor.col), + character = (e and self.offset + e - 2 or self.context.cursor.col - 1), }, } end) @@ -223,7 +222,10 @@ source.get_keyword_pattern = function(self) return c.keyword_pattern end if self.source.get_keyword_pattern then - return self.source:get_keyword_pattern(misc.copy(c)) + local keyword_pattern = self.source:get_keyword_pattern(misc.copy(c)) + if keyword_pattern then + return keyword_pattern + end end return config.get().completion.keyword_pattern end @@ -239,21 +241,30 @@ source.get_keyword_length = function(self) end ---Get filter ---@return function +--@return fun(entry: cmp.Entry, context: cmp.Context): boolean source.get_entry_filter = function(self) local c = self:get_source_config() if c.entry_filter then - return c.entry_filter + return c.entry_filter --[[@as fun(entry: cmp.Entry, context: cmp.Context): boolean]] end return function(_, _) return true end end +---Get lsp.PositionEncodingKind +---@return lsp.PositionEncodingKind +source.get_position_encoding_kind = function(self) + if self.source.get_position_encoding_kind then + return self.source:get_position_encoding_kind() + end + return types.lsp.PositionEncodingKind.UTF16 +end + ---Invoke completion ---@param ctx cmp.Context ---@param callback function ----@return boolean Return true if not trigger completion. +---@return boolean? Return true if not trigger completion. source.complete = function(self, ctx, callback) local offset = ctx:get_offset(self:get_keyword_pattern()) @@ -318,6 +329,7 @@ source.complete = function(self, ctx, callback) completion_context = completion_context, }), self.complete_dedup(vim.schedule_wrap(function(response) + ---@type lsp.CompletionResponse response = response or {} self.incomplete = response.isIncomplete or false @@ -331,7 +343,7 @@ source.complete = function(self, ctx, callback) self.entries = {} for i, item in ipairs(response.items or response) do if (misc.safe(item) or {}).label then - local e = entry.new(ctx, self, item) + local e = entry.new(ctx, self, item, response.itemDefaults) self.entries[i] = e self.offset = math.min(self.offset, e:get_offset()) end diff --git a/lua/cmp/types/lsp.lua b/lua/cmp/types/lsp.lua index 001207a..bd8a155 100644 --- a/lua/cmp/types/lsp.lua +++ b/lua/cmp/types/lsp.lua @@ -4,10 +4,18 @@ local misc = require('cmp.utils.misc') ---@class lsp local lsp = {} +---@enum lsp.PositionEncodingKind +lsp.PositionEncodingKind = { + UTF8 = 'utf-8', + UTF16 = 'utf-16', + UTF32 = 'utf-32', +} + lsp.Position = { ---Convert lsp.Position to vim.Position ---@param buf integer ---@param position lsp.Position + -- ---@return vim.Position to_vim = function(buf, position) if not vim.api.nvim_buf_is_loaded(buf) then @@ -45,11 +53,77 @@ lsp.Position = { character = position.col - 1, } end, + + ---Convert position to utf8 from specified encoding. + ---@param text string + ---@param position lsp.Position + ---@param from_encoding? lsp.PositionEncodingKind + ---@return lsp.Position + to_utf8 = function(text, position, from_encoding) + from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 + if from_encoding == lsp.PositionEncodingKind.UTF8 then + return position + end + + local ok, byteindex = pcall(function() + return vim.str_byteindex(text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16) + end) + if not ok then + return position + end + return { line = position.line, character = byteindex } + end, + + ---Convert position to utf16 from specified encoding. + ---@param text string + ---@param position lsp.Position + ---@param from_encoding? lsp.PositionEncodingKind + ---@return lsp.Position + to_utf16 = function(text, position, from_encoding) + from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 + if from_encoding == lsp.PositionEncodingKind.UTF16 then + return position + end + + local utf8 = lsp.Position.to_utf8(text, position, from_encoding) + for index = utf8.character, 0, -1 do + local ok, utf16index = pcall(function() + return select(2, vim.str_utfindex(text, index)) + end) + if ok then + return { line = utf8.line, character = utf16index } + end + end + return position + end, + + ---Convert position to utf32 from specified encoding. + ---@param text string + ---@param position lsp.Position + ---@param from_encoding? lsp.PositionEncodingKind + ---@return lsp.Position + to_utf32 = function(text, position, from_encoding) + from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 + if from_encoding == lsp.PositionEncodingKind.UTF32 then + return position + end + + local utf8 = lsp.Position.to_utf8(text, position, from_encoding) + for index = utf8.character, 0, -1 do + local ok, utf32index = pcall(function() + return select(1, vim.str_utfindex(text, index)) + end) + if ok then + return { line = utf8.line, character = utf32index } + end + end + return position + end } lsp.Range = { ---Convert lsp.Range to vim.Range - ---@param buf integer|string + ---@param buf integer ---@param range lsp.Range ---@return vim.Range to_vim = function(buf, range) @@ -60,7 +134,7 @@ lsp.Range = { end, ---Convert vim.Range to lsp.Range - ---@param buf integer|string + ---@param buf integer ---@param range vim.Range ---@return lsp.Range to_lsp = function(buf, range) @@ -130,15 +204,23 @@ lsp.CompletionItemKind = { } lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind) +---@class lsp.internal.CompletionItemDefaults +---@field public commitCharacters? string[] +---@field public editRange? lsp.Range | { insert: lsp.Range, replace: lsp.Range } +---@field public insertTextFormat? lsp.InsertTextFormat +---@field public insertTextMode? lsp.InsertTextMode +---@field public data? any + ---@class lsp.CompletionContext ---@field public triggerKind lsp.CompletionTriggerKind ---@field public triggerCharacter string|nil ---@class lsp.CompletionList ---@field public isIncomplete boolean +---@field public itemDefaults? lsp.internal.CompletionItemDefaults ---@field public items lsp.CompletionItem[] ----@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]|nil +---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[] ---@class lsp.MarkupContent ---@field public kind lsp.MarkupKind @@ -168,37 +250,38 @@ lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind) ---@field public newText string ---@class lsp.internal.ReplaceTextEdit ----@field public insert lsp.Range +---@field public replace lsp.Range ---@field public newText string ---@class lsp.CompletionItemLabelDetails ----@field public detail string|nil ----@field public description string|nil +---@field public detail? string +---@field public description? string ----@class lsp.Cmp +---@class lsp.internal.CmpCompletionExtension ---@field public kind_text string ---@field public kind_hl_group string ---@class lsp.CompletionItem ---@field public label string ----@field public labelDetails lsp.CompletionItemLabelDetails|nil ----@field public kind lsp.CompletionItemKind|nil ----@field public tags lsp.CompletionItemTag[]|nil ----@field public detail string|nil ----@field public documentation lsp.MarkupContent|string|nil ----@field public deprecated boolean|nil ----@field public preselect boolean|nil ----@field public sortText string|nil ----@field public filterText string|nil ----@field public insertText string|nil ----@field public insertTextFormat lsp.InsertTextFormat ----@field public insertTextMode lsp.InsertTextMode ----@field public textEdit lsp.TextEdit|lsp.InsertReplaceTextEdit|nil ----@field public additionalTextEdits lsp.TextEdit[] ----@field public commitCharacters string[]|nil ----@field public command lsp.Command|nil ----@field public data any|nil ----@field public cmp lsp.Cmp|nil +---@field public labelDetails? lsp.CompletionItemLabelDetails +---@field public kind? lsp.CompletionItemKind +---@field public tags? lsp.CompletionItemTag[] +---@field public detail? string +---@field public documentation? lsp.MarkupContent|string +---@field public deprecated? boolean +---@field public preselect? boolean +---@field public sortText? string +---@field public filterText? string +---@field public insertText? string +---@field public insertTextFormat? lsp.InsertTextFormat +---@field public insertTextMode? lsp.InsertTextMode +---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit +---@field public textEditText? string +---@field public additionalTextEdits? lsp.TextEdit[] +---@field public commitCharacters? string[] +---@field public command? lsp.Command +---@field public data? any +---@field public cmp? lsp.internal.CmpCompletionExtension --- ---TODO: Should send the issue for upstream? ---@field public word string|nil diff --git a/lua/cmp/types/vim.lua b/lua/cmp/types/vim.lua index e9d9075..8532534 100644 --- a/lua/cmp/types/vim.lua +++ b/lua/cmp/types/vim.lua @@ -11,7 +11,7 @@ ---@field public kind_hl_group string|nil ---@field public menu_hl_group string|nil ----@class vim.Position +---@class vim.Position 1-based index ---@field public row integer ---@field public col integer diff --git a/lua/cmp/utils/keymap.lua b/lua/cmp/utils/keymap.lua index 4b5c59c..48032b1 100644 --- a/lua/cmp/utils/keymap.lua +++ b/lua/cmp/utils/keymap.lua @@ -83,6 +83,21 @@ keymap.backspace = function(count) return table.concat(keys, '') end +---Create delete keys. +---@param count string|integer +---@return string +keymap.delete = function(count) + if type(count) == 'string' then + count = vim.fn.strchars(count, true) + end + if count <= 0 then + return '' + end + local keys = {} + table.insert(keys, keymap.t(string.rep('', count))) + return table.concat(keys, '') +end + ---Update indentkeys. ---@param expr? string ---@return string