From 0f28030aef5b7b15708aaed8ed90b94d206aa712 Mon Sep 17 00:00:00 2001 From: hrsh7th Date: Sat, 16 Oct 2021 23:37:32 +0900 Subject: [PATCH] Improve macro & dot-repeat support (#363) * manual support dot-repeat * cmdwin and terminal * cmdline only * Fix * fix * Improve * Fix test * Support macro * disable cmdline for now * Simplify * fmt * consume once * Ignore = type * cmdline * Remove cmdline features --- lua/cmp/config.lua | 7 +- lua/cmp/config/mapping.lua | 20 ++-- lua/cmp/context.lua | 5 +- lua/cmp/core.lua | 13 ++- lua/cmp/init.lua | 17 +++ lua/cmp/utils/api.lua | 56 ++++++++++ lua/cmp/utils/keymap.lua | 158 +++++++++++++++++++++++---- lua/cmp/utils/keymap_spec.lua | 20 ++++ lua/cmp/utils/misc.lua | 18 --- lua/cmp/utils/window.lua | 1 + lua/cmp/view/custom_entries_view.lua | 38 +++---- lua/cmp/view/ghost_text_view.lua | 4 + lua/cmp/view/native_entries_view.lua | 6 +- 13 files changed, 279 insertions(+), 84 deletions(-) create mode 100644 lua/cmp/utils/api.lua diff --git a/lua/cmp/config.lua b/lua/cmp/config.lua index a701016..fc73eb8 100644 --- a/lua/cmp/config.lua +++ b/lua/cmp/config.lua @@ -1,5 +1,6 @@ local cache = require('cmp.utils.cache') local misc = require('cmp.utils.misc') +local api = require('cmp.utils.api') ---@class cmp.Config ---@field public g cmp.ConfigSchema @@ -33,10 +34,10 @@ end ---@return cmp.ConfigSchema config.get = function() - local bufnr = vim.api.nvim_get_current_buf() local global = config.global + local bufnr = vim.api.nvim_get_current_buf() local buffer = config.buffers[bufnr] or { revision = 1 } - return config.cache:ensure({ 'get', bufnr, global.revision or 0, buffer.revision or 0 }, function() + return config.cache:ensure({ 'get_buffer', bufnr, global.revision or 0, buffer.revision or 0 }, function() return misc.merge(buffer, global) end) end @@ -47,7 +48,7 @@ config.enabled = function() if type(enabled) == 'function' then enabled = enabled() end - return enabled and misc.is_suitable_mode() + return enabled and api.is_suitable_mode() end ---Return source config diff --git a/lua/cmp/config/mapping.lua b/lua/cmp/config/mapping.lua index 1732b12..7d967eb 100644 --- a/lua/cmp/config/mapping.lua +++ b/lua/cmp/config/mapping.lua @@ -1,11 +1,17 @@ -local mapping = setmetatable({}, { +local api = require('cmp.utils.api') + +local mapping +mapping = setmetatable({}, { __call = function(_, invoke, modes) - return { - invoke = function(...) - invoke(...) - end, - modes = modes or { 'i' }, - } + if type(invoke) == 'function' then + return { + invoke = function(...) + invoke(...) + end, + modes = modes or { 'i' }, + } + end + return invoke end, }) diff --git a/lua/cmp/context.lua b/lua/cmp/context.lua index 2a3e539..1342cd4 100644 --- a/lua/cmp/context.lua +++ b/lua/cmp/context.lua @@ -2,6 +2,7 @@ local misc = require('cmp.utils.misc') local pattern = require('cmp.utils.pattern') local types = require('cmp.types') local cache = require('cmp.utils.cache') +local api = require('cmp.utils.api') ---@class cmp.Context ---@field public id string @@ -47,8 +48,8 @@ context.new = function(prev_context, option) self.mode = vim.api.nvim_get_mode().mode self.bufnr = vim.api.nvim_get_current_buf() - local cursor = vim.api.nvim_win_get_cursor(0) - self.cursor_line = vim.api.nvim_get_current_line() + local cursor = api.get_cursor() + self.cursor_line = api.get_current_line() self.cursor = {} self.cursor.row = cursor[1] self.cursor.col = cursor[2] + 1 diff --git a/lua/cmp/core.lua b/lua/cmp/core.lua index d9d673d..7b0c9aa 100644 --- a/lua/cmp/core.lua +++ b/lua/cmp/core.lua @@ -9,6 +9,7 @@ local view = require('cmp.view') local misc = require('cmp.utils.misc') local config = require('cmp.config') local types = require('cmp.types') +local api = require('cmp.utils.api') local SOURCE_TIMEOUT = 500 local THROTTLE_TIME = 120 @@ -196,14 +197,14 @@ end ---@param callback function core.autoindent = function(self, event, callback) if event == types.cmp.TriggerEvent.TextChanged then - local cursor_before_line = misc.get_cursor_before_line() + local cursor_before_line = api.get_cursor_before_line() local prefix = pattern.matchstr('[^[:blank:]]\\+$', cursor_before_line) if prefix then for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then local release = self:suspend() vim.schedule(function() - if cursor_before_line == misc.get_cursor_before_line() then + if cursor_before_line == api.get_cursor_before_line() then local indentkeys = vim.bo.indentkeys vim.bo.indentkeys = indentkeys .. ',!^F' keymap.feedkeys(keymap.t(''), 'n', function() @@ -226,7 +227,7 @@ end ---Invoke completion ---@param ctx cmp.Context core.complete = function(self, ctx) - if not misc.is_suitable_mode() then + if not api.is_suitable_mode() then return end self:set_context(ctx) @@ -258,7 +259,7 @@ end ---Update completion menu core.filter = async.throttle( vim.schedule_wrap(function(self) - if not misc.is_suitable_mode() then + if not api.is_suitable_mode() then return end if self.view:get_active_entry() ~= nil then @@ -370,10 +371,10 @@ core.confirm = function(self, e, option, callback) local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet if is_snippet then - table.insert(keys, keymap.t('u') .. e:get_word() .. keymap.t('u')) + table.insert(keys, keymap.undobreak() .. e:get_word() .. keymap.undobreak()) table.insert(keys, keymap.backspace(vim.str_utfindex(e:get_word()))) else - table.insert(keys, keymap.t('u') .. completion_item.textEdit.newText .. keymap.t('u')) + table.insert(keys, keymap.undobreak() .. completion_item.textEdit.newText .. keymap.undobreak()) end keymap.feedkeys(table.concat(keys, ''), 'n', function() if is_snippet then diff --git a/lua/cmp/init.lua b/lua/cmp/init.lua index 89095a7..7e6f72a 100644 --- a/lua/cmp/init.lua +++ b/lua/cmp/init.lua @@ -84,6 +84,12 @@ end ---Select next item if possible cmp.select_next_item = function(option) option = option or {} + + -- Hack: Ignore when executing macro. + if vim.fn.reg_executing() ~= '' then + return true + end + if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:select_next_item(option) @@ -103,6 +109,12 @@ end ---Select prev item if possible cmp.select_prev_item = function(option) option = option or {} + + -- Hack: Ignore when executing macro. + if vim.fn.reg_executing() ~= '' then + return true + end + if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:select_prev_item(option) @@ -133,6 +145,11 @@ end cmp.confirm = function(option) option = option or {} + -- Hack: Ignore when executing macro. + if vim.fn.reg_executing() ~= '' then + return true + end + local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil) if e then cmp.core:confirm(e, { diff --git a/lua/cmp/utils/api.lua b/lua/cmp/utils/api.lua new file mode 100644 index 0000000..54487bd --- /dev/null +++ b/lua/cmp/utils/api.lua @@ -0,0 +1,56 @@ +local api = {} + +api.is_insert_mode = function() + return vim.tbl_contains({ + 'i', + 'ic', + 'ix', + }, vim.api.nvim_get_mode().mode) +end + +api.is_cmdline_mode = function() + return vim.tbl_contains({ + 'c', + 'cv', + }, vim.api.nvim_get_mode().mode) +end + +api.is_select_mode = function() + return vim.tbl_contains({ + 's', + 'S', + }, vim.api.nvim_get_mode().mode) +end + +api.is_suitable_mode = function() + return api.is_insert_mode() or api.is_cmdline_mode() +end + +api.get_current_line = function() + if api.is_cmdline_mode() then + return vim.fn.getcmdline() + end + return vim.api.nvim_get_current_line() +end + +api.get_cursor = function() + if api.is_cmdline_mode() then + return { vim.o.lines - (vim.api.nvim_get_option('cmdheight') or 1) + 1, vim.fn.getcmdpos() - 1 } + end + return vim.api.nvim_win_get_cursor(0) +end + +api.get_screen_cursor = function() + local cursor = api.get_cursor() + if api.is_cmdline_mode() then + return cursor + end + local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) + return { pos.row, pos.col - 1 } +end + +api.get_cursor_before_line = function() + return string.sub(api.get_current_line(), 1, api.get_cursor()[2] + 1) +end + +return api diff --git a/lua/cmp/utils/keymap.lua b/lua/cmp/utils/keymap.lua index 05a5cb1..90b9f68 100644 --- a/lua/cmp/utils/keymap.lua +++ b/lua/cmp/utils/keymap.lua @@ -1,6 +1,7 @@ local misc = require('cmp.utils.misc') local str = require('cmp.utils.str') local cache = require('cmp.utils.cache') +local api = require('cmp.utils.api') local keymap = {} @@ -72,6 +73,22 @@ keymap.to_keymap = function(s) end) end +---Mode safe break undo +keymap.undobreak = function() + if api.is_cmdline_mode() then + return '' + end + return keymap.t('u') +end + +---Mode safe join undo +keymap.undojoin = function() + if api.is_cmdline_mode() then + return '' + end + return keymap.t('U') +end + ---Create backspace keys. ---@param count number ---@return string @@ -80,9 +97,7 @@ keymap.backspace = function(count) return '' end local keys = {} - table.insert(keys, keymap.t('set backspace=start')) table.insert(keys, keymap.t(string.rep('', count))) - table.insert(keys, keymap.t(('set backspace=%s'):format(vim.o.backspace))) return table.concat(keys, '') end @@ -95,31 +110,48 @@ keymap.equals = function(a, 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 #keys == 0 then - return callback and callback() or nil + if vim.fn.reg_recording() ~= '' then + return keymap.feedkeys_macro_safe(keys, mode, callback) end - vim.api.nvim_feedkeys(keys, mode, true) + local is_typed = string.match(mode, 't') ~= nil + local is_insert = string.match(mode, 'i') ~= nil - if callback then - if vim.fn.reg_recording() == '' then - local id = misc.id('cmp.utils.keymap.feedkeys') - self.callbacks[id] = callback - vim.api.nvim_feedkeys(keymap.t('call v:lua.cmp.utils.keymap.feedkeys.run(%s)'):format(id), 'n', true) - else - -- Does not feed extra keys if macro recording. - local wait - wait = vim.schedule_wrap(function() - if vim.fn.getchar(1) == 0 then - return callback() - end - vim.defer_fn(wait, 1) - end) - wait() + 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 is_typed then + vim.fn.setreg('".', vim.fn.getreg('".') .. keys) + end + 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, @@ -132,6 +164,63 @@ misc.set(_G, { 'cmp', 'utils', 'keymap', 'feedkeys', 'run' }, function(id) 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(), @@ -169,9 +258,15 @@ keymap.listen = setmetatable({ }) misc.set(_G, { 'cmp', 'utils', 'keymap', 'listen', 'run' }, function(id) local definition = keymap.listen.cache:get({ 'definition', id }) - definition.callback(definition.keys, misc.once(function() - keymap.feedkeys(keymap.t(definition.fallback), 'i') - end)) + if definition.mode == 'c' and vim.fn.getcmdtype() == '=' then + return vim.api.nvim_feedkeys(keymap.t(definition.fallback), 'i', true) + end + definition.callback( + definition.keys, + misc.once(function() + vim.api.nvim_feedkeys(keymap.t(definition.fallback), 'i', true) + end) + ) return keymap.t('') end) @@ -252,4 +347,21 @@ keymap.find_map_by_lhs = function(mode, lhs) } 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 06f74fa..3f9a7ca 100644 --- a/lua/cmp/utils/keymap_spec.lua +++ b/lua/cmp/utils/keymap_spec.lua @@ -17,6 +17,26 @@ describe('keymap', function() assert.are.equal(keymap.escape('C-d>'), 'C-d>') end) + describe('feedkeys', function() + it('dot-repeat', function() + vim.fn.setreg('".', '') + keymap.feedkeys(keymap.t('i'), 'nt') + keymap.feedkeys(keymap.t('aiueo'), 'nt') + keymap.feedkeys(keymap.t(''), 'nx') + assert.are.equal(vim.fn.getreg('".'), keymap.t('iaiueo')) + 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/utils/misc.lua b/lua/cmp/utils/misc.lua index 270bae5..9b32fe8 100644 --- a/lua/cmp/utils/misc.lua +++ b/lua/cmp/utils/misc.lua @@ -29,24 +29,6 @@ misc.concat = function(list1, list2) return new_list end ----Get cursor before line ----@return string -misc.get_cursor_before_line = function() - local cursor = vim.api.nvim_win_get_cursor(0) - return string.sub(vim.api.nvim_get_current_line(), 1, cursor[2]) -end - ----Return current mode is insert-mode or not. ----@return boolean -misc.is_suitable_mode = function() - local mode = vim.api.nvim_get_mode().mode - return vim.tbl_contains({ - 'i', - 'ic', - 'ix', - }, mode) -end - ---Merge two tables recursively ---@generic T ---@param v1 T diff --git a/lua/cmp/utils/window.lua b/lua/cmp/utils/window.lua index 0f2cd3d..f6b3afa 100644 --- a/lua/cmp/utils/window.lua +++ b/lua/cmp/utils/window.lua @@ -1,6 +1,7 @@ local cache = require('cmp.utils.cache') local misc = require('cmp.utils.misc') local buffer = require('cmp.utils.buffer') +local api = require('cmp.utils.api') ---@class cmp.WindowStyle ---@field public relative string diff --git a/lua/cmp/view/custom_entries_view.lua b/lua/cmp/view/custom_entries_view.lua index 8b8cc5f..500450d 100644 --- a/lua/cmp/view/custom_entries_view.lua +++ b/lua/cmp/view/custom_entries_view.lua @@ -4,7 +4,7 @@ local window = require('cmp.utils.window') local config = require('cmp.config') local types = require('cmp.types') local keymap = require('cmp.utils.keymap') -local misc = require('cmp.utils.misc') +local api = require('cmp.utils.api') ---@class cmp.CustomEntriesView ---@field private entries_win cmp.Window @@ -129,16 +129,16 @@ custom_entries_view.open = function(self, offset, entries) width = width + self.column_width.kind + (self.column_width.menu > 0 and 1 or 0) width = width + self.column_width.menu + 1 - local cursor = vim.api.nvim_win_get_cursor(0) - local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) + local cursor = api.get_cursor() + local pos = api.get_screen_cursor() local height = vim.api.nvim_get_option('pumheight') height = height == 0 and #self.entries or height height = math.min(height, #self.entries) - if (vim.o.lines - pos.row) <= 8 and pos.row - 8 > 0 then - height = math.min(height, pos.row - 1) - pos.row = pos.row - height - 1 + if (vim.o.lines - pos[1]) <= 8 and pos[1] - 8 > 0 then + height = math.min(height, pos[1] - 1) + pos[1] = pos[1] - height - 1 else - height = math.min(height, vim.o.lines - pos.row) + height = math.min(height, vim.o.lines - pos[1]) end if width < 1 or height < 1 then @@ -149,8 +149,8 @@ custom_entries_view.open = function(self, offset, entries) self.entries_win:open({ relative = 'editor', style = 'minimal', - row = pos.row, - col = pos.col - 1 - delta - 1, + row = pos[1], + col = pos[2] - delta - 1, width = width, height = height, zindex = 1001, @@ -270,7 +270,7 @@ custom_entries_view._select = function(self, cursor, option) local is_insert = (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert if is_insert then if vim.api.nvim_win_get_cursor(self.entries_win.win)[2] == 1 then - self.prefix = string.sub(vim.api.nvim_get_current_line(), self.offset, vim.api.nvim_win_get_cursor(0)[2]) or '' + self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or '' end end @@ -286,18 +286,12 @@ custom_entries_view._select = function(self, cursor, option) end custom_entries_view._insert = function(self, word) - vim.api.nvim_buf_set_keymap(0, 'i', '(cmp.view.custom_entries_view._insert.remove)', ('v:lua.cmp.view.custom_entries_view._insert.remove(%s)'):format(self.offset), { - expr = true, - noremap = true, - }) - keymap.feedkeys(keymap.t('(cmp.view.custom_entries_view._insert.remove)'), 't') - keymap.feedkeys(word, 'nt') + keymap.feedkeys('', 'n', function() + local release = require('cmp').core:suspend() + local cursor = api.get_cursor() + local length = vim.str_utfindex(string.sub(api.get_current_line(), self.offset, cursor[2])) + keymap.feedkeys(keymap.backspace(length) .. word, 'int', vim.schedule_wrap(release)) + end) end -misc.set(_G, { 'cmp', 'view', 'custom_entries_view', '_insert', 'remove' }, function(offset) - local cursor = vim.api.nvim_win_get_cursor(0) - local length = vim.str_utfindex(string.sub(vim.api.nvim_get_current_line(), offset, cursor[2])) - return keymap.backspace(length) -end) - return custom_entries_view diff --git a/lua/cmp/view/ghost_text_view.lua b/lua/cmp/view/ghost_text_view.lua index bfac25b..d12239f 100644 --- a/lua/cmp/view/ghost_text_view.lua +++ b/lua/cmp/view/ghost_text_view.lua @@ -1,6 +1,7 @@ local config = require('cmp.config') local str = require('cmp.utils.str') local types = require('cmp.types') +local api = require('cmp.utils.api') ---@class cmp.GhostTextView local ghost_text_view = {} @@ -53,6 +54,9 @@ end ---Show ghost text ---@param e cmp.Entry ghost_text_view.show = function(self, e) + if not api.is_insert_mode() then + return + end local changed = e ~= self.entry self.win = vim.api.nvim_get_current_win() self.entry = e diff --git a/lua/cmp/view/native_entries_view.lua b/lua/cmp/view/native_entries_view.lua index 0b810c9..b71c7f6 100644 --- a/lua/cmp/view/native_entries_view.lua +++ b/lua/cmp/view/native_entries_view.lua @@ -3,7 +3,7 @@ local autocmd = require('cmp.utils.autocmd') local keymap = require('cmp.utils.keymap') local types = require('cmp.types') local config = require('cmp.config') -local misc = require('cmp.utils.misc') +local api = require('cmp.utils.api') ---@class cmp.NativeEntriesView ---@field private offset number @@ -70,7 +70,7 @@ native_entries_view.open = function(self, offset, entries) end native_entries_view.close = function(self) - if misc.is_suitable_mode() then + if api.is_suitable_mode() then vim.fn.complete(1, {}) end self.offset = -1 @@ -80,7 +80,7 @@ native_entries_view.close = function(self) end native_entries_view.abort = function(_) - if misc.is_suitable_mode() then + if api.is_suitable_mode() then vim.api.nvim_select_popupmenu_item(-1, true, true, {}) end end