aboutsummaryrefslogtreecommitdiffstats
path: root/lua
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2021-09-07 02:44:09 +0200
committerGitHub <noreply@github.com>2021-09-07 02:44:09 +0200
commit00294b84031711013a385f18c0fb0e8db84ebaf9 (patch)
treee45de668229c6b41643c5d1fa0fdb5beb0ff60fa /lua
parentlazily require servers for faster startup times (#77) (diff)
downloadmason-00294b84031711013a385f18c0fb0e8db84ebaf9.tar
mason-00294b84031711013a385f18c0fb0e8db84ebaf9.tar.gz
mason-00294b84031711013a385f18c0fb0e8db84ebaf9.tar.bz2
mason-00294b84031711013a385f18c0fb0e8db84ebaf9.tar.lz
mason-00294b84031711013a385f18c0fb0e8db84ebaf9.tar.xz
mason-00294b84031711013a385f18c0fb0e8db84ebaf9.tar.zst
mason-00294b84031711013a385f18c0fb0e8db84ebaf9.zip
add direct integration with libuv instead of going through termopen, also implement a UI (#79)
* add direct integration with libuv instead of going through termopen, also implement a UI * alleged free perf boosts yo that's free cycles
Diffstat (limited to 'lua')
-rw-r--r--lua/nvim-lsp-installer.lua138
-rw-r--r--lua/nvim-lsp-installer/data.lua36
-rw-r--r--lua/nvim-lsp-installer/dispatcher.lua4
-rw-r--r--lua/nvim-lsp-installer/installers/go.lua28
-rw-r--r--lua/nvim-lsp-installer/installers/init.lua41
-rw-r--r--lua/nvim-lsp-installer/installers/npm.lua10
-rw-r--r--lua/nvim-lsp-installer/installers/pip3.lua21
-rw-r--r--lua/nvim-lsp-installer/installers/shell.lua38
-rw-r--r--lua/nvim-lsp-installer/installers/zx.lua52
-rw-r--r--lua/nvim-lsp-installer/log.lua18
-rw-r--r--lua/nvim-lsp-installer/process.lua135
-rw-r--r--lua/nvim-lsp-installer/server.lua38
-rw-r--r--lua/nvim-lsp-installer/servers/init.lua105
-rw-r--r--lua/nvim-lsp-installer/servers/tflint/init.lua23
-rw-r--r--lua/nvim-lsp-installer/ui/display.lua239
-rw-r--r--lua/nvim-lsp-installer/ui/init.lua70
-rw-r--r--lua/nvim-lsp-installer/ui/state.lua22
-rw-r--r--lua/nvim-lsp-installer/ui/status-win/init.lua334
18 files changed, 1119 insertions, 233 deletions
diff --git a/lua/nvim-lsp-installer.lua b/lua/nvim-lsp-installer.lua
index ba61d462..27727370 100644
--- a/lua/nvim-lsp-installer.lua
+++ b/lua/nvim-lsp-installer.lua
@@ -1,145 +1,36 @@
local notify = require "nvim-lsp-installer.notify"
local dispatcher = require "nvim-lsp-installer.dispatcher"
+local status_win = require "nvim-lsp-installer.ui.status-win"
+local servers = require "nvim-lsp-installer.servers"
local M = {}
-function Set(list)
- local set = {}
- for _, l in ipairs(list) do
- set[l] = true
- end
- return set
-end
-
--- :'<,'>!sort
-local CORE_SERVERS = Set {
- "angularls",
- "ansiblels",
- "bashls",
- "clangd",
- "clojure_lsp",
- "cmake",
- "cssls",
- "denols",
- "diagnosticls",
- "dockerls",
- "efm",
- "elixirls",
- "elmls",
- "ember",
- "eslintls",
- "fortls",
- "gopls",
- "graphql",
- "groovyls",
- "hls",
- "html",
- "intelephense",
- "jedi_language_server",
- "jsonls",
- "kotlin_language_server",
- "omnisharp",
- "purescriptls",
- "pylsp",
- "pyright",
- "rescriptls",
- "rome",
- "rust_analyzer",
- "solargraph",
- "sqlls",
- "sqls",
- "stylelint_lsp",
- "sumneko_lua",
- "svelte",
- "tailwindcss",
- "terraformls",
- "texlab",
- "tflint",
- "tsserver",
- "vimls",
- "vuels",
- "yamlls",
-}
-
-local CUSTOM_SERVERS_MAP = {}
-
-function M.get_server(server_name)
- -- Registered custom servers have precedence
- if CUSTOM_SERVERS_MAP[server_name] then
- return true, CUSTOM_SERVERS_MAP[server_name]
- end
-
- if not CORE_SERVERS[server_name] then
- return false, ("Server %s does not exist."):format(server_name)
- end
-
- local ok, server = pcall(require, ("nvim-lsp-installer.servers.%s"):format(server_name))
- if ok then
- return true, server
- end
- return false,
- (
- "Unable to import server %s.\n\nThis is an unexpected error, please file an issue at %s with the following information:\n%s"
- ):format(server_name, "https://github.com/williamboman/nvim-lsp-installer", server)
-end
-
-function M.get_available_servers()
- return vim.tbl_map(function(server_name)
- local ok, server = M.get_server(server_name)
- if not ok then
- error(server)
- end
- return server
- end, vim.tbl_keys(
- vim.tbl_extend("force", CORE_SERVERS, CUSTOM_SERVERS_MAP)
- ))
-end
-
-function M.get_installed_servers()
- return vim.tbl_filter(function(server)
- return server:is_installed()
- end, M.get_available_servers())
-end
-
-function M.get_uninstalled_servers()
- return vim.tbl_filter(function(server)
- return not server:is_installed()
- end, M.get_available_servers())
+function M.display()
+ status_win().open()
end
function M.install(server_name)
- local ok, server = M.get_server(server_name)
+ local ok, server = servers.get_server(server_name)
if not ok then
return notify(("Unable to find LSP server %s.\n\n%s"):format(server_name, server), vim.log.levels.ERROR)
end
- local success, error = pcall(server.install, server)
- if not success then
- pcall(server.uninstall, server)
- return notify(("Failed to install %s.\n\n%s"):format(server_name, vim.inspect(error)), vim.log.levels.ERROR)
- end
+ status_win().install_server(server)
+ status_win().open()
end
function M.uninstall(server_name)
- local ok, server = M.get_server(server_name)
+ local ok, server = servers.get_server(server_name)
if not ok then
return notify(("Unable to find LSP server %s.\n\n%s"):format(server_name, server), vim.log.levels.ERROR)
end
- local success, error = pcall(server.uninstall, server)
- if not success then
- notify(("Unable to uninstall %s.\n\n%s"):format(server_name, vim.inspect(error)), vim.log.levels.ERROR)
- return success
- end
- notify(("Successfully uninstalled %s."):format(server_name))
-end
-
-function M.register(server)
- CUSTOM_SERVERS_MAP[server.name] = server
+ status_win().uninstall_server(server)
+ status_win().open()
end
function M.on_server_ready(cb)
dispatcher.register_server_ready_callback(cb)
vim.schedule(function()
- for _, server in pairs(M.get_installed_servers()) do
+ for _, server in pairs(servers.get_installed_servers()) do
dispatcher.dispatch_server_ready(server)
end
end)
@@ -160,4 +51,11 @@ function M.lsp_attach_proxy()
end)
end
+-- old API
+M.get_server = servers.get_server
+M.get_available_servers = servers.get_available_servers
+M.get_installed_servers = servers.get_installed_servers
+M.get_uninstalled_servers = servers.get_uninstalled_servers
+M.register = servers.register
+
return M
diff --git a/lua/nvim-lsp-installer/data.lua b/lua/nvim-lsp-installer/data.lua
new file mode 100644
index 00000000..3d6bf196
--- /dev/null
+++ b/lua/nvim-lsp-installer/data.lua
@@ -0,0 +1,36 @@
+local Data = {}
+
+function Data.enum(values)
+ local result = {}
+ for i = 1, #values do
+ local v = values[i]
+ result[v] = v
+ end
+ return result
+end
+
+function Data.set_of(list)
+ local set = {}
+ for i = 1, #list do
+ set[list[i]] = true
+ end
+ return set
+end
+
+function Data.list_reverse(list)
+ local result = {}
+ for i = #list, 1, -1 do
+ result[#result + 1] = list[i]
+ end
+ return result
+end
+
+function Data.list_map(fn, list)
+ local result = {}
+ for i = 1, #list do
+ result[#result + 1] = fn(list[i], i)
+ end
+ return result
+end
+
+return Data
diff --git a/lua/nvim-lsp-installer/dispatcher.lua b/lua/nvim-lsp-installer/dispatcher.lua
index 036c6d91..00f38de9 100644
--- a/lua/nvim-lsp-installer/dispatcher.lua
+++ b/lua/nvim-lsp-installer/dispatcher.lua
@@ -2,11 +2,11 @@ local M = {}
local registered_callbacks = {}
-function M.dispatch_server_ready(server)
+M.dispatch_server_ready = vim.schedule_wrap(function(server)
for _, callback in pairs(registered_callbacks) do
callback(server)
end
-end
+end)
local idx = 0
function M.register_server_ready_callback(callback)
diff --git a/lua/nvim-lsp-installer/installers/go.lua b/lua/nvim-lsp-installer/installers/go.lua
index 9c9a1ad5..61e29383 100644
--- a/lua/nvim-lsp-installer/installers/go.lua
+++ b/lua/nvim-lsp-installer/installers/go.lua
@@ -1,22 +1,24 @@
local path = require "nvim-lsp-installer.path"
-local shell = require "nvim-lsp-installer.installers.shell"
+local process = require "nvim-lsp-installer.process"
local M = {}
function M.packages(packages)
- return function(server, callback)
- local shell_installer = shell.polyshell(
- ("go get -v %s && go clean -modcache"):format(table.concat(packages, " ")),
- {
- env = {
- GO111MODULE = "on",
- GOBIN = server._root_dir,
- GOPATH = server._root_dir,
- },
- }
- )
+ return function(server, callback, context)
+ local c = process.chain {
+ env = process.graft_env {
+ GO111MODULE = "on",
+ GOBIN = server.root_dir,
+ GOPATH = server.root_dir,
+ },
+ cwd = server.root_dir,
+ stdio_sink = context.stdio_sink,
+ }
- shell_installer(server, callback)
+ c.run("go", vim.list_extend({ "get", "-v" }, packages))
+ c.run("go", { "clean", "-modcache" })
+
+ c.spawn(callback)
end
end
diff --git a/lua/nvim-lsp-installer/installers/init.lua b/lua/nvim-lsp-installer/installers/init.lua
index f394a33d..991c773e 100644
--- a/lua/nvim-lsp-installer/installers/init.lua
+++ b/lua/nvim-lsp-installer/installers/init.lua
@@ -1,48 +1,57 @@
local platform = require "nvim-lsp-installer.platform"
+local Data = require "nvim-lsp-installer.data"
local M = {}
-function M.compose(installers)
+function M.join(installers)
if #installers == 0 then
- error "No installers to compose."
+ error "No installers to join."
end
- return function(server, callback)
+ return function(server, callback, context)
local function execute(idx)
- installers[idx](server, function(success, result)
+ installers[idx](server, function(success)
if not success then
-- oh no, error. exit early
- callback(success, result)
- elseif installers[idx - 1] then
+ callback(success)
+ elseif installers[idx + 1] then
-- iterate
- execute(idx - 1)
+ execute(idx + 1)
else
-- we done
- callback(success, result)
+ callback(success)
end
- end)
+ end, context)
end
- execute(#installers)
+ execute(1)
end
end
+-- much fp, very wow
+function M.compose(installers)
+ return M.join(Data.list_reverse(installers))
+end
+
function M.when(platform_table)
- return function(server, callback)
+ return function(server, callback, context)
if platform.is_unix() then
if platform_table.unix then
- platform_table.unix(server, callback)
+ platform_table.unix(server, callback, context)
else
- callback(false, ("Unix is not yet supported for server %q."):format(server.name))
+ context.stdio_sink.stderr(("Unix is not yet supported for server %q."):format(server.name))
+ callback(false)
end
elseif platform.is_win() then
if platform_table.win then
- platform_table.win(server, callback)
+ platform_table.win(server, callback, context)
else
- callback(false, ("Windows is not yet supported for server %q."):format(server.name))
+ context.stdio_sink.stderr(("Windows is not yet supported for server %q."):format(server.name))
+ callback(false)
end
else
- callback(false, "installers.when: Could not find installer for current platform.")
+ context.sdtio_sink.stderr "installers.when: Could not find installer for current platform."
+ callback(false)
end
end
end
diff --git a/lua/nvim-lsp-installer/installers/npm.lua b/lua/nvim-lsp-installer/installers/npm.lua
index 6b6b820e..d3c8167f 100644
--- a/lua/nvim-lsp-installer/installers/npm.lua
+++ b/lua/nvim-lsp-installer/installers/npm.lua
@@ -1,11 +1,17 @@
local path = require "nvim-lsp-installer.path"
local platform = require "nvim-lsp-installer.platform"
-local shell = require "nvim-lsp-installer.installers.shell"
+local process = require "nvim-lsp-installer.process"
local M = {}
function M.packages(packages)
- return shell.polyshell(("npm install %s"):format(table.concat(packages, " ")))
+ return function(server, callback, context)
+ process.spawn(platform.is_win() and "npm.cmd" or "npm", {
+ args = vim.list_extend({ "install" }, packages),
+ cwd = server.root_dir,
+ stdio_sink = context.stdio_sink,
+ }, callback)
+ end
end
function M.executable(root_dir, executable)
diff --git a/lua/nvim-lsp-installer/installers/pip3.lua b/lua/nvim-lsp-installer/installers/pip3.lua
index 79fbb0f7..5aefe372 100644
--- a/lua/nvim-lsp-installer/installers/pip3.lua
+++ b/lua/nvim-lsp-installer/installers/pip3.lua
@@ -1,22 +1,23 @@
local path = require "nvim-lsp-installer.path"
local platform = require "nvim-lsp-installer.platform"
-local shell = require "nvim-lsp-installer.installers.shell"
+local process = require "nvim-lsp-installer.process"
local M = {}
local REL_INSTALL_DIR = "venv"
function M.packages(packages)
- local venv_activate_cmd = platform.is_win() and (".\\%s\\Scripts\\activate"):format(REL_INSTALL_DIR)
- or ("source ./%s/bin/activate"):format(REL_INSTALL_DIR)
+ return function(server, callback, context)
+ local c = process.chain {
+ cwd = server.root_dir,
+ stdio_sink = context.stdio_sink,
+ }
- return shell.polyshell(
- ("python3 -m venv %q && %s && pip3 install -U %s"):format(
- REL_INSTALL_DIR,
- venv_activate_cmd,
- table.concat(packages, " ")
- )
- )
+ c.run("python3", { "-m", "venv", REL_INSTALL_DIR })
+ c.run(M.executable(server.root_dir, "pip3"), vim.list_extend({ "install", "-U" }, packages))
+
+ c.spawn(callback)
+ end
end
function M.executable(root_dir, executable)
diff --git a/lua/nvim-lsp-installer/installers/shell.lua b/lua/nvim-lsp-installer/installers/shell.lua
index 98f12103..37b5e03f 100644
--- a/lua/nvim-lsp-installer/installers/shell.lua
+++ b/lua/nvim-lsp-installer/installers/shell.lua
@@ -1,31 +1,21 @@
local installers = require "nvim-lsp-installer.installers"
+local process = require "nvim-lsp-installer.process"
local M = {}
-local function termopen(opts)
- return function(server, callback)
- local jobstart_opts = {
- cwd = server._root_dir,
- on_exit = function(_, exit_code)
- if exit_code ~= 0 then
- callback(false, ("Exit code %d"):format(exit_code))
- else
- callback(true, nil)
- end
- end,
- }
+local function shell(opts)
+ return function(server, callback, installer_opts)
+ local _, stdio = process.spawn(opts.shell, {
+ cwd = server.root_dir,
+ stdio_sink = installer_opts.stdio_sink,
+ env = process.graft_env(opts.env or {}),
+ }, callback)
- if type(opts.env) == "table" and vim.tbl_count(opts.env) > 0 then
- -- passing an empty Lua table causes E475, for whatever reason
- jobstart_opts.env = opts.env
- end
+ local stdin = stdio[1]
- local orig_shell = vim.o.shell
- vim.o.shell = opts.shell
- vim.cmd [[new]]
- vim.fn.termopen(opts.cmd, jobstart_opts)
- vim.o.shell = orig_shell
- vim.cmd [[startinsert]] -- so that we tail the term log nicely ¯\_(ツ)_/¯
+ stdin:write(opts.cmd)
+ stdin:write "\n"
+ stdin:close()
end
end
@@ -36,7 +26,7 @@ function M.bash(raw_script, opts)
}
opts = vim.tbl_deep_extend("force", default_opts, opts or {})
- return termopen {
+ return shell {
shell = "/bin/bash",
cmd = (opts.prefix or "") .. raw_script,
env = opts.env,
@@ -53,7 +43,7 @@ function M.cmd(raw_script, opts)
}
opts = vim.tbl_deep_extend("force", default_opts, opts or {})
- return termopen {
+ return shell {
shell = "cmd.exe",
cmd = raw_script,
env = opts.env,
diff --git a/lua/nvim-lsp-installer/installers/zx.lua b/lua/nvim-lsp-installer/installers/zx.lua
index b713b4ec..9b24e1af 100644
--- a/lua/nvim-lsp-installer/installers/zx.lua
+++ b/lua/nvim-lsp-installer/installers/zx.lua
@@ -1,12 +1,9 @@
local fs = require "nvim-lsp-installer.fs"
local path = require "nvim-lsp-installer.path"
-local notify = require "nvim-lsp-installer.notify"
local installers = require "nvim-lsp-installer.installers"
local platform = require "nvim-lsp-installer.platform"
-local shell = require "nvim-lsp-installer.installers.shell"
local npm = require "nvim-lsp-installer.installers.npm"
-
-local uv = vim.loop
+local process = require "nvim-lsp-installer.process"
local M = {}
@@ -18,7 +15,7 @@ local has_installed_zx = false
local function zx_installer(force)
force = force or false -- be careful with boolean logic if flipping this
- return function(_, callback)
+ return function(_, callback, opts)
if has_installed_zx and not force then
callback(true, "zx already installed")
return
@@ -33,38 +30,47 @@ local function zx_installer(force)
local npm_command = is_zx_already_installed and "update" or "install"
if not is_zx_already_installed then
- notify(("Preparing for :LspInstall… ($ npm %s zx)"):format(npm_command))
+ opts.stdio_sink.stdout(("Preparing for installation… (npm %s zx)"):format(npm_command))
end
fs.mkdirp(INSTALL_DIR)
- local handle, pid = uv.spawn(
- platform.is_win() and "npm.cmd" or "npm",
- {
- args = { npm_command, "zx@1" },
- cwd = INSTALL_DIR,
- },
- vim.schedule_wrap(function(code)
- if code ~= 0 then
- callback(false, "Failed to install zx.")
- return
- end
+ -- todo use process installer
+ local handle, pid = process.spawn(platform.is_win() and "npm.cmd" or "npm", {
+ args = { npm_command, "zx@1" },
+ cwd = INSTALL_DIR,
+ stdio_sink = opts.stdio_sink,
+ }, function(success)
+ if success then
has_installed_zx = true
- vim.cmd [[ echon "" ]] -- clear the previously printed feedback message… ¯\_(ツ)_/¯
- callback(true, nil)
- end)
- )
+ callback(true)
+ else
+ opts.stdio_sink.stderr "Failed to install zx."
+ callback(false)
+ end
+ end)
if handle == nil then
- callback(false, ("Failed to install/update zx. %s"):format(pid))
+ opts.stdio_sink.stderr(("Failed to install/update zx. %s"):format(pid))
+ callback(false)
end
end
end
+local function exec(file)
+ return function(server, callback, context)
+ process.spawn(ZX_EXECUTABLE, {
+ args = { file },
+ cwd = server.root_dir,
+ stdio_sink = context.stdio_sink,
+ }, callback)
+ end
+end
+
function M.file(relpath)
local script_path = path.realpath(relpath, 3)
return installers.compose {
- shell.polyshell(("%q %q"):format(ZX_EXECUTABLE, ("file:///%s"):format(script_path))),
+ exec(("file:///%s"):format(script_path)),
zx_installer(false),
}
end
diff --git a/lua/nvim-lsp-installer/log.lua b/lua/nvim-lsp-installer/log.lua
new file mode 100644
index 00000000..1c61180b
--- /dev/null
+++ b/lua/nvim-lsp-installer/log.lua
@@ -0,0 +1,18 @@
+local M = {}
+
+-- TODO
+
+function M.debug(...)
+ -- print("[debug]", vim.inspect(...))
+end
+function M.error(...)
+ -- print("[error]", vim.inspect(...))
+end
+function M.warn(...)
+ -- print("[warn]", vim.inspect(...))
+end
+function M.info(...)
+ -- print("[info]", vim.inspect(...))
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/process.lua b/lua/nvim-lsp-installer/process.lua
new file mode 100644
index 00000000..61097a88
--- /dev/null
+++ b/lua/nvim-lsp-installer/process.lua
@@ -0,0 +1,135 @@
+local log = require "nvim-lsp-installer.log"
+local uv = vim.loop
+
+local M = {}
+
+local function connect_sink(pipe, sink)
+ return function(err, data)
+ if err then
+ log.error { "Unexpected error when reading pipe.", err }
+ end
+ if data ~= nil then
+ local lines = vim.split(data, "\n")
+ for i = 1, #lines do
+ sink(lines[i])
+ end
+ else
+ pipe:read_stop()
+ pipe:close()
+ end
+ end
+end
+
+function M.graft_env(env)
+ local root_env = {}
+ for key, val in pairs(vim.fn.environ()) do
+ root_env[#root_env + 1] = key .. "=" .. val
+ end
+ for key, val in pairs(env) do
+ root_env[#root_env + 1] = key .. "=" .. val
+ end
+ return root_env
+end
+
+function M.spawn(cmd, opts, callback)
+ local stdin = uv.new_pipe(false)
+ local stdout = uv.new_pipe(false)
+ local stderr = uv.new_pipe(false)
+
+ local stdio = { stdin, stdout, stderr }
+
+ log.debug { "Spawning", cmd, opts }
+
+ local spawn_opts = {
+ env = opts.env,
+ stdio = stdio,
+ args = opts.args,
+ cwd = opts.cwd,
+ detached = false,
+ hide = true,
+ }
+
+ local handle, pid
+ handle, pid = uv.spawn(cmd, spawn_opts, function(exit_code, signal)
+ local successful = exit_code == 0 and signal == 0
+ handle:close()
+ if not stdin:is_closing() then
+ stdin:close()
+ end
+
+ -- ensure all pipes are closed, for I am a qualified plumber
+ local check = uv.new_check()
+ check:start(function()
+ for i = 1, #stdio do
+ local pipe = stdio[i]
+ if not pipe:is_closing() then
+ return
+ end
+ end
+ check:stop()
+ callback(successful)
+ end)
+ end)
+
+ if handle == nil then
+ opts.stdio_sink.stderr(("Failed to spawn process cmd=%s pid=%s"):format(cmd, pid))
+ callback(false)
+ return nil, nil
+ end
+
+ handle:unref()
+ log.debug { "Spawned with pid", pid }
+
+ stdout:read_start(connect_sink(stdout, opts.stdio_sink.stdout))
+ stderr:read_start(connect_sink(stderr, opts.stdio_sink.stderr))
+
+ return handle, stdio
+end
+
+function M.chain(opts)
+ local stack = {}
+ return {
+ run = function(cmd, args)
+ stack[#stack + 1] = { cmd = cmd, args = args }
+ end,
+ spawn = function(callback)
+ local function execute(idx)
+ local item = stack[idx]
+ M.spawn(
+ item.cmd,
+ vim.tbl_deep_extend("force", opts, {
+ args = item.args,
+ }),
+ function(successful)
+ if successful and stack[idx + 1] then
+ -- iterate
+ execute(idx + 1)
+ else
+ -- we done
+ callback(successful)
+ end
+ end
+ )
+ end
+
+ execute(1)
+ end,
+ }
+end
+
+function M.empty_sink()
+ local function noop() end
+ return {
+ stdout = noop,
+ stderr = noop,
+ }
+end
+
+function M.simple_sink()
+ return {
+ stdout = print,
+ stderr = vim.api.nvim_err_writeln,
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/server.lua b/lua/nvim-lsp-installer/server.lua
index d760f643..a2bc4b5d 100644
--- a/lua/nvim-lsp-installer/server.lua
+++ b/lua/nvim-lsp-installer/server.lua
@@ -1,7 +1,7 @@
-local notify = require "nvim-lsp-installer.notify"
local dispatcher = require "nvim-lsp-installer.dispatcher"
local fs = require "nvim-lsp-installer.fs"
local path = require "nvim-lsp-installer.path"
+local status_win = require "nvim-lsp-installer.ui.status-win"
local M = {}
@@ -37,8 +37,9 @@ M.Server.__index = M.Server
function M.Server:new(opts)
return setmetatable({
name = opts.name,
+ root_dir = opts.root_dir,
+ _root_dir = opts.root_dir, -- @deprecated Use the `root_dir` property instead.
_installer = opts.installer,
- _root_dir = opts.root_dir,
_default_options = opts.default_options,
_pre_install_check = opts.pre_install_check,
_post_setup = opts.post_setup,
@@ -72,6 +73,27 @@ function M.Server:create_root_dir()
end
function M.Server:install()
+ status_win().install_server(self)
+end
+
+function M.Server:install_attached(opts, callback)
+ local ok, err = pcall(self.pre_install, self)
+ if not ok then
+ opts.stdio_sink.stderr(tostring(err))
+ callback(false)
+ return
+ end
+ self._installer(self, function(success)
+ if not success then
+ pcall(self.uninstall, self)
+ else
+ dispatcher.dispatch_server_ready(self)
+ end
+ callback(success)
+ end, opts)
+end
+
+function M.Server:pre_install()
if self._pre_install_check then
self._pre_install_check()
end
@@ -82,18 +104,6 @@ function M.Server:install()
self:uninstall()
self:create_root_dir()
-
- notify(("Installing %s…"):format(self.name))
-
- self._installer(self, function(success, result)
- if not success then
- notify(("Server installation failed for %s.\n\n%s"):format(self.name, result), vim.log.levels.ERROR)
- pcall(self.uninstall, self)
- else
- notify(("Successfully installed %s."):format(self.name))
- dispatcher.dispatch_server_ready(self)
- end
- end)
end
function M.Server:uninstall()
diff --git a/lua/nvim-lsp-installer/servers/init.lua b/lua/nvim-lsp-installer/servers/init.lua
new file mode 100644
index 00000000..54932cab
--- /dev/null
+++ b/lua/nvim-lsp-installer/servers/init.lua
@@ -0,0 +1,105 @@
+local Data = require "nvim-lsp-installer.data"
+
+local M = {}
+
+-- :'<,'>!sort
+local CORE_SERVERS = Data.set_of {
+ "angularls",
+ "ansiblels",
+ "bashls",
+ "clangd",
+ "clojure_lsp",
+ "cmake",
+ "cssls",
+ "denols",
+ "diagnosticls",
+ "dockerls",
+ "efm",
+ "elixirls",
+ "elmls",
+ "ember",
+ "eslintls",
+ "fortls",
+ "gopls",
+ "graphql",
+ "groovyls",
+ "hls",
+ "html",
+ "intelephense",
+ "jedi_language_server",
+ "jsonls",
+ "kotlin_language_server",
+ "omnisharp",
+ "purescriptls",
+ "pylsp",
+ "pyright",
+ "rescriptls",
+ "rome",
+ "rust_analyzer",
+ "solargraph",
+ "sqlls",
+ "sqls",
+ "stylelint_lsp",
+ "sumneko_lua",
+ "svelte",
+ "tailwindcss",
+ "terraformls",
+ "texlab",
+ "tflint",
+ "tsserver",
+ "vimls",
+ "vuels",
+ "yamlls",
+}
+
+local CUSTOM_SERVERS_MAP = {}
+
+function M.get_server(server_name)
+ -- Registered custom servers have precedence
+ if CUSTOM_SERVERS_MAP[server_name] then
+ return true, CUSTOM_SERVERS_MAP[server_name]
+ end
+
+ if not CORE_SERVERS[server_name] then
+ return false, ("Server %s does not exist."):format(server_name)
+ end
+
+ local ok, server = pcall(require, ("nvim-lsp-installer.servers.%s"):format(server_name))
+ if ok then
+ return true, server
+ end
+ return false,
+ (
+ "Unable to import server %s.\n\nThis is an unexpected error, please file an issue at %s with the following information:\n%s"
+ ):format(server_name, "https://github.com/williamboman/nvim-lsp-installer", server)
+end
+
+function M.get_available_servers()
+ return Data.list_map(function(server_name)
+ local ok, server = M.get_server(server_name)
+ if not ok then
+ error(server)
+ end
+ return server
+ end, vim.tbl_keys(
+ vim.tbl_extend("force", CORE_SERVERS, CUSTOM_SERVERS_MAP)
+ ))
+end
+
+function M.get_installed_servers()
+ return vim.tbl_filter(function(server)
+ return server:is_installed()
+ end, M.get_available_servers())
+end
+
+function M.get_uninstalled_servers()
+ return vim.tbl_filter(function(server)
+ return not server:is_installed()
+ end, M.get_available_servers())
+end
+
+function M.register(server)
+ CUSTOM_SERVERS_MAP[server.name] = server
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/servers/tflint/init.lua b/lua/nvim-lsp-installer/servers/tflint/init.lua
index 51de105e..289f68dc 100644
--- a/lua/nvim-lsp-installer/servers/tflint/init.lua
+++ b/lua/nvim-lsp-installer/servers/tflint/init.lua
@@ -3,6 +3,7 @@ local notify = require "nvim-lsp-installer.notify"
local path = require "nvim-lsp-installer.path"
local installers = require "nvim-lsp-installer.installers"
local shell = require "nvim-lsp-installer.installers.shell"
+local process = require "nvim-lsp-installer.process"
local root_dir = server.get_server_root_path "tflint"
@@ -25,17 +26,21 @@ return server.Server:new {
post_setup = function()
function _G.lsp_installer_tflint_init()
notify "Installing TFLint plugins…"
- vim.fn.termopen(("%q --init"):format(bin_path), {
- cwd = path.cwd(),
- on_exit = function(_, exit_code)
- if exit_code ~= 0 then
- notify(("Failed to install TFLint (exit code %)."):format(exit_code))
- else
+ process.spawn(
+ bin_path,
+ {
+ args = { "--init" },
+ cwd = path.cwd(),
+ stdio_sink = process.simple_sink(),
+ },
+ vim.schedule_wrap(function(success)
+ if success then
notify "Successfully installed TFLint plugins."
+ else
+ notify "Failed to install TFLint."
end
- end,
- })
- vim.cmd [[startinsert]] -- so that we tail the term log nicely ¯\_(ツ)_/¯
+ end)
+ )
end
vim.cmd [[ command! TFLintInit call v:lua.lsp_installer_tflint_init() ]]
diff --git a/lua/nvim-lsp-installer/ui/display.lua b/lua/nvim-lsp-installer/ui/display.lua
new file mode 100644
index 00000000..1027afaa
--- /dev/null
+++ b/lua/nvim-lsp-installer/ui/display.lua
@@ -0,0 +1,239 @@
+local Ui = require "nvim-lsp-installer.ui"
+local log = require "nvim-lsp-installer.log"
+local state = require "nvim-lsp-installer.ui.state"
+
+local M = {}
+
+local redraw_by_winnr = {}
+
+function _G.lsp_install_redraw(winnr)
+ local fn = redraw_by_winnr[winnr]
+ if fn then
+ fn()
+ end
+end
+
+local function debounced(debounced_fn)
+ local queued = false
+ local last_arg = nil
+ return function(a)
+ last_arg = a
+ if queued then
+ return
+ end
+ queued = true
+ vim.schedule(function()
+ debounced_fn(last_arg)
+ queued = false
+ end)
+ end
+end
+
+local function get_styles(line, render_context)
+ local indentation = 0
+
+ for i = 1, #render_context.applied_block_styles do
+ local styles = render_context.applied_block_styles[i]
+ for j = 1, #styles do
+ local style = styles[j]
+ if style == Ui.CascadingStyle.INDENT then
+ indentation = indentation + 2
+ elseif style == Ui.CascadingStyle.CENTERED then
+ local padding = math.floor((render_context.context.win_width - #line) / 2)
+ indentation = math.max(0, padding) -- CENTERED overrides any already applied indentation
+ end
+ end
+ end
+
+ return {
+ indentation = indentation,
+ }
+end
+
+local function render_node(context, node, _render_context, _output)
+ local render_context = _render_context or {
+ context = context,
+ applied_block_styles = {},
+ }
+ local output = _output or {
+ lines = {},
+ virt_texts = {},
+ highlights = {},
+ }
+
+ if node.type == Ui.NodeType.VIRTUAL_TEXT then
+ output.virt_texts[#output.virt_texts + 1] = {
+ line = #output.lines - 1,
+ content = node.virt_text,
+ }
+ elseif node.type == Ui.NodeType.HL_TEXT then
+ for i = 1, #node.lines do
+ local line = node.lines[i]
+ local line_highlights = {}
+ local full_line = ""
+ for j = 1, #line do
+ local span = line[j]
+ local content, hl_group = span[1], span[2]
+ local col_start = #full_line
+ full_line = full_line .. content
+ line_highlights[#line_highlights + 1] = {
+ hl_group = hl_group,
+ line = #output.lines,
+ col_start = col_start,
+ col_end = col_start + #content,
+ }
+ end
+
+ local active_styles = get_styles(full_line, render_context)
+
+ -- apply indentation
+ full_line = (" "):rep(active_styles.indentation) .. full_line
+ for i = 1, #line_highlights do
+ local highlight = line_highlights[i]
+ highlight.col_start = highlight.col_start + active_styles.indentation
+ highlight.col_end = highlight.col_end + active_styles.indentation
+ output.highlights[#output.highlights + 1] = highlight
+ end
+
+ output.lines[#output.lines + 1] = full_line
+ end
+ elseif node.type == Ui.NodeType.NODE or node.type == Ui.NodeType.STYLE_BLOCK then
+ if node.type == Ui.NodeType.STYLE_BLOCK then
+ render_context.applied_block_styles[#render_context.applied_block_styles + 1] = node.styles
+ end
+ for i = 1, #node.children do
+ render_node(context, node.children[i], render_context, output)
+ end
+ if node.type == Ui.NodeType.STYLE_BLOCK then
+ render_context.applied_block_styles[#render_context.applied_block_styles] = nil
+ end
+ end
+
+ return output
+end
+
+function M.new_view_only_win(name)
+ local namespace = vim.api.nvim_create_namespace(("lsp_installer_%s"):format(name))
+ local win, buf, renderer, mutate_state, get_state, unsubscribe
+ local has_initiated = false
+
+ local function open(opts)
+ opts = opts or {}
+ local win_width, highlight_groups = opts.win_width, opts.highlight_groups
+
+ if win_width then
+ vim.cmd(("%dvnew"):format(win_width))
+ else
+ vim.cmd [[vnew]]
+ end
+
+ win = vim.api.nvim_get_current_win()
+ buf = vim.api.nvim_get_current_buf()
+
+ vim.api.nvim_buf_set_option(buf, "modifiable", false)
+ vim.api.nvim_buf_set_option(buf, "swapfile", false)
+ vim.api.nvim_buf_set_option(buf, "textwidth", 0)
+ vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
+ vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
+ vim.api.nvim_buf_set_option(buf, "buflisted", false)
+ vim.api.nvim_buf_set_option(buf, "filetype", "lsp-installer")
+
+ vim.api.nvim_win_set_option(win, "wrap", false)
+ vim.api.nvim_win_set_option(win, "spell", false)
+ vim.api.nvim_win_set_option(win, "number", false)
+ vim.api.nvim_win_set_option(win, "relativenumber", false)
+ vim.api.nvim_win_set_option(win, "foldenable", false)
+ vim.api.nvim_win_set_option(win, "signcolumn", "no")
+ vim.api.nvim_win_set_option(win, "colorcolumn", "")
+
+ vim.cmd [[ syntax clear ]]
+
+ for _, redraw_event in ipairs { "WinEnter", "WinLeave", "VimResized" } do
+ vim.cmd(("autocmd %s <buffer> call v:lua.lsp_install_redraw(%d)"):format(redraw_event, win))
+ end
+
+ if highlight_groups then
+ for i = 1, #highlight_groups do
+ vim.cmd(highlight_groups[i])
+ end
+ end
+ end
+
+ local draw = debounced(function(view)
+ if not win or not vim.api.nvim_win_is_valid(win) then
+ -- the window has been closed, e.g, by the user
+ unsubscribe(true)
+ return log.debug { "Window is no longer valid", name, win }
+ end
+
+ local win_width = vim.api.nvim_win_get_width(win)
+ local context = {
+ win_width = win_width,
+ }
+ local output = render_node(context, view)
+ local lines, virt_texts, highlights = output.lines, output.virt_texts, output.highlights
+
+ vim.api.nvim_buf_clear_namespace(0, namespace, 0, -1)
+ vim.api.nvim_buf_set_option(buf, "modifiable", true)
+ vim.api.nvim_buf_set_lines(buf, 0, -1, true, lines)
+ vim.api.nvim_buf_set_option(buf, "modifiable", false)
+ for i = 1, #virt_texts do
+ local virt_text = virt_texts[i]
+ vim.api.nvim_buf_set_extmark(buf, namespace, virt_text.line, 0, {
+ virt_text = virt_text.content,
+ })
+ end
+ for i = 1, #highlights do
+ local highlight = highlights[i]
+ vim.api.nvim_buf_add_highlight(
+ buf,
+ namespace,
+ highlight.hl_group,
+ highlight.line,
+ highlight.col_start,
+ highlight.col_end
+ )
+ end
+ end)
+
+ return {
+ view = function(x)
+ renderer = x
+ end,
+ init = function(initial_state)
+ assert(renderer ~= nil, "No view function has been registered. Call .view() before .init().")
+ has_initiated = true
+
+ mutate_state, get_state, unsubscribe = state.create_state_container(initial_state, function(new_state)
+ draw(renderer(new_state))
+ end)
+
+ return mutate_state, get_state
+ end,
+ open = vim.schedule_wrap(function(opts)
+ log.debug { "opening window" }
+ assert(has_initiated, "Display has not been initiated, cannot open.")
+ if win and vim.api.nvim_win_is_valid(win) then
+ return
+ end
+ unsubscribe(false)
+ open(opts)
+ draw(renderer(get_state()))
+ redraw_by_winnr[win] = function()
+ draw(renderer(get_state()))
+ end
+ end),
+ -- This is probably not needed.
+ -- destroy = vim.schedule_wrap(function()
+ -- assert(has_initiated, "Display has not been initiated, cannot destroy.")
+ -- TODO: what happens with the state container, etc?
+ -- unsubscribe(true)
+ -- redraw_by_winnr[win] = nil
+ -- if win then
+ -- vim.api.nvim_win_close(win, true)
+ -- end
+ -- end),
+ }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/ui/init.lua b/lua/nvim-lsp-installer/ui/init.lua
new file mode 100644
index 00000000..3b8d830a
--- /dev/null
+++ b/lua/nvim-lsp-installer/ui/init.lua
@@ -0,0 +1,70 @@
+local Data = require "nvim-lsp-installer.data"
+local M = {}
+
+M.NodeType = Data.enum {
+ "NODE",
+ "STYLE_BLOCK",
+ "VIRTUAL_TEXT",
+ "HL_TEXT",
+}
+
+function M.Node(children)
+ return {
+ type = M.NodeType.NODE,
+ children = children,
+ }
+end
+
+function M.HlTextNode(lines_with_span_tuples)
+ if type(lines_with_span_tuples[1]) == "string" then
+ -- this enables a convenience API for just rendering a single line (with just a single span)
+ lines_with_span_tuples = { { lines_with_span_tuples } }
+ end
+ return {
+ type = M.NodeType.HL_TEXT,
+ lines = lines_with_span_tuples,
+ }
+end
+
+function M.Text(lines)
+ return M.HlTextNode(Data.list_map(function(line)
+ return { { line, "Normal" } }
+ end, lines))
+end
+
+M.CascadingStyle = Data.enum {
+ "INDENT",
+ "CENTERED",
+}
+
+function M.CascadingStyleNode(styles, children)
+ return {
+ type = M.NodeType.STYLE_BLOCK,
+ styles = styles,
+ children = children,
+ }
+end
+
+function M.VirtualTextNode(virt_text)
+ return {
+ type = M.NodeType.VIRTUAL_TEXT,
+ virt_text = virt_text,
+ }
+end
+
+function M.When(condition, a)
+ if condition then
+ if type(a) == "function" then
+ return a()
+ else
+ return a
+ end
+ end
+ return M.Node {}
+end
+
+function M.EmptyLine()
+ return M.Text { "" }
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/ui/state.lua b/lua/nvim-lsp-installer/ui/state.lua
new file mode 100644
index 00000000..ff54c657
--- /dev/null
+++ b/lua/nvim-lsp-installer/ui/state.lua
@@ -0,0 +1,22 @@
+local M = {}
+
+function M.create_state_container(initial_state, subscriber)
+ -- we do deepcopy to make sure instances of state containers doesn't mutate the initial state
+ local state = vim.deepcopy(initial_state)
+ local has_unsubscribed = false
+
+ return function(mutate_fn)
+ mutate_fn(state)
+ if not has_unsubscribed then
+ subscriber(state)
+ end
+ end,
+ function()
+ return state
+ end,
+ function(val)
+ has_unsubscribed = val
+ end
+end
+
+return M
diff --git a/lua/nvim-lsp-installer/ui/status-win/init.lua b/lua/nvim-lsp-installer/ui/status-win/init.lua
new file mode 100644
index 00000000..aef7060f
--- /dev/null
+++ b/lua/nvim-lsp-installer/ui/status-win/init.lua
@@ -0,0 +1,334 @@
+local Ui = require "nvim-lsp-installer.ui"
+local log = require "nvim-lsp-installer.log"
+local Data = require "nvim-lsp-installer.data"
+local display = require "nvim-lsp-installer.ui.display"
+
+local function ServerGroupHeading(props)
+ return Ui.HlTextNode {
+ { { props.title, props.highlight or "LspInstallerHeading" }, { (" (%d)"):format(props.count), "Comment" } },
+ }
+end
+
+local function Indent(children)
+ return Ui.CascadingStyleNode({ Ui.CascadingStyle.INDENT }, children)
+end
+
+local function Header()
+ return Ui.CascadingStyleNode({ Ui.CascadingStyle.CENTERED }, {
+ Ui.HlTextNode {
+ { { "nvim-lsp-installer", "LspInstallerHeader" } },
+ { { "https://github.com/williamboman/nvim-lsp-installer", "LspInstallerLink" } },
+ },
+ })
+end
+
+-- TODO make configurable
+local LIST_ICON = "◍"
+
+local function InstalledServers(servers)
+ return Ui.Node(Data.list_map(function(server)
+ return Ui.Node {
+ Ui.HlTextNode {
+ {
+ { LIST_ICON, "LspInstallerGreen" },
+ { " " .. server.name, "Normal" },
+ { (server.installer.has_run and " (new)" or ""), "Comment" },
+ },
+ },
+ }
+ end, servers))
+end
+
+local function TailedOutput(server)
+ return Ui.HlTextNode(Data.list_map(function(line)
+ return { { line, "LspInstallerGray" } }
+ end, server.installer.tailed_output))
+end
+
+local function get_last_non_empty_line(output)
+ for i = #output, 1, -1 do
+ local line = output[i]
+ if #line > 0 then
+ return line
+ end
+ end
+ return ""
+end
+
+local function PendingServers(servers)
+ return Ui.Node(Data.list_map(function(server)
+ local has_failed = server.installer.has_run or server.uninstaller.has_run
+ local note = has_failed and "(failed)" or (server.installer.is_queued and "(queued)" or "(running)")
+ return Ui.Node {
+ Ui.HlTextNode {
+ {
+ { LIST_ICON, has_failed and "LspInstallerError" or "LspInstallerOrange" },
+ { " " .. server.name, server.installer.is_running and "Normal" or "LspInstallerGray" },
+ { " " .. note, "Comment" },
+ { has_failed and "" or (" " .. get_last_non_empty_line(server.installer.tailed_output)), "Comment" },
+ },
+ },
+ Ui.When(has_failed, function()
+ return Indent { Indent { TailedOutput(server) } }
+ end),
+ Ui.When(
+ server.uninstaller.error,
+ Indent {
+ Ui.HlTextNode { server.uninstaller.error, "Comment" },
+ }
+ ),
+ }
+ end, servers))
+end
+
+local function UninstalledServers(servers)
+ return Ui.Node(Data.list_map(function(server)
+ return Ui.Node {
+ Ui.HlTextNode {
+ {
+ { LIST_ICON, "LspInstallerGray" },
+ { " " .. server.name, "Comment" },
+ { server.uninstaller.has_run and " (just uninstalled)" or "", "Comment" },
+ },
+ },
+ }
+ end, servers))
+end
+
+local function ServerGroup(props)
+ local total_server_count = 0
+ local chunks = props.servers
+ for i = 1, #chunks do
+ local servers = chunks[i]
+ total_server_count = total_server_count + #servers
+ end
+
+ return Ui.When(total_server_count > 0 or not props.hide_when_empty, function()
+ return Ui.Node {
+ Ui.EmptyLine(),
+ ServerGroupHeading {
+ title = props.title,
+ count = total_server_count,
+ },
+ Indent(Data.list_map(function(servers)
+ return props.renderer(servers)
+ end, props.servers)),
+ }
+ end)
+end
+
+local function Servers(servers)
+ local grouped_servers = {
+ installed = {},
+ queued = {},
+ session_installed = {},
+ uninstall_failed = {},
+ installing = {},
+ install_failed = {},
+ uninstalled = {},
+ session_uninstalled = {},
+ }
+
+ -- giggity
+ for _, server in pairs(servers) do
+ if server.installer.is_running then
+ grouped_servers.installing[#grouped_servers.installing + 1] = server
+ elseif server.installer.is_queued then
+ grouped_servers.queued[#grouped_servers.queued + 1] = server
+ elseif server.uninstaller.has_run then
+ if server.uninstaller.error then
+ grouped_servers.uninstall_failed[#grouped_servers.uninstall_failed + 1] = server
+ else
+ grouped_servers.session_uninstalled[#grouped_servers.session_uninstalled + 1] = server
+ end
+ elseif server.is_installed then
+ if server.installer.has_run then
+ grouped_servers.session_installed[#grouped_servers.session_installed + 1] = server
+ else
+ grouped_servers.installed[#grouped_servers.installed + 1] = server
+ end
+ elseif server.installer.has_run then
+ grouped_servers.install_failed[#grouped_servers.install_failed + 1] = server
+ else
+ grouped_servers.uninstalled[#grouped_servers.uninstalled + 1] = server
+ end
+ end
+
+ return Ui.Node {
+ ServerGroup {
+ title = "Installed servers",
+ renderer = InstalledServers,
+ servers = { grouped_servers.session_installed, grouped_servers.installed },
+ },
+ ServerGroup {
+ title = "Pending servers",
+ hide_when_empty = true,
+ renderer = PendingServers,
+ servers = {
+ grouped_servers.installing,
+ grouped_servers.queued,
+ grouped_servers.install_failed,
+ grouped_servers.uninstall_failed,
+ },
+ },
+ ServerGroup {
+ title = "Available servers",
+ renderer = UninstalledServers,
+ servers = { grouped_servers.session_uninstalled, grouped_servers.uninstalled },
+ },
+ }
+end
+
+local function create_server_state(server)
+ return {
+ name = server.name,
+ is_installed = server:is_installed(),
+ installer = {
+ is_queued = false,
+ is_running = false,
+ has_run = false,
+ tailed_output = {},
+ },
+ uninstaller = { has_run = false, error = nil },
+ }
+end
+
+local function init(all_servers)
+ local window = display.new_view_only_win "LSP servers"
+
+ window.view(function(state)
+ return Ui.Node {
+ Header(),
+ Servers(state.servers),
+ }
+ end)
+
+ local servers = {}
+ for i = 1, #all_servers do
+ local server = all_servers[i]
+ servers[server.name] = create_server_state(server)
+ end
+
+ local mutate_state, get_state = window.init {
+ servers = servers,
+ }
+
+ local function open()
+ window.open {
+ win_width = 95,
+ highlight_groups = {
+ "hi def LspInstallerHeader gui=bold guifg=#ebcb8b",
+ "hi def link LspInstallerLink Comment",
+ "hi def LspInstallerHeading gui=bold",
+ "hi def LspInstallerGreen guifg=#a3be8c",
+ "hi def LspInstallerOrange ctermfg=222 guifg=#ebcb8b",
+ "hi def LspInstallerGray guifg=#888888 ctermfg=144",
+ "hi def LspInstallerError ctermfg=203 guifg=#f44747",
+ },
+ }
+ end
+
+ local function start_install(server, on_complete)
+ mutate_state(function(state)
+ state.servers[server.name].installer.is_queued = false
+ state.servers[server.name].installer.is_running = true
+ end)
+
+ server:install_attached({
+ stdio_sink = {
+ stdout = function(line)
+ mutate_state(function(state)
+ local tailed_output = state.servers[server.name].installer.tailed_output
+ tailed_output[#tailed_output + 1] = line
+ end)
+ end,
+ stderr = function(line)
+ mutate_state(function(state)
+ local tailed_output = state.servers[server.name].installer.tailed_output
+ tailed_output[#tailed_output + 1] = line
+ end)
+ end,
+ },
+ }, function(success)
+ mutate_state(function(state)
+ if success then
+ -- release stdout/err output table.. hopefully ¯\_(ツ)_/¯
+ state.servers[server.name].installer.tailed_output = {}
+ end
+ state.servers[server.name].is_installed = success
+ state.servers[server.name].installer.is_running = false
+ state.servers[server.name].installer.has_run = true
+ end)
+ on_complete()
+ end)
+ end
+
+ -- We have a queue because installers have a tendency to hog resources.
+ local queue = (function()
+ local max_running = 2
+ 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)
+
+ return function(server)
+ q[#q + 1] = server
+ check_queue()
+ end
+ end)()
+
+ return {
+ open = open,
+ install_server = function(server)
+ log.debug { "installing server", server }
+ local server_state = get_state().servers[server.name]
+ if server_state and (server_state.installer.is_running or server_state.installer.is_queued) then
+ log.debug { "Installer is already queued/running", server.name }
+ return
+ end
+ mutate_state(function(state)
+ -- reset state
+ state.servers[server.name] = create_server_state(server)
+ state.servers[server.name].installer.is_queued = true
+ end)
+ queue(server)
+ end,
+ uninstall_server = function(server)
+ local server_state = get_state().servers[server.name]
+ if server_state and (server_state.installer.is_running or server_state.installer.is_queued) then
+ log.debug { "Installer is already queued/running", server.name }
+ return
+ end
+
+ local is_uninstalled, err = pcall(server.uninstall, server)
+ mutate_state(function(state)
+ state.servers[server.name] = create_server_state(server)
+ if is_uninstalled then
+ state.servers[server.name].is_installed = false
+ end
+ state.servers[server.name].uninstaller.has_run = true
+ state.servers[server.name].uninstaller.error = err
+ end)
+ end,
+ }
+end
+
+local win
+return function()
+ if win then
+ return win
+ end
+ local servers = require "nvim-lsp-installer.servers"
+ win = init(servers.get_available_servers())
+ return win
+end