Skip to content

Commit

Permalink
feat: allow providers customize documentation rendering (#650)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
mikavilpas and Saghen authored Dec 19, 2024
1 parent 96e279a commit bc94c75
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 27 deletions.
29 changes: 22 additions & 7 deletions lua/blink/cmp/completion/windows/documentation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,35 @@ 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()
return
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

Expand Down
2 changes: 1 addition & 1 deletion lua/blink/cmp/config/completion/list.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion lua/blink/cmp/config/keymap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
--- {
Expand Down
47 changes: 29 additions & 18 deletions lua/blink/cmp/lib/window/docs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions lua/blink/cmp/types.lua
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit bc94c75

Please sign in to comment.