Files
nvim-cmp/lua/cmp/init.lua
hrsh7th fae808a2bc dev (#813)
* feat: completion menu borders (#472)

* feat(custom_entries_view): pass custom border option

* feat(window): calculate offset needed for borders

* fix(window): adjust window height w/ too many results

* fix(window): center scrollbar with borders

* ref(custom_entries_view): use `FloatBorder` for borders

* fix(window): offset at bottom of window

* ref(window): move height adjustment to more logical place

* fix(window): improve popup placement

* fix(window): `border_offset` always `0` first time

* feat(window): support compact scrollbar with border

* fix(window): completion popup on cursorline

* perf(window): simplify offset calculation

String indexing will result in the same thing as if I gated it behind
`type()` calls here.

* docs(window): add `border` to `cmp.WindowStyle`

* docs(window): correct `border_offset_scrollbar`

* perf(window): calulated row -> `screenrow`

This will also be more accurate since it accounts for wrapped lines, as
well as buffers.

* fix(window): edge case with multiple splits

* ref(winhighlight): don't specify defaults by default

`NormalFloat:NormalFloat` isn't needed, since `NormalFloat` defaults to
`NormalFloat`. As for `FloatBorder`, that should be set to `Floatborder`
rather than `NormalFloat` or else you get unintended artifacts on the
edges of borders.

* fix(window): popup covers cursor when scrollbar disappears

* ref(window): calc `border_offset_col` on `set_style`

* perf(window): remove unecessary `col` calculation

Taking it out didn't change anything about the popup behavior.

* feat: add `CmpItemMenuThumb` group

* feat(window): improve scrollbar appearance

* chore(window): remove references to unused property

* docs: document new option `thin_scrollbar`

* ref(plugin): remove background from `thin_scrollbar`

* feat(view): pass `thin_scrollbar` option to window

* feat(window): gate new `thin_scrollbar` behind option

* fix(window): cmdline bugging out

* fix(cmp): docs_view pops up overlapped when using borders

This is related to 1cfe2f7dfd. The
calculation for how the popup position is calculated was changed, and
so it needed to be reworked to include borders in order to be able to
work.

* ref: `thin_scrollbar` flag -> `scrollbar` option

This change allows users to define which character they will use for
their scrollbar.

* fix(window): use `scrollbar` setting for scrollbar character

Thanks @Astrantia for pointing this one out.

* docs(README): add completion appearance options to FAQ

* fix(): account for `border_offset_row` with `has_bottom_space`

* style(custom_entries_view): group offset with `row`/`col`

* fix(window): scrollbar at full view height

Because the `bar_height` variable must be whole number, and must be rounded up
from a percent, there is a change that we will end up with the maximum
height as a number.

For example, `info.height` = 24 and `total` = 25.

* feat(window): allow scrollbar to be disabled

* fix(window): scrollbar size < 1

* ref(cmp): move border logic to `window.info`

* ref!: window highlighting based on borders

BREAKING CHANGE: `documentation.winhighlight` does not determine the
                 highlighting of the `documentation` view— `CmpWindow`
		 or `CmpBorderedWindow` depending on whether it has a
		 border.

* ref!: float appearance opts -> `cmp.setup.window`

`cmp.setup.completion.border` and `.scrollbar` were both moved to
`cmp.setup.window.completion.border` and `.scrollbar`

BREAKING CHANGE: `cmp.setup.documentation` has been moved to
                 `cmp.window.documentation`, as all of the pertaining
		 options were cosmetic.

TODO: document the change

* fix(window): attempt to get scrollbar's border

* fix(cmp): restore `view.menu.hl_group`

* fix(window): wrong scrollbar position

* ref: get default `CmpItemMenu` from border existence

* chore(cmp): remove old PR comments

* fix(window): scrollbar sometimes too big

* fix(window): docs far away with complete menu scrollbar

* perf(docs_view): reuse `border_width` value

* rev(cmp): restore `CmpItemMenu`

* ref(cmp): distinguish between `ScrollBar` and `ScrollThumb`

* fix(plugin): consistently refer to `Thumb` as `Thumb`

* rev(window): `Pmenu`-style scrollbar when no border

* fix(window): docs_view size wrong when first shown

* fix(window): docs_view scrollbar not responding to size

* fix(window): scrollbar sometimes to small, take 2

* fix(window): scrollbar bg not hiding

* ref(docs_view): put docs closer to completion menu

* fix(window): scrollbar position wrong with right border

* ref(config): add default border to documentation

* fix(window): scrollbar too close without border

* ref(plugin): link `CmpWindow` to `Pmenu`

I set `CmpWindow` to `NormalFloat`, because that is what you would
expect a floating window to use for a highlight group. However at
request I changed it to `Pmenu`.

* ref(plugin): link `CmpWindowBorder` to `CmpWindow`

* fix(window): scrollbar following thumb while scrolling

* ref: add more highlight groups

There just weren't enough highlight groups to satisfy the demands of the
project. If you change `CmpWindow` to `Pmenu`, then the `docs_view`
becomes `Pmenu` as well when on `main` it is `NormalFloat`.

* fix(window): scrollbar overlapping `docs_view` by default

* ref: remove `Bordered` highlight variants

* ref(utils): extract whitespace check to func

* feat: `window.completion.zindex` setting

* ref: `maxwidth|height` -> `max_`

* ref: simplify highlight groups

* feat: `window.*.winhighlight` setting

* ref(utils): `is_whitespace_char` -> `is_visible`

As hrsh7th noted, `''` is not a whitespace character. Yet, it is
necessary to group `''` and `' '` together for certain border behaviors
that are based on visibility. Thus I have renamed the function

* feat: specify `window.*.winhighlight` for un/bordered

* fix(custom_entries_view): set `winhighlight` on `open`

* ref: remove `Cmp*Scroll*` variants

There's no way for `window` to know which kind of window it is drawing a
scrollbar on. Simpler to just have one kind of scrollbar

* feat: distinguish between bordered and unbordered

* ref(cmp): `is_visible` -> `is_invisible`

That's what the function was checking for.

* fix(default): mislabeling of `default` and `bordered`

* chore: rebase fixup

* Change default highlight

* Add misc.rep

* Fix left-side docs_view with scrollbar

* Fix scrollbar

* Fix sbar/thumb win
Improve highlights

* Remove scrollbar cutomization for now

* Remove scrollbar option

* Simplify implementation

* Fix doc width

* Fix outdated docs

* Add comments

* Fix configuration schema

* fmt

* Fix for lint

Co-authored-by: Iron-E <36409591+Iron-E@users.noreply.github.com>
Co-authored-by: hrsh7th <>
2022-04-13 23:51:55 +09:00

347 lines
8.6 KiB
Lua

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')
local cmp = {}
cmp.core = core.new()
---Expose types
for k, v in pairs(require('cmp.types.cmp')) do
cmp[k] = v
end
cmp.lsp = require('cmp.types.lsp')
cmp.vim = require('cmp.types.vim')
---Expose event
cmp.event = cmp.core.event
---Export mapping for special case
cmp.mapping = require('cmp.config.mapping')
---Export default config presets
cmp.config = {}
cmp.config.disable = misc.none
cmp.config.compare = require('cmp.config.compare')
cmp.config.sources = require('cmp.config.sources')
cmp.config.mapping = require('cmp.config.mapping')
cmp.config.window = require('cmp.config.window')
---Sync asynchronous process.
cmp.sync = function(callback)
return function(...)
cmp.core.filter:sync(1000)
if callback then
return callback(...)
end
end
end
---Suspend completion.
cmp.suspend = function()
return cmp.core:suspend()
end
---Register completion sources
---@param name string
---@param s cmp.Source
---@return number
cmp.register_source = function(name, s)
local src = source.new(name, s)
cmp.core:register_source(src)
return src.id
end
---Unregister completion source
---@param id number
cmp.unregister_source = function(id)
cmp.core:unregister_source(id)
end
---Get current configuration.
---@return cmp.ConfigSchema
cmp.get_config = function()
return require('cmp.config').get()
end
---Invoke completion manually
---@param option cmp.CompleteParams
cmp.complete = cmp.sync(function(option)
option = option or {}
config.set_onetime(option.config)
cmp.core:complete(cmp.core:get_context({ reason = option.reason or cmp.ContextReason.Manual }))
return true
end)
---Complete common string in current entries.
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
end)
---Get current selected entry or nil
cmp.get_selected_entry = cmp.sync(function()
return cmp.core.view:get_selected_entry()
end)
---Get current active entry or nil
cmp.get_active_entry = cmp.sync(function()
return cmp.core.view:get_active_entry()
end)
---Get current all entries
cmp.get_entries = cmp.sync(function()
return cmp.core.view:get_entries()
end)
---Close current completion
cmp.close = cmp.sync(function()
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:close()
cmp.core:reset()
vim.schedule(release)
return true
else
return false
end
end)
---Abort current completion
cmp.abort = cmp.sync(function()
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:abort()
vim.schedule(release)
return true
else
return false
end
end)
---Select next item if possible
cmp.select_next_item = cmp.sync(function(option)
option = option or {}
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:select_next_item(option)
vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
-- Special handling for native pum. Required to facilitate key mapping processing.
if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t('<C-n>'), 'in')
else
feedkeys.call(keymap.t('<Down>'), 'in')
end
return true
end
return false
end)
---Select prev item if possible
cmp.select_prev_item = cmp.sync(function(option)
option = option or {}
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:select_prev_item(option)
vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
-- Special handling for native pum. Required to facilitate key mapping processing.
if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t('<C-p>'), 'in')
else
feedkeys.call(keymap.t('<Up>'), 'in')
end
return true
end
return false
end)
---Scrolling documentation window if possible
cmp.scroll_docs = cmp.sync(function(delta)
if cmp.core.view:visible() then
cmp.core.view:scroll_docs(delta)
return true
else
return false
end
end)
---Confirm completion
cmp.confirm = cmp.sync(function(option, callback)
option = option or {}
callback = callback or function() 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, {
behavior = option.behavior,
}, function()
callback()
cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
end)
return true
else
-- Special handling for native puma. Required to facilitate key mapping processing.
if vim.fn.complete_info({ 'selected' }).selected ~= -1 then
feedkeys.call(keymap.t('<C-y>'), 'in')
return true
end
return false
end
end)
---Show status
cmp.status = function()
local kinds = {}
kinds.available = {}
kinds.unavailable = {}
kinds.installed = {}
kinds.invalid = {}
local names = {}
for _, s in pairs(cmp.core.sources) do
names[s.name] = true
if config.get_source_config(s.name) then
if s:is_available() then
table.insert(kinds.available, s:get_debug_name())
else
table.insert(kinds.unavailable, s:get_debug_name())
end
else
table.insert(kinds.installed, s:get_debug_name())
end
end
for _, s in ipairs(config.get().sources) do
if not names[s.name] then
table.insert(kinds.invalid, s.name)
end
end
if #kinds.available > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# ready source names\n', 'Special' } }, false, {})
for _, name in ipairs(kinds.available) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
if #kinds.unavailable > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# unavailable source names\n', 'Comment' } }, false, {})
for _, name in ipairs(kinds.unavailable) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
if #kinds.installed > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# unused source names\n', 'WarningMsg' } }, false, {})
for _, name in ipairs(kinds.installed) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
if #kinds.invalid > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# unknown source names\n', 'ErrorMsg' } }, false, {})
for _, name in ipairs(kinds.invalid) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
end
---@type cmp.Setup
cmp.setup = setmetatable({
global = function(c)
config.set_global(c)
end,
filetype = function(filetype, c)
config.set_filetype(c, filetype)
end,
buffer = function(c)
config.set_buffer(c, vim.api.nvim_get_current_buf())
end,
cmdline = function(type, c)
config.set_cmdline(c, type)
end,
}, {
__call = function(self, c)
self.global(c)
end,
})
autocmd.subscribe('InsertEnter', function()
feedkeys.call('', 'i', function()
if config.enabled() then
cmp.core:prepare()
cmp.core:on_change('InsertEnter')
end
end)
end)
autocmd.subscribe('InsertLeave', function()
cmp.core:reset()
cmp.core.view:close()
end)
autocmd.subscribe('CmdlineEnter', function()
if config.enabled() then
cmp.core:prepare()
cmp.core:on_change('InsertEnter')
end
end)
autocmd.subscribe('CmdlineLeave', function()
cmp.core:reset()
cmp.core.view:close()
end)
autocmd.subscribe('TextChanged', function()
if config.enabled() then
cmp.core:on_change('TextChanged')
end
end)
autocmd.subscribe('CursorMoved', function()
if config.enabled() then
cmp.core:on_moved()
else
cmp.core:reset()
cmp.core.view:close()
end
end)
autocmd.subscribe('InsertEnter', function()
cmp.config.compare.scopes:update()
cmp.config.compare.locality:update()
end)
cmp.event:on('complete_done', function(evt)
if evt.entry then
cmp.config.compare.recently_used:add_entry(evt.entry)
end
cmp.config.compare.scopes:update()
cmp.config.compare.locality:update()
end)
cmp.event:on('confirm_done', function(evt)
if evt.entry then
cmp.config.compare.recently_used:add_entry(evt.entry)
end
end)
return cmp