From bc94c7508379b4828206759562162ce10af82b68 Mon Sep 17 00:00:00 2001 From: Mika Vilpas Date: Thu, 19 Dec 2024 16:26:54 +0200 Subject: [PATCH] feat: allow providers customize documentation rendering (#650) * style: fix typos * feat: allow providers customize documentation rendering Issue ===== The default way of rendering documentation using markdown and treesitter works great for most cases, but some providers may want to customize the rendering of the documentation. However, some providers may want to customize the rendering in order to add additional information or custom highlighting. Solution ======== Add a new field `render_documentation_fn` to the `CompletionItem` type. Providers can set this to a function that will be called when the documentation is about to be rendered. They can then choose to do custom rendering or fallback to the default rendering. Use cases: - when a treesitter parser is not available for code examples, providers can display colored text using Neovim's regex based syntax highlighting - can add additional highlighting to the documentation - avoid showing extra empty lines caused by markdown fenced code blocks * feat: use `item.documentation.render`, allow overriding default rendering --------- Co-authored-by: Liam Dyer --- .../cmp/completion/windows/documentation.lua | 29 +++++++++--- lua/blink/cmp/config/completion/list.lua | 2 +- lua/blink/cmp/config/keymap.lua | 2 +- lua/blink/cmp/lib/window/docs.lua | 47 ++++++++++++------- lua/blink/cmp/types.lua | 8 ++++ 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/lua/blink/cmp/completion/windows/documentation.lua b/lua/blink/cmp/completion/windows/documentation.lua index fd4014eb..4d704b3b 100644 --- a/lua/blink/cmp/completion/windows/documentation.lua +++ b/lua/blink/cmp/completion/windows/documentation.lua @@ -62,6 +62,7 @@ function docs.show_item(context, item) -- 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.win:close() @@ -69,13 +70,27 @@ function docs.show_item(context, item) end if docs.shown_item ~= item then - require('blink.cmp.lib.window.docs').render_detail_and_documentation( - docs.win:get_buf(), - item.detail, - item.documentation, - docs.win.config.max_width, - config.treesitter_highlighting - ) + --- @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 diff --git a/lua/blink/cmp/config/completion/list.lua b/lua/blink/cmp/config/completion/list.lua index 31fd1d44..ab2e25a4 100644 --- a/lua/blink/cmp/config/completion/list.lua +++ b/lua/blink/cmp/config/completion/list.lua @@ -6,7 +6,7 @@ --- @alias blink.cmp.CompletionListSelection --- | 'preselect' Select the first item in the completion list --- | 'manual' Don't select any item by default ---- | 'auto_insert' Don't select any item by default, and insert the completion items automatically when selecting them. You may want to bind a key to the `cancel` command when using this option, which will undo the selection and hide the completiom menu +--- | 'auto_insert' Don't select any item by default, and insert the completion items automatically when selecting them. You may want to bind a key to the `cancel` command when using this option, which will undo the selection and hide the completion menu --- @class (exact) blink.cmp.CompletionListCycleConfig --- @field from_bottom boolean When `true`, calling `select_next` at the *bottom* of the completion list will select the *first* completion item. diff --git a/lua/blink/cmp/config/keymap.lua b/lua/blink/cmp/config/keymap.lua index 6cadf7d1..a7984596 100644 --- a/lua/blink/cmp/config/keymap.lua +++ b/lua/blink/cmp/config/keymap.lua @@ -35,7 +35,7 @@ --- } --- ``` --- | 'default' ---- Mappings simliar to VSCode. +--- Mappings similar to VSCode. --- You may want to set `completion.trigger.show_in_snippet = false` or use `completion.list.selection = "manual" | "auto_insert"` when using this mapping: --- ```lua --- { diff --git a/lua/blink/cmp/lib/window/docs.lua b/lua/blink/cmp/lib/window/docs.lua index 44001130..dc8d38c6 100644 --- a/lua/blink/cmp/lib/window/docs.lua +++ b/lua/blink/cmp/lib/window/docs.lua @@ -2,23 +2,34 @@ local highlight_ns = require('blink.cmp.config').appearance.highlight_ns local docs = {} ---- @param bufnr number ---- @param detail? string ---- @param documentation? lsp.MarkupContent | string ---- @param max_width number ---- @param use_treesitter_highlighting boolean -function docs.render_detail_and_documentation(bufnr, detail, documentation, max_width, use_treesitter_highlighting) +--- @class blink.cmp.RenderDetailAndDocumentationOpts +--- @field bufnr number +--- @field detail? string +--- @field documentation? lsp.MarkupContent | string +--- @field max_width number +--- @field use_treesitter_highlighting boolean? + +--- @class blink.cmp.RenderDetailAndDocumentationOptsPartial +--- @field bufnr? number +--- @field detail? string +--- @field documentation? lsp.MarkupContent | string +--- @field max_width? number +--- @field use_treesitter_highlighting boolean? + +--- @param opts blink.cmp.RenderDetailAndDocumentationOpts +function docs.render_detail_and_documentation(opts) local detail_lines = {} - if detail and detail ~= '' then detail_lines = docs.split_lines(detail) end + if opts.detail and opts.detail ~= '' then detail_lines = docs.split_lines(opts.detail) end local doc_lines = {} - if documentation ~= nil then - local doc = type(documentation) == 'string' and documentation or documentation.value + if opts.documentation ~= nil then + local doc = type(opts.documentation) == 'string' and opts.documentation or opts.documentation.value doc_lines = docs.split_lines(doc) end detail_lines, doc_lines = docs.extract_detail_from_doc(detail_lines, doc_lines) + ---@type string[] local combined_lines = vim.list_extend({}, detail_lines) -- add a blank line for the --- separator @@ -27,27 +38,27 @@ function docs.render_detail_and_documentation(bufnr, detail, documentation, max_ -- skip original separator in doc_lines, so we can highlight it later vim.list_extend(combined_lines, doc_lines, doc_already_has_separator and 2 or 1) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, combined_lines) - vim.api.nvim_set_option_value('modified', false, { buf = bufnr }) + vim.api.nvim_buf_set_lines(opts.bufnr, 0, -1, true, combined_lines) + vim.api.nvim_set_option_value('modified', false, { buf = opts.bufnr }) -- Highlight with treesitter - vim.api.nvim_buf_clear_namespace(bufnr, highlight_ns, 0, -1) + vim.api.nvim_buf_clear_namespace(opts.bufnr, highlight_ns, 0, -1) - if #detail_lines > 0 and use_treesitter_highlighting then - docs.highlight_with_treesitter(bufnr, vim.bo.filetype, 0, #detail_lines) + if #detail_lines > 0 and opts.use_treesitter_highlighting then + docs.highlight_with_treesitter(opts.bufnr, vim.bo.filetype, 0, #detail_lines) end -- Only add the separator if there are documentation lines (otherwise only display the detail) if #detail_lines > 0 and #doc_lines > 0 then - vim.api.nvim_buf_set_extmark(bufnr, highlight_ns, #detail_lines, 0, { - virt_text = { { string.rep('─', max_width), 'BlinkCmpDocSeparator' } }, + vim.api.nvim_buf_set_extmark(opts.bufnr, highlight_ns, #detail_lines, 0, { + virt_text = { { string.rep('─', opts.max_width), 'BlinkCmpDocSeparator' } }, virt_text_pos = 'overlay', }) end - if #doc_lines > 0 and use_treesitter_highlighting then + if #doc_lines > 0 and opts.use_treesitter_highlighting then local start = #detail_lines + (#detail_lines > 0 and 1 or 0) - docs.highlight_with_treesitter(bufnr, 'markdown', start, start + #doc_lines) + docs.highlight_with_treesitter(opts.bufnr, 'markdown', start, start + #doc_lines) end end diff --git a/lua/blink/cmp/types.lua b/lua/blink/cmp/types.lua index 8586c866..3904f9d5 100644 --- a/lua/blink/cmp/types.lua +++ b/lua/blink/cmp/types.lua @@ -1,12 +1,20 @@ --- @alias blink.cmp.Mode 'cmdline' | 'default' --- @class blink.cmp.CompletionItem : lsp.CompletionItem +--- @field documentation? string | { kind: lsp.MarkupKind, value: string, render?: blink.cmp.SourceRenderDocumentation } --- @field score_offset? number --- @field source_id string --- @field source_name string --- @field cursor_column number --- @field client_id? number +--- @class blink.cmp.SourceRenderDocumentationOpts +--- @field item blink.cmp.CompletionItem +--- @field window blink.cmp.Window +--- @field default_implementation fun(opts: blink.cmp.RenderDetailAndDocumentationOptsPartial) + +--- @alias blink.cmp.SourceRenderDocumentation fun(opts: blink.cmp.SourceRenderDocumentationOpts) + return { -- some plugins mutate the vim.lsp.protocol.CompletionItemKind table -- so we use our own copy