diff options
| author | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:52 +0100 |
|---|---|---|
| committer | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:52 +0100 |
| commit | b77413ff8f59f380612074f0c9bd49093d8db695 (patch) | |
| tree | 32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /lua/blink/cmp/lib/window/scrollbar | |
Squashed 'mut/neovim/pack/plugins/start/blink.cmp/' content from commit 1cc3b1a
git-subtree-dir: mut/neovim/pack/plugins/start/blink.cmp
git-subtree-split: 1cc3b1a908fbcfd15451c4772759549724f38524
Diffstat (limited to 'lua/blink/cmp/lib/window/scrollbar')
| -rw-r--r-- | lua/blink/cmp/lib/window/scrollbar/geometry.lua | 92 | ||||
| -rw-r--r-- | lua/blink/cmp/lib/window/scrollbar/init.lua | 37 | ||||
| -rw-r--r-- | lua/blink/cmp/lib/window/scrollbar/win.lua | 107 |
3 files changed, 236 insertions, 0 deletions
diff --git a/lua/blink/cmp/lib/window/scrollbar/geometry.lua b/lua/blink/cmp/lib/window/scrollbar/geometry.lua new file mode 100644 index 0000000..ad481a0 --- /dev/null +++ b/lua/blink/cmp/lib/window/scrollbar/geometry.lua @@ -0,0 +1,92 @@ +--- Helper for calculating placement of the scrollbar thumb and gutter + +--- @class blink.cmp.ScrollbarGeometry +--- @field width number +--- @field height number +--- @field row number +--- @field col number +--- @field zindex number +--- @field relative string +--- @field win number + +local M = {} + +--- @param target_win number +--- @return number +local function get_win_buf_height(target_win) + local buf = vim.api.nvim_win_get_buf(target_win) + + -- not wrapping, so just get the line count + if not vim.wo[target_win].wrap then return vim.api.nvim_buf_line_count(buf) end + + local width = vim.api.nvim_win_get_width(target_win) + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local height = 0 + for _, l in ipairs(lines) do + height = height + math.max(1, (math.ceil(vim.fn.strwidth(l) / width))) + end + return height +end + +--- @param border string|string[] +--- @return number +local function get_col_offset(border) + -- we only need an extra offset when working with a padded window + if type(border) == 'table' and border[1] == ' ' and border[4] == ' ' and border[7] == ' ' and border[8] == ' ' then + return 1 + end + return 0 +end + +--- Gets the starting line, handling line wrapping if enabled +--- @param target_win number +--- @param width number +--- @return number +local get_content_start_line = function(target_win, width) + local start_line = math.max(1, vim.fn.line('w0', target_win)) + if not vim.wo[target_win].wrap then return start_line end + + local bufnr = vim.api.nvim_win_get_buf(target_win) + local wrapped_start_line = 1 + for _, text in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, start_line - 1, false)) do + -- nvim_buf_get_lines sometimes returns a blob. see hrsh7th/nvim-cmp#2050 + if vim.fn.type(text) == vim.v.t_blob then text = vim.fn.string(text) end + wrapped_start_line = wrapped_start_line + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / width)) + end + return wrapped_start_line +end + +--- @param target_win number +--- @return { should_hide: boolean, thumb: blink.cmp.ScrollbarGeometry, gutter: blink.cmp.ScrollbarGeometry } +function M.get_geometry(target_win) + local config = vim.api.nvim_win_get_config(target_win) + local width = config.width + local height = config.height + local zindex = config.zindex + + local buf_height = get_win_buf_height(target_win) + local thumb_height = math.max(1, math.floor(height * height / buf_height + 0.5) - 1) + + local start_line = get_content_start_line(target_win, width or 1) + + local pct = (start_line - 1) / (buf_height - height) + local thumb_offset = math.floor((pct * (height - thumb_height)) + 0.5) + thumb_height = thumb_offset + thumb_height > height and height - thumb_offset or thumb_height + thumb_height = math.max(1, thumb_height) + + local common_geometry = { + width = 1, + row = thumb_offset, + col = width + get_col_offset(config.border), + relative = 'win', + win = target_win, + } + + return { + should_hide = height >= buf_height, + thumb = vim.tbl_deep_extend('force', common_geometry, { height = thumb_height, zindex = zindex + 2 }), + gutter = vim.tbl_deep_extend('force', common_geometry, { row = 0, height = height, zindex = zindex + 1 }), + } +end + +return M diff --git a/lua/blink/cmp/lib/window/scrollbar/init.lua b/lua/blink/cmp/lib/window/scrollbar/init.lua new file mode 100644 index 0000000..c72615a --- /dev/null +++ b/lua/blink/cmp/lib/window/scrollbar/init.lua @@ -0,0 +1,37 @@ +-- TODO: move the set_config and set_height calls from the menu/documentation/signature files +-- to helpers in the window lib, and call scrollbar updates from there. This way, consumers of +-- the window lib don't need to worry about scrollbars + +--- @class blink.cmp.ScrollbarConfig +--- @field enable_gutter boolean + +--- @class blink.cmp.Scrollbar +--- @field win blink.cmp.ScrollbarWin +--- +--- @field new fun(opts: blink.cmp.ScrollbarConfig): blink.cmp.Scrollbar +--- @field is_visible fun(self: blink.cmp.Scrollbar): boolean +--- @field update fun(self: blink.cmp.Scrollbar, target_win: number | nil) + +--- @type blink.cmp.Scrollbar +--- @diagnostic disable-next-line: missing-fields +local scrollbar = {} + +function scrollbar.new(opts) + local self = setmetatable({}, { __index = scrollbar }) + self.win = require('blink.cmp.lib.window.scrollbar.win').new(opts) + return self +end + +function scrollbar:is_visible() return self.win:is_visible() end + +function scrollbar:update(target_win) + if target_win == nil or not vim.api.nvim_win_is_valid(target_win) then return self.win:hide() end + + local geometry = require('blink.cmp.lib.window.scrollbar.geometry').get_geometry(target_win) + if geometry.should_hide then return self.win:hide() end + + self.win:show_thumb(geometry.thumb) + self.win:show_gutter(geometry.gutter) +end + +return scrollbar diff --git a/lua/blink/cmp/lib/window/scrollbar/win.lua b/lua/blink/cmp/lib/window/scrollbar/win.lua new file mode 100644 index 0000000..9ac3193 --- /dev/null +++ b/lua/blink/cmp/lib/window/scrollbar/win.lua @@ -0,0 +1,107 @@ +--- Manages creating/updating scrollbar gutter and thumb windows + +--- @class blink.cmp.ScrollbarWin +--- @field enable_gutter boolean +--- @field thumb_win? number +--- @field gutter_win? number +--- @field buf? number +--- +--- @field new fun(opts: blink.cmp.ScrollbarConfig): blink.cmp.ScrollbarWin +--- @field is_visible fun(self: blink.cmp.ScrollbarWin): boolean +--- @field show_thumb fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry) +--- @field show_gutter fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry) +--- @field hide_thumb fun(self: blink.cmp.ScrollbarWin) +--- @field hide_gutter fun(self: blink.cmp.ScrollbarWin) +--- @field hide fun(self: blink.cmp.ScrollbarWin) +--- @field _make_win fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry, hl_group: string): number +--- @field redraw_if_needed fun(self: blink.cmp.ScrollbarWin) + +--- @type blink.cmp.ScrollbarWin +--- @diagnostic disable-next-line: missing-fields +local scrollbar_win = {} + +function scrollbar_win.new(opts) return setmetatable(opts, { __index = scrollbar_win }) end + +function scrollbar_win:is_visible() return self.thumb_win ~= nil and vim.api.nvim_win_is_valid(self.thumb_win) end + +function scrollbar_win:show_thumb(geometry) + -- create window if it doesn't exist + if self.thumb_win == nil or not vim.api.nvim_win_is_valid(self.thumb_win) then + self.thumb_win = self:_make_win(geometry, 'BlinkCmpScrollBarThumb') + else + -- update with the geometry + local thumb_existing_config = vim.api.nvim_win_get_config(self.thumb_win) + local thumb_config = vim.tbl_deep_extend('force', thumb_existing_config, geometry) + vim.api.nvim_win_set_config(self.thumb_win, thumb_config) + end + + self:redraw_if_needed() +end + +function scrollbar_win:show_gutter(geometry) + if not self.enable_gutter then return end + + -- create window if it doesn't exist + if self.gutter_win == nil or not vim.api.nvim_win_is_valid(self.gutter_win) then + self.gutter_win = self:_make_win(geometry, 'BlinkCmpScrollBarGutter') + else + -- update with the geometry + local gutter_existing_config = vim.api.nvim_win_get_config(self.gutter_win) + local gutter_config = vim.tbl_deep_extend('force', gutter_existing_config, geometry) + vim.api.nvim_win_set_config(self.gutter_win, gutter_config) + end + + self:redraw_if_needed() +end + +function scrollbar_win:hide_thumb() + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_close(self.thumb_win, true) + self.thumb_win = nil + self:redraw_if_needed() + end +end + +function scrollbar_win:hide_gutter() + if self.gutter_win and vim.api.nvim_win_is_valid(self.gutter_win) then + vim.api.nvim_win_close(self.gutter_win, true) + self.gutter_win = nil + self:redraw_if_needed() + end +end + +function scrollbar_win:hide() + self:hide_thumb() + self:hide_gutter() +end + +function scrollbar_win:_make_win(geometry, hl_group) + if self.buf == nil or not vim.api.nvim_buf_is_valid(self.buf) then self.buf = vim.api.nvim_create_buf(false, true) end + + local win_config = vim.tbl_deep_extend('force', geometry, { + style = 'minimal', + focusable = false, + noautocmd = true, + }) + local win = vim.api.nvim_open_win(self.buf, false, win_config) + vim.api.nvim_set_option_value('winhighlight', 'Normal:' .. hl_group .. ',EndOfBuffer:' .. hl_group, { win = win }) + return win +end + +local redraw_queued = false +function scrollbar_win:redraw_if_needed() + if redraw_queued or vim.api.nvim_get_mode().mode ~= 'c' then return end + + redraw_queued = true + vim.schedule(function() + redraw_queued = false + if self.gutter_win ~= nil and vim.api.nvim_win_is_valid(self.gutter_win) then + vim.api.nvim__redraw({ win = self.gutter_win, flush = true }) + end + if self.thumb_win ~= nil and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim__redraw({ win = self.thumb_win, flush = true }) + end + end) +end + +return scrollbar_win |
