diff options
Diffstat (limited to 'lua/nvim-lsp-installer/ui')
| -rw-r--r-- | lua/nvim-lsp-installer/ui/display.lua | 80 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/init.lua | 73 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/state.lua | 4 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/status-win/init.lua | 109 |
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 |
