diff --git a/lua/lspcontainers/backends/docker.lua b/lua/lspcontainers/backends/docker.lua new file mode 100644 index 0000000..d4423aa --- /dev/null +++ b/lua/lspcontainers/backends/docker.lua @@ -0,0 +1,173 @@ +local util = require('lspcontainers.util') + +local M = {} + +M.supported_languages = { + bashls = { image = "docker.io/lspcontainers/bash-language-server" }, + clangd = { image = "docker.io/lspcontainers/clangd-language-server" }, + dockerls = { image = "docker.io/lspcontainers/docker-language-server" }, + gopls = { + cmd_builder = function (runtime, workdir, image, network) + local volume = workdir..":"..workdir..":z" + local env = vim.api.nvim_eval('environ()') + local gopath = env.GOPATH or env.HOME.."/go" + local gopath_volume = gopath..":"..gopath..":z" + + local group_handle = io.popen("id -g") + local user_handle = io.popen("id -u") + + local group_id = string.gsub(group_handle:read("*a"), "%s+", "") + local user_id = string.gsub(user_handle:read("*a"), "%s+", "") + + group_handle:close() + user_handle:close() + + local user = user_id..":"..group_id + + if runtime == "docker" then + network = "bridge" + elseif runtime == "podman" then + network = "slirp4netns" + end + + return { + runtime, + "container", + "run", + "--env", + "GOPATH="..gopath, + "--interactive", + "--network="..network, + "--rm", + "--workdir="..workdir, + "--volume="..volume, + "--volume="..gopath_volume, + "--user="..user, + image + } + end, + image = "docker.io/lspcontainers/gopls", + }, + graphql = { image = "docker.io/lspcontainers/graphql-language-service-cli" }, + html = { image = "docker.io/lspcontainers/html-language-server" }, + intelephense = { image = "docker.io/lspcontainers/intelephense" }, + jsonls = { image = "docker.io/lspcontainers/json-language-server" }, + omnisharp = { image = "docker.io/lspcontainers/omnisharp" }, + powershell_es = { image = "docker.io/lspcontainers/powershell-language-server" }, + prismals = { image = "docker.io/lspcontainers/prisma-language-server" }, + pylsp = { image = "docker.io/lspcontainers/python-lsp-server" }, + pyright = { image = "docker.io/lspcontainers/pyright-langserver" }, + rust_analyzer = { image = "docker.io/lspcontainers/rust-analyzer" }, + solargraph = { image = "docker.io/lspcontainers/solargraph" }, + sumneko_lua = { image = "docker.io/lspcontainers/lua-language-server" }, + svelte = { image = "docker.io/lspcontainers/svelte-language-server" }, + tailwindcss= { image = "docker.io/lspcontainers/tailwindcss-language-server" }, + terraformls = { image = "docker.io/lspcontainers/terraform-ls" }, + tsserver = { image = "docker.io/lspcontainers/typescript-language-server" }, + vuels = { image = "docker.io/lspcontainers/vue-language-server" }, + yamlls = { image = "docker.io/lspcontainers/yaml-language-server" }, +} + + +-- default command to run the lsp container +local function default_cmd(runtime, workdir, image, network, docker_volume) + if vim.loop.os_uname().sysname == "Windows_NT" then + workdir = util.Dos2UnixSafePath(workdir) + end + + local mnt_volume + if docker_volume ~= nil then + mnt_volume ="--volume="..docker_volume..":"..workdir..":z" + else + mnt_volume = "--volume="..workdir..":"..workdir..":z" + end + + return { + runtime, + "container", + "run", + "--interactive", + "--rm", + "--network="..network, + "--workdir="..workdir, + mnt_volume, + image + } +end + +function M.command(server, user_opts) + -- Start out with the default values: + local opts = { + container_runtime = "docker", + root_dir = vim.fn.getcwd(), + cmd_builder = default_cmd, + network = "none", + docker_volume = nil, + } + + -- If the LSP is known, it override the defaults: + if M.supported_languages[server] ~= nil then + opts = vim.tbl_extend("force", opts, M.supported_languages[server]) + end + + -- If any opts were passed, those override the defaults: + if user_opts ~= nil then + opts = vim.tbl_extend("force", opts, user_opts) + end + + if not opts.image then + error(string.format("lspcontainers: no image specified for `%s`", server)) + return 1 + end + + return opts.cmd_builder(opts.container_runtime, opts.root_dir, opts.image, opts.network, opts.docker_volume) +end + +function M.images_pull(runtime) + local jobs = {} + runtime = runtime or "docker" + + for idx, server_name in ipairs(Config.ensure_installed) do + local server = M.supported_languages[server_name] + + local job_id = + vim.fn.jobstart( + runtime.." image pull "..server['image'], + { + on_stderr = util.on_event, + on_stdout = util.on_event, + on_exit = util.on_event, + } + ) + + table.insert(jobs, idx, job_id) + end + + local _ = vim.fn.jobwait(jobs) + + print("lspcontainers: Language servers successfully pulled") +end + +function M.images_remove(runtime) + local jobs = {} + runtime = runtime or "docker" + + for _, v in pairs(M.supported_languages) do + local job = vim.fn.jobstart( + runtime.." image rm --force "..v['image']..":latest", + { + on_stderr = util.on_event, + on_stdout = util.on_event, + on_exit = util.on_event, + } + ) + + table.insert(jobs, job) + end + + local _ = vim.fn.jobwait(jobs) + + print("lspcontainers: All language servers removed") +end + +return M diff --git a/lua/lspcontainers/backends/nix.lua b/lua/lspcontainers/backends/nix.lua new file mode 100644 index 0000000..fe79ac2 --- /dev/null +++ b/lua/lspcontainers/backends/nix.lua @@ -0,0 +1,139 @@ +local util = require('lspcontainers.util') +local config = require('lspcontainers.config') +local M = {} + + +local function get_store_path(package, channel, runtime) + runtime = runtime or config.nix.runtime + channel = channel or config.nix.channel + + local storepath = "" + vim.fn.jobwait{vim.fn.jobstart( + string.format("%s eval --raw %s#%s", runtime, channel, package), + { + on_stdout = function(_, data, _) + if data then + for _, v in pairs(data) do + if string.sub(v, 1, 4) == "/nix" then + storepath = v + end + end + end + end, + } + )} + return storepath +end + +M.supported_languages = { + bashls = { + package = "nodePackages.bash-language-server", + binary = "bash-language-server", + arguments = { "start" }, + }, + clangd = { package = "clang-tools", }, + dockerls = { package = "docker-ls", }, + -- TODO: extend this list +} + +function M.command(server, user_opts) + -- Start out with the default values: + local opts = config.nix + + -- If the LSP is known, it override the defaults: + if M.supported_languages[server] ~= nil then + opts = vim.tbl_extend("force", opts, M.supported_languages[server]) + end + + -- If any opts were passed, those override the defaults: + if user_opts ~= nil then + opts = vim.tbl_extend("force", opts, user_opts) + end + + if not opts.package then + error(string.format("lspcontainers: no package specified for `%s`", server)) + return 1 + end + + if opts.binary == nil then + opts.binary = opts.package + end + + local ret = { + opts.runtime, + "shell", + string.format("%s#%s", opts.channel, opts.package), + "-c", + opts.binary, + unpack(opts.arguments) + } + + if opts.extraOptions ~= nil then + assert( + type(opts.extraOptions) == "table", + "configuration value nix.extraOptions has invalid type: \""..type(opts.extraOptions).."\", expected table of strings") + + for i, v in ipairs(opts.extraOptions) do + assert( + type(v) == "string", + "invalid value for element "..i.." in nix.extraOptions: "..type(v)..", expected string") + table.insert(ret, 3 + i, v) + end + end + + return ret +end + + +function M.images_pull(Config, channel, runtime) + runtime = runtime or config.nix.runtime + channel = channel or config.nix.channel + + local jobs = {} + for idx, server_name in ipairs(Config.ensure_installed) do + local server = M.supported_languages[server_name] + local channel_ = server.channel or channel + + local job_id = vim.fn.jobstart( + string.format("%s build --no-link %s#%s", runtime, channel_, server.package), + { + on_stderr = util.on_event, + on_stdout = util.on_event, + on_exit = util.on_event, + } + ) + + table.insert(jobs, idx, job_id) + end + local _ = vim.fn.jobwait(jobs) + + print("lspcontainers: Language servers successfully pulled") +end + + +-- lazily remove paths from the nix store -> only deletes paths if not referenced by other derivations +function M.images_remove(channel, runtime) + local jobs = {} + channel = channel or config.nix.channel + runtime = runtime or config.nix.runtime + + for _, v in pairs(M.supported_languages) do + local storepath = get_store_path(v.package, channel, runtime) + local job = vim.fn.jobstart( + string.format("%s store delete %s", runtime, storepath), + { + on_stderr = util.on_event, + on_stdout = util.on_event, + on_exit = util.on_event, + } + ) + + table.insert(jobs, job) + end + + local _ = vim.fn.jobwait(jobs) + + print("lspcontainers: All language servers removed") +end + +return M diff --git a/lua/lspcontainers/config.lua b/lua/lspcontainers/config.lua new file mode 100644 index 0000000..c909059 --- /dev/null +++ b/lua/lspcontainers/config.lua @@ -0,0 +1,26 @@ +local defaults = { + backend = "docker", + ensure_installed = {}, + nix = { + runtime = "nix", + channel = "github:nixos/nixpkgs/nixpkgs-unstable", + extraOptions = {}, + }, + docker = {}, +} + +local M = vim.deepcopy(defaults) + +M.setup = function(config) + -- reset M to default when calling setup + if not vim.deep_equal(M, defaults) then + for k, v in pairs(defaults) do M[k] = v end + end + + -- apply user supplied config to M + for k, v in pairs(vim.tbl_deep_extend("force", M, config)) do M[k] = v end + + return M +end + +return M diff --git a/lua/lspcontainers/init.lua b/lua/lspcontainers/init.lua index 0fae8e5..e5737d4 100644 --- a/lua/lspcontainers/init.lua +++ b/lua/lspcontainers/init.lua @@ -1,206 +1,32 @@ -Config = { - ensure_installed = {} -} - -local supported_languages = { - bashls = { image = "docker.io/lspcontainers/bash-language-server" }, - clangd = { image = "docker.io/lspcontainers/clangd-language-server" }, - dockerls = { image = "docker.io/lspcontainers/docker-language-server" }, - gopls = { - cmd_builder = function (runtime, workdir, image, network) - local volume = workdir..":"..workdir..":z" - local env = vim.api.nvim_eval('environ()') - local gopath = env.GOPATH or env.HOME.."/go" - local gopath_volume = gopath..":"..gopath..":z" - - local group_handle = io.popen("id -g") - local user_handle = io.popen("id -u") - - local group_id = string.gsub(group_handle:read("*a"), "%s+", "") - local user_id = string.gsub(user_handle:read("*a"), "%s+", "") - - group_handle:close() - user_handle:close() +local backends = {} +backends["docker"] = require('lspcontainers.backends.docker') +backends["nix"] = require("lspcontainers.backends.nix") - local user = user_id..":"..group_id +local defaultbackend = backends["docker"] - if runtime == "docker" then - network = "bridge" - elseif runtime == "podman" then - network = "slirp4netns" - end - - return { - runtime, - "container", - "run", - "--env", - "GOPATH="..gopath, - "--interactive", - "--network="..network, - "--rm", - "--workdir="..workdir, - "--volume="..volume, - "--volume="..gopath_volume, - "--user="..user, - image - } - end, - image = "docker.io/lspcontainers/gopls", - }, - graphql = { image = "docker.io/lspcontainers/graphql-language-service-cli" }, - html = { image = "docker.io/lspcontainers/html-language-server" }, - intelephense = { image = "docker.io/lspcontainers/intelephense" }, - jsonls = { image = "docker.io/lspcontainers/json-language-server" }, - omnisharp = { image = "docker.io/lspcontainers/omnisharp" }, - powershell_es = { image = "docker.io/lspcontainers/powershell-language-server" }, - prismals = { image = "docker.io/lspcontainers/prisma-language-server" }, - pylsp = { image = "docker.io/lspcontainers/python-lsp-server" }, - pyright = { image = "docker.io/lspcontainers/pyright-langserver" }, - rust_analyzer = { image = "docker.io/lspcontainers/rust-analyzer" }, - solargraph = { image = "docker.io/lspcontainers/solargraph" }, - sumneko_lua = { image = "docker.io/lspcontainers/lua-language-server" }, - svelte = { image = "docker.io/lspcontainers/svelte-language-server" }, - tailwindcss= { image = "docker.io/lspcontainers/tailwindcss-language-server" }, - terraformls = { image = "docker.io/lspcontainers/terraform-ls" }, - tsserver = { image = "docker.io/lspcontainers/typescript-language-server" }, - vuels = { image = "docker.io/lspcontainers/vue-language-server" }, - yamlls = { image = "docker.io/lspcontainers/yaml-language-server" }, +local M = { + command = defaultbackend.command, + images_pull = defaultbackend.images_pull, + images_remove = defaultbackend.images_remove, + supported_languages = defaultbackend.supported_languages, } --- default command to run the lsp container -local default_cmd = function (runtime, workdir, image, network, docker_volume) - if vim.loop.os_uname().sysname == "Windows_NT" then - workdir = Dos2UnixSafePath(workdir) - end - - local mnt_volume - if docker_volume ~= nil then - mnt_volume ="--volume="..docker_volume..":"..workdir..":z" - else - mnt_volume = "--volume="..workdir..":"..workdir..":z" - end - - return { - runtime, - "container", - "run", - "--interactive", - "--rm", - "--network="..network, - "--workdir="..workdir, - mnt_volume, - image - } -end - -local function command(server, user_opts) - -- Start out with the default values: - local opts = { - container_runtime = "docker", - root_dir = vim.fn.getcwd(), - cmd_builder = default_cmd, - network = "none", - docker_volume = nil, - } - - -- If the LSP is known, it override the defaults: - if supported_languages[server] ~= nil then - opts = vim.tbl_extend("force", opts, supported_languages[server]) - end - - -- If any opts were passed, those override the defaults: - if user_opts ~= nil then - opts = vim.tbl_extend("force", opts, user_opts) - end - - if not opts.image then - error(string.format("lspcontainers: no image specified for `%s`", server)) - return 1 - end - - return opts.cmd_builder(opts.container_runtime, opts.root_dir, opts.image, opts.network, opts.docker_volume) -end - -Dos2UnixSafePath = function(workdir) - workdir = string.gsub(workdir, ":", "") - workdir = string.gsub(workdir, "\\", "/") - workdir = "/" .. workdir - return workdir -end - -local function on_event(_, data, event) - --if event == "stdout" or event == "stderr" then - if event == "stdout" then - if data then - for _, v in pairs(data) do - print(v) - end - end - end -end - -local function images_pull(runtime) - local jobs = {} - runtime = runtime or "docker" - - for idx, server_name in ipairs(Config.ensure_installed) do - local server = supported_languages[server_name] - - local job_id = - vim.fn.jobstart( - runtime.." image pull "..server['image'], - { - on_stderr = on_event, - on_stdout = on_event, - on_exit = on_event, - } - ) - - table.insert(jobs, idx, job_id) - end - - local _ = vim.fn.jobwait(jobs) - - print("lspcontainers: Language servers successfully pulled") -end - -local function images_remove(runtime) - local jobs = {} - runtime = runtime or "docker" - - for _, v in pairs(supported_languages) do - local job = - vim.fn.jobstart( - runtime.." image rm --force "..v['image']..":latest", - { - on_stderr = on_event, - on_stdout = on_event, - on_exit = on_event, - } - ) +local function setup(options) + options = options or {} + local config = require('lspcontainers.config').setup(options) - table.insert(jobs, job) - end + local backend = backends[config.backend] + assert(backend ~= nil, "configuration value backend has invalid value: \""..config.backend.."\"") - local _ = vim.fn.jobwait(jobs) + M.command = backend.command + M.images_pull = backend.images_pull + M.images_remove = backend.images_remove + M.supported_languages = backend.supported_languages - print("lspcontainers: All language servers removed") + vim.api.nvim_create_user_command("LspImagesPull", function() backend.images_pull(config) end, {}) + vim.api.nvim_create_user_command("LspImagesRemove", backend.images_remove, {}) end -vim.api.nvim_create_user_command("LspImagesPull", images_pull, {}) -vim.api.nvim_create_user_command("LspImagesRemove", images_remove, {}) +M.setup = setup -local function setup(options) - if options['ensure_installed'] then - Config.ensure_installed = options['ensure_installed'] - end -end - -return { - command = command, - images_pull = images_pull, - images_remove = images_remove, - setup = setup, - supported_languages = supported_languages -} +return M diff --git a/lua/lspcontainers/util.lua b/lua/lspcontainers/util.lua new file mode 100644 index 0000000..5e3cd99 --- /dev/null +++ b/lua/lspcontainers/util.lua @@ -0,0 +1,21 @@ +local M = {} + +function M.on_event(_, data, event) + --if event == "stdout" or event == "stderr" then + if event == "stdout" then + if data then + for _, v in pairs(data) do + print(v) + end + end + end +end + +function M.Dos2UnixSafePath(workdir) + workdir = string.gsub(workdir, ":", "") + workdir = string.gsub(workdir, "\\", "/") + workdir = "/" .. workdir + return workdir +end + +return M