diff --git a/README.md b/README.md index a096cc4..9ddfa7f 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,10 @@ lua <'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), [''] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), @@ -131,85 +135,16 @@ lua < lua vimrc.cmp.lsp() - inoremap lua vimrc.cmp.snippet() -]]) -``` - -### Full managed completion behavior. - -```lua -local cmp = require('cmp') - -cmp.setup { - completion = { - autocomplete = false, -- disable auto-completion. - } -} - -_G.vimrc = _G.vimrc or {} -_G.vimrc.cmp = _G.vimrc.cmp or {} -_G.vimrc.cmp.on_text_changed = function() - local cursor = vim.api.nvim_win_get_cursor(0) - local line = vim.api.nvim_get_current_line() - local before = string.sub(line, 1, cursor[2] + 1) - if before:match('%s*$') then - cmp.complete() -- Trigger completion only if the cursor is placed at the end of line. - end -end -vim.cmd([[ - augroup vimrc - autocmd - autocmd TextChanged,TextChangedI,TextChangedP * call luaeval('vimrc.cmp.on_text_changed()') - augroup END -]]) -``` - - +See the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki) diff --git a/doc/cmp.txt b/doc/cmp.txt index bdb12d0..da11aa0 100644 --- a/doc/cmp.txt +++ b/doc/cmp.txt @@ -14,11 +14,10 @@ Command |cmp-command| Highlight |cmp-highlight| Autocmd |cmp-autocmd| Config |cmp-config| +Config Helper |cmp-config-helper| Develop |cmp-develop| FAQ |cmp-faq| - - ============================================================================== Abstract *cmp-abstract* @@ -29,8 +28,6 @@ This is nvim-cmp's document. 2. The advanced configuration is noted in wiki. - https://github.com/hrsh7th/nvim-cmp/wiki - - ============================================================================== Concept *cmp-concept* @@ -39,8 +36,6 @@ Concept *cmp-concept* - Smart handling of key mapping - No flicker - - ============================================================================== Usage *cmp-usage* @@ -48,7 +43,7 @@ The recommendation configurations are the below. NOTE: 1. You must setup `snippet.expand` function. - 2. The `cmp.setup.cmdline` won't work if you are using `native` completion menu. + 2. The `cmp.setup.cmdline` won't work if you are using `native` completion menu. 3. You can disable the `default` options via specifying `cmp.config.disable` value. > call plug#begin(s:plug_dir) @@ -92,6 +87,10 @@ NOTE: -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users. end, }, + window = { + -- completion = cmp.config.window.bordered(), + -- documentation = cmp.config.window.bordered(), + }, mapping = { [''] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), [''] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), @@ -139,7 +138,6 @@ NOTE: EOF < - ============================================================================== Function *cmp-function* @@ -232,8 +230,6 @@ NOTE: You can call these functions in mapping via `lua require('cmp').compl - `complete_done`: emit after current completion is done. - `confirm_done`: emit after confirmation is done. - - ============================================================================== Mapping *cmp-mapping* @@ -305,8 +301,6 @@ You can also use built-in mapping helpers. The built-in mapping helper is only available as a configuration option. If you want to call the nvim-cmp features directly, please use |cmp-function| instead. - - ============================================================================== Command *cmp-command* @@ -315,8 +309,6 @@ Command *cmp-command* Sometimes `unknown` source will be printed but it isn't problem. (e.g. `cmp-nvim-lsp`) That the reason is the `cmp-nvim-lsp` will registered on the InsertEnter autocmd. - - ============================================================================== Highlight *cmp-highlight* @@ -345,7 +337,6 @@ Highlight *cmp-highlight* The menu field's highlight group. - ============================================================================== Autocmd *cmp-autocmd* @@ -418,7 +409,7 @@ formatting.fields~ formatting.format~ `fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem` The function to customize the completion menu appearance. See |complete-items|. - This value also can be used to modify `dup` property. + This value also can be used to modify `dup` property. NOTE: The `vim.CompletedItem` can have special properties `abbr_hl_group`, `kind_hl_group` and `menu_hl_group`. @@ -524,6 +515,32 @@ view~ Specify the view class to customize appearance. Currently, the possible configurations are: + *cmp-config.window.{completion,documentation}.border* +window.{completion,documentation}.border~ + `string | string[] | nil` + Border characters used for the completion popup menu when + |experimental.native_menu| is disabled. + + *cmp-config.window.{completion,documentation}.winhighlight* +window.{completion,documentation}.winhighlight~ + `string | cmp.WinhighlightConfig` + Specify the window's winhighlight option. + + *cmp-config.window.{completion,documentation}.zindex* +window.{completion,documentation}.zindex~ + `number` + The completion window's zindex. + + *cmp-config.window.documentation.max_width* +window.documentation.max_width~ + `number` + The documentation window's max width. + + *cmp-config.window.documentation.max_height* +window.documentation.max_height~ + `number` + The documentation window's max height. + *cmp-config.experimental.ghost_text* experimental.ghost_text~ `boolean | { hl_group = string }` @@ -531,6 +548,66 @@ experimental.ghost_text~ +============================================================================== +Config Helper *cmp-config-helper* + +You can use the following configuration helpers. + +cmp.config.compare~ + + TBD + +cmp.config.context~ + + The `cmp.config.context` can be used to context-aware completion toggling. +> + cmp.setup { + enabled = function() + -- disable completion if the cursor is `Comment` syntax group. + return not cmp.config.context.in_syntax_group('Comment') + end + } +< + *cmp.config.context.in_syntax_group* (group) + You can specify the vim's built-in syntax group. + If you use tree-sitter, You should use `cmp.config.context.in_treesitter_capture` instead. + + *cmp.config.context.in_treesitter_capture* (capture) + You can specify the treesitter capture name. + If you don't use `nvim-treesitter`, this helper does not work correctly. + +cmp.config.mapping~ + + See |:help cmp-mapping| + +cmp.config.sources~ + + *cmp.config.sources* (...sources) + You can specify multiple sources arrays. + The sources are grouped in the order you specify, and the groups are displayed in a fallback, like chain completion. +> + cmp.setup { + window = { + completion = cmp.config.window.bordered(), + documentation = cmp.config.window.bordered(), + } + } +< +cmp.config.window~ + + *cmp.config.window.bordered* (option) + Specify some window as `bordered`. + The option is described in the `cmp.ConfigSchema`. +> + cmp.setup { + window = { + completion = cmp.config.window.bordered(), + documentation = cmp.config.window.bordered(), + } + } +< + + ============================================================================== Develop *cmp-develop* @@ -611,7 +688,6 @@ You can create custom source like the following example. require('cmp').register_source('month', source.new()) < - ============================================================================== FAQ *cmp-faq* @@ -628,7 +704,6 @@ How to disable the preselect feature? ~ } < - How to disable auto-completion?~ How to use nvim-cmp as like omnifunc?~ @@ -660,6 +735,17 @@ How to setup on the specific buffer?~ }) < +How to disable documentation window?~ + + You can specify the following config. +> + cmp.setup.filetype({ 'markdown', 'help' }, { + window = { + documentation = cmp.config.disable + } + }) +< + How to integrate with copilot.vim?~ The copilot.vim and nvim-cmp both have a `key-mapping fallback` mechanism. @@ -689,8 +775,5 @@ How to customize menu appearance?~ You can see the nvim-cmp wiki (https://github.com/hrsh7th/nvim-cmp/wiki). - - ============================================================================== - vim:tw=78:ts=4:et:ft=help:norl: - + vim:tw=78:ts=2:et:ft=help:norl: diff --git a/lua/cmp/config.lua b/lua/cmp/config.lua index 1772184..12f3211 100644 --- a/lua/cmp/config.lua +++ b/lua/cmp/config.lua @@ -29,7 +29,7 @@ config.onetime = {} ---Set configuration for global. ---@param c cmp.ConfigSchema config.set_global = function(c) - config.global = misc.merge(config.normalize(c), config.normalize(config.global)) + config.global = config.normalize(misc.merge(c, config.global)) config.global.revision = config.global.revision or 1 config.global.revision = config.global.revision + 1 end @@ -82,7 +82,7 @@ config.get = function() global_config.revision or 0, onetime_config.revision or 0, }, function() - return misc.merge(config.normalize(onetime_config), config.normalize(global_config)) + return config.normalize(misc.merge(onetime_config, global_config)) end) elseif api.is_cmdline_mode() then local cmdtype = vim.fn.getcmdtype() @@ -94,7 +94,7 @@ config.get = function() cmdtype, cmdline_config.revision or 0, }, function() - return misc.merge(config.normalize(cmdline_config), config.normalize(global_config)) + return config.normalize(misc.merge(cmdline_config, global_config)) end) else local bufnr = vim.api.nvim_get_current_buf() @@ -111,9 +111,9 @@ config.get = function() buffer_config.revision or 0, }, function() local c = {} - c = misc.merge(c, config.normalize(buffer_config)) - c = misc.merge(c, config.normalize(filetype_config)) - c = misc.merge(c, config.normalize(global_config)) + c = config.normalize(misc.merge(c, buffer_config)) + c = config.normalize(misc.merge(c, filetype_config)) + c = config.normalize(misc.merge(c, global_config)) return c end) end @@ -160,6 +160,7 @@ config.normalize = function(c) -- make sure c is not 'nil' c = c == nil and {} or c + -- Normalize mapping. if c.mapping then local normalized = {} for k, v in pairs(c.mapping) do @@ -168,6 +169,7 @@ config.normalize = function(c) c.mapping = normalized end + -- Notice experimental.native_menu. if c.experimental and c.experimental.native_menu then vim.api.nvim_echo({ { '[nvim-cmp] ', 'Normal' }, @@ -182,6 +184,7 @@ config.normalize = function(c) c.view.entries = c.view.entries or 'native' end + -- Notice sources.[n].opts if c.sources then for _, s in ipairs(c.sources) do if s.opts and not s.option then diff --git a/lua/cmp/config/compare.lua b/lua/cmp/config/compare.lua index 91db9fc..b198773 100644 --- a/lua/cmp/config/compare.lua +++ b/lua/cmp/config/compare.lua @@ -145,7 +145,7 @@ compare.locality = setmetatable({ self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row)) end end - end + end, }, { __call = function(self, entry1, entry2) local local1 = self.locality_map[entry1:get_word()] @@ -159,7 +159,7 @@ compare.locality = setmetatable({ end return local1 < local2 end - end + end, }) -- scopes diff --git a/lua/cmp/config/default.lua b/lua/cmp/config/default.lua index d596444..d05675f 100644 --- a/lua/cmp/config/default.lua +++ b/lua/cmp/config/default.lua @@ -86,12 +86,12 @@ return function() }, completion = { - keyword_length = 1, - keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], autocomplete = { types.cmp.TriggerEvent.TextChanged, }, completeopt = 'menu,menuone,noselect', + keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], + keyword_length = 1, }, formatting = { @@ -125,13 +125,6 @@ return function() sources = {}, - documentation = { - border = { '', '', '', ' ', '', '', '', ' ' }, - winhighlight = 'NormalFloat:NormalFloat,FloatBorder:NormalFloat', - maxwidth = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))), - maxheight = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)), - }, - confirmation = { default_behavior = types.cmp.ConfirmBehavior.Insert, get_commit_characters = function(commit_characters) @@ -148,5 +141,18 @@ return function() view = { entries = { name = 'custom', selection_order = 'top_down' }, }, + + window = { + completion = { + border = { '', '', '', '', '', '', '', '' }, + winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None', + }, + documentation = { + max_height = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)), + max_width = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))), + border = { '', '', '', ' ', '', '', '', ' ' }, + winhighlight = 'FloatBorder:NormalFloat', + }, + }, } end diff --git a/lua/cmp/config/window.lua b/lua/cmp/config/window.lua new file mode 100644 index 0000000..b6fec0f --- /dev/null +++ b/lua/cmp/config/window.lua @@ -0,0 +1,12 @@ +local window = {} + +window.bordered = function(opts) + opts = opts or {} + return { + border = opts.border or 'rounded', + winhighlight = opts.winhighlight or 'Normal:Normal,FloatBorder:Normal,CursorLine:Visual,Search:None', + zindex = opts.zindex or 1001, + } +end + +return window diff --git a/lua/cmp/init.lua b/lua/cmp/init.lua index dbcd866..ac839cb 100644 --- a/lua/cmp/init.lua +++ b/lua/cmp/init.lua @@ -29,6 +29,7 @@ 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) diff --git a/lua/cmp/types/cmp.lua b/lua/cmp/types/cmp.lua index b4e9402..574e533 100644 --- a/lua/cmp/types/cmp.lua +++ b/lua/cmp/types/cmp.lua @@ -75,7 +75,7 @@ cmp.ItemField.Menu = 'menu' ---@field public enabled fun():boolean|boolean ---@field public preselect cmp.PreselectMode ---@field public completion cmp.CompletionConfig ----@field public documentation cmp.DocumentationConfig|"false" +---@field public window cmp.WindowConfig|nil ---@field public confirmation cmp.ConfirmationConfig ---@field public matching cmp.MatchingConfig ---@field public sorting cmp.SortingConfig @@ -86,19 +86,23 @@ cmp.ItemField.Menu = 'menu' ---@field public view cmp.ViewConfig ---@field public experimental cmp.ExperimentalConfig +--- @class cmp.WindowConfig +--- @field completion cmp.WindowConfig +--- @field documentation cmp.WindowConfig|nil + ---@class cmp.CompletionConfig ---@field public autocomplete cmp.TriggerEvent[] ---@field public completeopt string ----@field public keyword_pattern string ----@field public keyword_length number ---@field public get_trigger_characters fun(trigger_characters: string[]): string[] +---@field public keyword_length number +---@field public keyword_pattern string ----@class cmp.DocumentationConfig ----@field public border string[] +---@class cmp.WindowConfig +---@field public border string|string[] ---@field public winhighlight string ----@field public maxwidth number|nil ----@field public maxheight number|nil ---@field public zindex number|nil +---@field public max_width number|nil +---@field public max_height number|nil ---@class cmp.ConfirmationConfig ---@field public default_behavior cmp.ConfirmBehavior diff --git a/lua/cmp/utils/misc.lua b/lua/cmp/utils/misc.lua index 8dd3529..9535f28 100644 --- a/lua/cmp/utils/misc.lua +++ b/lua/cmp/utils/misc.lua @@ -29,6 +29,24 @@ misc.concat = function(list1, list2) return new_list end +---Repeat values +---@generic T +---@param str_or_tbl T +---@param count number +---@return T +misc.rep = function(str_or_tbl, count) + if type(str_or_tbl) == 'string' then + return string.rep(str_or_tbl, count) + end + local rep = {} + for _ = 1, count do + for _, v in ipairs(str_or_tbl) do + table.insert(rep, v) + end + end + return rep +end + ---Return the valu is empty or not. ---@param v any ---@return boolean diff --git a/lua/cmp/utils/str.lua b/lua/cmp/utils/str.lua index 450c991..bca210c 100644 --- a/lua/cmp/utils/str.lua +++ b/lua/cmp/utils/str.lua @@ -1,5 +1,4 @@ local char = require('cmp.utils.char') -local pattern = require('cmp.utils.pattern') local str = {} @@ -73,23 +72,6 @@ str.remove_suffix = function(text, suffix) return string.sub(text, 1, -#suffix - 1) end ----strikethrough ----@param text string ----@return string -str.strikethrough = function(text) - local r = pattern.regex('.') - local buffer = '' - while text ~= '' do - local s, e = r:match_str(text) - if not s then - break - end - buffer = buffer .. string.sub(text, s, e) .. '̶' - text = string.sub(text, e + 1) - end - return buffer -end - ---trim ---@param text string ---@return string diff --git a/lua/cmp/utils/str_spec.lua b/lua/cmp/utils/str_spec.lua index 541414f..1a21855 100644 --- a/lua/cmp/utils/str_spec.lua +++ b/lua/cmp/utils/str_spec.lua @@ -12,10 +12,6 @@ describe('utils.str', function() assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps') end) - it('strikethrough', function() - assert.are.equal(str.strikethrough('あいうえお'), 'あ̶い̶う̶え̶お̶') - end) - it('remove_suffix', function() assert.are.equal(str.remove_suffix('log()', '$0'), 'log()') assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()') diff --git a/lua/cmp/utils/window.lua b/lua/cmp/utils/window.lua index 8e2bf02..a8a271e 100644 --- a/lua/cmp/utils/window.lua +++ b/lua/cmp/utils/window.lua @@ -9,13 +9,14 @@ local api = require('cmp.utils.api') ---@field public col number ---@field public width number ---@field public height number +---@field public border string|string[]|nil ---@field public zindex number|nil ---@class cmp.Window ---@field public name string ---@field public win number|nil ----@field public swin1 number|nil ----@field public swin2 number|nil +---@field public thumb_win number|nil +---@field public sbar_win number|nil ---@field public style cmp.WindowStyle ---@field public opt table ---@field public buffer_opt table @@ -28,8 +29,8 @@ window.new = function() local self = setmetatable({}, { __index = window }) self.name = misc.id('cmp.utils.window.new') self.win = nil - self.swin1 = nil - self.swin2 = nil + self.sbar_win = nil + self.thumb_win = nil self.style = {} self.cache = cache.new() self.opt = {} @@ -79,13 +80,13 @@ end ---Set style. ---@param style cmp.WindowStyle window.set_style = function(self, style) - if vim.o.columns and vim.o.columns <= style.col + style.width then - style.width = vim.o.columns - style.col - 1 - end - if vim.o.lines and vim.o.lines <= style.row + style.height then - style.height = vim.o.lines - style.row - 1 - end self.style = style + local info = self:info() + + if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then + self.style.height = vim.o.lines - info.row - info.border_info.vert - 1 + end + self.style.zindex = self.style.zindex or 1 end @@ -127,49 +128,57 @@ end ---Update window.update = function(self) - if self:has_scrollbar() then - local total = self:get_content_height() - local info = self:info() - local bar_height = math.ceil(info.height * (info.height / total)) - local bar_offset = math.min(info.height - bar_height, math.floor(info.height * (vim.fn.getwininfo(self.win)[1].topline / total))) - local style1 = {} - style1.relative = 'editor' - style1.style = 'minimal' - style1.width = 1 - style1.height = info.height - style1.row = info.row - style1.col = info.col + info.width - (info.has_scrollbar and 1 or 0) - style1.zindex = (self.style.zindex and (self.style.zindex + 1) or 1) - if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then - vim.api.nvim_win_set_config(self.swin1, style1) - else - style1.noautocmd = true - self.swin1 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf1'), false, style1) - vim.api.nvim_win_set_option(self.swin1, 'winhighlight', 'EndOfBuffer:PmenuSbar,Normal:PmenuSbar,NormalNC:PmenuSbar,NormalFloat:PmenuSbar') + local info = self:info() + if info.scrollable then + -- Draw the background of the scrollbar + + if not info.border_info.visible then + local style = { + relative = 'editor', + style = 'minimal', + width = 1, + height = self.style.height, + row = info.row, + col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset. + zindex = (self.style.zindex and (self.style.zindex + 1) or 1), + } + if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then + vim.api.nvim_win_set_config(self.sbar_win, style) + else + style.noautocmd = true + self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style) + vim.api.nvim_win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar') + end end - local style2 = {} - style2.relative = 'editor' - style2.style = 'minimal' - style2.width = 1 - style2.height = bar_height - style2.row = info.row + bar_offset - style2.col = info.col + info.width - (info.has_scrollbar and 1 or 0) - style2.zindex = (self.style.zindex and (self.style.zindex + 2) or 2) - if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then - vim.api.nvim_win_set_config(self.swin2, style2) + + -- Draw the scrollbar thumb + local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height()) + 0.5) + local thumb_offset = math.floor(info.inner_height * (vim.fn.getwininfo(self.win)[1].topline / self:get_content_height())) + + local style = { + relative = 'editor', + style = 'minimal', + width = 1, + height = math.max(1, thumb_height), + row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0), + col = info.col + info.width - 1, -- info.col was already added scrollbar offset. + zindex = (self.style.zindex and (self.style.zindex + 2) or 2), + } + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_set_config(self.thumb_win, style) else - style2.noautocmd = true - self.swin2 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf2'), false, style2) - vim.api.nvim_win_set_option(self.swin2, 'winhighlight', 'EndOfBuffer:PmenuThumb,Normal:PmenuThumb,NormalNC:PmenuThumb,NormalFloat:PmenuThumb') + style.noautocmd = true + self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style) + vim.api.nvim_win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb') end else - if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then - vim.api.nvim_win_hide(self.swin1) - self.swin1 = nil + if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then + vim.api.nvim_win_hide(self.sbar_win) + self.sbar_win = nil end - if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then - vim.api.nvim_win_hide(self.swin2) - self.swin2 = nil + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_hide(self.thumb_win) + self.thumb_win = nil end end @@ -188,13 +197,13 @@ window.close = function(self) vim.api.nvim_win_hide(self.win) self.win = nil end - if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then - vim.api.nvim_win_hide(self.swin1) - self.swin1 = nil + if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then + vim.api.nvim_win_hide(self.sbar_win) + self.sbar_win = nil end - if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then - vim.api.nvim_win_hide(self.swin2) - self.swin2 = nil + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_hide(self.thumb_win) + self.thumb_win = nil end end end @@ -204,91 +213,101 @@ window.visible = function(self) return self.win and vim.api.nvim_win_is_valid(self.win) end ----Return the scrollbar will shown or not. -window.has_scrollbar = function(self) - return (self.style.height or 0) < self:get_content_height() -end - ---Return win info. window.info = function(self) - local border_width = self:get_border_width() - local has_scrollbar = self:has_scrollbar() - return { + local border_info = self:get_border_info() + local info = { row = self.style.row, col = self.style.col, - width = self.style.width + border_width + (has_scrollbar and 1 or 0), - height = self.style.height, - border_width = border_width, - has_scrollbar = has_scrollbar, + width = self.style.width + border_info.left + border_info.right, + height = self.style.height + border_info.top + border_info.bottom, + inner_width = self.style.width, + inner_height = self.style.height, + border_info = border_info, + scrollable = false, + scrollbar_offset = 0, } + + if self:get_content_height() > info.inner_height then + info.scrollable = true + if not border_info.visible then + info.scrollbar_offset = 1 + info.width = info.width + 1 + end + end + + return info end ----Get border width ----@return number -window.get_border_width = function(self) +---Return border information. +---@return { top: number, left: number, right: number, bottom: number, vert: number, horiz: number, visible: boolean } +window.get_border_info = function(self) local border = self.style.border - if type(border) == 'table' then - local new_border = {} - while #new_border < 8 do - for _, b in ipairs(border) do - table.insert(new_border, b) - end + if not border or border == 'none' then + return { + top = 0, + left = 0, + right = 0, + bottom = 0, + vert = 0, + horiz = 0, + visible = false, + } + end + if type(border) == 'string' then + if border == 'shadow' then + return { + top = 0, + left = 0, + right = 1, + bottom = 1, + vert = 1, + horiz = 1, + visible = false, + } end - border = new_border + return { + top = 1, + left = 1, + right = 1, + bottom = 1, + vert = 2, + horiz = 2, + visible = true, + } end - local w = 0 - if border then - if type(border) == 'string' then - if border == 'single' then - w = 2 - elseif border == 'solid' then - w = 2 - elseif border == 'double' then - w = 2 - elseif border == 'rounded' then - w = 2 - elseif border == 'shadow' then - w = 1 - end - elseif type(border) == 'table' then - local b4 = type(border[4]) == 'table' and border[4][1] or border[4] - if #b4 > 0 then - w = w + 1 - end - local b8 = type(border[8]) == 'table' and border[8][1] or border[8] - if #b8 > 0 then - w = w + 1 - end + local new_border = {} + while #new_border <= 8 do + for _, b in ipairs(border) do + table.insert(new_border, type(b) == 'string' and b or b[1]) end end - return w + local info = {} + info.top = new_border[2] == '' and 0 or 1 + info.right = new_border[4] == '' and 0 or 1 + info.bottom = new_border[6] == '' and 0 or 1 + info.left = new_border[8] == '' and 0 or 1 + info.vert = info.top + info.bottom + info.horiz = info.left + info.right + info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8])) + return info end ---Get scroll height. +---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view). ---@return number window.get_content_height = function(self) if not self:option('wrap') then return vim.api.nvim_buf_line_count(self:get_buffer()) end - - return self.cache:ensure({ - 'get_content_height', - self.style.width, - self:get_buffer(), - vim.api.nvim_buf_get_changedtick(self:get_buffer()), - }, function() - local height = 0 - local buf = self:get_buffer() - -- The result of vim.fn.strdisplaywidth depends on the buffer it was called - -- in (see comment in cmp.Entry.get_view). - vim.api.nvim_buf_call(buf, function() - for _, text in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do - height = height + math.ceil(math.max(1, vim.fn.strdisplaywidth(text)) / self.style.width) - end - end) - return height + local height = 0 + vim.api.nvim_buf_call(self:get_buffer(), function() + for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do + height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width)) + end end) + return height end return window diff --git a/lua/cmp/view/custom_entries_view.lua b/lua/cmp/view/custom_entries_view.lua index 0004480..8020a9b 100644 --- a/lua/cmp/view/custom_entries_view.lua +++ b/lua/cmp/view/custom_entries_view.lua @@ -25,6 +25,7 @@ custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_ 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') @@ -32,7 +33,6 @@ custom_entries_view.new = function() 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') -- This is done so that strdisplaywidth calculations for lines in the -- custom_entries_view window exactly match with what is really displayed, -- see comment in cmp.Entry.get_view. Setting tabstop to 1 makes all tabs be @@ -118,13 +118,11 @@ custom_entries_view.is_direction_top_down = function(self) end custom_entries_view.open = function(self, offset, entries) + local completion = config.get().window.completion self.offset = offset self.entries = {} self.column_width = { abbr = 0, kind = 0, menu = 0 } - -- 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 lines = {} local dedup = {} @@ -159,16 +157,24 @@ custom_entries_view.open = function(self, offset, entries) local pos = api.get_screen_cursor() local cursor = api.get_cursor() local delta = cursor[2] + 1 - self.offset - local has_bottom_space = (vim.o.lines - pos[1]) >= DEFAULT_HEIGHT local row, col = pos[1], pos[2] - delta - 1 - if not has_bottom_space and math.floor(vim.o.lines * 0.5) <= row and vim.o.lines - row <= height then + local border_info = window.get_border_info({ style = completion }) + local border_offset_row = border_info.top + border_info.bottom + local border_offset_col = border_info.left + border_info.right + if math.floor(vim.o.lines * 0.5) <= row + border_offset_row and vim.o.lines - row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) then height = math.min(height, row - 1) - row = row - height - 1 + row = row - height - border_offset_row - 1 + if row < 0 then + height = height + row + end end - if math.floor(vim.o.columns * 0.5) <= col and vim.o.columns - col <= width then + if math.floor(vim.o.columns * 0.5) <= col + border_offset_col and vim.o.columns - col - border_offset_col <= width then width = math.min(width, vim.o.columns - 1) - col = vim.o.columns - width - 1 + col = vim.o.columns - width - border_offset_col - 1 + if col < 0 then + width = width + col + end end if pos[1] > row then @@ -187,6 +193,9 @@ custom_entries_view.open = function(self, offset, entries) end end + -- Apply window options (that might be changed) on the custom completion menu. + self.entries_win:option('winblend', vim.o.pumblend) + self.entries_win:option('winhighlight', completion.winhighlight) self.entries_win:open({ relative = 'editor', style = 'minimal', @@ -194,7 +203,8 @@ custom_entries_view.open = function(self, offset, entries) col = math.max(0, col), width = width, height = height, - zindex = 1001, + border = completion.border, + zindex = completion.zindex or 1001, }) -- always set cursor when starting. It will be adjusted on the call to _select vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 }) diff --git a/lua/cmp/view/docs_view.lua b/lua/cmp/view/docs_view.lua index 5a9acb4..9d2cd3f 100644 --- a/lua/cmp/view/docs_view.lua +++ b/lua/cmp/view/docs_view.lua @@ -23,7 +23,7 @@ end ---@param e cmp.Entry ---@param view cmp.WindowStyle docs_view.open = function(self, e, view) - local documentation = config.get().documentation + local documentation = config.get().window.documentation if not documentation then return end @@ -32,11 +32,12 @@ docs_view.open = function(self, e, view) 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) - 1) + local border_info = window.get_border_info({ style = documentation }) + local right_space = vim.o.columns - (view.col + view.width) - 1 + local left_space = view.col - 1 + local max_width = math.min(documentation.max_width, math.max(left_space, right_space)) - -- update buffer content if needed. + -- 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 @@ -46,24 +47,26 @@ docs_view.open = function(self, e, view) self.entry = e vim.api.nvim_buf_call(self.window:get_buffer(), function() vim.cmd([[syntax clear]]) + vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {}) end) vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, { - max_width = maxwidth, - max_height = documentation.maxheight, + max_width = max_width, + max_height = documentation.max_height, }) end + -- Calculate window size. local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), { - max_width = maxwidth, - max_height = documentation.maxheight, + max_width = max_width - border_info.horiz, + max_height = documentation.max_height - border_info.vert, }) if width <= 0 or height <= 0 then return self:close() end + -- Calculate window position. local right_col = view.col + view.width - local left_col = view.col - width - 2 - + local left_col = view.col - width - border_info.horiz local col, left if right_space >= width and left_space >= width then if right_space < left_space then @@ -81,8 +84,10 @@ docs_view.open = function(self, e, view) return self:close() end + -- Render window. + self.window:option('winblend', vim.o.pumblend) self.window:option('winhighlight', documentation.winhighlight) - self.window:set_style({ + local style = { relative = 'editor', style = 'minimal', width = width, @@ -91,11 +96,14 @@ docs_view.open = function(self, e, view) col = col, border = documentation.border, zindex = documentation.zindex or 50, - }) - if left and self.window:has_scrollbar() then - self.window.style.col = self.window.style.col - 1 + } + self.window:open(style) + + -- Correct left-col for scrollbar existence. + if left then + style.col = style.col - self.window:info().scrollbar_offset + self.window:open(style) end - self.window:open() end ---Close floating window diff --git a/lua/cmp/view/native_entries_view.lua b/lua/cmp/view/native_entries_view.lua index d1dc8de..d8b0b1b 100644 --- a/lua/cmp/view/native_entries_view.lua +++ b/lua/cmp/view/native_entries_view.lua @@ -101,7 +101,7 @@ 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), + width = info.width + (info.scrollable and 1 or 0), height = info.height, row = info.row, col = info.col, diff --git a/plugin/cmp.lua b/plugin/cmp.lua index a977a78..6c0809f 100644 --- a/plugin/cmp.lua +++ b/plugin/cmp.lua @@ -80,37 +80,27 @@ misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function() guibg = 'NONE', ctermbg = 'NONE', }) + highlight.inherit('CmpItemMenuDefault', 'Pmenu', { + guibg = 'NONE', + ctermbg = 'NONE', + }) for name in pairs(types.lsp.CompletionItemKind) do if type(name) == 'string' then vim.cmd(([[highlight default link CmpItemKind%sDefault CmpItemKind]]):format(name)) end end - highlight.inherit('CmpItemMenuDefault', 'Pmenu', { - guibg = 'NONE', - ctermbg = 'NONE', - }) end) _G.cmp.plugin.colorscheme() -if vim.fn.hlexists('CmpItemAbbr') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbr CmpItemAbbrDefault]] -end +vim.cmd [[ + highlight default link CmpItemAbbr CmpItemAbbrDefault + highlight default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault + highlight default link CmpItemAbbrMatch CmpItemAbbrMatchDefault + highlight default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault + highlight default link CmpItemKind CmpItemKindDefault + highlight default link CmpItemMenu CmpItemMenuDefault +]] -if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]] -end - -if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]] -end - -if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]] -end - -if vim.fn.hlexists('CmpItemKind') ~= 1 then - vim.cmd [[highlight default link CmpItemKind CmpItemKindDefault]] -end for name in pairs(types.lsp.CompletionItemKind) do if type(name) == 'string' then local hi = ('CmpItemKind%s'):format(name) @@ -120,14 +110,6 @@ for name in pairs(types.lsp.CompletionItemKind) do end end -if vim.fn.hlexists('CmpItemMenu') ~= 1 then - vim.cmd [[highlight default link CmpItemMenu CmpItemMenuDefault]] -end - -vim.cmd [[command! CmpStatus lua require('cmp').status()]] - -vim.cmd [[doautocmd User CmpReady]] - if vim.on_key then vim.on_key(function(keys) if keys == vim.api.nvim_replace_termcodes('', true, true, true) then @@ -140,3 +122,7 @@ if vim.on_key then end, vim.api.nvim_create_namespace('cmp.plugin')) end +vim.cmd [[command! CmpStatus lua require('cmp').status()]] + +vim.cmd [[doautocmd User CmpReady]] +