From 124f1611f1b7cd9fb2892f0597881e007c99b652 Mon Sep 17 00:00:00 2001 From: tzachar Date: Tue, 1 Feb 2022 09:47:34 +0200 Subject: [PATCH] Statusline view in search mode or command line mode (#729) * Search mode horizontal view Enabled by setting `experimental.horizontal_search = true` * use stylua * move to a floating window instead of abusing status line * pass all tests * rework 1. rename entries view file and memeber to wildmenu_entries_view 2. move config to config.view.entries 3. support both in insert mode and cmdline 4. make separator configurable by config.view.separator * rearange wildmenu config Changed config to: ```lua view = { entries = {name = 'wildmenu', separator = '|' } }, ``` * allow view.entries to be either a literal string or a table --- lua/cmp/config/default.lua | 4 + lua/cmp/types/cmp.lua | 9 + lua/cmp/view.lua | 23 +- lua/cmp/view/wildmenu_entries_view.lua | 301 +++++++++++++++++++++++++ 4 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 lua/cmp/view/wildmenu_entries_view.lua diff --git a/lua/cmp/config/default.lua b/lua/cmp/config/default.lua index fb70397..eda83a7 100644 --- a/lua/cmp/config/default.lua +++ b/lua/cmp/config/default.lua @@ -123,5 +123,9 @@ return function() }, sources = {}, + + view = { + entries = { name = 'custom' }, + }, } end diff --git a/lua/cmp/types/cmp.lua b/lua/cmp/types/cmp.lua index a3aedd8..7d6b25f 100644 --- a/lua/cmp/types/cmp.lua +++ b/lua/cmp/types/cmp.lua @@ -81,6 +81,7 @@ cmp.ItemField.Menu = 'menu' ---@field public snippet cmp.SnippetConfig ---@field public mapping table ---@field public sources cmp.SourceConfig[] +---@field public view cmp.ViewConfig ---@field public experimental cmp.ExperimentalConfig ---@class cmp.CompletionConfig @@ -129,4 +130,12 @@ cmp.ItemField.Menu = 'menu' ---@field public max_item_count number|nil ---@field public group_index number|nil +---@class cmp.EntriesConfig +---@field name string +---@field separator string|nil + +---@class cmp.ViewConfig +---@field public entries string|cmp.EntriesConfig +---@field public separator string + return cmp diff --git a/lua/cmp/view.lua b/lua/cmp/view.lua index e29e6f7..129f979 100644 --- a/lua/cmp/view.lua +++ b/lua/cmp/view.lua @@ -4,6 +4,7 @@ local event = require('cmp.utils.event') local keymap = require('cmp.utils.keymap') local docs_view = require('cmp.view.docs_view') local custom_entries_view = require('cmp.view.custom_entries_view') +local wildmenu_entries_view = require('cmp.view.wildmenu_entries_view') local native_entries_view = require('cmp.view.native_entries_view') local ghost_text_view = require('cmp.view.ghost_text_view') @@ -12,6 +13,7 @@ local ghost_text_view = require('cmp.view.ghost_text_view') ---@field private resolve_dedup cmp.AsyncDedup ---@field private native_entries_view cmp.NativeEntriesView ---@field private custom_entries_view cmp.CustomEntriesView +---@field private wildmenu_entries_view cmp.CustomEntriesView ---@field private change_dedup cmp.AsyncDedup ---@field private docs_view cmp.DocsView ---@field private ghost_text_view cmp.GhostTextView @@ -23,6 +25,7 @@ view.new = function() self.resolve_dedup = async.dedup() self.custom_entries_view = custom_entries_view.new() self.native_entries_view = native_entries_view.new() + self.wildmenu_entries_view = wildmenu_entries_view.new() self.docs_view = docs_view.new() self.ghost_text_view = ghost_text_view.new() self.event = event.new() @@ -181,19 +184,19 @@ end view._get_entries_view = function(self) self.native_entries_view.event:clear() self.custom_entries_view.event:clear() + self.wildmenu_entries_view.event:clear() local c = config.get() - if c.experimental.native_menu then - self.native_entries_view.event:on('change', function() - self:on_entry_change() - end) - return self.native_entries_view - else - self.custom_entries_view.event:on('change', function() - self:on_entry_change() - end) - return self.custom_entries_view + local v = self.custom_entries_view + if (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'wildmenu' then + v = self.wildmenu_entries_view + elseif (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'native' then + v = self.native_entries_view end + v.event:on('change', function() + self:on_entry_change() + end) + return v end ---On entry change diff --git a/lua/cmp/view/wildmenu_entries_view.lua b/lua/cmp/view/wildmenu_entries_view.lua new file mode 100644 index 0000000..02ffd7b --- /dev/null +++ b/lua/cmp/view/wildmenu_entries_view.lua @@ -0,0 +1,301 @@ +local event = require('cmp.utils.event') +local autocmd = require('cmp.utils.autocmd') +local feedkeys = require('cmp.utils.feedkeys') +local config = require('cmp.config') +local window = require('cmp.utils.window') +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 offset number +---@field private entries_win cmp.Window +---@field private active boolean +---@field private entries cmp.Entry[] +---@field public event cmp.Event +local statusline_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') + +statusline_entries_view.init = function(self) + self.event = event.new() + self.offset = -1 + self.active = false + self.entries = {} + self.selected_index = nil + self.last_displayed_indices = {} + self.moving_forwards = nil +end + +statusline_entries_view.new = function() + local self = setmetatable({}, { __index = statusline_entries_view }) + self:init() + + self.entries_win = window.new() + self.entries_win:option('conceallevel', 2) + self.entries_win:option('concealcursor', 'n') + self.entries_win:option('cursorlineopt', 'line') + self.entries_win:option('foldenable', false) + self.entries_win:option('wrap', false) + self.entries_win:option('scrolloff', 0) + 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, { + on_win = function(_, win, buf, _, _) + if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then + return + end + + local location = 0 + for _, i in ipairs(self.last_displayed_indices) do + 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, { + end_line = 0, + end_col = location + view['abbr'].bytes, + hl_group = view['abbr'].hl_group, + hl_mode = 'combine', + ephemeral = true, + }) + + if i == self.selected_index then + vim.api.nvim_buf_set_extmark(buf, statusline_entries_view.ns, 0, location, { + end_line = 0, + end_col = location + view['abbr'].bytes, + hl_group = 'PmenuSel', + hl_mode = 'combine', + ephemeral = true, + }) + 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, { + end_line = 0, + end_col = location + m.word_match_end, + hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch', + hl_mode = 'combine', + ephemeral = true, + }) + end + + location = location + view['abbr'].bytes + get_separator():len() + end + end + end, + }) + + autocmd.subscribe( + 'CompleteChanged', + vim.schedule_wrap(function() + if self:visible() and vim.fn.pumvisible() == 1 then + self:close() + end + end) + ) + return self +end + +statusline_entries_view.close = function(self) + self.entries_win:close() + self:init() +end + +statusline_entries_view.ready = function() + return vim.fn.pumvisible() == 0 +end + +statusline_entries_view.on_change = function(self) + self.active = false +end + +statusline_entries_view.open = function(self, offset, entries) + self.offset = offset + self.entries = {} + self.last_displayed_indices = {} + + -- Apply window options (that might be changed) on the custom completion menu. + self.entries_win:option('winblend', vim.o.pumblend) + + -- local entries_buf = self.entries_win:get_buffer() + local dedup = {} + local preselect = 0 + local i = 1 + for _, e in ipairs(entries) do + local view = e:get_view(offset, 0) + if view.dup == 1 or not dedup[e.completion_item.label] then + dedup[e.completion_item.label] = true + table.insert(self.entries, e) + if preselect == 0 and e.completion_item.preselect then + preselect = i + end + i = i + 1 + end + end + + self.entries_win:open({ + relative = 'editor', + style = 'minimal', + row = vim.api.nvim_win_get_height(0), + col = 0, + width = vim.api.nvim_win_get_width(0), + height = 1, + zindex = 1001, + }) + if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then + self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select }) + elseif not string.match(config.get().completion.completeopt, 'noselect') then + self:_select(1, { behavior = types.cmp.SelectBehavior.Select }) + else + self:_select(nil, { behavior = types.cmp.SelectBehavior.Select }) + end +end + +statusline_entries_view.abort = function(self) + feedkeys.call('', 'n', function() + self:close() + end) +end + +statusline_entries_view.draw = function(self) + local entries_buf = self.entries_win:get_buffer() + local texts = {} + local lengths = {} + for _, e in ipairs(self.entries) do + if e then + local view = e:get_view(self.offset, entries_buf) + -- add 1 to lengths, to account for the added separator + table.insert(lengths, view['abbr'].bytes + get_separator():len()) + table.insert(texts, view['abbr'].text) + end + end + + local selected_index = (self.selected_index or 1) + local start_index = (self.selected_index or 1) + local lst_dspl_ind = self.last_displayed_indices + if #lst_dspl_ind == 0 then + start_index = start_index + elseif vim.tbl_contains(lst_dspl_ind, selected_index) then + start_index = lst_dspl_ind[1] + elseif self.moving_forwards then + local needed_length = lengths[selected_index] + start_index = lst_dspl_ind[1] + while needed_length > 0 and vim.tbl_contains(lst_dspl_ind, start_index) do + needed_length = needed_length - lengths[start_index] + start_index = start_index + 1 + end + else -- we need to scroll back + local needed_length = lengths[selected_index] + start_index = lst_dspl_ind[1] + while needed_length > 0 and vim.tbl_contains(lst_dspl_ind, start_index) do + needed_length = needed_length - lengths[start_index] + start_index = start_index - 1 + if start_index <= 0 then + start_index = #self.entries + end + end + end + local statusline = {} + local total_length = 0 + local displayed_indices = {} + for index = start_index, #self.entries * 2 do + if index > #self.entries then + index = index - #self.entries + end + if total_length + lengths[index] < vim.api.nvim_win_get_width(self.entries_win.win) then + if total_length ~= 0 and index == start_index then + break + end + table.insert(statusline, texts[index]) + table.insert(displayed_indices, index) + total_length = total_length + lengths[index] + else + -- always add the last entry + table.insert(statusline, texts[index]) + break + end + end + + statusline = table.concat(statusline, get_separator()) + self.last_displayed_indices = displayed_indices + + vim.api.nvim_buf_set_lines(entries_buf, 0, 1, false, { statusline }) + vim.api.nvim_buf_set_option(entries_buf, 'modified', false) + + vim.api.nvim_win_call(0, function() + misc.redraw() + end) +end + +statusline_entries_view.visible = function(self) + return self.entries_win:visible() +end + +statusline_entries_view.info = function(self) + return self.entries_win:info() +end + +statusline_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 + self:_select(1, option) + else + self:_select(self.selected_index + 1, option) + end + end +end + +statusline_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 + self:_select(#self.entries, option) + else + self:_select(self.selected_index - 1, option) + end + end +end + +statusline_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) + if self:visible() and self.active then + return self.entries[self.selected_index] + end +end + +statusline_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, _) + 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) + end + + self.entries_win:update() + self:draw() + self.event:emit('change') +end + +return statusline_entries_view