Use floating window for completion menus (#224)

* WIP

* WIP

* Fix #226

* Insert text

* Emulate vim native

* テキトウ

* Tekito

* Move scrollbar impl

* aaa

* Ignore unexpected event

* fix

* fix scroll

* Refactor (conflict...)

* Fix bug

* Positive integer

* Refactor a bit

* Fix for pumheight=0

* fx

* Improve matching highlight

* Improve colorscheme handling

* fmt

* Add cmp.visible

* Fix pum pos

* ABBR_MARGIN

* Fix cel calculation

* up

* refactor

* fix

* a

* a

* compat

* Remove current completion state

* Fix ghost text

* Add feature toggle

* highlight customization

* Update

* Add breaking change announcement

* Add README.md

* Remove unused function

* extmark ephemeral ghost text

* Support native comp

* Fix docs  pos

* a

* Remove if native menu visible

* theme async

* Improvement idea: option to disables insert on select item (#240)

* use ghost text instead of insertion on prev/next item

* add disables_insert_on_selection option

* move disable_insert_on_select option as argumet on

* update README

* use an enum behavior to disable insert on select

* Adopt contribution

* Preselect

* Improve

* Change configuration option

* a

* Improve

* Improve

* Implement proper <C-e> behavior to native/custom

* Support <C-c> maybe

* Improve docs view

* Improve

* Avoid syntax leak

* TODO: refactor

* Fix

* Revert win pos

* fmt

* ghost text remaining

* Don't use italic by default

* bottom

* dedup by label

* Ignore events

* up

* Hacky native view partial support

* up

* perf

* improve

* more cache

* fmt

* Fix format option

* fmt

* recheck

* Fix

* Improve

* Improve

* compat

* implement redraw

* improve

* up

* fmt/lint

* immediate ghost text

* source timeout

* up

* Support multibyte

* disable highlight

* up

* improve

* fmt

* fmt

* fix

* fix

* up

* up

* Use screenpos

* Add undojoin check

* Fix height

* matcher bug

* Fix dot-repeat

* Remove undojoin

* macro

* Support dot-repeat

* MacroSafe

* Default item count is 200

* fmt

Co-authored-by: Eric Puentes <eric.puentes@mercadolibre.com.co>
This commit is contained in:
hrsh7th
2021-10-08 18:27:33 +09:00
committed by GitHub
parent 5bed2dc9f3
commit ada9ddeff7
31 changed files with 1802 additions and 718 deletions

View File

@@ -0,0 +1,295 @@
local event = require('cmp.utils.event')
local autocmd = require('cmp.utils.autocmd')
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')
---@class cmp.CustomEntriesView
---@field private entries_win cmp.Window
---@field private offset number
---@field private entries cmp.Entry[]
---@field private column_bytes any
---@field private column_width any
---@field public event cmp.Event
local custom_entries_view = {}
custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_view')
custom_entries_view.new = function()
local self = setmetatable({}, { __index = custom_entries_view })
self.entries_win = window.new()
self.entries_win:option('conceallevel', 2)
self.entries_win:option('concealcursor', 'n')
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.event = event.new()
self.offset = -1
self.entries = {}
autocmd.subscribe(
'CompleteChanged',
vim.schedule_wrap(function()
if self:visible() and vim.fn.pumvisible() == 1 then
self:close()
end
end)
)
vim.api.nvim_set_decoration_provider(custom_entries_view.ns, {
on_win = function(_, win, buf, top, bot)
if win ~= self.entries_win.win then
return
end
for i = top, bot do
local e = self.entries[i + 1]
if e then
local v = e:get_view(self.offset)
local o = 1
for _, key in ipairs({ 'abbr', 'kind', 'menu' }) do
if self.column_bytes[key] > 0 then
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o, {
end_line = i,
end_col = o + v[key].bytes,
hl_group = v[key].hl_group,
hl_mode = 'combine',
ephemeral = true,
})
o = o + self.column_bytes[key] + 1
end
end
for _, m in ipairs(e.matches or {}) do
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, m.word_match_start, {
end_line = i,
end_col = m.word_match_end + 1,
hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch',
hl_mode = 'combine',
ephemeral = true,
})
end
end
end
end,
})
return self
end
custom_entries_view.ready = function()
return vim.fn.pumvisible() == 0
end
custom_entries_view.redraw = function()
-- noop
end
custom_entries_view.open = function(self, offset, entries)
self.offset = offset
self.entries = {}
self.column_bytes = { abbr = 0, kind = 0, menu = 0 }
self.column_width = { abbr = 0, kind = 0, menu = 0 }
local lines = {}
local dedup = {}
local preselect = 0
local i = 1
for _, e in ipairs(entries) do
local view = e:get_view(offset)
if view.dup == 1 or not dedup[e.completion_item.label] then
dedup[e.completion_item.label] = true
self.column_bytes.abbr = math.max(self.column_bytes.abbr, view.abbr.bytes)
self.column_bytes.kind = math.max(self.column_bytes.kind, view.kind.bytes)
self.column_bytes.menu = math.max(self.column_bytes.menu, view.menu.bytes)
self.column_width.abbr = math.max(self.column_width.abbr, view.abbr.width)
self.column_width.kind = math.max(self.column_width.kind, view.kind.width)
self.column_width.menu = math.max(self.column_width.menu, view.menu.width)
table.insert(self.entries, e)
table.insert(lines, ' ')
if preselect == 0 and e.completion_item.preselect then
preselect = i
end
i = i + 1
end
end
vim.api.nvim_buf_set_lines(self.entries_win.buf, 0, -1, false, lines)
local width = 0
width = width + 1
width = width + self.column_width.abbr + (self.column_width.kind > 0 and 1 or 0)
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 row = vim.fn.screenpos('.', cursor[1], cursor[2] + 1).row
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 - row) <= 8 and row - 8 > 0 then
height = math.min(height, row - 1)
row = row - height - 1
else
height = math.min(height, vim.o.lines - row)
end
if width < 1 or height < 1 then
return
end
local delta = cursor[2] + 1 - self.offset
self.entries_win:option('cursorline', false)
self.entries_win:open({
relative = 'editor',
style = 'minimal',
row = row,
col = vim.fn.screencol() - 1 - delta - 1,
width = width,
height = height,
zindex = 1001,
})
vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 1 })
if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
self:preselect(preselect)
elseif string.match(config.get().completion.completeopt, 'noinsert') then
self:preselect(1)
else
self:draw()
end
self.event:emit('change')
end
custom_entries_view.close = function(self)
self.offset = -1
self.entries = {}
self.entries_win:close()
end
custom_entries_view.abort = function(self)
if self.prefix then
self:_insert(self.prefix)
end
self:close()
end
custom_entries_view.draw = function(self)
local info = vim.fn.getwininfo(self.entries_win.win)[1]
local topline = info.topline - 1
local botline = info.topline + info.height - 1
local texts = {}
for i = topline, botline - 1 do
local view = self.entries[i + 1]:get_view(self.offset)
local text = {}
table.insert(text, ' ')
table.insert(text, view.abbr.text)
table.insert(text, string.rep(' ', 1 + self.column_width.abbr - view.abbr.width))
table.insert(text, view.kind.text)
table.insert(text, string.rep(' ', 1 + self.column_width.kind - view.kind.width))
table.insert(text, view.menu.text)
table.insert(text, string.rep(' ', 1 + self.column_width.menu - view.menu.width))
table.insert(text, ' ')
table.insert(texts, table.concat(text, ''))
end
vim.api.nvim_buf_set_lines(self.entries_win.buf, topline, botline, false, texts)
end
custom_entries_view.visible = function(self)
return self.entries_win:visible()
end
custom_entries_view.info = function(self)
return self.entries_win:info()
end
custom_entries_view.preselect = function(self, index)
if self:visible() then
if index <= #self.entries then
self.entries_win:option('cursorline', true)
vim.api.nvim_win_set_cursor(self.entries_win.win, { index, 1 })
self.entries_win:update()
self:draw()
end
end
end
custom_entries_view.select_next_item = function(self, option)
if self.entries_win:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] + 1
if not self.entries_win:option('cursorline') then
cursor = 1
elseif #self.entries < cursor then
cursor = 0
end
self:_select(cursor, option)
end
end
custom_entries_view.select_prev_item = function(self, option)
if self.entries_win:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] - 1
if not self.entries_win:option('cursorline') then
cursor = #self.entries
end
self:_select(cursor, option)
end
end
custom_entries_view.get_first_entry = function(self)
if self.entries_win:visible() then
return self.entries[1]
end
end
custom_entries_view.get_selected_entry = function(self)
if self.entries_win:visible() and self.entries_win:option('cursorline') then
return self.entries[vim.api.nvim_win_get_cursor(self.entries_win.win)[1]]
end
end
custom_entries_view.get_active_entry = function(self)
if self.entries_win:visible() and self.entries_win:option('cursorline') then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)
if cursor[2] == 0 then
return self:get_selected_entry()
end
end
end
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 ''
end
end
self.entries_win:option('cursorline', cursor > 0)
vim.api.nvim_win_set_cursor(self.entries_win.win, { math.max(cursor, 1), is_insert and 0 or 1 })
if is_insert then
self:_insert(self.entries[cursor] and self.entries[cursor]:get_vim_item(self.offset).word or self.prefix)
end
self.entries_win:update()
self:draw()
self.event:emit('change')
end
custom_entries_view._insert = function(self, word)
vim.api.nvim_buf_set_keymap(0, 'i', '<Plug>(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('<Plug>(cmp.view.custom_entries_view._insert.remove)'), 't')
keymap.feedkeys(word, 'nt')
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.t(string.rep('<C-g>U<Left><Del>', length))
end)
return custom_entries_view

126
lua/cmp/view/docs_view.lua Normal file
View File

@@ -0,0 +1,126 @@
local window = require('cmp.utils.window')
local config = require('cmp.config')
---@class cmp.DocsView
---@field public window cmp.Window
local docs_view = {}
---Create new floating window module
docs_view.new = function()
local self = setmetatable({}, { __index = docs_view })
self.entry = nil
self.window = window.new()
self.window:option('conceallevel', 2)
self.window:option('concealcursor', 'n')
self.window:option('foldenable', false)
self.window:option('scrolloff', 0)
self.window:option('wrap', true)
return self
end
---Open documentation window
---@param e cmp.Entry
---@param view cmp.WindowStyle
docs_view.open = function(self, e, view)
local documentation = config.get().documentation
if not documentation then
return
end
if not e or not view then
return self:close()
end
local right_space = vim.o.columns - (view.col + view.width) - 2
local left_space = view.col - 2
local maxwidth = math.min(documentation.maxwidth, math.max(left_space, right_space))
-- update buffer content if needed.
if not self.entry or e.id ~= self.entry.id then
local documents = e:get_documentation()
if #documents == 0 then
return self:close()
end
self.entry = e
vim.api.nvim_buf_call(self.window.buf, function()
vim.cmd([[syntax clear]])
end)
vim.lsp.util.stylize_markdown(self.window.buf, documents, {
max_width = maxwidth,
max_height = documentation.maxheight,
})
end
local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window.buf, 0, -1, false), {
max_width = maxwidth,
max_height = documentation.maxheight,
})
if width <= 0 or height <= 0 then
return self:close()
end
local right_col = view.col + view.width
local left_col = view.col - width - 2
local col, left
if right_space >= width and left_space >= width then
if right_space < left_space then
col = left_col
left = true
else
col = right_col
end
elseif right_space >= width then
col = right_col
elseif left_space >= width then
col = left_col
left = true
else
return self:close()
end
self.window:option('winhighlight', documentation.winhighlight)
self.window:set_style({
relative = 'editor',
style = 'minimal',
width = width,
height = height,
row = view.row,
col = col,
border = documentation.border,
})
if left and self.window:has_scrollbar() then
self.window.style.col = self.window.style.col - 1
end
self.window:open()
end
---Close floating window
docs_view.close = function(self)
self.window:close()
self.entry = nil
end
docs_view.scroll = function(self, delta)
if self:visible() then
local info = vim.fn.getwininfo(self.window.win)[1] or {}
local top = info.topline or 1
top = top + delta
top = math.max(top, 1)
top = math.min(top, self.window:get_content_height() - info.height + 1)
vim.defer_fn(function()
vim.api.nvim_buf_call(self.window.buf, function()
vim.api.nvim_command('normal! ' .. top .. 'zt')
self.window:update()
end)
end, 0)
end
end
docs_view.visible = function(self)
return self.window:visible()
end
return docs_view

View File

@@ -0,0 +1,72 @@
local config = require('cmp.config')
local str = require('cmp.utils.str')
local types = require('cmp.types')
---@class cmp.GhostTextView
local ghost_text_view = {}
ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT')
ghost_text_view.new = function()
local self = setmetatable({}, { __index = ghost_text_view })
self.win = nil
self.entry = nil
vim.api.nvim_set_decoration_provider(ghost_text_view.ns, {
on_win = function(_, win)
return win == self.win
end,
on_line = function(_)
local c = config.get().experimental.ghost_text
if not c then
return
end
if not self.entry then
return
end
local cursor = vim.api.nvim_win_get_cursor(0)
if string.sub(vim.api.nvim_get_current_line(), cursor[2] + 1) ~= '' then
return
end
local diff = 1 + cursor[2] - self.entry:get_offset()
local text = self.entry:get_insert_text()
if self.entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
text = vim.lsp.util.parse_snippet(text)
end
text = string.sub(str.oneline(text), diff + 1)
if #text > 0 then
vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, cursor[1] - 1, cursor[2], {
right_gravity = false,
virt_text = { { text, c.hl_group or 'Comment' } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
ephemeral = true,
})
end
end,
})
return self
end
---Show ghost text
---@param e cmp.Entry
ghost_text_view.show = function(self, e)
local changed = e ~= self.entry
self.win = vim.api.nvim_get_current_win()
self.entry = e
if changed then
vim.cmd([[redraw!]]) -- force invoke decoration provider.
end
end
ghost_text_view.hide = function(self)
if self.win and self.entry then
self.win = nil
self.entry = nil
vim.cmd([[redraw!]]) -- force invoke decoration provider.
end
end
return ghost_text_view

View File

@@ -0,0 +1,152 @@
local event = require('cmp.utils.event')
local autocmd = require('cmp.utils.autocmd')
local keymap = require('cmp.utils.keymap')
local types = require('cmp.types')
local config = require('cmp.config')
---@class cmp.NativeEntriesView
---@field private offset number
---@field private items vim.CompletedItem
---@field private entries cmp.Entry[]
---@field private preselect number
---@field public event cmp.Event
local native_entries_view = {}
native_entries_view.new = function()
local self = setmetatable({}, { __index = native_entries_view })
self.event = event.new()
self.offset = -1
self.items = {}
self.entries = {}
self.preselect = 0
autocmd.subscribe('CompleteChanged', function()
self.event:emit('change')
end)
return self
end
native_entries_view.ready = function(_)
if vim.fn.pumvisible() == 0 then
return true
end
return vim.fn.complete_info({ 'mode' }).mode == 'eval'
end
native_entries_view.redraw = function(self)
if #self.entries > 0 and self.offset <= vim.api.nvim_win_get_cursor(0)[2] then
local completeopt = vim.o.completeopt
vim.o.completeopt = self.preselect == 1 and 'menu,menuone,noinsert' or config.get().completion.completeopt
vim.fn.complete(self.offset, self.items)
vim.o.completeopt = completeopt
if self.preselect > 1 and config.get().preselect == types.cmp.PreselectMode.Item then
self:preselect(self.preselect)
end
end
end
native_entries_view.open = function(self, offset, entries)
local dedup = {}
local items = {}
local dedup_entries = {}
local preselect = 0
for _, e in ipairs(entries) do
local item = e:get_vim_item(offset)
if item.dup == 1 or not dedup[item.abbr] then
dedup[item.abbr] = true
table.insert(items, item)
table.insert(dedup_entries, e)
if preselect == 0 and e.completion_item.preselect then
preselect = #dedup_entries
end
end
end
self.offset = offset
self.items = items
self.entries = dedup_entries
self.preselect = preselect
self:redraw()
end
native_entries_view.close = function(self)
if string.sub(vim.api.nvim_get_mode().mode, 1, 1) == 'i' then
vim.fn.complete(1, {})
end
self.offset = -1
self.entries = {}
self.items = {}
self.preselect = 0
end
native_entries_view.abort = function(_)
if string.sub(vim.api.nvim_get_mode().mode, 1, 1) == 'i' then
vim.api.nvim_select_popupmenu_item(-1, true, true, {})
end
end
native_entries_view.visible = function(_)
return vim.fn.pumvisible() == 1
end
native_entries_view.info = function(self)
if self:visible() then
local info = vim.fn.pum_getpos()
return {
width = info.width + (info.scrollbar and 1 or 0),
height = info.height,
row = info.row,
col = info.col,
}
end
end
native_entries_view.preselect = function(self, index)
if self:visible() then
if index <= #self.entries then
vim.api.nvim_select_popupmenu_item(index - 1, false, false, {})
end
end
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('<C-n>'), 'n')
else
keymap.feedkeys(keymap.t('<Down>'), 'n')
end
end
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('<C-p>'), 'n')
else
keymap.feedkeys(keymap.t('<Up>'), 'n')
end
end
end
native_entries_view.get_first_entry = function(self)
if self:visible() then
return self.entries[1]
end
end
native_entries_view.get_selected_entry = function(self)
if self:visible() then
local idx = vim.fn.complete_info({ 'selected' }).selected
if idx > -1 then
return self.entries[math.max(0, idx) + 1]
end
end
end
native_entries_view.get_active_entry = function(self)
if self:visible() and (vim.v.completed_item or {}).word then
return self:get_selected_entry()
end
end
return native_entries_view