Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/cmdline/popupmenu.lua

Raw
local 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,
}