aboutsummaryrefslogtreecommitdiffstats
path: root/lua/nvim-lsp-installer/ui
diff options
context:
space:
mode:
Diffstat (limited to 'lua/nvim-lsp-installer/ui')
-rw-r--r--lua/nvim-lsp-installer/ui/display.lua80
-rw-r--r--lua/nvim-lsp-installer/ui/init.lua73
-rw-r--r--lua/nvim-lsp-installer/ui/state.lua4
-rw-r--r--lua/nvim-lsp-installer/ui/status-win/init.lua109
4 files changed, 194 insertions, 72 deletions
diff --git a/lua/nvim-lsp-installer/ui/display.lua b/lua/nvim-lsp-installer/ui/display.lua
index e675d715..7ab8fa4a 100644
--- a/lua/nvim-lsp-installer/ui/display.lua
+++ b/lua/nvim-lsp-installer/ui/display.lua
@@ -1,4 +1,3 @@
-local Ui = require "nvim-lsp-installer.ui"
local log = require "nvim-lsp-installer.log"
local process = require "nvim-lsp-installer.process"
local state = require "nvim-lsp-installer.ui.state"
@@ -17,6 +16,8 @@ local function to_hex(str)
end))
end
+---@param line string
+---@param render_context RenderContext
local function get_styles(line, render_context)
local indentation = 0
@@ -24,10 +25,10 @@ local function get_styles(line, render_context)
local styles = render_context.applied_block_styles[i]
for j = 1, #styles do
local style = styles[j]
- if style == Ui.CascadingStyle.INDENT then
+ if style == "INDENT" then
indentation = indentation + 2
- elseif style == Ui.CascadingStyle.CENTERED then
- local padding = math.floor((render_context.context.win_width - #line) / 2)
+ elseif style == "CENTERED" then
+ local padding = math.floor((render_context.viewport_context.win_width - #line) / 2)
indentation = math.max(0, padding) -- CENTERED overrides any already applied indentation
end
end
@@ -38,11 +39,36 @@ local function get_styles(line, render_context)
}
end
-local function render_node(context, node, _render_context, _output)
- local render_context = _render_context or {
- context = context,
- applied_block_styles = {},
- }
+---@param viewport_context ViewportContext
+---@param node INode
+---@param _render_context RenderContext|nil
+---@param _output RenderOutput|nil
+local function render_node(viewport_context, node, _render_context, _output)
+ ---@class RenderContext
+ ---@field viewport_context ViewportContext
+ ---@field applied_block_styles CascadingStyle[]
+ local render_context = _render_context
+ or {
+ viewport_context = viewport_context,
+ applied_block_styles = {},
+ }
+ ---@class RenderHighlight
+ ---@field hl_group string
+ ---@field line number
+ ---@field col_start number
+ ---@field col_end number
+
+ ---@class RenderKeybind
+ ---@field line number
+ ---@field key string
+ ---@field effect string
+ ---@field payload any
+
+ ---@class RenderOutput
+ ---@field lines string[] @The buffer lines.
+ ---@field virt_texts string[][] @List of (text, highlight) tuples.
+ ---@field highlights RenderHighlight[]
+ ---@field keybinds RenderKeybind[]
local output = _output
or {
lines = {},
@@ -51,12 +77,12 @@ local function render_node(context, node, _render_context, _output)
keybinds = {},
}
- if node.type == Ui.NodeType.VIRTUAL_TEXT then
+ if node.type == "VIRTUAL_TEXT" then
output.virt_texts[#output.virt_texts + 1] = {
line = #output.lines - 1,
content = node.virt_text,
}
- elseif node.type == Ui.NodeType.HL_TEXT then
+ elseif node.type == "HL_TEXT" then
for i = 1, #node.lines do
local line = node.lines[i]
local line_highlights = {}
@@ -87,17 +113,17 @@ local function render_node(context, node, _render_context, _output)
output.lines[#output.lines + 1] = full_line
end
- elseif node.type == Ui.NodeType.NODE or node.type == Ui.NodeType.CASCADING_STYLE then
- if node.type == Ui.NodeType.CASCADING_STYLE then
+ elseif node.type == "NODE" or node.type == "CASCADING_STYLE" then
+ if node.type == "CASCADING_STYLE" then
render_context.applied_block_styles[#render_context.applied_block_styles + 1] = node.styles
end
for i = 1, #node.children do
- render_node(context, node.children[i], render_context, output)
+ render_node(viewport_context, node.children[i], render_context, output)
end
- if node.type == Ui.NodeType.CASCADING_STYLE then
+ if node.type == "CASCADING_STYLE" then
render_context.applied_block_styles[#render_context.applied_block_styles] = nil
end
- elseif node.type == Ui.NodeType.KEYBIND_HANDLER then
+ elseif node.type == "KEYBIND_HANDLER" then
output.keybinds[#output.keybinds + 1] = {
line = node.is_global and -1 or #output.lines,
key = node.key,
@@ -130,6 +156,9 @@ local active_keybinds_by_bufnr = {}
local registered_keymaps_by_bufnr = {}
local redraw_by_win_id = {}
+---@param bufnr number
+---@param line number
+---@param key string
local function call_effect_handler(bufnr, line, key)
local line_keybinds = active_keybinds_by_bufnr[bufnr][line]
if line_keybinds then
@@ -194,6 +223,7 @@ function M.new_view_only_win(name)
local bufnr, renderer, mutate_state, get_state, unsubscribe, win_id
local has_initiated = false
+ ---@param opts DisplayOpenOpts
local function open(opts)
opts = opts or {}
local highlight_groups = opts.highlight_groups
@@ -269,10 +299,11 @@ function M.new_view_only_win(name)
end
local win_width = vim.api.nvim_win_get_width(win_id)
- local context = {
+ ---@class ViewportContext
+ local viewport_context = {
win_width = win_width,
}
- local output = render_node(context, view)
+ local output = render_node(viewport_context, view)
local lines, virt_texts, highlights, keybinds =
output.lines, output.virt_texts, output.highlights, output.keybinds
@@ -330,9 +361,13 @@ function M.new_view_only_win(name)
end)
return {
- view = function(x)
- renderer = x
+ ---@param _renderer fun(state: table): table
+ view = function(_renderer)
+ renderer = _renderer
end,
+ ---@generic T : table
+ ---@param initial_state T
+ ---@return fun(mutate_fn: fun(current_state: T)), fun(): T
init = function(initial_state)
assert(renderer ~= nil, "No view function has been registered. Call .view() before .init().")
has_initiated = true
@@ -346,6 +381,8 @@ function M.new_view_only_win(name)
return mutate_state, get_state
end,
+ ---@alias DisplayOpenOpts {effects: table<string, fun()>, highlight_groups: string[]|nil}
+ ---@type fun(opts: DisplayOpenOpts)
open = vim.schedule_wrap(function(opts)
log.debug "Opening window"
assert(has_initiated, "Display has not been initiated, cannot open.")
@@ -364,15 +401,18 @@ function M.new_view_only_win(name)
end
end
end),
+ ---@type fun()
close = vim.schedule_wrap(function()
assert(has_initiated, "Display has not been initiated, cannot close.")
unsubscribe(true)
M.delete_win_buf(win_id, bufnr)
end),
+ ---@param pos number[] @(row, col) tuple
set_cursor = function(pos)
assert(win_id ~= nil, "Window has not been opened, cannot set cursor.")
return vim.api.nvim_win_set_cursor(win_id, pos)
end,
+ ---@return number[] @(row, col) tuple
get_cursor = function()
assert(win_id ~= nil, "Window has not been opened, cannot get cursor.")
return vim.api.nvim_win_get_cursor(win_id)
diff --git a/lua/nvim-lsp-installer/ui/init.lua b/lua/nvim-lsp-installer/ui/init.lua
index 4f8d6935..b40c0c47 100644
--- a/lua/nvim-lsp-installer/ui/init.lua
+++ b/lua/nvim-lsp-installer/ui/init.lua
@@ -1,83 +1,106 @@
local Data = require "nvim-lsp-installer.data"
local M = {}
-M.NodeType = Data.enum {
- "NODE",
- "CASCADING_STYLE",
- "VIRTUAL_TEXT",
- "HL_TEXT",
- "KEYBIND_HANDLER",
-}
+---@alias NodeType
+---| '"NODE"'
+---| '"CASCADING_STYLE"'
+---| '"VIRTUAL_TEXT"'
+---| '"HL_TEXT"'
+---| '"KEYBIND_HANDLER"'
+---@alias INode Node | HlTextNode | CascadingStyleNode | VirtualTextNode | KeybindHandlerNode
+
+---@param children INode[]
function M.Node(children)
- return {
- type = M.NodeType.NODE,
+ ---@class Node
+ local node = {
+ type = "NODE",
children = children,
}
+ return node
end
+---@param lines_with_span_tuples string[][]|string[]
function M.HlTextNode(lines_with_span_tuples)
if type(lines_with_span_tuples[1]) == "string" then
-- this enables a convenience API for just rendering a single line (with just a single span)
lines_with_span_tuples = { { lines_with_span_tuples } }
end
- return {
- type = M.NodeType.HL_TEXT,
+ ---@class HlTextNode
+ local node = {
+ type = "HL_TEXT",
lines = lines_with_span_tuples,
}
+ return node
end
+---@param lines string[]
function M.Text(lines)
return M.HlTextNode(Data.list_map(function(line)
return { { line, "" } }
end, lines))
end
-M.CascadingStyle = Data.enum {
- "INDENT",
- "CENTERED",
-}
+---@alias CascadingStyle
+---| '"INDENT"'
+---| '"CENTERED"'
+---@param styles CascadingStyle[]
+---@param children INode[]
function M.CascadingStyleNode(styles, children)
- return {
- type = M.NodeType.CASCADING_STYLE,
+ ---@class CascadingStyleNode
+ local node = {
+ type = "CASCADING_STYLE",
styles = styles,
children = children,
}
+ return node
end
+---@param virt_text string[][] @List of (text, highlight) tuples.
function M.VirtualTextNode(virt_text)
- return {
- type = M.NodeType.VIRTUAL_TEXT,
+ ---@class VirtualTextNode
+ local node = {
+ type = "VIRTUAL_TEXT",
virt_text = virt_text,
}
+ return node
end
-function M.When(condition, a)
+---@param condition boolean
+---@param node INode | fun(): INode
+function M.When(condition, node)
if condition then
- if type(a) == "function" then
- return a()
+ if type(node) == "function" then
+ return node()
else
- return a
+ return node
end
end
return M.Node {}
end
+---@param key string @The keymap to register to. Example: "<CR>".
+---@param effect string @The effect to call when keymap is triggered by the user.
+---@param payload any @The payload to pass to the effect handler when triggered.
+---@param is_global boolean @Whether to register the keybind to apply on all lines in the buffer.
function M.Keybind(key, effect, payload, is_global)
- return {
- type = M.NodeType.KEYBIND_HANDLER,
+ ---@class KeybindHandlerNode
+ local node = {
+ type = "KEYBIND_HANDLER",
key = key,
effect = effect,
payload = payload,
is_global = is_global or false,
}
+ return node
end
function M.EmptyLine()
return M.Text { "" }
end
+---@param rows string[][][] @A list of rows to include in the table. Each row consists of an array of (text, highlight) tuples (aka spans).
function M.Table(rows)
local col_maxwidth = {}
for i = 1, #rows do
diff --git a/lua/nvim-lsp-installer/ui/state.lua b/lua/nvim-lsp-installer/ui/state.lua
index ff54c657..628b8391 100644
--- a/lua/nvim-lsp-installer/ui/state.lua
+++ b/lua/nvim-lsp-installer/ui/state.lua
@@ -1,10 +1,14 @@
local M = {}
+---@generic T : table
+---@param initial_state T
+---@param subscriber fun(state: T)
function M.create_state_container(initial_state, subscriber)
-- we do deepcopy to make sure instances of state containers doesn't mutate the initial state
local state = vim.deepcopy(initial_state)
local has_unsubscribed = false
+ ---@param mutate_fn fun(current_state: table)
return function(mutate_fn)
mutate_fn(state)
if not has_unsubscribed then
diff --git a/lua/nvim-lsp-installer/ui/status-win/init.lua b/lua/nvim-lsp-installer/ui/status-win/init.lua
index 3eb7b206..1e4ffab3 100644
--- a/lua/nvim-lsp-installer/ui/status-win/init.lua
+++ b/lua/nvim-lsp-installer/ui/status-win/init.lua
@@ -11,6 +11,7 @@ local HELP_KEYMAP = "?"
local CLOSE_WINDOW_KEYMAP_1 = "<Esc>"
local CLOSE_WINDOW_KEYMAP_2 = "q"
+---@param props {title: string, count: number}
local function ServerGroupHeading(props)
return Ui.HlTextNode {
{ { props.title, props.highlight or "LspInstallerHeading" }, { (" (%d)"):format(props.count), "Comment" } },
@@ -18,10 +19,12 @@ local function ServerGroupHeading(props)
end
local function Indent(children)
- return Ui.CascadingStyleNode({ Ui.CascadingStyle.INDENT }, children)
+ return Ui.CascadingStyleNode({ "INDENT" }, children)
end
-local create_vader = Data.memoize(function(saber_ticks)
+local create_vader = Data.memoize(
+ ---@param saber_ticks number
+ function(saber_ticks)
-- stylua: ignore start
return {
{ { [[ _______________________________________________________________________ ]], "LspInstallerMuted" } },
@@ -37,9 +40,12 @@ local create_vader = Data.memoize(function(saber_ticks)
{ { [[ Cowth Vader (alleged Neovim user) ]], "LspInstallerMuted" } },
{ { [[ ]], "LspInstallerMuted" } },
}
- -- stylua: ignore end
-end)
+ -- stylua: ignore end
+ end
+)
+---@param is_current_settings_expanded boolean
+---@param vader_saber_ticks number
local function Help(is_current_settings_expanded, vader_saber_ticks)
local keymap_tuples = {
{ "Toggle help", HELP_KEYMAP },
@@ -108,8 +114,9 @@ local function Help(is_current_settings_expanded, vader_saber_ticks)
}
end
+---@param props {is_showing_help: boolean, help_command_text: string}
local function Header(props)
- return Ui.CascadingStyleNode({ Ui.CascadingStyle.CENTERED }, {
+ return Ui.CascadingStyleNode({ "CENTERED" }, {
Ui.HlTextNode {
{
{ props.is_showing_help and props.help_command_text or "", "LspInstallerHighlighted" },
@@ -139,6 +146,7 @@ local Seconds = {
YEAR = 29030400, -- 60 * 60 * 24 * 7 * 4 * 12
}
+---@param time number
local function get_relative_install_time(time)
local now = os.time()
local delta = math.max(now - time, 0)
@@ -157,6 +165,7 @@ local function get_relative_install_time(time)
end
end
+---@param server ServerState
local function ServerMetadata(server)
return Ui.Node(Data.list_not_nil(
Data.lazy(server.is_installed and server.deprecated, function()
@@ -202,6 +211,8 @@ local function ServerMetadata(server)
))
end
+---@param servers ServerState[]
+---@param props ServerGroupProps
local function InstalledServers(servers, props)
return Ui.Node(Data.list_map(function(server)
local is_expanded = props.expanded_server == server.name
@@ -225,12 +236,15 @@ local function InstalledServers(servers, props)
end, servers))
end
+---@param server ServerState
local function TailedOutput(server)
return Ui.HlTextNode(Data.list_map(function(line)
return { { line, "LspInstallerMuted" } }
end, server.installer.tailed_output))
end
+---@param output string[]
+---@return string
local function get_last_non_empty_line(output)
for i = #output, 1, -1 do
local line = output[i]
@@ -241,8 +255,11 @@ local function get_last_non_empty_line(output)
return ""
end
+---@param servers ServerState[]
local function PendingServers(servers)
- return Ui.Node(Data.list_map(function(server)
+ return Ui.Node(Data.list_map(function(_server)
+ ---@type ServerState
+ local server = _server
local has_failed = server.installer.has_run or server.uninstaller.has_run
local note = has_failed and "(failed)" or (server.installer.is_queued and "(queued)" or "(installing)")
return Ui.Node {
@@ -274,6 +291,8 @@ local function PendingServers(servers)
end, servers))
end
+---@param servers ServerState[]
+---@param props ServerGroupProps
local function UninstalledServers(servers, props)
return Ui.Node(Data.list_map(function(server)
local is_prioritized = props.prioritized_servers[server.name]
@@ -301,6 +320,9 @@ local function UninstalledServers(servers, props)
end, servers))
end
+---@alias ServerGroupProps {title: string, hide_when_empty: boolean|nil, servers: ServerState[][], expanded_server: string|nil, renderer: fun(servers: ServerState[], props: ServerGroupProps)}
+
+---@param props ServerGroupProps
local function ServerGroup(props)
local total_server_count = 0
local chunks = props.servers
@@ -323,6 +345,9 @@ local function ServerGroup(props)
end)
end
+---@param servers table<string, ServerState>
+---@param expanded_server string|nil
+---@param prioritized_servers string[]
local function Servers(servers, expanded_server, prioritized_servers)
local grouped_servers = {
installed = {},
@@ -398,8 +423,10 @@ local function Servers(servers, expanded_server, prioritized_servers)
}
end
+---@param server Server
local function create_initial_server_state(server)
- return {
+ ---@class ServerState
+ local server_state = {
name = server.name,
is_installed = server:is_installed(),
deprecated = server.deprecated,
@@ -415,8 +442,12 @@ local function create_initial_server_state(server)
has_run = false,
tailed_output = { "" },
},
- uninstaller = { has_run = false, error = nil },
+ uninstaller = {
+ has_run = false,
+ error = nil,
+ },
}
+ return server_state
end
local function normalize_chunks_line_endings(chunk, dest)
@@ -430,39 +461,53 @@ end
local function init(all_servers)
local window = display.new_view_only_win "LSP servers"
- window.view(function(state)
- return Indent {
- Ui.Keybind(HELP_KEYMAP, "TOGGLE_HELP", nil, true),
- Ui.Keybind(CLOSE_WINDOW_KEYMAP_1, "CLOSE_WINDOW", nil, true),
- Ui.Keybind(CLOSE_WINDOW_KEYMAP_2, "CLOSE_WINDOW", nil, true),
- Header {
- is_showing_help = state.is_showing_help,
- help_command_text = state.help_command_text,
- },
- Ui.When(state.is_showing_help, function()
- return Help(state.is_current_settings_expanded, state.vader_saber_ticks)
- end),
- Ui.When(not state.is_showing_help, function()
- return Servers(state.servers, state.expanded_server, state.prioritized_servers)
- end),
- }
- end)
+ window.view(
+ --- @param state StatusWinState
+ function(state)
+ return Indent {
+ Ui.Keybind(HELP_KEYMAP, "TOGGLE_HELP", nil, true),
+ Ui.Keybind(CLOSE_WINDOW_KEYMAP_1, "CLOSE_WINDOW", nil, true),
+ Ui.Keybind(CLOSE_WINDOW_KEYMAP_2, "CLOSE_WINDOW", nil, true),
+ Header {
+ is_showing_help = state.is_showing_help,
+ help_command_text = state.help_command_text,
+ },
+ Ui.When(state.is_showing_help, function()
+ return Help(state.is_current_settings_expanded, state.vader_saber_ticks)
+ end),
+ Ui.When(not state.is_showing_help, function()
+ return Servers(state.servers, state.expanded_server, state.prioritized_servers)
+ end),
+ }
+ end
+ )
+ ---@type table<string, ServerState>
local servers = {}
for i = 1, #all_servers do
local server = all_servers[i]
servers[server.name] = create_initial_server_state(server)
end
- local mutate_state, get_state = window.init {
+ ---@class StatusWinState
+ ---@field prioritized_servers string[]
+ local initial_state = {
servers = servers,
is_showing_help = false,
+ is_current_settings_expanded = false,
prioritized_servers = {},
expanded_server = nil,
help_command_text = "", -- for "animating" the ":help" text when toggling the help window
vader_saber_ticks = 0, -- for "animating" the cowthvader lightsaber
}
+ local mutate_state_generic, get_state_generic = window.init(initial_state)
+ -- Generics don't really work with higher-order functions so we cast it here.
+ ---@type fun(mutate_fn: fun(current_state: StatusWinState))
+ local mutate_state = mutate_state_generic
+ ---@type fun(): StatusWinState
+ local get_state = get_state_generic
+
-- TODO: memoize or throttle.. or cache. Do something. Also, as opposed to what the naming currently suggests, this
-- is not really doing anything async stuff, but will very likely do so in the future :tm:.
local async_populate_server_metadata = vim.schedule_wrap(function(server_name)
@@ -488,8 +533,12 @@ local function init(all_servers)
end)
end
+ ---@alias ServerInstallTuple {[1]:Server, [2]: string|nil}
+
+ ---@param server_tuple ServerInstallTuple
+ ---@param on_complete fun()
local function start_install(server_tuple, on_complete)
- local server, requested_version = unpack(server_tuple)
+ local server, requested_version = server_tuple[1], server_tuple[2]
mutate_state(function(state)
state.servers[server.name].installer.is_queued = false
state.servers[server.name].installer.is_running = true
@@ -533,6 +582,7 @@ local function init(all_servers)
local queue
do
local max_running = settings.current.max_concurrent_installers
+ ---@type ServerInstallTuple[]
local q = {}
local r = 0
@@ -548,12 +598,16 @@ local function init(all_servers)
end
end)
+ ---@param server Server
+ ---@param version string|nil
queue = function(server, version)
q[#q + 1] = { server, version }
check_queue()
end
end
+ ---@param server Server
+ ---@param version string|nil
local function install_server(server, version)
log.debug("Installing server", server, version)
local server_state = get_state().servers[server.name]
@@ -569,6 +623,7 @@ local function init(all_servers)
queue(server, version)
end
+ ---@param server Server
local function uninstall_server(server)
local server_state = get_state().servers[server.name]
if server_state and (server_state.installer.is_running or server_state.installer.is_queued) then