summaryrefslogtreecommitdiff
path: root/lua/blink/cmp/lib/window/scrollbar
diff options
context:
space:
mode:
authorMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
committerMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
commitb77413ff8f59f380612074f0c9bd49093d8db695 (patch)
tree32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /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.lua92
-rw-r--r--lua/blink/cmp/lib/window/scrollbar/init.lua37
-rw-r--r--lua/blink/cmp/lib/window/scrollbar/win.lua107
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