diff --git a/lua/epo/init.lua b/lua/epo/init.lua index 1f128c6..22fbad4 100644 --- a/lua/epo/init.lua +++ b/lua/epo/init.lua @@ -9,6 +9,9 @@ local ns = api.nvim_create_namespace('Epo') local match_fuzzy = false local signature = false local debounce_time = 100 +-- Ctrl-Y will trigger TextChangedI again +-- avoid completion redisplay add a status check +local disable = false local cmp_data = {} local function buf_data_init(bufnr) @@ -57,120 +60,51 @@ local function lspkind(kind) return k:lower():sub(1, 1) end -local function completion_handler(_, result, ctx) - local client = lsp.get_clients({ id = ctx.client_id }) - if not result or not client or not api.nvim_buf_is_valid(ctx.bufnr) then - return - end - local entrys = {} - - local compitems - if vim.tbl_islist(result) then - compitems = result - else - compitems = result.items - cmp_data[ctx.bufnr].incomplete[ctx.client_id] = result.isIncomplete or false - end - - local col = vfn.charcol('.') - local line = api.nvim_get_current_line() - local before_text = col == 1 and '' or line:sub(1, col - 1) - - -- Get the start position of the current keyword - local ok, retval = pcall(vfn.matchstrpos, before_text, '\\k*$') - if not ok or not #retval == 0 then - return - end - local prefix, start_idx = unpack(retval) - cmp_data[ctx.bufnr].startidx = start_idx - local startcol = start_idx + 1 - prefix = prefix:lower() - - for _, item in ipairs(compitems) do - local entry = { - abbr = item.label, - kind = lspkind(item.kind), - icase = 1, - dup = 1, - empty = 1, - user_data = { - nvim = { - lsp = { - completion_item = item, - }, - }, - }, - } - - local textEdit = vim.tbl_get(item, 'textEdit') - if textEdit then - local start_col = #prefix ~= 0 and vfn.charidx(before_text, start_idx) + 1 or col - local range = {} - if textEdit.range then - range = textEdit.range - elseif textEdit.insert then - range = textEdit.insert - end - local te_startcol = charidx_without_comp(ctx.bufnr, range.start) - if te_startcol ~= start_col then - local offset = start_col - te_startcol - 1 - entry.word = textEdit.newText:sub(offset) - else - entry.word = textEdit.newText - end - elseif vim.tbl_get(item, 'insertText') then - entry.word = item.insertText - else - entry.word = item.label - end - - local register = true - if lsp.protocol.InsertTextFormat[item.insertTextFormat] == 'Snippet' then - entry.word = make_valid_word(entry.word) - -- entry.word = util.parse_snippet(entry.word) - elseif not cmp_data[ctx.bufnr].incomplete then - if #prefix ~= 0 then - local filter = item.filterText or entry.word - if - filter and (match_fuzzy and #vfn.matchfuzzy({ filter }, prefix) == 0) - or (not vim.startswith(filter:lower(), prefix) or not vim.startswith(filter, prefix)) - then - register = false - end - end +local function get_documentation(selected, param, bufnr) + lsp.buf_request(bufnr, ms.completionItem_resolve, param, function(_, result) + if not vim.tbl_get(result, 'documentation', 'value') then + return end - - if register then - if item.detail and #item.detail > 0 then - entry.menu = vim.split(item.detail, '\n', { trimempty = true })[1] - end - - if item.documentation and #item.documentation > 0 then - entry.info = item.info - end - - entry.score = item.sortText or item.label - entrys[#entrys + 1] = entry + local wininfo = api.nvim_complete_set_info(selected, result.documentation.value) + if not vim.tbl_isempty(wininfo) and wininfo.bufnr and api.nvim_buf_is_valid(wininfo.bufnr) then + vim.bo[wininfo.bufnr].filetype = 'markdown' end - end - - table.sort(entrys, function(a, b) - return a.score < b.score end) +end - local mode = api.nvim_get_mode()['mode'] - if mode == 'i' or mode == 'ic' then - vfn.complete(startcol, entrys) +local function show_info(cmp_info, bufnr) + if not cmp_info.items[cmp_info.selected + 1] then + return + end + local info = vim.tbl_get(cmp_info.items[cmp_info.selected + 1], 'info') + if not info or #info == 0 then + local param = vim.tbl_get( + cmp_info.items[cmp_info.selected + 1], + 'user_data', + 'nvim', + 'lsp', + 'completion_item' + ) + get_documentation(cmp_info.selected, param, bufnr) end end -local function completion_request(client, bufnr, trigger_kind, trigger_char) - local params = util.make_position_params(api.nvim_get_current_win(), client.offset_encoding) - params.context = { - triggerKind = trigger_kind, - triggerCharacter = trigger_char, - } - client.request(ms.textDocument_completion, params, completion_handler, bufnr) +local function complete_changed(bufnr) + api.nvim_create_autocmd('CompleteChanged', { + buffer = bufnr, + group = group, + once = true, + callback = function(args) + local cmp_info = vfn.complete_info() + if cmp_info.selected == -1 then + return + end + local build = vim.version().build + if build:match('^g') or build:match('dirty') then + show_info(cmp_info, args.buf) + end + end, + }) end local function signature_help(client, bufnr, lnum) @@ -252,7 +186,11 @@ local function complete_ondone(bufnr) api.nvim_create_autocmd('CompleteDone', { group = group, buffer = bufnr, + once = true, callback = function(args) + if not disable then + disable = true + end local item = vim.v.completed_item if not item or vim.tbl_isempty(item) then return @@ -301,51 +239,122 @@ local function complete_ondone(bufnr) }) end -local function get_documentation(selected, param, bufnr) - lsp.buf_request(bufnr, ms.completionItem_resolve, param, function(_, result) - if not vim.tbl_get(result, 'documentation', 'value') then - return +local function completion_handler(_, result, ctx) + local client = lsp.get_clients({ id = ctx.client_id }) + if not result or not client or not api.nvim_buf_is_valid(ctx.bufnr) then + return + end + local entrys = {} + + local compitems + if vim.tbl_islist(result) then + compitems = result + else + compitems = result.items + cmp_data[ctx.bufnr].incomplete[ctx.client_id] = result.isIncomplete or false + end + + local col = vfn.charcol('.') + local line = api.nvim_get_current_line() + local before_text = col == 1 and '' or line:sub(1, col - 1) + + -- Get the start position of the current keyword + local ok, retval = pcall(vfn.matchstrpos, before_text, '\\k*$') + if not ok or not #retval == 0 then + return + end + local prefix, start_idx = unpack(retval) + cmp_data[ctx.bufnr].startidx = start_idx + local startcol = start_idx + 1 + prefix = prefix:lower() + + for _, item in ipairs(compitems) do + local entry = { + abbr = item.label, + kind = lspkind(item.kind), + icase = 1, + dup = 1, + empty = 1, + user_data = { + nvim = { + lsp = { + completion_item = item, + }, + }, + }, + } + + local textEdit = vim.tbl_get(item, 'textEdit') + if textEdit then + local start_col = #prefix ~= 0 and vfn.charidx(before_text, start_idx) + 1 or col + local range = {} + if textEdit.range then + range = textEdit.range + elseif textEdit.insert then + range = textEdit.insert + end + local te_startcol = charidx_without_comp(ctx.bufnr, range.start) + if te_startcol ~= start_col then + local offset = start_col - te_startcol - 1 + entry.word = textEdit.newText:sub(offset) + else + entry.word = textEdit.newText + end + elseif vim.tbl_get(item, 'insertText') then + entry.word = item.insertText + else + entry.word = item.label end - local wininfo = api.nvim_complete_set_info(selected, result.documentation.value) - if not vim.tbl_isempty(wininfo) and wininfo.bufnr and api.nvim_buf_is_valid(wininfo.bufnr) then - vim.bo[wininfo.bufnr].filetype = 'markdown' + + local register = true + if lsp.protocol.InsertTextFormat[item.insertTextFormat] == 'Snippet' then + entry.word = make_valid_word(entry.word) + -- entry.word = util.parse_snippet(entry.word) + elseif not cmp_data[ctx.bufnr].incomplete then + if #prefix ~= 0 then + local filter = item.filterText or entry.word + if + filter and (match_fuzzy and #vfn.matchfuzzy({ filter }, prefix) == 0) + or (not vim.startswith(filter:lower(), prefix) or not vim.startswith(filter, prefix)) + then + register = false + end + end end - end) -end -local function show_info(cmp_info, bufnr) - if not cmp_info.items[cmp_info.selected + 1] then - return + if register then + if item.detail and #item.detail > 0 then + entry.menu = vim.split(item.detail, '\n', { trimempty = true })[1] + end + + if item.documentation and #item.documentation > 0 then + entry.info = item.info + end + + entry.score = item.sortText or item.label + entrys[#entrys + 1] = entry + end end - local info = vim.tbl_get(cmp_info.items[cmp_info.selected + 1], 'info') - if not info or #info == 0 then - local param = vim.tbl_get( - cmp_info.items[cmp_info.selected + 1], - 'user_data', - 'nvim', - 'lsp', - 'completion_item' - ) - get_documentation(cmp_info.selected, param, bufnr) + table.sort(entrys, function(a, b) + return a.score < b.score + end) + + local mode = api.nvim_get_mode()['mode'] + if mode == 'i' or mode == 'ic' then + vfn.complete(startcol, entrys) + complete_ondone(ctx.bufnr) + complete_changed(ctx.bufnr) end end -local function complete_changed(bufnr) - api.nvim_create_autocmd('CompleteChanged', { - buffer = bufnr, - group = group, - callback = function(args) - local cmp_info = vfn.complete_info() - if cmp_info.selected == -1 then - return - end - local build = vim.version().build - if build:match('^g') or build:match('dirty') then - show_info(cmp_info, args.buf) - end - end, - }) +local function completion_request(client, bufnr, trigger_kind, trigger_char) + local params = util.make_position_params(api.nvim_get_current_win(), client.offset_encoding) + params.context = { + triggerKind = trigger_kind, + triggerCharacter = trigger_char, + } + client.request(ms.textDocument_completion, params, completion_handler, bufnr) end local function debounce(client, bufnr, triggerKind, triggerChar) @@ -370,6 +379,10 @@ local function auto_complete(client, bufnr) group = group, buffer = bufnr, callback = function(args) + if disable then + disable = false + return + end local col = vfn.charcol('.') local line = api.nvim_get_current_line() if col == 0 or #line == 0 then @@ -377,36 +390,30 @@ local function auto_complete(client, bufnr) end local triggerKind = lsp.protocol.CompletionTriggerKind.Invoked local triggerChar = '' - - local ok, val = pcall(api.nvim_eval, ([['%s' !~ '\k']]):format(line:sub(col - 1, col - 1))) + local char = line:sub(col - 1, col - 1) + local ok, val = pcall(api.nvim_eval, ([['%s' !~ '\k']]):format(char)) if not ok then return end - if val ~= 0 then local triggerCharacters = client.server_capabilities.completionProvider.triggerCharacters or {} - if not vim.tbl_contains(triggerCharacters, line:sub(col - 1, col - 1)) then + if not vim.tbl_contains(triggerCharacters, char) then return end triggerKind = lsp.protocol.CompletionTriggerKind.TriggerCharacter - triggerChar = line:sub(col - 1, col - 1) + triggerChar = char end - if not cmp_data[args.buf] then buf_data_init(args.buf) end - debounce(client, args.buf, triggerKind, triggerChar) end, }) - - complete_ondone(bufnr) local build = vim.version().build if build:match('^g') or build:match('dirty') then api.nvim_set_option_value('completeopt', 'menu,noinsert,popup', { scope = 'global' }) end - complete_changed(bufnr) end local function register_cap() @@ -441,6 +448,7 @@ local function setup(opt) vim.notify('neovim version a bit old', vim.logs.level.WARN) end + -- Usually I just use one client for completion so just one api.nvim_create_autocmd('LspAttach', { group = group, callback = function(args) @@ -453,7 +461,7 @@ local function setup(opt) return end local created = api.nvim_get_autocmds({ - event = { 'TextChangedI', 'CompleteChanged', 'CompleteDone' }, + event = { 'TextChangedI' }, group = group, buffer = args.buf, })