From 070c5572adb1b54e0826af0ad695f8148d7da57d Mon Sep 17 00:00:00 2001 From: hrsh7th Date: Sun, 13 Feb 2022 14:34:28 +0900 Subject: [PATCH] Implement shell like common string completion. Fix #785 --- doc/cmp.txt | 20 ++++++++ lua/cmp/config/mapping.lua | 9 ++++ lua/cmp/core.lua | 27 +++++++++++ lua/cmp/init.lua | 4 ++ lua/cmp/utils/keymap.lua | 3 ++ lua/cmp/utils/str.lua | 11 +++++ lua/cmp/view.lua | 11 +++++ lua/cmp/view/custom_entries_view.lua | 20 ++++++-- lua/cmp/view/native_entries_view.lua | 14 ++++++ lua/cmp/view/wildmenu_entries_view.lua | 65 +++++++++++++++----------- 10 files changed, 154 insertions(+), 30 deletions(-) diff --git a/doc/cmp.txt b/doc/cmp.txt index 7680a02..90a4d3c 100644 --- a/doc/cmp.txt +++ b/doc/cmp.txt @@ -204,6 +204,23 @@ NOTE: You can call these functions in mapping via `lua require('cmp').compl < NOTE: The `config` means a temporary setting, but the `config.mapping` remains permanent. +*cmp.complete_common_string* () + Complete common string as like as shell completion behavior. +> + cmp.setup { + mapping = { + [''] = cmp.mapping(function(fallback) + if cmp.visible() then + if cmp.complete_common_string() then + return + end + return cmp.select_next_item() + end + fallback() + end, { 'i', 'c' }), + } + } +< *cmp.confirm* (option: cmp.ConfirmOption, callback: function) Accept current selected completion item. If you didn't select any items and specified the `{ select = true }` for @@ -279,6 +296,9 @@ You can also use built-in mapping helpers. *cmp.mapping.complete* (option: cmp.CompleteParams) Same as |cmp.complete| + *cmp.mapping.complete_common_string* () + Same as |cmp.complete_common_string| + *cmp.mapping.confirm* (option: cmp.ConfirmOption) Same as |cmp.confirm| diff --git a/lua/cmp/config/mapping.lua b/lua/cmp/config/mapping.lua index ec73ed3..0c2e81a 100644 --- a/lua/cmp/config/mapping.lua +++ b/lua/cmp/config/mapping.lua @@ -22,6 +22,15 @@ mapping.complete = function(option) end end +---Complete common string. +mapping.complete_common_string = function() + return function(fallback) + if not require('cmp').complete_common_string() then + fallback() + end + end +end + ---Close current completion menu if it displayed. mapping.close = function() return function(fallback) diff --git a/lua/cmp/core.lua b/lua/cmp/core.lua index 673a5cd..8df922d 100644 --- a/lua/cmp/core.lua +++ b/lua/cmp/core.lua @@ -1,4 +1,5 @@ local debug = require('cmp.utils.debug') +local str = require('cmp.utils.str') local char = require('cmp.utils.char') local pattern = require('cmp.utils.pattern') local feedkeys = require('cmp.utils.feedkeys') @@ -217,6 +218,32 @@ core.autoindent = function(self, trigger_event, callback) callback() end +---Complete common string for current completed entries. +core.complete_common_string = function(self) + if not self.view:visible() then + return false + end + + self.filter:sync(1000) + + local cursor = api.get_cursor() + local offset = self.view:get_offset() + local common_string + for _, e in ipairs(self.view:get_entries()) do + local vim_item = e:get_vim_item(offset) + if not common_string then + common_string = vim_item.word + else + common_string = str.get_common_string(common_string, vim_item.word) + end + end + if common_string and #common_string > (1 + cursor[2] - offset) then + feedkeys.call(keymap.backspace(string.sub(api.get_current_line(), offset, cursor[2])) .. common_string, 'n') + return true + end + return false +end + ---Invoke completion ---@param ctx cmp.Context core.complete = function(self, ctx) diff --git a/lua/cmp/init.lua b/lua/cmp/init.lua index decebfd..593a2aa 100644 --- a/lua/cmp/init.lua +++ b/lua/cmp/init.lua @@ -75,6 +75,10 @@ cmp.complete = cmp.sync(function(option) return true end) +cmp.complete_common_string = cmp.sync(function() + return cmp.core:complete_common_string() +end) + ---Return view is visible or not. cmp.visible = cmp.sync(function() return cmp.core.view:visible() or vim.fn.pumvisible() == 1 diff --git a/lua/cmp/utils/keymap.lua b/lua/cmp/utils/keymap.lua index 5f78285..67bec80 100644 --- a/lua/cmp/utils/keymap.lua +++ b/lua/cmp/utils/keymap.lua @@ -67,6 +67,9 @@ end ---@param count number ---@return string keymap.backspace = function(count) + if type(count) == 'string' then + count = vim.fn.strchars(count, true) + end if count <= 0 then return '' end diff --git a/lua/cmp/utils/str.lua b/lua/cmp/utils/str.lua index cbdcbf4..450c991 100644 --- a/lua/cmp/utils/str.lua +++ b/lua/cmp/utils/str.lua @@ -43,6 +43,17 @@ str.has_prefix = function(text, prefix) return true end +---get_common_string +str.get_common_string = function(text1, text2) + local min = math.min(#text1, #text2) + for i = 1, min do + if not char.match(string.byte(text1, i), string.byte(text2, i)) then + return string.sub(text1, 1, i - 1) + end + end + return string.sub(text1, 1, min) +end + ---Remove suffix ---@param text string ---@param suffix string diff --git a/lua/cmp/view.lua b/lua/cmp/view.lua index 129f979..4c2d03f 100644 --- a/lua/cmp/view.lua +++ b/lua/cmp/view.lua @@ -161,6 +161,17 @@ view.select_prev_item = function(self, option) self:_get_entries_view():select_prev_item(option) end +---Get offset. +view.get_offset = function(self) + return self:_get_entries_view():get_offset() +end + +---Get entries. +---@return cmp.Entry[] +view.get_entries = function(self) + return self:_get_entries_view():get_entries() +end + ---Get first entry ---@param self cmp.Entry|nil view.get_first_entry = function(self) diff --git a/lua/cmp/view/custom_entries_view.lua b/lua/cmp/view/custom_entries_view.lua index a6b60c8..84adbd5 100644 --- a/lua/cmp/view/custom_entries_view.lua +++ b/lua/cmp/view/custom_entries_view.lua @@ -257,6 +257,20 @@ custom_entries_view.select_prev_item = function(self, option) end end +custom_entries_view.get_offset = function(self) + if self:visible() then + return self.offset + end + return nil +end + +custom_entries_view.get_entries = function(self) + if self:visible() then + return self.entries + end + return {} +end + custom_entries_view.get_first_entry = function(self) if self:visible() then return self.entries[1] @@ -301,8 +315,7 @@ custom_entries_view._insert = setmetatable({ word = word or '' if api.is_cmdline_mode() then local cursor = api.get_cursor() - local length = vim.fn.strchars(string.sub(api.get_current_line(), self.offset, cursor[2]), true) - vim.api.nvim_feedkeys(keymap.backspace(length) .. word, 'int', true) + vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true) else if this.pending then return @@ -312,10 +325,9 @@ custom_entries_view._insert = setmetatable({ local release = require('cmp').suspend() feedkeys.call('', '', function() local cursor = api.get_cursor() - local length = vim.fn.strchars(string.sub(api.get_current_line(), self.offset, cursor[2]), true) local keys = {} table.insert(keys, keymap.indentkeys()) - table.insert(keys, keymap.backspace(length)) + table.insert(keys, keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2]))) table.insert(keys, word) table.insert(keys, keymap.indentkeys(vim.bo.indentkeys)) feedkeys.call( diff --git a/lua/cmp/view/native_entries_view.lua b/lua/cmp/view/native_entries_view.lua index a74f697..d1dc8de 100644 --- a/lua/cmp/view/native_entries_view.lua +++ b/lua/cmp/view/native_entries_view.lua @@ -143,6 +143,20 @@ native_entries_view.select_prev_item = function(self, option) end end +native_entries_view.get_offset = function(self) + if self:visible() then + return self.offset + end + return nil +end + +native_entries_view.get_entries = function(self) + if self:visible() then + return self.entries + end + return {} +end + native_entries_view.get_first_entry = function(self) if self:visible() then return self.entries[1] diff --git a/lua/cmp/view/wildmenu_entries_view.lua b/lua/cmp/view/wildmenu_entries_view.lua index 252fb40..9cafb9d 100644 --- a/lua/cmp/view/wildmenu_entries_view.lua +++ b/lua/cmp/view/wildmenu_entries_view.lua @@ -14,16 +14,16 @@ local api = require('cmp.utils.api') ---@field private active boolean ---@field private entries cmp.Entry[] ---@field public event cmp.Event -local statusline_entries_view = {} +local wildmenu_entries_view = {} local function get_separator() local c = config.get() return (c and c.view and c.view.entries and c.view.entries.separator) or ' ' end -statusline_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.statusline_entries_view') +wildmenu_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.statusline_entries_view') -statusline_entries_view.init = function(self) +wildmenu_entries_view.init = function(self) self.event = event.new() self.offset = -1 self.active = false @@ -33,8 +33,8 @@ statusline_entries_view.init = function(self) self.moving_forwards = nil end -statusline_entries_view.new = function() - local self = setmetatable({}, { __index = statusline_entries_view }) +wildmenu_entries_view.new = function() + local self = setmetatable({}, { __index = wildmenu_entries_view }) self:init() self.entries_win = window.new() @@ -47,7 +47,7 @@ statusline_entries_view.new = function() self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None') self.entries_win:buffer_option('tabstop', 1) - vim.api.nvim_set_decoration_provider(statusline_entries_view.ns, { + vim.api.nvim_set_decoration_provider(wildmenu_entries_view.ns, { on_win = function(_, win, buf, _, _) if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then return @@ -58,7 +58,7 @@ statusline_entries_view.new = function() local e = self.entries[i] if e then local view = e:get_view(self.offset, buf) - vim.api.nvim_buf_set_extmark(buf, statusline_entries_view.ns, 0, location, { + vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, location, { end_line = 0, end_col = location + view['abbr'].bytes, hl_group = view['abbr'].hl_group, @@ -67,7 +67,7 @@ statusline_entries_view.new = function() }) if i == self.selected_index then - vim.api.nvim_buf_set_extmark(buf, statusline_entries_view.ns, 0, location, { + vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, location, { end_line = 0, end_col = location + view['abbr'].bytes, hl_group = 'PmenuSel', @@ -77,7 +77,7 @@ statusline_entries_view.new = function() end for _, m in ipairs(e.matches or {}) do - vim.api.nvim_buf_set_extmark(buf, statusline_entries_view.ns, 0, location + m.word_match_start - 1, { + vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, location + m.word_match_start - 1, { end_line = 0, end_col = location + m.word_match_end, hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch', @@ -103,20 +103,20 @@ statusline_entries_view.new = function() return self end -statusline_entries_view.close = function(self) +wildmenu_entries_view.close = function(self) self.entries_win:close() self:init() end -statusline_entries_view.ready = function() +wildmenu_entries_view.ready = function() return vim.fn.pumvisible() == 0 end -statusline_entries_view.on_change = function(self) +wildmenu_entries_view.on_change = function(self) self.active = false end -statusline_entries_view.open = function(self, offset, entries) +wildmenu_entries_view.open = function(self, offset, entries) self.offset = offset self.entries = {} self.last_displayed_indices = {} @@ -159,13 +159,13 @@ statusline_entries_view.open = function(self, offset, entries) end end -statusline_entries_view.abort = function(self) +wildmenu_entries_view.abort = function(self) feedkeys.call('', 'n', function() self:close() end) end -statusline_entries_view.draw = function(self) +wildmenu_entries_view.draw = function(self) local entries_buf = self.entries_win:get_buffer() local texts = {} local lengths = {} @@ -235,15 +235,15 @@ statusline_entries_view.draw = function(self) end) end -statusline_entries_view.visible = function(self) +wildmenu_entries_view.visible = function(self) return self.entries_win:visible() end -statusline_entries_view.info = function(self) +wildmenu_entries_view.info = function(self) return self.entries_win:info() end -statusline_entries_view.select_next_item = function(self, option) +wildmenu_entries_view.select_next_item = function(self, option) if self:visible() then self.moving_forwards = true if self.selected_index == nil or self.selected_index == #self.entries then @@ -254,7 +254,7 @@ statusline_entries_view.select_next_item = function(self, option) end end -statusline_entries_view.select_prev_item = function(self, option) +wildmenu_entries_view.select_prev_item = function(self, option) if self:visible() then self.moving_forwards = false if self.selected_index == nil or self.selected_index <= 1 then @@ -265,33 +265,46 @@ statusline_entries_view.select_prev_item = function(self, option) end end -statusline_entries_view.get_first_entry = function(self) +wildmenu_entries_view.get_offset = function(self) + if self:visible() then + return self.offset + end + return nil +end + +wildmenu_entries_view.get_entries = function(self) + if self:visible() then + return self.entries + end + return {} +end + +wildmenu_entries_view.get_first_entry = function(self) if self:visible() then return self.entries[1] end end -statusline_entries_view.get_selected_entry = function(self) +wildmenu_entries_view.get_selected_entry = function(self) if self:visible() and self.active then return self.entries[self.selected_index] end end -statusline_entries_view.get_active_entry = function(self) +wildmenu_entries_view.get_active_entry = function(self) if self:visible() and self.active then return self:get_selected_entry() end end -statusline_entries_view._select = function(self, selected_index, _) +wildmenu_entries_view._select = function(self, selected_index, _) self.selected_index = selected_index self.active = (selected_index ~= nil) if self.active then local cursor = api.get_cursor() local word = self:get_active_entry():get_vim_item(self.offset).word - local length = vim.fn.strchars(string.sub(api.get_current_line(), self.offset, cursor[2]), true) - vim.api.nvim_feedkeys(keymap.backspace(length) .. word, 'int', true) + vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true) end self.entries_win:update() @@ -299,4 +312,4 @@ statusline_entries_view._select = function(self, selected_index, _) self.event:emit('change') end -return statusline_entries_view +return wildmenu_entries_view