local validate = require('blink.cmp.config.utils').validate --- @class (exact) blink.cmp.CompletionMenuConfig --- @field enabled boolean --- @field min_width number --- @field max_height number --- @field border blink.cmp.WindowBorder --- @field winblend number --- @field winhighlight string --- @field scrolloff number Keep the cursor X lines away from the top/bottom of the window --- @field scrollbar boolean Note that the gutter will be disabled when border ~= 'none' --- @field direction_priority ("n" | "s")[] Which directions to show the window, falling back to the next direction when there's not enough space --- @field order blink.cmp.CompletionMenuOrderConfig TODO: implement --- @field auto_show boolean | fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean Whether to automatically show the window when new completion items are available --- @field cmdline_position fun(): number[] Screen coordinates (0-indexed) of the command line --- @field draw blink.cmp.Draw Controls how the completion items are rendered on the popup window --- @class (exact) blink.cmp.CompletionMenuOrderConfig --- @field n 'top_down' | 'bottom_up' --- @field s 'top_down' | 'bottom_up' local window = { --- @type blink.cmp.CompletionMenuConfig default = { enabled = true, min_width = 15, max_height = 10, border = 'none', winblend = 0, winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None', -- keep the cursor X lines away from the top/bottom of the window scrolloff = 2, -- note that the gutter will be disabled when border ~= 'none' scrollbar = true, -- which directions to show the window, -- falling back to the next direction when there's not enough space direction_priority = { 's', 'n' }, -- which direction previous/next items show up -- TODO: implement order = { n = 'bottom_up', s = 'top_down' }, -- Whether to automatically show the window when new completion items are available auto_show = true, -- Screen coordinates of the command line cmdline_position = function() if vim.g.ui_cmdline_pos ~= nil then local pos = vim.g.ui_cmdline_pos -- (1, 0)-indexed return { pos[1] - 1, pos[2] } end local height = (vim.o.cmdheight == 0) and 1 or vim.o.cmdheight return { vim.o.lines - height, 0 } end, -- Controls how the completion items are rendered on the popup window draw = { -- Aligns the keyword you've typed to a component in the menu align_to = 'label', -- or 'none' to disable -- Left and right padding, optionally { left, right } for different padding on each side padding = 1, -- Gap between columns gap = 1, treesitter = {}, -- Use treesitter to highlight the label text of completions from these sources -- Components to render, grouped by column columns = { { 'kind_icon' }, { 'label', 'label_description', gap = 1 } }, -- Definitions for possible components to render. Each component defines: -- ellipsis: whether to add an ellipsis when truncating the text -- width: control the min, max and fill behavior of the component -- text function: will be called for each item -- highlight function: will be called only when the line appears on screen components = { kind_icon = { ellipsis = false, text = function(ctx) return ctx.kind_icon .. ctx.icon_gap end, highlight = function(ctx) return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or ('BlinkCmpKind' .. ctx.kind) end, }, kind = { ellipsis = false, width = { fill = true }, text = function(ctx) return ctx.kind end, highlight = function(ctx) return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or ('BlinkCmpKind' .. ctx.kind) end, }, label = { width = { fill = true, max = 60 }, text = function(ctx) return ctx.label .. ctx.label_detail end, highlight = function(ctx) -- label and label details local label = ctx.label local highlights = { { 0, #label, group = ctx.deprecated and 'BlinkCmpLabelDeprecated' or 'BlinkCmpLabel' }, } if ctx.label_detail then table.insert(highlights, { #label, #label + #ctx.label_detail, group = 'BlinkCmpLabelDetail' }) end if vim.list_contains(ctx.self.treesitter, ctx.source_id) then -- add treesitter highlights vim.list_extend(highlights, require('blink.cmp.completion.windows.render.treesitter').highlight(ctx)) end -- characters matched on the label by the fuzzy matcher for _, idx in ipairs(ctx.label_matched_indices) do table.insert(highlights, { idx, idx + 1, group = 'BlinkCmpLabelMatch' }) end return highlights end, }, label_description = { width = { max = 30 }, text = function(ctx) return ctx.label_description end, highlight = 'BlinkCmpLabelDescription', }, source_name = { width = { max = 30 }, -- source_name or source_id are supported text = function(ctx) return ctx.source_name end, highlight = 'BlinkCmpSource', }, }, }, }, } function window.validate(config) validate('completion.menu', { enabled = { config.enabled, 'boolean' }, min_width = { config.min_width, 'number' }, max_height = { config.max_height, 'number' }, border = { config.border, { 'string', 'table' } }, winblend = { config.winblend, 'number' }, winhighlight = { config.winhighlight, 'string' }, scrolloff = { config.scrolloff, 'number' }, scrollbar = { config.scrollbar, 'boolean' }, direction_priority = { config.direction_priority, function(direction_priority) for _, direction in ipairs(direction_priority) do if not vim.tbl_contains({ 'n', 's' }, direction) then return false end end return true end, 'one of: "n", "s"', }, order = { config.order, 'table' }, auto_show = { config.auto_show, { 'boolean', 'function' } }, cmdline_position = { config.cmdline_position, 'function' }, draw = { config.draw, 'table' }, }, config) validate('completion.menu.order', { n = { config.order.n, { 'string', 'nil' } }, s = { config.order.s, { 'string', 'nil' } }, }, config.order) validate('completion.menu.draw', { align_to = { config.draw.align_to, function(align) if align == 'none' or align == 'cursor' then return true end for _, column in ipairs(config.draw.columns) do for _, component in ipairs(column) do if component == align then return true end end end return false end, '"none" or one of the components defined in the "columns"', }, padding = { config.draw.padding, function(padding) if type(padding) == 'number' then return true end if type(padding) ~= 'table' or #padding ~= 2 then return false end if type(padding[1]) == 'number' and type(padding[2]) == 'number' then return true end return false end, 'a number or a tuple of 2 numbers (i.e. [1, 2])', }, gap = { config.draw.gap, 'number' }, treesitter = { config.draw.treesitter, 'table' }, columns = { config.draw.columns, function(columns) local available_components = vim.tbl_keys(config.draw.components) if type(columns) ~= 'table' or #columns == 0 then return false end for _, column in ipairs(columns) do if #column == 0 then return false end for _, component in ipairs(column) do if not vim.tbl_contains(available_components, component) then return false end end if column.gap ~= nil and type(column.gap) ~= 'number' then return false end end return true end, 'a table of tables, where each table contains a list of components and an optional gap. List of available components: ' .. table.concat(vim.tbl_keys(config.draw.components), ', '), }, components = { config.draw.components, 'table' }, }, config.draw) for component, definition in pairs(config.draw.components) do validate('completion.menu.draw.components.' .. component, { ellipsis = { definition.ellipsis, 'boolean', true }, width = { definition.width, 'table', true }, text = { definition.text, 'function' }, highlight = { definition.highlight, { 'string', 'function' }, true }, }, config.draw.components[component]) end end return window