aboutsummaryrefslogtreecommitdiffstats
path: root/lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua')
-rw-r--r--lua/nvim-lsp-installer.lua12
-rw-r--r--lua/nvim-lsp-installer/data.lua30
-rw-r--r--lua/nvim-lsp-installer/installers/composer.lua70
-rw-r--r--lua/nvim-lsp-installer/installers/context.lua16
-rw-r--r--lua/nvim-lsp-installer/installers/gem.lua5
-rw-r--r--lua/nvim-lsp-installer/installers/go.lua4
-rw-r--r--lua/nvim-lsp-installer/installers/init.lua41
-rw-r--r--lua/nvim-lsp-installer/installers/npm.lua53
-rw-r--r--lua/nvim-lsp-installer/installers/pip3.lua6
-rw-r--r--lua/nvim-lsp-installer/installers/shell.lua15
-rw-r--r--lua/nvim-lsp-installer/installers/std.lua64
-rw-r--r--lua/nvim-lsp-installer/path.lua2
-rw-r--r--lua/nvim-lsp-installer/platform.lua6
-rw-r--r--lua/nvim-lsp-installer/process.lua47
-rw-r--r--lua/nvim-lsp-installer/server.lua41
-rw-r--r--lua/nvim-lsp-installer/servers/init.lua26
-rw-r--r--lua/nvim-lsp-installer/servers/lemminx/init.lua1
-rw-r--r--lua/nvim-lsp-installer/servers/ltex/configure.lua2
-rw-r--r--lua/nvim-lsp-installer/settings.lua5
-rw-r--r--lua/nvim-lsp-installer/ui/display.lua80
-rw-r--r--lua/nvim-lsp-installer/ui/init.lua73
-rw-r--r--lua/nvim-lsp-installer/ui/state.lua4
-rw-r--r--lua/nvim-lsp-installer/ui/status-win/init.lua109
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