From 5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa Mon Sep 17 00:00:00 2001 From: William Boman Date: Thu, 6 Jan 2022 18:21:19 +0100 Subject: feat(ui): display outdated servers (#395) --- lua/nvim-lsp-installer/core/clients/eclipse.lua | 21 +++ lua/nvim-lsp-installer/core/clients/github.lua | 82 ++++++++++ lua/nvim-lsp-installer/core/receipt.lua | 64 ++++++-- lua/nvim-lsp-installer/fs.lua | 11 ++ lua/nvim-lsp-installer/installers/context.lua | 79 +++------- .../jobs/outdated-servers/gem.lua | 81 ++++++++++ .../jobs/outdated-servers/git.lua | 42 +++++ .../jobs/outdated-servers/github_release_file.lua | 29 ++++ .../jobs/outdated-servers/github_tag.lua | 25 +++ .../jobs/outdated-servers/init.lua | 80 ++++++++++ .../jobs/outdated-servers/jdtls.lua | 24 +++ .../jobs/outdated-servers/npm.lua | 44 ++++++ .../jobs/outdated-servers/pip3.lua | 72 +++++++++ .../jobs/outdated-servers/version-check-result.lua | 39 +++++ lua/nvim-lsp-installer/jobs/pool.lua | 41 +++++ lua/nvim-lsp-installer/server.lua | 23 ++- lua/nvim-lsp-installer/servers/ansiblels/init.lua | 9 +- .../servers/arduino_language_server/init.lua | 5 + lua/nvim-lsp-installer/servers/ccls/init.lua | 20 ++- lua/nvim-lsp-installer/servers/groovyls/init.lua | 7 +- lua/nvim-lsp-installer/servers/jdtls/init.lua | 7 +- lua/nvim-lsp-installer/servers/phpactor/init.lua | 6 +- lua/nvim-lsp-installer/servers/spectral/init.lua | 4 +- lua/nvim-lsp-installer/settings.lua | 8 +- lua/nvim-lsp-installer/ui/status-win/init.lua | 170 ++++++++++++++------- 25 files changed, 834 insertions(+), 159 deletions(-) create mode 100644 lua/nvim-lsp-installer/core/clients/eclipse.lua create mode 100644 lua/nvim-lsp-installer/core/clients/github.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/git.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/init.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua create mode 100644 lua/nvim-lsp-installer/jobs/outdated-servers/version-check-result.lua create mode 100644 lua/nvim-lsp-installer/jobs/pool.lua (limited to 'lua') diff --git a/lua/nvim-lsp-installer/core/clients/eclipse.lua b/lua/nvim-lsp-installer/core/clients/eclipse.lua new file mode 100644 index 00000000..a788f2e2 --- /dev/null +++ b/lua/nvim-lsp-installer/core/clients/eclipse.lua @@ -0,0 +1,21 @@ +local fetch = require "nvim-lsp-installer.core.fetch" +local M = {} + +---@param version string The version string as found in the latest.txt endpoint. +---@return string The parsed version number. +function M._parse_jdtls_version_string(version) + return vim.trim(version):gsub("^jdt%-language%-server%-", ""):gsub("%.tar%.gz$", "") +end + +---@param callback fun(err: string|nil, data: string|nil) +function M.fetch_latest_jdtls_version(callback) + fetch("https://download.eclipse.org/jdtls/snapshots/latest.txt", function(err, data) + if err then + callback(err, nil) + else + callback(nil, M._parse_jdtls_version_string(data)) + end + end) +end + +return M diff --git a/lua/nvim-lsp-installer/core/clients/github.lua b/lua/nvim-lsp-installer/core/clients/github.lua new file mode 100644 index 00000000..080edbb0 --- /dev/null +++ b/lua/nvim-lsp-installer/core/clients/github.lua @@ -0,0 +1,82 @@ +local fetch = require "nvim-lsp-installer.core.fetch" +local Data = require "nvim-lsp-installer.data" +local log = require "nvim-lsp-installer.log" + +local list_find_first = Data.list_find_first + +local M = {} + +---@alias GitHubRelease {tag_name:string, prerelease: boolean, draft: boolean} +---@alias GitHubTag {name: string} + +---@param repo string The GitHub repo ("username/repo"). +---@param callback fun(error: string|nil, data: GitHubRelease[]|nil) +function M.fetch_releases(repo, callback) + log.fmt_trace("Fetching GitHub releases for repo=%s", repo) + fetch(("https://api.github.com/repos/%s/releases"):format(repo), function(err, response) + if err then + log.fmt_error("Failed to fetch releases for repo=%s", repo) + return callback("Failed to fetch GitHub releases.", nil) + end + callback(nil, vim.json.decode(response)) + end) +end + +---@alias FetchLatestGithubReleaseOpts {tag_name_pattern:string} +---@param repo string The GitHub repo ("username/repo"). +---@param opts FetchLatestGithubReleaseOpts|nil +---@param callback fun(error: string|nil, data: GitHubRelease|nil) +function M.fetch_latest_release(repo, opts, callback) + M.fetch_releases(repo, function(err, releases) + if err then + callback(err, nil) + return + end + + local latest_release = list_find_first(releases, function(_release) + ---@type GitHubRelease + local release = _release + local is_stable_release = not release.prerelease and not release.draft + if opts.tag_name_pattern then + return is_stable_release and release.tag_name:match(opts.tag_name_pattern) + end + return is_stable_release + end) + + if not latest_release then + log.fmt_info("Failed to find latest release. repo=%s, opts=%s", repo, opts) + return callback("Failed to find latest release.", nil) + end + + log.fmt_debug("Resolved latest version repo=%s, tag_name=%s", repo, latest_release.tag_name) + callback(nil, latest_release) + end) +end + +---@param repo string The GitHub repo ("username/repo"). +---@param callback fun(err: string|nil, tags: GitHubTag[]|nil) +function M.fetch_tags(repo, callback) + fetch(("https://api.github.com/repos/%s/tags"):format(repo), function(err, response) + if err then + log.fmt_error("Failed to fetch tags for repo=%s", err) + return callback("Failed to fetch tags.", nil) + end + callback(nil, vim.json.decode(response)) + end) +end + +---@param repo string The GitHub repo ("username/repo"). +---@param callback fun(err: string|nil, latest_tag: GitHubTag|nil) +function M.fetch_latest_tag(repo, callback) + M.fetch_tags(repo, function(err, tags) + if err then + return callback(err, nil) + end + if vim.tbl_count(tags) == 0 then + return callback("No tags found.", nil) + end + callback(nil, tags[1]) + end) +end + +return M diff --git a/lua/nvim-lsp-installer/core/receipt.lua b/lua/nvim-lsp-installer/core/receipt.lua index 5808dfeb..834bb68c 100644 --- a/lua/nvim-lsp-installer/core/receipt.lua +++ b/lua/nvim-lsp-installer/core/receipt.lua @@ -1,41 +1,62 @@ local M = {} ----@alias InstallerReceiptSource table +---@alias InstallReceiptSchemaVersion +---| '"1.0"' +---| '"1.0a"' + +---@alias InstallReceiptSourceType +---| '"npm"' +---| '"pip3"' +---| '"gem"' +---| '"go"' +---| '"dotnet"' +---| '"unmanaged"' +---| '"system"' +---| '"jdtls"' +---| '"git"' +---| '"github_tag"' +---| '"github_release_file"' + +---@alias InstallReceiptSource {type: InstallReceiptSourceType} ---@class InstallReceiptBuilder ----@field private secondary_sources InstallerReceiptSource[] +---@field public is_marked_invalid boolean Whether this instance of the builder has been marked as invalid. This is an exception that only apply to a few select servers whose installation is not yet compatible with the receipt schema due to having a too complicated installation structure. +---@field private secondary_sources InstallReceiptSource[] ---@field private epoch_time number local InstallReceiptBuilder = {} InstallReceiptBuilder.__index = InstallReceiptBuilder function InstallReceiptBuilder.new() return setmetatable({ + is_marked_invalid = false, secondary_sources = {}, }, InstallReceiptBuilder) end +function InstallReceiptBuilder:mark_invalid() + self.is_marked_invalid = true + return self +end + ---@param name string function InstallReceiptBuilder:with_name(name) self.name = name return self end ----@alias InstallerReceiptSchemaVersion ----| '"1.0"' - ----@param version InstallerReceiptSchemaVersion +---@param version InstallReceiptSchemaVersion function InstallReceiptBuilder:with_schema_version(version) self.schema_version = version return self end ----@param source InstallerReceiptSource +---@param source InstallReceiptSource function InstallReceiptBuilder:with_primary_source(source) self.primary_source = source return self end ----@param source InstallerReceiptSource +---@param source InstallReceiptSource function InstallReceiptBuilder:with_secondary_source(source) table.insert(self.secondary_sources, source) return self @@ -81,7 +102,7 @@ function InstallReceiptBuilder:build() } end ----@param type string +---@param type InstallReceiptSourceType local function package_source(type) ---@param package string return function(package) @@ -103,13 +124,12 @@ function InstallReceiptBuilder.system(dependency) end ---@param remote_url string ----@param revision string -function InstallReceiptBuilder.git_remote(remote_url, revision) - return { type = "git", remote = remote_url, revision = revision } +function InstallReceiptBuilder.git_remote(remote_url) + return { type = "git", remote = remote_url } end ---@param ctx ServerInstallContext ----@param opts UseGithubReleaseOpts|nil +---@param opts FetchLatestGithubReleaseOpts|nil function InstallReceiptBuilder.github_release_file(ctx, opts) opts = opts or {} return { @@ -129,6 +149,24 @@ function InstallReceiptBuilder.github_tag(ctx) } end +---@class InstallReceipt +---@field public name string +---@field public schema_version InstallReceiptSchemaVersion +---@field public metrics {start_time:integer, completion_time:integer} +---@field public primary_source InstallReceiptSource +---@field public secondary_sources InstallReceiptSource[] +local InstallReceipt = {} +InstallReceipt.__index = InstallReceipt + +function InstallReceipt.new(props) + return setmetatable(props, InstallReceipt) +end + +function InstallReceipt.from_json(json) + return InstallReceipt.new(json) +end + M.InstallReceiptBuilder = InstallReceiptBuilder +M.InstallReceipt = InstallReceipt return M diff --git a/lua/nvim-lsp-installer/fs.lua b/lua/nvim-lsp-installer/fs.lua index 38ac053f..3725f154 100644 --- a/lua/nvim-lsp-installer/fs.lua +++ b/lua/nvim-lsp-installer/fs.lua @@ -101,6 +101,17 @@ function M.write_file(path, contents) assert(uv.fs_close(fd)) end +---@param path string @The full path to the file to read. +function M.read_file(path) + log.fmt_debug("fs: read_file %s", path) + assert_ownership(path) + local fd = assert(uv.fs_open(path, "r", 438)) + local fstat = assert(uv.fs_fstat(fd)) + local contents = assert(uv.fs_read(fd, fstat.size, 0)) + assert(uv.fs_close(fd)) + return contents +end + function M.append_file(path, contents) log.fmt_debug("fs: append_file %s", path) assert_ownership(path) diff --git a/lua/nvim-lsp-installer/installers/context.lua b/lua/nvim-lsp-installer/installers/context.lua index 6032df18..7f3761d6 100644 --- a/lua/nvim-lsp-installer/installers/context.lua +++ b/lua/nvim-lsp-installer/installers/context.lua @@ -4,10 +4,7 @@ local installers = require "nvim-lsp-installer.installers" local platform = require "nvim-lsp-installer.platform" local fs = require "nvim-lsp-installer.fs" local path = require "nvim-lsp-installer.path" -local fetch = require "nvim-lsp-installer.core.fetch" -local Data = require "nvim-lsp-installer.data" - -local list_find_first = Data.list_find_first +local github = require "nvim-lsp-installer.core.clients.github" local M = {} @@ -15,6 +12,7 @@ local M = {} function M.use_github_latest_tag(repo) ---@type ServerInstallerFunction return function(_, callback, context) + context.github_repo = repo if context.requested_server_version then log.fmt_debug( "Requested server version already provided (%s), skipping fetching tags from GitHub.", @@ -24,37 +22,26 @@ function M.use_github_latest_tag(repo) return callback(true) end context.stdio_sink.stdout "Fetching tags from GitHub API...\n" - fetch( - ("https://api.github.com/repos/%s/tags"):format(repo), - vim.schedule_wrap(function(err, raw_data) - if err then - context.stdio_sink.stderr(tostring(err) .. "\n") - callback(false) - return - end + github.fetch_latest_tag(repo, function(err, latest_tag) + if err then + context.stdio_sink.stderr(tostring(err) .. "\n") + callback(false) + return + end - local data = vim.json.decode(raw_data) - if vim.tbl_count(data) == 0 then - context.stdio_sink.stderr("No tags found for GitHub repo %s.\n", repo) - callback(false) - return - end - context.requested_server_version = data[1].name - context.github_repo = repo - callback(true) - end) - ) + context.requested_server_version = latest_tag.name + callback(true) + end) end end ----@alias UseGithubReleaseOpts {tag_name_pattern:string} - ---@param repo string @The GitHub repo ("username/repo"). ----@param opts UseGithubReleaseOpts|nil +---@param opts FetchLatestGithubReleaseOpts|nil function M.use_github_release(repo, opts) opts = opts or {} ---@type ServerInstallerFunction - return function(server, callback, context) + return function(_, callback, context) + context.github_repo = repo if context.requested_server_version then log.fmt_debug( "Requested server version already provided (%s), skipping fetching latest release from GitHub.", @@ -64,40 +51,20 @@ function M.use_github_release(repo, opts) return callback(true) end context.stdio_sink.stdout "Fetching latest release version from GitHub API...\n" - fetch( - ("https://api.github.com/repos/%s/releases"):format(repo), - vim.schedule_wrap(function(err, response) - if err then - log.fmt_error("Failed to fetch releases for repo=%s", repo) - context.stdio_sink.stderr(tostring(err) .. "\n") - return callback(false) - end - - local latest_release = list_find_first(vim.json.decode(response), function(release) - local is_stable_release = not release.prerelease and not release.draft - if opts.tag_name_pattern then - return is_stable_release and release.tag_name:match(opts.tag_name_pattern) - end - return is_stable_release - end) - - if not latest_release then - log.fmt_info("Failed to find latest release. repo=%s, opts=%s", repo, opts) - callback(false) - return - end - log.debug("Resolved latest version", server.name, repo, latest_release.tag_name) - context.requested_server_version = latest_release.tag_name - context.github_repo = repo - callback(true) - end) - ) + github.fetch_latest_release(repo, opts, function(err, latest_release) + if err then + context.stdio_sink.stderr(tostring(err) .. "\n") + return callback(false) + end + context.requested_server_version = latest_release.tag_name + callback(true) + end) end end ---@param repo string @The GitHub report ("username/repo"). ---@param file string|fun(resolved_version: string): string @The name of a file available in the provided repo's GitHub releases. ----@param opts UseGithubReleaseOpts +---@param opts FetchLatestGithubReleaseOpts function M.use_github_release_file(repo, file, opts) return installers.pipe { M.use_github_release(repo, opts), diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua new file mode 100644 index 00000000..909bf7c3 --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua @@ -0,0 +1,81 @@ +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 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, +}, { + __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 new file mode 100644 index 00000000..74007abc --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/git.lua @@ -0,0 +1,42 @@ +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 new file mode 100644 index 00000000..37e30798 --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua @@ -0,0 +1,29 @@ +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 + + 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 + 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 new file mode 100644 index 00000000..6f45b4ba --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua @@ -0,0 +1,25 @@ +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 + + 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)) + end + end) +end diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua new file mode 100644 index 00000000..6f6ca5a3 --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua @@ -0,0 +1,80 @@ +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 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 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 M = {} + +local jobpool = JobExecutionPool:new { + size = 4, +} + +local function noop(server, _, on_result) + on_result(VersionCheckResult.empty(server)) +end + +local checkers = { + ["npm"] = npm_check, + ["pip3"] = pip3_check, + ["gem"] = gem_check, + ["go"] = noop, -- TODO + ["dotnet"] = noop, -- TODO + ["unmanaged"] = noop, + ["system"] = noop, + ["jdtls"] = jdtls, + ["git"] = git_check, + ["github_release_file"] = github_release_file_check, + ["github_tag"] = github_tag_check, +} + +local pending_servers = {} + +---@param servers Server[] +---@param on_check_start fun(server: Server) +---@param on_result fun(result: VersionCheckResult) +function M.identify_outdated_servers(servers, on_check_start, on_result) + for _, server in ipairs(servers) do + if not pending_servers[server.name] then + pending_servers[server.name] = true + jobpool:supply(function(_done) + local function complete(...) + pending_servers[server.name] = nil + on_result(...) + _done() + end + + local receipt = server:get_receipt() + if receipt then + if + vim.tbl_contains({ "github_release_file", "github_tag" }, receipt.primary_source.type) + and receipt.schema_version == "1.0" + then + -- Receipts of this version are in some cases incomplete. + return complete(VersionCheckResult.fail(server)) + end + + local checker = checkers[receipt.primary_source.type] + if checker then + on_check_start(server) + checker(server, receipt.primary_source, complete) + else + complete(VersionCheckResult.empty(server)) + log.fmt_error("Unable to find checker for source=%s", receipt.primary_source.type) + end + else + complete(VersionCheckResult.empty(server)) + log.fmt_trace("No receipt found for server=%s", server.name) + end + end) + end + end +end + +return M diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua new file mode 100644 index 00000000..bb40714a --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua @@ -0,0 +1,24 @@ +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)) + end + end) +end diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua new file mode 100644 index 00000000..ca42edbd --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua @@ -0,0 +1,44 @@ +local process = require "nvim-lsp-installer.process" +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", + { + 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 + 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 new file mode 100644 index 00000000..9534617a --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua @@ -0,0 +1,72 @@ +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/outdated-servers/version-check-result.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/version-check-result.lua new file mode 100644 index 00000000..a745c215 --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/version-check-result.lua @@ -0,0 +1,39 @@ +---@class CheckResult +---@field public server Server +---@field public success boolean +---@field public outdated_packages OutdatedPackage[] +local VersionCheckResult = {} +VersionCheckResult.__index = VersionCheckResult + +---@alias OutdatedPackage {name: string, current_version: string, latest_version: string} + +---@param server Server +---@param outdated_packages OutdatedPackage[] +function VersionCheckResult.new(server, success, outdated_packages) + local self = setmetatable({}, VersionCheckResult) + self.server = server + self.success = success + self.outdated_packages = outdated_packages + return self +end + +---@param server Server +function VersionCheckResult.fail(server) + return VersionCheckResult.new(server, false) +end + +---@param server Server +---@param outdated_packages OutdatedPackage[] +function VersionCheckResult.success(server, outdated_packages) + return VersionCheckResult.new(server, true, outdated_packages) +end + +function VersionCheckResult.empty(server) + return VersionCheckResult.success(server, {}) +end + +function VersionCheckResult:has_outdated_packages() + return #self.outdated_packages > 0 +end + +return VersionCheckResult diff --git a/lua/nvim-lsp-installer/jobs/pool.lua b/lua/nvim-lsp-installer/jobs/pool.lua new file mode 100644 index 00000000..78302643 --- /dev/null +++ b/lua/nvim-lsp-installer/jobs/pool.lua @@ -0,0 +1,41 @@ +local log = require "nvim-lsp-installer.log" + +local JobExecutionPool = {} +JobExecutionPool.__index = JobExecutionPool + +function JobExecutionPool:new(opts) + return setmetatable({ + size = opts.size, + _queue = {}, + _supplied_jobs = 0, + _running_jobs = 0, + }, JobExecutionPool) +end + +function JobExecutionPool:supply(fn) + self._supplied_jobs = self._supplied_jobs + 1 + self._queue[#self._queue + 1] = setmetatable({ + id = self._supplied_jobs, + }, { + __call = function(_, ...) + fn(...) + end, + }) + self:_dequeue() +end + +function JobExecutionPool:_dequeue() + log.fmt_trace("Dequeuing job running_jobs=%s, size=%s", self._running_jobs, self.size) + if self._running_jobs < self.size and #self._queue > 0 then + local dequeued = table.remove(self._queue, 1) + self._running_jobs = self._running_jobs + 1 + log.fmt_trace("Dequeued job job_id=%s, running_jobs=%s, size=%s", dequeued.id, self._running_jobs, self.size) + dequeued(function() + log.fmt_trace("Job finished job_id=%s", dequeued.id) + self._running_jobs = self._running_jobs - 1 + self:_dequeue() + end) + end +end + +return JobExecutionPool diff --git a/lua/nvim-lsp-installer/server.lua b/lua/nvim-lsp-installer/server.lua index b06249b3..948d11db 100644 --- a/lua/nvim-lsp-installer/server.lua +++ b/lua/nvim-lsp-installer/server.lua @@ -174,19 +174,36 @@ function M.Server:promote_install_dir(install_dir) return true end +function M.Server:_get_receipt_path() + return path.concat { self.root_dir, "nvim-lsp-installer-receipt.json" } +end + ---@param receipt_builder InstallReceiptBuilder function M.Server:_write_receipt(receipt_builder) - receipt_builder:with_name(self.name):with_schema_version("1.0"):with_completion_time(vim.loop.gettimeofday()) + if receipt_builder.is_marked_invalid then + log.fmt_debug("Skipping writing receipt for %s because it is marked as invalid.", self.name) + return + end + receipt_builder:with_name(self.name):with_schema_version("1.0a"):with_completion_time(vim.loop.gettimeofday()) local receipt_success, install_receipt = pcall(receipt_builder.build, receipt_builder) if receipt_success then - local install_receipt_path = path.concat { self.root_dir, "nvim-lsp-installer-receipt.json" } - pcall(fs.write_file, install_receipt_path, vim.json.encode(install_receipt)) + pcall(fs.write_file, self:_get_receipt_path(), vim.json.encode(install_receipt)) else log.fmt_error("Failed to build receipt for server=%s. Error=%s", self.name, install_receipt) end end +---@return InstallReceipt|nil +function M.Server:get_receipt() + local receipt_path = self:_get_receipt_path() + if fs.file_exists(receipt_path) then + local receipt_json = vim.json.decode(fs.read_file(receipt_path)) + return receipt.InstallReceipt.from_json(receipt_json) + end + return nil +end + ---@param context ServerInstallContext ---@param callback ServerInstallCallback function M.Server:install_attached(context, callback) diff --git a/lua/nvim-lsp-installer/servers/ansiblels/init.lua b/lua/nvim-lsp-installer/servers/ansiblels/init.lua index 6c0c6c73..f5e0213e 100644 --- a/lua/nvim-lsp-installer/servers/ansiblels/init.lua +++ b/lua/nvim-lsp-installer/servers/ansiblels/init.lua @@ -16,13 +16,8 @@ return function(name, root_dir) npm.exec("npm", { "install" }), npm.run "compile", npm.exec("npm", { "install", "--production" }), - context.receipt(function(receipt, ctx) - receipt:with_primary_source( - receipt.git_remote( - "https://github.com/ansible/ansible-language-server", - ctx.requested_server_version - ) - ) + context.receipt(function(receipt) + receipt:with_primary_source(receipt.git_remote "https://github.com/ansible/ansible-language-server") end), }, default_options = { diff --git a/lua/nvim-lsp-installer/servers/arduino_language_server/init.lua b/lua/nvim-lsp-installer/servers/arduino_language_server/init.lua index 4007803f..49da467a 100644 --- a/lua/nvim-lsp-installer/servers/arduino_language_server/init.lua +++ b/lua/nvim-lsp-installer/servers/arduino_language_server/init.lua @@ -99,6 +99,11 @@ return function(name, root_dir) clangd_installer, arduino_cli_installer, arduino_language_server_installer, + context.receipt(function(receipt) + -- We install 3 different components to 3 different subdirectories. This is currently not captured in + -- the receipt structure. + receipt:mark_invalid() + end), }, default_options = { cmd = { diff --git a/lua/nvim-lsp-installer/servers/ccls/init.lua b/lua/nvim-lsp-installer/servers/ccls/init.lua index e77ae80b..3da11377 100644 --- a/lua/nvim-lsp-installer/servers/ccls/init.lua +++ b/lua/nvim-lsp-installer/servers/ccls/init.lua @@ -98,9 +98,6 @@ return function(name, root_dir) ), } end), - context.receipt(function(receipt, ctx) - receipt:with_secondary_source(receipt.github_release_file(ctx)) - end), } end @@ -134,11 +131,6 @@ return function(name, root_dir) c.spawn(callback) end, std.rmrf "ccls-git", - context.receipt(function(receipt, ctx) - receipt:with_primary_source( - receipt.git_remote("https://github.com/MaskRay/ccls", ctx.requested_server_version) - ) - end), } local linux_ccls_installer = installers.pipe { @@ -175,9 +167,15 @@ return function(name, root_dir) root_dir = root_dir, homepage = "https://github.com/MaskRay/ccls", languages = { "c", "c++", "objective-c" }, - installer = installers.when { - mac = mac_ccls_installer, - linux = linux_ccls_installer, + installer = { + installers.when { + mac = mac_ccls_installer, + linux = linux_ccls_installer, + }, + context.receipt(function(receipt) + -- The cloned ccls git repo gets deleted during installation, so we have no local copy. + receipt:mark_invalid() + end), }, default_options = { cmd_env = { diff --git a/lua/nvim-lsp-installer/servers/groovyls/init.lua b/lua/nvim-lsp-installer/servers/groovyls/init.lua index becc1dd7..8dbb1b5c 100644 --- a/lua/nvim-lsp-installer/servers/groovyls/init.lua +++ b/lua/nvim-lsp-installer/servers/groovyls/init.lua @@ -16,12 +16,9 @@ return function(name, root_dir) std.gradlew { args = { "build" }, }, - context.receipt(function(receipt, ctx) + context.receipt(function(receipt) receipt:with_primary_source( - receipt.git_remote( - "https://github.com/GroovyLanguageServer/groovy-language-server", - ctx.requested_server_version - ) + receipt.git_remote "https://github.com/GroovyLanguageServer/groovy-language-server" ) end), }, diff --git a/lua/nvim-lsp-installer/servers/jdtls/init.lua b/lua/nvim-lsp-installer/servers/jdtls/init.lua index ddc00c26..600a1112 100644 --- a/lua/nvim-lsp-installer/servers/jdtls/init.lua +++ b/lua/nvim-lsp-installer/servers/jdtls/init.lua @@ -5,6 +5,7 @@ local context = require "nvim-lsp-installer.installers.context" local platform = require "nvim-lsp-installer.platform" local Data = require "nvim-lsp-installer.data" local fetch = require "nvim-lsp-installer.core.fetch" +local eclipse = require "nvim-lsp-installer.core.clients.eclipse" return function(name, root_dir) local function get_cmd(workspace_name) @@ -56,14 +57,12 @@ return function(name, root_dir) callback(true) return end - fetch("https://download.eclipse.org/jdtls/snapshots/latest.txt", function(err, data) + eclipse.fetch_latest_jdtls_version(function(err, latest_version) if err then ctx.stdio_sink.stderr "Failed to fetch latest verison.\n" callback(false) else - ctx.requested_server_version = vim.trim(data) - :gsub("^jdt%-language%-server%-", "") - :gsub("%.tar%.gz$", "") + ctx.requested_server_version = latest_version callback(true) end end) diff --git a/lua/nvim-lsp-installer/servers/phpactor/init.lua b/lua/nvim-lsp-installer/servers/phpactor/init.lua index 42ef65f6..45ce5ccd 100644 --- a/lua/nvim-lsp-installer/servers/phpactor/init.lua +++ b/lua/nvim-lsp-installer/servers/phpactor/init.lua @@ -16,10 +16,8 @@ return function(name, root_dir) unix = { std.git_clone "https://github.com/phpactor/phpactor.git", composer.install(), - context.receipt(function(receipt, ctx) - receipt:with_primary_source( - receipt.git_remote("https://github.com/phpactor/phpactor.git", ctx.requested_server_version) - ) + context.receipt(function(receipt) + receipt:with_primary_source(receipt.git_remote "https://github.com/phpactor/phpactor.git") end), }, }, diff --git a/lua/nvim-lsp-installer/servers/spectral/init.lua b/lua/nvim-lsp-installer/servers/spectral/init.lua index 28a09274..e308369c 100644 --- a/lua/nvim-lsp-installer/servers/spectral/init.lua +++ b/lua/nvim-lsp-installer/servers/spectral/init.lua @@ -21,7 +21,9 @@ return function(name, root_dir) installers.always_succeed(npm.run "compile"), context.set_working_dir "server", context.receipt(function(receipt, ctx) - receipt:with_primary_source(receipt.github_release_file(ctx)) + receipt + :mark_invalid() -- Due to the `context.set_working_dir` after clone, we essentially erase any trace of the cloned git repo, so we mark this as invalid. + :with_primary_source(receipt.git_remote "https://github.com/stoplightio/vscode-spectral") end), }, default_options = { diff --git a/lua/nvim-lsp-installer/settings.lua b/lua/nvim-lsp-installer/settings.lua index 0eea98fc..ad875b72 100644 --- a/lua/nvim-lsp-installer/settings.lua +++ b/lua/nvim-lsp-installer/settings.lua @@ -16,12 +16,16 @@ local DEFAULT_SETTINGS = { keymaps = { -- Keymap to expand a server in the UI toggle_server_expand = "", - -- Keymap to install a server + -- Keymap to install the server under the current cursor position install_server = "i", - -- Keymap to reinstall/update a server + -- Keymap to reinstall/update the server under the current cursor position update_server = "u", + -- Keymap to check for new version for the server under the current cursor position + check_server_version = "c", -- Keymap to update all installed servers update_all_servers = "U", + -- Keymap to check which installed servers are outdated + check_outdated_servers = "C", -- Keymap to uninstall a server uninstall_server = "X", }, diff --git a/lua/nvim-lsp-installer/ui/status-win/init.lua b/lua/nvim-lsp-installer/ui/status-win/init.lua index 182370a7..9ab658c4 100644 --- a/lua/nvim-lsp-installer/ui/status-win/init.lua +++ b/lua/nvim-lsp-installer/ui/status-win/init.lua @@ -5,6 +5,8 @@ local Data = require "nvim-lsp-installer.data" local display = require "nvim-lsp-installer.ui.display" local settings = require "nvim-lsp-installer.settings" local lsp_servers = require "nvim-lsp-installer.servers" +local JobExecutionPool = require "nvim-lsp-installer.jobs.pool" +local jobs = require "nvim-lsp-installer.jobs.outdated-servers" local ServerHints = require "nvim-lsp-installer.ui.status-win.server_hints" local ServerSettingsSchema = require "nvim-lsp-installer.ui.status-win.components.settings-schema" @@ -53,6 +55,8 @@ local function Help(is_current_settings_expanded, vader_saber_ticks) { "Toggle server info", settings.current.ui.keymaps.toggle_server_expand }, { "Update server", settings.current.ui.keymaps.update_server }, { "Update all installed server", settings.current.ui.keymaps.update_all_servers }, + { "Check for new server version", settings.current.ui.keymaps.check_server_version }, + { "Check for new versions (all servers)", settings.current.ui.keymaps.check_outdated_servers }, { "Uninstall server", settings.current.ui.keymaps.uninstall_server }, { "Install server", settings.current.ui.keymaps.install_server }, { "Close window", CLOSE_WINDOW_KEYMAP_1 }, @@ -180,6 +184,23 @@ local function get_relative_install_time(time) end end +---@param outdated_packages OutdatedPackage[] +---@return string +local function format_new_package_versions(outdated_packages) + local result = {} + for _, outdated_package in ipairs(outdated_packages) do + table.insert( + result, + ("%s@%s → %s"):format( + outdated_package.name, + outdated_package.current_version, + outdated_package.latest_version + ) + ) + end + return table.concat(result, ", ") +end + ---@param server ServerState local function ServerMetadata(server) return Ui.Node(Data.list_not_nil( @@ -201,6 +222,12 @@ local function ServerMetadata(server) )) end), Ui.Table(Data.list_not_nil( + Data.lazy(#server.metadata.outdated_packages > 0, function() + return { + { "new version", "LspInstallerMuted" }, + { format_new_package_versions(server.metadata.outdated_packages), "LspInstallerGreen" }, + } + end), Data.lazy(server.metadata.install_timestamp_seconds, function() return { { "last updated", "LspInstallerMuted" }, @@ -253,26 +280,39 @@ end ---@param servers ServerState[] ---@param props ServerGroupProps local function InstalledServers(servers, props) - return Ui.Node(Data.list_map(function(server) - local is_expanded = props.expanded_server == server.name - return Ui.Node { - Ui.HlTextNode { - Data.list_not_nil( - { settings.current.ui.icons.server_installed, "LspInstallerGreen" }, - { " " .. server.name, "" }, - Data.when(server.deprecated, { " deprecated", "LspInstallerOrange" }) - ), - }, - Ui.Keybind(settings.current.ui.keymaps.toggle_server_expand, "EXPAND_SERVER", { server.name }), - Ui.Keybind(settings.current.ui.keymaps.update_server, "INSTALL_SERVER", { server.name }), - Ui.Keybind(settings.current.ui.keymaps.uninstall_server, "UNINSTALL_SERVER", { server.name }), - Ui.When(is_expanded, function() - return Indent { - ServerMetadata(server), - } - end), - } - end, servers)) + return Ui.Node(Data.list_map( + ---@param server ServerState + function(server) + local is_expanded = props.expanded_server == server.name + return Ui.Node { + Ui.HlTextNode { + Data.list_not_nil( + { settings.current.ui.icons.server_installed, "LspInstallerGreen" }, + { " " .. server.name, "" }, + Data.when(server.deprecated, { " deprecated", "LspInstallerOrange" }), + Data.when(server.is_checking_outdated_packages, { + " (checking for updates)", + "LspInstallerMuted", + }), + Data.when( + #server.metadata.outdated_packages > 0, + { " new version available", "LspInstallerGreen" } + ) + ), + }, + Ui.Keybind(settings.current.ui.keymaps.toggle_server_expand, "EXPAND_SERVER", { server.name }), + Ui.Keybind(settings.current.ui.keymaps.update_server, "INSTALL_SERVER", { server.name }), + Ui.Keybind(settings.current.ui.keymaps.check_server_version, "CHECK_SERVER_VERSION", { server.name }), + Ui.Keybind(settings.current.ui.keymaps.uninstall_server, "UNINSTALL_SERVER", { server.name }), + Ui.When(is_expanded, function() + return Indent { + ServerMetadata(server), + } + end), + } + end, + servers + )) end ---@param server ServerState @@ -473,6 +513,7 @@ local function create_initial_server_state(server) local server_state = { name = server.name, is_installed = server:is_installed(), + is_checking_outdated_packages = false, deprecated = server.deprecated, hints = tostring(ServerHints.new(server)), expanded_schema_properties = {}, @@ -485,6 +526,8 @@ local function create_initial_server_state(server) install_timestamp_seconds = nil, -- lazy install_dir = vim.fn.fnamemodify(server.root_dir, ":~"), filetypes = table.concat(server:get_supported_filetypes(), ", "), + ---@type OutdatedPackage[] + outdated_packages = {}, }, installer = { is_queued = false, @@ -519,6 +562,7 @@ local function init(all_servers) Ui.Keybind(HELP_KEYMAP, "TOGGLE_HELP", nil, true), Ui.Keybind(CLOSE_WINDOW_KEYMAP_1, "CLOSE_WINDOW", nil, true), Ui.Keybind(CLOSE_WINDOW_KEYMAP_2, "CLOSE_WINDOW", nil, true), + Ui.Keybind(settings.current.ui.keymaps.check_outdated_servers, "CHECK_OUTDATED_SERVERS", nil, true), Ui.Keybind(settings.current.ui.keymaps.update_all_servers, "UPDATE_ALL_SERVERS", nil, true), Header { is_showing_help = state.is_showing_help, @@ -598,12 +642,10 @@ local function init(all_servers) end) end - ---@alias ServerInstallTuple {[1]:Server, [2]: string|nil} - - ---@param server_tuple ServerInstallTuple + ---@param server Server + ---@param requested_version string|nil ---@param on_complete fun() - local function start_install(server_tuple, on_complete) - local server, requested_version = server_tuple[1], server_tuple[2] + local function start_install(server, requested_version, on_complete) mutate_state(function(state) state.servers[server.name].installer.is_queued = false state.servers[server.name].installer.is_running = true @@ -637,6 +679,9 @@ local function init(all_servers) state.servers[server.name].is_installed = success state.servers[server.name].installer.is_running = false state.servers[server.name].installer.has_run = true + if not state.expanded_server then + expand_server(server.name) + end end) if not get_state().expanded_server then expand_server(server.name) @@ -646,33 +691,9 @@ local function init(all_servers) end -- We have a queue because installers have a tendency to hog resources. - local queue - do - local max_running = settings.current.max_concurrent_installers - ---@type ServerInstallTuple[] - local q = {} - local r = 0 - - local check_queue - check_queue = vim.schedule_wrap(function() - if #q > 0 and r < max_running then - local dequeued_server = table.remove(q, 1) - r = r + 1 - start_install(dequeued_server, function() - r = r - 1 - check_queue() - end) - end - end) - - ---@param server Server - ---@param version string|nil - queue = function(server, version) - q[#q + 1] = { server, version } - check_queue() - end - end - + local job_pool = JobExecutionPool:new { + size = settings.current.max_concurrent_installers, + } ---@param server Server ---@param version string|nil local function install_server(server, version) @@ -687,7 +708,9 @@ local function init(all_servers) state.servers[server.name] = create_initial_server_state(server) state.servers[server.name].installer.is_queued = true end) - queue(server, version) + job_pool:supply(function(cb) + start_install(server, version, cb) + end) end ---@param server Server @@ -802,6 +825,27 @@ local function init(all_servers) end end + local has_opened = false + + local function identify_outdated_servers(servers) + -- Sort servers the same way as in the UI, gives a more structured impression + table.sort(servers, function(a, b) + return a.name < b.name + end) + jobs.identify_outdated_servers(servers, function(server) + mutate_state(function(state) + state.servers[server.name].is_checking_outdated_packages = true + end) + end, function(check_result) + mutate_state(function(state) + state.servers[check_result.server.name].is_checking_outdated_packages = false + if check_result.success and check_result:has_outdated_packages() then + state.servers[check_result.server.name].metadata.outdated_packages = check_result.outdated_packages + end + end) + end) + end + local function open() local open_filetypes = {} for _, open_bufnr in ipairs(vim.api.nvim_list_bufs()) do @@ -820,6 +864,13 @@ local function init(all_servers) state.prioritized_servers = Data.set_of(prioritized_servers) end) + if not has_opened then + -- Only do this automatically once - when opening the window the first time + vim.defer_fn(function() + identify_outdated_servers(lsp_servers.get_installed_servers()) + end, 100) + end + window.open { highlight_groups = { "hi def LspInstallerHeader gui=bold guifg=#ebcb8b", @@ -848,6 +899,18 @@ local function init(all_servers) ["CLOSE_WINDOW"] = function() close() end, + ["CHECK_OUTDATED_SERVERS"] = function() + vim.schedule(function() + identify_outdated_servers(lsp_servers.get_installed_servers()) + end) + end, + ["CHECK_SERVER_VERSION"] = function(e) + local server_name = e.payload[1] + local ok, server = lsp_servers.get_server(server_name) + if ok then + identify_outdated_servers { server } + end + end, ["TOGGLE_EXPAND_CURRENT_SETTINGS"] = function() mutate_state(function(state) state.is_current_settings_expanded = not state.is_current_settings_expanded @@ -902,6 +965,7 @@ local function init(all_servers) end, }, } + has_opened = true end return { -- cgit v1.2.3-70-g09d2