diff --git a/lua/cmp/core.lua b/lua/cmp/core.lua index 9e29464..9bd5e6a 100644 --- a/lua/cmp/core.lua +++ b/lua/cmp/core.lua @@ -1,6 +1,7 @@ local debug = require('cmp.utils.debug') local char = require('cmp.utils.char') local pattern = require('cmp.utils.pattern') +local feedkeys = require('cmp.utils.feedkeys') local async = require('cmp.utils.async') local keymap = require('cmp.utils.keymap') local context = require('cmp.context') @@ -214,9 +215,9 @@ core.autoindent = function(self, trigger_event, callback) local release = self:suspend() vim.schedule(function() if cursor_before_line == api.get_cursor_before_line() then - keymap.feedkeys(keymap.t('setlocal cindent'), 'n') - keymap.feedkeys(keymap.t(''), 'n') - keymap.feedkeys(keymap.t('setlocal %scindent'):format(vim.bo.cindent and '' or 'no'), 'n', function() + feedkeys.call(keymap.t('setlocal cindent'), 'n') + feedkeys.call(keymap.t(''), 'n') + feedkeys.call(keymap.t('setlocal %scindent'):format(vim.bo.cindent and '' or 'no'), 'n', function() release() callback() end) @@ -318,12 +319,12 @@ core.confirm = function(self, e, option, callback) local confirm = {} table.insert(confirm, keymap.backspace(ctx.cursor.character - misc.to_utfindex(e.context.cursor_before_line, e:get_offset()))) table.insert(confirm, e:get_word()) - keymap.feedkeys(table.concat(confirm, ''), 'nt', function() + feedkeys.call(table.concat(confirm, ''), 'nt', function() -- Restore to the requested state. local restore = {} table.insert(restore, keymap.backspace(vim.str_utfindex(e:get_word()))) table.insert(restore, string.sub(e.context.cursor_before_line, e:get_offset())) - keymap.feedkeys(table.concat(restore, ''), 'n', function() + feedkeys.call(table.concat(restore, ''), 'n', function() --@see https://github.com/microsoft/vscode/blob/main/src/vs/editor/contrib/suggest/suggestController.ts#L334 if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then local pre = context.new() @@ -384,7 +385,7 @@ core.confirm = function(self, e, option, callback) table.insert(keys, completion_item.textEdit.newText) end - keymap.feedkeys(table.concat(keys, ''), 'n', function() + feedkeys.call(table.concat(keys, ''), 'n', function() if is_snippet then -- remove snippet prefix without changing `dot` register. local snippet_ctx = context.new() diff --git a/lua/cmp/init.lua b/lua/cmp/init.lua index fe5461f..6338010 100644 --- a/lua/cmp/init.lua +++ b/lua/cmp/init.lua @@ -1,6 +1,7 @@ local core = require('cmp.core') local source = require('cmp.source') local config = require('cmp.config') +local feedkeys = require('cmp.utils.feedkeys') local autocmd = require('cmp.utils.autocmd') local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') @@ -117,9 +118,9 @@ cmp.select_next_item = function(option) return true elseif vim.fn.pumvisible() == 1 then if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') else - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') end return true end @@ -142,9 +143,9 @@ cmp.select_prev_item = function(option) return true elseif vim.fn.pumvisible() == 1 then if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') else - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') end return true end @@ -182,7 +183,7 @@ cmp.confirm = function(option, callback) return true else if vim.fn.complete_info({ 'selected' }).selected ~= -1 then - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') return true end return false diff --git a/lua/cmp/utils/feedkeys.lua b/lua/cmp/utils/feedkeys.lua new file mode 100644 index 0000000..b8e3813 --- /dev/null +++ b/lua/cmp/utils/feedkeys.lua @@ -0,0 +1,106 @@ +local keymap = require('cmp.utils.keymap') +local misc = require('cmp.utils.misc') + +local feedkeys = {} + +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 queue = {} + if #keys > 0 then + table.insert(queue, { keymap.t('set backspace=start'), 'n' }) + table.insert(queue, { keymap.t('set eventignore=all'), 'n' }) + table.insert(queue, { keys, string.gsub(mode, '[it]', ''), true }) + table.insert(queue, { keymap.t('set backspace=%s'):format(vim.o.backspace or ''), 'n' }) + table.insert(queue, { keymap.t('set eventignore=%s'):format(vim.o.eventignore or ''), 'n' }) + end + if #keys > 0 or callback then + local id = misc.id('cmp.utils.feedkeys.call') + self.callbacks[id] = function() + if callback then + callback() + end + end + table.insert(queue, { keymap.t('call v:lua.cmp.utils.feedkeys.call.run(%s)'):format(id), 'n', true }) + end + + if is_insert then + for i = #queue, 1, -1 do + vim.api.nvim_feedkeys(queue[i][1], queue[i][2] .. 'i', queue[i][3]) + end + else + for i = 1, #queue do + vim.api.nvim_feedkeys(queue[i][1], queue[i][2], queue[i][3]) + end + end + end, +}) +misc.set(_G, { 'cmp', 'utils', 'feedkeys', 'call', 'run' }, function(id) + if feedkeys.call.callbacks[id] then + feedkeys.call.callbacks[id]() + feedkeys.call.callbacks[id] = nil + end + 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 new file mode 100644 index 0000000..abe0663 --- /dev/null +++ b/lua/cmp/utils/feedkeys_spec.lua @@ -0,0 +1,23 @@ +local spec = require('cmp.utils.spec') +local keymap = require('cmp.utils.keymap') + +local feedkeys = require('cmp.utils.feedkeys') + +describe('feedkeys', function() + before_each(spec.before) + + it('dot-repeat', function() + feedkeys.call(keymap.t('iaiueo'), 'nx') + assert.are.equal(vim.fn.getreg('.'), keymap.t('aiueo')) + end) + it('macro', function() + vim.fn.setreg('q', '') + vim.cmd([[normal! qq]]) + feedkeys.call(keymap.t('iaiueo'), 'nt') + feedkeys.call(keymap.t(''), 'nt', function() + vim.cmd([[normal! q]]) + assert.are.equal(vim.fn.getreg('q'), keymap.t('iaiueo')) + print(vim.fn.getreg('q')) + end) + end) +end) diff --git a/lua/cmp/utils/keymap.lua b/lua/cmp/utils/keymap.lua index c618988..272708e 100644 --- a/lua/cmp/utils/keymap.lua +++ b/lua/cmp/utils/keymap.lua @@ -98,154 +98,33 @@ keymap.equals = function(a, b) return keymap.t(a) == keymap.t(b) end ----Feedkeys with callback ----@param keys string ----@param mode string ----@param callback function -keymap.feedkeys = setmetatable({ - callbacks = {}, -}, { - __call = function(self, keys, mode, callback) - if vim.fn.reg_recording() ~= '' then - return keymap.feedkeys_macro_safe(keys, mode, callback) - end - - local is_insert = string.match(mode, 'i') ~= nil - - local queue = {} - if #keys > 0 then - table.insert(queue, { keymap.t('set backspace=start'), 'n' }) - table.insert(queue, { keymap.t('set eventignore=all'), 'n' }) - table.insert(queue, { keys, string.gsub(mode, '[it]', ''), true }) - table.insert(queue, { keymap.t('set backspace=%s'):format(vim.o.backspace or ''), 'n' }) - table.insert(queue, { keymap.t('set eventignore=%s'):format(vim.o.eventignore or ''), 'n' }) - end - if #keys > 0 or callback then - local id = misc.id('cmp.utils.keymap.feedkeys') - self.callbacks[id] = function() - if callback then - callback() - end - end - table.insert(queue, { keymap.t('call v:lua.cmp.utils.keymap.feedkeys.run(%s)'):format(id), 'n', true }) - end - - if is_insert then - for i = #queue, 1, -1 do - vim.api.nvim_feedkeys(queue[i][1], queue[i][2] .. 'i', queue[i][3]) - end - else - for i = 1, #queue do - vim.api.nvim_feedkeys(queue[i][1], queue[i][2], queue[i][3]) - end - end - end, -}) -misc.set(_G, { 'cmp', 'utils', 'keymap', 'feedkeys', 'run' }, function(id) - if keymap.feedkeys.callbacks[id] then - keymap.feedkeys.callbacks[id]() - keymap.feedkeys.callbacks[id] = nil - end - return '' -end) - ----Macro safe feedkeys. ----@param keys string ----@param mode string ----@param callback function -keymap.feedkeys_macro_safe = 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, -}) - ---Register keypress handler. keymap.listen = setmetatable({ cache = cache.new(), }, { __call = function(self, mode, keys_or_chars, callback) local keys = keymap.normalize(keymap.to_keymap(keys_or_chars)) - local bufnr = vim.api.nvim_get_current_buf() - local existing = keymap.find_map_by_lhs(mode, keys) - - local cur_definition = self.cache:get({ 'definition', self.cache:get({ 'id', mode, bufnr, keys }) or '' }) - if cur_definition then - local same = true - same = same and existing - same = same and cur_definition.existing.lhs == existing.lhs - same = same and cur_definition.existing.rhs == existing.rhs - same = same and cur_definition.existing.expr == existing.expr - same = same and cur_definition.existing.noremap == existing.noremap - same = same and cur_definition.existing.script == existing.script - if not existing or same then - return - end + local existing = keymap.get_mapping(mode, keys) + if not existing then + return end + local bufnr = existing.buffer and vim.api.nvim_get_current_buf() or '*' self.cache:set({ 'id', mode, bufnr, keys }, misc.id('cmp.utils.keymap.listen')) - existing = existing or { - lhs = keys, - rhs = keys, - expr = 0, - script = 0, - noremap = 1, - nowait = 0, - silent = 1, - } - local fallback = keymap.evacuate(mode, keys) - vim.api.nvim_buf_set_keymap(0, mode, keys, ('call v:lua.cmp.utils.keymap.listen.run(%s)'):format(self.cache:get({ 'id', mode, bufnr, keys })), { - expr = false, - noremap = true, - silent = true, - }) + if existing.buffer then + vim.api.nvim_buf_set_keymap(0, mode, keys, ('call v:lua.cmp.utils.keymap.listen.run(%s)'):format(self.cache:get({ 'id', mode, bufnr, keys })), { + expr = false, + noremap = true, + silent = true, + }) + else + vim.api.nvim_set_keymap(mode, keys, ('call v:lua.cmp.utils.keymap.listen.run(%s)'):format(self.cache:get({ 'id', mode, bufnr, keys })), { + expr = false, + noremap = true, + silent = true, + }) + end self.cache:set({ 'definition', self.cache:get({ 'id', mode, bufnr, keys }) }, { keys = keys, @@ -271,43 +150,97 @@ misc.set(_G, { 'cmp', 'utils', 'keymap', 'listen', 'run' }, function(id) return keymap.t('') end) +---Get mapping +---@param mode string +---@param lhs string +---@return table +keymap.get_mapping = function(mode, lhs) + lhs = keymap.normalize(lhs) + + for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do + if keymap.equals(map.lhs, lhs) then + if string.match(map.rhs, vim.pesc('v:lua.cmp.utils.keymap.listen.run')) then + return nil + end + return { + lhs = map.lhs, + rhs = map.rhs, + expr = map.expr == 1, + noremap = map.noremap == 1, + script = map.script == 1, + silent = map.silent == 1, + nowait = map.nowait == 1, + buffer = true, + } + end + end + + for _, map in ipairs(vim.api.nvim_get_keymap(mode)) do + if keymap.equals(map.lhs, lhs) then + if string.match(map.rhs, vim.pesc('v:lua.cmp.utils.keymap.listen.run')) then + return nil + end + return { + lhs = map.lhs, + rhs = map.rhs, + expr = map.expr == 1, + noremap = map.noremap == 1, + script = map.script == 1, + silent = map.silent == 1, + nowait = map.nowait == 1, + buffer = false, + } + end + end + return { + lhs = lhs, + rhs = lhs, + expr = false, + noremap = true, + script = false, + silent = false, + nowait = false, + buffer = false, + } +end + ---Evacuate existing key mapping ---@param mode string ---@param lhs string ---@return { keys: string, mode: string } keymap.evacuate = function(mode, lhs) - local map = keymap.find_map_by_lhs(mode, lhs) + local map = keymap.get_mapping(mode, lhs) if not map then return { keys = lhs, mode = 'itn' } end -- Keep existing mapping as mapping. We escape fisrt recursive key sequence. See `:help recursive_mapping`) local rhs = map.rhs - if map.noremap == 0 and map.expr == 1 then + if not map.noremap and map.expr then -- remap & expr mapping should evacuate as mapping with solving recursive mapping. rhs = string.format('v:lua.cmp.utils.keymap.evacuate.expr("%s", "%s", "%s")', mode, str.escape(keymap.escape(lhs), { '"' }), str.escape(keymap.escape(rhs), { '"' })) - elseif map.noremap ~= 0 and map.expr == 1 then + elseif map.noremap and map.expr then -- noremap & expr mapping should always evacuate as mapping. rhs = rhs - elseif map.script == 1 then + elseif map.script then -- script mapping should always evacuate as mapping. rhs = rhs - elseif map.noremap == 0 then + elseif not map.noremap then -- remap & non-expr mapping should be checked if recursive or not. rhs = keymap.recursive(mode, lhs, rhs) - if rhs == map.rhs or map.noremap ~= 0 then - return { keys = rhs, mode = 'it' .. (map.noremap == 1 and 'n' or '') } + if rhs == map.rhs or map.noremap then + return { keys = rhs, mode = 'it' .. (map.noremap and 'n' or '') } end else -- noremap & non-expr mapping doesn't need to evacuate. - return { keys = rhs, mode = 'it' .. (map.noremap == 1 and 'n' or '') } + return { keys = rhs, mode = 'it' .. (map.noremap and 'n' or '') } end local fallback = ('(cmp-utils-keymap-evacuate-rhs:%s)'):format(map.lhs) vim.api.nvim_buf_set_keymap(0, mode, fallback, rhs, { - expr = map.expr ~= 0, - noremap = map.noremap ~= 0, - script = map.script ~= 0, + expr = map.expr, + noremap = map.noremap, + script = map.script, silent = mode ~= 'c', -- I can't understand but it solves the #427 (wilder.nvim's mapping does not work if silent=true in cmdline mode...) }) return { keys = fallback, mode = 'it' } @@ -335,45 +268,4 @@ keymap.recursive = function(mode, lhs, rhs) return new_rhs end ----Get specific key mapping ----@param mode string ----@param lhs string ----@return table -keymap.find_map_by_lhs = function(mode, lhs) - for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do - if keymap.equals(map.lhs, lhs) then - if string.match(map.rhs, vim.pesc('v:lua.cmp.utils.keymap.listen.run')) then - return nil - end - return map - end - end - - for _, map in ipairs(vim.api.nvim_get_keymap(mode)) do - if keymap.equals(map.lhs, lhs) then - if string.match(map.rhs, vim.pesc('v:lua.cmp.utils.keymap.listen.run')) then - return nil - end - return map - end - end -end - -keymap.spec = function() - vim.fn.setreg('q', '') - vim.cmd([[normal! qq]]) - vim.schedule(function() - keymap.feedkeys('i', 'nt', function() - keymap.feedkeys(keymap.t('foo2'), 'n') - keymap.feedkeys(keymap.t('bar2'), 'nt') - keymap.feedkeys(keymap.t('baz2'), 'n', function() - vim.cmd([[normal! q]]) - end) - keymap.feedkeys(keymap.t('baz1'), 'ni') - keymap.feedkeys(keymap.t('bar1'), 'nti') - keymap.feedkeys(keymap.t('foo1'), 'ni') - end) - end) -end - return keymap diff --git a/lua/cmp/utils/keymap_spec.lua b/lua/cmp/utils/keymap_spec.lua index 257f6f4..2c705fb 100644 --- a/lua/cmp/utils/keymap_spec.lua +++ b/lua/cmp/utils/keymap_spec.lua @@ -17,23 +17,6 @@ describe('keymap', function() assert.are.equal(keymap.escape('C-d>'), 'C-d>') end) - describe('feedkeys', function() - it('dot-repeat', function() - keymap.feedkeys(keymap.t('iaiueo'), 'nx') - assert.are.equal(vim.fn.getreg('.'), keymap.t('aiueo')) - end) - it('macro', function() - vim.fn.setreg('q', '') - vim.cmd([[normal! qq]]) - keymap.feedkeys(keymap.t('iaiueo'), 'nt') - keymap.feedkeys(keymap.t(''), 'nt', function() - vim.cmd([[normal! q]]) - assert.are.equal(vim.fn.getreg('q'), keymap.t('iaiueo')) - print(vim.fn.getreg('q')) - end) - end) - end) - describe('evacuate', function() before_each(spec.before) diff --git a/lua/cmp/view/custom_entries_view.lua b/lua/cmp/view/custom_entries_view.lua index 7cd1f0d..4f98ce7 100644 --- a/lua/cmp/view/custom_entries_view.lua +++ b/lua/cmp/view/custom_entries_view.lua @@ -1,5 +1,6 @@ local event = require('cmp.utils.event') local autocmd = require('cmp.utils.autocmd') +local feedkeys = require('cmp.utils.feedkeys') local window = require('cmp.utils.window') local config = require('cmp.config') local types = require('cmp.types') @@ -183,7 +184,7 @@ custom_entries_view.abort = function(self) if self.prefix then self:_insert(self.prefix) end - keymap.feedkeys('', 'n', function() + feedkeys.call('', 'n', function() self:close() end) end @@ -306,10 +307,10 @@ custom_entries_view._insert = function(self, word) vim.api.nvim_feedkeys(keymap.backspace(length) .. word, 'int', true) else local release = require('cmp').core:suspend() - keymap.feedkeys('', 'n', function() + feedkeys.call('', 'n', function() local cursor = api.get_cursor() local length = vim.str_utfindex(string.sub(api.get_current_line(), self.offset, cursor[2])) - keymap.feedkeys( + feedkeys.call( keymap.backspace(length) .. word, 'int', vim.schedule_wrap(function() diff --git a/lua/cmp/view/native_entries_view.lua b/lua/cmp/view/native_entries_view.lua index 939a5c7..ff40eb5 100644 --- a/lua/cmp/view/native_entries_view.lua +++ b/lua/cmp/view/native_entries_view.lua @@ -1,6 +1,7 @@ local event = require('cmp.utils.event') local autocmd = require('cmp.utils.autocmd') local keymap = require('cmp.utils.keymap') +local feedkeys = require('cmp.utils.feedkeys') local types = require('cmp.types') local config = require('cmp.config') local api = require('cmp.utils.api') @@ -118,9 +119,9 @@ end native_entries_view.select_next_item = function(self, option) if self:visible() then if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') else - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') end end end @@ -128,9 +129,9 @@ end native_entries_view.select_prev_item = function(self, option) if self:visible() then if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') else - keymap.feedkeys(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'n') end end end