diff options
Diffstat (limited to 'lua')
23 files changed, 538 insertions, 174 deletions
diff --git a/lua/nvim-lsp-installer.lua b/lua/nvim-lsp-installer.lua index 81899a2c..6b2f4a06 100644 --- a/lua/nvim-lsp-installer.lua +++ b/lua/nvim-lsp-installer.lua @@ -11,12 +11,16 @@ local M = {} M.settings = settings.set +--- Opens the status window. function M.display() status_win().open() end -function M.install(server_tuple) - local server_name, version = unpack(servers.parse_server_tuple(server_tuple)) +--- Queues a server to be installed. Will also open the status window. +--- Use the .on_server_ready(cb) function to register a handler to be executed when a server is ready to be set up. +---@param server_identifier string @The server to install. This can also include a requested version, for example "rust_analyzer@nightly". +function M.install(server_identifier) + local server_name, version = unpack(servers.parse_server_identifier(server_identifier)) local ok, server = servers.get_server(server_name) if not ok then return notify(("Unable to find LSP server %s.\n\n%s"):format(server_name, server), vim.log.levels.ERROR) @@ -25,6 +29,8 @@ function M.install(server_tuple) status_win().open() end +--- Queues a server to be uninstalled. Will also open the status window. +---@param server_name string The server to uninstall. function M.uninstall(server_name) local ok, server = servers.get_server(server_name) if not ok then @@ -34,6 +40,7 @@ function M.uninstall(server_name) status_win().open() end +--- Queues all servers to be uninstalled. Will also open the status window. function M.uninstall_all() local choice = vim.fn.confirm( ("This will uninstall all servers currently installed at %q. Continue?"):format( @@ -65,6 +72,7 @@ function M.uninstall_all() end end +---@param cb fun(server: Server) @Callback to be executed whenever a server is ready to be set up. function M.on_server_ready(cb) dispatcher.register_server_ready_callback(cb) vim.schedule(function() diff --git a/lua/nvim-lsp-installer/data.lua b/lua/nvim-lsp-installer/data.lua index d9764bee..dc14ef01 100644 --- a/lua/nvim-lsp-installer/data.lua +++ b/lua/nvim-lsp-installer/data.lua @@ -1,5 +1,8 @@ local Data = {} +---@generic T : string +---@param values T[] +---@return table<T, T> function Data.enum(values) local result = {} for i = 1, #values do @@ -9,6 +12,9 @@ function Data.enum(values) return result end +---@generic T +---@param list T[] +---@return table<T, boolean> function Data.set_of(list) local set = {} for i = 1, #list do @@ -17,6 +23,9 @@ function Data.set_of(list) return set end +---@generic T +---@param list T[] +---@return T[] function Data.list_reverse(list) local result = {} for i = #list, 1, -1 do @@ -25,6 +34,10 @@ function Data.list_reverse(list) return result end +---@generic T, U +---@param fn fun(item: T): U +---@param list T[] +---@return U[] function Data.list_map(fn, list) local result = {} for i = 1, #list do @@ -70,6 +83,9 @@ function Data.coalesce(...) end end +---@generic T +---@param list T[] +---@return T[] @A shallow copy of the list. function Data.list_copy(list) local result = {} for i = 1, #list do @@ -78,6 +94,10 @@ function Data.list_copy(list) return result end +---@generic T +---@param list T[] +---@param predicate fun(item: T): boolean +---@return T | nil function Data.list_find_first(list, predicate) local result for i = 1, #list do @@ -89,6 +109,10 @@ function Data.list_find_first(list, predicate) return result end +---@generic T +---@param list T[] +---@param predicate fun(item: T): boolean +---@return boolean function Data.list_any(list, predicate) for i = 1, #list do if predicate(list[i]) then @@ -98,6 +122,8 @@ function Data.list_any(list, predicate) return false end +---@param data string @The JSON data to decode/deserialize. +---@return table function Data.json_decode(data) if vim.json and vim.json.decode then return vim.json.decode(data) @@ -106,6 +132,10 @@ function Data.json_decode(data) end end +---@generic T : fun(...) +---@param fn T +---@param cache_key_generator fun(...): string | nil +---@return T function Data.memoize(fn, cache_key_generator) cache_key_generator = cache_key_generator or function(a) return a diff --git a/lua/nvim-lsp-installer/installers/composer.lua b/lua/nvim-lsp-installer/installers/composer.lua index d206cc46..ebe591d4 100644 --- a/lua/nvim-lsp-installer/installers/composer.lua +++ b/lua/nvim-lsp-installer/installers/composer.lua @@ -8,6 +8,7 @@ local process = require "nvim-lsp-installer.process" local composer = platform.is_win and "composer.bat" or "composer" +---@param installer ServerInstallerFunction local function ensure_composer(installer) return installers.pipe { std.ensure_executables { @@ -20,45 +21,54 @@ end local M = {} +---@param packages string[] @The Gem packages to install. The first item in this list will be the recipient of the server version, should the user request a specific one. function M.packages(packages) - return ensure_composer(function(server, callback, context) - local c = process.chain { - cwd = server.root_dir, - stdio_sink = context.stdio_sink, - } + return ensure_composer( + ---@type ServerInstallerFunction + function(server, callback, context) + local c = process.chain { + cwd = server.root_dir, + stdio_sink = context.stdio_sink, + } - if not (fs.file_exists(path.concat { server.root_dir, "composer.json" })) then - c.run(composer, { "init", "--no-interaction", "--stability=dev" }) - c.run(composer, { "config", "prefer-stable", "true" }) - end + if not (fs.file_exists(path.concat { server.root_dir, "composer.json" })) then + c.run(composer, { "init", "--no-interaction", "--stability=dev" }) + c.run(composer, { "config", "prefer-stable", "true" }) + end - local pkgs = Data.list_copy(packages or {}) - if context.requested_server_version then - -- The "head" package is the recipient for the requested version. It's.. by design... don't ask. - pkgs[1] = ("%s:%s"):format(pkgs[1], context.requested_server_version) - end + local pkgs = Data.list_copy(packages or {}) + if context.requested_server_version then + -- The "head" package is the recipient for the requested version. It's.. by design... don't ask. + pkgs[1] = ("%s:%s"):format(pkgs[1], context.requested_server_version) + end - c.run(composer, vim.list_extend({ "require" }, pkgs)) - c.spawn(callback) - end) + c.run(composer, vim.list_extend({ "require" }, pkgs)) + c.spawn(callback) + end + ) end function M.install() - return ensure_composer(function(server, callback, context) - process.spawn(composer, { - args = { - "install", - "--no-interaction", - "--no-dev", - "--optimize-autoloader", - "--classmap-authoritative", - }, - cwd = server.root_dir, - stdio_sink = context.stdio_sink, - }, callback) - end) + return ensure_composer( + ---@type ServerInstallerFunction + function(server, callback, context) + process.spawn(composer, { + args = { + "install", + "--no-interaction", + "--no-dev", + "--optimize-autoloader", + "--classmap-authoritative", + }, + cwd = server.root_dir, + stdio_sink = context.stdio_sink, + }, callback) + end + ) end +---@param root_dir string @The directory to resolve the executable from. +---@param executable string function M.executable(root_dir, executable) return path.concat { root_dir, "vendor", "bin", platform.is_win and ("%s.bat"):format(executable) or executable } end diff --git a/lua/nvim-lsp-installer/installers/context.lua b/lua/nvim-lsp-installer/installers/context.lua index c36a9e65..13775fc0 100644 --- a/lua/nvim-lsp-installer/installers/context.lua +++ b/lua/nvim-lsp-installer/installers/context.lua @@ -6,6 +6,8 @@ local platform = require "nvim-lsp-installer.platform" local M = {} +---@param url string @The url to fetch. +---@param callback fun(err: string|nil, raw_data: string) local function fetch(url, callback) local stdio = process.in_memory_sink() log.fmt_debug("Fetching URL %s", url) @@ -57,7 +59,9 @@ local function fetch(url, callback) } end +---@param repo string @The GitHub repo ("username/repo"). function M.use_github_release(repo) + ---@type ServerInstallerFunction return function(server, callback, context) if context.requested_server_version then log.fmt_debug( @@ -84,6 +88,8 @@ function M.use_github_release(repo) end end +---@param repo string @The GitHub report ("username/repo"). +---@param file string @The name of a file availabine in the provided repo's GitHub releases. function M.use_github_release_file(repo, file) return installers.pipe { M.use_github_release(repo), @@ -117,14 +123,20 @@ function M.use_github_release_file(repo, file) } end +---Access the context ojbect to create a new installer. +---@param fn fun(context: ServerInstallContext): ServerInstallerFunction function M.capture(fn) - return function(server, callback, context, ...) + ---@type ServerInstallerFunction + return function(server, callback, context) local installer = fn(context) - installer(server, callback, context, ...) + installer(server, callback, context) end end +---Update the context object. +---@param fn fun(context: ServerInstallContext): ServerInstallerFunction function M.set(fn) + ---@type ServerInstallerFunction return function(_, callback, context) fn(context) callback(true) diff --git a/lua/nvim-lsp-installer/installers/gem.lua b/lua/nvim-lsp-installer/installers/gem.lua index 091cda27..331be055 100644 --- a/lua/nvim-lsp-installer/installers/gem.lua +++ b/lua/nvim-lsp-installer/installers/gem.lua @@ -9,12 +9,14 @@ local M = {} local gem = platform.is_win and "gem.cmd" or "gem" +---@param packages string[] @The Gem packages to install. The first item in this list will be the recipient of the server version, should the user request a specific one. function M.packages(packages) return installers.pipe { std.ensure_executables { { "ruby", "ruby was not found in path, refer to https://wiki.openstack.org/wiki/RubyGems." }, { "gem", "gem was not found in path, refer to https://wiki.openstack.org/wiki/RubyGems." }, }, + ---@type ServerInstallerFunction function(server, callback, context) local pkgs = Data.list_copy(packages or {}) if context.requested_server_version then @@ -38,10 +40,13 @@ function M.packages(packages) } end +---@param root_dir string @The directory to resolve the executable from. +---@param executable string function M.executable(root_dir, executable) return path.concat { root_dir, "bin", executable } end +---@param root_dir string function M.env(root_dir) return { GEM_HOME = root_dir, diff --git a/lua/nvim-lsp-installer/installers/go.lua b/lua/nvim-lsp-installer/installers/go.lua index 005ce842..c2dbcb37 100644 --- a/lua/nvim-lsp-installer/installers/go.lua +++ b/lua/nvim-lsp-installer/installers/go.lua @@ -6,9 +6,11 @@ local process = require "nvim-lsp-installer.process" local M = {} +---@param packages string[] @The Go packages to install. The first item in this list will be the recipient of the server version, should the user request a specific one. function M.packages(packages) return installers.pipe { std.ensure_executables { { "go", "go was not found in path, refer to https://golang.org/doc/install." } }, + ---@type ServerInstallerFunction function(server, callback, context) local pkgs = Data.list_copy(packages or {}) local c = process.chain { @@ -34,6 +36,8 @@ function M.packages(packages) } end +---@param root_dir string @The directory to resolve the executable from. +---@param executable string function M.executable(root_dir, executable) return path.concat { root_dir, executable } end diff --git a/lua/nvim-lsp-installer/installers/init.lua b/lua/nvim-lsp-installer/installers/init.lua index 83b46a1b..f8490586 100644 --- a/lua/nvim-lsp-installer/installers/init.lua +++ b/lua/nvim-lsp-installer/installers/init.lua @@ -4,6 +4,18 @@ local Data = require "nvim-lsp-installer.data" local M = {} +---@alias ServerInstallCallback fun(success: boolean) + +---@class ServerInstallContext +---@field requested_server_version string|nil @The version requested by the user. +---@field stdio_sink StdioSink +---@field github_release_file string|nil @Only available if context.use_github_release_file has been called. + +---@alias ServerInstallerFunction fun(server: Server, callback: ServerInstallCallback, context: ServerInstallContext) + +--- Composes multiple installer functions into one. +---@param installers ServerInstallerFunction[] +---@return ServerInstallerFunction function M.pipe(installers) if #installers == 0 then error "No installers to pipe." @@ -33,6 +45,14 @@ function M.pipe(installers) end end +--- Composes multiple installer function into one - in reversed order. +---@param installers ServerInstallerFunction[] +function M.compose(installers) + return M.pipe(Data.list_reverse(installers)) +end + +---@param installers ServerInstallerFunction[] +---@return ServerInstallerFunction @An installer function that will serially execute the provided installers, until the first one succeeds. function M.first_successful(installers) if #installers == 0 then error "No installers to pipe." @@ -64,11 +84,9 @@ function M.first_successful(installers) end end --- much fp, very wow -function M.compose(installers) - return M.pipe(Data.list_reverse(installers)) -end - +--- Wraps the provided server installer to always succeeds. +---@param installer ServerInstallerFunction +---@return ServerInstallerFunction function M.always_succeed(installer) return function(server, callback, context) installer(server, function() @@ -77,8 +95,11 @@ function M.always_succeed(installer) end end +---@param platform_table table<Platform, ServerInstallerFunction> +---@return ServerInstallerFunction | nil local function get_by_platform(platform_table) if platform.is_mac then + platform_table.mac() return platform_table.mac or platform_table.unix elseif platform.is_linux then return platform_table.linux or platform_table.unix @@ -91,7 +112,10 @@ local function get_by_platform(platform_table) end end --- non-exhaustive +--- Creates a server installer that executes the given installer for the current platform. +--- If there is no server installer provided for the current platform, the installer will instantly exit successfully. +---@param platform_table table<Platform, ServerInstallerFunction> +---@return ServerInstallerFunction function M.on(platform_table) return function(server, callback, context) local installer = get_by_platform(platform_table) @@ -107,7 +131,10 @@ function M.on(platform_table) end end --- exhaustive +--- Creates a server installer that executes the given installer for the current platform. +--- If there is no server installer provided for the current platform, the installer will instantly exit with a failure. +---@param platform_table table<Platform, ServerInstallerFunction> +---@return ServerInstallerFunction function M.when(platform_table) return function(server, callback, context) local installer = get_by_platform(platform_table) diff --git a/lua/nvim-lsp-installer/installers/npm.lua b/lua/nvim-lsp-installer/installers/npm.lua index b165d5f1..d6805b0e 100644 --- a/lua/nvim-lsp-installer/installers/npm.lua +++ b/lua/nvim-lsp-installer/installers/npm.lua @@ -10,6 +10,7 @@ local M = {} local npm = platform.is_win and "npm.cmd" or "npm" +---@param installer ServerInstallerFunction local function ensure_npm(installer) return installers.pipe { std.ensure_executables { @@ -24,13 +25,16 @@ local function ensure_npm(installer) end local function create_installer(read_version_from_context) + ---@param packages string[] return function(packages) - return ensure_npm(function(server, callback, context) - local pkgs = Data.list_copy(packages or {}) - local c = process.chain { - cwd = server.root_dir, - stdio_sink = context.stdio_sink, - } + return ensure_npm( + ---@type ServerInstallerFunction + function(server, callback, context) + local pkgs = Data.list_copy(packages or {}) + local c = process.chain { + cwd = server.root_dir, + stdio_sink = context.stdio_sink, + } -- stylua: ignore start if not (fs.dir_exists(path.concat { server.root_dir, "node_modules" }) or fs.file_exists(path.concat { server.root_dir, "package.json" })) @@ -43,17 +47,25 @@ local function create_installer(read_version_from_context) pkgs[1] = ("%s@%s"):format(pkgs[1], context.requested_server_version) end - -- stylua: ignore end - c.run(npm, vim.list_extend({ "install" }, pkgs)) - c.spawn(callback) - end) + -- stylua: ignore end + c.run(npm, vim.list_extend({ "install" }, pkgs)) + c.spawn(callback) + end + ) end end +---Creates an installer that installs the provided packages. Will respect user's requested version. M.packages = create_installer(true) +---Creates an installer that installs the provided packages. Will NOT respect user's requested version. +---This is useful in situation where there's a need to install an auxiliary npm package. M.install = create_installer(false) +---Creates a server installer that executes the given executable. +---@param executable string +---@param args string[] function M.exec(executable, args) + ---@type ServerInstallerFunction return function(server, callback, context) process.spawn(M.executable(server.root_dir, executable), { args = args, @@ -63,16 +75,23 @@ function M.exec(executable, args) end end +---Creates a server installer that runs the given script. +---@param script string @The npm script to run (npm run). function M.run(script) - return ensure_npm(function(server, callback, context) - process.spawn(npm, { - args = { "run", script }, - cwd = server.root_dir, - stdio_sink = context.stdio_sink, - }, callback) - end) + return ensure_npm( + ---@type ServerInstallerFunction + function(server, callback, context) + process.spawn(npm, { + args = { "run", script }, + cwd = server.root_dir, + stdio_sink = context.stdio_sink, + }, callback) + end + ) end +---@param root_dir string @The directory to resolve the executable from. +---@param executable string function M.executable(root_dir, executable) return path.concat { root_dir, diff --git a/lua/nvim-lsp-installer/installers/pip3.lua b/lua/nvim-lsp-installer/installers/pip3.lua index 8ae3d2db..6b593858 100644 --- a/lua/nvim-lsp-installer/installers/pip3.lua +++ b/lua/nvim-lsp-installer/installers/pip3.lua @@ -10,6 +10,8 @@ local M = {} local REL_INSTALL_DIR = "venv" +---@param python_executable string +---@param packages string[] local function create_installer(python_executable, packages) return installers.pipe { std.ensure_executables { @@ -18,6 +20,7 @@ local function create_installer(python_executable, packages) ("%s was not found in path. Refer to https://www.python.org/downloads/."):format(python_executable), }, }, + ---@type ServerInstallerFunction function(server, callback, context) local pkgs = Data.list_copy(packages or {}) local c = process.chain { @@ -40,12 +43,15 @@ local function create_installer(python_executable, packages) } end +---@param packages string[] @The pip packages to install. The first item in this list will be the recipient of the server version, should the user request a specific one. function M.packages(packages) local py3 = create_installer("python3", packages) local py = create_installer("python", packages) return installers.first_successful(platform.is_win and { py, py3 } or { py3, py }) -- see https://github.com/williamboman/nvim-lsp-installer/issues/128 end +---@param root_dir string @The directory to resolve the executable from. +---@param executable string function M.executable(root_dir, executable) return path.concat { root_dir, REL_INSTALL_DIR, platform.is_win and "Scripts" or "bin", executable } end diff --git a/lua/nvim-lsp-installer/installers/shell.lua b/lua/nvim-lsp-installer/installers/shell.lua index 752a216c..35f0cf6e 100644 --- a/lua/nvim-lsp-installer/installers/shell.lua +++ b/lua/nvim-lsp-installer/installers/shell.lua @@ -3,7 +3,9 @@ local process = require "nvim-lsp-installer.process" local M = {} +---@param opts {shell: string, cmd: string[], env: table|nil} local function shell(opts) + ---@type ServerInstallerFunction return function(server, callback, context) local _, stdio = process.spawn(opts.shell, { cwd = server.root_dir, @@ -21,6 +23,8 @@ local function shell(opts) end end +---@param raw_script string @The bash script to execute as-is. +---@param opts {prefix: string, env: table} function M.bash(raw_script, opts) local default_opts = { prefix = "set -euo pipefail;", @@ -35,6 +39,8 @@ function M.bash(raw_script, opts) } end +---@param raw_script string @The sh script to execute as-is. +---@param opts {prefix: string, env: table} function M.sh(raw_script, opts) local default_opts = { prefix = "set -eu;", @@ -49,6 +55,8 @@ function M.sh(raw_script, opts) } end +---@param raw_script string @The cmd.exe script to execute as-is. +---@param opts {env: table} function M.cmd(raw_script, opts) local default_opts = { env = {}, @@ -62,6 +70,8 @@ function M.cmd(raw_script, opts) } end +---@param raw_script string @The powershell script to execute as-is. +---@param opts {prefix: string, env: table} function M.powershell(raw_script, opts) local default_opts = { env = {}, @@ -77,10 +87,15 @@ function M.powershell(raw_script, opts) } end +---@deprecated Unsafe. +---@param url string @The url to the powershell script to execute. +---@param opts {prefix: string, env: table} function M.remote_powershell(url, opts) return M.powershell(("iwr -UseBasicParsing %q | iex"):format(url), opts) end +---@param raw_script string @A script that is compatible with bash and cmd.exe. +---@param opts {env: table} function M.polyshell(raw_script, opts) local default_opts = { env = {}, diff --git a/lua/nvim-lsp-installer/installers/std.lua b/lua/nvim-lsp-installer/installers/std.lua index c011279d..0e060351 100644 --- a/lua/nvim-lsp-installer/installers/std.lua +++ b/lua/nvim-lsp-installer/installers/std.lua @@ -7,8 +7,11 @@ local shell = require "nvim-lsp-installer.installers.shell" local M = {} +---@param url string @The url to download. +---@param out_file string @The relative path to where to write the contents of the url. function M.download_file(url, out_file) return installers.when { + ---@type ServerInstallerFunction unix = function(server, callback, context) context.stdio_sink.stdout(("Downloading file %q...\n"):format(url)) process.attempt { @@ -31,9 +34,12 @@ function M.download_file(url, out_file) } end +---@param file string @The relative path to the file to unzip. +---@param dest string|nil @The destination of the unzip (defaults to "."). function M.unzip(file, dest) return installers.pipe { installers.when { + ---@type ServerInstallerFunction unix = function(server, callback, context) process.spawn("unzip", { args = { "-d", dest, file }, @@ -47,6 +53,8 @@ function M.unzip(file, dest) } end +---@see unzip(). +---@param url string @The url of the .zip file. function M.unzip_remote(url, dest) return installers.pipe { M.download_file(url, "archive.zip"), @@ -54,8 +62,10 @@ function M.unzip_remote(url, dest) } end +---@param file string @The relative path to the tar file to extract. function M.untar(file) return installers.pipe { + ---@type ServerInstallerFunction function(server, callback, context) process.spawn("tar", { args = { "-xvf", file }, @@ -67,8 +77,10 @@ function M.untar(file) } end +---@param file string local function win_extract(file) return installers.pipe { + ---@type ServerInstallerFunction function(server, callback, context) -- The trademarked "throw shit until it sticks" technique local sevenzip = process.lazy_spawn("7z", { @@ -95,6 +107,7 @@ local function win_extract(file) } end +---@param file string local function win_untarxz(file) return installers.pipe { win_extract(file), @@ -102,8 +115,10 @@ local function win_untarxz(file) } end +---@param file string local function win_arc_unarchive(file) return installers.pipe { + ---@type ServerInstallerFunction function(server, callback, context) context.stdio_sink.stdout "Attempting to unarchive using arc." process.spawn("arc", { @@ -116,6 +131,7 @@ local function win_arc_unarchive(file) } end +---@param url string @The url to the .tar.xz file to extract. function M.untarxz_remote(url) return installers.pipe { M.download_file(url, "archive.tar.xz"), @@ -129,6 +145,7 @@ function M.untarxz_remote(url) } end +---@param url string @The url to the .tar.gz file to extract. function M.untargz_remote(url) return installers.pipe { M.download_file(url, "archive.tar.gz"), @@ -136,8 +153,10 @@ function M.untargz_remote(url) } end +---@param file string @The relative path to the file to gunzip. function M.gunzip(file) return installers.when { + ---@type ServerInstallerFunction unix = function(server, callback, context) process.spawn("gzip", { args = { "-d", file }, @@ -149,6 +168,9 @@ function M.gunzip(file) } end +---@see gunzip() +---@param url string @The url to the .gz file to extract. +---@param out_file string|nil @The name of the extracted .gz file. function M.gunzip_remote(url, out_file) local archive = ("%s.gz"):format(out_file or "archive") return installers.pipe { @@ -158,7 +180,10 @@ function M.gunzip_remote(url, out_file) } end +---Recursively deletes the provided path. Will fail on paths that are not inside the configured install_root_dir. +---@param rel_path string @The relative path to the file/directory to remove. function M.rmrf(rel_path) + ---@type ServerInstallerFunction return function(server, callback, context) local abs_path = path.concat { server.root_dir, rel_path } context.stdio_sink.stdout(("Deleting %q\n"):format(abs_path)) @@ -174,7 +199,10 @@ function M.rmrf(rel_path) end end +---Shallow git clone. +---@param repo_url string function M.git_clone(repo_url) + ---@type ServerInstallerFunction return function(server, callback, context) local c = process.chain { cwd = server.root_dir, @@ -192,7 +220,9 @@ function M.git_clone(repo_url) end end +---@param opts {args: string[]} function M.gradlew(opts) + ---@type ServerInstallerFunction return function(server, callback, context) process.spawn(path.concat { server.root_dir, platform.is_win and "gradlew.bat" or "gradlew" }, { args = opts.args, @@ -202,23 +232,32 @@ function M.gradlew(opts) end end +---Creates an installer that ensures that the provided executables are available in the current runtime. +---@param executables string[][] @A list of (executable, error_msg) tuples. +---@return ServerInstallerFunction function M.ensure_executables(executables) - return vim.schedule_wrap(function(_, callback, context) - local has_error = false - for i = 1, #executables do - local entry = executables[i] - local executable = entry[1] - local error_msg = entry[2] - if vim.fn.executable(executable) ~= 1 then - has_error = true - context.stdio_sink.stderr(error_msg .. "\n") + return vim.schedule_wrap( + ---@type ServerInstallerFunction + function(_, callback, context) + local has_error = false + for i = 1, #executables do + local entry = executables[i] + local executable = entry[1] + local error_msg = entry[2] + if vim.fn.executable(executable) ~= 1 then + has_error = true + context.stdio_sink.stderr(error_msg .. "\n") + end end + callback(not has_error) end - callback(not has_error) - end) + ) end +---@path old_path string @The relative path to the file/dir to rename. +---@path new_path string @The relative path to what to rename the file/dir to. function M.rename(old_path, new_path) + ---@type ServerInstallerFunction return function(server, callback, context) local ok = pcall( fs.rename, @@ -232,8 +271,11 @@ function M.rename(old_path, new_path) end end +---@param flags string[] @The chmod flags to apply. +---@param files string[] @A list of relative paths to apply the chmod on. function M.chmod(flags, files) return installers.on { + ---@type ServerInstallerFunction unix = function(server, callback, context) process.spawn("chmod", { args = vim.list_extend({ flags }, files), diff --git a/lua/nvim-lsp-installer/path.lua b/lua/nvim-lsp-installer/path.lua index 41961ffd..ed906954 100644 --- a/lua/nvim-lsp-installer/path.lua +++ b/lua/nvim-lsp-installer/path.lua @@ -21,6 +21,8 @@ function M.cwd() return uv.fs_realpath "." end +---@param path_components string[] +---@return string function M.concat(path_components) return table.concat(path_components, sep) end diff --git a/lua/nvim-lsp-installer/platform.lua b/lua/nvim-lsp-installer/platform.lua index 6ac6c15b..44b99199 100644 --- a/lua/nvim-lsp-installer/platform.lua +++ b/lua/nvim-lsp-installer/platform.lua @@ -2,6 +2,12 @@ local M = {} local uname = vim.loop.os_uname() +---@alias Platform +---| '"win"' +---| '"unix"' +---| '"linux"' +---| '"mac"' + local arch_aliases = { ["x86_64"] = "x64", ["i386"] = "x86", diff --git a/lua/nvim-lsp-installer/process.lua b/lua/nvim-lsp-installer/process.lua index ae3647cb..ad17465c 100644 --- a/lua/nvim-lsp-installer/process.lua +++ b/lua/nvim-lsp-installer/process.lua @@ -5,9 +5,20 @@ local uv = vim.loop local list_any = Data.list_any +---@alias luv_pipe any +---@alias luv_handle any + +---@class StdioSink +---@field stdout fun(chunk: string) +---@field stderr fun(chunk: string) + local M = {} +---@param pipe luv_pipe +---@param sink fun(chunk: string) local function connect_sink(pipe, sink) + ---@param err string | nil + ---@param data string | nil return function(err, data) if err then log.error("Unexpected error when reading pipe.", err) @@ -25,6 +36,7 @@ end -- Also, there's no particular reason we need to refresh the environment (yet). local initial_environ = vim.fn.environ() +---@param new_paths string[] @A list of paths to prepend the existing PATH with. function M.extend_path(new_paths) local new_path_str = table.concat(new_paths, platform.path_sep) if initial_environ["PATH"] then @@ -33,6 +45,8 @@ function M.extend_path(new_paths) return new_path_str end +---Merges the provided env param with the user's full environent. Provided env has precedence. +---@param env table<string, string> function M.graft_env(env) local merged_env = {} for key, val in pairs(initial_environ) do @@ -46,6 +60,7 @@ function M.graft_env(env) return merged_env end +---@param env_list string[] local function sanitize_env_list(env_list) local sanitized_list = {} for _, env in ipairs(env_list) do @@ -70,6 +85,18 @@ local function sanitize_env_list(env_list) return sanitized_list end +---@alias JobSpawnCallback fun(success: boolean) + +---@class JobSpawnOpts +---@field env string[] @List of "key=value" string. +---@field args string[] +---@field cwd string +---@field stdio_sink StdioSink + +---@param cmd string @The command/executable. +---@param opts JobSpawnOpts +---@param callback JobSpawnCallback +---@return luv_handle,luv_pipe[]|nil @Returns the job handle and the stdio array on success, otherwise returns nil. function M.spawn(cmd, opts, callback) local stdin = uv.new_pipe(false) local stdout = uv.new_pipe(false) @@ -140,9 +167,12 @@ function M.spawn(cmd, opts, callback) return handle, stdio end +---@param opts JobSpawnOpts @The job spawn opts to apply in every job in this "chain". function M.chain(opts) local jobs = {} return { + ---@param cmd string + ---@param args string[] run = function(cmd, args) jobs[#jobs + 1] = M.lazy_spawn( cmd, @@ -151,6 +181,7 @@ function M.chain(opts) }) ) end, + ---@param callback JobSpawnCallback spawn = function(callback) local function execute(idx) local ok, err = pcall(jobs[idx], function(successful) @@ -203,7 +234,10 @@ function M.in_memory_sink() } end --- this prob belongs elsewhere ¯\_(ツ)_/¯ +--- This probably belongs elsewhere ¯\_(ツ)_/¯ +---@generic T +---@param debounced_fn fun(arg1: T) +---@return fun(arg1: T) function M.debounced(debounced_fn) local queued = false local last_arg = nil @@ -221,12 +255,23 @@ function M.debounced(debounced_fn) end end +---@alias LazyJob fun(callback: JobSpawnCallback) + +---@param cmd string +---@param opts JobSpawnOpts function M.lazy_spawn(cmd, opts) + ---@param callback JobSpawnCallback return function(callback) return M.spawn(cmd, opts, callback) end end +---@class JobAttemptOpts +---@field jobs LazyJob[] +---@field on_finish JobSpawnCallback +---@field on_iterate fun() + +---@param opts JobAttemptOpts function M.attempt(opts) local jobs, on_finish, on_iterate = opts.jobs, opts.on_finish, opts.on_iterate if #jobs == 0 then diff --git a/lua/nvim-lsp-installer/server.lua b/lua/nvim-lsp-installer/server.lua index cdabdcfd..f1e0fdef 100644 --- a/lua/nvim-lsp-installer/server.lua +++ b/lua/nvim-lsp-installer/server.lua @@ -10,46 +10,37 @@ local M = {} -- old, but also somewhat convenient, API M.get_server_root_path = servers.get_server_install_path +---@alias ServerDeprecation {message:string, replace_with:string|nil} +---@alias ServerOpts {name:string, root_dir:string, homepage:string|nil, deprecated:ServerDeprecation, installer:ServerInstallerFunction|ServerInstallerFunction[], default_options:table, pre_setup:fun()|nil, post_setup:fun()|nil} + +---@class Server +---@field public name string @The server name. This is the same as lspconfig's server names. +---@field public root_dir string @The directory where the server should be installed in. +---@field public homepage string|nil @The homepage where users can find more information. This is shown to users in the UI. +---@field public deprecated ServerDeprecation|nil @The existence (not nil) of this field indicates this server is depracted. +---@field private _installer ServerInstallerFunction +---@field private _default_options table @The server's default options. This is used in @see Server#setup. +---@field private _pre_setup fun()|nil @Function to be called in @see Server#setup, before trying to setup. +---@field private _post_setup fun()|nil @Function to be called in @see Server#setup, after successful setup. M.Server = {} M.Server.__index = M.Server ----@class Server ---@param opts table --- @field name (string) The name of the LSP server. This MUST correspond with lspconfig's naming. --- --- @field homepage (string) A URL to the homepage of this server. This is for example where users can --- report issues and receive support. --- --- @field installer (function) The function that installs the LSP (see the .installers module). The function signature should be `function (server, callback)`, where --- `server` is the Server instance being installed, and `callback` is a function that must be called upon completion. The `callback` function --- has the signature `function (success, result)`, where `success` is a boolean and `result` is of any type (similar to `pcall`). --- --- @field default_options (table) The default options to be passed to lspconfig's .setup() function. Each server should provide at least the `cmd` field. --- --- @field root_dir (string) The absolute path to the directory of the installation. --- This MUST be a directory inside nvim-lsp-installer's designated root install directory inside stdpath("data"). Most servers will make use of server.get_server_root_path() to produce its root_dir path. --- --- @field post_setup (function) An optional function to be executed after the setup function has been successfully called. --- Use this to defer setting up server specific things until they're actually --- needed, like custom commands. --- --- @field pre_setup (function) An optional function to be executed prior to calling lspconfig's setup(). --- Use this to defer setting up server specific things until they're actually needed. --- +---@param opts ServerOpts +---@return Server function M.Server:new(opts) return setmetatable({ name = opts.name, root_dir = opts.root_dir, homepage = opts.homepage, deprecated = opts.deprecated, - _root_dir = opts.root_dir, -- @deprecated Use the `root_dir` property instead. _installer = type(opts.installer) == "function" and opts.installer or installers.pipe(opts.installer), _default_options = opts.default_options, - _post_setup = opts.post_setup, _pre_setup = opts.pre_setup, + _post_setup = opts.post_setup, }, M.Server) end +---@param opts table @User-defined options. This is directly passed to the lspconfig's setup() method. function M.Server:setup(opts) if self._pre_setup then log.fmt_debug("Calling pre_setup for server=%s", self.name) diff --git a/lua/nvim-lsp-installer/servers/init.lua b/lua/nvim-lsp-installer/servers/init.lua index 94a67b3c..d98bd224 100644 --- a/lua/nvim-lsp-installer/servers/init.lua +++ b/lua/nvim-lsp-installer/servers/init.lua @@ -5,6 +5,8 @@ local settings = require "nvim-lsp-installer.settings" local M = {} +---@param name string +---@return string local function vscode_langservers_extracted(name) return settings.current.allow_federated_servers and "vscode-langservers-extracted" or "vscode-langservers-extracted_" .. name @@ -39,7 +41,6 @@ local INSTALL_DIRS = { ["yamlls"] = "yaml", } --- :'<,'>!sort local CORE_SERVERS = Data.set_of { "angularls", "ansiblels", @@ -107,6 +108,7 @@ local CORE_SERVERS = Data.set_of { "zls", } +---@type table<string, Server> local INITIALIZED_SERVERS = {} local cached_server_roots @@ -115,6 +117,7 @@ local function scan_server_roots() if cached_server_roots then return cached_server_roots end + ---@type string[] local result = {} local ok, entries = pcall(fs.readdir, settings.current.install_root_dir) if not ok then @@ -134,6 +137,8 @@ local function scan_server_roots() return cached_server_roots end +---@param server_name string +---@return string local function get_server_install_dir(server_name) return INSTALL_DIRS[server_name] or server_name end @@ -142,17 +147,24 @@ function M.get_server_install_path(dirname) return path.concat { settings.current.install_root_dir, dirname } end +---@param server_name string function M.is_server_installed(server_name) local scanned_server_dirs = scan_server_roots() local dirname = get_server_install_dir(server_name) return scanned_server_dirs[dirname] or false end --- returns a tuple of [server_name, requested_version], where requested_version may be nil -function M.parse_server_tuple(server_name) - return vim.split(server_name, "@") +---@class ServerTuple +---@field public [1] string The server name. +---@field public [2] string|nil The requested server version. + +---@param server_identifier string @The server identifier to parse. +---@return ServerTuple +function M.parse_server_identifier(server_identifier) + return vim.split(server_identifier, "@") end +---@param server_name string function M.get_server(server_name) if INITIALIZED_SERVERS[server_name] then return true, INITIALIZED_SERVERS[server_name] @@ -176,6 +188,8 @@ function M.get_server(server_name) ):format(server_name, "https://github.com/williamboman/nvim-lsp-installer", server_factory) end +---@param server_names string[] +---@return Server[] local function resolve_servers(server_names) return Data.list_map(function(server_name) local ok, server = M.get_server(server_name) @@ -186,16 +200,19 @@ local function resolve_servers(server_names) end, server_names) end +---@return string[] function M.get_available_server_names() return vim.tbl_keys(vim.tbl_extend("force", CORE_SERVERS, INITIALIZED_SERVERS)) end +---@return string[] function M.get_installed_server_names() return vim.tbl_filter(function(server_name) return M.is_server_installed(server_name) end, M.get_available_server_names()) end +---@return string[] function M.get_uninstalled_server_names() return vim.tbl_filter(function(server_name) return not M.is_server_installed(server_name) @@ -217,6 +234,7 @@ function M.get_uninstalled_servers() return resolve_servers(M.get_uninstalled_server_names()) end +---@param server Server @The server to register. function M.register(server) INSTALL_DIRS[server.name] = vim.fn.fnamemodify(server.root_dir, ":t") INITIALIZED_SERVERS[server.name] = server diff --git a/lua/nvim-lsp-installer/servers/lemminx/init.lua b/lua/nvim-lsp-installer/servers/lemminx/init.lua index d88a3005..3bdb24cf 100644 --- a/lua/nvim-lsp-installer/servers/lemminx/init.lua +++ b/lua/nvim-lsp-installer/servers/lemminx/init.lua @@ -1,6 +1,5 @@ local server = require "nvim-lsp-installer.server" local path = require "nvim-lsp-installer.path" -local fs = require "nvim-lsp-installer.fs" local std = require "nvim-lsp-installer.installers.std" local Data = require "nvim-lsp-installer.data" local context = require "nvim-lsp-installer.installers.context" diff --git a/lua/nvim-lsp-installer/servers/ltex/configure.lua b/lua/nvim-lsp-installer/servers/ltex/configure.lua index 999f1867..78cfdcdb 100644 --- a/lua/nvim-lsp-installer/servers/ltex/configure.lua +++ b/lua/nvim-lsp-installer/servers/ltex/configure.lua @@ -145,7 +145,7 @@ configs.ltex = { hiddenFalsePositives = {}, }, }, - on_attach = function(client, bufnr) + on_attach = function(client) -- local lang = client.config.settings.ltex.language for lang, _ in ipairs(client.config.dictionary_files) do -- updateConfig(lang, "dictionary") diff --git a/lua/nvim-lsp-installer/settings.lua b/lua/nvim-lsp-installer/settings.lua index f10b9e28..66c59e19 100644 --- a/lua/nvim-lsp-installer/settings.lua +++ b/lua/nvim-lsp-installer/settings.lua @@ -2,7 +2,8 @@ local path = require "nvim-lsp-installer.path" local M = {} -M._DEFAULT_SETTINGS = { +---@class LspInstallerSettings +local DEFAULT_SETTINGS = { ui = { icons = { -- The list icon to use for installed servers. @@ -50,8 +51,10 @@ M._DEFAULT_SETTINGS = { max_concurrent_installers = 4, } +M._DEFAULT_SETTINGS = DEFAULT_SETTINGS M.current = M._DEFAULT_SETTINGS +---@param opts LspInstallerSettings function M.set(opts) M.current = vim.tbl_deep_extend("force", M.current, opts) end 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 |
