Initial commit

This commit is contained in:
Patrick Alvin Alcala 2025-06-26 16:53:43 +08:00
commit 209ba130c0
4852 changed files with 1517959 additions and 0 deletions

View file

@ -0,0 +1,580 @@
-- A configurable listbox that can be used as tooltip, selection box and
-- selection box with fuzzy search, this may change in the future.
--
-- @note This code is a readaptation of autocomplete plugin from rxi :)
--
-- TODO implement select box with fuzzy search
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local style = require "core.style"
local keymap = require "core.keymap"
local util = require "plugins.lsp.util"
local RootView = require "core.rootview"
local DocView = require "core.docview"
---@class lsp.listbox.item
---@field text string
---@field info string
---@field on_draw fun(item:lsp.listbox.item, x:number, y:number, calc_only?:boolean):number
---@alias lsp.listbox.callback fun(doc: core.doc, item: lsp.listbox.item)
---@class lsp.listbox.signature_param
---@field label string
---@class lsp.listbox.signature
---@field label string
---@field activeParameter? integer
---@field activeSignature? integer
---@field parameters lsp.listbox.signature_param[]
---@class lsp.listbox.signature_list
---@field activeParameter? integer
---@field activeSignature? integer
---@field signatures lsp.listbox.signature[]
---@class lsp.listbox.position
---@field line integer
---@field col integer
---@class lsp.listbox
local listbox = {}
---@class lsp.listbox.settings
---@field items lsp.listbox.item[]
---@field shown_items lsp.listbox.item[]
---@field selected_item_idx integer
---@field show_items_count boolean
---@field max_height integer
---@field active_view core.docview | nil
---@field line integer | nil
---@field col integer | nil
---@field last_line integer | nil
---@field last_col integer | nil
---@field callback lsp.listbox.callback | nil
---@field is_list boolean
---@field has_fuzzy_search boolean
---@field above_text boolean
local settings = {
items = {},
shown_items = {},
selected_item_idx = 1,
show_items_count = false,
max_height = 6,
active_view = nil,
line = nil,
col = nil,
last_line = nil,
last_col = nil,
callback = nil,
is_list = false,
has_fuzzy_search = false,
above_text = false,
}
local mt = { __tostring = function(t) return t.text end }
--------------------------------------------------------------------------------
-- Private functions
--------------------------------------------------------------------------------
---@return core.docview | nil
local function get_active_view()
if getmetatable(core.active_view) == DocView then
return core.active_view
end
end
---@param active_view core.docview
---@return number x
---@return number y
---@return number width
---@return number height
local function get_suggestions_rect(active_view)
if #settings.shown_items == 0 then
listbox.hide()
return 0, 0, 0, 0
end
local line, col
if settings.line then
line, col = settings.line, settings.col
else
line, col = active_view.doc:get_selection()
end
-- Validate line against current view because there can be cases
-- when user rapidly switches between tabs causing the deferred draw
-- to be called late and the current document view already changed.
if line > #active_view.doc.lines then
listbox.hide()
return 0, 0, 0, 0
end
local x, y = active_view:get_line_screen_position(line)
-- This function causes tokenizer to fail if given line is greater than
-- the amount of lines the document holds, so validation above is needed.
x = x + active_view:get_col_x_offset(line, col)
local padding_x = style.padding.x
local padding_y = style.padding.y
if settings.above_text and line > 1 then
y = y - active_view:get_line_height() - style.padding.y
else
y = y + active_view:get_line_height() + style.padding.y
end
local font = settings.is_list and active_view:get_font() or style.font
local text_height = font:get_height()
local max_width = 0
for _, item in ipairs(settings.shown_items) do
local w = 0
if item.on_draw then
w = item.on_draw(item, 0, 0, true)
else
w = font:get_width(item.text)
if item.info then
w = w + style.font:get_width(item.info) + style.padding.x
end
end
max_width = math.max(max_width, w)
end
local max_items = #settings.shown_items
if settings.is_list and max_items > settings.max_height then
max_items = settings.max_height
end
-- additional line to display total items
if settings.show_items_count then
max_items = max_items + 1
end
if max_width < 150 then
max_width = 150
end
local height = max_items * (text_height + (padding_y/4)) + (padding_y*2)
local width = max_width + padding_x * 2
x = x - padding_x
y = y - padding_y
local win_w = system.get_window_size()
if (width/win_w*100) >= 85 and (width+style.padding.x*4) < win_w then
x = win_w - width - style.padding.x*2
elseif width > (win_w - x) then
x = x - (width - (win_w - x))
if x < 0 then
x = 0
end
end
return x, y, width, height
end
---@param av core.docview
local function draw_listbox(av)
if #settings.shown_items <= 0 then
return
end
-- draw background rect
local rx, ry, rw, rh = get_suggestions_rect(av)
-- draw border
if not settings.is_list then
local border_width = 1
renderer.draw_rect(
rx - border_width,
ry - border_width,
rw + (border_width * 2),
rh + (border_width * 2),
style.divider
)
end
renderer.draw_rect(rx, ry, rw, rh, style.background3)
local padding_x = style.padding.x
local padding_y = style.padding.y
-- draw text
local font = settings.is_list and av:get_font() or style.font
local line_height = font:get_height() + (padding_y / 4)
local y = ry + padding_y
local max_height = settings.max_height
local show_count = (
#settings.shown_items <= max_height or not settings.is_list
) and
#settings.shown_items or max_height
local start_index = settings.selected_item_idx > max_height and
(settings.selected_item_idx-(max_height-1)) or 1
for i=start_index, start_index+show_count-1, 1 do
if not settings.shown_items[i] then
break
end
local item = settings.shown_items[i]
if item.on_draw then
item.on_draw(item, rx + padding_x, y)
else
local color = (i == settings.selected_item_idx and settings.is_list) and
style.accent or style.text
common.draw_text(
font, color, item.text, "left",
rx + padding_x, y, rw, line_height
)
if item.info then
color = (i == settings.selected_item_idx and settings.is_list) and
style.text or style.dim
common.draw_text(
style.font, color, item.info, "right",
rx, y, rw - padding_x, line_height
)
end
end
y = y + line_height
end
if settings.show_items_count then
renderer.draw_rect(rx, y, rw, 2, style.caret)
renderer.draw_rect(rx, y+2, rw, line_height, style.background)
common.draw_text(
style.font,
style.accent,
"Items",
"left",
rx + padding_x, y, rw, line_height
)
common.draw_text(
style.font,
style.accent,
tostring(settings.selected_item_idx) .. "/" .. tostring(#settings.shown_items),
"right",
rx, y, rw - padding_x, line_height
)
end
end
---Set the document position where the listbox will be draw.
---@param position? lsp.listbox.position
local function set_position(position)
if type(position) == "table" then
settings.line = position.line
settings.col = position.col
else
settings.line = nil
settings.col = nil
end
end
--------------------------------------------------------------------------------
-- Public functions
--------------------------------------------------------------------------------
---@param elements lsp.listbox.item[]
function listbox.add(elements)
if type(elements) == "table" and #elements > 0 then
local items = {}
for _, element in pairs(elements) do
table.insert(items, setmetatable(element, mt))
end
settings.items = items
end
end
function listbox.clear()
settings.items = {}
settings.selected_item_idx = 1
settings.shown_items = {}
settings.line = nil
settings.col = nil
end
---@param element lsp.listbox.item
function listbox.append(element)
table.insert(settings.items, setmetatable(element, mt))
end
function listbox.hide()
settings.active_view = nil
settings.line = nil
settings.col = nil
settings.selected_item_idx = 1
settings.shown_items = {}
core.redraw = true
end
---@param is_list? boolean
---@param position? lsp.listbox.position
function listbox.show(is_list, position)
set_position(position)
local active_view = get_active_view()
if active_view then
settings.active_view = active_view
settings.last_line, settings.last_col = active_view.doc:get_selection()
if settings.items and #settings.items > 0 then
settings.is_list = is_list or false
settings.shown_items = settings.items
end
end
core.redraw = true
end
---@param text string
---@param position? lsp.listbox.position
function listbox.show_text(text, position)
if text and type("text") == "string" then
local win_w = system.get_window_size() - style.padding.x * 6
text = util.wrap_text(text, style.font, win_w)
local items = {}
for result in string.gmatch(text.."\n", "(.-)\n") do
table.insert(items, {text = result})
end
listbox.add(items)
end
listbox.show(false, position)
end
---@param items lsp.listbox.item[]
---@param callback lsp.listbox.callback
---@param position? lsp.listbox.position
function listbox.show_list(items, callback, position)
listbox.add(items)
if callback then
settings.callback = callback
end
listbox.show(true, position)
end
---@param signatures lsp.listbox.signature_list
---@param position? lsp.listbox.position
function listbox.show_signatures(signatures, position)
local active_parameter = nil
local active_signature = nil
if signatures.activeParameter then
active_parameter = signatures.activeParameter + 1
end
if signatures.activeSignature then
active_signature = signatures.activeSignature + 1
end
local signatures_count = #signatures.signatures
local items = {}
for index, signature in ipairs(signatures.signatures) do
table.insert(items, {
text = signature.label,
signature = signature,
on_draw = function(item, x, y, calc_only)
local width = 0
local height = style.font:get_height()
if item.signature.parameters then
if signatures_count > 1 then
if index == active_signature then
width = style.font:get_width("> ")
else
width = style.font:get_width("> ")
x = x + style.font:get_width("> ")
end
end
width = width
+ style.font:get_width("(")
+ style.font:get_width(")")
if not calc_only then
if signatures_count > 1 and index == active_signature then
x = renderer.draw_text(style.font, "> ", x, y, style.caret)
end
x = renderer.draw_text(style.font, "(", x, y, style.text)
end
local params_count = #item.signature.parameters
for pindex, param in ipairs(item.signature.parameters) do
local label = ""
if type(param.label) == "table" then
label = signature.label:sub(param.label[1]+1, param.label[2])
else
label = param.label
end
if label and pindex ~= params_count then
label = label .. ", "
end
width = width + style.font:get_width(label)
if not calc_only then
local color = style.text
if
(
signature.activeParameter
and
(signature.activeParameter + 1) == pindex
)
or
(index == active_signature and active_parameter == pindex)
then
color = style.accent
end
x = renderer.draw_text(
style.font,
label,
x, y,
color
)
end
end
if not calc_only then
renderer.draw_text(style.font, ")", x, y, style.text)
end
else
width = style.font:get_width(item.signature.label)
if not calc_only then
renderer.draw_text(
style.font,
item.signature.label,
x, y,
style.text
)
end
end
return width, width > 0 and height or 0
end
})
end
listbox.add(items)
listbox.show(false, position)
end
function listbox.toggle_above(enable)
if enable then
settings.above_text = true
else
settings.above_text = false
end
end
--------------------------------------------------------------------------------
-- Patch event logic into RootView
--------------------------------------------------------------------------------
local root_view_update = RootView.update
local root_view_draw = RootView.draw
RootView.update = function(...)
root_view_update(...)
if not settings.active_view then return end
local active_view = get_active_view()
if active_view then
-- reset suggestions if caret was moved or not same active view
local line, col = active_view.doc:get_selection()
if
settings.active_view ~= active_view
or
line ~= settings.last_line or col ~= settings.last_col
then
listbox.hide()
end
else
listbox.hide()
end
end
RootView.draw = function(...)
if settings.active_view then
local active_view = get_active_view()
if
active_view and settings.active_view == active_view
and
#settings.shown_items > 0
then
-- draw suggestions box after everything else
core.root_view:defer_draw(draw_listbox, active_view)
end
end
root_view_draw(...)
end
--------------------------------------------------------------------------------
-- Commands
--------------------------------------------------------------------------------
local function predicate()
local av = get_active_view()
return av and settings.active_view and #settings.shown_items > 0, av
end
command.add(predicate, {
["listbox:select"] = function(av)
---@cast av core.docview
if settings.is_list then
local doc = av.doc
local item = settings.shown_items[settings.selected_item_idx]
if settings.callback then
settings.callback(doc, item)
end
listbox.hide()
end
end,
["listbox:previous"] = function()
if settings.is_list then
settings.selected_item_idx = math.max(settings.selected_item_idx - 1, 1)
else
listbox.hide()
end
end,
["listbox:next"] = function()
if settings.is_list then
settings.selected_item_idx = math.min(
settings.selected_item_idx + 1, #settings.shown_items
)
else
listbox.hide()
end
end,
["listbox:cancel"] = function()
listbox.hide()
end,
})
--------------------------------------------------------------------------------
-- Keymaps
--------------------------------------------------------------------------------
keymap.add {
["tab"] = "listbox:select",
["up"] = "listbox:previous",
["down"] = "listbox:next",
["escape"] = "listbox:cancel",
}
return listbox