diff options
| author | William Boman <william@redwill.se> | 2021-09-07 02:44:09 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-09-07 02:44:09 +0200 |
| commit | 00294b84031711013a385f18c0fb0e8db84ebaf9 (patch) | |
| tree | e45de668229c6b41643c5d1fa0fdb5beb0ff60fa /lua | |
| parent | lazily require servers for faster startup times (#77) (diff) | |
| download | mason-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.lua | 138 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/data.lua | 36 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/dispatcher.lua | 4 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/installers/go.lua | 28 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/installers/init.lua | 41 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/installers/npm.lua | 10 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/installers/pip3.lua | 21 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/installers/shell.lua | 38 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/installers/zx.lua | 52 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/log.lua | 18 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/process.lua | 135 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/server.lua | 38 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/servers/init.lua | 105 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/servers/tflint/init.lua | 23 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/display.lua | 239 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/init.lua | 70 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/state.lua | 22 | ||||
| -rw-r--r-- | lua/nvim-lsp-installer/ui/status-win/init.lua | 334 |
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 |
