repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/cmdline/popupmenu.lua
Rawlocal icon = require 'std.icon'
local table = require 'std.table'
local position = require 'bugabinga.cmdline.position'
local format = require 'bugabinga.cmdline.format'
local render = require 'bugabinga.cmdline.render'
local ns = vim.api.nvim_create_namespace 'bugabinga.cmdline.popupmenu'
local state = {
buffer = -1,
window = -1,
items = {},
selected = -1,
grid = -1,
above = false,
format_type = 'cmdline',
height = 0,
top_idx = 0,
}
local on_hide
local function kind_icon(kind)
if not kind or kind == '' then
return ''
end
return kind
end
local function format_item(item)
local abbr = item[1] or ''
local kind = item[2] or ''
local menu = item[3] or ''
local info = item[4] or ''
local kind_str = kind_icon(kind)
local parts = {}
if kind_str ~= '' then
table.insert(parts, kind_str .. ' ')
end
table.insert(parts, abbr)
if menu ~= '' then
table.insert(parts, ' ')
table.insert(parts, menu)
end
return table.concat(parts)
end
local function max_width(items)
local max = 0
for _, item in ipairs(items) do
local text = format_item(item)
max = math.max(max, #text)
end
return max
end
local function get_buf()
if vim.api.nvim_buf_is_valid(state.buffer) then
return state.buffer
end
state.buffer = vim.api.nvim_create_buf(false, true)
vim.bo[state.buffer].buflisted = false
vim.bo[state.buffer].filetype = 'bugabinga_popupmenu'
return state.buffer
end
local function get_anchor(row, col, grid, popup_height)
if grid == -1 then
local cmdline_anchor = position.get_cmdline_anchor()
if cmdline_anchor then
local cmdline_row = cmdline_anchor.screenpos.row
local screen_mid = vim.o.lines / 2
local above = cmdline_row > screen_mid
state.above = above
if above then
return {
row = cmdline_row - popup_height - 2,
col = cmdline_anchor.screenpos.col + col,
}
else
return {
row = cmdline_row + 3,
col = cmdline_anchor.screenpos.col + col,
}
end
end
end
state.above = false
return {
row = row + 1,
col = col,
}
end
local function get_win(buf, anchor, width, height)
local is_new = not vim.api.nvim_win_is_valid(state.window)
if is_new then
state.window = vim.api.nvim_open_win(buf, false, {
relative = 'editor',
width = width,
height = height,
row = anchor.row,
col = anchor.col,
style = 'minimal',
border = vim.g.border_style,
focusable = false,
zindex = 201,
})
end
vim.wo[state.window].cursorline = true
vim.wo[state.window].winbar = ''
vim.wo[state.window].foldenable = false
vim.wo[state.window].winhighlight = format.winhighlight(state.format_type, 'Pmenu')
return state.window
end
local function apply_scrollbar(buf, height)
local total = #state.items
if total <= height then
return
end
local selected = state.selected
local top_idx = state.top_idx
local thumb_row
if selected < 0 then
thumb_row = 0
else
local relative = selected - top_idx
thumb_row = math.max(0, math.min(height - 1, relative))
end
if state.above then
thumb_row = height - 1 - thumb_row
end
for i = 0, height - 1 do
if i == thumb_row then
vim.api.nvim_buf_set_extmark(buf, ns, i, 0, {
virt_text = { { '┼', 'PmenuThumb' } },
virt_text_pos = 'right_align',
})
else
vim.api.nvim_buf_set_extmark(buf, ns, i, 0, {
virt_text = { { '│', 'PmenuSbar' } },
virt_text_pos = 'right_align',
})
end
end
end
local function apply_highlights(buf, items, lines, prefix)
for i = 0, #lines - 1 do
local item_idx
if state.above then
item_idx = state.top_idx + #lines - 1 - i
else
item_idx = state.top_idx + i
end
local item = items[item_idx + 1]
if not item then
break
end
local col_offset = 0
local kind_str = kind_icon(item[2])
if kind_str ~= '' then
vim.api.nvim_buf_set_extmark(buf, ns, i, 0, {
end_col = #kind_str + 1,
hl_group = 'PmenuKind',
})
col_offset = #kind_str + 1
end
local abbr = item[1] or ''
if prefix ~= '' and abbr:sub(1, #prefix):lower() == prefix:lower() then
vim.api.nvim_buf_set_extmark(buf, ns, i, col_offset, {
end_col = col_offset + #prefix,
hl_group = 'PmenuMatch',
})
end
end
end
local function on_show(items, selected, row, col, grid, prefix)
if not items or #items == 0 then
on_hide()
return
end
state.items = items
state.selected = selected
state.grid = grid
state.format_type = render.get_format_type()
local width = math.min(max_width(items) + 2, vim.o.columns - 4)
local pumheight = vim.o.pumheight
local height = pumheight > 0 and math.min(#items, pumheight) or #items
state.height = height
state.top_idx = math.max(0, math.min(selected, #items - height))
local anchor = get_anchor(row, col, grid, height)
local buf = get_buf()
local win = get_win(buf, anchor, width, height)
vim.api.nvim_win_set_config(win, {
relative = 'editor',
width = width,
height = height,
row = anchor.row,
col = anchor.col,
})
vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
local lines = vim.iter(items):map(function(item)
return format_item(item)
end):totable()
if state.above then
lines = vim.iter(lines):rev():totable()
end
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
apply_highlights(buf, items, lines, prefix or '')
apply_scrollbar(buf, height)
if selected >= 0 and selected < #items then
if state.above then
local visible_start = #items - height
if selected >= visible_start then
local reversed_idx = #items - 1 - selected
vim.api.nvim_win_set_cursor(win, { reversed_idx + 1, 0 })
end
else
vim.api.nvim_win_set_cursor(win, { selected + 1, 0 })
end
end
end
local function on_select(selected)
if not vim.api.nvim_win_is_valid(state.window) then
return
end
local height = state.height
local total = #state.items
local old_top_idx = state.top_idx
if height > 0 and total > 0 then
if selected < state.top_idx then
state.top_idx = math.max(0, selected)
elseif selected >= state.top_idx + height then
state.top_idx = math.min(selected, total - height)
end
end
state.selected = selected
if selected >= 0 and total > 0 then
local cursor_row
if state.above then
cursor_row = (total - 1 - selected) - state.top_idx + 1
else
cursor_row = selected - state.top_idx + 1
end
cursor_row = math.max(1, math.min(height, cursor_row))
vim.api.nvim_win_set_cursor(state.window, { cursor_row, 0 })
end
if state.buffer ~= -1 and state.height then
vim.api.nvim_buf_clear_namespace(state.buffer, ns, 0, -1)
if state.top_idx ~= old_top_idx then
local visible = {}
for i = state.top_idx, math.min(state.top_idx + height - 1, total - 1) do
table.insert(visible, format_item(state.items[i + 1]))
end
if state.above then
visible = vim.iter(visible):rev():totable()
end
vim.api.nvim_buf_set_lines(state.buffer, 0, -1, false, visible)
end
local lines = vim.iter(state.items):map(function(item)
return format_item(item)
end):totable()
if state.above then
lines = vim.iter(lines):rev():totable()
end
apply_highlights(state.buffer, state.items, lines, '')
apply_scrollbar(state.buffer, state.height)
end
end
on_hide = function()
if vim.api.nvim_win_is_valid(state.window) then
vim.api.nvim_win_hide(state.window)
end
if vim.api.nvim_buf_is_valid(state.buffer) then
vim.api.nvim_buf_delete(state.buffer, { force = true })
end
state.buffer = -1
state.items = {}
state.selected = -1
state.grid = -1
state.above = false
state.format_type = 'cmdline'
state.height = 0
state.top_idx = 0
end
return {
on_show = on_show,
on_select = on_select,
on_hide = on_hide,
}