diff options
| author | William Boman <william@redwill.se> | 2022-05-11 16:10:57 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-11 16:10:57 +0200 |
| commit | 5e3385d90668c792919c7e2791620a6c0d569538 (patch) | |
| tree | 4b0158d3ac7765db47411d57ec754a96f8eaf1f9 /lua/nvim-lsp-installer/ui | |
| parent | chore!: remove zeta_note (diff) | |
| download | mason-5e3385d90668c792919c7e2791620a6c0d569538.tar mason-5e3385d90668c792919c7e2791620a6c0d569538.tar.gz mason-5e3385d90668c792919c7e2791620a6c0d569538.tar.bz2 mason-5e3385d90668c792919c7e2791620a6c0d569538.tar.lz mason-5e3385d90668c792919c7e2791620a6c0d569538.tar.xz mason-5e3385d90668c792919c7e2791620a6c0d569538.tar.zst mason-5e3385d90668c792919c7e2791620a6c0d569538.zip | |
chore: further decouple module structure (#685)
Diffstat (limited to 'lua/nvim-lsp-installer/ui')
| -rw-r--r-- | lua/nvim-lsp-installer/ui/components/settings-schema.lua (renamed from lua/nvim-lsp-installer/ui/status-win/components/settings-schema.lua) | 6 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/display.lua | 443 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/init.lua | 1118 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/server_hints.lua (renamed from lua/nvim-lsp-installer/ui/status-win/server_hints.lua) | 0 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/state.lua | 24 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/status-win/init.lua | 1056 |
6 files changed, 1027 insertions, 1620 deletions
diff --git a/lua/nvim-lsp-installer/ui/status-win/components/settings-schema.lua b/lua/nvim-lsp-installer/ui/components/settings-schema.lua index 550dcdb5..4954d456 100644 --- a/lua/nvim-lsp-installer/ui/status-win/components/settings-schema.lua +++ b/lua/nvim-lsp-installer/ui/components/settings-schema.lua @@ -1,8 +1,8 @@ -- Here be dragons -local Ui = require "nvim-lsp-installer.ui" -local Data = require "nvim-lsp-installer.data" +local Ui = require "nvim-lsp-installer.core.ui" +local functional = require "nvim-lsp-installer.core.functional" -local list_map = Data.list_map +local list_map = functional.list_map local property_type_highlights = { ["string"] = "String", diff --git a/lua/nvim-lsp-installer/ui/display.lua b/lua/nvim-lsp-installer/ui/display.lua deleted file mode 100644 index 8a786810..00000000 --- a/lua/nvim-lsp-installer/ui/display.lua +++ /dev/null @@ -1,443 +0,0 @@ -local log = require "nvim-lsp-installer.log" -local process = require "nvim-lsp-installer.process" -local state = require "nvim-lsp-installer.ui.state" - -local M = {} - -local function from_hex(str) - return (str:gsub("..", function(cc) - return string.char(tonumber(cc, 16)) - end)) -end - -local function to_hex(str) - return (str:gsub(".", function(c) - return string.format("%02X", string.byte(c)) - end)) -end - ----@param line string ----@param render_context RenderContext -local function get_styles(line, render_context) - local indentation = 0 - - for i = 1, #render_context.applied_block_styles do - local styles = render_context.applied_block_styles[i] - for j = 1, #styles do - local style = styles[j] - if style == "INDENT" then - indentation = indentation + 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 - end - - return { - indentation = indentation, - } -end - ----@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 = {}, - virt_texts = {}, - highlights = {}, - keybinds = {}, - } - - if node.type == "VIRTUAL_TEXT" then - output.virt_texts[#output.virt_texts + 1] = { - line = #output.lines - 1, - content = node.virt_text, - } - elseif node.type == "HL_TEXT" then - for i = 1, #node.lines do - local line = node.lines[i] - local line_highlights = {} - local full_line = "" - for j = 1, #line do - local span = line[j] - local content, hl_group = span[1], span[2] - local col_start = #full_line - full_line = full_line .. content - if hl_group ~= "" then - line_highlights[#line_highlights + 1] = { - hl_group = hl_group, - line = #output.lines, - col_start = col_start, - col_end = col_start + #content, - } - end - end - - local active_styles = get_styles(full_line, render_context) - - -- apply indentation - full_line = (" "):rep(active_styles.indentation) .. full_line - for j = 1, #line_highlights do - local highlight = line_highlights[j] - highlight.col_start = highlight.col_start + active_styles.indentation - highlight.col_end = highlight.col_end + active_styles.indentation - output.highlights[#output.highlights + 1] = highlight - end - - output.lines[#output.lines + 1] = full_line - end - 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(viewport_context, node.children[i], render_context, output) - end - if node.type == "CASCADING_STYLE" then - render_context.applied_block_styles[#render_context.applied_block_styles] = nil - end - elseif node.type == "KEYBIND_HANDLER" then - output.keybinds[#output.keybinds + 1] = { - line = node.is_global and -1 or #output.lines, - key = node.key, - effect = node.effect, - payload = node.payload, - } - end - - return output -end - --- exported for tests -M._render_node = render_node - ----@param sizes_only boolean @Whether to only return properties that control the window size. -local function create_popup_window_opts(sizes_only) - local win_height = vim.o.lines - vim.o.cmdheight - 2 -- Add margin for status and buffer line - local win_width = vim.o.columns - local height = math.floor(win_height * 0.9) - local width = math.floor(win_width * 0.8) - local popup_layout = { - height = height, - width = width, - row = math.floor((win_height - height) / 2), - col = math.floor((win_width - width) / 2), - relative = "editor", - style = "minimal", - } - - if not sizes_only then - popup_layout.border = "rounded" - end - - return popup_layout -end - -local registered_effect_handlers_by_bufnr = {} -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 - local keybind = line_keybinds[key] - if keybind then - local effect_handler = registered_effect_handlers_by_bufnr[bufnr][keybind.effect] - if effect_handler then - log.fmt_trace("Calling handler for effect %s on line %d for key %s", keybind.effect, line, key) - effect_handler { payload = keybind.payload } - return true - end - end - end - return false -end - -M.dispatch_effect = function(bufnr, hex_key) - local key = from_hex(hex_key) - local line = vim.api.nvim_win_get_cursor(0)[1] - log.fmt_trace("Dispatching effect on line %d, key %s, bufnr %s", line, key, bufnr) - call_effect_handler(bufnr, line, key) -- line keybinds - call_effect_handler(bufnr, -1, key) -- global keybinds -end - -function M.redraw_win(win_id) - local fn = redraw_by_win_id[win_id] - if fn then - fn() - end -end - -function M.delete_win_buf(win_id, bufnr) - -- We queue the win_buf to be deleted in a schedule call, otherwise when used with folke/which-key (and - -- set timeoutlen=0) we run into a weird segfault. - -- It should probably be unnecessary once https://github.com/neovim/neovim/issues/15548 is resolved - vim.schedule(function() - if win_id and vim.api.nvim_win_is_valid(win_id) then - log.trace "Deleting window" - vim.api.nvim_win_close(win_id, true) - end - if bufnr and vim.api.nvim_buf_is_valid(bufnr) then - log.trace "Deleting buffer" - vim.api.nvim_buf_delete(bufnr, { force = true }) - end - if redraw_by_win_id[win_id] then - redraw_by_win_id[win_id] = nil - end - if active_keybinds_by_bufnr[bufnr] then - active_keybinds_by_bufnr[bufnr] = nil - end - if registered_effect_handlers_by_bufnr[bufnr] then - registered_effect_handlers_by_bufnr[bufnr] = nil - end - if registered_keymaps_by_bufnr[bufnr] then - registered_keymaps_by_bufnr[bufnr] = nil - end - end) -end - -function M.new_view_only_win(name) - local namespace = vim.api.nvim_create_namespace(("lsp_installer_%s"):format(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 - bufnr = vim.api.nvim_create_buf(false, true) - win_id = vim.api.nvim_open_win(bufnr, true, create_popup_window_opts(false)) - - registered_effect_handlers_by_bufnr[bufnr] = {} - active_keybinds_by_bufnr[bufnr] = {} - registered_keymaps_by_bufnr[bufnr] = {} - - local buf_opts = { - modifiable = false, - swapfile = false, - textwidth = 0, - buftype = "nofile", - bufhidden = "wipe", - buflisted = false, - filetype = "lsp-installer", - } - - local win_opts = { - number = false, - relativenumber = false, - wrap = false, - spell = false, - foldenable = false, - signcolumn = "no", - colorcolumn = "", - cursorline = true, - } - - -- window options - for key, value in pairs(win_opts) do - vim.api.nvim_win_set_option(win_id, key, value) - end - - -- buffer options - for key, value in pairs(buf_opts) do - vim.api.nvim_buf_set_option(bufnr, key, value) - end - - vim.cmd [[ syntax clear ]] - - local resize_autocmd = ( - "autocmd VimResized <buffer> lua require('nvim-lsp-installer.ui.display').redraw_win(%d)" - ):format(win_id) - local autoclose_autocmd = ( - "autocmd WinLeave,BufHidden,BufLeave <buffer> ++once lua require('nvim-lsp-installer.ui.display').delete_win_buf(%d, %d)" - ):format(win_id, bufnr) - - vim.cmd(([[ - augroup LspInstallerWindow - autocmd! - %s - %s - augroup END - ]]):format(resize_autocmd, autoclose_autocmd)) - - if highlight_groups then - for i = 1, #highlight_groups do - vim.cmd(highlight_groups[i]) - end - end - - return win_id - end - - local draw = function(view) - local win_valid = win_id ~= nil and vim.api.nvim_win_is_valid(win_id) - local buf_valid = bufnr ~= nil and vim.api.nvim_buf_is_valid(bufnr) - log.fmt_trace("got bufnr=%s", bufnr) - log.fmt_trace("got win_id=%s", win_id) - - if not win_valid or not buf_valid then - -- the window has been closed or the buffer is somehow no longer valid - unsubscribe(true) - log.trace("Buffer or window is no longer valid", win_id, bufnr) - return - end - - local win_width = vim.api.nvim_win_get_width(win_id) - ---@class ViewportContext - local viewport_context = { - win_width = win_width, - } - local output = render_node(viewport_context, view) - local lines, virt_texts, highlights, keybinds = - output.lines, output.virt_texts, output.highlights, output.keybinds - - -- set line contents - vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) - vim.api.nvim_buf_set_option(bufnr, "modifiable", true) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - vim.api.nvim_buf_set_option(bufnr, "modifiable", false) - - for i = 1, #virt_texts do - local virt_text = virt_texts[i] - vim.api.nvim_buf_set_extmark(bufnr, namespace, virt_text.line, 0, { - virt_text = virt_text.content, - }) - end - - -- set highlights - for i = 1, #highlights do - local highlight = highlights[i] - vim.api.nvim_buf_add_highlight( - bufnr, - namespace, - highlight.hl_group, - highlight.line, - highlight.col_start, - highlight.col_end - ) - end - - -- set keybinds - local buf_keybinds = {} - active_keybinds_by_bufnr[bufnr] = buf_keybinds - for i = 1, #keybinds do - local keybind = keybinds[i] - if not buf_keybinds[keybind.line] then - buf_keybinds[keybind.line] = {} - end - buf_keybinds[keybind.line][keybind.key] = keybind - if not registered_keymaps_by_bufnr[bufnr][keybind.key] then - vim.api.nvim_buf_set_keymap( - bufnr, - "n", - keybind.key, - ("<cmd>lua require('nvim-lsp-installer.ui.display').dispatch_effect(%d, %q)<cr>"):format( - bufnr, - -- We transfer the keybinding as hex to avoid issues with (neo)vim interpreting the key as a - -- literal input to the command. For example, "<CR>" would cause vim to issue an actual carriage - -- return - even if it's quoted as a string. - to_hex(keybind.key) - ), - { nowait = true, silent = true, noremap = true } - ) - end - end - end - - return { - ---@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 - - mutate_state, get_state, unsubscribe = state.create_state_container( - initial_state, - process.debounced(function(new_state) - draw(renderer(new_state)) - end) - ) - - -- we don't need to subscribe to state changes until the window is actually opened - unsubscribe(true) - - 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.trace "Opening window" - assert(has_initiated, "Display has not been initiated, cannot open.") - if win_id and vim.api.nvim_win_is_valid(win_id) then - -- window is already open - return - end - unsubscribe(false) - local opened_win_id = open(opts) - draw(renderer(get_state())) - registered_effect_handlers_by_bufnr[bufnr] = opts.effects - redraw_by_win_id[opened_win_id] = function() - if vim.api.nvim_win_is_valid(opened_win_id) then - draw(renderer(get_state())) - vim.api.nvim_win_set_config(opened_win_id, create_popup_window_opts(true)) - 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) - end, - } -end - -return M diff --git a/lua/nvim-lsp-installer/ui/init.lua b/lua/nvim-lsp-installer/ui/init.lua index 667bb35a..276941c0 100644 --- a/lua/nvim-lsp-installer/ui/init.lua +++ b/lua/nvim-lsp-installer/ui/init.lua @@ -1,128 +1,1058 @@ -local Data = require "nvim-lsp-installer.data" -local M = {} +local a = require "nvim-lsp-installer.core.async" +local Ui = require "nvim-lsp-installer.core.ui" +local display = require "nvim-lsp-installer.core.ui.display" +local fs = require "nvim-lsp-installer.core.fs" +local log = require "nvim-lsp-installer.log" +local functional = require "nvim-lsp-installer.core.functional" +local settings = require "nvim-lsp-installer.settings" +local lsp_servers = require "nvim-lsp-installer.servers" +local JobExecutionPool = require "nvim-lsp-installer.jobs.pool" +local outdated_servers = require "nvim-lsp-installer.jobs.outdated-servers" +local version_check = require "nvim-lsp-installer.jobs.version-check" +local ServerHints = require "nvim-lsp-installer.ui.server_hints" +local ServerSettingsSchema = require "nvim-lsp-installer.ui.components.settings-schema" ----@alias NodeType ----| '"NODE"' ----| '"CASCADING_STYLE"' ----| '"VIRTUAL_TEXT"' ----| '"HL_TEXT"' ----| '"KEYBIND_HANDLER"' +local HELP_KEYMAP = "?" +local CLOSE_WINDOW_KEYMAP_1 = "<Esc>" +local CLOSE_WINDOW_KEYMAP_2 = "q" ----@alias INode Node | HlTextNode | CascadingStyleNode | VirtualTextNode | KeybindHandlerNode - ----@param children INode[] -function M.Node(children) - ---@class Node - local node = { - type = "NODE", - children = children, +---@param props {title: string, subtitle: string[][], count: number} +local function ServerGroupHeading(props) + local line = { + { props.title, props.highlight or "LspInstallerHeading" }, + { " (" .. props.count .. ") ", "Comment" }, } - return node + if props.subtitle then + vim.list_extend(line, props.subtitle) + end + return Ui.HlTextNode { line } +end + +local function Indent(children) + return Ui.CascadingStyleNode({ "INDENT" }, children) 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 } } +local create_vader = functional.memoize( + ---@param saber_ticks number + function(saber_ticks) + -- stylua: ignore start + return { + { { [[ _________________________________________________________________________________________ ]], "LspInstallerMuted" } }, + { { [[ < Help sponsor nvim-lsp-installer development! ]], "LspInstallerMuted" }, { "https://github.com/sponsors/williamboman", "LspInstallerHighlighted"}, {[[ > ]], "LspInstallerMuted" } }, + { { [[ < Help sponsor neovim development! ]], "LspInstallerMuted" }, { "https://github.com/sponsors/neovim", "LspInstallerHighlighted"}, {[[ > ]], "LspInstallerMuted" } }, + { { [[ ----------------------------------------------------------------------------------------- ]], "LspInstallerMuted" } }, + { { [[ ]], ""}, {[[\]], saber_ticks >= 3 and "LspInstallerVaderSaber" or "LspInstallerMuted"}, {[[ ,-^-. ]], "LspInstallerMuted" } }, + { { [[ ]], ""}, {[[\]], saber_ticks >= 2 and "LspInstallerVaderSaber" or "LspInstallerMuted"}, {[[ !oYo! ]], "LspInstallerMuted" } }, + { { [[ ]], ""}, {[[\]], saber_ticks >= 1 and "LspInstallerVaderSaber" or "LspInstallerMuted"}, {[[ /./=\.\______ ]], "LspInstallerMuted" } }, + { { [[ ## )\/\ ]], "LspInstallerMuted" } }, + { { [[ ||-----w|| ]], "LspInstallerMuted" } }, + { { [[ || || ]], "LspInstallerMuted" } }, + { { [[ ]], "LspInstallerMuted" } }, + { { [[ Cowth Vader (alleged Neovim user) ]], "LspInstallerMuted" } }, + { { [[ ]], "LspInstallerMuted" } }, + } + -- stylua: ignore end end - ---@class HlTextNode - local node = { - type = "HL_TEXT", - lines = lines_with_span_tuples, +) + +---@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 }, + { "Toggle server info", settings.current.ui.keymaps.toggle_server_expand }, + { "Update server", settings.current.ui.keymaps.update_server }, + { "Update all installed servers", settings.current.ui.keymaps.update_all_servers }, + { "Check for new server version", settings.current.ui.keymaps.check_server_version }, + { "Check for new versions (all servers)", settings.current.ui.keymaps.check_outdated_servers }, + { "Uninstall server", settings.current.ui.keymaps.uninstall_server }, + { "Install server", settings.current.ui.keymaps.install_server }, + { "Close window", CLOSE_WINDOW_KEYMAP_1 }, + { "Close window", CLOSE_WINDOW_KEYMAP_2 }, + } + + local very_reasonable_cow = create_vader(vader_saber_ticks) + + return Ui.Node { + Ui.EmptyLine(), + Ui.HlTextNode { + { { "Installer log: ", "LspInstallerMuted" }, { log.outfile, "" } }, + }, + Ui.EmptyLine(), + Ui.Table(vim.list_extend( + { + { + { "Keyboard shortcuts", "LspInstallerLabel" }, + }, + }, + functional.list_map(function(keymap_tuple) + return { { keymap_tuple[1], "LspInstallerMuted" }, { keymap_tuple[2], "LspInstallerHighlighted" } } + end, keymap_tuples) + )), + Ui.EmptyLine(), + Ui.HlTextNode { + { { "Problems installing/uninstalling servers", "LspInstallerLabel" } }, + { + { + "Make sure you meet the minimum requirements to install servers. For debugging, refer to:", + "LspInstallerMuted", + }, + }, + }, + Indent { + Ui.HlTextNode { + { + { ":help nvim-lsp-installer-debugging", "LspInstallerHighlighted" }, + }, + { + { ":checkhealth nvim-lsp-installer", "LspInstallerHighlighted" }, + }, + }, + }, + Ui.EmptyLine(), + Ui.HlTextNode { + { { "Problems with server functionality", "LspInstallerLabel" } }, + { + { + "Please refer to each language server's own homepage for further assistance.", + "LspInstallerMuted", + }, + }, + }, + Ui.EmptyLine(), + Ui.HlTextNode { + { { "Missing a server?", "LspInstallerLabel" } }, + { + { + "Create an issue at ", + "LspInstallerMuted", + }, + { + "https://github.com/williamboman/nvim-lsp-installer/issues/new/choose", + "LspInstallerHighlighted", + }, + }, + }, + Ui.EmptyLine(), + Ui.HlTextNode { + { { "How do I customize server settings?", "LspInstallerLabel" } }, + { + { "For information on how to customize a server's settings, see ", "LspInstallerMuted" }, + { ":help lspconfig-setup", "LspInstallerHighlighted" }, + }, + }, + Ui.EmptyLine(), + Ui.HlTextNode { + { + { + ("%s Current settings"):format(is_current_settings_expanded and "↓" or "→"), + "LspInstallerLabel", + }, + { " :help nvim-lsp-installer-settings", "LspInstallerHighlighted" }, + }, + }, + Ui.Keybind("<CR>", "TOGGLE_EXPAND_CURRENT_SETTINGS", nil), + Ui.When(is_current_settings_expanded, function() + local settings_split_by_newline = vim.split(vim.inspect(settings.current), "\n") + local current_settings = functional.list_map(function(line) + return { { line, "LspInstallerMuted" } } + end, settings_split_by_newline) + return Ui.HlTextNode(current_settings) + end), + Ui.EmptyLine(), + Ui.HlTextNode(very_reasonable_cow), } - return node end ----@param lines string[] -function M.Text(lines) - return M.HlTextNode(Data.list_map(function(line) - return { { line, "" } } - end, lines)) +---@param props {is_showing_help: boolean, help_command_text: string} +local function Header(props) + return Ui.CascadingStyleNode({ "CENTERED" }, { + Ui.HlTextNode { + { + { props.is_showing_help and props.help_command_text or "", "LspInstallerHighlighted" }, + { + props.is_showing_help and "nvim-lsp-installer" .. (" "):rep(#props.help_command_text) + or "nvim-lsp-installer", + props.is_showing_help and "LspInstallerHighlighted" or "LspInstallerHeader", + }, + }, + { + { props.is_showing_help and " press " or "press ", "LspInstallerMuted" }, + { "?", props.is_showing_help and "LspInstallerOrange" or "LspInstallerHighlighted" }, + { props.is_showing_help and " for server list" or " for help", "LspInstallerMuted" }, + }, + { + { "https://github.com/williamboman/nvim-lsp-installer", "Comment" }, + }, + }, + }) end ----@alias CascadingStyle ----| '"INDENT"' ----| '"CENTERED"' +---@param time number +local function format_time(time) + return os.date("%d %b %Y %H:%M", time) +end ----@param styles CascadingStyle[] ----@param children INode[] -function M.CascadingStyleNode(styles, children) - ---@class CascadingStyleNode - local node = { - type = "CASCADING_STYLE", - styles = styles, - children = children, - } - return node +---@param outdated_packages OutdatedPackage[] +---@return string +local function format_new_package_versions(outdated_packages) + local result = {} + if #outdated_packages == 1 then + return outdated_packages[1].latest_version + end + for _, outdated_package in ipairs(outdated_packages) do + result[#result + 1] = ("%s@%s"):format(outdated_package.name, outdated_package.latest_version) + end + return table.concat(result, ", ") end ----@param virt_text string[][] @List of (text, highlight) tuples. -function M.VirtualTextNode(virt_text) - ---@class VirtualTextNode - local node = { - type = "VIRTUAL_TEXT", - virt_text = virt_text, - } - return node +---@param server ServerState +local function ServerMetadata(server) + return Ui.Node(functional.list_not_nil( + functional.lazy_when(server.is_installed and server.deprecated, function() + return Ui.Node(functional.list_not_nil( + Ui.HlTextNode { server.deprecated.message, "Comment" }, + functional.lazy_when(server.deprecated.replace_with, function() + return Ui.Node { + Ui.HlTextNode { + { + { "Replace with: ", "LspInstallerMuted" }, + { server.deprecated.replace_with, "LspInstallerHighlighted" }, + }, + }, + Ui.Keybind("<CR>", "REPLACE_SERVER", { server.name, server.deprecated.replace_with }), + Ui.EmptyLine(), + } + end) + )) + end), + Ui.Table(functional.list_not_nil( + functional.lazy_when(server.is_installed, function() + return { + { "version", "LspInstallerMuted" }, + server.installed_version_err and { + "Unable to detect version.", + "LspInstallerMuted", + } or { server.installed_version or "Loading...", "" }, + } + end), + functional.lazy_when(#server.metadata.outdated_packages > 0, function() + return { + { "latest version", "LspInstallerGreen" }, + { + format_new_package_versions(server.metadata.outdated_packages), + "LspInstallerGreen", + }, + } + end), + functional.lazy_when(server.metadata.install_timestamp_seconds, function() + return { + { "installed", "LspInstallerMuted" }, + { format_time(server.metadata.install_timestamp_seconds), "" }, + } + end), + functional.when(not server.is_installed, { + { "filetypes", "LspInstallerMuted" }, + { server.metadata.filetypes, "" }, + }), + functional.when(server.is_installed, { + { "path", "LspInstallerMuted" }, + { server.metadata.install_dir, "String" }, + }), + { + { "homepage", "LspInstallerMuted" }, + server.metadata.homepage and { server.metadata.homepage, "LspInstallerLink" } or { + "-", + "LspInstallerMuted", + }, + } + )), + Ui.When(server.schema, function() + return Ui.Node { + Ui.EmptyLine(), + Ui.HlTextNode { + { + { + ("%s Server configuration schema"):format(server.has_expanded_schema and "↓" or "→"), + "LspInstallerLabel", + }, + { + (" (press enter to %s)"):format(server.has_expanded_schema and "collapse" or "expand"), + "Comment", + }, + }, + }, + Ui.Keybind("<CR>", "TOGGLE_SERVER_SETTINGS_SCHEMA", { server.name }), + Ui.When(server.has_expanded_schema, function() + return Indent { + Ui.HlTextNode { + { + { + "This is a read-only representation of the settings this server accepts. Note that some settings might not apply to neovim.", + "LspInstallerMuted", + }, + }, + { + { "For information on how to customize these settings, see ", "LspInstallerMuted" }, + { ":help lspconfig-setup", "LspInstallerHighlighted" }, + }, + }, + Ui.EmptyLine(), + ServerSettingsSchema(server, server.schema), + } + end), + Ui.EmptyLine(), + } + end) + )) +end + +---@param servers ServerState[] +---@param props ServerGroupProps +local function InstalledServers(servers, props) + return Ui.Node(functional.list_map( + ---@param server ServerState + function(server) + local is_expanded = props.expanded_server == server.name + return Ui.Node { + Ui.HlTextNode { + functional.list_not_nil( + { settings.current.ui.icons.server_installed, "LspInstallerGreen" }, + { " " .. server.name .. " ", "" }, + { server.hints, "Comment" }, + functional.when(server.deprecated, { " deprecated", "LspInstallerOrange" }), + functional.when( + #server.metadata.outdated_packages > 0 and not is_expanded, + { " new version available", "LspInstallerGreen" } + ) + ), + }, + Ui.Keybind(settings.current.ui.keymaps.toggle_server_expand, "EXPAND_SERVER", { server.name }), + Ui.Keybind(settings.current.ui.keymaps.update_server, "INSTALL_SERVER", { server.name }), + Ui.Keybind(settings.current.ui.keymaps.check_server_version, "CHECK_SERVER_VERSION", { server.name }), + Ui.Keybind(settings.current.ui.keymaps.uninstall_server, "UNINSTALL_SERVER", { server.name }), + Ui.When(is_expanded, function() + return Indent { + ServerMetadata(server), + } + end), + } + end, + servers + )) +end + +---@param server ServerState +local function TailedOutput(server) + return Ui.HlTextNode(functional.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] + if #line > 0 then + return line + end + end + return "" +end + +---@param servers ServerState[] +local function PendingServers(servers) + return Ui.Node(functional.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 { + Ui.HlTextNode { + functional.list_not_nil( + { + settings.current.ui.icons.server_pending, + has_failed and "LspInstallerError" or "LspInstallerOrange", + }, + { " " .. server.name, server.installer.is_running and "" or "LspInstallerMuted" }, + { " " .. note, "Comment" }, + functional.when(not has_failed, { + (" " .. get_last_non_empty_line(server.installer.tailed_output)), + "Comment", + }) + ), + }, + Ui.Keybind(settings.current.ui.keymaps.install_server, "INSTALL_SERVER", { server.name }), + Ui.When(has_failed, function() + return Indent { Indent { TailedOutput(server) } } + end), + Ui.When( + server.uninstaller.error, + Indent { + Ui.HlTextNode { server.uninstaller.error, "Comment" }, + } + ), + } + end, servers)) +end + +---@param servers ServerState[] +---@param props ServerGroupProps +local function UninstalledServers(servers, props) + return Ui.Node(functional.list_map(function(_server) + ---@type ServerState + local server = _server + local is_prioritized = props.prioritized_servers[server.name] + local is_expanded = props.expanded_server == server.name + return Ui.Node { + Ui.HlTextNode { + functional.list_not_nil( + { + settings.current.ui.icons.server_uninstalled, + is_prioritized and "LspInstallerHighlighted" or "LspInstallerMuted", + }, + { " " .. server.name .. " ", "LspInstallerMuted" }, + { server.hints, "Comment" }, + functional.when(server.uninstaller.has_run, { " (uninstalled) ", "Comment" }), + functional.when(server.deprecated, { "deprecated ", "LspInstallerOrange" }) + ), + }, + Ui.Keybind(settings.current.ui.keymaps.toggle_server_expand, "EXPAND_SERVER", { server.name }), + Ui.Keybind(settings.current.ui.keymaps.install_server, "INSTALL_SERVER", { server.name }), + Ui.When(is_expanded, function() + return Indent { + ServerMetadata(server), + } + end), + } + end, servers)) +end + +---@alias ServerGroupProps {title: string, subtitle: string|nil, 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 + for i = 1, #chunks do + local servers = chunks[i] + total_server_count = total_server_count + #servers + end + + return Ui.When(total_server_count > 0 or not props.hide_when_empty, function() + return Ui.Node { + Ui.EmptyLine(), + ServerGroupHeading { + title = props.title, + subtitle = props.subtitle, + count = total_server_count, + }, + Indent(functional.list_map(function(servers) + return props.renderer(servers, props) + end, props.servers)), + } + end) end ----@param condition boolean ----@param node INode | fun(): INode ----@param default_val any -function M.When(condition, node, default_val) - if condition then - if type(node) == "function" then - return node() +---@param state StatusWinState +local function Servers(state) + local grouped_servers = { + installed = {}, + queued = {}, + session_installed = {}, + uninstall_failed = {}, + installing = {}, + install_failed = {}, + uninstalled_prioritized = {}, + uninstalled = {}, + session_uninstalled = {}, + } + + local servers, server_name_order, prioritized_servers, expanded_server = + state.servers, state.server_name_order, state.prioritized_servers, state.expanded_server + + -- giggity + for _, server_name in ipairs(server_name_order) do + local server = servers[server_name] + if server.installer.is_running then + grouped_servers.installing[#grouped_servers.installing + 1] = server + elseif server.installer.is_queued then + grouped_servers.queued[#grouped_servers.queued + 1] = server + elseif server.uninstaller.has_run then + if server.uninstaller.error then + grouped_servers.uninstall_failed[#grouped_servers.uninstall_failed + 1] = server + else + grouped_servers.session_uninstalled[#grouped_servers.session_uninstalled + 1] = server + end + elseif server.is_installed then + if server.installer.has_run then + grouped_servers.session_installed[#grouped_servers.session_installed + 1] = server + else + grouped_servers.installed[#grouped_servers.installed + 1] = server + end + elseif server.installer.has_run then + grouped_servers.install_failed[#grouped_servers.install_failed + 1] = server else - return node + if prioritized_servers[server.name] then + grouped_servers.uninstalled_prioritized[#grouped_servers.uninstalled_prioritized + 1] = server + else + grouped_servers.uninstalled[#grouped_servers.uninstalled + 1] = server + end end end - return default_val or M.Node {} + + return Ui.Node { + ServerGroup { + title = "Installed servers", + subtitle = state.server_version_check_completed_percentage ~= nil and { + { + "checking for new versions ", + "Comment", + }, + { + state.server_version_check_completed_percentage .. "%", + state.server_version_check_completed_percentage == 100 and "LspInstallerVersionCheckLoaderDone" + or "LspInstallerVersionCheckLoader", + }, + { + string.rep(" ", math.floor(state.server_version_check_completed_percentage / 5)), + state.server_version_check_completed_percentage == 100 and "LspInstallerVersionCheckLoaderDone" + or "LspInstallerVersionCheckLoader", + }, + }, + renderer = InstalledServers, + servers = { grouped_servers.session_installed, grouped_servers.installed }, + expanded_server = expanded_server, + }, + ServerGroup { + title = "Pending servers", + hide_when_empty = true, + renderer = PendingServers, + servers = { + grouped_servers.installing, + grouped_servers.queued, + grouped_servers.install_failed, + grouped_servers.uninstall_failed, + }, + expanded_server = expanded_server, + }, + ServerGroup { + title = "Available servers", + renderer = UninstalledServers, + servers = { + grouped_servers.session_uninstalled, + grouped_servers.uninstalled_prioritized, + grouped_servers.uninstalled, + }, + expanded_server = expanded_server, + prioritized_servers = prioritized_servers, + }, + } 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) - ---@class KeybindHandlerNode - local node = { - type = "KEYBIND_HANDLER", - key = key, - effect = effect, - payload = payload, - is_global = is_global or false, +---@param server Server +local function create_initial_server_state(server) + ---@class ServerState + local server_state = { + name = server.name, + is_installed = server:is_installed(), + deprecated = server.deprecated, + hints = tostring(ServerHints.new(server)), + expanded_schema_properties = {}, + has_expanded_schema = false, + installed_version = nil, -- lazy + installed_version_err = nil, -- lazy + ---@type table + schema = nil, -- lazy + metadata = { + homepage = server.homepage, + ---@type number + install_timestamp_seconds = nil, -- lazy + install_dir = vim.fn.fnamemodify(server.root_dir, ":~"), + filetypes = table.concat(server:get_supported_filetypes(), ", "), + ---@type OutdatedPackage[] + outdated_packages = {}, + }, + installer = { + is_queued = false, + is_running = false, + has_run = false, + tailed_output = { "" }, + }, + uninstaller = { + has_run = false, + error = nil, + }, } - return node + return server_state end -function M.EmptyLine() - return M.Text { "" } +local function normalize_chunks_line_endings(chunk, dest) + local chunk_lines = vim.split(chunk, "\n") + dest[#dest] = dest[#dest] .. chunk_lines[1] + for i = 2, #chunk_lines do + dest[#dest + 1] = chunk_lines[i] + end 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 - local row = rows[i] - for j = 1, #row do - local col = row[j] - local content = col[1] - col_maxwidth[j] = math.max(vim.api.nvim_strwidth(content), col_maxwidth[j] or 0) +local function init(all_servers) + local filetype_map = require "nvim-lsp-installer._generated.filetype_map" + local window = display.new_view_only_win "LSP servers" + + log.trace "Initializing status window" + + 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), + Ui.Keybind(settings.current.ui.keymaps.check_outdated_servers, "CHECK_OUTDATED_SERVERS", nil, true), + Ui.Keybind(settings.current.ui.keymaps.update_all_servers, "UPDATE_ALL_SERVERS", 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) + end), + } + end + ) + + ---@type table<string, ServerState> + local servers = {} + ---@type string[] + local server_name_order = {} + for i = 1, #all_servers do + local server = all_servers[i] + servers[server.name] = create_initial_server_state(server) + server_name_order[#server_name_order + 1] = server.name + end + + table.sort(server_name_order) + + ---@class StatusWinState + ---@field prioritized_servers string[] + local initial_state = { + server_name_order = server_name_order, + servers = servers, + server_version_check_completed_percentage = nil, + 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 + + local async_populate_server_metadata = a.scope(function(server_name) + a.scheduler() + local ok, server = lsp_servers.get_server(server_name) + if not ok then + return log.warn("Unable to get server when populating metadata.", server_name) + end + local fstat_ok, fstat = pcall(fs.async.fstat, server.root_dir) + mutate_state(function(state) + if fstat_ok then + state.servers[server.name].metadata.install_timestamp_seconds = fstat.mtime.sec + end + state.servers[server.name].schema = server:get_settings_schema() + end) + local version = version_check.check_server_version(server) + mutate_state(function(state) + if version:is_success() then + state.servers[server.name].installed_version = version:get_or_nil() + state.servers[server.name].installed_version_err = nil + else + state.servers[server.name].installed_version_err = true + end + end) + end) + + ---@param server_name string + local function expand_server(server_name) + mutate_state(function(state) + local should_expand = state.expanded_server ~= server_name + state.expanded_server = should_expand and server_name or nil + if should_expand then + async_populate_server_metadata(server_name) + end + end) + end + + ---@param server Server + ---@param requested_version string|nil + ---@param on_complete fun() + local function start_install(server, requested_version, on_complete) + mutate_state(function(state) + state.servers[server.name].installer.is_queued = false + state.servers[server.name].installer.is_running = true + end) + + log.fmt_info("Starting install server_name=%s, requested_version=%s", server.name, requested_version or "") + + server:install_attached({ + requested_server_version = requested_version, + stdio_sink = { + stdout = function(chunk) + mutate_state(function(state) + local tailed_output = state.servers[server.name].installer.tailed_output + normalize_chunks_line_endings(chunk, tailed_output) + end) + end, + stderr = function(chunk) + mutate_state(function(state) + local tailed_output = state.servers[server.name].installer.tailed_output + normalize_chunks_line_endings(chunk, tailed_output) + end) + end, + }, + }, function(success) + log.fmt_info("Installation completed server_name=%s, success=%s", server.name, success) + mutate_state(function(state) + if success then + -- release stdout/err output table.. hopefully ¯\_(ツ)_/¯ + state.servers[server.name].installer.tailed_output = { "" } + end + state.servers[server.name].is_installed = success + state.servers[server.name].installer.is_running = false + state.servers[server.name].installer.has_run = true + if not state.expanded_server then + -- Only automatically expand the server upon installation if none is already expanded, for UX reasons + expand_server(server.name) + elseif state.expanded_server == server.name then + -- Refresh server metadata + async_populate_server_metadata(server.name) + end + end) + on_complete() + end) + end + + -- We have a queue because installers have a tendency to hog resources. + local job_pool = JobExecutionPool:new { + size = settings.current.max_concurrent_installers, + } + ---@param server Server + ---@param version string|nil + local function install_server(server, version) + log.fmt_debug("Queuing server=%s, version=%s for installation", server.name, version or "") + local server_state = get_state().servers[server.name] + if server_state and (server_state.installer.is_running or server_state.installer.is_queued) then + log.debug("Installer is already queued/running", server.name) + return + end + mutate_state(function(state) + -- reset state + state.servers[server.name] = create_initial_server_state(server) + state.servers[server.name].installer.is_queued = true + end) + job_pool:supply(function(cb) + start_install(server, version, cb) + end) + 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 + log.debug("Installer is already queued/running", server.name) + return + end + + local is_uninstalled, err = pcall(server.uninstall, server) + mutate_state(function(state) + -- reset state + state.servers[server.name] = create_initial_server_state(server) + if is_uninstalled then + state.servers[server.name].is_installed = false + end + state.servers[server.name].uninstaller.has_run = true + state.servers[server.name].uninstaller.error = err + end) + end + + local function mark_all_servers_uninstalled() + mutate_state(function(state) + for _, server_name in ipairs(lsp_servers.get_available_server_names()) do + if state.servers[server_name].is_installed then + state.servers[server_name].is_installed = false + state.servers[server_name].uninstaller.has_run = true + end + end + end) + end + + local make_animation = function(opts) + local animation_fn = opts[1] + local is_animating = false + local start_animation = function() + if is_animating then + return + end + local tick, start + + tick = function(current_tick) + animation_fn(current_tick) + if current_tick < opts.end_tick then + vim.defer_fn(function() + tick(current_tick + 1) + end, opts.delay_ms) + else + is_animating = false + if opts.iteration_delay_ms then + start(opts.iteration_delay_ms) + end + end + end + + start = function(delay_ms) + is_animating = true + if delay_ms then + vim.defer_fn(function() + tick(opts.start_tick) + end, delay_ms) + else + tick(opts.start_tick) + end + end + + start(opts.start_delay_ms) + + local function cancel() + is_animating = false + end + + return cancel + end + + return start_animation + end + + local start_help_command_animation + do + local help_command = ":help " + local help_command_len = #help_command + start_help_command_animation = make_animation { + function(tick) + mutate_state(function(state) + state.help_command_text = help_command:sub(help_command_len - tick, help_command_len) + end) + end, + start_tick = 0, + end_tick = help_command_len, + delay_ms = 80, + } + end + + local start_vader_saber_animation = make_animation { + function(tick) + mutate_state(function(state) + state.vader_saber_ticks = tick + end) + end, + start_tick = 0, + end_tick = 3, + delay_ms = 350, + iteration_delay_ms = 10000, + start_delay_ms = 1000, + } + + local function close() + if window then + window.close() end end - for i = 1, #rows do - local row = rows[i] - for j = 1, #row do - local col = row[j] - local content = col[1] - col[1] = content .. string.rep(" ", col_maxwidth[j] - vim.api.nvim_strwidth(content) + 1) -- +1 for default minimum padding + local has_opened = false + + local function identify_outdated_servers(servers) + -- Sort servers the same way as in the UI, gives a more structured impression + table.sort(servers, function(a, b) + return a.name < b.name + end) + if #servers > 0 then + mutate_state(function(state) + state.server_version_check_completed_percentage = 0 + end) end + outdated_servers.identify_outdated_servers(servers, function(check_result, progress) + mutate_state(function(state) + local completed_percentage = progress.completed / progress.total + state.server_version_check_completed_percentage = math.floor(completed_percentage * 100) + if completed_percentage == 1 then + vim.defer_fn(function() + mutate_state(function(state) + state.server_version_check_completed_percentage = nil + end) + end, 700) + end + + if check_result.success and check_result:has_outdated_packages() then + state.servers[check_result.server.name].metadata.outdated_packages = check_result.outdated_packages + end + end) + end) end - return M.HlTextNode(rows) + local function open() + local open_filetypes = {} + for _, open_bufnr in ipairs(vim.api.nvim_list_bufs()) do + table.insert(open_filetypes, vim.api.nvim_buf_get_option(open_bufnr, "filetype")) + end + + local prioritized_servers = {} + for _, filetype in ipairs(open_filetypes) do + if filetype_map[filetype] then + vim.list_extend(prioritized_servers, filetype_map[filetype]) + end + end + + mutate_state(function(state) + state.is_showing_help = false + state.prioritized_servers = functional.set_of(prioritized_servers) + end) + + if not has_opened then + -- Only do this automatically once - when opening the window the first time + vim.defer_fn(function() + identify_outdated_servers(lsp_servers.get_installed_servers()) + end, 100) + end + + window.open { + highlight_groups = { + "hi def LspInstallerHeader gui=bold guifg=#ebcb8b", + "hi def LspInstallerServerExpanded gui=italic", + "hi def LspInstallerHeading gui=bold", + "hi def LspInstallerGreen guifg=#a3be8c", + "hi def LspInstallerVaderSaber guifg=#f44747 gui=bold", + "hi def LspInstallerOrange ctermfg=222 guifg=#ebcb8b", + "hi def LspInstallerMuted guifg=#888888 ctermfg=144", + "hi def LspInstallerLabel gui=bold", + "hi def LspInstallerError ctermfg=203 guifg=#f44747", + "hi def LspInstallerHighlighted guifg=#56B6C2", + "hi def LspInstallerVersionCheckLoader gui=bold guifg=#222222 guibg=#888888", + "hi def LspInstallerVersionCheckLoaderDone gui=bold guifg=#222222 guibg=#a3be8c", + "hi def link LspInstallerLink LspInstallerHighlighted", + }, + effects = { + ["TOGGLE_HELP"] = function() + if not get_state().is_showing_help then + start_help_command_animation() + start_vader_saber_animation() + window.set_cursor { 1, 1 } + end + mutate_state(function(state) + state.is_showing_help = not state.is_showing_help + end) + end, + ["CLOSE_WINDOW"] = function() + close() + end, + ["CHECK_OUTDATED_SERVERS"] = function() + vim.schedule(function() + identify_outdated_servers(lsp_servers.get_installed_servers()) + end) + end, + ["CHECK_SERVER_VERSION"] = function(e) + local server_name = e.payload[1] + local ok, server = lsp_servers.get_server(server_name) + if ok then + identify_outdated_servers { server } + end + end, + ["TOGGLE_EXPAND_CURRENT_SETTINGS"] = function() + mutate_state(function(state) + state.is_current_settings_expanded = not state.is_current_settings_expanded + end) + end, + ["EXPAND_SERVER"] = function(e) + local server_name = e.payload[1] + expand_server(server_name) + end, + ["TOGGLE_SERVER_SETTINGS_SCHEMA"] = function(e) + local server_name = e.payload[1] + mutate_state(function(state) + state.servers[server_name].has_expanded_schema = + not state.servers[server_name].has_expanded_schema + end) + end, + ["TOGGLE_SERVER_SCHEMA_SETTING"] = function(e) + local server_name = e.payload.name + local key = e.payload.key + mutate_state(function(state) + state.servers[server_name].expanded_schema_properties[key] = + not state.servers[server_name].expanded_schema_properties[key] + end) + end, + ["INSTALL_SERVER"] = function(e) + local server_name = e.payload[1] + local ok, server = lsp_servers.get_server(server_name) + if ok then + install_server(server, nil) + end + end, + ["UPDATE_ALL_SERVERS"] = function() + local installed_servers = lsp_servers.get_installed_servers() + local state = get_state() + local outdated_servers = vim.tbl_filter(function(server) + return #state.servers[server.name].metadata.outdated_packages > 0 + end, installed_servers) + -- Install servers that are identified as outdated, otherwise update all installed servers. + local servers_to_update = #outdated_servers > 0 and outdated_servers or installed_servers + for _, server in ipairs(servers_to_update) do + install_server(server, nil) + end + end, + ["UNINSTALL_SERVER"] = function(e) + local server_name = e.payload[1] + local ok, server = lsp_servers.get_server(server_name) + if ok then + uninstall_server(server) + end + end, + ["REPLACE_SERVER"] = function(e) + local old_server_name, new_server_name = e.payload[1], e.payload[2] + local old_ok, old_server = lsp_servers.get_server(old_server_name) + local new_ok, new_server = lsp_servers.get_server(new_server_name) + if old_ok and new_ok then + uninstall_server(old_server) + install_server(new_server) + end + end, + }, + } + has_opened = true + end + + return { + open = open, + close = close, + install_server = install_server, + uninstall_server = uninstall_server, + mark_all_servers_uninstalled = mark_all_servers_uninstalled, + } end -return M +local win +return function() + if win then + return win + end + win = init(lsp_servers.get_available_servers()) + return win +end diff --git a/lua/nvim-lsp-installer/ui/status-win/server_hints.lua b/lua/nvim-lsp-installer/ui/server_hints.lua index daf5e9b6..daf5e9b6 100644 --- a/lua/nvim-lsp-installer/ui/status-win/server_hints.lua +++ b/lua/nvim-lsp-installer/ui/server_hints.lua diff --git a/lua/nvim-lsp-installer/ui/state.lua b/lua/nvim-lsp-installer/ui/state.lua deleted file mode 100644 index 9d7bcdda..00000000 --- a/lua/nvim-lsp-installer/ui/state.lua +++ /dev/null @@ -1,24 +0,0 @@ -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 - subscriber(state) - end - end, function() - return state - end, function(val) - has_unsubscribed = val - end -end - -return M diff --git a/lua/nvim-lsp-installer/ui/status-win/init.lua b/lua/nvim-lsp-installer/ui/status-win/init.lua deleted file mode 100644 index 689e3710..00000000 --- a/lua/nvim-lsp-installer/ui/status-win/init.lua +++ /dev/null @@ -1,1056 +0,0 @@ -local a = require "nvim-lsp-installer.core.async" -local Ui = require "nvim-lsp-installer.ui" -local fs = require "nvim-lsp-installer.fs" -local log = require "nvim-lsp-installer.log" -local Data = require "nvim-lsp-installer.data" -local display = require "nvim-lsp-installer.ui.display" -local settings = require "nvim-lsp-installer.settings" -local lsp_servers = require "nvim-lsp-installer.servers" -local JobExecutionPool = require "nvim-lsp-installer.jobs.pool" -local outdated_servers = require "nvim-lsp-installer.jobs.outdated-servers" -local version_check = require "nvim-lsp-installer.jobs.version-check" -local ServerHints = require "nvim-lsp-installer.ui.status-win.server_hints" -local ServerSettingsSchema = require "nvim-lsp-installer.ui.status-win.components.settings-schema" - -local HELP_KEYMAP = "?" -local CLOSE_WINDOW_KEYMAP_1 = "<Esc>" -local CLOSE_WINDOW_KEYMAP_2 = "q" - ----@param props {title: string, subtitle: string[][], count: number} -local function ServerGroupHeading(props) - local line = { - { props.title, props.highlight or "LspInstallerHeading" }, - { " (" .. props.count .. ") ", "Comment" }, - } - if props.subtitle then - vim.list_extend(line, props.subtitle) - end - return Ui.HlTextNode { line } -end - -local function Indent(children) - return Ui.CascadingStyleNode({ "INDENT" }, children) -end - -local create_vader = Data.memoize( - ---@param saber_ticks number - function(saber_ticks) - -- stylua: ignore start - return { - { { [[ _________________________________________________________________________________________ ]], "LspInstallerMuted" } }, - { { [[ < Help sponsor nvim-lsp-installer development! ]], "LspInstallerMuted" }, { "https://github.com/sponsors/williamboman", "LspInstallerHighlighted"}, {[[ > ]], "LspInstallerMuted" } }, - { { [[ < Help sponsor neovim development! ]], "LspInstallerMuted" }, { "https://github.com/sponsors/neovim", "LspInstallerHighlighted"}, {[[ > ]], "LspInstallerMuted" } }, - { { [[ ----------------------------------------------------------------------------------------- ]], "LspInstallerMuted" } }, - { { [[ ]], ""}, {[[\]], saber_ticks >= 3 and "LspInstallerVaderSaber" or "LspInstallerMuted"}, {[[ ,-^-. ]], "LspInstallerMuted" } }, - { { [[ ]], ""}, {[[\]], saber_ticks >= 2 and "LspInstallerVaderSaber" or "LspInstallerMuted"}, {[[ !oYo! ]], "LspInstallerMuted" } }, - { { [[ ]], ""}, {[[\]], saber_ticks >= 1 and "LspInstallerVaderSaber" or "LspInstallerMuted"}, {[[ /./=\.\______ ]], "LspInstallerMuted" } }, - { { [[ ## )\/\ ]], "LspInstallerMuted" } }, - { { [[ ||-----w|| ]], "LspInstallerMuted" } }, - { { [[ || || ]], "LspInstallerMuted" } }, - { { [[ ]], "LspInstallerMuted" } }, - { { [[ Cowth Vader (alleged Neovim user) ]], "LspInstallerMuted" } }, - { { [[ ]], "LspInstallerMuted" } }, - } - -- 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 }, - { "Toggle server info", settings.current.ui.keymaps.toggle_server_expand }, - { "Update server", settings.current.ui.keymaps.update_server }, - { "Update all installed servers", settings.current.ui.keymaps.update_all_servers }, - { "Check for new server version", settings.current.ui.keymaps.check_server_version }, - { "Check for new versions (all servers)", settings.current.ui.keymaps.check_outdated_servers }, - { "Uninstall server", settings.current.ui.keymaps.uninstall_server }, - { "Install server", settings.current.ui.keymaps.install_server }, - { "Close window", CLOSE_WINDOW_KEYMAP_1 }, - { "Close window", CLOSE_WINDOW_KEYMAP_2 }, - } - - local very_reasonable_cow = create_vader(vader_saber_ticks) - - return Ui.Node { - Ui.EmptyLine(), - Ui.HlTextNode { - { { "Installer log: ", "LspInstallerMuted" }, { log.outfile, "" } }, - }, - Ui.EmptyLine(), - Ui.Table(vim.list_extend( - { - { - { "Keyboard shortcuts", "LspInstallerLabel" }, - }, - }, - Data.list_map(function(keymap_tuple) - return { { keymap_tuple[1], "LspInstallerMuted" }, { keymap_tuple[2], "LspInstallerHighlighted" } } - end, keymap_tuples) - )), - Ui.EmptyLine(), - Ui.HlTextNode { - { { "Problems installing/uninstalling servers", "LspInstallerLabel" } }, - { - { - "Make sure you meet the minimum requirements to install servers. For debugging, refer to:", - "LspInstallerMuted", - }, - }, - }, - Indent { - Ui.HlTextNode { - { - { ":help nvim-lsp-installer-debugging", "LspInstallerHighlighted" }, - }, - { - { ":checkhealth nvim-lsp-installer", "LspInstallerHighlighted" }, - }, - }, - }, - Ui.EmptyLine(), - Ui.HlTextNode { - { { "Problems with server functionality", "LspInstallerLabel" } }, - { - { - "Please refer to each language server's own homepage for further assistance.", - "LspInstallerMuted", - }, - }, - }, - Ui.EmptyLine(), - Ui.HlTextNode { - { { "Missing a server?", "LspInstallerLabel" } }, - { - { - "Create an issue at ", - "LspInstallerMuted", - }, - { - "https://github.com/williamboman/nvim-lsp-installer/issues/new/choose", - "LspInstallerHighlighted", - }, - }, - }, - Ui.EmptyLine(), - Ui.HlTextNode { - { { "How do I customize server settings?", "LspInstallerLabel" } }, - { - { "For information on how to customize a server's settings, see ", "LspInstallerMuted" }, - { ":help lspconfig-setup", "LspInstallerHighlighted" }, - }, - }, - Ui.EmptyLine(), - Ui.HlTextNode { - { - { - ("%s Current settings"):format(is_current_settings_expanded and "↓" or "→"), - "LspInstallerLabel", - }, - { " :help nvim-lsp-installer-settings", "LspInstallerHighlighted" }, - }, - }, - Ui.Keybind("<CR>", "TOGGLE_EXPAND_CURRENT_SETTINGS", nil), - Ui.When(is_current_settings_expanded, function() - local settings_split_by_newline = vim.split(vim.inspect(settings.current), "\n") - local current_settings = Data.list_map(function(line) - return { { line, "LspInstallerMuted" } } - end, settings_split_by_newline) - return Ui.HlTextNode(current_settings) - end), - Ui.EmptyLine(), - Ui.HlTextNode(very_reasonable_cow), - } -end - ----@param props {is_showing_help: boolean, help_command_text: string} -local function Header(props) - return Ui.CascadingStyleNode({ "CENTERED" }, { - Ui.HlTextNode { - { - { props.is_showing_help and props.help_command_text or "", "LspInstallerHighlighted" }, - { - props.is_showing_help and "nvim-lsp-installer" .. (" "):rep(#props.help_command_text) - or "nvim-lsp-installer", - props.is_showing_help and "LspInstallerHighlighted" or "LspInstallerHeader", - }, - }, - { - { props.is_showing_help and " press " or "press ", "LspInstallerMuted" }, - { "?", props.is_showing_help and "LspInstallerOrange" or "LspInstallerHighlighted" }, - { props.is_showing_help and " for server list" or " for help", "LspInstallerMuted" }, - }, - { - { "https://github.com/williamboman/nvim-lsp-installer", "Comment" }, - }, - }, - }) -end - ----@param time number -local function format_time(time) - return os.date("%d %b %Y %H:%M", time) -end - ----@param outdated_packages OutdatedPackage[] ----@return string -local function format_new_package_versions(outdated_packages) - local result = {} - if #outdated_packages == 1 then - return outdated_packages[1].latest_version - end - for _, outdated_package in ipairs(outdated_packages) do - result[#result + 1] = ("%s@%s"):format(outdated_package.name, outdated_package.latest_version) - end - return table.concat(result, ", ") -end - ----@param server ServerState -local function ServerMetadata(server) - return Ui.Node(Data.list_not_nil( - Data.lazy_when(server.is_installed and server.deprecated, function() - return Ui.Node(Data.list_not_nil( - Ui.HlTextNode { server.deprecated.message, "Comment" }, - Data.lazy_when(server.deprecated.replace_with, function() - return Ui.Node { - Ui.HlTextNode { - { - { "Replace with: ", "LspInstallerMuted" }, - { server.deprecated.replace_with, "LspInstallerHighlighted" }, - }, - }, - Ui.Keybind("<CR>", "REPLACE_SERVER", { server.name, server.deprecated.replace_with }), - Ui.EmptyLine(), - } - end) - )) - end), - Ui.Table(Data.list_not_nil( - Data.lazy_when(server.is_installed, function() - return { - { "version", "LspInstallerMuted" }, - server.installed_version_err and { - "Unable to detect version.", - "LspInstallerMuted", - } or { server.installed_version or "Loading...", "" }, - } - end), - Data.lazy_when(#server.metadata.outdated_packages > 0, function() - return { - { "latest version", "LspInstallerGreen" }, - { - format_new_package_versions(server.metadata.outdated_packages), - "LspInstallerGreen", - }, - } - end), - Data.lazy_when(server.metadata.install_timestamp_seconds, function() - return { - { "installed", "LspInstallerMuted" }, - { format_time(server.metadata.install_timestamp_seconds), "" }, - } - end), - Data.when(not server.is_installed, { - { "filetypes", "LspInstallerMuted" }, - { server.metadata.filetypes, "" }, - }), - Data.when(server.is_installed, { - { "path", "LspInstallerMuted" }, - { server.metadata.install_dir, "String" }, - }), - { - { "homepage", "LspInstallerMuted" }, - server.metadata.homepage and { server.metadata.homepage, "LspInstallerLink" } or { - "-", - "LspInstallerMuted", - }, - } - )), - Ui.When(server.schema, function() - return Ui.Node { - Ui.EmptyLine(), - Ui.HlTextNode { - { - { - ("%s Server configuration schema"):format(server.has_expanded_schema and "↓" or "→"), - "LspInstallerLabel", - }, - { - (" (press enter to %s)"):format(server.has_expanded_schema and "collapse" or "expand"), - "Comment", - }, - }, - }, - Ui.Keybind("<CR>", "TOGGLE_SERVER_SETTINGS_SCHEMA", { server.name }), - Ui.When(server.has_expanded_schema, function() - return Indent { - Ui.HlTextNode { - { - { - "This is a read-only representation of the settings this server accepts. Note that some settings might not apply to neovim.", - "LspInstallerMuted", - }, - }, - { - { "For information on how to customize these settings, see ", "LspInstallerMuted" }, - { ":help lspconfig-setup", "LspInstallerHighlighted" }, - }, - }, - Ui.EmptyLine(), - ServerSettingsSchema(server, server.schema), - } - end), - Ui.EmptyLine(), - } - end) - )) -end - ----@param servers ServerState[] ----@param props ServerGroupProps -local function InstalledServers(servers, props) - return Ui.Node(Data.list_map( - ---@param server ServerState - function(server) - local is_expanded = props.expanded_server == server.name - return Ui.Node { - Ui.HlTextNode { - Data.list_not_nil( - { settings.current.ui.icons.server_installed, "LspInstallerGreen" }, - { " " .. server.name .. " ", "" }, - { server.hints, "Comment" }, - Data.when(server.deprecated, { " deprecated", "LspInstallerOrange" }), - Data.when( - #server.metadata.outdated_packages > 0 and not is_expanded, - { " new version available", "LspInstallerGreen" } - ) - ), - }, - Ui.Keybind(settings.current.ui.keymaps.toggle_server_expand, "EXPAND_SERVER", { server.name }), - Ui.Keybind(settings.current.ui.keymaps.update_server, "INSTALL_SERVER", { server.name }), - Ui.Keybind(settings.current.ui.keymaps.check_server_version, "CHECK_SERVER_VERSION", { server.name }), - Ui.Keybind(settings.current.ui.keymaps.uninstall_server, "UNINSTALL_SERVER", { server.name }), - Ui.When(is_expanded, function() - return Indent { - ServerMetadata(server), - } - end), - } - 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] - if #line > 0 then - return line - end - end - return "" -end - ----@param servers ServerState[] -local function PendingServers(servers) - 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 { - Ui.HlTextNode { - Data.list_not_nil( - { - settings.current.ui.icons.server_pending, - has_failed and "LspInstallerError" or "LspInstallerOrange", - }, - { " " .. server.name, server.installer.is_running and "" or "LspInstallerMuted" }, - { " " .. note, "Comment" }, - Data.when(not has_failed, { - (" " .. get_last_non_empty_line(server.installer.tailed_output)), - "Comment", - }) - ), - }, - Ui.Keybind(settings.current.ui.keymaps.install_server, "INSTALL_SERVER", { server.name }), - Ui.When(has_failed, function() - return Indent { Indent { TailedOutput(server) } } - end), - Ui.When( - server.uninstaller.error, - Indent { - Ui.HlTextNode { server.uninstaller.error, "Comment" }, - } - ), - } - end, servers)) -end - ----@param servers ServerState[] ----@param props ServerGroupProps -local function UninstalledServers(servers, props) - return Ui.Node(Data.list_map(function(_server) - ---@type ServerState - local server = _server - local is_prioritized = props.prioritized_servers[server.name] - local is_expanded = props.expanded_server == server.name - return Ui.Node { - Ui.HlTextNode { - Data.list_not_nil( - { - settings.current.ui.icons.server_uninstalled, - is_prioritized and "LspInstallerHighlighted" or "LspInstallerMuted", - }, - { " " .. server.name .. " ", "LspInstallerMuted" }, - { server.hints, "Comment" }, - Data.when(server.uninstaller.has_run, { " (uninstalled) ", "Comment" }), - Data.when(server.deprecated, { "deprecated ", "LspInstallerOrange" }) - ), - }, - Ui.Keybind(settings.current.ui.keymaps.toggle_server_expand, "EXPAND_SERVER", { server.name }), - Ui.Keybind(settings.current.ui.keymaps.install_server, "INSTALL_SERVER", { server.name }), - Ui.When(is_expanded, function() - return Indent { - ServerMetadata(server), - } - end), - } - end, servers)) -end - ----@alias ServerGroupProps {title: string, subtitle: string|nil, 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 - for i = 1, #chunks do - local servers = chunks[i] - total_server_count = total_server_count + #servers - end - - return Ui.When(total_server_count > 0 or not props.hide_when_empty, function() - return Ui.Node { - Ui.EmptyLine(), - ServerGroupHeading { - title = props.title, - subtitle = props.subtitle, - count = total_server_count, - }, - Indent(Data.list_map(function(servers) - return props.renderer(servers, props) - end, props.servers)), - } - end) -end - ----@param state StatusWinState -local function Servers(state) - local grouped_servers = { - installed = {}, - queued = {}, - session_installed = {}, - uninstall_failed = {}, - installing = {}, - install_failed = {}, - uninstalled_prioritized = {}, - uninstalled = {}, - session_uninstalled = {}, - } - - local servers, server_name_order, prioritized_servers, expanded_server = - state.servers, state.server_name_order, state.prioritized_servers, state.expanded_server - - -- giggity - for _, server_name in ipairs(server_name_order) do - local server = servers[server_name] - if server.installer.is_running then - grouped_servers.installing[#grouped_servers.installing + 1] = server - elseif server.installer.is_queued then - grouped_servers.queued[#grouped_servers.queued + 1] = server - elseif server.uninstaller.has_run then - if server.uninstaller.error then - grouped_servers.uninstall_failed[#grouped_servers.uninstall_failed + 1] = server - else - grouped_servers.session_uninstalled[#grouped_servers.session_uninstalled + 1] = server - end - elseif server.is_installed then - if server.installer.has_run then - grouped_servers.session_installed[#grouped_servers.session_installed + 1] = server - else - grouped_servers.installed[#grouped_servers.installed + 1] = server - end - elseif server.installer.has_run then - grouped_servers.install_failed[#grouped_servers.install_failed + 1] = server - else - if prioritized_servers[server.name] then - grouped_servers.uninstalled_prioritized[#grouped_servers.uninstalled_prioritized + 1] = server - else - grouped_servers.uninstalled[#grouped_servers.uninstalled + 1] = server - end - end - end - - return Ui.Node { - ServerGroup { - title = "Installed servers", - subtitle = state.server_version_check_completed_percentage ~= nil and { - { - "checking for new versions ", - "Comment", - }, - { - state.server_version_check_completed_percentage .. "%", - state.server_version_check_completed_percentage == 100 and "LspInstallerVersionCheckLoaderDone" - or "LspInstallerVersionCheckLoader", - }, - { - string.rep(" ", math.floor(state.server_version_check_completed_percentage / 5)), - state.server_version_check_completed_percentage == 100 and "LspInstallerVersionCheckLoaderDone" - or "LspInstallerVersionCheckLoader", - }, - }, - renderer = InstalledServers, - servers = { grouped_servers.session_installed, grouped_servers.installed }, - expanded_server = expanded_server, - }, - ServerGroup { - title = "Pending servers", - hide_when_empty = true, - renderer = PendingServers, - servers = { - grouped_servers.installing, - grouped_servers.queued, - grouped_servers.install_failed, - grouped_servers.uninstall_failed, - }, - expanded_server = expanded_server, - }, - ServerGroup { - title = "Available servers", - renderer = UninstalledServers, - servers = { - grouped_servers.session_uninstalled, - grouped_servers.uninstalled_prioritized, - grouped_servers.uninstalled, - }, - expanded_server = expanded_server, - prioritized_servers = prioritized_servers, - }, - } -end - ----@param server Server -local function create_initial_server_state(server) - ---@class ServerState - local server_state = { - name = server.name, - is_installed = server:is_installed(), - deprecated = server.deprecated, - hints = tostring(ServerHints.new(server)), - expanded_schema_properties = {}, - has_expanded_schema = false, - installed_version = nil, -- lazy - installed_version_err = nil, -- lazy - ---@type table - schema = nil, -- lazy - metadata = { - homepage = server.homepage, - ---@type number - install_timestamp_seconds = nil, -- lazy - install_dir = vim.fn.fnamemodify(server.root_dir, ":~"), - filetypes = table.concat(server:get_supported_filetypes(), ", "), - ---@type OutdatedPackage[] - outdated_packages = {}, - }, - installer = { - is_queued = false, - is_running = false, - has_run = false, - tailed_output = { "" }, - }, - uninstaller = { - has_run = false, - error = nil, - }, - } - return server_state -end - -local function normalize_chunks_line_endings(chunk, dest) - local chunk_lines = vim.split(chunk, "\n") - dest[#dest] = dest[#dest] .. chunk_lines[1] - for i = 2, #chunk_lines do - dest[#dest + 1] = chunk_lines[i] - end -end - -local function init(all_servers) - local filetype_map = require "nvim-lsp-installer._generated.filetype_map" - local window = display.new_view_only_win "LSP servers" - - 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), - Ui.Keybind(settings.current.ui.keymaps.check_outdated_servers, "CHECK_OUTDATED_SERVERS", nil, true), - Ui.Keybind(settings.current.ui.keymaps.update_all_servers, "UPDATE_ALL_SERVERS", 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) - end), - } - end - ) - - ---@type table<string, ServerState> - local servers = {} - ---@type string[] - local server_name_order = {} - for i = 1, #all_servers do - local server = all_servers[i] - servers[server.name] = create_initial_server_state(server) - server_name_order[#server_name_order + 1] = server.name - end - - table.sort(server_name_order) - - ---@class StatusWinState - ---@field prioritized_servers string[] - local initial_state = { - server_name_order = server_name_order, - servers = servers, - server_version_check_completed_percentage = nil, - 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 - - local async_populate_server_metadata = a.scope(function(server_name) - a.scheduler() - local ok, server = lsp_servers.get_server(server_name) - if not ok then - return log.warn("Unable to get server when populating metadata.", server_name) - end - local fstat_ok, fstat = pcall(fs.fstat, server.root_dir) - mutate_state(function(state) - if fstat_ok then - state.servers[server.name].metadata.install_timestamp_seconds = fstat.mtime.sec - end - state.servers[server.name].schema = server:get_settings_schema() - end) - local version = version_check.check_server_version(server) - mutate_state(function(state) - if version:is_success() then - state.servers[server.name].installed_version = version:get_or_nil() - state.servers[server.name].installed_version_err = nil - else - state.servers[server.name].installed_version_err = true - end - end) - end) - - ---@param server_name string - local function expand_server(server_name) - mutate_state(function(state) - local should_expand = state.expanded_server ~= server_name - state.expanded_server = should_expand and server_name or nil - if should_expand then - async_populate_server_metadata(server_name) - end - end) - end - - ---@param server Server - ---@param requested_version string|nil - ---@param on_complete fun() - local function start_install(server, requested_version, on_complete) - mutate_state(function(state) - state.servers[server.name].installer.is_queued = false - state.servers[server.name].installer.is_running = true - end) - - log.fmt_info("Starting install server_name=%s, requested_version=%s", server.name, requested_version or "") - - server:install_attached({ - requested_server_version = requested_version, - stdio_sink = { - stdout = function(chunk) - mutate_state(function(state) - local tailed_output = state.servers[server.name].installer.tailed_output - normalize_chunks_line_endings(chunk, tailed_output) - end) - end, - stderr = function(chunk) - mutate_state(function(state) - local tailed_output = state.servers[server.name].installer.tailed_output - normalize_chunks_line_endings(chunk, tailed_output) - end) - end, - }, - }, function(success) - log.fmt_info("Installation completed server_name=%s, success=%s", server.name, success) - mutate_state(function(state) - if success then - -- release stdout/err output table.. hopefully ¯\_(ツ)_/¯ - state.servers[server.name].installer.tailed_output = { "" } - end - state.servers[server.name].is_installed = success - state.servers[server.name].installer.is_running = false - state.servers[server.name].installer.has_run = true - if not state.expanded_server then - -- Only automatically expand the server upon installation if none is already expanded, for UX reasons - expand_server(server.name) - elseif state.expanded_server == server.name then - -- Refresh server metadata - async_populate_server_metadata(server.name) - end - end) - on_complete() - end) - end - - -- We have a queue because installers have a tendency to hog resources. - local job_pool = JobExecutionPool:new { - size = settings.current.max_concurrent_installers, - } - ---@param server Server - ---@param version string|nil - local function install_server(server, version) - log.fmt_debug("Queuing server=%s, version=%s for installation", server.name, version or "") - local server_state = get_state().servers[server.name] - if server_state and (server_state.installer.is_running or server_state.installer.is_queued) then - log.debug("Installer is already queued/running", server.name) - return - end - mutate_state(function(state) - -- reset state - state.servers[server.name] = create_initial_server_state(server) - state.servers[server.name].installer.is_queued = true - end) - job_pool:supply(function(cb) - start_install(server, version, cb) - end) - 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 - log.debug("Installer is already queued/running", server.name) - return - end - - local is_uninstalled, err = pcall(server.uninstall, server) - mutate_state(function(state) - -- reset state - state.servers[server.name] = create_initial_server_state(server) - if is_uninstalled then - state.servers[server.name].is_installed = false - end - state.servers[server.name].uninstaller.has_run = true - state.servers[server.name].uninstaller.error = err - end) - end - - local function mark_all_servers_uninstalled() - mutate_state(function(state) - for _, server_name in ipairs(lsp_servers.get_available_server_names()) do - if state.servers[server_name].is_installed then - state.servers[server_name].is_installed = false - state.servers[server_name].uninstaller.has_run = true - end - end - end) - end - - local make_animation = function(opts) - local animation_fn = opts[1] - local is_animating = false - local start_animation = function() - if is_animating then - return - end - local tick, start - - tick = function(current_tick) - animation_fn(current_tick) - if current_tick < opts.end_tick then - vim.defer_fn(function() - tick(current_tick + 1) - end, opts.delay_ms) - else - is_animating = false - if opts.iteration_delay_ms then - start(opts.iteration_delay_ms) - end - end - end - - start = function(delay_ms) - is_animating = true - if delay_ms then - vim.defer_fn(function() - tick(opts.start_tick) - end, delay_ms) - else - tick(opts.start_tick) - end - end - - start(opts.start_delay_ms) - - local function cancel() - is_animating = false - end - - return cancel - end - - return start_animation - end - - local start_help_command_animation - do - local help_command = ":help " - local help_command_len = #help_command - start_help_command_animation = make_animation { - function(tick) - mutate_state(function(state) - state.help_command_text = help_command:sub(help_command_len - tick, help_command_len) - end) - end, - start_tick = 0, - end_tick = help_command_len, - delay_ms = 80, - } - end - - local start_vader_saber_animation = make_animation { - function(tick) - mutate_state(function(state) - state.vader_saber_ticks = tick - end) - end, - start_tick = 0, - end_tick = 3, - delay_ms = 350, - iteration_delay_ms = 10000, - start_delay_ms = 1000, - } - - local function close() - if window then - window.close() - end - end - - local has_opened = false - - local function identify_outdated_servers(servers) - -- Sort servers the same way as in the UI, gives a more structured impression - table.sort(servers, function(a, b) - return a.name < b.name - end) - if #servers > 0 then - mutate_state(function(state) - state.server_version_check_completed_percentage = 0 - end) - end - outdated_servers.identify_outdated_servers(servers, function(check_result, progress) - mutate_state(function(state) - local completed_percentage = progress.completed / progress.total - state.server_version_check_completed_percentage = math.floor(completed_percentage * 100) - if completed_percentage == 1 then - vim.defer_fn(function() - mutate_state(function(state) - state.server_version_check_completed_percentage = nil - end) - end, 700) - end - - if check_result.success and check_result:has_outdated_packages() then - state.servers[check_result.server.name].metadata.outdated_packages = check_result.outdated_packages - end - end) - end) - end - - local function open() - local open_filetypes = {} - for _, open_bufnr in ipairs(vim.api.nvim_list_bufs()) do - table.insert(open_filetypes, vim.api.nvim_buf_get_option(open_bufnr, "filetype")) - end - - local prioritized_servers = {} - for _, filetype in ipairs(open_filetypes) do - if filetype_map[filetype] then - vim.list_extend(prioritized_servers, filetype_map[filetype]) - end - end - - mutate_state(function(state) - state.is_showing_help = false - state.prioritized_servers = Data.set_of(prioritized_servers) - end) - - if not has_opened then - -- Only do this automatically once - when opening the window the first time - vim.defer_fn(function() - identify_outdated_servers(lsp_servers.get_installed_servers()) - end, 100) - end - - window.open { - highlight_groups = { - "hi def LspInstallerHeader gui=bold guifg=#ebcb8b", - "hi def LspInstallerServerExpanded gui=italic", - "hi def LspInstallerHeading gui=bold", - "hi def LspInstallerGreen guifg=#a3be8c", - "hi def LspInstallerVaderSaber guifg=#f44747 gui=bold", - "hi def LspInstallerOrange ctermfg=222 guifg=#ebcb8b", - "hi def LspInstallerMuted guifg=#888888 ctermfg=144", - "hi def LspInstallerLabel gui=bold", - "hi def LspInstallerError ctermfg=203 guifg=#f44747", - "hi def LspInstallerHighlighted guifg=#56B6C2", - "hi def LspInstallerVersionCheckLoader gui=bold guifg=#222222 guibg=#888888", - "hi def LspInstallerVersionCheckLoaderDone gui=bold guifg=#222222 guibg=#a3be8c", - "hi def link LspInstallerLink LspInstallerHighlighted", - }, - effects = { - ["TOGGLE_HELP"] = function() - if not get_state().is_showing_help then - start_help_command_animation() - start_vader_saber_animation() - window.set_cursor { 1, 1 } - end - mutate_state(function(state) - state.is_showing_help = not state.is_showing_help - end) - end, - ["CLOSE_WINDOW"] = function() - close() - end, - ["CHECK_OUTDATED_SERVERS"] = function() - vim.schedule(function() - identify_outdated_servers(lsp_servers.get_installed_servers()) - end) - end, - ["CHECK_SERVER_VERSION"] = function(e) - local server_name = e.payload[1] - local ok, server = lsp_servers.get_server(server_name) - if ok then - identify_outdated_servers { server } - end - end, - ["TOGGLE_EXPAND_CURRENT_SETTINGS"] = function() - mutate_state(function(state) - state.is_current_settings_expanded = not state.is_current_settings_expanded - end) - end, - ["EXPAND_SERVER"] = function(e) - local server_name = e.payload[1] - expand_server(server_name) - end, - ["TOGGLE_SERVER_SETTINGS_SCHEMA"] = function(e) - local server_name = e.payload[1] - mutate_state(function(state) - state.servers[server_name].has_expanded_schema = - not state.servers[server_name].has_expanded_schema - end) - end, - ["TOGGLE_SERVER_SCHEMA_SETTING"] = function(e) - local server_name = e.payload.name - local key = e.payload.key - mutate_state(function(state) - state.servers[server_name].expanded_schema_properties[key] = - not state.servers[server_name].expanded_schema_properties[key] - end) - end, - ["INSTALL_SERVER"] = function(e) - local server_name = e.payload[1] - local ok, server = lsp_servers.get_server(server_name) - if ok then - install_server(server, nil) - end - end, - ["UPDATE_ALL_SERVERS"] = function() - local installed_servers = lsp_servers.get_installed_servers() - local state = get_state() - local outdated_servers = vim.tbl_filter(function(server) - return #state.servers[server.name].metadata.outdated_packages > 0 - end, installed_servers) - -- Install servers that are identified as outdated, otherwise update all installed servers. - local servers_to_update = #outdated_servers > 0 and outdated_servers or installed_servers - for _, server in ipairs(servers_to_update) do - install_server(server, nil) - end - end, - ["UNINSTALL_SERVER"] = function(e) - local server_name = e.payload[1] - local ok, server = lsp_servers.get_server(server_name) - if ok then - uninstall_server(server) - end - end, - ["REPLACE_SERVER"] = function(e) - local old_server_name, new_server_name = e.payload[1], e.payload[2] - local old_ok, old_server = lsp_servers.get_server(old_server_name) - local new_ok, new_server = lsp_servers.get_server(new_server_name) - if old_ok and new_ok then - uninstall_server(old_server) - install_server(new_server) - end - end, - }, - } - has_opened = true - end - - return { - open = open, - close = close, - install_server = install_server, - uninstall_server = uninstall_server, - mark_all_servers_uninstalled = mark_all_servers_uninstalled, - } -end - -local win -return function() - if win then - return win - end - win = init(lsp_servers.get_available_servers()) - return win -end |
