aboutsummaryrefslogtreecommitdiffstats
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
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
-rw-r--r--CUSTOM_SERVERS.md25
-rw-r--r--README.md16
-rw-r--r--doc/nvim-lsp-installer.txt121
-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
-rw-r--r--plugin/nvim-lsp-installer.vim29
22 files changed, 1234 insertions, 309 deletions
diff --git a/CUSTOM_SERVERS.md b/CUSTOM_SERVERS.md
index 4e1614ce..5db90113 100644
--- a/CUSTOM_SERVERS.md
+++ b/CUSTOM_SERVERS.md
@@ -8,8 +8,11 @@ to the [Lua docs](./lua/nvim-lsp-installer/server.lua) for more details.
# Installers
-Each `Server` instance must provide an `installer` property. This _must_ be a function with the signature `function (server, callback)`, where `server` is the server instance that is being installed, and `callback` is a function that
-_must_ be called upon completion (successful or not) by the installer implementation.
+Each `Server` instance must provide an `installer` property. This _must_ be a function with the signature `function (server, callback, context)`, where:
+
+- `server` is the server instance that is being installed,
+- `callback` is a function that _must_ be called upon completion (successful or not) by the installer implementation
+- `context` is a table containing contextual data, such as `stdio_sink` (see existing installer implementations for reference)
## Core installers
@@ -149,11 +152,11 @@ Example:
local installers = require "nvim-lsp-installer.installers"
local shell = require "nvim-lsp-installer.installers.shell"
-installers.compose {
- shell.raw [[ echo "I won't run at all because the previous installer failed." ]],
- shell.raw [[ exit 1 ]],
- pip3.packages { "another-package" },
+installers.join {
npm.packages { "some-package" },
+ pip3.packages { "another-package" },
+ shell.raw [[ exit 1 ]],
+ shell.raw [[ echo "I won't run at all because the previous installer failed." ]],
}
```
@@ -164,7 +167,7 @@ The following is a full example of setting up a completely custom server install
```lua
local lspconfig = require "lspconfig"
local configs = require "lspconfig/configs"
-local lsp_installer = require "nvim-lsp-installer"
+local servers = require "nvim-lsp-installer.servers"
local server = require "nvim-lsp-installer.server"
local path = require "nvim-lsp-installer.path"
@@ -182,12 +185,12 @@ configs[server_name] = {
local root_dir = server.get_server_root_path(server_name)
-- You may also use one of the prebuilt installers (e.g., npm, pip3, go, shell, zx).
-local my_installer = function(server, callback)
+local my_installer = function(server, callback, context)
local is_success = code_that_installs_given_server(server)
if is_success then
- callback(true, nil)
+ callback(true)
else
- callback(false, "Error message here.")
+ callback(false)
end
end
@@ -203,5 +206,5 @@ local my_server = server.Server:new {
-- 3. (optional, recommended) Register your server with nvim-lsp-installer.
-- This makes it available via other APIs (e.g., :LspInstall, lsp_installer.get_available_servers()).
-lsp_installer.register(my_server)
+servers.register(my_server)
```
diff --git a/README.md b/README.md
index 754547f2..89193bc4 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,9 @@
</p>
<p align="center">
- <img src="https://user-images.githubusercontent.com/6705160/130315792-43865221-9574-4f24-90fb-3de745fff1ef.gif" width="650" />
+ <a href="https://asciinema.org/a/434365" target="_blank" rel="noopener">
+ <img src="https://user-images.githubusercontent.com/6705160/132266914-e0f89b07-35e2-45ff-a55e-560f612f8a45.gif" width="650" />
+ </a>
</p>
## About
@@ -13,9 +15,10 @@ LSP servers locally (inside `:echo stdpath("data")`).
On top of just providing commands for installing & uninstalling LSP servers, it:
+- provides a graphical UI
- provides configurations for servers that aren't supported by nvim-lspconfig (`eslint`)
- has support for a variety of different install methods (e.g., [google/zx](https://github.com/google/zx))
-- common install tasks are abstracted behind Lua APIs
+- common install tasks are abstracted behind Lua APIs (has direct integration with libuv via vim.loop)
- <img src="https://user-images.githubusercontent.com/6705160/131256603-cacf7f66-dfa9-4515-8ae4-0e42d08cfc6a.png" height="20"> supports Windows for a majority of server installations
## Installation
@@ -50,8 +53,9 @@ Plug 'williamboman/nvim-lsp-installer'
### Commands
-- `:LspInstall <server>` - installs/reinstalls a language server
-- `:LspUninstall <server>` - uninstalls a language server
+- `:LspInstallInfo` - opens the UI
+- `:LspInstall <server> ...` - installs/reinstalls language servers
+- `:LspUninstall <server> ...` - uninstalls language servers
- `:LspUninstallAll` - uninstalls all language servers
- `:LspPrintInstalled` - prints all installed language servers
@@ -77,9 +81,9 @@ end)
For more advanced use cases you may also interact with more APIs nvim-lsp-installer has to offer, for example the following (refer to `:help nvim-lsp-installer` for more docs):
```lua
-local lsp_installer = require'nvim-lsp-installer'
+local lsp_installer_servers = require'nvim-lsp-installer.servers'
-local ok, rust_analyzer = lsp_installer.get_server("rust_analyzer")
+local ok, rust_analyzer = lsp_installer_servers.get_server("rust_analyzer")
if ok then
if not rust_analyzer:is_installed() then
rust_analyzer:install()
diff --git a/doc/nvim-lsp-installer.txt b/doc/nvim-lsp-installer.txt
index 80249169..22313b22 100644
--- a/doc/nvim-lsp-installer.txt
+++ b/doc/nvim-lsp-installer.txt
@@ -26,11 +26,23 @@ https://github.com/williamboman/nvim-lsp-installer/blob/main/CUSTOM_SERVERS.md.
==============================================================================
QUICK START *nvim-lsp-installer-quickstart*
+To view the UI for nvim-lsp-installer, run: >
+
+ :LspInstallInfo
+
+<
+
Install a language server via `:LspInstall`, for example: >
:LspInstall tsserver
<
+You may also install multiple languages at a time: >
+
+ :LspInstall tsserver graphql eslintls
+
+<
+
Then, somewhere in your initialization script (see `:h init.lua`): >
local lsp_installer = require("nvim-lsp-installer")
@@ -57,15 +69,20 @@ Then, somewhere in your initialization script (see `:h init.lua`): >
==============================================================================
COMMANDS *nvim-lsp-installer-commands*
+ *:LspInstallInfo*
+:LspInstallInfo
+
+Opens the UI for nvim-lsp-installer.
+
*:LspInstall*
-:LspInstall {server_name}
+:LspInstall {server_name} ...
-Installs a language server
+Installs language servers.
*:LspUninstall*
-:LspUninstall {server_name}
+:LspUninstall {server_name} ...
-Uninstalls a language server.
+Uninstalls language servers.
*:LspUninstallAll*
:LspUninstallAll
@@ -104,41 +121,9 @@ Lua: vim.g.lsp_installer_allow_federated_servers
<
==============================================================================
-Lua module: nvim-lsp-installer *lsp_installer*
-
- *lsp_installer.get_available_servers()*
-get_available_servers()
- Return: ~
- |lsp_installer.Server|[] A list containing all available language servers.
-
- *lsp_installer.get_installed_servers()*
-get_installed_servers()
- Return: ~
- |lsp_installer.Server|[] A list of servers that are currently installed.
-
- *lsp_installer.get_uninstalled_servers()*
-get_uninstalled_servers()
- Return: ~
- |lsp_installer.Server|[] A list of servers that are not installed.
+Lua module: nvim-lsp-installer
- *lsp_installer.get_server()*
-get_server({server_name})
- Parameters: ~
- {server_name} (string) The server instance to retrieve.
-
- Return: ~
- ok: boolean, server: |lsp_installer.Server|
-
- Example: ~
->
- local lsp_installer = require'nvim-lsp-installer'
- local ok, rust_server = lsp_installer.get_server("rust_analyzer")
- if ok then
- rust_server:install()
- end
-<
-
- *lsp_installer.install()*
+ *nvim-lsp-installer.install()*
install({server_name})
Installs the provided {server_name}. If {server_name} is already installed, it
is reinstalled.
@@ -146,21 +131,14 @@ install({server_name})
Parameters: ~
{server_name} (string) The server to install.
- *lsp_installer.uninstall()*
+ *nvim-lsp-installer.uninstall()*
uninstall({server_name})
Uninstalls the provided {server_name}.
Parameters: ~
{server_name} (string) The server to uninstall.
-register({server})
- Registers a {server} instance with nvim-lsp-installer.
-
- {server} must be an instance of |lsp_installer.Server|.
-
- Parameters: ~
- {server} (|lsp_installer.Server|) The server to register.
-
+ *nvim-lsp-installer.on_server_ready()*
on_server_ready({cb})
Registers a callback to be executed each time a server is
ready to be initiated.
@@ -178,9 +156,54 @@ on_server_ready({cb})
cb} from any future dispatches.
==============================================================================
-Lua module: nvim-lsp-installer.server *lsp_installer.server*
+Lua module: nvim-lsp-installer.servers *nvim-lsp-installer.servers*
+
+ *nvim-lsp-installer.get_available_servers()*
+get_available_servers()
+ Return: ~
+ |lsp_installer.Server|[] A list containing all available language servers.
+
+ *nvim-lsp-installer.get_installed_servers()*
+get_installed_servers()
+ Return: ~
+ |lsp_installer.Server|[] A list of servers that are currently installed.
+
+ *nvim-lsp-installer.get_uninstalled_servers()*
+get_uninstalled_servers()
+ Return: ~
+ |lsp_installer.Server|[] A list of servers that are not installed.
+
+ *nvim-lsp-installer.register()*
+register({server})
+ Registers a {server} instance with nvim-lsp-installer.
+
+ {server} must be an instance of |lsp_installer.Server|.
+
+ Parameters: ~
+ {server} (|lsp_installer.Server|) The server to register.
+
+ *nvim-lsp-installer.get_server()*
+get_server({server_name})
+ Parameters: ~
+ {server_name} (string) The server instance to retrieve.
+
+ Return: ~
+ ok: boolean, server: |lsp_installer.Server|
+
+ Example: ~
+>
+ local lsp_installer = require'nvim-lsp-installer'
+ local ok, rust_server = lsp_installer.get_server("rust_analyzer")
+ if ok then
+ rust_server:install()
+ end
+<
+
+
+==============================================================================
+Lua module: nvim-lsp-installer.server *nvim-lsp-installer.server*
- *lsp_installer.Server*
+ *nvim-lsp-installer.Server*
class: Server
This class enables installing, uninstalling, and setting up language
servers.
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
diff --git a/plugin/nvim-lsp-installer.vim b/plugin/nvim-lsp-installer.vim
index cd610f3c..1cd772ef 100644
--- a/plugin/nvim-lsp-installer.vim
+++ b/plugin/nvim-lsp-installer.vim
@@ -10,36 +10,45 @@ function! s:MapServerName(servers) abort
endfunction
function! s:LspInstallCompletion(...) abort
- return join(sort(s:MapServerName(luaeval("require'nvim-lsp-installer'.get_available_servers()"))), "\n")
+ return join(sort(s:MapServerName(luaeval("require'nvim-lsp-installer.servers'.get_available_servers()"))), "\n")
endfunction
function! s:LspUninstallCompletion(...) abort
- return join(sort(s:MapServerName(luaeval("require'nvim-lsp-installer'.get_installed_servers()"))), "\n")
+ return join(sort(s:MapServerName(luaeval("require'nvim-lsp-installer.servers'.get_installed_servers()"))), "\n")
endfunction
-function! s:LspInstall(server_name) abort
- call luaeval("require'nvim-lsp-installer'.install(_A)", a:server_name)
+function! s:LspInstall(server_names) abort
+ for server_name in split(a:server_names, " ")
+ call luaeval("require'nvim-lsp-installer'.install(_A)", server_name)
+ endfor
endfunction
-function! s:LspUninstall(server_name) abort
- call luaeval("require'nvim-lsp-installer'.uninstall(_A)", a:server_name)
+function! s:LspUninstall(server_names) abort
+ for server_name in split(a:server_names, " ")
+ call luaeval("require'nvim-lsp-installer'.uninstall(_A)", server_name)
+ endfor
endfunction
function! s:LspUninstallAll() abort
- for server in s:MapServerName(luaeval("require'nvim-lsp-installer'.get_installed_servers()"))
+ for server in s:MapServerName(luaeval("require'nvim-lsp-installer.servers'.get_installed_servers()"))
call s:LspUninstall(server)
endfor
endfunction
function! s:LspPrintInstalled() abort
- echo s:MapServerName(luaeval("require'nvim-lsp-installer'.get_installed_servers()"))
+ echo s:MapServerName(luaeval("require'nvim-lsp-installer.servers'.get_installed_servers()"))
+endfunction
+
+function! s:LspInstallInfo() abort
+ lua require'nvim-lsp-installer'.display()
endfunction
-command! -nargs=1 -complete=custom,s:LspInstallCompletion LspInstall exe s:LspInstall("<args>")
-command! -nargs=1 -complete=custom,s:LspUninstallCompletion LspUninstall exe s:LspUninstall("<args>")
+command! -nargs=+ -complete=custom,s:LspInstallCompletion LspInstall exe s:LspInstall("<args>")
+command! -nargs=+ -complete=custom,s:LspUninstallCompletion LspUninstall exe s:LspUninstall("<args>")
command! LspUninstallAll call s:LspUninstallAll()
command! LspPrintInstalled call s:LspPrintInstalled()
+command! LspInstallInfo call s:LspInstallInfo()
autocmd User LspAttachBuffers lua require"nvim-lsp-installer".lsp_attach_proxy()