aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2022-03-26 13:41:50 +0100
committerGitHub <noreply@github.com>2022-03-26 13:41:50 +0100
commit212d17a039da449043b67529c29851db37acc236 (patch)
tree38411b14487895cef0d7648e198b79fd28793fe6
parentrun autogen_metadata.lua (diff)
downloadmason-212d17a039da449043b67529c29851db37acc236.tar
mason-212d17a039da449043b67529c29851db37acc236.tar.gz
mason-212d17a039da449043b67529c29851db37acc236.tar.bz2
mason-212d17a039da449043b67529c29851db37acc236.tar.lz
mason-212d17a039da449043b67529c29851db37acc236.tar.xz
mason-212d17a039da449043b67529c29851db37acc236.tar.zst
mason-212d17a039da449043b67529c29851db37acc236.zip
add async managers (#536)
-rw-r--r--lua/nvim-lsp-installer/core/async/init.lua4
-rw-r--r--lua/nvim-lsp-installer/core/async/uv.lua55
-rw-r--r--lua/nvim-lsp-installer/core/context.lua167
-rw-r--r--lua/nvim-lsp-installer/core/fs.lua68
-rw-r--r--lua/nvim-lsp-installer/core/managers/cargo/init.lua100
-rw-r--r--lua/nvim-lsp-installer/core/managers/composer/init.lua106
-rw-r--r--lua/nvim-lsp-installer/core/managers/dotnet/init.lua31
-rw-r--r--lua/nvim-lsp-installer/core/managers/gem/init.lua132
-rw-r--r--lua/nvim-lsp-installer/core/managers/git/init.lua54
-rw-r--r--lua/nvim-lsp-installer/core/managers/go/init.lua109
-rw-r--r--lua/nvim-lsp-installer/core/managers/npm/init.lua129
-rw-r--r--lua/nvim-lsp-installer/core/managers/opam/init.lua41
-rw-r--r--lua/nvim-lsp-installer/core/managers/pip3/init.lua144
-rw-r--r--lua/nvim-lsp-installer/core/optional.lua90
-rw-r--r--lua/nvim-lsp-installer/core/result.lua51
-rw-r--r--lua/nvim-lsp-installer/core/spawn.lua (renamed from lua/nvim-lsp-installer/core/async/spawn.lua)26
-rw-r--r--lua/nvim-lsp-installer/installers/npm.lua1
-rw-r--r--lua/nvim-lsp-installer/installers/pip3.lua1
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/cargo.lua59
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/composer.lua42
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua95
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/git.lua42
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua41
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua34
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/init.lua51
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua35
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua45
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua72
-rw-r--r--lua/nvim-lsp-installer/jobs/version-check/init.lua95
-rw-r--r--lua/nvim-lsp-installer/path.lua2
-rw-r--r--scripts/autogen_metadata.lua14
-rw-r--r--tests/core/async/async_spec.lua19
-rw-r--r--tests/core/managers/cargo_spec.lua210
-rw-r--r--tests/core/managers/composer_spec.lua188
-rw-r--r--tests/core/managers/dotnet_spec.lua47
-rw-r--r--tests/core/managers/gem_spec.lua226
-rw-r--r--tests/core/managers/git_spec.lua179
-rw-r--r--tests/core/managers/go_spec.lua187
-rw-r--r--tests/core/managers/npm_spec.lua205
-rw-r--r--tests/core/managers/opam_spec.lua64
-rw-r--r--tests/core/managers/pip3_spec.lua253
-rw-r--r--tests/core/optional_spec.lua63
-rw-r--r--tests/core/result_spec.lua32
-rw-r--r--tests/core/spawn_spec.lua (renamed from tests/core/async/spawn_spec.lua)34
-rw-r--r--tests/helpers/lua/luassertx.lua62
-rw-r--r--tests/helpers/lua/test_helpers.lua75
-rw-r--r--tests/jobs/outdated-servers/cargo_spec.lua32
-rw-r--r--tests/jobs/outdated-servers/gem_spec.lua45
-rw-r--r--tests/jobs/outdated-servers/pip3_spec.lua10
-rw-r--r--tests/luassertx/lua/luassertx.lua38
-rw-r--r--tests/minimal_init.vim31
-rw-r--r--tests/server_spec.lua22
52 files changed, 3271 insertions, 687 deletions
diff --git a/lua/nvim-lsp-installer/core/async/init.lua b/lua/nvim-lsp-installer/core/async/init.lua
index 6ecb5a61..0c278373 100644
--- a/lua/nvim-lsp-installer/core/async/init.lua
+++ b/lua/nvim-lsp-installer/core/async/init.lua
@@ -91,8 +91,8 @@ local function new_execution_context(suspend_fn, callback, ...)
end
end
-exports.run = function(suspend_fn, callback)
- return new_execution_context(suspend_fn, callback)
+exports.run = function(suspend_fn, callback, ...)
+ return new_execution_context(suspend_fn, callback, ...)
end
exports.scope = function(suspend_fn)
diff --git a/lua/nvim-lsp-installer/core/async/uv.lua b/lua/nvim-lsp-installer/core/async/uv.lua
new file mode 100644
index 00000000..a2249be9
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/async/uv.lua
@@ -0,0 +1,55 @@
+local a = require "nvim-lsp-installer.core.async"
+
+---@type Record<UvMethod, async fun(...)>
+local M = setmetatable({}, {
+ __index = function(_, method)
+ ---@async
+ return function(...)
+ local err, result = a.promisify(vim.loop[method])(...)
+ if err then
+ error(err, 2)
+ end
+ return result
+ end
+ end,
+})
+
+return M
+
+---@alias UvMethod
+---| '"fs_close"'
+---| '"fs_open"'
+---| '"fs_read"'
+---| '"fs_unlink"'
+---| '"fs_write"'
+---| '"fs_mkdir"'
+---| '"fs_mkdtemp"'
+---| '"fs_mkstemp"'
+---| '"fs_rmdir"'
+---| '"fs_scandir"'
+---| '"fs_stat"'
+---| '"fs_fstat"'
+---| '"fs_lstat"'
+---| '"fs_rename"'
+---| '"fs_fsync"'
+---| '"fs_fdatasync"'
+---| '"fs_ftruncate"'
+---| '"fs_sendfile"'
+---| '"fs_access"'
+---| '"fs_chmod"'
+---| '"fs_fchmod"'
+---| '"fs_utime"'
+---| '"fs_futime"'
+---| '"fs_lutime"'
+---| '"fs_link"'
+---| '"fs_symlink"'
+---| '"fs_readlink"'
+---| '"fs_realpath"'
+---| '"fs_chown"'
+---| '"fs_fchown"'
+---| '"fs_lchown"'
+---| '"fs_copyfile"'
+---| '"fs_opendir"'
+---| '"fs_readdir"'
+---| '"fs_closedir"'
+---| '"fs_statfs"'
diff --git a/lua/nvim-lsp-installer/core/context.lua b/lua/nvim-lsp-installer/core/context.lua
new file mode 100644
index 00000000..128ad417
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/context.lua
@@ -0,0 +1,167 @@
+local spawn = require "nvim-lsp-installer.core.spawn"
+local log = require "nvim-lsp-installer.log"
+local Optional = require "nvim-lsp-installer.core.optional"
+local fs = require "nvim-lsp-installer.core.fs"
+local settings = require "nvim-lsp-installer.settings"
+local Result = require "nvim-lsp-installer.core.result"
+local path = require "nvim-lsp-installer.path"
+local platform = require "nvim-lsp-installer.platform"
+
+---@class ContextualSpawn
+---@field cwd CwdManager
+---@field stdio_sink StdioSink
+local ContextualSpawn = {}
+
+---@param cwd CwdManager
+---@param stdio_sink StdioSink
+function ContextualSpawn.new(cwd, stdio_sink)
+ return setmetatable({ cwd = cwd, stdio_sink = stdio_sink }, ContextualSpawn)
+end
+function ContextualSpawn.__index(self, cmd)
+ return function(args)
+ args.cwd = args.cwd or self.cwd:get()
+ args.stdio_sink = args.stdio_sink or self.stdio_sink
+ -- We get_or_throw() here for convenience reasons.
+ -- Almost every time spawn is called via context we want the command to succeed.
+ return spawn[cmd](args):get_or_throw()
+ end
+end
+
+---@class ContextualFs
+---@field private cwd CwdManager
+local ContextualFs = {}
+ContextualFs.__index = ContextualFs
+
+---@param cwd CwdManager
+function ContextualFs.new(cwd)
+ return setmetatable({ cwd = cwd }, ContextualFs)
+end
+
+---@async
+---@param rel_path string @The relative path from the current working directory to the file to append.
+---@param contents string
+function ContextualFs:append_file(rel_path, contents)
+ return fs.append_file(path.concat { self.cwd:get(), rel_path }, contents)
+end
+
+---@async
+---@param rel_path string @The relative path from the current working directory.
+function ContextualFs:file_exists(rel_path)
+ return fs.file_exists(path.concat { self.cwd:get(), rel_path })
+end
+
+---@async
+---@param rel_path string @The relative path from the current working directory.
+function ContextualFs:dir_exists(rel_path)
+ return fs.dir_exists(path.concat { self.cwd:get(), rel_path })
+end
+
+---@class CwdManager
+local CwdManager = {}
+CwdManager.__index = CwdManager
+function CwdManager.new(cwd)
+ return setmetatable({ cwd = cwd }, CwdManager)
+end
+function CwdManager:get()
+ return self.cwd
+end
+function CwdManager:set(new_cwd)
+ self.cwd = new_cwd
+end
+
+---@class InstallContext
+---@field public receipt InstallReceiptBuilder
+---@field public requested_version Optional
+---@field public fs ContextualFs
+---@field public spawn JobSpawn
+---@field private cwd_manager CwdManager
+---@field private destination_dir string
+local InstallContext = {}
+InstallContext.__index = InstallContext
+
+function InstallContext.new(opts)
+ local cwd_manager = CwdManager.new(opts.cwd)
+ return setmetatable({
+ cwd_manager = cwd_manager,
+ spawn = ContextualSpawn.new(cwd_manager, opts.stdio_sink),
+ fs = ContextualFs.new(cwd_manager),
+ receipt = opts.receipt,
+ requested_version = opts.requested_version,
+ destination_dir = opts.destination_dir,
+ }, InstallContext)
+end
+
+---@deprecated
+---@param ctx ServerInstallContext
+---@param destination_dir string
+function InstallContext.from_server_context(ctx, destination_dir)
+ return InstallContext.new {
+ cwd = ctx.install_dir,
+ receipt = ctx.receipt,
+ stdio_sink = ctx.stdio_sink,
+ requested_version = Optional.of_nilable(ctx.requested_server_version),
+ destination_dir = destination_dir,
+ }
+end
+
+function InstallContext:cwd()
+ return self.cwd_manager:get()
+end
+
+---@param new_cwd string @The new cwd (absolute path).
+function InstallContext:set_cwd(new_cwd)
+ self
+ :ensure_path_ownership(new_cwd)
+ :map(function(p)
+ self.cwd_manager:set(p)
+ return p
+ end)
+ :get_or_throw()
+end
+
+---@param abs_path string
+function InstallContext:ensure_path_ownership(abs_path)
+ if path.is_subdirectory(self:cwd_manager(), abs_path) or self.destination_dir == abs_path then
+ return Result.success(abs_path)
+ else
+ return Result.failure(
+ ("Path %q is outside of current path ownership (%q)."):format(abs_path, settings.current.install_root_dir)
+ )
+ end
+end
+
+---@async
+function InstallContext:promote_cwd()
+ local cwd = self:cwd()
+ if self.destination_dir == cwd then
+ log.fmt_debug("cwd %s is already promoted (at %s)", cwd, self.destination_dir)
+ return Result.success "Current working dir is already in destination."
+ end
+ log.fmt_debug("Promoting cwd %s to %s", cwd, self.destination_dir)
+ return Result.run_catching(function()
+ -- 1. Remove destination dir, if it exists
+ if fs.dir_exists(self.destination_dir) then
+ fs.rmrf(self.destination_dir)
+ end
+ return self.destination_dir
+ end)
+ :map_catching(function(destination_dir)
+ -- 2. Prepare for renaming cwd to destination
+ if platform.is_unix then
+ -- Some Unix systems will raise an error when renaming a directory to a destination that does not already exist.
+ fs.mkdir(destination_dir)
+ end
+ return destination_dir
+ end)
+ :map_catching(function(destination_dir)
+ -- 3. Move the cwd to the final installation directory
+ fs.rename(cwd, destination_dir)
+ return destination_dir
+ end)
+ :map_catching(function(destination_dir)
+ -- 4. Update cwd
+ self:set_cwd(destination_dir)
+ end)
+end
+
+return InstallContext
diff --git a/lua/nvim-lsp-installer/core/fs.lua b/lua/nvim-lsp-installer/core/fs.lua
new file mode 100644
index 00000000..f3db813a
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/fs.lua
@@ -0,0 +1,68 @@
+local uv = require "nvim-lsp-installer.core.async.uv"
+local log = require "nvim-lsp-installer.log"
+local a = require "nvim-lsp-installer.core.async"
+
+local M = {}
+
+---@async
+---@param path string
+---@param contents string
+function M.append_file(path, contents)
+ local fd = uv.fs_open(path, "a", 438)
+ uv.fs_write(fd, contents, -1)
+ uv.fs_close(fd)
+end
+
+---@async
+---@param path string
+function M.file_exists(path)
+ local ok, fd = pcall(uv.fs_open, path, "r", 438)
+ if not ok then
+ return false
+ end
+ local fstat = uv.fs_fstat(fd)
+ uv.fs_close(fd)
+ return fstat.type == "file"
+end
+
+---@async
+---@param path string
+function M.dir_exists(path)
+ local ok, fd = pcall(uv.fs_open, path, "r", 438)
+ if not ok then
+ return false
+ end
+ local fstat = uv.fs_fstat(fd)
+ uv.fs_close(fd)
+ return fstat.type == "directory"
+end
+
+---@async
+---@param path string
+function M.rmrf(path)
+ log.debug("fs: rmrf", path)
+ if vim.in_fast_event() then
+ a.scheduler()
+ end
+ if vim.fn.delete(path, "rf") ~= 0 then
+ log.debug "fs: rmrf failed"
+ error(("rmrf: Could not remove directory %q."):format(path))
+ end
+end
+
+---@async
+---@param path string
+function M.mkdir(path)
+ log.debug("fs: mkdir", path)
+ uv.fs_mkdir(path, 493) -- 493(10) == 755(8)
+end
+
+---@async
+---@param path string
+---@param new_path string
+function M.rename(path, new_path)
+ log.debug("fs: rename", path, new_path)
+ uv.fs_rename(path, new_path)
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/cargo/init.lua b/lua/nvim-lsp-installer/core/managers/cargo/init.lua
new file mode 100644
index 00000000..29c07868
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/cargo/init.lua
@@ -0,0 +1,100 @@
+local process = require "nvim-lsp-installer.process"
+local path = require "nvim-lsp-installer.path"
+local spawn = require "nvim-lsp-installer.core.spawn"
+local a = require "nvim-lsp-installer.core.async"
+local Optional = require "nvim-lsp-installer.core.optional"
+local crates = require "nvim-lsp-installer.core.clients.crates"
+local Result = require "nvim-lsp-installer.core.result"
+
+local fetch_crate = a.promisify(crates.fetch_crate, true)
+
+local M = {}
+
+---@param crate string The crate to install.
+---@param opts {git:boolean, features:string|nil}
+function M.crate(crate, opts)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ opts = opts or {}
+ ctx.requested_version:if_present(function()
+ assert(not opts.git, "Providing a version when installing a git crate is not allowed.")
+ end)
+
+ ctx.receipt:with_primary_source(ctx.receipt.cargo(crate))
+
+ ctx.spawn.cargo {
+ "install",
+ "--root",
+ ".",
+ "--locked",
+ ctx.requested_version
+ :map(function(version)
+ return { "--version", version }
+ end)
+ :or_else(vim.NIL),
+ opts.features and { "--features", opts.features } or vim.NIL,
+ opts.git and { "--git", crate } or crate,
+ }
+ end
+end
+
+---@param output string @The `cargo install --list` output.
+---@return Record<string, string> @Key is the crate name, value is its version.
+function M.parse_installed_crates(output)
+ local installed_crates = {}
+ for _, line in ipairs(vim.split(output, "\n")) do
+ local name, version = line:match "^(.+)%s+v([.%S]+)[%s:]"
+ if name and version then
+ installed_crates[name] = version
+ end
+ end
+ return installed_crates
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.check_outdated_primary_package(receipt, install_dir)
+ local installed_version = M.get_installed_primary_package_version(receipt, install_dir):get_or_throw()
+
+ local response = fetch_crate(receipt.primary_source.package)
+ if installed_version ~= response.crate.max_stable_version then
+ return Result.success {
+ name = receipt.primary_source.package,
+ current_version = installed_version,
+ latest_version = response.crate.max_stable_version,
+ }
+ else
+ return Result.failure "Primary package is not outdated."
+ end
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.get_installed_primary_package_version(receipt, install_dir)
+ return spawn.cargo({
+ "install",
+ "--list",
+ "--root",
+ ".",
+ cwd = install_dir,
+ }):map_catching(function(result)
+ local installed_crates = M.parse_installed_crates(result.stdout)
+ if vim.in_fast_event() then
+ a.scheduler() -- needed because vim.fn.* call
+ end
+ local package = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
+ return Optional.of_nilable(installed_crates[package]):or_else_throw "Failed to find cargo package version."
+ end)
+end
+
+---@param install_dir string
+function M.env(install_dir)
+ return {
+ PATH = process.extend_path { path.concat { install_dir, "bin" } },
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/composer/init.lua b/lua/nvim-lsp-installer/core/managers/composer/init.lua
new file mode 100644
index 00000000..9a1e2dd4
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/composer/init.lua
@@ -0,0 +1,106 @@
+local Data = require "nvim-lsp-installer.data"
+local process = require "nvim-lsp-installer.process"
+local path = require "nvim-lsp-installer.path"
+local Result = require "nvim-lsp-installer.core.result"
+local spawn = require "nvim-lsp-installer.core.spawn"
+local Optional = require "nvim-lsp-installer.core.optional"
+
+local list_copy, list_find_first = Data.list_copy, Data.list_find_first
+
+local M = {}
+
+---@param packages string[] The composer 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.require(packages)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ local pkgs = list_copy(packages)
+
+ ctx.receipt:with_primary_source(ctx.receipt.composer(pkgs[1]))
+ for i = 2, #pkgs do
+ ctx.receipt:with_secondary_source(ctx.receipt.composer(pkgs[i]))
+ end
+
+ if not ctx.fs:file_exists "composer.json" then
+ ctx.spawn.composer { "init", "--no-interaction", "--stability=stable" }
+ end
+
+ ctx.requested_version:if_present(function(version)
+ pkgs[1] = ("%s:%s"):format(pkgs[1], version)
+ end)
+
+ ctx.spawn.composer { "require", pkgs }
+ end
+end
+
+function M.install()
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ ctx.spawn.composer {
+ "install",
+ "--no-interaction",
+ "--no-dev",
+ "--optimize-autoloader",
+ "--classmap-authoritative",
+ }
+ end
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.check_outdated_primary_package(receipt, install_dir)
+ if receipt.primary_source.type ~= "composer" then
+ return Result.failure "Receipt does not have a primary source of type composer"
+ end
+ return spawn.composer({
+ "outdated",
+ "--no-interaction",
+ "--format=json",
+ cwd = install_dir,
+ }):map_catching(function(result)
+ local outdated_packages = vim.json.decode(result.stdout)
+ local outdated_package = list_find_first(outdated_packages.installed, function(package)
+ return package.name == receipt.primary_source.package
+ end)
+ return Optional.of_nilable(outdated_package)
+ :map(function(package)
+ if package.version ~= package.latest then
+ return {
+ name = package.name,
+ current_version = package.version,
+ latest_version = package.latest,
+ }
+ end
+ end)
+ :or_else_throw "Primary package is not outdated."
+ end)
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.get_installed_primary_package_version(receipt, install_dir)
+ if receipt.primary_source.type ~= "composer" then
+ return Result.failure "Receipt does not have a primary source of type composer"
+ end
+ return spawn.composer({
+ "info",
+ "--format=json",
+ receipt.primary_source.package,
+ cwd = install_dir,
+ }):map_catching(function(result)
+ local info = vim.json.decode(result.stdout)
+ return info.versions[1]
+ end)
+end
+
+---@param install_dir string
+function M.env(install_dir)
+ return {
+ PATH = process.extend_path { path.concat { install_dir, "vendor", "bin" } },
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/dotnet/init.lua b/lua/nvim-lsp-installer/core/managers/dotnet/init.lua
new file mode 100644
index 00000000..c862146f
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/dotnet/init.lua
@@ -0,0 +1,31 @@
+local process = require "nvim-lsp-installer.process"
+local M = {}
+
+---@param package string
+function M.package(package)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ ctx.receipt:with_primary_source(ctx.receipt.dotnet(package))
+ ctx.spawn.dotnet {
+ "tool",
+ "update",
+ "--tool-path",
+ ".",
+ ctx.requested_version
+ :map(function(version)
+ return { "--version", version }
+ end)
+ :or_else(vim.NIL),
+ package,
+ }
+ end
+end
+
+function M.env(root_dir)
+ return {
+ PATH = process.extend_path { root_dir },
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/gem/init.lua b/lua/nvim-lsp-installer/core/managers/gem/init.lua
new file mode 100644
index 00000000..7b030797
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/gem/init.lua
@@ -0,0 +1,132 @@
+local Data = require "nvim-lsp-installer.data"
+local process = require "nvim-lsp-installer.process"
+local path = require "nvim-lsp-installer.path"
+local Result = require "nvim-lsp-installer.core.result"
+local spawn = require "nvim-lsp-installer.core.spawn"
+local Optional = require "nvim-lsp-installer.core.optional"
+
+local list_copy, list_find_first = Data.list_copy, Data.list_find_first
+
+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)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ local pkgs = list_copy(packages or {})
+
+ ctx.receipt:with_primary_source(ctx.receipt.gem(pkgs[1]))
+ for i = 2, #pkgs do
+ ctx.receipt:with_secondary_source(ctx.receipt.gem(pkgs[i]))
+ end
+
+ ctx.requested_version:if_present(function(version)
+ pkgs[1] = ("%s:%s"):format(pkgs[1], version)
+ end)
+
+ ctx.spawn.gem {
+ "install",
+ "--no-user-install",
+ "--install-dir=.",
+ "--bindir=bin",
+ "--no-document",
+ pkgs,
+ }
+ end
+end
+
+---@alias GemOutdatedPackage {name:string, current_version: string, latest_version: string}
+
+---Parses a string input like "package (0.1.0 < 0.2.0)" into its components
+---@param outdated_gem string
+---@return GemOutdatedPackage
+function M.parse_outdated_gem(outdated_gem)
+ local package_name, version_expression = outdated_gem:match "^(.+) %((.+)%)"
+ if not package_name or not version_expression then
+ -- unparseable
+ return nil
+ end
+ local current_version, latest_version = unpack(vim.split(version_expression, "<"))
+
+ ---@type GemOutdatedPackage
+ local outdated_package = {
+ name = vim.trim(package_name),
+ current_version = vim.trim(current_version),
+ latest_version = vim.trim(latest_version),
+ }
+ return outdated_package
+end
+
+---Parses the stdout of the `gem list` command into a Record<package_name, version>
+---@param output string
+function M.parse_gem_list_output(output)
+ ---@type Record<string, string>
+ local gem_versions = {}
+ for _, line in ipairs(vim.split(output, "\n")) do
+ local gem_package, version = line:match "^(%S+) %((%S+)%)$"
+ if gem_package and version then
+ gem_versions[gem_package] = version
+ end
+ end
+ return gem_versions
+end
+
+local function not_empty(s)
+ return s ~= nil and s ~= ""
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.check_outdated_primary_package(receipt, install_dir)
+ if receipt.primary_source.type ~= "gem" then
+ return Result.failure "Receipt does not have a primary source of type gem"
+ end
+ return spawn.gem({ "outdated", cwd = install_dir, env = process.graft_env(M.env(install_dir)) }):map_catching(
+ function(result)
+ ---@type string[]
+ local lines = vim.split(result.stdout, "\n")
+ local outdated_gems = vim.tbl_map(M.parse_outdated_gem, vim.tbl_filter(not_empty, lines))
+
+ local outdated_gem = list_find_first(outdated_gems, function(gem)
+ return gem.name == receipt.primary_source.package and gem.current_version ~= gem.latest_version
+ end)
+
+ return Optional.of_nilable(outdated_gem)
+ :map(function(gem)
+ return {
+ name = receipt.primary_source.package,
+ current_version = assert(gem.current_version),
+ latest_version = assert(gem.latest_version),
+ }
+ end)
+ :or_else_throw "Primary package is not outdated."
+ end
+ )
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.get_installed_primary_package_version(receipt, install_dir)
+ return spawn.gem({
+ "list",
+ cwd = install_dir,
+ env = process.graft_env(M.env(install_dir)),
+ }):map_catching(function(result)
+ local gems = M.parse_gem_list_output(result.stdout)
+ return Optional.of_nilable(gems[receipt.primary_source.package]):or_else_throw "Failed to find gem package version."
+ end)
+end
+
+---@param install_dir string
+function M.env(install_dir)
+ return {
+ GEM_HOME = install_dir,
+ GEM_PATH = install_dir,
+ PATH = process.extend_path { path.concat { install_dir, "bin" } },
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/git/init.lua b/lua/nvim-lsp-installer/core/managers/git/init.lua
new file mode 100644
index 00000000..bdd79917
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/git/init.lua
@@ -0,0 +1,54 @@
+local spawn = require "nvim-lsp-installer.core.spawn"
+local Result = require "nvim-lsp-installer.core.result"
+local M = {}
+
+---@param opts {[1]:string} @The first item in the table is the repository to clone.
+function M.clone(opts)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ local repo = assert(opts[1], "No git URL provided.")
+ ctx.spawn.git { "clone", "--depth", "1", repo, "." }
+ ctx.requested_version:if_present(function(version)
+ ctx.spawn.git { "fetch", "--depth", "1", "origin", version }
+ ctx.spawn.git { "checkout", "FETCH_HEAD" }
+ end)
+ ctx.receipt:with_primary_source(ctx.receipt.git_remote(repo))
+ end
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.check_outdated_git_clone(receipt, install_dir)
+ if receipt.primary_source.type ~= "git" then
+ return Result.failure "Receipt does not have a primary source of type git"
+ end
+ return spawn.git({ "fetch", "origin", "HEAD", cwd = install_dir }):map_catching(function()
+ local result = spawn.git({ "rev-parse", "FETCH_HEAD", "HEAD", cwd = install_dir }):get_or_throw()
+ local remote_head, local_head = unpack(vim.split(result.stdout, "\n"))
+ if remote_head == local_head then
+ error("Git clone is up to date.", 2)
+ end
+ return {
+ name = receipt.primary_source.remote,
+ current_version = assert(local_head),
+ latest_version = assert(remote_head),
+ }
+ end)
+end
+
+---@async
+---@param install_dir string
+function M.get_installed_revision(install_dir)
+ return spawn.git({
+ "rev-parse",
+ "--short",
+ "HEAD",
+ cwd = install_dir,
+ }):map_catching(function(result)
+ return assert(vim.trim(result.stdout))
+ end)
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/go/init.lua b/lua/nvim-lsp-installer/core/managers/go/init.lua
new file mode 100644
index 00000000..167cfff4
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/go/init.lua
@@ -0,0 +1,109 @@
+local process = require "nvim-lsp-installer.process"
+local spawn = require "nvim-lsp-installer.core.spawn"
+local a = require "nvim-lsp-installer.core.async"
+local Optional = require "nvim-lsp-installer.core.optional"
+
+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)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ local env = process.graft_env {
+ GOBIN = ctx.cwd(),
+ }
+ -- Install the head package
+ do
+ local head_package = packages[1]
+ ctx.receipt:with_primary_source(ctx.receipt.go(head_package))
+ local version = ctx.requested_version:or_else "latest"
+ ctx.spawn.go {
+ "install",
+ "-v",
+ ("%s@%s"):format(head_package, version),
+ env = env,
+ }
+ end
+
+ -- Install secondary packages
+ for i = 2, #packages do
+ local package = packages[i]
+ ctx.receipt:with_secondary_source(ctx.receipt.go(package))
+ ctx.spawn.go { "install", "-v", ("%s@latest"):format(package), env = env }
+ end
+ end
+end
+
+---@param output string @The output from `go version -m` command.
+function M.parse_mod_version_output(output)
+ ---@type {path: string[], mod: string[], dep: string[], build: string[]}
+ local result = {}
+ local lines = vim.split(output, "\n")
+ for _, line in ipairs { unpack(lines, 2) } do
+ local type, id, value = unpack(vim.split(line, "%s+", { trimempty = true }))
+ if type and id then
+ result[type] = result[type] or {}
+ result[type][id] = value or ""
+ end
+ end
+ return result
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.get_installed_primary_package_version(receipt, install_dir)
+ if vim.in_fast_event() then
+ a.scheduler()
+ end
+ -- trims e.g. golang.org/x/tools/gopls to gopls
+ local executable = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
+ return spawn.go({
+ "version",
+ "-m",
+ executable,
+ cwd = install_dir,
+ }):map_catching(function(result)
+ local parsed_output = M.parse_mod_version_output(result.stdout)
+ return Optional.of_nilable(parsed_output.mod[receipt.primary_source.package]):or_else_throw "Failed to parse mod version"
+ end)
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.check_outdated_primary_package(receipt, install_dir)
+ return spawn.go({
+ "list",
+ "-json",
+ "-m",
+ "-versions",
+ receipt.primary_source.package,
+ cwd = install_dir,
+ }):map_catching(function(result)
+ ---@type {Path: string, Versions: string[]}
+ local output = vim.json.decode(result.stdout)
+ return Optional.of_nilable(output.Versions[#output.Versions])
+ :map(function(latest_version)
+ local installed_version = M.get_installed_primary_package_version(receipt, install_dir):get_or_throw()
+ if installed_version ~= latest_version then
+ return {
+ name = receipt.primary_source.package,
+ current_version = assert(installed_version),
+ latest_version = assert(latest_version),
+ }
+ end
+ end)
+ :or_else_throw "Primary package is not outdated."
+ end)
+end
+
+---@param install_dir string
+function M.env(install_dir)
+ return {
+ PATH = process.extend_path { install_dir },
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/npm/init.lua b/lua/nvim-lsp-installer/core/managers/npm/init.lua
new file mode 100644
index 00000000..c3be01da
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/npm/init.lua
@@ -0,0 +1,129 @@
+local Data = require "nvim-lsp-installer.data"
+local spawn = require "nvim-lsp-installer.core.spawn"
+local Optional = require "nvim-lsp-installer.core.optional"
+local Result = require "nvim-lsp-installer.core.result"
+local process = require "nvim-lsp-installer.process"
+local path = require "nvim-lsp-installer.path"
+
+local list_copy = Data.list_copy
+
+local M = {}
+
+---@async
+---@param ctx InstallContext
+local function ensure_npm_root(ctx)
+ if not (ctx.fs:dir_exists "node_modules" or ctx.fs:file_exists "package.json") then
+ -- Create a package.json to set a boundary for where npm installs packages.
+ ctx.spawn.npm { "init", "--yes", "--scope=lsp-installer" }
+ end
+end
+
+---@param packages string[] @The npm packages to install. The first item in this list will be the recipient of the requested version, if set.
+function M.packages(packages)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ local pkgs = list_copy(packages)
+
+ -- Use global-style. The reasons for this are:
+ -- a) To avoid polluting the executables (aka bin-links) that npm creates.
+ -- b) The installation is, after all, more similar to a "global" installation. We don't really gain
+ -- any of the benefits of not using global style (e.g., deduping the dependency tree).
+ --
+ -- We write to .npmrc manually instead of going through npm because managing a local .npmrc file
+ -- is a bit unreliable across npm versions (especially <7), so we take extra measures to avoid
+ -- inadvertently polluting global npm config.
+ ctx.fs:append_file(".npmrc", "global-style=true")
+
+ ctx.receipt:with_primary_source(ctx.receipt.npm(pkgs[1]))
+ for i = 2, #pkgs do
+ ctx.receipt:with_secondary_source(ctx.receipt.npm(pkgs[i]))
+ end
+
+ ctx.requested_version:if_present(function(version)
+ pkgs[1] = ("%s@%s"):format(pkgs[1], version)
+ end)
+
+ M.install(pkgs)(ctx)
+ end
+end
+
+---@param packages string[] @The npm packages to install.
+function M.install(packages)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ ensure_npm_root(ctx)
+ ctx.spawn.npm { "install", packages }
+ end
+end
+
+---@param exec_args string[] @The arguments to pass to npm exec.
+function M.exec(exec_args)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ ctx.spawn.npm { "exec", "--yes", exec_args }
+ end
+end
+
+---@param script string @The npm script to run.
+function M.run(script)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ ctx.spawn.npm { "run", script }
+ end
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.get_installed_primary_package_version(receipt, install_dir)
+ if receipt.primary_source.type ~= "npm" then
+ return Result.failure "Receipt does not have a primary source of type npm"
+ end
+ return spawn.npm({ "ls", "--json", cwd = install_dir }):map_catching(function(result)
+ local npm_packages = vim.json.decode(result.stdout)
+ return npm_packages.dependencies[receipt.primary_source.package].version
+ end)
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.check_outdated_primary_package(receipt, install_dir)
+ if receipt.primary_source.type ~= "npm" then
+ return Result.failure "Receipt does not have a primary source of type npm"
+ end
+ local primary_package = receipt.primary_source.package
+ local npm_outdated = spawn.npm { "outdated", "--json", primary_package, cwd = install_dir }
+ if npm_outdated:is_success() then
+ return Result.failure "Primary package is not outdated."
+ end
+ return npm_outdated:recover_catching(function(result)
+ assert(result.exit_code == 1, "Expected npm outdated to return exit code 1.")
+ local data = vim.json.decode(result.stdout)
+
+ return Optional.of_nilable(data[primary_package])
+ :map(function(outdated_package)
+ if outdated_package.current ~= outdated_package.latest then
+ return {
+ name = primary_package,
+ current_version = assert(outdated_package.current),
+ latest_version = assert(outdated_package.latest),
+ }
+ end
+ end)
+ :or_else_throw()
+ end)
+end
+
+---@param install_dir string
+function M.env(install_dir)
+ return {
+ PATH = process.extend_path { path.concat { install_dir, "node_modules", ".bin" } },
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/opam/init.lua b/lua/nvim-lsp-installer/core/managers/opam/init.lua
new file mode 100644
index 00000000..8fd60de7
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/opam/init.lua
@@ -0,0 +1,41 @@
+local Data = require "nvim-lsp-installer.data"
+local path = require "nvim-lsp-installer.path"
+local process = require "nvim-lsp-installer.process"
+
+local M = {}
+
+local list_copy = Data.list_copy
+
+---@param packages string[] @The opam packages to install. The first item in this list will be the recipient of the requested version, if set.
+function M.packages(packages)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ local pkgs = list_copy(packages)
+
+ ctx.receipt:with_primary_source(ctx.receipt.opam(pkgs[1]))
+ for i = 2, #pkgs do
+ ctx.receipt:with_secondary_source(ctx.receipt.opam(pkgs[i]))
+ end
+
+ ctx.requested_version:if_present(function(version)
+ pkgs[1] = ("%s.%s"):format(pkgs[1], version)
+ end)
+
+ ctx.spawn.opam {
+ "install",
+ "--destdir=.",
+ "--yes",
+ "--verbose",
+ pkgs,
+ }
+ end
+end
+
+function M.env(root_dir)
+ return {
+ PATH = process.extend_path { path.concat { root_dir, "bin" } },
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/managers/pip3/init.lua b/lua/nvim-lsp-installer/core/managers/pip3/init.lua
new file mode 100644
index 00000000..61130a96
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/managers/pip3/init.lua
@@ -0,0 +1,144 @@
+local Data = require "nvim-lsp-installer.data"
+local settings = require "nvim-lsp-installer.settings"
+local process = require "nvim-lsp-installer.process"
+local path = require "nvim-lsp-installer.path"
+local platform = require "nvim-lsp-installer.platform"
+local Optional = require "nvim-lsp-installer.core.optional"
+local Result = require "nvim-lsp-installer.core.result"
+local spawn = require "nvim-lsp-installer.core.spawn"
+
+local list_find_first, list_copy, list_not_nil = Data.list_find_first, Data.list_copy, Data.list_not_nil
+local VENV_DIR = "venv"
+
+local M = {}
+
+---@param packages string[] @The pip packages to install. The first item in this list will be the recipient of the requested version, if set.
+function M.packages(packages)
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ local pkgs = list_copy(packages)
+
+ ctx.receipt:with_primary_source(ctx.receipt.pip3(pkgs[1]))
+ for i = 2, #pkgs do
+ ctx.receipt:with_secondary_source(ctx.receipt.pip3(pkgs[i]))
+ end
+
+ ctx.requested_version:if_present(function(version)
+ pkgs[1] = ("%s==%s"):format(pkgs[1], version)
+ end)
+
+ local executables = platform.is_win and list_not_nil(vim.g.python3_host_prog, "python", "python3")
+ or list_not_nil(vim.g.python3_host_prog, "python3", "python")
+
+ -- pip3 will hardcode the full path to venv executables, so we need to promote cwd to make sure pip uses the final destination path.
+ ctx:promote_cwd():get_or_throw()
+
+ -- Find first executable that manages to create venv
+ local executable = list_find_first(executables, function(executable)
+ return pcall(ctx.spawn[executable], { "-m", "venv", VENV_DIR })
+ end)
+
+ Optional.of_nilable(executable)
+ :if_present(function(python3)
+ ctx.spawn[python3] {
+ env = process.graft_env(M.env(ctx:cwd())), -- use venv env
+ "-m",
+ "pip",
+ "install",
+ "-U",
+ settings.current.pip.install_args,
+ pkgs,
+ }
+ end)
+ :or_else_throw "Unable to create python3 venv environment."
+ end
+end
+
+---@param package string
+---@return string
+function M.normalize_package(package)
+ -- https://stackoverflow.com/a/60307740
+ local s = package:gsub("%[.*%]", "")
+ return s
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.check_outdated_primary_package(receipt, install_dir)
+ if receipt.primary_source.type ~= "pip3" then
+ return Result.failure "Receipt does not have a primary source of type pip3"
+ end
+ local normalized_package = M.normalize_package(receipt.primary_source.package)
+ return spawn.python({
+ "-m",
+ "pip",
+ "list",
+ "--outdated",
+ "--format=json",
+ cwd = install_dir,
+ env = process.graft_env(M.env(install_dir)), -- use venv
+ }):map_catching(function(result)
+ ---@alias PipOutdatedPackage {name: string, version: string, latest_version: string}
+ ---@type PipOutdatedPackage[]
+ local packages = vim.json.decode(result.stdout)
+
+ local outdated_primary_package = list_find_first(packages, function(outdated_package)
+ return outdated_package.name == normalized_package
+ and outdated_package.version ~= outdated_package.latest_version
+ end)
+
+ return Optional.of_nilable(outdated_primary_package)
+ :map(function(package)
+ return {
+ name = normalized_package,
+ current_version = assert(package.version),
+ latest_version = assert(package.latest_version),
+ }
+ end)
+ :or_else_throw "Primary package is not outdated."
+ end)
+end
+
+---@async
+---@param receipt InstallReceipt
+---@param install_dir string
+function M.get_installed_primary_package_version(receipt, install_dir)
+ if receipt.primary_source.type ~= "pip3" then
+ return Result.failure "Receipt does not have a primary source of type pip3"
+ end
+ return spawn.python({
+ "-m",
+ "pip",
+ "list",
+ "--format=json",
+ cwd = install_dir,
+ env = process.graft_env(M.env(install_dir)), -- use venv env
+ }):map_catching(function(result)
+ local pip_packages = vim.json.decode(result.stdout)
+ local normalized_pip_package = M.normalize_package(receipt.primary_source.package)
+ local pip_package = list_find_first(pip_packages, function(package)
+ return package.name == normalized_pip_package
+ end)
+ return Optional.of_nilable(pip_package)
+ :map(function(package)
+ return package.version
+ end)
+ :or_else_throw "Unable to find pip package."
+ end)
+end
+
+---@param install_dir string
+function M.env(install_dir)
+ return {
+ PATH = process.extend_path { M.venv_path(install_dir) },
+ }
+end
+
+---@param install_dir string
+function M.venv_path(install_dir)
+ return path.concat { install_dir, VENV_DIR, platform.is_win and "Scripts" or "bin" }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/core/optional.lua b/lua/nvim-lsp-installer/core/optional.lua
new file mode 100644
index 00000000..d37a7e69
--- /dev/null
+++ b/lua/nvim-lsp-installer/core/optional.lua
@@ -0,0 +1,90 @@
+---@class Optional
+---@field private _value unknown
+local Optional = {}
+Optional.__index = Optional
+
+---@param value any
+function Optional.new(value)
+ return setmetatable({ _value = value }, Optional)
+end
+
+local EMPTY = Optional.new(nil)
+
+---@param value any
+function Optional.of_nilable(value)
+ if value == nil then
+ return EMPTY
+ else
+ return Optional.new(value)
+ end
+end
+
+function Optional.empty()
+ return EMPTY
+end
+
+---@param value any
+function Optional.of(value)
+ return Optional.new(value)
+end
+
+---@param mapper_fn fun(value: any): any
+function Optional:map(mapper_fn)
+ if self:is_present() then
+ return Optional.of_nilable(mapper_fn(self._value))
+ else
+ return EMPTY
+ end
+end
+
+function Optional:get()
+ if not self:is_present() then
+ error("No value present.", 2)
+ end
+ return self._value
+end
+
+---@param value any
+function Optional:or_else(value)
+ if self:is_present() then
+ return self._value
+ else
+ return value
+ end
+end
+
+---@param supplier fun(): Optional
+function Optional:when_empty(supplier)
+ if self:is_present() then
+ return self._value
+ else
+ return supplier()
+ end
+end
+
+---@param exception any @(optional) The exception to throw if the result is a failure.
+function Optional:or_else_throw(exception)
+ if self:is_present() then
+ return self._value
+ else
+ if exception then
+ error(exception, 2)
+ else
+ error("No value present.", 2)
+ end
+ end
+end
+
+---@param fn fun(value: any)
+function Optional:if_present(fn)
+ if self:is_present() then
+ fn(self._value)
+ end
+ return self
+end
+
+function Optional:is_present()
+ return self._value ~= nil
+end
+
+return Optional
diff --git a/lua/nvim-lsp-installer/core/result.lua b/lua/nvim-lsp-installer/core/result.lua
index a94ffe52..f5c2d747 100644
--- a/lua/nvim-lsp-installer/core/result.lua
+++ b/lua/nvim-lsp-installer/core/result.lua
@@ -32,11 +32,24 @@ function Result:get_or_nil()
end
end
-function Result:get_or_throw()
+function Result:get_or_else(value)
if self:is_success() then
return self.value
else
- error(self.value.error, 2)
+ return value
+ end
+end
+
+---@param exception any @(optional) The exception to throw if the result is a failure.
+function Result:get_or_throw(exception)
+ if self:is_success() then
+ return self.value
+ else
+ if exception ~= nil then
+ error(exception, 2)
+ else
+ error(self.value.error, 2)
+ end
end
end
@@ -77,4 +90,38 @@ function Result:map_catching(mapper_fn)
end
end
+---@param recover_fn fun(value: any): any
+function Result:recover(recover_fn)
+ if self:is_failure() then
+ return Result.success(recover_fn(self:err_or_nil()))
+ else
+ return self
+ end
+end
+
+---@param recover_fn fun(value: any): any
+function Result:recover_catching(recover_fn)
+ if self:is_failure() then
+ local ok, value = pcall(recover_fn, self:err_or_nil())
+ if ok then
+ return Result.success(value)
+ else
+ return Result.failure(value)
+ end
+ else
+ return self
+ end
+end
+
+---@param fn fun(): any
+---@return Result
+function Result.run_catching(fn)
+ local ok, result = pcall(fn)
+ if ok then
+ return Result.success(result)
+ else
+ return Result.failure(result)
+ end
+end
+
return Result
diff --git a/lua/nvim-lsp-installer/core/async/spawn.lua b/lua/nvim-lsp-installer/core/spawn.lua
index 5fc7eee7..992b1557 100644
--- a/lua/nvim-lsp-installer/core/async/spawn.lua
+++ b/lua/nvim-lsp-installer/core/spawn.lua
@@ -5,11 +5,22 @@ local platform = require "nvim-lsp-installer.platform"
local async_spawn = a.promisify(process.spawn)
----@type Record<string, fun(opts: JobSpawnOpts): Result>
+---@alias JobSpawn Record<string, async fun(opts: JobSpawnOpts): Result>
+---@type JobSpawn
local spawn = {
- aliases = {
+ _aliases = {
npm = platform.is_win and "npm.cmd" or "npm",
+ gem = platform.is_win and "gem.cmd" or "gem",
+ composer = platform.is_win and "composer.bat" or "composer",
},
+ -- Utility function for optionally including arguments.
+ ---@generic T
+ ---@param condition boolean
+ ---@param value T
+ ---@return T
+ _when = function(condition, value)
+ return condition and value or vim.NIL
+ end,
}
local function Failure(err, cmd)
@@ -22,10 +33,15 @@ end
setmetatable(spawn, {
__index = function(self, k)
+ ---@param args string|nil|string[][]
return function(args)
local cmd_args = {}
- for i, arg in ipairs(args) do
- cmd_args[i] = arg
+ for _, arg in ipairs(args) do
+ if type(arg) == "table" then
+ vim.list_extend(cmd_args, arg)
+ elseif arg ~= vim.NIL then
+ cmd_args[#cmd_args + 1] = arg
+ end
end
---@type JobSpawnOpts
local spawn_args = {
@@ -41,7 +57,7 @@ setmetatable(spawn, {
spawn_args.stdio_sink = stdio.sink
end
- local cmd = self.aliases[k] or k
+ local cmd = self._aliases[k] or k
local _, exit_code = async_spawn(cmd, spawn_args)
if exit_code == 0 then
diff --git a/lua/nvim-lsp-installer/installers/npm.lua b/lua/nvim-lsp-installer/installers/npm.lua
index f891589e..12ca407f 100644
--- a/lua/nvim-lsp-installer/installers/npm.lua
+++ b/lua/nvim-lsp-installer/installers/npm.lua
@@ -1,3 +1,4 @@
+---@deprecated Will be replaced by core.managers.npm
local path = require "nvim-lsp-installer.path"
local fs = require "nvim-lsp-installer.fs"
local Data = require "nvim-lsp-installer.data"
diff --git a/lua/nvim-lsp-installer/installers/pip3.lua b/lua/nvim-lsp-installer/installers/pip3.lua
index 9476318d..67032a40 100644
--- a/lua/nvim-lsp-installer/installers/pip3.lua
+++ b/lua/nvim-lsp-installer/installers/pip3.lua
@@ -1,3 +1,4 @@
+---@deprecated Will be replaced by core.managers.pip3
local path = require "nvim-lsp-installer.path"
local Data = require "nvim-lsp-installer.data"
local installers = require "nvim-lsp-installer.installers"
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/cargo.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/cargo.lua
deleted file mode 100644
index 19fe7448..00000000
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/cargo.lua
+++ /dev/null
@@ -1,59 +0,0 @@
-local process = require "nvim-lsp-installer.process"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
-local crates = require "nvim-lsp-installer.core.clients.crates"
-
----@param output string The `cargo install --list` output.
-local function parse_installed_crates(output)
- local installed_crates = {}
- for _, line in ipairs(vim.split(output, "\n")) do
- local name, version = line:match "^(.+)%s+v([.%S]+)[%s:]"
- if name and version then
- installed_crates[name] = version
- end
- end
- return installed_crates
-end
-
----@param server Server
----@param source InstallReceiptSource
----@param on_result fun(result: VersionCheckResult)
-local function cargo_check(server, source, on_result)
- local stdio = process.in_memory_sink()
- process.spawn("cargo", {
- args = { "install", "--list", "--root", "." },
- cwd = server.root_dir,
- stdio_sink = stdio.sink,
- }, function(success)
- if not success then
- return on_result(VersionCheckResult.fail(server))
- end
- local installed_crates = parse_installed_crates(table.concat(stdio.buffers.stdout, ""))
- if not installed_crates[source.package] then
- return on_result(VersionCheckResult.fail(server))
- end
- crates.fetch_crate(source.package, function(err, response)
- if err then
- return on_result(VersionCheckResult.fail(server))
- end
- if response.crate.max_stable_version ~= installed_crates[source.package] then
- return on_result(VersionCheckResult.success(server, {
- {
- name = source.package,
- current_version = installed_crates[source.package],
- latest_version = response.crate.max_stable_version,
- },
- }))
- else
- return on_result(VersionCheckResult.empty(server))
- end
- end)
- end)
-end
-
-return setmetatable({
- parse_installed_crates = parse_installed_crates,
-}, {
- __call = function(_, ...)
- return cargo_check(...)
- end,
-})
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/composer.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/composer.lua
deleted file mode 100644
index b8e75424..00000000
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/composer.lua
+++ /dev/null
@@ -1,42 +0,0 @@
-local process = require "nvim-lsp-installer.process"
-local composer = require "nvim-lsp-installer.installers.composer"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
-
----@param server Server
----@param source InstallReceiptSource
----@param on_check_complete fun(result: VersionCheckResult)
-local function composer_checker(server, source, on_check_complete)
- local stdio = process.in_memory_sink()
- process.spawn(composer.composer_cmd, {
- args = { "outdated", "--no-interaction", "--format=json" },
- cwd = server.root_dir,
- stdio_sink = stdio.sink,
- }, function(success)
- if not success then
- return on_check_complete(VersionCheckResult.fail(server))
- end
- ---@type {installed: {name: string, version: string, latest: string}[]}
- local decode_ok, outdated_json = pcall(vim.json.decode, table.concat(stdio.buffers.stdout, ""))
-
- if not decode_ok then
- return on_check_complete(VersionCheckResult.fail(server))
- end
-
- ---@type OutdatedPackage[]
- local outdated_packages = {}
-
- for _, outdated_package in ipairs(outdated_json.installed) do
- if outdated_package.name == source.package and outdated_package.version ~= outdated_package.latest then
- table.insert(outdated_packages, {
- name = outdated_package.name,
- current_version = outdated_package.version,
- latest_version = outdated_package.latest,
- })
- end
- end
-
- on_check_complete(VersionCheckResult.success(server, outdated_packages))
- end)
-end
-
-return composer_checker
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua
deleted file mode 100644
index cf880fd0..00000000
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua
+++ /dev/null
@@ -1,95 +0,0 @@
-local process = require "nvim-lsp-installer.process"
-local gem = require "nvim-lsp-installer.installers.gem"
-local log = require "nvim-lsp-installer.log"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
-
-local function not_empty(s)
- return s ~= nil and s ~= ""
-end
-
----Parses a string input like "package (0.1.0 < 0.2.0)" into its components
----@param outdated_gem string
----@return GemOutdatedPackage
-local function parse_outdated_gem(outdated_gem)
- local package_name, version_expression = outdated_gem:match "^(.+) %((.+)%)"
- if not package_name or not version_expression then
- -- unparseable
- return nil
- end
- local current_version, latest_version = unpack(vim.split(version_expression, "<"))
-
- ---@alias GemOutdatedPackage {name:string, current_version: string, latest_version: string}
- local outdated_package = {
- name = vim.trim(package_name),
- current_version = vim.trim(current_version),
- latest_version = vim.trim(latest_version),
- }
- return outdated_package
-end
-
----@param output string
-local function parse_gem_list_output(output)
- ---@type Record<string, string>
- local gem_versions = {}
- for _, line in ipairs(vim.split(output, "\n")) do
- local gem_package, version = line:match "^(%S+) %((%S+)%)$"
- if gem_package and version then
- gem_versions[gem_package] = version
- end
- end
- return gem_versions
-end
-
----@param server Server
----@param source InstallReceiptSource
----@param on_check_complete fun(result: VersionCheckResult)
-local function gem_checker(server, source, on_check_complete)
- local stdio = process.in_memory_sink()
- process.spawn(
- "gem",
- {
- args = { "outdated" },
- cwd = server.root_dir,
- stdio_sink = stdio.sink,
- env = process.graft_env(gem.env(server.root_dir)),
- },
- vim.schedule_wrap(function(success)
- if not success then
- return on_check_complete(VersionCheckResult.fail(server))
- end
- ---@type string[]
- local lines = vim.split(table.concat(stdio.buffers.stdout, ""), "\n")
- log.trace("Gem outdated lines output", lines)
- local outdated_gems = vim.tbl_map(parse_outdated_gem, vim.tbl_filter(not_empty, lines))
- log.trace("Gem outdated packages", outdated_gems)
-
- ---@type OutdatedPackage[]
- local outdated_packages = {}
-
- for _, outdated_gem in ipairs(outdated_gems) do
- if
- outdated_gem.name == source.package
- and outdated_gem.current_version ~= outdated_gem.latest_version
- then
- table.insert(outdated_packages, {
- name = outdated_gem.name,
- current_version = outdated_gem.current_version,
- latest_version = outdated_gem.latest_version,
- })
- end
- end
-
- on_check_complete(VersionCheckResult.success(server, outdated_packages))
- end)
- )
-end
-
--- to allow tests to access internals
-return setmetatable({
- parse_outdated_gem = parse_outdated_gem,
- parse_gem_list_output = parse_gem_list_output,
-}, {
- __call = function(_, ...)
- return gem_checker(...)
- end,
-})
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/git.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/git.lua
deleted file mode 100644
index 74007abc..00000000
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/git.lua
+++ /dev/null
@@ -1,42 +0,0 @@
-local process = require "nvim-lsp-installer.process"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
-
----@param server Server
----@param source InstallReceiptSource
----@param on_check_complete fun(result: VersionCheckResult)
-return function(server, source, on_check_complete)
- process.spawn("git", {
- -- We assume git installation track the remote HEAD branch
- args = { "fetch", "origin", "HEAD" },
- cwd = server.root_dir,
- stdio_sink = process.empty_sink(),
- }, function(fetch_success)
- local stdio = process.in_memory_sink()
- if not fetch_success then
- return on_check_complete(VersionCheckResult.fail(server))
- end
- process.spawn("git", {
- args = { "rev-parse", "FETCH_HEAD", "HEAD" },
- cwd = server.root_dir,
- stdio_sink = stdio.sink,
- }, function(success)
- if success then
- local stdout = table.concat(stdio.buffers.stdout, "")
- local remote_head, local_head = unpack(vim.split(stdout, "\n"))
- if remote_head ~= local_head then
- on_check_complete(VersionCheckResult.success(server, {
- {
- name = source.remote,
- latest_version = remote_head,
- current_version = local_head,
- },
- }))
- else
- on_check_complete(VersionCheckResult.empty(server))
- end
- else
- on_check_complete(VersionCheckResult.fail(server))
- end
- end)
- end)
-end
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua
index 37e30798..527a37ff 100644
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua
+++ b/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua
@@ -1,29 +1,22 @@
+local a = require "nvim-lsp-installer.core.async"
+local Result = require "nvim-lsp-installer.core.result"
local github = require "nvim-lsp-installer.core.clients.github"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
----@param server Server
----@param source InstallReceiptSource
----@param on_result fun(result: VersionCheckResult)
-return function(server, source, on_result)
- github.fetch_latest_release(
- source.repo,
- { tag_name_pattern = source.tag_name_pattern },
- function(err, latest_release)
- if err then
- return on_result(VersionCheckResult.fail(server))
- end
+local fetch_latest_release = a.promisify(github.fetch_latest_release, true)
- if source.release ~= latest_release.tag_name then
- return on_result(VersionCheckResult.success(server, {
- {
- name = source.repo,
- current_version = source.release,
- latest_version = latest_release.tag_name,
- },
- }))
- else
- return on_result(VersionCheckResult.empty(server))
- end
+---@async
+---@param receipt InstallReceipt
+return function(receipt)
+ local source = receipt.primary_source
+ return Result.run_catching(function()
+ local latest_release = fetch_latest_release(source.repo, { tag_name_pattern = source.tag_name_pattern })
+ if source.release ~= latest_release.tag_name then
+ return {
+ name = source.repo,
+ current_version = source.release,
+ latest_version = latest_release.tag_name,
+ }
end
- )
+ error "Primary package is not outdated."
+ end)
end
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua
index 6f45b4ba..f9df2438 100644
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua
+++ b/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua
@@ -1,25 +1,23 @@
+local a = require "nvim-lsp-installer.core.async"
+local Result = require "nvim-lsp-installer.core.result"
local github = require "nvim-lsp-installer.core.clients.github"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
----@param server Server
----@param source InstallReceiptSource
----@param on_result fun(result: VersionCheckResult)
-return function(server, source, on_result)
- github.fetch_latest_tag(source.repo, function(err, latest_tag)
- if err then
- return on_result(VersionCheckResult.fail(server))
- end
+local fetch_latest_tag = a.promisify(github.fetch_latest_tag, true)
+
+---@async
+---@param receipt InstallReceipt
+return function(receipt)
+ local source = receipt.primary_source
+ return Result.run_catching(function()
+ local latest_tag = fetch_latest_tag(source.repo)
if source.tag ~= latest_tag.name then
- return on_result(VersionCheckResult.success(server, {
- {
- name = source.repo,
- current_version = source.tag,
- latest_version = latest_tag.name,
- },
- }))
- else
- return on_result(VersionCheckResult.empty(server))
+ return {
+ name = source.repo,
+ current_version = source.tag,
+ latest_version = latest_tag.name,
+ }
end
+ error "Primary package is not outdated."
end)
end
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua
index b892e847..e50ca9fd 100644
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua
+++ b/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua
@@ -1,16 +1,18 @@
+local a = require "nvim-lsp-installer.core.async"
local JobExecutionPool = require "nvim-lsp-installer.jobs.pool"
local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
local log = require "nvim-lsp-installer.log"
-local npm_check = require "nvim-lsp-installer.jobs.outdated-servers.npm"
-local cargo_check = require "nvim-lsp-installer.jobs.outdated-servers.cargo"
-local pip3_check = require "nvim-lsp-installer.jobs.outdated-servers.pip3"
-local gem_check = require "nvim-lsp-installer.jobs.outdated-servers.gem"
-local git_check = require "nvim-lsp-installer.jobs.outdated-servers.git"
+local npm = require "nvim-lsp-installer.core.managers.npm"
+local pip3 = require "nvim-lsp-installer.core.managers.pip3"
+local git = require "nvim-lsp-installer.core.managers.git"
+local gem = require "nvim-lsp-installer.core.managers.gem"
+local go = require "nvim-lsp-installer.core.managers.go"
+local cargo = require "nvim-lsp-installer.core.managers.cargo"
+local composer = require "nvim-lsp-installer.core.managers.composer"
local github_release_file_check = require "nvim-lsp-installer.jobs.outdated-servers.github_release_file"
local github_tag_check = require "nvim-lsp-installer.jobs.outdated-servers.github_tag"
-local jdtls = require "nvim-lsp-installer.jobs.outdated-servers.jdtls"
-local composer_check = require "nvim-lsp-installer.jobs.outdated-servers.composer"
+local jdtls_check = require "nvim-lsp-installer.jobs.outdated-servers.jdtls"
local M = {}
@@ -18,27 +20,18 @@ local jobpool = JobExecutionPool:new {
size = 4,
}
-local function noop(server, _, on_result)
- on_result(VersionCheckResult.empty(server))
-end
-
----@type Record<InstallReceiptSourceType, function>
+---@type Record<InstallReceiptSourceType, async fun(receipt: InstallReceipt, install_dir: string): Result>
local checkers = {
- ["npm"] = npm_check,
- ["pip3"] = pip3_check,
- ["cargo"] = cargo_check,
- ["gem"] = gem_check,
- ["composer"] = composer_check,
- ["go"] = noop, -- TODO
- ["dotnet"] = noop, -- TODO
- ["r_package"] = noop, -- TODO
- ["unmanaged"] = noop,
- ["system"] = noop,
- ["jdtls"] = jdtls,
- ["git"] = git_check,
+ ["npm"] = npm.check_outdated_primary_package,
+ ["pip3"] = pip3.check_outdated_primary_package,
+ ["git"] = git.check_outdated_git_clone,
+ ["cargo"] = cargo.check_outdated_primary_package,
+ ["composer"] = composer.check_outdated_primary_package,
+ ["gem"] = gem.check_outdated_primary_package,
+ ["go"] = go.check_outdated_primary_package,
+ ["jdtls"] = jdtls_check,
["github_release_file"] = github_release_file_check,
["github_tag"] = github_tag_check,
- ["opam"] = noop,
}
local pending_servers = {}
@@ -73,7 +66,13 @@ function M.identify_outdated_servers(servers, on_result)
local checker = checkers[receipt.primary_source.type]
if checker then
- checker(server, receipt.primary_source, complete)
+ a.run(checker, function(success, result)
+ if success and result:is_success() then
+ complete(VersionCheckResult.success(server, { result:get_or_nil() }))
+ else
+ complete(VersionCheckResult.fail(server))
+ end
+ end, receipt, server.root_dir)
else
complete(VersionCheckResult.empty(server))
log.fmt_error("Unable to find checker for source=%s", receipt.primary_source.type)
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua
index bb40714a..3b10f42a 100644
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua
+++ b/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua
@@ -1,24 +1,21 @@
+local a = require "nvim-lsp-installer.core.async"
+local Result = require "nvim-lsp-installer.core.result"
local eclipse = require "nvim-lsp-installer.core.clients.eclipse"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
----@param server Server
----@param source InstallReceiptSource
----@param on_check_result fun(result: VersionCheckResult)
-return function(server, source, on_check_result)
- eclipse.fetch_latest_jdtls_version(function(err, latest_version)
- if err then
- return on_check_result(VersionCheckResult.fail(server))
- end
- if source.version ~= latest_version then
- return on_check_result(VersionCheckResult.success(server, {
- {
- name = "jdtls",
- current_version = source.version,
- latest_version = latest_version,
- },
- }))
- else
- return on_check_result(VersionCheckResult.empty(server))
+local fetch_latest_jdtls_version = a.promisify(eclipse.fetch_latest_jdtls_version, true)
+
+---@async
+---@param receipt InstallReceipt
+return function(receipt)
+ return Result.run_catching(function()
+ local latest_version = fetch_latest_jdtls_version()
+ if receipt.primary_source.version ~= latest_version then
+ return {
+ name = "jdtls",
+ current_version = receipt.primary_source.version,
+ latest_version = latest_version,
+ }
end
+ error "Primary package is not outdated."
end)
end
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua
deleted file mode 100644
index bad76e81..00000000
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua
+++ /dev/null
@@ -1,45 +0,0 @@
-local process = require "nvim-lsp-installer.process"
-local npm = require "nvim-lsp-installer.installers.npm"
-local log = require "nvim-lsp-installer.log"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
-
----@param server Server
----@param source InstallReceiptSource
----@param on_check_complete fun(result: VersionCheckResult)
-return function(server, source, on_check_complete)
- local stdio = process.in_memory_sink()
- process.spawn(
- npm.npm_command,
- {
- args = vim.list_extend({ "outdated", "--json" }, { source.package }),
- cwd = server.root_dir,
- stdio_sink = stdio.sink,
- },
- -- Note that `npm outdated` exits with code 1 if it finds outdated packages
- vim.schedule_wrap(function()
- ---@alias NpmOutdatedPackage {current: string, wanted: string, latest: string, dependent: string, location: string}
- ---@type table<string, NpmOutdatedPackage>
- local ok, data = pcall(vim.json.decode, table.concat(stdio.buffers.stdout, ""))
-
- if not ok then
- log.fmt_error("Failed to parse npm outdated --json output. %s", data)
- return on_check_complete(VersionCheckResult.fail(server))
- end
-
- ---@type OutdatedPackage[]
- local outdated_packages = {}
-
- for package, outdated_package in pairs(data) do
- if outdated_package.current ~= outdated_package.latest then
- table.insert(outdated_packages, {
- name = package,
- current_version = outdated_package.current,
- latest_version = outdated_package.latest,
- })
- end
- end
-
- on_check_complete(VersionCheckResult.success(server, outdated_packages))
- end)
- )
-end
diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua
deleted file mode 100644
index 9534617a..00000000
--- a/lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua
+++ /dev/null
@@ -1,72 +0,0 @@
-local process = require "nvim-lsp-installer.process"
-local pip3 = require "nvim-lsp-installer.installers.pip3"
-local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result"
-local log = require "nvim-lsp-installer.log"
-
----@param package string
----@return string
-local function normalize_package(package)
- -- https://stackoverflow.com/a/60307740
- local s = package:gsub("%[.*%]", "")
- return s
-end
-
----@param server Server
----@param source InstallReceiptSource
----@param on_check_complete fun(result: VersionCheckResult)
-local function pip3_check(server, source, on_check_complete)
- local normalized_package = normalize_package(source.package)
- log.fmt_trace("Normalized package from %s to %s.", source.package, normalized_package)
- local stdio = process.in_memory_sink()
- process.spawn(
- "python",
- {
- args = { "-m", "pip", "list", "--outdated", "--format=json" },
- cwd = server.root_dir,
- stdio_sink = stdio.sink,
- env = process.graft_env(pip3.env(server.root_dir)),
- },
- vim.schedule_wrap(function(success)
- if not success then
- return on_check_complete(VersionCheckResult.fail(server))
- end
- ---@alias PipOutdatedPackage {name: string, version: string, latest_version: string}
- ---@type PipOutdatedPackage[]
- local ok, packages = pcall(vim.json.decode, table.concat(stdio.buffers.stdout, ""))
-
- if not ok then
- log.fmt_error("Failed to parse pip3 output. %s", packages)
- return on_check_complete(VersionCheckResult.fail(server))
- end
-
- log.trace("Outdated packages", packages)
-
- ---@type OutdatedPackage[]
- local outdated_packages = {}
-
- for _, outdated_package in ipairs(packages) do
- if
- outdated_package.name == normalized_package
- and outdated_package.version ~= outdated_package.latest_version
- then
- table.insert(outdated_packages, {
- name = outdated_package.name,
- current_version = outdated_package.version,
- latest_version = outdated_package.latest_version,
- })
- end
- end
-
- on_check_complete(VersionCheckResult.success(server, outdated_packages))
- end)
- )
-end
-
--- to allow tests to access internals
-return setmetatable({
- normalize_package = normalize_package,
-}, {
- __call = function(_, ...)
- return pip3_check(...)
- end,
-})
diff --git a/lua/nvim-lsp-installer/jobs/version-check/init.lua b/lua/nvim-lsp-installer/jobs/version-check/init.lua
index da33ea9d..ef36fefa 100644
--- a/lua/nvim-lsp-installer/jobs/version-check/init.lua
+++ b/lua/nvim-lsp-installer/jobs/version-check/init.lua
@@ -1,12 +1,11 @@
-local a = require "nvim-lsp-installer.core.async"
local Result = require "nvim-lsp-installer.core.result"
-local process = require "nvim-lsp-installer.process"
-local pip3 = require "nvim-lsp-installer.installers.pip3"
-local gem = require "nvim-lsp-installer.installers.gem"
-local cargo_check = require "nvim-lsp-installer.jobs.outdated-servers.cargo"
-local gem_check = require "nvim-lsp-installer.jobs.outdated-servers.gem"
-local pip3_check = require "nvim-lsp-installer.jobs.outdated-servers.pip3"
-local spawn = require "nvim-lsp-installer.core.async.spawn"
+local npm = require "nvim-lsp-installer.core.managers.npm"
+local cargo = require "nvim-lsp-installer.core.managers.cargo"
+local pip3 = require "nvim-lsp-installer.core.managers.pip3"
+local gem = require "nvim-lsp-installer.core.managers.gem"
+local go = require "nvim-lsp-installer.core.managers.go"
+local git = require "nvim-lsp-installer.core.managers.git"
+local composer = require "nvim-lsp-installer.core.managers.composer"
local M = {}
@@ -26,89 +25,35 @@ local function noop()
return Result.failure "Unable to detect version."
end
----@type Record<InstallReceiptSourceType, fun(server: Server, receipt: InstallReceipt): Result>
+---@type Record<InstallReceiptSourceType, async fun(server: Server, receipt: InstallReceipt): Result>
local version_checker = {
["npm"] = function(server, receipt)
- return spawn.npm({
- "ls",
- "--json",
- cwd = server.root_dir,
- }):map_catching(function(result)
- local npm_packages = vim.json.decode(result.stdout)
- return npm_packages.dependencies[receipt.primary_source.package].version
- end)
+ return npm.get_installed_primary_package_version(receipt, server.root_dir)
end,
["pip3"] = function(server, receipt)
- return spawn.python3({
- "-m",
- "pip",
- "list",
- "--format",
- "json",
- cwd = server.root_dir,
- env = process.graft_env(pip3.env(server.root_dir)),
- }):map_catching(function(result)
- local pip_packages = vim.json.decode(result.stdout)
- local normalized_pip_package = pip3_check.normalize_package(receipt.primary_source.package)
- for _, pip_package in ipairs(pip_packages) do
- if pip_package.name == normalized_pip_package then
- return pip_package.version
- end
- end
- error "Unable to find pip package."
- end)
+ return pip3.get_installed_primary_package_version(receipt, server.root_dir)
end,
["gem"] = function(server, receipt)
- return spawn.gem({
- "list",
- cwd = server.root_dir,
- env = process.graft_env(gem.env(server.root_dir)),
- }):map_catching(function(result)
- local gems = gem_check.parse_gem_list_output(result.stdout)
- if gems[receipt.primary_source.package] then
- return gems[receipt.primary_source.package]
- else
- error "Failed to find gem package version."
- end
- end)
+ return gem.get_installed_primary_package_version(receipt, server.root_dir)
end,
["cargo"] = function(server, receipt)
- return spawn.cargo({
- "install",
- "--list",
- "--root",
- server.root_dir,
- cwd = server.root_dir,
- }):map_catching(function(result)
- local crates = cargo_check.parse_installed_crates(result.stdout)
- a.scheduler() -- needed because vim.fn.* call
- local package = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
- if crates[package] then
- return crates[package]
- else
- error "Failed to find cargo package version."
- end
- end)
+ return cargo.get_installed_primary_package_version(receipt, server.root_dir)
+ end,
+ ["composer"] = function(server, receipt)
+ return composer.get_installed_primary_package_version(receipt, server.root_dir)
end,
["git"] = function(server)
- return spawn.git({
- "rev-parse",
- "--short",
- "HEAD",
- cwd = server.root_dir,
- }):map_catching(function(result)
- return vim.trim(result.stdout)
- end)
+ return git.get_installed_revision(server.root_dir)
+ end,
+ ["go"] = function(server, receipt)
+ return go.get_installed_primary_package_version(receipt, server.root_dir)
end,
- ["opam"] = noop,
- ["dotnet"] = noop,
- ["r_package"] = noop,
["github_release_file"] = version_in_receipt "release",
["github_tag"] = version_in_receipt "tag",
["jdtls"] = version_in_receipt "version",
}
---- Async function.
+---@async
---@param server Server
---@return Result
function M.check_server_version(server)
diff --git a/lua/nvim-lsp-installer/path.lua b/lua/nvim-lsp-installer/path.lua
index 5d3fcee4..2f0fd84b 100644
--- a/lua/nvim-lsp-installer/path.lua
+++ b/lua/nvim-lsp-installer/path.lua
@@ -27,6 +27,8 @@ function M.concat(path_components)
return table.concat(path_components, sep)
end
+---@path root_path string
+---@path path string
function M.is_subdirectory(root_path, path)
return root_path == path or path:sub(1, #root_path + 1) == root_path .. sep
end
diff --git a/scripts/autogen_metadata.lua b/scripts/autogen_metadata.lua
index 7ff2dc24..46b939af 100644
--- a/scripts/autogen_metadata.lua
+++ b/scripts/autogen_metadata.lua
@@ -4,6 +4,8 @@ local Path = require "nvim-lsp-installer.path"
local fetch = require "nvim-lsp-installer.core.fetch"
local Data = require "nvim-lsp-installer.data"
+local async_fetch = a.promisify(fetch, true)
+
local coalesce = Data.coalesce
package.loaded["nvim-lsp-installer.servers"] = nil
@@ -74,6 +76,7 @@ local function get_supported_filetypes(server)
return filetypes
end
+---@async
local function create_filetype_map()
local filetype_map = {}
@@ -91,6 +94,7 @@ local function create_filetype_map()
write_file(Path.concat { generated_dir, "filetype_map.lua" }, "return " .. vim.inspect(filetype_map), "w")
end
+---@async
local function create_autocomplete_map()
---@type table<string, Server>
local language_map = {}
@@ -133,6 +137,7 @@ local function create_autocomplete_map()
)
end
+---@async
local function create_server_metadata()
local metadata = {}
@@ -149,19 +154,18 @@ local function create_server_metadata()
write_file(Path.concat { generated_dir, "metadata.lua" }, "return " .. vim.inspect(metadata), "w")
end
+---@async
local function create_setting_schema_files()
local available_servers = servers.get_available_servers()
- local gist_err, gist_data =
- a.promisify(fetch) "https://gist.githubusercontent.com/williamboman/a01c3ce1884d4b57cc93422e7eae7702/raw/lsp-packages.json"
- assert(not gist_err, "Failed to fetch gist.")
+ local gist_data =
+ async_fetch "https://gist.githubusercontent.com/williamboman/a01c3ce1884d4b57cc93422e7eae7702/raw/lsp-packages.json"
local package_json_mappings = vim.json.decode(gist_data)
for _, server in pairs(available_servers) do
local package_json_url = package_json_mappings[server.name]
if package_json_url then
print(("Fetching %q..."):format(package_json_url))
- local err, response = a.promisify(fetch)(package_json_url)
- assert(not err, "Failed to fetch package.json for " .. server.name)
+ local response = async_fetch(package_json_url)
local schema = vim.json.decode(response)
if schema.contributes and schema.contributes.configuration then
schema = schema.contributes.configuration
diff --git a/tests/core/async/async_spec.lua b/tests/core/async/async_spec.lua
index 662e493a..0e881c8b 100644
--- a/tests/core/async/async_spec.lua
+++ b/tests/core/async/async_spec.lua
@@ -13,14 +13,29 @@ describe("async", function()
it("should run in blocking mode", function()
local start = timestamp()
a.run_blocking(function()
- a.sleep(1000)
+ a.sleep(100)
end)
local stop = timestamp()
local grace_ms = 25
- assert.is_true((stop - start) >= (1000 - grace_ms))
+ assert.is_true((stop - start) >= (100 - grace_ms))
end)
it(
+ "should pass arguments to .run",
+ async_test(function()
+ local callback = spy.new()
+ local start = timestamp()
+ a.run(a.sleep, callback, 100)
+ assert.wait_for(function()
+ assert.spy(callback).was_called(1)
+ local stop = timestamp()
+ local grace_ms = 25
+ assert.is_true((stop - start) >= (100 - grace_ms))
+ end, 150)
+ end)
+ )
+
+ it(
"should wrap callback-style async functions",
async_test(function()
local stdio = process.in_memory_sink()
diff --git a/tests/core/managers/cargo_spec.lua b/tests/core/managers/cargo_spec.lua
new file mode 100644
index 00000000..303bf5d0
--- /dev/null
+++ b/tests/core/managers/cargo_spec.lua
@@ -0,0 +1,210 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local Optional = require "nvim-lsp-installer.core.optional"
+local cargo = require "nvim-lsp-installer.core.managers.cargo"
+local Result = require "nvim-lsp-installer.core.result"
+local spawn = require "nvim-lsp-installer.core.spawn"
+
+describe("cargo manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ cargo = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should call cargo install",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ cargo.crate "my-crate"(ctx)
+ assert.spy(ctx.spawn.cargo).was_called(1)
+ assert.spy(ctx.spawn.cargo).was_called_with {
+ "install",
+ "--root",
+ ".",
+ "--locked",
+ { "--version", "42.13.37" },
+ vim.NIL, -- --features
+ "my-crate",
+ }
+ end)
+ )
+
+ it(
+ "should call cargo install with git source",
+ async_test(function()
+ cargo.crate("https://my-crate.git", { git = true })(ctx)
+ assert.spy(ctx.spawn.cargo).was_called(1)
+ assert.spy(ctx.spawn.cargo).was_called_with {
+ "install",
+ "--root",
+ ".",
+ "--locked",
+ vim.NIL,
+ vim.NIL, -- --features
+ { "--git", "https://my-crate.git" },
+ }
+ end)
+ )
+
+ it(
+ "should respect options",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ cargo.crate("my-crate", {
+ features = "lsp",
+ })(ctx)
+ assert.spy(ctx.spawn.cargo).was_called(1)
+ assert.spy(ctx.spawn.cargo).was_called_with {
+ "install",
+ "--root",
+ ".",
+ "--locked",
+ { "--version", "42.13.37" },
+ { "--features", "lsp" },
+ "my-crate",
+ }
+ end)
+ )
+
+ it(
+ "should not allow combining version with git crate",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ local err = assert.has_error(function()
+ cargo.crate("my-crate", {
+ git = true,
+ })(ctx)
+ end)
+ assert.equals("Providing a version when installing a git crate is not allowed.", err)
+ assert.spy(ctx.spawn.cargo).was_called(0)
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ cargo.crate "main-package"(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "cargo",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ end)
+ )
+end)
+
+describe("cargo version check", function()
+ it("parses cargo installed packages output", function()
+ assert.equal(
+ vim.inspect {
+ ["bat"] = "0.18.3",
+ ["exa"] = "0.10.1",
+ ["git-select-branch"] = "0.1.1",
+ ["hello_world"] = "0.0.1",
+ ["rust-analyzer"] = "0.0.0",
+ ["stylua"] = "0.11.2",
+ ["zoxide"] = "0.5.0",
+ },
+ vim.inspect(cargo.parse_installed_crates [[bat v0.18.3:
+ bat
+exa v0.10.1:
+ exa
+git-select-branch v0.1.1:
+ git-select-branch
+hello_world v0.0.1 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.LGbguATJHj):
+ hello_world
+rust-analyzer v0.0.0 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.YlsHeA9JVL/crates/rust-analyzer):
+ rust-analyzer
+stylua v0.11.2:
+ stylua
+zoxide v0.5.0:
+ zoxide
+]])
+ )
+ end)
+
+ it(
+ "should return current version",
+ async_test(function()
+ spawn.cargo = spy.new(function()
+ return Result.success {
+ stdout = [[flux-lsp v0.8.8 (https://github.com/influxdata/flux-lsp#4e452f07):
+ flux-lsp
+]],
+ }
+ end)
+
+ local result = cargo.get_installed_primary_package_version(
+ mock.new {
+ primary_source = mock.new {
+ type = "cargo",
+ package = "https://github.com/influxdata/flux-lsp",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.cargo).was_called(1)
+ assert.spy(spawn.cargo).was_called_with(match.tbl_containing {
+ "install",
+ "--list",
+ "--root",
+ ".",
+ cwd = "/tmp/install/dir",
+ })
+ assert.is_true(result:is_success())
+ assert.equals("0.8.8", result:get_or_nil())
+
+ spawn.cargo = nil
+ end)
+ )
+
+ -- XXX: This test will actually send http request to crates.io's API. It's not mocked.
+ it(
+ "should return outdated primary package",
+ async_test(function()
+ spawn.cargo = spy.new(function()
+ return Result.success {
+ stdout = [[lelwel v0.4.0:
+ lelwel-ls
+]],
+ }
+ end)
+
+ local result = cargo.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "cargo",
+ package = "lelwel",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.cargo).was_called(1)
+ assert.spy(spawn.cargo).was_called_with(match.tbl_containing {
+ "install",
+ "--list",
+ "--root",
+ ".",
+ cwd = "/tmp/install/dir",
+ })
+ assert.is_true(result:is_success())
+ assert.is_true(match.tbl_containing {
+ current_version = "0.4.0",
+ latest_version = match.matches "%d.%d.%d",
+ name = "lelwel",
+ }(result:get_or_nil()))
+
+ spawn.cargo = nil
+ end)
+ )
+end)
diff --git a/tests/core/managers/composer_spec.lua b/tests/core/managers/composer_spec.lua
new file mode 100644
index 00000000..caa0721e
--- /dev/null
+++ b/tests/core/managers/composer_spec.lua
@@ -0,0 +1,188 @@
+local spy = require "luassert.spy"
+local mock = require "luassert.mock"
+local Optional = require "nvim-lsp-installer.core.optional"
+local composer = require "nvim-lsp-installer.core.managers.composer"
+local Result = require "nvim-lsp-installer.core.result"
+local spawn = require "nvim-lsp-installer.core.spawn"
+
+describe("composer manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ composer = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should call composer require",
+ async_test(function()
+ ctx.fs.file_exists = mockx.returns(false)
+ ctx.requested_version = Optional.of "42.13.37"
+ composer.require { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.spawn.composer).was_called(2)
+ assert.spy(ctx.spawn.composer).was_called_with {
+ "init",
+ "--no-interaction",
+ "--stability=stable",
+ }
+ assert.spy(ctx.spawn.composer).was_called_with {
+ "require",
+ {
+ "main-package:42.13.37",
+ "supporting-package",
+ "supporting-package2",
+ },
+ }
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ composer.require { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "composer",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ assert.equals(
+ vim.inspect {
+ {
+ type = "composer",
+ package = "supporting-package",
+ },
+ {
+ type = "composer",
+ package = "supporting-package2",
+ },
+ },
+ vim.inspect(ctx.receipt.secondary_sources)
+ )
+ end)
+ )
+end)
+
+describe("composer version check", function()
+ it(
+ "should return current version",
+ async_test(function()
+ spawn.composer = spy.new(function()
+ return Result.success {
+ stdout = [[
+{
+ "name": "vimeo/psalm",
+ "versions": [
+ "4.0.0"
+ ]
+}
+]],
+ }
+ end)
+
+ local result = composer.get_installed_primary_package_version(
+ mock.new {
+ primary_source = mock.new {
+ type = "composer",
+ package = "vimeo/psalm",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.composer).was_called(1)
+ assert.spy(spawn.composer).was_called_with {
+ "info",
+ "--format=json",
+ "vimeo/psalm",
+ cwd = "/tmp/install/dir",
+ }
+ assert.is_true(result:is_success())
+ assert.equals("4.0.0", result:get_or_nil())
+
+ spawn.composer = nil
+ end)
+ )
+
+ it(
+ "should return outdated primary package",
+ async_test(function()
+ spawn.composer = spy.new(function()
+ return Result.success {
+ stdout = [[
+{
+ "installed": [
+ {
+ "name": "vimeo/psalm",
+ "version": "4.0.0",
+ "latest": "4.22.0",
+ "latest-status": "semver-safe-update",
+ "description": "A static analysis tool for finding errors in PHP applications"
+ }
+ ]
+}
+]],
+ }
+ end)
+
+ local result = composer.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "composer",
+ package = "vimeo/psalm",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.composer).was_called(1)
+ assert.spy(spawn.composer).was_called_with {
+ "outdated",
+ "--no-interaction",
+ "--format=json",
+ cwd = "/tmp/install/dir",
+ }
+ assert.is_true(result:is_success())
+ assert.equals(
+ vim.inspect {
+ name = "vimeo/psalm",
+ current_version = "4.0.0",
+ latest_version = "4.22.0",
+ },
+ vim.inspect(result:get_or_nil())
+ )
+
+ spawn.composer = nil
+ end)
+ )
+
+ it(
+ "should return failure if primary package is not outdated",
+ async_test(function()
+ spawn.composer = spy.new(function()
+ return Result.success {
+ stdout = [[{"installed": []}]],
+ }
+ end)
+
+ local result = composer.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "composer",
+ package = "vimeo/psalm",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.is_true(result:is_failure())
+ assert.equals("Primary package is not outdated.", result:err_or_nil())
+ spawn.composer = nil
+ end)
+ )
+end)
diff --git a/tests/core/managers/dotnet_spec.lua b/tests/core/managers/dotnet_spec.lua
new file mode 100644
index 00000000..4a6887da
--- /dev/null
+++ b/tests/core/managers/dotnet_spec.lua
@@ -0,0 +1,47 @@
+local mock = require "luassert.mock"
+local Optional = require "nvim-lsp-installer.core.optional"
+local dotnet = require "nvim-lsp-installer.core.managers.dotnet"
+
+describe("dotnet manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ dotnet = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should call dotnet tool update",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ dotnet.package "main-package"(ctx)
+ assert.spy(ctx.spawn.dotnet).was_called(1)
+ assert.spy(ctx.spawn.dotnet).was_called_with {
+ "tool",
+ "update",
+ "--tool-path",
+ ".",
+ { "--version", "42.13.37" },
+ "main-package",
+ }
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ dotnet.package "main-package"(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "dotnet",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ end)
+ )
+end)
diff --git a/tests/core/managers/gem_spec.lua b/tests/core/managers/gem_spec.lua
new file mode 100644
index 00000000..e258c65b
--- /dev/null
+++ b/tests/core/managers/gem_spec.lua
@@ -0,0 +1,226 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local Optional = require "nvim-lsp-installer.core.optional"
+local gem = require "nvim-lsp-installer.core.managers.gem"
+local Result = require "nvim-lsp-installer.core.result"
+local spawn = require "nvim-lsp-installer.core.spawn"
+
+describe("gem manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ gem = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should call gem install",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ gem.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.spawn.gem).was_called(1)
+ assert.spy(ctx.spawn.gem).was_called_with(match.tbl_containing {
+ "install",
+ "--no-user-install",
+ "--install-dir=.",
+ "--bindir=bin",
+ "--no-document",
+ match.tbl_containing {
+ "main-package:42.13.37",
+ "supporting-package",
+ "supporting-package2",
+ },
+ })
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ gem.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "gem",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ assert.equals(
+ vim.inspect {
+ {
+ type = "gem",
+ package = "supporting-package",
+ },
+ {
+ type = "gem",
+ package = "supporting-package2",
+ },
+ },
+ vim.inspect(ctx.receipt.secondary_sources)
+ )
+ end)
+ )
+end)
+
+describe("gem version check", function()
+ it(
+ "should return current version",
+ async_test(function()
+ spawn.gem = spy.new(function()
+ return Result.success {
+ stdout = [[shellwords (default: 0.1.0)
+singleton (default: 0.1.1)
+solargraph (0.44.0)
+stringio (default: 3.0.1)
+strscan (default: 3.0.1)
+]],
+ }
+ end)
+
+ local result = gem.get_installed_primary_package_version(
+ mock.new {
+ primary_source = mock.new {
+ type = "gem",
+ package = "solargraph",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.gem).was_called(1)
+ assert.spy(spawn.gem).was_called_with(match.tbl_containing {
+ "list",
+ cwd = "/tmp/install/dir",
+ env = match.all_of(
+ match.list_containing "GEM_HOME=/tmp/install/dir",
+ match.list_containing "GEM_PATH=/tmp/install/dir"
+ ),
+ })
+ assert.is_true(result:is_success())
+ assert.equals("0.44.0", result:get_or_nil())
+
+ spawn.gem = nil
+ end)
+ )
+
+ it(
+ "should return outdated primary package",
+ async_test(function()
+ spawn.gem = spy.new(function()
+ return Result.success {
+ stdout = [[bigdecimal (3.1.1 < 3.1.2)
+cgi (0.3.1 < 0.3.2)
+logger (1.5.0 < 1.5.1)
+ostruct (0.5.2 < 0.5.3)
+reline (0.3.0 < 0.3.1)
+securerandom (0.1.1 < 0.2.0)
+solargraph (0.44.0 < 0.44.3)
+]],
+ }
+ end)
+
+ local result = gem.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "gem",
+ package = "solargraph",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.gem).was_called(1)
+ assert.spy(spawn.gem).was_called_with(match.tbl_containing {
+ "outdated",
+ cwd = "/tmp/install/dir",
+ env = match.all_of(
+ match.list_containing "GEM_HOME=/tmp/install/dir",
+ match.list_containing "GEM_PATH=/tmp/install/dir"
+ ),
+ })
+ assert.is_true(result:is_success())
+ assert.equals(
+ vim.inspect {
+ name = "solargraph",
+ current_version = "0.44.0",
+ latest_version = "0.44.3",
+ },
+ vim.inspect(result:get_or_nil())
+ )
+
+ spawn.gem = nil
+ end)
+ )
+
+ it(
+ "should return failure if primary package is not outdated",
+ async_test(function()
+ spawn.gem = spy.new(function()
+ return Result.success {
+ stdout = "",
+ }
+ end)
+
+ local result = gem.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "gem",
+ package = "solargraph",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.is_true(result:is_failure())
+ assert.equals("Primary package is not outdated.", result:err_or_nil())
+ spawn.gem = nil
+ end)
+ )
+
+ it("parses outdated gem output", function()
+ local normalize = gem.parse_outdated_gem
+ assert.equal(
+ vim.inspect {
+ name = "solargraph",
+ current_version = "0.42.2",
+ latest_version = "0.44.2",
+ },
+ vim.inspect(normalize [[solargraph (0.42.2 < 0.44.2)]])
+ )
+ assert.equal(
+ vim.inspect {
+ name = "sorbet-runtime",
+ current_version = "0.5.9307",
+ latest_version = "0.5.9468",
+ },
+ vim.inspect(normalize [[sorbet-runtime (0.5.9307 < 0.5.9468)]])
+ )
+ end)
+
+ it("returns nil when unable to parse outdated gem", function()
+ assert.is_nil(gem.parse_outdated_gem "a whole bunch of gibberish!")
+ assert.is_nil(gem.parse_outdated_gem "")
+ end)
+
+ it("should parse gem list output", function()
+ assert.equals(
+ vim.inspect {
+ ["solargraph"] = "0.44.3",
+ ["unicode-display_width"] = "2.1.0",
+ },
+ vim.inspect(gem.parse_gem_list_output [[
+
+*** LOCAL GEMS ***
+
+nokogiri (1.13.3 arm64-darwin)
+solargraph (0.44.3)
+unicode-display_width (2.1.0)
+]])
+ )
+ end)
+end)
diff --git a/tests/core/managers/git_spec.lua b/tests/core/managers/git_spec.lua
new file mode 100644
index 00000000..2f6c6ace
--- /dev/null
+++ b/tests/core/managers/git_spec.lua
@@ -0,0 +1,179 @@
+local spy = require "luassert.spy"
+local mock = require "luassert.mock"
+local spawn = require "nvim-lsp-installer.core.spawn"
+local Result = require "nvim-lsp-installer.core.result"
+
+local git = require "nvim-lsp-installer.core.managers.git"
+local Optional = require "nvim-lsp-installer.core.optional"
+
+describe("git manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ git = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should fail if no git repo provided",
+ async_test(function()
+ local err = assert.has_errors(function()
+ git.clone {}(ctx)
+ end)
+ assert.equals("No git URL provided.", err)
+ assert.spy(ctx.spawn.git).was_not_called()
+ end)
+ )
+
+ it(
+ "should clone provided repo",
+ async_test(function()
+ git.clone { "https://github.com/williamboman/nvim-lsp-installer.git" }(ctx)
+ assert.spy(ctx.spawn.git).was_called(1)
+ assert.spy(ctx.spawn.git).was_called_with {
+ "clone",
+ "--depth",
+ "1",
+ "https://github.com/williamboman/nvim-lsp-installer.git",
+ ".",
+ }
+ end)
+ )
+
+ it(
+ "should fetch and checkout revision if requested",
+ async_test(function()
+ ctx.requested_version = Optional.of "1337"
+ git.clone { "https://github.com/williamboman/nvim-lsp-installer.git" }(ctx)
+ assert.spy(ctx.spawn.git).was_called(3)
+ assert.spy(ctx.spawn.git).was_called_with {
+ "clone",
+ "--depth",
+ "1",
+ "https://github.com/williamboman/nvim-lsp-installer.git",
+ ".",
+ }
+ assert.spy(ctx.spawn.git).was_called_with {
+ "fetch",
+ "--depth",
+ "1",
+ "origin",
+ "1337",
+ }
+ assert.spy(ctx.spawn.git).was_called_with { "checkout", "FETCH_HEAD" }
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ git.clone { "https://github.com/williamboman/nvim-lsp-installer.git" }(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "git",
+ remote = "https://github.com/williamboman/nvim-lsp-installer.git",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ assert.is_true(#ctx.receipt.secondary_sources == 0)
+ end)
+ )
+end)
+
+describe("git version check", function()
+ it(
+ "should return current version",
+ async_test(function()
+ spawn.git = spy.new(function()
+ return Result.success {
+ stdout = [[19c668c]],
+ }
+ end)
+
+ local result = git.get_installed_revision "/tmp/install/dir"
+
+ assert.spy(spawn.git).was_called(1)
+ assert.spy(spawn.git).was_called_with { "rev-parse", "--short", "HEAD", cwd = "/tmp/install/dir" }
+ assert.is_true(result:is_success())
+ assert.equals("19c668c", result:get_or_nil())
+
+ spawn.git = nil
+ end)
+ )
+
+ it(
+ "should check for outdated git clone",
+ async_test(function()
+ spawn.git = spy.new(function()
+ return Result.success {
+ stdout = [[728307a74cd5f2dec7ca2ca164785c25673d6328
+19c668cd10695b243b09452f0dfd53570c1a2e7d]],
+ }
+ end)
+
+ local result = git.check_outdated_git_clone(
+ mock.new {
+ primary_source = mock.new {
+ type = "git",
+ remote = "https://github.com/williamboman/nvim-lsp-installer.git",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.git).was_called(2)
+ assert.spy(spawn.git).was_called_with {
+ "fetch",
+ "origin",
+ "HEAD",
+ cwd = "/tmp/install/dir",
+ }
+ assert.spy(spawn.git).was_called_with {
+ "rev-parse",
+ "FETCH_HEAD",
+ "HEAD",
+ cwd = "/tmp/install/dir",
+ }
+ assert.is_true(result:is_success())
+ assert.equals(
+ vim.inspect {
+ name = "https://github.com/williamboman/nvim-lsp-installer.git",
+ current_version = "19c668cd10695b243b09452f0dfd53570c1a2e7d",
+ latest_version = "728307a74cd5f2dec7ca2ca164785c25673d6328",
+ },
+ vim.inspect(result:get_or_nil())
+ )
+
+ spawn.git = nil
+ end)
+ )
+
+ it(
+ "should return failure if clone is not outdated",
+ async_test(function()
+ spawn.git = spy.new(function()
+ return Result.success {
+ stdout = [[19c668cd10695b243b09452f0dfd53570c1a2e7d
+19c668cd10695b243b09452f0dfd53570c1a2e7d]],
+ }
+ end)
+
+ local result = git.check_outdated_git_clone(
+ mock.new {
+ primary_source = mock.new {
+ type = "git",
+ remote = "https://github.com/williamboman/nvim-lsp-installer.git",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.is_true(result:is_failure())
+ assert.equals("Git clone is up to date.", result:err_or_nil())
+ spawn.git = nil
+ end)
+ )
+end)
diff --git a/tests/core/managers/go_spec.lua b/tests/core/managers/go_spec.lua
new file mode 100644
index 00000000..4152d671
--- /dev/null
+++ b/tests/core/managers/go_spec.lua
@@ -0,0 +1,187 @@
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local stub = require "luassert.stub"
+local spy = require "luassert.spy"
+local Optional = require "nvim-lsp-installer.core.optional"
+local Result = require "nvim-lsp-installer.core.result"
+local go = require "nvim-lsp-installer.core.managers.go"
+local spawn = require "nvim-lsp-installer.core.spawn"
+
+describe("go manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ go = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should call go install",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ go.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.spawn.go).was_called(3)
+ assert.spy(ctx.spawn.go).was_called_with(match.tbl_containing {
+ "install",
+ "-v",
+ "main-package@42.13.37",
+ env = match.list_containing "GOBIN=/tmp/install-dir",
+ })
+ assert.spy(ctx.spawn.go).was_called_with(match.tbl_containing {
+ "install",
+ "-v",
+ "supporting-package@latest",
+ env = match.list_containing "GOBIN=/tmp/install-dir",
+ })
+ assert.spy(ctx.spawn.go).was_called_with(match.tbl_containing {
+ "install",
+ "-v",
+ "supporting-package2@latest",
+ env = match.list_containing "GOBIN=/tmp/install-dir",
+ })
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ go.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "go",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ assert.equals(
+ vim.inspect {
+ {
+ type = "go",
+ package = "supporting-package",
+ },
+ {
+ type = "go",
+ package = "supporting-package2",
+ },
+ },
+ vim.inspect(ctx.receipt.secondary_sources)
+ )
+ end)
+ )
+end)
+
+describe("go version check", function()
+ local go_version_output = [[
+gopls: go1.18
+ path golang.org/x/tools/gopls
+ mod golang.org/x/tools/gopls v0.8.1 h1:q5nDpRopYrnF4DN/1o8ZQ7Oar4Yd4I5OtGMx5RyV2/8=
+ dep github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+ dep mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc=
+ build -compiler=gc
+ build GOOS=darwin
+]]
+
+ it("should parse go version output", function()
+ local parsed = go.parse_mod_version_output(go_version_output)
+ assert.equals(
+ vim.inspect {
+ path = { ["golang.org/x/tools/gopls"] = "" },
+ mod = { ["golang.org/x/tools/gopls"] = "v0.8.1" },
+ dep = { ["github.com/google/go-cmp"] = "v0.5.7", ["mvdan.cc/xurls/v2"] = "v2.4.0" },
+ build = { ["-compiler=gc"] = "", ["GOOS=darwin"] = "" },
+ },
+ vim.inspect(parsed)
+ )
+ end)
+
+ it(
+ "should return current version",
+ async_test(function()
+ spawn.go = spy.new(function()
+ return Result.success { stdout = go_version_output }
+ end)
+
+ local result = go.get_installed_primary_package_version(
+ mock.new {
+ primary_source = mock.new {
+ type = "go",
+ package = "golang.org/x/tools/gopls",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.go).was_called(1)
+ assert.spy(spawn.go).was_called_with {
+ "version",
+ "-m",
+ "gopls",
+ cwd = "/tmp/install/dir",
+ }
+ assert.is_true(result:is_success())
+ assert.equals("v0.8.1", result:get_or_nil())
+
+ spawn.go = nil
+ end)
+ )
+
+ it(
+ "should return outdated primary package",
+ async_test(function()
+ stub(spawn, "go")
+ spawn.go.on_call_with({
+ "list",
+ "-json",
+ "-m",
+ "-versions",
+ "golang.org/x/tools/gopls",
+ cwd = "/tmp/install/dir",
+ }).returns(Result.success {
+ stdout = [[
+ {
+ "Path": "/tmp/install/dir",
+ "Versions": [
+ "v1.0.0",
+ "v1.0.1",
+ "v2.0.0"
+ ]
+ }
+ ]],
+ })
+ spawn.go.on_call_with({
+ "version",
+ "-m",
+ "gopls",
+ cwd = "/tmp/install/dir",
+ }).returns(Result.success {
+ stdout = go_version_output,
+ })
+
+ local result = go.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "go",
+ package = "golang.org/x/tools/gopls",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.is_true(result:is_success())
+ assert.equals(
+ vim.inspect {
+ name = "golang.org/x/tools/gopls",
+ current_version = "v0.8.1",
+ latest_version = "v2.0.0",
+ },
+ vim.inspect(result:get_or_nil())
+ )
+
+ spawn.go = nil
+ end)
+ )
+end)
diff --git a/tests/core/managers/npm_spec.lua b/tests/core/managers/npm_spec.lua
new file mode 100644
index 00000000..c5e2b2b9
--- /dev/null
+++ b/tests/core/managers/npm_spec.lua
@@ -0,0 +1,205 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local Optional = require "nvim-lsp-installer.core.optional"
+local npm = require "nvim-lsp-installer.core.managers.npm"
+local Result = require "nvim-lsp-installer.core.result"
+local spawn = require "nvim-lsp-installer.core.spawn"
+
+describe("npm manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ npm = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should call npm install",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ npm.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.spawn.npm).was_called(1)
+ assert.spy(ctx.spawn.npm).was_called_with(match.tbl_containing {
+ "install",
+ match.tbl_containing {
+ "main-package@42.13.37",
+ "supporting-package",
+ "supporting-package2",
+ },
+ })
+ end)
+ )
+
+ it(
+ "should call npm init if node_modules and package.json doesnt exist",
+ async_test(function()
+ ctx.fs.file_exists = mockx.returns(false)
+ ctx.fs.dir_exists = mockx.returns(false)
+ npm.install { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.spawn.npm).was_called_with {
+ "init",
+ "--yes",
+ "--scope=lsp-installer",
+ }
+ end)
+ )
+
+ it(
+ "should append .npmrc file",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ npm.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.fs.append_file).was_called(1)
+ assert.spy(ctx.fs.append_file).was_called_with(ctx.fs, ".npmrc", "global-style=true")
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ npm.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "npm",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ assert.equals(
+ vim.inspect {
+ {
+ type = "npm",
+ package = "supporting-package",
+ },
+ {
+ type = "npm",
+ package = "supporting-package2",
+ },
+ },
+ vim.inspect(ctx.receipt.secondary_sources)
+ )
+ end)
+ )
+end)
+
+describe("npm version check", function()
+ it(
+ "should return current version",
+ async_test(function()
+ spawn.npm = spy.new(function()
+ return Result.success {
+ stdout = [[
+ {
+ "name": "bash",
+ "dependencies": {
+ "bash-language-server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/bash-language-server/-/bash-language-server-2.0.0.tgz"
+ }
+ }
+ }
+ ]],
+ }
+ end)
+
+ local result = npm.get_installed_primary_package_version(
+ mock.new {
+ primary_source = mock.new {
+ type = "npm",
+ package = "bash-language-server",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.npm).was_called(1)
+ assert.spy(spawn.npm).was_called_with { "ls", "--json", cwd = "/tmp/install/dir" }
+ assert.is_true(result:is_success())
+ assert.equals("2.0.0", result:get_or_nil())
+
+ spawn.npm = nil
+ end)
+ )
+
+ it(
+ "should return outdated primary package",
+ async_test(function()
+ spawn.npm = spy.new(function()
+ -- npm outdated returns with exit code 1 if outdated packages are found!
+ return Result.failure {
+ exit_code = 1,
+ stdout = [[
+ {
+ "bash-language-server": {
+ "current": "1.17.0",
+ "wanted": "1.17.0",
+ "latest": "2.0.0",
+ "dependent": "bash",
+ "location": "/tmp/install/dir"
+ }
+ }
+ ]],
+ }
+ end)
+
+ local result = npm.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "npm",
+ package = "bash-language-server",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.npm).was_called(1)
+ assert.spy(spawn.npm).was_called_with {
+ "outdated",
+ "--json",
+ "bash-language-server",
+ cwd = "/tmp/install/dir",
+ }
+ assert.is_true(result:is_success())
+ assert.equals(
+ vim.inspect {
+ name = "bash-language-server",
+ current_version = "1.17.0",
+ latest_version = "2.0.0",
+ },
+ vim.inspect(result:get_or_nil())
+ )
+
+ spawn.npm = nil
+ end)
+ )
+
+ it(
+ "should return failure if primary package is not outdated",
+ async_test(function()
+ spawn.npm = spy.new(function()
+ return Result.success {
+ stdout = "{}",
+ }
+ end)
+
+ local result = npm.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "npm",
+ package = "bash-language-server",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.is_true(result:is_failure())
+ assert.equals("Primary package is not outdated.", result:err_or_nil())
+ spawn.npm = nil
+ end)
+ )
+end)
diff --git a/tests/core/managers/opam_spec.lua b/tests/core/managers/opam_spec.lua
new file mode 100644
index 00000000..0ec003ec
--- /dev/null
+++ b/tests/core/managers/opam_spec.lua
@@ -0,0 +1,64 @@
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local Optional = require "nvim-lsp-installer.core.optional"
+local opam = require "nvim-lsp-installer.core.managers.opam"
+
+describe("opam manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ opam = mockx.returns {},
+ },
+ }
+ end)
+
+ it(
+ "should call opam install",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ opam.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.spawn.opam).was_called(1)
+ assert.spy(ctx.spawn.opam).was_called_with(match.tbl_containing {
+ "install",
+ "--destdir=.",
+ "--yes",
+ "--verbose",
+ match.tbl_containing {
+ "main-package.42.13.37",
+ "supporting-package",
+ "supporting-package2",
+ },
+ })
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ opam.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "opam",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ assert.equals(
+ vim.inspect {
+ {
+ type = "opam",
+ package = "supporting-package",
+ },
+ {
+ type = "opam",
+ package = "supporting-package2",
+ },
+ },
+ vim.inspect(ctx.receipt.secondary_sources)
+ )
+ end)
+ )
+end)
diff --git a/tests/core/managers/pip3_spec.lua b/tests/core/managers/pip3_spec.lua
new file mode 100644
index 00000000..704a32e8
--- /dev/null
+++ b/tests/core/managers/pip3_spec.lua
@@ -0,0 +1,253 @@
+local mock = require "luassert.mock"
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+
+local pip3 = require "nvim-lsp-installer.core.managers.pip3"
+local Optional = require "nvim-lsp-installer.core.optional"
+local Result = require "nvim-lsp-installer.core.result"
+local settings = require "nvim-lsp-installer.settings"
+local spawn = require "nvim-lsp-installer.core.spawn"
+
+describe("pip3 manager", function()
+ ---@type InstallContext
+ local ctx
+ before_each(function()
+ ctx = InstallContextGenerator {
+ spawn = mock.new {
+ python3 = mockx.returns {},
+ },
+ }
+ end)
+
+ it("normalizes pip3 packages", function()
+ local normalize = pip3.normalize_package
+ assert.equal("python-lsp-server", normalize "python-lsp-server[all]")
+ assert.equal("python-lsp-server", normalize "python-lsp-server[]")
+ assert.equal("python-lsp-server", normalize "python-lsp-server[[]]")
+ end)
+
+ it(
+ "should create venv and call pip3 install",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ pip3.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.spy(ctx.promote_cwd).was_called(1)
+ assert.spy(ctx.spawn.python3).was_called(2)
+ assert.spy(ctx.spawn.python3).was_called_with {
+ "-m",
+ "venv",
+ "venv",
+ }
+ assert.spy(ctx.spawn.python3).was_called_with(match.tbl_containing {
+ "-m",
+ "pip",
+ "install",
+ "-U",
+ match.table(),
+ match.tbl_containing {
+ "main-package==42.13.37",
+ "supporting-package",
+ "supporting-package2",
+ },
+ env = match.is_table(),
+ })
+ end)
+ )
+
+ it(
+ "should exhaust python3 executable candidates if all fail",
+ async_test(function()
+ vim.g.python3_host_prog = "/my/python3"
+ ctx.spawn = mock.new {
+ python3 = mockx.throws(),
+ python = mockx.throws(),
+ [vim.g.python3_host_prog] = mockx.throws(),
+ }
+ local err = assert.has_error(function()
+ pip3.packages { "package" }(ctx)
+ end)
+ vim.g.python3_host_prog = nil
+
+ assert.equals("Unable to create python3 venv environment.", err)
+ assert.spy(ctx.spawn["/my/python3"]).was_called(1)
+ assert.spy(ctx.spawn.python3).was_called(1)
+ assert.spy(ctx.spawn.python).was_called(1)
+ end)
+ )
+
+ it(
+ "should not exhaust python3 executable if one succeeds",
+ async_test(function()
+ vim.g.python3_host_prog = "/my/python3"
+ ctx.spawn = mock.new {
+ python3 = mockx.throws(),
+ python = mockx.throws(),
+ [vim.g.python3_host_prog] = mockx.returns {},
+ }
+ pip3.packages { "package" }(ctx)
+ vim.g.python3_host_prog = nil
+ assert.spy(ctx.spawn.python3).was_called(0)
+ assert.spy(ctx.spawn.python).was_called(0)
+ assert.spy(ctx.spawn["/my/python3"]).was_called()
+ end)
+ )
+
+ it(
+ "should use install_args from settings",
+ async_test(function()
+ settings.set {
+ pip = {
+ install_args = { "--proxy", "http://localhost:8080" },
+ },
+ }
+ pip3.packages { "package" }(ctx)
+ settings.set(settings._DEFAULT_SETTINGS)
+ assert.spy(ctx.spawn.python3).was_called_with(match.tbl_containing {
+ "-m",
+ "pip",
+ "install",
+ "-U",
+ match.tbl_containing { "--proxy", "http://localhost:8080" },
+ match.tbl_containing { "package" },
+ env = match.is_table(),
+ })
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ ctx.requested_version = Optional.of "42.13.37"
+ pip3.packages { "main-package", "supporting-package", "supporting-package2" }(ctx)
+ assert.equals(
+ vim.inspect {
+ type = "pip3",
+ package = "main-package",
+ },
+ vim.inspect(ctx.receipt.primary_source)
+ )
+ assert.equals(
+ vim.inspect {
+ {
+ type = "pip3",
+ package = "supporting-package",
+ },
+ {
+ type = "pip3",
+ package = "supporting-package2",
+ },
+ },
+ vim.inspect(ctx.receipt.secondary_sources)
+ )
+ end)
+ )
+end)
+
+describe("pip3 version check", function()
+ it(
+ "should return current version",
+ async_test(function()
+ spawn.python = spy.new(function()
+ return Result.success {
+ stdout = [[
+ [{"name": "astroid", "version": "2.9.3"}, {"name": "mccabe", "version": "0.6.1"}, {"name": "python-lsp-server", "version": "1.3.0", "latest_version": "1.4.0", "latest_filetype": "wheel"}, {"name": "wrapt", "version": "1.13.3", "latest_version": "1.14.0", "latest_filetype": "wheel"}]
+ ]],
+ }
+ end)
+
+ local result = pip3.get_installed_primary_package_version(
+ mock.new {
+ primary_source = mock.new {
+ type = "pip3",
+ package = "python-lsp-server",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.python).was_called(1)
+ assert.spy(spawn.python).was_called_with(match.tbl_containing {
+ "-m",
+ "pip",
+ "list",
+ "--format=json",
+ cwd = "/tmp/install/dir",
+ env = match.table(),
+ })
+ assert.is_true(result:is_success())
+ assert.equals("1.3.0", result:get_or_nil())
+
+ spawn.python = nil
+ end)
+ )
+
+ it(
+ "should return outdated primary package",
+ async_test(function()
+ spawn.python = spy.new(function()
+ return Result.success {
+ stdout = [[
+[{"name": "astroid", "version": "2.9.3", "latest_version": "2.11.0", "latest_filetype": "wheel"}, {"name": "mccabe", "version": "0.6.1", "latest_version": "0.7.0", "latest_filetype": "wheel"}, {"name": "python-lsp-server", "version": "1.3.0", "latest_version": "1.4.0", "latest_filetype": "wheel"}, {"name": "wrapt", "version": "1.13.3", "latest_version": "1.14.0", "latest_filetype": "wheel"}]
+ ]],
+ }
+ end)
+
+ local result = pip3.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "pip3",
+ package = "python-lsp-server",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.spy(spawn.python).was_called(1)
+ assert.spy(spawn.python).was_called_with(match.tbl_containing {
+ "-m",
+ "pip",
+ "list",
+ "--outdated",
+ "--format=json",
+ cwd = "/tmp/install/dir",
+ env = match.table(),
+ })
+ assert.is_true(result:is_success())
+ assert.equals(
+ vim.inspect {
+ name = "python-lsp-server",
+ current_version = "1.3.0",
+ latest_version = "1.4.0",
+ },
+ vim.inspect(result:get_or_nil())
+ )
+
+ spawn.python = nil
+ end)
+ )
+
+ it(
+ "should return failure if primary package is not outdated",
+ async_test(function()
+ spawn.python = spy.new(function()
+ return Result.success {
+ stdout = "[]",
+ }
+ end)
+
+ local result = pip3.check_outdated_primary_package(
+ mock.new {
+ primary_source = mock.new {
+ type = "pip3",
+ package = "python-lsp-server",
+ },
+ },
+ "/tmp/install/dir"
+ )
+
+ assert.is_true(result:is_failure())
+ assert.equals("Primary package is not outdated.", result:err_or_nil())
+ spawn.python = nil
+ end)
+ )
+end)
diff --git a/tests/core/optional_spec.lua b/tests/core/optional_spec.lua
new file mode 100644
index 00000000..02f8800e
--- /dev/null
+++ b/tests/core/optional_spec.lua
@@ -0,0 +1,63 @@
+local Optional = require "nvim-lsp-installer.core.optional"
+local spy = require "luassert.spy"
+
+describe("Optional.of_nilable", function()
+ it("should create empty optionals", function()
+ local empty = Optional.empty()
+ assert.is_false(empty:is_present())
+ end)
+
+ it("should create non-empty optionals", function()
+ local empty = Optional.of_nilable "value"
+ assert.is_true(empty:is_present())
+ end)
+
+ it("should use memoized empty value", function()
+ assert.is_true(Optional.empty() == Optional.empty())
+ end)
+end)
+
+describe("Optional.get()", function()
+ it("should map non-empty values", function()
+ local str = Optional.of_nilable("world!")
+ :map(function(val)
+ return "Hello " .. val
+ end)
+ :get()
+ assert.equals("Hello world!", str)
+ end)
+
+ it("should raise error when getting empty value", function()
+ local err = assert.has_error(function()
+ Optional.empty():get()
+ end)
+ assert.equals("No value present.", err)
+ end)
+end)
+
+describe("Optional.or_else()", function()
+ it("should use .or_else() value if empty", function()
+ local value = Optional.empty():or_else "Hello!"
+ assert.equals("Hello!", value)
+ end)
+
+ it("should not use .or_else() value if not empty", function()
+ local value = Optional.of_nilable("Good bye!"):or_else "Hello!"
+ assert.equals("Good bye!", value)
+ end)
+end)
+
+describe("Optional.if_present()", function()
+ it("should not call .if_present() if value is empty", function()
+ local present = spy.new()
+ Optional.empty():if_present(present)
+ assert.spy(present).was_not_called()
+ end)
+
+ it("should call .if_present() if value is not empty", function()
+ local present = spy.new()
+ Optional.of_nilable("value"):if_present(present)
+ assert.spy(present).was_called(1)
+ assert.spy(present).was_called_with "value"
+ end)
+end)
diff --git a/tests/core/result_spec.lua b/tests/core/result_spec.lua
index 725041de..cd10acb5 100644
--- a/tests/core/result_spec.lua
+++ b/tests/core/result_spec.lua
@@ -85,4 +85,36 @@ describe("result", function()
assert.is_true(mapped:is_failure())
assert.is_true(match.has_match "This is an error$"(mapped:err_or_nil()))
end)
+
+ it("should recover errors", function()
+ local result = Result.failure("call an ambulance"):recover(function(err)
+ return err .. ". but not for me!"
+ end)
+ assert.is_true(result:is_success())
+ assert.equals("call an ambulance. but not for me!", result:get_or_nil())
+ end)
+
+ it("should catch errors in recover", function()
+ local result = Result.failure("call an ambulance"):recover_catching(function(err)
+ error("Oh no... " .. err, 2)
+ end)
+ assert.is_true(result:is_failure())
+ assert.equals("Oh no... call an ambulance", result:err_or_nil())
+ end)
+
+ it("should return results in run_catching", function()
+ local result = Result.run_catching(function()
+ return "Hello world!"
+ end)
+ assert.is_true(result:is_success())
+ assert.equals("Hello world!", result:get_or_nil())
+ end)
+
+ it("should return failures in run_catching", function()
+ local result = Result.run_catching(function()
+ error("Oh noes", 2)
+ end)
+ assert.is_true(result:is_failure())
+ assert.equals("Oh noes", result:err_or_nil())
+ end)
end)
diff --git a/tests/core/async/spawn_spec.lua b/tests/core/spawn_spec.lua
index 721e4d08..3bfafbc3 100644
--- a/tests/core/async/spawn_spec.lua
+++ b/tests/core/spawn_spec.lua
@@ -1,4 +1,4 @@
-local spawn = require "nvim-lsp-installer.core.async.spawn"
+local spawn = require "nvim-lsp-installer.core.spawn"
local process = require "nvim-lsp-installer.process"
describe("async spawn", function()
@@ -44,4 +44,36 @@ describe("async spawn", function()
assert.equals("", result:get_or_nil().stderr)
end)
)
+
+ it(
+ "should ignore vim.NIL args",
+ async_test(function()
+ local result = spawn.bash {
+ vim.NIL,
+ spawn._when(true, "-c"),
+ spawn._when(false, "shouldnotbeincluded"),
+ vim.NIL,
+ 'echo "Hello $VAR"',
+ env = { "VAR=world" },
+ }
+
+ assert.is_true(result:is_success())
+ assert.equals("Hello world\n", result:get_or_nil().stdout)
+ assert.equals("", result:get_or_nil().stderr)
+ end)
+ )
+
+ it(
+ "should flatten table args",
+ async_test(function()
+ local result = spawn.bash {
+ { "-c", 'echo "Hello $VAR"' },
+ env = { "VAR=world" },
+ }
+
+ assert.is_true(result:is_success())
+ assert.equals("Hello world\n", result:get_or_nil().stdout)
+ assert.equals("", result:get_or_nil().stderr)
+ end)
+ )
end)
diff --git a/tests/helpers/lua/luassertx.lua b/tests/helpers/lua/luassertx.lua
new file mode 100644
index 00000000..06d08ede
--- /dev/null
+++ b/tests/helpers/lua/luassertx.lua
@@ -0,0 +1,62 @@
+local assert = require "luassert"
+local match = require "luassert.match"
+local a = require "nvim-lsp-installer.core.async"
+
+local function wait_for(_, arguments)
+ ---@type fun() @Function to execute until it does not error.
+ local assertions_fn = arguments[1]
+ ---@type number @Timeout in milliseconds. Defaults to 5000.
+ local timeout = arguments[2]
+ timeout = timeout or 15000
+
+ local start = vim.loop.hrtime()
+ local is_ok, err
+ repeat
+ is_ok, err = pcall(assertions_fn)
+ if not is_ok then
+ a.sleep(math.min(timeout, 100))
+ end
+ until is_ok or ((vim.loop.hrtime() - start) / 1e6) > timeout
+
+ if not is_ok then
+ error(err)
+ end
+
+ return is_ok
+end
+
+local function tbl_containing(_, arguments, _)
+ return function(value)
+ local expected = arguments[1]
+ for key, val in pairs(expected) do
+ if match.is_matcher(val) then
+ if not val(value[key]) then
+ return false
+ end
+ elseif value[key] ~= val then
+ return false
+ end
+ end
+ return true
+ end
+end
+
+local function list_containing(_, arguments, _)
+ return function(value)
+ local expected = arguments[1]
+ for _, val in pairs(value) do
+ if match.is_matcher(expected) then
+ if expected(val) then
+ return true
+ end
+ elseif expected == val then
+ return true
+ end
+ end
+ return false
+ end
+end
+
+assert:register("matcher", "tbl_containing", tbl_containing)
+assert:register("matcher", "list_containing", list_containing)
+assert:register("assertion", "wait_for", wait_for)
diff --git a/tests/helpers/lua/test_helpers.lua b/tests/helpers/lua/test_helpers.lua
new file mode 100644
index 00000000..23353fcb
--- /dev/null
+++ b/tests/helpers/lua/test_helpers.lua
@@ -0,0 +1,75 @@
+---@diagnostic disable: lowercase-global
+local mock = require "luassert.mock"
+local util = require "luassert.util"
+
+local a = require "nvim-lsp-installer.core.async"
+local process = require "nvim-lsp-installer.process"
+local server = require "nvim-lsp-installer.server"
+local Optional = require "nvim-lsp-installer.core.optional"
+local Result = require "nvim-lsp-installer.core.result"
+local receipt = require "nvim-lsp-installer.core.receipt"
+
+function async_test(suspend_fn)
+ return function()
+ local ok, err = pcall(a.run_blocking, suspend_fn)
+ if not ok then
+ error(err, util.errorlevel())
+ end
+ end
+end
+
+mockx = {
+ just_runs = function() end,
+ returns = function(val)
+ return function()
+ return val
+ end
+ end,
+ throws = function(exception)
+ return function()
+ error(exception, 2)
+ end
+ end,
+}
+
+function ServerGenerator(opts)
+ return server.Server:new(vim.tbl_deep_extend("force", {
+ name = "dummy",
+ languages = { "dummylang" },
+ root_dir = server.get_server_root_path "dummy",
+ homepage = "https://dummylang.org",
+ installer = function(_, callback, ctx)
+ ctx.stdio_sink.stdout "Installing dummy!\n"
+ callback(true)
+ end,
+ }, opts))
+end
+
+function FailingServerGenerator(opts)
+ return ServerGenerator(vim.tbl_deep_extend("force", {
+ installer = function(_, callback, ctx)
+ ctx.stdio_sink.stdout "Installing failing dummy!\n"
+ callback(false)
+ end,
+ }, opts))
+end
+
+function InstallContextGenerator(opts)
+ ---@type InstallContext
+ local default_opts = {
+ fs = mock.new {
+ append_file = mockx.just_runs,
+ dir_exists = mockx.returns(true),
+ file_exists = mockx.returns(true),
+ },
+ spawn = mock.new {},
+ cwd = function()
+ return "/tmp/install-dir"
+ end,
+ promote_cwd = mockx.returns(Result.success()),
+ receipt = receipt.InstallReceiptBuilder.new(),
+ requested_version = Optional.empty(),
+ }
+ local merged_opts = vim.tbl_deep_extend("force", default_opts, opts)
+ return mock.new(merged_opts)
+end
diff --git a/tests/jobs/outdated-servers/cargo_spec.lua b/tests/jobs/outdated-servers/cargo_spec.lua
deleted file mode 100644
index f99e6b7c..00000000
--- a/tests/jobs/outdated-servers/cargo_spec.lua
+++ /dev/null
@@ -1,32 +0,0 @@
-local cargo_check = require "nvim-lsp-installer.jobs.outdated-servers.cargo"
-
-describe("cargo outdated package checker", function()
- it("parses cargo installed packages output", function()
- assert.equal(
- vim.inspect {
- ["bat"] = "0.18.3",
- ["exa"] = "0.10.1",
- ["git-select-branch"] = "0.1.1",
- ["hello_world"] = "0.0.1",
- ["rust-analyzer"] = "0.0.0",
- ["stylua"] = "0.11.2",
- ["zoxide"] = "0.5.0",
- },
- vim.inspect(cargo_check.parse_installed_crates [[bat v0.18.3:
- bat
-exa v0.10.1:
- exa
-git-select-branch v0.1.1:
- git-select-branch
-hello_world v0.0.1 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.LGbguATJHj):
- hello_world
-rust-analyzer v0.0.0 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.YlsHeA9JVL/crates/rust-analyzer):
- rust-analyzer
-stylua v0.11.2:
- stylua
-zoxide v0.5.0:
- zoxide
-]])
- )
- end)
-end)
diff --git a/tests/jobs/outdated-servers/gem_spec.lua b/tests/jobs/outdated-servers/gem_spec.lua
deleted file mode 100644
index eb2ce086..00000000
--- a/tests/jobs/outdated-servers/gem_spec.lua
+++ /dev/null
@@ -1,45 +0,0 @@
-local gem_check = require "nvim-lsp-installer.jobs.outdated-servers.gem"
-
-describe("gem outdated package checker", function()
- it("parses outdated gem output", function()
- local normalize = gem_check.parse_outdated_gem
- assert.equal(
- vim.inspect {
- name = "solargraph",
- current_version = "0.42.2",
- latest_version = "0.44.2",
- },
- vim.inspect(normalize [[solargraph (0.42.2 < 0.44.2)]])
- )
- assert.equal(
- vim.inspect {
- name = "sorbet-runtime",
- current_version = "0.5.9307",
- latest_version = "0.5.9468",
- },
- vim.inspect(normalize [[sorbet-runtime (0.5.9307 < 0.5.9468)]])
- )
- end)
-
- it("returns nil when unable to parse outdated gem", function()
- assert.is_nil(gem_check.parse_outdated_gem "a whole bunch of gibberish!")
- assert.is_nil(gem_check.parse_outdated_gem "")
- end)
-
- it("should parse gem list output", function()
- assert.equals(
- vim.inspect {
- ["solargraph"] = "0.44.3",
- ["unicode-display_width"] = "2.1.0",
- },
- vim.inspect(gem_check.parse_gem_list_output [[
-
-*** LOCAL GEMS ***
-
-nokogiri (1.13.3 arm64-darwin)
-solargraph (0.44.3)
-unicode-display_width (2.1.0)
-]])
- )
- end)
-end)
diff --git a/tests/jobs/outdated-servers/pip3_spec.lua b/tests/jobs/outdated-servers/pip3_spec.lua
deleted file mode 100644
index f22bb7e1..00000000
--- a/tests/jobs/outdated-servers/pip3_spec.lua
+++ /dev/null
@@ -1,10 +0,0 @@
-local pip3_check = require "nvim-lsp-installer.jobs.outdated-servers.pip3"
-
-describe("pip3 outdated package checker", function()
- it("normalizes pip3 packages", function()
- local normalize = pip3_check.normalize_package
- assert.equal("python-lsp-server", normalize "python-lsp-server[all]")
- assert.equal("python-lsp-server", normalize "python-lsp-server[]")
- assert.equal("python-lsp-server", normalize "python-lsp-server[[]]")
- end)
-end)
diff --git a/tests/luassertx/lua/luassertx.lua b/tests/luassertx/lua/luassertx.lua
deleted file mode 100644
index 0722a6d7..00000000
--- a/tests/luassertx/lua/luassertx.lua
+++ /dev/null
@@ -1,38 +0,0 @@
-local a = require "nvim-lsp-installer.core.async"
-local assert = require "luassert"
-
-local util = require "luassert.util"
-
-function async_test(suspend_fn)
- return function()
- local ok, err = pcall(a.run_blocking, suspend_fn)
- if not ok then
- error(err, util.errorlevel())
- end
- end
-end
-
-local function wait_for(_, arguments)
- ---@type fun() @Function to execute until it does not error.
- local assertions_fn = arguments[1]
- ---@type number @Timeout in milliseconds. Defaults to 5000.
- local timeout = arguments[2]
- timeout = timeout or 15000
-
- local start = vim.loop.hrtime()
- local is_ok, err
- repeat
- is_ok, err = pcall(assertions_fn)
- if not is_ok then
- a.sleep(math.min(timeout, 100))
- end
- until is_ok or ((vim.loop.hrtime() - start) / 1e6) > timeout
-
- if not is_ok then
- error(err)
- end
-
- return is_ok
-end
-
-assert:register("assertion", "wait_for", wait_for)
diff --git a/tests/minimal_init.vim b/tests/minimal_init.vim
index 35bc2bcb..9af729b0 100644
--- a/tests/minimal_init.vim
+++ b/tests/minimal_init.vim
@@ -4,41 +4,16 @@ set directory=""
set noswapfile
let $lsp_installer = getcwd()
-let $luassertx_rtp = getcwd() .. "/tests/luassertx"
+let $test_helpers = getcwd() .. "/tests/helpers"
let $dependencies = getcwd() .. "/dependencies"
-set rtp+=$lsp_installer,$luassertx_rtp
+set rtp+=$lsp_installer,$test_helpers
set packpath=$dependencies
packloadall
-lua <<EOF
-local server = require("nvim-lsp-installer.server")
-function ServerGenerator(opts)
- return server.Server:new(vim.tbl_deep_extend("force", {
- name = "dummy",
- languages = { "dummylang" },
- root_dir = server.get_server_root_path("dummy"),
- homepage = "https://dummylang.org",
- installer = function(_, callback, ctx)
- ctx.stdio_sink.stdout "Installing dummy!\n"
- callback(true)
- end
- }, opts))
-end
-
-function FailingServerGenerator(opts)
- return ServerGenerator(vim.tbl_deep_extend("force", {
- installer = function(_, callback, ctx)
- ctx.stdio_sink.stdout "Installing failing dummy!\n"
- callback(false)
- end
- }, opts))
-end
-EOF
-
-" Luassert extensions
lua require("luassertx")
+lua require("test_helpers")
lua <<EOF
require("nvim-lsp-installer").settings {
diff --git a/tests/server_spec.lua b/tests/server_spec.lua
index 81ae83b0..41a40750 100644
--- a/tests/server_spec.lua
+++ b/tests/server_spec.lua
@@ -55,6 +55,28 @@ describe("server", function()
end)
)
+ -- it(
+ -- "should be able to run async installer functions",
+ -- async_test(function()
+ -- local srv = ServerGenerator {
+ -- name = "async_installer_fixture",
+ -- root_dir = server.get_server_root_path "async_installer_fixture",
+ -- async = true,
+ -- installer = function()
+ -- a.sleep(130)
+ -- end,
+ -- }
+ -- local start = timestamp()
+ -- srv:install()
+ -- a.sleep(100)
+ -- assert.wait_for(function()
+ -- assert.is_true(srv:is_installed())
+ -- end)
+ -- local stop = timestamp()
+ -- assert.is_true(stop - start >= 100)
+ -- end)
+ -- )
+
it(
"should remove directories upon installation failure",
async_test(function()