--- @class blink.cmp.CompletionDocumentationWindow --- @field win blink.cmp.Window --- @field last_context_id? number --- @field auto_show_timer uv_timer_t --- @field shown_item? blink.cmp.CompletionItem --- --- @field auto_show_item fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem) --- @field show_item fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem) --- @field update_position fun() --- @field scroll_up fun(amount: number) --- @field scroll_down fun(amount: number) --- @field close fun() local config = require('blink.cmp.config').completion.documentation local win_config = config.window local sources = require('blink.cmp.sources.lib') local menu = require('blink.cmp.completion.windows.menu') --- @type blink.cmp.CompletionDocumentationWindow --- @diagnostic disable-next-line: missing-fields local docs = { win = require('blink.cmp.lib.window').new({ min_width = win_config.min_width, max_width = win_config.max_width, max_height = win_config.max_height, border = win_config.border, winblend = win_config.winblend, winhighlight = win_config.winhighlight, scrollbar = win_config.scrollbar, wrap = true, filetype = 'blink-cmp-documentation', scrolloff = 0, }), last_context_id = nil, auto_show_timer = vim.uv.new_timer(), } menu.position_update_emitter:on(function() docs.update_position() end) menu.close_emitter:on(function() docs.close() end) function docs.auto_show_item(context, item) docs.auto_show_timer:stop() if docs.win:is_open() then docs.auto_show_timer:start(config.update_delay_ms, 0, function() vim.schedule(function() docs.show_item(context, item) end) end) elseif config.auto_show then docs.auto_show_timer:start(config.auto_show_delay_ms, 0, function() vim.schedule(function() docs.show_item(context, item) end) end) end end function docs.show_item(context, item) docs.auto_show_timer:stop() if item == nil or not menu.win:is_open() then return docs.win:close() end -- TODO: cancellation -- TODO: only resolve if documentation does not exist sources .resolve(context, item) ---@param item blink.cmp.CompletionItem :map(function(item) if item.documentation == nil and item.detail == nil then docs.close() return end if docs.shown_item ~= item then --- @type blink.cmp.RenderDetailAndDocumentationOpts local default_render_opts = { bufnr = docs.win:get_buf(), detail = item.detail, documentation = item.documentation, max_width = docs.win.config.max_width, use_treesitter_highlighting = config and config.treesitter_highlighting, } local render = require('blink.cmp.lib.window.docs').render_detail_and_documentation if item.documentation and item.documentation.render ~= nil then -- let the provider render the documentation and optionally override -- the default rendering item.documentation.render({ item = item, window = docs.win, default_implementation = function(opts) render(vim.tbl_extend('force', default_render_opts, opts)) end, }) else render(default_render_opts) end end docs.shown_item = item if menu.win:get_win() then docs.win:open() docs.win:set_cursor({ 1, 0 }) -- reset scroll docs.update_position() end end) :catch(function(err) vim.notify(err, vim.log.levels.ERROR, { title = 'blink.cmp' }) end) end -- TODO: compensate for wrapped lines function docs.scroll_up(amount) local winnr = docs.win:get_win() if winnr == nil then return end local top_line = math.max(1, vim.fn.line('w0', winnr)) local desired_line = math.max(1, top_line - amount) docs.win:set_cursor({ desired_line, 0 }) end -- TODO: compensate for wrapped lines function docs.scroll_down(amount) local winnr = docs.win:get_win() if winnr == nil then return end local line_count = vim.api.nvim_buf_line_count(docs.win:get_buf()) local bottom_line = math.max(1, vim.fn.line('w$', winnr)) local desired_line = math.min(line_count, bottom_line + amount) docs.win:set_cursor({ desired_line, 0 }) end function docs.update_position() if not docs.win:is_open() or not menu.win:is_open() then return end docs.win:update_size() local menu_winnr = menu.win:get_win() if not menu_winnr then return end local menu_win_config = vim.api.nvim_win_get_config(menu_winnr) local menu_win_height = menu.win:get_height() local menu_border_size = menu.win:get_border_size() local cursor_win_row = vim.fn.winline() -- decide direction priority based on the menu window's position local menu_win_is_up = menu_win_config.row - cursor_win_row < 0 local direction_priority = menu_win_is_up and win_config.direction_priority.menu_north or win_config.direction_priority.menu_south -- remove the direction priority of the signature window if it's open local signature = require('blink.cmp.signature.window') if signature.win and signature.win:is_open() then direction_priority = vim.tbl_filter( function(dir) return dir ~= (menu_win_is_up and 's' or 'n') end, direction_priority ) end -- decide direction, width and height of window local win_width = docs.win:get_width() local win_height = docs.win:get_height() local pos = docs.win:get_direction_with_window_constraints(menu.win, direction_priority, { width = math.min(win_width, win_config.desired_min_width), height = math.min(win_height, win_config.desired_min_height), }) -- couldn't find anywhere to place the window if not pos then docs.win:close() return end -- set width and height based on available space docs.win:set_height(pos.height) docs.win:set_width(pos.width) -- set position based on provided direction local height = docs.win:get_height() local width = docs.win:get_width() local function set_config(opts) docs.win:set_win_config({ relative = 'win', win = menu_winnr, row = opts.row, col = opts.col }) end if pos.direction == 'n' then if menu_win_is_up then set_config({ row = -height - menu_border_size.top, col = -menu_border_size.left }) else set_config({ row = -1 - height - menu_border_size.top, col = -menu_border_size.left }) end elseif pos.direction == 's' then if menu_win_is_up then set_config({ row = 1 + menu_win_height - menu_border_size.top, col = -menu_border_size.left, }) else set_config({ row = menu_win_height - menu_border_size.top, col = -menu_border_size.left, }) end elseif pos.direction == 'e' then if menu_win_is_up and menu_win_height < height then set_config({ row = menu_win_height - menu_border_size.top - height, col = menu_win_config.width + menu_border_size.right, }) else set_config({ row = -menu_border_size.top, col = menu_win_config.width + menu_border_size.right, }) end elseif pos.direction == 'w' then if menu_win_is_up and menu_win_height < height then set_config({ row = menu_win_height - menu_border_size.top - height, col = -width - menu_border_size.left, }) else set_config({ row = -menu_border_size.top, col = -width - menu_border_size.left }) end end end function docs.close() docs.win:close() docs.auto_show_timer:stop() docs.shown_item = nil end return docs