diff options
| author | William Boman <william@redwill.se> | 2022-03-26 13:41:50 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-03-26 13:41:50 +0100 |
| commit | 212d17a039da449043b67529c29851db37acc236 (patch) | |
| tree | 38411b14487895cef0d7648e198b79fd28793fe6 /lua | |
| parent | run autogen_metadata.lua (diff) | |
| download | mason-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)
Diffstat (limited to 'lua')
30 files changed, 1396 insertions, 526 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 |
