blink.cmp icon indicating copy to clipboard operation
blink.cmp copied to clipboard

feat: Allow callbacks for the documentation window

Open OXY2DEV opened this issue 1 year ago • 11 comments

Feature Description

It would be a nice feature if we could have callbacks to run for the documentation window.

Why?

  1. I find the usage of min/max width & height quite problematic as I prefer to have them as % of the window size. So, a way to change them on the fly would be nice.

  2. It would be great for extending the plugin.

Image Using a markdown previewer inside the LSP hover window.

OXY2DEV avatar Jan 29 '25 18:01 OXY2DEV

Whoops, should've marked it as breaking. @mikavilpas the documentation.render function has been renamed to documentation.draw but I've maintained support for .render so users can upgrade your plugin without running into issues. You'll likely want to wait for a new release to go out before migrating

Added support for custom drawing of the documentation window. The min/max width seems like we could support the typical way, with a function: completion.documentation.min_width = function() return vim.o.columns / 123 end, but I haven't gotten around to that yet

saghen avatar Feb 01 '25 22:02 saghen

I have finally managed to get it to work.

Image

You can find the source code here.

[!NOTE] You have to use defer_fn or timers, if you need to do anything with the window as it's not available during draw

OXY2DEV avatar May 08 '25 02:05 OXY2DEV

Re-opening because I'd still like to enable dynamic min/max widths: https://github.com/Saghen/blink.cmp/issues/1113#issuecomment-2629139275

saghen avatar May 08 '25 16:05 saghen

@Saghen, Shouldn't a simple eval function be enough for this?

---@param val table
---@param ignore string[] Ignore these keys(e.g. some function that needs to be run on something instead of returning a value).
---@param ... any
---@return table
local function eval (val, ignore, ...)
  ignore = ignore or {};
  local new_val = {};

  for k, v in pairs(val) do
    if type(v) ~= "function" or vim.list_contains(ignore, k) then
      new_val[k] = v;
    else
      local can_eval, _v = pcall(v, ...);

      if can_eval and _v ~= nil then
        new_val[k] = _v;
      end
    end
  end

  return new_val;
end

OXY2DEV avatar May 08 '25 16:05 OXY2DEV

Unfortunately that would lose type checking, and something like this doesn't seem to work:

--- @generic T
--- @generic R
--- @param func T | fun(...: R): T
--- @param ... R
--- @return T
function utils.eval(func, ...)
  if type(func) == 'function' then
    return func(...)
  else
    return func
  end
end

---@param bar string
---@return string
local function foo(bar) return 'foo' end

local test = utils.eval(foo, 123) -- should be `string` but get `string|function`
local test2 = utils.eval('ast') -- should be and is `string`

saghen avatar May 08 '25 17:05 saghen

that would lose type checking

Can't we simply use 2 type definition, 1 exposed to the user and 1 used internally.

---@class config.window
---@param X string | fun(): string

---@class config.window__static
---@param X string

And then use a local function where the values need to be used,

---@param config config.window
---@return config.window__static
local function parse_winconf (config)
  return utils.eval(config);
end

This should solve conflicts with existing type definition.

OXY2DEV avatar May 08 '25 17:05 OXY2DEV

Also, I think it would be simpler to assume everything inside window can be a function, instead of handling specific options differently.

OXY2DEV avatar May 08 '25 17:05 OXY2DEV

I don't love having a fourth definition for all the configs with dynamic properties (we already have types_partial.lua and reference.md). If we can't figure out type inference for it, I think I'd rather do something like below so that we compute the value at the last possible moment:

local min = utils.eval(config.min, 'some', 'args')
--- @cast min number

This is essentially what we already do, but we don't have a helper for it

saghen avatar May 08 '25 17:05 saghen

Sure. I would suggest not to do any direct function call for getting the value.

nvim-cmp does this and there were plenty of time when I got soft locked out of my editor since it kept calling the same function and creating new error messages. I would prefer if that isn't the case with blink.


Also, I would suggest making use of autocmds(specifically :h User) as in my opinion it's much easier to set up and allows users to run their own functions on specific events without you needing to manually handle them.

You simply call nvim_exec_autocmds() and everything else should be handled by Neovim.

OXY2DEV avatar May 08 '25 18:05 OXY2DEV

Which autocmds are missing as is? I ought to document them, but you can find them scattered in the codebase (second arg to event_emitter.new()): https://github.com/search?q=repo%3ASaghen%2Fblink.cmp%20event_emitter&type=code

there were plenty of time when I got soft locked out of my editor since it kept calling the same function and creating new error messages.

Sure, the utils.eval could pcall it and then only show the error once, defaulting to the default config value on failure

saghen avatar May 08 '25 18:05 saghen

but you can find them scattered in the codebase

It appears that I didn't check throughly enough. Oh well, I guess you can just ignore my previous comment.

OXY2DEV avatar May 08 '25 18:05 OXY2DEV