diff --git a/README.md b/README.md index cd63286..633f388 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Concept - No flicker - Setup ==================== @@ -134,15 +133,14 @@ EOF ``` - ### Where can I find more completion sources? - See the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki/List-of-sources) - See the [GitHub topic](https://github.com/topics/nvim-cmp). - ### Where can I find advanced configuration examples? See the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki) + diff --git a/lua/cmp/core.lua b/lua/cmp/core.lua index eb280c3..49dd19e 100644 --- a/lua/cmp/core.lua +++ b/lua/cmp/core.lua @@ -16,7 +16,7 @@ local event = require('cmp.utils.event') local SOURCE_TIMEOUT = 500 local DEBOUNCE_TIME = 80 -local THROTTLE_TIME = 60 +local THROTTLE_TIME = 40 ---@class cmp.Core ---@field public suspending boolean diff --git a/lua/cmp/init.lua b/lua/cmp/init.lua index ac839cb..ffb1b69 100644 --- a/lua/cmp/init.lua +++ b/lua/cmp/init.lua @@ -5,6 +5,7 @@ local feedkeys = require('cmp.utils.feedkeys') local autocmd = require('cmp.utils.autocmd') local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') +local async = require('cmp.utils.async') local cmp = {} @@ -283,51 +284,48 @@ cmp.setup = setmetatable({ end, }) -autocmd.subscribe('InsertEnter', function() - feedkeys.call('', 'i', function() +-- In InsertEnter autocmd, vim will detects mode=normal unexpectedly. +autocmd.subscribe( + { 'InsertEnter', 'CmdlineEnter' }, + async.debounce_safe_state(function() if config.enabled() then + cmp.config.compare.scopes:update() + cmp.config.compare.locality:update() cmp.core:prepare() cmp.core:on_change('InsertEnter') end end) -end) +) -autocmd.subscribe('InsertLeave', function() - cmp.core:reset() - cmp.core.view:close() -end) +-- async.throttle is needed for performance. The mapping `:...` will fire `CmdlineChanged` for each character. +autocmd.subscribe( + { 'TextChangedI', 'TextChangedP', 'CmdlineChanged' }, + async.debounce_safe_state(function() + if config.enabled() then + cmp.core:on_change('TextChanged') + end + end) +) -autocmd.subscribe('CmdlineEnter', function() +-- If make this asynchronous, the completion menu will not close when the command output is displayed. +autocmd.subscribe({ 'InsertLeave', 'CmdlineLeave' }, function() if config.enabled() then - cmp.core:prepare() - cmp.core:on_change('InsertEnter') - end -end) - -autocmd.subscribe('CmdlineLeave', function() - cmp.core:reset() - cmp.core.view:close() -end) - -autocmd.subscribe('TextChanged', function() - if config.enabled() then - cmp.core:on_change('TextChanged') - end -end) - -autocmd.subscribe('CursorMoved', function() - if config.enabled() then - cmp.core:on_moved() - else cmp.core:reset() cmp.core.view:close() end end) -autocmd.subscribe('InsertEnter', function() - cmp.config.compare.scopes:update() - cmp.config.compare.locality:update() -end) +autocmd.subscribe( + 'CursorMovedI', + async.debounce_safe_state(function() + if config.enabled() then + cmp.core:on_moved() + else + cmp.core:reset() + cmp.core.view:close() + end + end) +) cmp.event:on('complete_done', function(evt) if evt.entry then diff --git a/lua/cmp/matcher.lua b/lua/cmp/matcher.lua index 7649f04..7a22d9e 100644 --- a/lua/cmp/matcher.lua +++ b/lua/cmp/matcher.lua @@ -66,9 +66,15 @@ end -- -- `candlesingle` -> candle#accept#single -- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~ --- -- * The `accept`'s `a` should not match to `candle`'s `a` -- +-- 7. Avoid false positive matching +-- +-- `,` -> print, +-- ~ +-- * Typically, the middle match with symbol characters only is false positive. should be ignored. +-- +-- ---Match entry ---@param input string ---@param word string @@ -100,12 +106,14 @@ matcher.match = function(input, word, option) local input_end_index = 1 local word_index = 1 local word_bound_index = 1 + local no_symbol_match = false while input_end_index <= #input and word_index <= #word do local m = matcher.find_match_region(input, input_start_index, input_end_index, word, word_index) if m and input_end_index <= m.input_match_end then m.index = word_bound_index input_start_index = m.input_match_start + 1 input_end_index = m.input_match_end + 1 + no_symbol_match = no_symbol_match or m.no_symbol_match word_index = char.get_next_semantic_index(word, m.word_match_end) table.insert(matches, m) else @@ -146,6 +154,10 @@ matcher.match = function(input, word, option) end end + if no_symbol_match and not prefix then + return 0, {} + end + -- Compute prefix match score local score = prefix and matcher.PREFIX_FACTOR or 0 local offset = prefix and matches[1].index - 1 or 0 @@ -260,6 +272,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, local word_offset = 0 local strict_count = 0 local match_count = 0 + local no_symbol_match = false while input_index <= #input and word_index + word_offset <= #word do local c1 = string.byte(input, input_index) local c2 = string.byte(word, word_index + word_offset) @@ -272,6 +285,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, strict_count = strict_count + (c1 == c2 and 1 or 0) match_count = match_count + 1 word_offset = word_offset + 1 + no_symbol_match = no_symbol_match or char.is_symbol(c1) else -- Match end (partial region) if input_match_start ~= -1 then @@ -281,6 +295,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, word_match_start = word_index, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, + no_symbol_match = no_symbol_match, fuzzy = false, } else @@ -298,6 +313,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, word_match_start = word_index, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, + no_symbol_match = no_symbol_match, fuzzy = false, } end diff --git a/lua/cmp/matcher_spec.lua b/lua/cmp/matcher_spec.lua index 768988e..c95dfc3 100644 --- a/lua/cmp/matcher_spec.lua +++ b/lua/cmp/matcher_spec.lua @@ -28,6 +28,9 @@ describe('matcher', function() assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list')) assert.is.truthy(matcher.match('2', '[[2021') >= 1) + assert.is.truthy(matcher.match(',', 'pri,') == 0) + assert.is.truthy(matcher.match('/', '/**') >= 1) + assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }) == matcher.match('true', 'true')) assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }) > matcher.match('g', 'dein#get', { 'dein#get' })) end) diff --git a/lua/cmp/utils/async.lua b/lua/cmp/utils/async.lua index 8c698e7..a536357 100644 --- a/lua/cmp/utils/async.lua +++ b/lua/cmp/utils/async.lua @@ -1,3 +1,5 @@ +local feedkeys = require('cmp.utils.feedkeys') + local async = {} ---@class cmp.AsyncThrottle @@ -109,4 +111,19 @@ async.sync = function(runner, timeout) end, 10, false) end +---Wait and callback for next safe state. +async.debounce_safe_state = function(callback) + local running = false + return function() + if running then + return + end + running = true + feedkeys.call('', 'n', function() + running = false + callback() + end) + end +end + return async diff --git a/lua/cmp/utils/autocmd.lua b/lua/cmp/utils/autocmd.lua index 4af766a..438e231 100644 --- a/lua/cmp/utils/autocmd.lua +++ b/lua/cmp/utils/autocmd.lua @@ -2,20 +2,38 @@ local debug = require('cmp.utils.debug') local autocmd = {} +autocmd.group = vim.api.nvim_create_augroup('___cmp___', { clear = true }) + autocmd.events = {} ---Subscribe autocmd ----@param event string +---@param events string|string[] ---@param callback function ---@return function -autocmd.subscribe = function(event, callback) - autocmd.events[event] = autocmd.events[event] or {} - table.insert(autocmd.events[event], callback) +autocmd.subscribe = function(events, callback) + events = type(events) == 'string' and { events } or events + + for _, event in ipairs(events) do + if not autocmd.events[event] then + autocmd.events[event] = {} + vim.api.nvim_create_autocmd(event, { + desc = ('nvim-cmp: autocmd: %s'):format(event), + group = autocmd.group, + callback = function() + autocmd.emit(event) + end, + }) + end + table.insert(autocmd.events[event], callback) + end + return function() - for i, callback_ in ipairs(autocmd.events[event]) do - if callback_ == callback then - table.remove(autocmd.events[event], i) - break + for _, event in ipairs(events) do + for i, callback_ in ipairs(autocmd.events[event]) do + if callback_ == callback then + table.remove(autocmd.events[event], i) + break + end end end end diff --git a/lua/cmp/utils/feedkeys.lua b/lua/cmp/utils/feedkeys.lua index e7810a7..cd20f60 100644 --- a/lua/cmp/utils/feedkeys.lua +++ b/lua/cmp/utils/feedkeys.lua @@ -7,22 +7,18 @@ feedkeys.call = setmetatable({ callbacks = {}, }, { __call = function(self, keys, mode, callback) - if vim.fn.reg_recording() ~= '' then - return feedkeys.call_macro(keys, mode, callback) - end - local is_insert = string.match(mode, 'i') ~= nil local is_immediate = string.match(mode, 'x') ~= nil local queue = {} if #keys > 0 then - table.insert(queue, { keymap.t('set lazyredraw'), 'n' }) - table.insert(queue, { keymap.t('set textwidth=0'), 'n' }) - table.insert(queue, { keymap.t('set eventignore=all'), 'n' }) + table.insert(queue, { keymap.t('setlocal lazyredraw'), 'n' }) + table.insert(queue, { keymap.t('setlocal textwidth=0'), 'n' }) + table.insert(queue, { keymap.t('setlocal backspace=2'), 'n' }) table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true }) - table.insert(queue, { keymap.t('set %slazyredraw'):format(vim.o.lazyredraw and '' or 'no'), 'n' }) - table.insert(queue, { keymap.t('set textwidth=%s'):format(vim.bo.textwidth or 0), 'n' }) - table.insert(queue, { keymap.t('set eventignore=%s'):format(vim.o.eventignore or ''), 'n' }) + table.insert(queue, { keymap.t('setlocal %slazyredraw'):format(vim.o.lazyredraw and '' or 'no'), 'n' }) + table.insert(queue, { keymap.t('setlocal textwidth=%s'):format(vim.bo.textwidth or 0), 'n' }) + table.insert(queue, { keymap.t('setlocal backspace=%s'):format(vim.go.backspace or 2), 'n' }) end if callback then @@ -54,57 +50,4 @@ misc.set(_G, { 'cmp', 'utils', 'feedkeys', 'call', 'run' }, function(id) return '' end) -feedkeys.call_macro = setmetatable({ - queue = {}, - current = nil, - timer = vim.loop.new_timer(), - running = false, -}, { - __call = function(self, keys, mode, callback) - local is_insert = string.match(mode, 'i') ~= nil - table.insert(self.queue, is_insert and 1 or #self.queue + 1, { - keys = keys, - mode = mode, - callback = callback, - }) - - if not self.running then - self.running = true - local consume - consume = vim.schedule_wrap(function() - if vim.fn.getchar(1) == 0 then - if self.current then - vim.cmd(('set backspace=%s'):format(self.current.backspace or '')) - vim.cmd(('set eventignore=%s'):format(self.current.eventignore or '')) - if self.current.callback then - self.current.callback() - end - self.current = nil - end - - local current = table.remove(self.queue, 1) - if current then - self.current = { - keys = current.keys, - callback = current.callback, - backspace = vim.o.backspace, - eventignore = vim.o.eventignore, - } - vim.api.nvim_feedkeys(keymap.t('set backspace=start'), 'n', true) - vim.api.nvim_feedkeys(keymap.t('set eventignore=all'), 'n', true) - vim.api.nvim_feedkeys(current.keys, string.gsub(current.mode, '[i]', ''), true) -- 'i' flag is manually resolved. - end - end - - if #self.queue ~= 0 or self.current then - vim.defer_fn(consume, 1) - else - self.running = false - end - end) - vim.defer_fn(consume, 1) - end - end, -}) - return feedkeys diff --git a/lua/cmp/utils/feedkeys_spec.lua b/lua/cmp/utils/feedkeys_spec.lua index a4e71f3..24fba71 100644 --- a/lua/cmp/utils/feedkeys_spec.lua +++ b/lua/cmp/utils/feedkeys_spec.lua @@ -23,6 +23,15 @@ describe('feedkeys', function() }) end) + it('bacckspace', function() + vim.cmd([[setlocal backspace=0]]) + feedkeys.call(keymap.t('iaiueo'), 'nx') + feedkeys.call(keymap.t('a'), 'nx') + assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { + 'aiu', + }) + end) + it('testability', function() feedkeys.call('i', 'n', function() feedkeys.call('', 'n', function() diff --git a/lua/cmp/utils/highlight.lua b/lua/cmp/utils/highlight.lua index cbe25f5..5df0b04 100644 --- a/lua/cmp/utils/highlight.lua +++ b/lua/cmp/utils/highlight.lua @@ -1,46 +1,30 @@ local highlight = {} highlight.keys = { - 'gui', - 'guifg', - 'guibg', - 'cterm', - 'ctermfg', - 'ctermbg', + 'fg', + 'bg', + 'bold', + 'italic', + 'reverse', + 'standout', + 'underline', + 'undercurl', + 'strikethrough', } -highlight.inherit = function(name, source, override) - local cmd = ('highlight default %s'):format(name) +highlight.inherit = function(name, source, settings) for _, key in ipairs(highlight.keys) do - if override[key] then - cmd = cmd .. (' %s=%s'):format(key, override[key]) - else - local v = highlight.get(source, key) - v = v == '' and 'NONE' or v - cmd = cmd .. (' %s=%s'):format(key, v) - end - end - vim.cmd(cmd) -end - -highlight.get = function(source, key) - if key == 'gui' or key == 'cterm' then - local ui = {} - for _, k in ipairs({ 'bold', 'italic', 'reverse', 'inverse', 'standout', 'underline', 'undercurl', 'strikethrough' }) do - if vim.fn.synIDattr(vim.fn.hlID(source), k, key) == 1 then - table.insert(ui, k) + if not settings[key] then + local v = vim.fn.synIDattr(vim.fn.hlID(source), key) + if key ~= 'fg' and key ~= 'bg' then + v = v == 1 + end + if v then + settings[key] = v == '' and 'NONE' or v end end - return table.concat(ui, ',') - elseif key == 'guifg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'fg#', 'gui') - elseif key == 'guibg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'bg#', 'gui') - elseif key == 'ctermfg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'fg', 'term') - elseif key == 'ctermbg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'bg', 'term') end + vim.api.nvim_set_hl(0, name, settings) end return highlight diff --git a/plugin/cmp.lua b/plugin/cmp.lua index 27599fa..1f0bdfe 100644 --- a/plugin/cmp.lua +++ b/plugin/cmp.lua @@ -3,126 +3,54 @@ if vim.g.loaded_cmp then end vim.g.loaded_cmp = true -local api = require "cmp.utils.api" -local misc = require('cmp.utils.misc') +local api = require('cmp.utils.api') local types = require('cmp.types') -local config = require('cmp.config') local highlight = require('cmp.utils.highlight') +local autocmd = require('cmp.utils.autocmd') --- TODO: https://github.com/neovim/neovim/pull/14661 -vim.cmd [[ - augroup ___cmp___ - autocmd! - autocmd InsertEnter * lua require'cmp.utils.autocmd'.emit('InsertEnter') - autocmd InsertLeave * lua require'cmp.utils.autocmd'.emit('InsertLeave') - autocmd TextChangedI,TextChangedP * lua require'cmp.utils.autocmd'.emit('TextChanged') - autocmd CursorMovedI * lua require'cmp.utils.autocmd'.emit('CursorMoved') - autocmd CompleteChanged * lua require'cmp.utils.autocmd'.emit('CompleteChanged') - autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone') - autocmd ColorScheme * call v:lua.cmp.plugin.colorscheme() - autocmd CmdlineEnter * call v:lua.cmp.plugin.cmdline.enter() - autocmd CmdwinEnter * call v:lua.cmp.plugin.cmdline.leave() " for entering cmdwin with `` - augroup END -]] - -misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'enter' }, function() - if config.is_native_menu() then - return - end - if vim.fn.expand('') ~= '=' then - vim.schedule(function() - if api.is_cmdline_mode() then - vim.cmd [[ - augroup cmp-cmdline - autocmd! - autocmd CmdlineChanged * lua require'cmp.utils.autocmd'.emit('TextChanged') - autocmd CmdlineLeave * call v:lua.cmp.plugin.cmdline.leave() - augroup END - ]] - require('cmp.utils.autocmd').emit('CmdlineEnter') - end - end) - end -end) - -misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'leave' }, function() - if vim.fn.expand('') ~= '=' then - vim.cmd [[ - augroup cmp-cmdline - autocmd! - augroup END - ]] - require('cmp.utils.autocmd').emit('CmdlineLeave') - end -end) - -misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function() - highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { - gui = 'NONE', - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { - gui = 'NONE', - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { - gui = 'NONE', - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemKindDefault', 'Special', { - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemMenuDefault', 'Pmenu', { - guibg = 'NONE', - ctermbg = 'NONE', - }) - for name in pairs(types.lsp.CompletionItemKind) do - if type(name) == 'string' then - vim.cmd(([[highlight default link CmpItemKind%sDefault CmpItemKind]]):format(name)) - end - end -end) -_G.cmp.plugin.colorscheme() - -vim.cmd [[ - highlight default link CmpItemAbbr CmpItemAbbrDefault - highlight default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault - highlight default link CmpItemAbbrMatch CmpItemAbbrMatchDefault - highlight default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault - highlight default link CmpItemKind CmpItemKindDefault - highlight default link CmpItemMenu CmpItemMenuDefault -]] - -for name in pairs(types.lsp.CompletionItemKind) do - if type(name) == 'string' then - local hi = ('CmpItemKind%s'):format(name) - if vim.fn.hlexists(hi) ~= 1 then - vim.cmd(([[highlight default link %s %sDefault]]):format(hi, hi)) - end +vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true }) +for kind in pairs(types.lsp.CompletionItemKind) do + if type(kind) == 'string' then + local name = ('CmpItemKind%s'):format(kind) + vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true }) end end +autocmd.subscribe('ColorScheme', function() + highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = true }) + highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = true }) + highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = true }) + highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = true }) + highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = true }) + highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = true }) + for name in pairs(types.lsp.CompletionItemKind) do + if type(name) == 'string' then + vim.api.nvim_set_hl(0, ('CmpItemKind%sDefault'):format(name), { link = 'CmpItemKind', default = true }) + end + end +end) +autocmd.emit('ColorScheme') + if vim.on_key then vim.on_key(function(keys) if keys == vim.api.nvim_replace_termcodes('', true, true, true) then vim.schedule(function() if not api.is_suitable_mode() then - require('cmp.utils.autocmd').emit('InsertLeave') + autocmd.emit('InsertLeave') end end) end end, vim.api.nvim_create_namespace('cmp.plugin')) end -vim.cmd [[command! CmpStatus lua require('cmp').status()]] +vim.api.nvim_create_user_command('CmpStatus', function() + require('cmp').status() +end, { desc = 'Check status of cmp sources' }) -vim.cmd [[doautocmd User CmpReady]] +vim.cmd([[doautocmd User CmpReady]])