aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2022-01-06 18:21:19 +0100
committerGitHub <noreply@github.com>2022-01-06 18:21:19 +0100
commit5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa (patch)
treeb958f26ae3964e7ba8aa3572b81ece1e23b49df1
parentrerun autogen (diff)
downloadmason-5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa.tar
mason-5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa.tar.gz
mason-5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa.tar.bz2
mason-5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa.tar.lz
mason-5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa.tar.xz
mason-5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa.tar.zst
mason-5cc73ef7360866c65169e0e7d55d3b59fb3b6eaa.zip
feat(ui): display outdated servers (#395)
-rw-r--r--lua/nvim-lsp-installer/core/clients/eclipse.lua21
-rw-r--r--lua/nvim-lsp-installer/core/clients/github.lua82
-rw-r--r--lua/nvim-lsp-installer/core/receipt.lua64
-rw-r--r--lua/nvim-lsp-installer/fs.lua11
-rw-r--r--lua/nvim-lsp-installer/installers/context.lua79
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua81
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/git.lua42
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua29
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua25
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/init.lua80
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua24
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua44
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua72
-rw-r--r--lua/nvim-lsp-installer/jobs/outdated-servers/version-check-result.lua39
-rw-r--r--lua/nvim-lsp-installer/jobs/pool.lua41
-rw-r--r--lua/nvim-lsp-installer/server.lua23
-rw-r--r--lua/nvim-lsp-installer/servers/ansiblels/init.lua9
-rw-r--r--lua/nvim-lsp-installer/servers/arduino_language_server/init.lua5
-rw-r--r--lua/nvim-lsp-installer/servers/ccls/init.lua20
-rw-r--r--lua/nvim-lsp-installer/servers/groovyls/init.lua7
-rw-r--r--lua/nvim-lsp-installer/servers/jdtls/init.lua7
-rw-r--r--lua/nvim-lsp-installer/servers/phpactor/init.lua6
-rw-r--r--lua/nvim-lsp-installer/servers/spectral/init.lua4
-rw-r--r--lua/nvim-lsp-installer/settings.lua8
-rw-r--r--lua/nvim-lsp-installer/ui/status-win/init.lua170
-rw-r--r--tests/core/clients/eclipse_spec.lua10
-rw-r--r--tests/jobs/outdated-servers/gem_spec.lua28
-rw-r--r--tests/jobs/outdated-servers/pip3_spec.lua10
28 files changed, 882 insertions, 159 deletions
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<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
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 = "<CR>",
- -- 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 {
diff --git a/tests/core/clients/eclipse_spec.lua b/tests/core/clients/eclipse_spec.lua
new file mode 100644
index 00000000..bd7fac1d
--- /dev/null
+++ b/tests/core/clients/eclipse_spec.lua
@@ -0,0 +1,10 @@
+local eclipse = require "nvim-lsp-installer.core.clients.eclipse"
+
+describe("eclipse client", function()
+ it("parses jdtls version strings", function()
+ assert.equal(
+ "1.8.0-202112170540",
+ eclipse._parse_jdtls_version_string "jdt-language-server-1.8.0-202112170540.tar.gz"
+ )
+ end)
+end)
diff --git a/tests/jobs/outdated-servers/gem_spec.lua b/tests/jobs/outdated-servers/gem_spec.lua
new file mode 100644
index 00000000..6367d7be
--- /dev/null
+++ b/tests/jobs/outdated-servers/gem_spec.lua
@@ -0,0 +1,28 @@
+local gem_check = require "nvim-lsp-installer.jobs.outdated-servers.gem"
+
+describe("gem outdated package checker", function()
+ it("parses outdated gem output", function()
+ local normalize = gem_check.parse_outdated_gem
+ assert.equal(
+ vim.inspect {
+ name = "solargraph",
+ current_version = "0.42.2",
+ latest_version = "0.44.2",
+ },
+ vim.inspect(normalize [[solargraph (0.42.2 < 0.44.2)]])
+ )
+ assert.equal(
+ vim.inspect {
+ name = "sorbet-runtime",
+ current_version = "0.5.9307",
+ latest_version = "0.5.9468",
+ },
+ vim.inspect(normalize [[sorbet-runtime (0.5.9307 < 0.5.9468)]])
+ )
+ end)
+
+ it("returns nil when unable to parse outdated gem", function()
+ assert.is_nil(gem_check.parse_outdated_gem "a whole bunch of gibberish!")
+ assert.is_nil(gem_check.parse_outdated_gem "")
+ end)
+end)
diff --git a/tests/jobs/outdated-servers/pip3_spec.lua b/tests/jobs/outdated-servers/pip3_spec.lua
new file mode 100644
index 00000000..f22bb7e1
--- /dev/null
+++ b/tests/jobs/outdated-servers/pip3_spec.lua
@@ -0,0 +1,10 @@
+local pip3_check = require "nvim-lsp-installer.jobs.outdated-servers.pip3"
+
+describe("pip3 outdated package checker", function()
+ it("normalizes pip3 packages", function()
+ local normalize = pip3_check.normalize_package
+ assert.equal("python-lsp-server", normalize "python-lsp-server[all]")
+ assert.equal("python-lsp-server", normalize "python-lsp-server[]")
+ assert.equal("python-lsp-server", normalize "python-lsp-server[[]]")
+ end)
+end)