diff options
author | kylo252 <[email protected]> | 2021-10-09 22:17:30 +0200 |
---|---|---|
committer | kylo252 <[email protected]> | 2021-10-09 22:17:30 +0200 |
commit | caf62bcfed4fc6cfed26164e39d22a568d21f9d0 (patch) | |
tree | 47f7ddcbe7ef10b6cffd8398dbfc215d94fc2fae /lua/lsp | |
parent | 4126e5765d69840660fab2a05bbc664ad0117b95 (diff) | |
parent | 82b7a35858479223c1e34bea2f64451ecf1e5f66 (diff) |
Merge remote-tracking branch 'origin/rolling'
Diffstat (limited to 'lua/lsp')
-rw-r--r-- | lua/lsp/config.lua | 45 | ||||
-rw-r--r-- | lua/lsp/handlers.lua | 178 | ||||
-rw-r--r-- | lua/lsp/init.lua | 177 | ||||
-rw-r--r-- | lua/lsp/manager.lua | 86 | ||||
-rw-r--r-- | lua/lsp/null-ls/formatters.lua | 52 | ||||
-rw-r--r-- | lua/lsp/null-ls/init.lua | 60 | ||||
-rw-r--r-- | lua/lsp/null-ls/linters.lua | 52 | ||||
-rw-r--r-- | lua/lsp/null-ls/services.lua | 20 | ||||
-rw-r--r-- | lua/lsp/peek.lua | 18 | ||||
-rw-r--r-- | lua/lsp/providers/jsonls.lua | 197 | ||||
-rw-r--r-- | lua/lsp/providers/sumneko_lua.lua | 19 | ||||
-rw-r--r-- | lua/lsp/providers/vuels.lua | 26 | ||||
-rw-r--r-- | lua/lsp/providers/yamlls.lua | 30 | ||||
-rw-r--r-- | lua/lsp/templates.lua | 98 | ||||
-rw-r--r-- | lua/lsp/utils.lua | 58 |
15 files changed, 883 insertions, 233 deletions
diff --git a/lua/lsp/config.lua b/lua/lsp/config.lua new file mode 100644 index 00000000..f13d9659 --- /dev/null +++ b/lua/lsp/config.lua @@ -0,0 +1,45 @@ +return { + templates_dir = join_paths(get_runtime_dir(), "site", "after", "ftplugin"), + diagnostics = { + signs = { + active = true, + values = { + { name = "LspDiagnosticsSignError", text = "ï™™" }, + { name = "LspDiagnosticsSignWarning", text = "" }, + { name = "LspDiagnosticsSignHint", text = "ï µ" }, + { name = "LspDiagnosticsSignInformation", text = "" }, + }, + }, + virtual_text = true, + update_in_insert = false, + underline = true, + severity_sort = true, + }, + override = {}, + document_highlight = true, + code_lens_refresh = true, + popup_border = "single", + on_attach_callback = nil, + on_init_callback = nil, + automatic_servers_installation = true, + buffer_mappings = { + normal_mode = { + ["K"] = { "<cmd>lua vim.lsp.buf.hover()<CR>", "Show hover" }, + ["gd"] = { "<cmd>lua vim.lsp.buf.definition()<CR>", "Goto Definition" }, + ["gD"] = { "<cmd>lua vim.lsp.buf.declaration()<CR>", "Goto declaration" }, + ["gr"] = { "<cmd>lua vim.lsp.buf.references()<CR>", "Goto references" }, + ["gI"] = { "<cmd>lua vim.lsp.buf.implementation()<CR>", "Goto Implementation" }, + ["gs"] = { "<cmd>lua vim.lsp.buf.signature_help()<CR>", "show signature help" }, + ["gp"] = { "<cmd>lua require'lsp.peek'.Peek('definition')<CR>", "Peek definition" }, + ["gl"] = { + "<cmd>lua require'lsp.handlers'.show_line_diagnostics()<CR>", + "Show line diagnostics", + }, + }, + insert_mode = {}, + visual_mode = {}, + }, + null_ls = { + setup = {}, + }, +} diff --git a/lua/lsp/handlers.lua b/lua/lsp/handlers.lua index 2322e76a..ffb7564a 100644 --- a/lua/lsp/handlers.lua +++ b/lua/lsp/handlers.lua @@ -3,52 +3,73 @@ local M = {} function M.setup() - vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(vim.lsp.diagnostic.on_publish_diagnostics, { + local config = { -- your config virtual_text = lvim.lsp.diagnostics.virtual_text, - signs = lvim.lsp.diagnostics.signs.active, - underline = lvim.lsp.document_highlight, - }) - - vim.lsp.handlers["textDocument/publishDiagnostics"] = function(_, _, params, client_id, _) - local config = { -- your config - virtual_text = lvim.lsp.diagnostics.virtual_text, - signs = lvim.lsp.diagnostics.signs, - underline = lvim.lsp.diagnostics.underline, - update_in_insert = lvim.lsp.diagnostics.update_in_insert, - severity_sort = lvim.lsp.diagnostics.severity_sort, - } - local uri = params.uri - local bufnr = vim.uri_to_bufnr(uri) + signs = lvim.lsp.diagnostics.signs, + underline = lvim.lsp.diagnostics.underline, + update_in_insert = lvim.lsp.diagnostics.update_in_insert, + severity_sort = lvim.lsp.diagnostics.severity_sort, + } + if vim.fn.has "nvim-0.5.1" > 0 then + vim.lsp.handlers["textDocument/publishDiagnostics"] = function(_, result, ctx, _) + local uri = result.uri + local bufnr = vim.uri_to_bufnr(uri) + if not bufnr then + return + end - if not bufnr then - return - end + local diagnostics = result.diagnostics + local ok, vim_diag = pcall(require, "vim.diagnostic") + if ok then + -- FIX: why can't we just use vim.diagnostic.get(buf_id)? + config.signs = true + for i, diagnostic in ipairs(diagnostics) do + local rng = diagnostic.range + diagnostics[i].lnum = rng["start"].line + diagnostics[i].end_lnum = rng["end"].line + diagnostics[i].col = rng["start"].character + diagnostics[i].end_col = rng["end"].character + end + local namespace = vim.lsp.diagnostic.get_namespace(ctx.client_id) - local diagnostics = params.diagnostics + vim_diag.set(namespace, bufnr, diagnostics, config) + if not vim.api.nvim_buf_is_loaded(bufnr) then + return + end - for i, v in ipairs(diagnostics) do - local source = v.source - if source then - if string.find(source, "/") then - source = string.sub(v.source, string.find(v.source, "([%w-_]+)$")) + local sign_names = { + "DiagnosticSignError", + "DiagnosticSignWarn", + "DiagnosticSignInfo", + "DiagnosticSignHint", + } + for i, sign in ipairs(lvim.lsp.diagnostics.signs.values) do + vim.fn.sign_define(sign_names[i], { texthl = sign_names[i], text = sign.text, numhl = "" }) end - diagnostics[i].message = string.format("%s: %s", source, v.message) + vim_diag.show(namespace, bufnr, diagnostics, config) else - diagnostics[i].message = string.format("%s", v.message) - end - - if vim.tbl_contains(vim.tbl_keys(v), "code") then - diagnostics[i].message = diagnostics[i].message .. string.format(" [%s]", v.code) + vim.lsp.diagnostic.save(diagnostics, bufnr, ctx.client_id) + if not vim.api.nvim_buf_is_loaded(bufnr) then + return + end + vim.lsp.diagnostic.display(diagnostics, bufnr, ctx.client_id, config) end end + else + vim.lsp.handlers["textDocument/publishDiagnostics"] = function(_, _, params, client_id, _) + local uri = params.uri + local bufnr = vim.uri_to_bufnr(uri) + if not bufnr then + return + end - vim.lsp.diagnostic.save(diagnostics, bufnr, client_id) - - if not vim.api.nvim_buf_is_loaded(bufnr) then - return + local diagnostics = params.diagnostics + vim.lsp.diagnostic.save(diagnostics, bufnr, client_id) + if not vim.api.nvim_buf_is_loaded(bufnr) then + return + end + vim.lsp.diagnostic.display(diagnostics, bufnr, client_id, config) end - - vim.lsp.diagnostic.display(diagnostics, bufnr, client_id, config) end vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(vim.lsp.handlers.hover, { @@ -60,4 +81,89 @@ function M.setup() }) end +local function split_by_chunk(text, chunkSize) + local s = {} + for i = 1, #text, chunkSize do + s[#s + 1] = text:sub(i, i + chunkSize - 1) + end + return s +end + +function M.show_line_diagnostics() + -- TODO: replace all this with vim.diagnostic.show_position_diagnostics() + local diagnostics = vim.lsp.diagnostic.get_line_diagnostics() + local severity_highlight = { + "LspDiagnosticsFloatingError", + "LspDiagnosticsFloatingWarning", + "LspDiagnosticsFloatingInformation", + "LspDiagnosticsFloatingHint", + } + local ok, vim_diag = pcall(require, "vim.diagnostic") + if ok then + local buf_id = vim.api.nvim_win_get_buf(0) + local win_id = vim.api.nvim_get_current_win() + local cursor_position = vim.api.nvim_win_get_cursor(win_id) + severity_highlight = { + "DiagnosticFloatingError", + "DiagnosticFloatingWarn", + "DiagnosticFloatingInfo", + "DiagnosticFloatingHint", + } + diagnostics = vim_diag.get(buf_id, { lnum = cursor_position[1] - 1 }) + end + local lines = {} + local max_width = vim.fn.winwidth(0) - 5 + local height = #diagnostics + local width = 0 + local opts = {} + local close_events = { "CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre" } + if height == 0 then + return + end + local bufnr = vim.api.nvim_create_buf(false, true) + local diag_message + table.sort(diagnostics, function(a, b) + return a.severity < b.severity + end) + for i, diagnostic in ipairs(diagnostics) do + local source = diagnostic.source + diag_message = diagnostic.message:gsub("[\n\r]", " ") + if source then + if string.find(source, "/") then + source = string.sub(diagnostic.source, string.find(diagnostic.source, "([%w-_]+)$")) + end + diag_message = string.format("%d. %s: %s", i, source, diag_message) + else + diag_message = string.format("%d. %s", i, diag_message) + end + if diagnostic.code then + diag_message = string.format("%s [%s]", diag_message, diagnostic.code) + end + local msgs = split_by_chunk(diag_message, max_width) + for _, diag in ipairs(msgs) do + table.insert(lines, { message = diag, severity = diagnostic.severity }) + width = math.max(diag:len(), width) + end + end + height = #lines + opts = vim.lsp.util.make_floating_popup_options(width, height, opts) + opts["style"] = "minimal" + opts["border"] = "rounded" + opts["focusable"] = true + + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + local winnr = vim.api.nvim_open_win(bufnr, false, opts) + vim.api.nvim_win_set_option(winnr, "winblend", 0) + vim.api.nvim_buf_set_var(bufnr, "lsp_floating_window", winnr) + for i, diag in ipairs(lines) do + vim.api.nvim_buf_set_lines(bufnr, i - 1, i - 1, 0, { diag.message }) + vim.api.nvim_buf_add_highlight(bufnr, -1, severity_highlight[diag.severity], i - 1, 0, diag.message:len()) + end + + vim.api.nvim_command( + "autocmd QuitPre <buffer> ++nested ++once lua pcall(vim.api.nvim_win_close, " .. winnr .. ", true)" + ) + vim.lsp.util.close_preview_autocmd(close_events, winnr) +end + return M diff --git a/lua/lsp/init.lua b/lua/lsp/init.lua index 9c948803..88111005 100644 --- a/lua/lsp/init.lua +++ b/lua/lsp/init.lua @@ -1,15 +1,6 @@ local M = {} local Log = require "core.log" - -function M.config() - vim.lsp.protocol.CompletionItemKind = lvim.lsp.completion.item_kind - - for _, sign in ipairs(lvim.lsp.diagnostics.signs.values) do - vim.fn.sign_define(sign.name, { texthl = sign.name, text = sign.text, numhl = sign.name }) - end - - require("lsp.handlers").setup() -end +local utils = require "utils" local function lsp_highlight_document(client) if lvim.lsp.document_highlight == false then @@ -19,9 +10,6 @@ local function lsp_highlight_document(client) if client.resolved_capabilities.document_highlight then vim.api.nvim_exec( [[ - hi LspReferenceRead cterm=bold ctermbg=red guibg=#464646 - hi LspReferenceText cterm=bold ctermbg=red guibg=#464646 - hi LspReferenceWrite cterm=bold ctermbg=red guibg=#464646 augroup lsp_document_highlight autocmd! * <buffer> autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight() @@ -33,26 +21,49 @@ local function lsp_highlight_document(client) end end -local function add_lsp_buffer_keybindings(bufnr) - local status_ok, wk = pcall(require, "which-key") - if not status_ok then +local function lsp_code_lens_refresh(client) + if lvim.lsp.code_lens_refresh == false then return end - local keys = { - ["K"] = { "<cmd>lua vim.lsp.buf.hover()<CR>", "Show hover" }, - ["gd"] = { "<cmd>lua vim.lsp.buf.definition()<CR>", "Goto Definition" }, - ["gD"] = { "<cmd>lua vim.lsp.buf.declaration()<CR>", "Goto declaration" }, - ["gr"] = { "<cmd>lua vim.lsp.buf.references()<CR>", "Goto references" }, - ["gI"] = { "<cmd>lua vim.lsp.buf.implementation()<CR>", "Goto Implementation" }, - ["gs"] = { "<cmd>lua vim.lsp.buf.signature_help()<CR>", "show signature help" }, - ["gp"] = { "<cmd>lua require'lsp.peek'.Peek('definition')<CR>", "Peek definition" }, - ["gl"] = { - "<cmd>lua vim.lsp.diagnostic.show_line_diagnostics({ show_header = false, border = 'single' })<CR>", - "Show line diagnostics", - }, + if client.resolved_capabilities.code_lens then + vim.api.nvim_exec( + [[ + augroup lsp_code_lens_refresh + autocmd! * <buffer> + autocmd InsertLeave <buffer> lua vim.lsp.codelens.refresh() + autocmd InsertLeave <buffer> lua vim.lsp.codelens.display() + augroup END + ]], + false + ) + end +end + +local function add_lsp_buffer_keybindings(bufnr) + local mappings = { + normal_mode = "n", + insert_mode = "i", + visual_mode = "v", } - wk.register(keys, { mode = "n", buffer = bufnr }) + + if lvim.builtin.which_key.active then + -- Remap using which_key + local status_ok, wk = pcall(require, "which-key") + if not status_ok then + return + end + for mode_name, mode_char in pairs(mappings) do + wk.register(lvim.lsp.buffer_mappings[mode_name], { mode = mode_char, buffer = bufnr }) + end + else + -- Remap using nvim api + for mode_name, mode_char in pairs(mappings) do + for key, remap in pairs(lvim.lsp.buffer_mappings[mode_name]) do + vim.api.nvim_buf_set_keymap(bufnr, mode_char, key, remap[1], { noremap = true, silent = true }) + end + end + end end function M.common_capabilities() @@ -65,35 +76,30 @@ function M.common_capabilities() "additionalTextEdits", }, } - return capabilities -end -function M.get_ls_capabilities(client_id) - local client - if not client_id then - local buf_clients = vim.lsp.buf_get_clients() - for _, buf_client in ipairs(buf_clients) do - if buf_client.name ~= "null-ls" then - client_id = buf_client.id - break - end - end - end - if not client_id then - error "Unable to determine client_id" + local status_ok, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp") + if status_ok then + capabilities = cmp_nvim_lsp.update_capabilities(capabilities) end - client = vim.lsp.get_client_by_id(tonumber(client_id)) - - local enabled_caps = {} + return capabilities +end - for k, v in pairs(client.resolved_capabilities) do - if v == true then - table.insert(enabled_caps, k) +local function select_default_formater(client) + local client_formatting = client.resolved_capabilities.document_formatting + or client.resolved_capabilities.document_range_formatting + if client.name == "null-ls" or not client_formatting then + return + end + Log:debug("Checking for formatter overriding for " .. client.name) + local client_filetypes = client.config.filetypes or {} + for _, filetype in ipairs(client_filetypes) do + if lvim.lang[filetype] and #vim.tbl_keys(lvim.lang[filetype].formatters) > 0 then + Log:debug("Formatter overriding detected. Disabling formatting capabilities for " .. client.name) + client.resolved_capabilities.document_formatting = false + client.resolved_capabilities.document_range_formatting = false end end - - return enabled_caps end function M.common_on_init(client, bufnr) @@ -102,55 +108,58 @@ function M.common_on_init(client, bufnr) Log:debug "Called lsp.on_init_callback" return end - - local formatters = lvim.lang[vim.bo.filetype].formatters - if not vim.tbl_isempty(formatters) and formatters[1]["exe"] ~= nil and formatters[1].exe ~= "" then - client.resolved_capabilities.document_formatting = false - Log:debug( - string.format("Overriding language server [%s] with format provider [%s]", client.name, formatters[1].exe) - ) - end + select_default_formater(client) end function M.common_on_attach(client, bufnr) if lvim.lsp.on_attach_callback then lvim.lsp.on_attach_callback(client, bufnr) - Log:debug "Called lsp.on_init_callback" + Log:debug "Called lsp.on_attach_callback" end lsp_highlight_document(client) + lsp_code_lens_refresh(client) add_lsp_buffer_keybindings(bufnr) - require("lsp.null-ls").setup(vim.bo.filetype) end -function M.setup(lang) - local lsp_utils = require "lsp.utils" - local lsp = lvim.lang[lang].lsp - if lsp_utils.is_client_active(lsp.provider) then - return +local function bootstrap_nlsp(opts) + opts = opts or {} + local lsp_settings_status_ok, lsp_settings = pcall(require, "nlspsettings") + if lsp_settings_status_ok then + lsp_settings.setup(opts) end +end - local overrides = lvim.lsp.override - if type(overrides) == "table" then - if vim.tbl_contains(overrides, lang) then - return - end +function M.get_common_opts() + return { + on_attach = M.common_on_attach, + on_init = M.common_on_init, + capabilities = M.common_capabilities(), + } +end + +function M.setup() + Log:debug "Setting up LSP support" + + local lsp_status_ok, _ = pcall(require, "lspconfig") + if not lsp_status_ok then + return end - if lsp.provider ~= nil and lsp.provider ~= "" then - local lspconfig = require "lspconfig" + for _, sign in ipairs(lvim.lsp.diagnostics.signs.values) do + vim.fn.sign_define(sign.name, { texthl = sign.name, text = sign.text, numhl = sign.name }) + end - if not lsp.setup.on_attach then - lsp.setup.on_attach = M.common_on_attach - end - if not lsp.setup.on_init then - lsp.setup.on_init = M.common_on_init - end - if not lsp.setup.capabilities then - lsp.setup.capabilities = M.common_capabilities() - end + require("lsp.handlers").setup() - lspconfig[lsp.provider].setup(lsp.setup) + if not utils.is_directory(lvim.lsp.templates_dir) then + require("lsp.templates").generate_templates() end + + bootstrap_nlsp { config_home = utils.join_paths(get_config_dir(), "lsp-settings") } + + require("lsp.null-ls").setup() + + require("utils").toggle_autoformat() end return M diff --git a/lua/lsp/manager.lua b/lua/lsp/manager.lua new file mode 100644 index 00000000..9cb81910 --- /dev/null +++ b/lua/lsp/manager.lua @@ -0,0 +1,86 @@ +local M = {} + +local Log = require "core.log" +local lsp_utils = require "lsp.utils" + +function M.init_defaults(languages) + for _, entry in ipairs(languages) do + if not lvim.lang[entry] then + lvim.lang[entry] = { + formatters = {}, + linters = {}, + lsp = {}, + } + end + end +end + +local function is_overridden(server) + local overrides = lvim.lsp.override + if type(overrides) == "table" then + if vim.tbl_contains(overrides, server) then + return true + end + end +end + +---Resolve the configuration for a server based on both common and user configuration +---@param name string +---@param user_config table [optional] +---@return table +local function resolve_config(name, user_config) + local config = { + on_attach = require("lsp").common_on_attach, + on_init = require("lsp").common_on_init, + capabilities = require("lsp").common_capabilities(), + } + + local status_ok, custom_config = pcall(require, "lsp/providers/" .. name) + if status_ok then + Log:debug("Using custom configuration for requested server: " .. name) + config = vim.tbl_deep_extend("force", config, custom_config) + end + + if user_config then + config = vim.tbl_deep_extend("force", config, user_config) + end + + return config +end + +---Setup a language server by providing a name +---@param server_name string name of the language server +---@param user_config table [optional] when available it will take predence over any default configurations +function M.setup(server_name, user_config) + vim.validate { name = { server_name, "string" } } + + if lsp_utils.is_client_active(server_name) or is_overridden(server_name) then + return + end + + local config = resolve_config(server_name, user_config) + local server_available, requested_server = require("nvim-lsp-installer.servers").get_server(server_name) + + local function ensure_installed(server) + if server:is_installed() then + return true + end + if not lvim.lsp.automatic_servers_installation then + Log:debug(server.name .. " is not managed by the automatic installer") + return false + end + Log:debug(string.format("Installing [%s]", server.name)) + server:install() + vim.schedule(function() + vim.cmd [[LspStart]] + end) + end + + if server_available and ensure_installed(requested_server) then + requested_server:setup(config) + else + require("lspconfig")[server_name].setup(config) + end +end + +return M diff --git a/lua/lsp/null-ls/formatters.lua b/lua/lsp/null-ls/formatters.lua index 26be00da..4728b908 100644 --- a/lua/lsp/null-ls/formatters.lua +++ b/lua/lsp/null-ls/formatters.lua @@ -1,36 +1,23 @@ local M = {} -local formatters_by_ft = {} local null_ls = require "null-ls" local services = require "lsp.null-ls.services" local Log = require "core.log" -local function list_names(formatters, options) - options = options or {} - local filter = options.filter or "supported" - - return vim.tbl_keys(formatters[filter]) -end - function M.list_supported_names(filetype) - if not formatters_by_ft[filetype] then - return {} - end - return list_names(formatters_by_ft[filetype], { filter = "supported" }) -end - -function M.list_unsupported_names(filetype) - if not formatters_by_ft[filetype] then - return {} - end - return list_names(formatters_by_ft[filetype], { filter = "unsupported" }) + local null_ls_methods = require "null-ls.methods" + local formatter_method = null_ls_methods.internal["FORMATTING"] + local registered_providers = services.list_registered_providers_names(filetype) + return registered_providers[formatter_method] or {} end function M.list_available(filetype) local formatters = {} + local tbl = require "utils.table" for _, provider in pairs(null_ls.builtins.formatting) do - -- TODO: Add support for wildcard filetypes - if vim.tbl_contains(provider.filetypes or {}, filetype) then + if tbl.contains(provider.filetypes or {}, function(ft) + return ft == "*" or ft == filetype + end) then table.insert(formatters, provider.name) end end @@ -42,19 +29,24 @@ function M.list_configured(formatter_configs) local formatters, errors = {}, {} for _, fmt_config in ipairs(formatter_configs) do - local formatter = null_ls.builtins.formatting[fmt_config.exe] + local formatter_name = fmt_config.exe:gsub("-", "_") + local formatter = null_ls.builtins.formatting[formatter_name] if not formatter then - Log:error("Not a valid formatter:", fmt_config.exe) + Log:error("Not a valid formatter: " .. fmt_config.exe) errors[fmt_config.exe] = {} -- Add data here when necessary else local formatter_cmd = services.find_command(formatter._opts.command) if not formatter_cmd then - Log:warn("Not found:", formatter._opts.command) + Log:warn("Not found: " .. formatter._opts.command) errors[fmt_config.exe] = {} -- Add data here when necessary else - Log:debug("Using formatter:", formatter_cmd) - formatters[fmt_config.exe] = formatter.with { command = formatter_cmd, extra_args = fmt_config.args } + Log:debug("Using formatter: " .. formatter_cmd) + formatters[fmt_config.exe] = formatter.with { + command = formatter_cmd, + extra_args = fmt_config.args, + filetypes = fmt_config.filetypes, + } end end end @@ -62,13 +54,13 @@ function M.list_configured(formatter_configs) return { supported = formatters, unsupported = errors } end -function M.setup(filetype, options) - if not lvim.lang[filetype] or (formatters_by_ft[filetype] and not options.force_reload) then +function M.setup(formatter_configs) + if vim.tbl_isempty(formatter_configs) then return end - formatters_by_ft[filetype] = M.list_configured(lvim.lang[filetype].formatters) - null_ls.register { sources = formatters_by_ft[filetype].supported } + local formatters_by_ft = M.list_configured(formatter_configs) + null_ls.register { sources = formatters_by_ft.supported } end return M diff --git a/lua/lsp/null-ls/init.lua b/lua/lsp/null-ls/init.lua index ce4c07d9..0d030c22 100644 --- a/lua/lsp/null-ls/init.lua +++ b/lua/lsp/null-ls/init.lua @@ -1,44 +1,32 @@ local M = {} -function M.list_supported_provider_names(filetype) - local names = {} - - local formatters = require "lsp.null-ls.formatters" - local linters = require "lsp.null-ls.linters" - - vim.list_extend(names, formatters.list_supported_names(filetype)) - vim.list_extend(names, linters.list_supported_names(filetype)) - - return names -end - -function M.list_unsupported_provider_names(filetype) - local names = {} - - local formatters = require "lsp.null-ls.formatters" - local linters = require "lsp.null-ls.linters" - - vim.list_extend(names, formatters.list_unsupported_names(filetype)) - vim.list_extend(names, linters.list_unsupported_names(filetype)) - - return names -end - --- TODO: for linters and formatters with spaces and '-' replace with '_' -function M.setup(filetype, options) - options = options or {} - - local ok, _ = pcall(require, "null-ls") - if not ok then - require("core.log"):error "Missing null-ls dependency" +local Log = require "core.log" +local formatters = require "lsp.null-ls.formatters" +local linters = require "lsp.null-ls.linters" + +function M:setup() + local status_ok, null_ls = pcall(require, "null-ls") + if not status_ok then + Log:error "Missing null-ls dependency" return end - local formatters = require "lsp.null-ls.formatters" - local linters = require "lsp.null-ls.linters" - - formatters.setup(filetype, options) - linters.setup(filetype, options) + null_ls.config() + require("lspconfig")["null-ls"].setup(lvim.lsp.null_ls.setup) + for filetype, config in pairs(lvim.lang) do + if not vim.tbl_isempty(config.formatters) then + vim.tbl_map(function(c) + c.filetypes = { filetype } + end, config.formatters) + formatters.setup(config.formatters) + end + if not vim.tbl_isempty(config.linters) then + vim.tbl_map(function(c) + c.filetypes = { filetype } + end, config.formatters) + linters.setup(config.linters) + end + end end return M diff --git a/lua/lsp/null-ls/linters.lua b/lua/lsp/null-ls/linters.lua index bc191d7e..549c6cdd 100644 --- a/lua/lsp/null-ls/linters.lua +++ b/lua/lsp/null-ls/linters.lua @@ -1,36 +1,23 @@ local M = {} -local linters_by_ft = {} local null_ls = require "null-ls" local services = require "lsp.null-ls.services" local Log = require "core.log" -local function list_names(linters, options) - options = options or {} - local filter = options.filter or "supported" - - return vim.tbl_keys(linters[filter]) -end - function M.list_supported_names(filetype) - if not linters_by_ft[filetype] then - return {} - end - return list_names(linters_by_ft[filetype], { filter = "supported" }) -end - -function M.list_unsupported_names(filetype) - if not linters_by_ft[filetype] then - return {} - end - return list_names(linters_by_ft[filetype], { filter = "unsupported" }) + local null_ls_methods = require "null-ls.methods" + local linter_method = null_ls_methods.internal["DIAGNOSTICS"] + local registered_providers = services.list_registered_providers_names(filetype) + return registered_providers[linter_method] or {} end function M.list_available(filetype) local linters = {} + local tbl = require "utils.table" for _, provider in pairs(null_ls.builtins.diagnostics) do - -- TODO: Add support for wildcard filetypes - if vim.tbl_contains(provider.filetypes or {}, filetype) then + if tbl.contains(provider.filetypes or {}, function(ft) + return ft == "*" or ft == filetype + end) then table.insert(linters, provider.name) end end @@ -42,19 +29,24 @@ function M.list_configured(linter_configs) local linters, errors = {}, {} for _, lnt_config in pairs(linter_configs) do - local linter = null_ls.builtins.diagnostics[lnt_config.exe] + local linter_name = lnt_config.exe:gsub("-", "_") + local linter = null_ls.builtins.diagnostics[linter_name] if not linter then - Log:error("Not a valid linter:", lnt_config.exe) + Log:error("Not a valid linter: " .. lnt_config.exe) errors[lnt_config.exe] = {} -- Add data here when necessary else local linter_cmd = services.find_command(linter._opts.command) if not linter_cmd then - Log:warn("Not found:", linter._opts.command) + Log:warn("Not found: " .. linter._opts.command) errors[lnt_config.exe] = {} -- Add data here when necessary else - Log:debug("Using linter:", linter_cmd) - linters[lnt_config.exe] = linter.with { command = linter_cmd, extra_args = lnt_config.args } + Log:debug("Using linter: " .. linter_cmd) + linters[lnt_config.exe] = linter.with { + command = linter_cmd, + extra_args = lnt_config.args, + filetypes = lnt_config.filetypes, + } end end end @@ -62,13 +54,13 @@ function M.list_configured(linter_configs) return { supported = linters, unsupported = errors } end -function M.setup(filetype, options) - if not lvim.lang[filetype] or (linters_by_ft[filetype] and not options.force_reload) then +function M.setup(linter_configs) + if vim.tbl_isempty(linter_configs) then return end - linters_by_ft[filetype] = M.list_configured(lvim.lang[filetype].linters) - null_ls.register { sources = linters_by_ft[filetype].supported } + local linters = M.list_configured(linter_configs) + null_ls.register { sources = linters.supported } end return M diff --git a/lua/lsp/null-ls/services.lua b/lua/lsp/null-ls/services.lua index a1e3a06c..ef9e7d22 100644 --- a/lua/lsp/null-ls/services.lua +++ b/lua/lsp/null-ls/services.lua @@ -4,8 +4,8 @@ local function find_root_dir() local util = require "lspconfig/util" local lsp_utils = require "lsp.utils" - local status_ok, ts_client = lsp_utils.is_client_active "typescript" - if status_ok then + local ts_client = lsp_utils.is_client_active "typescript" + if ts_client then return ts_client.config.root_dir end local dirname = vim.fn.expand "%:p:h" @@ -28,6 +28,7 @@ local local_providers = { prettier_d_slim = { find = from_node_modules }, eslint_d = { find = from_node_modules }, eslint = { find = from_node_modules }, + stylelint = { find = from_node_modules }, } function M.find_command(command) @@ -44,4 +45,19 @@ function M.find_command(command) return nil end +function M.list_registered_providers_names(filetype) + local u = require "null-ls.utils" + local c = require "null-ls.config" + local registered = {} + for method, source in pairs(c.get()._methods) do + for name, filetypes in pairs(source) do + if u.filetype_matches(filetypes, filetype) then + registered[method] = registered[method] or {} + table.insert(registered[method], name) + end + end + end + return registered +end + return M diff --git a/lua/lsp/peek.lua b/lua/lsp/peek.lua index cc8e57a9..cb00488e 100644 --- a/lua/lsp/peek.lua +++ b/lua/lsp/peek.lua @@ -42,6 +42,7 @@ local function create_floating_file(location, opts) local winnr = vim.api.nvim_open_win(bufnr, false, opts) vim.api.nvim_win_set_option(winnr, "winblend", 0) + vim.api.nvim_win_set_cursor(winnr, { range.start.line + 1, range.start.character }) vim.api.nvim_buf_set_var(bufnr, "lsp_floating_window", winnr) -- Set some autocmds to close the window @@ -53,9 +54,8 @@ local function create_floating_file(location, opts) return bufnr, winnr end -local function preview_location_callback(_, method, result) +local function preview_location_callback(result) if result == nil or vim.tbl_isempty(result) then - print("peek: No location found: " .. method) return nil end @@ -73,6 +73,14 @@ local function preview_location_callback(_, method, result) end end +local function preview_location_callback_old_signature(_, _, result) + return preview_location_callback(result) +end + +local function preview_location_callback_new_signature(_, result) + return preview_location_callback(result) +end + function M.open_file() -- Get the file currently open in the floating window local filepath = vim.fn.expand "%:." @@ -128,7 +136,11 @@ function M.Peek(what) else -- Make a new request and then create the new window in the callback local params = vim.lsp.util.make_position_params() - local success, _ = pcall(vim.lsp.buf_request, 0, "textDocument/" .. what, params, preview_location_callback) + local preview_callback = preview_location_callback_old_signature + if vim.fn.has "nvim-0.5.1" > 0 then + preview_callback = preview_location_callback_new_signature + end + local success, _ = pcall(vim.lsp.buf_request, 0, "textDocument/" .. what, params, preview_callback) if not success then print( 'peek: Error calling LSP method "textDocument/' .. what .. '". The current language lsp might not support it.' diff --git a/lua/lsp/providers/jsonls.lua b/lua/lsp/providers/jsonls.lua new file mode 100644 index 00000000..1fffa686 --- /dev/null +++ b/lua/lsp/providers/jsonls.lua @@ -0,0 +1,197 @@ +local default_schemas = nil +local status_ok, jsonls_settings = pcall(require, "nlspsettings.jsonls") +if status_ok then + default_schemas = jsonls_settings.get_default_schemas() +end + +local schemas = { + { + description = "TypeScript compiler configuration file", + fileMatch = { + "tsconfig.json", + "tsconfig.*.json", + }, + url = "https://json.schemastore.org/tsconfig.json", + }, + { + description = "Lerna config", + fileMatch = { "lerna.json" }, + url = "https://json.schemastore.org/lerna.json", + }, + { + description = "Babel configuration", + fileMatch = { + ".babelrc.json", + ".babelrc", + "babel.config.json", + }, + url = "https://json.schemastore.org/babelrc.json", + }, + { + description = "ESLint config", + fileMatch = { + ".eslintrc.json", + ".eslintrc", + }, + url = "https://json.schemastore.org/eslintrc.json", + }, + { + description = "Bucklescript config", + fileMatch = { "bsconfig.json" }, + url = "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/8.2.0/docs/docson/build-schema.json", + }, + { + description = "Prettier config", + fileMatch = { + ".prettierrc", + ".prettierrc.json", + "prettier.config.json", + }, + url = "https://json.schemastore.org/prettierrc", + }, + { + description = "Vercel Now config", + fileMatch = { "now.json" }, + url = "https://json.schemastore.org/now", + }, + { + description = "Stylelint config", + fileMatch = { + ".stylelintrc", + ".stylelintrc.json", + "stylelint.config.json", + }, + url = "https://json.schemastore.org/stylelintrc", + }, + { + description = "A JSON schema for the ASP.NET LaunchSettings.json files", + fileMatch = { "launchsettings.json" }, + url = "https://json.schemastore.org/launchsettings.json", + }, + { + description = "Schema for CMake Presets", + fileMatch = { + "CMakePresets.json", + "CMakeUserPresets.json", + }, + url = "https://raw.githubusercontent.com/Kitware/CMake/master/Help/manual/presets/schema.json", + }, + { + description = "Configuration file as an alternative for configuring your repository in the settings page.", + fileMatch = { + ".codeclimate.json", + }, + url = "https://json.schemastore.org/codeclimate.json", + }, + { + description = "LLVM compilation database", + fileMatch = { + "compile_commands.json", + }, + url = "https://json.schemastore.org/compile-commands.json", + }, + { + description = "Config file for Command Task Runner", + fileMatch = { + "commands.json", + }, + url = "https://json.schemastore.org/commands.json", + }, + { + description = "AWS CloudFormation provides a common language for you to describe and provision all the infrastructure resources in your cloud environment.", + fileMatch = { + "*.cf.json", + "cloudformation.json", + }, + url = "https://raw.githubusercontent.com/awslabs/goformation/v5.2.9/schema/cloudformation.schema.json", + }, + { + description = "The AWS Serverless Application Model (AWS SAM, previously known as Project Flourish) extends AWS CloudFormation to provide a simplified way of defining the Amazon API Gateway APIs, AWS Lambda functions, and Amazon DynamoDB tables needed by your serverless application.", + fileMatch = { + "serverless.template", + "*.sam.json", + "sam.json", + }, + url = "https://raw.githubusercontent.com/awslabs/goformation/v5.2.9/schema/sam.schema.json", + }, + { + description = "Json schema for properties json file for a GitHub Workflow template", + fileMatch = { + ".github/workflow-templates/**.properties.json", + }, + url = "https://json.schemastore.org/github-workflow-template-properties.json", + }, + { + description = "golangci-lint configuration file", + fileMatch = { + ".golangci.toml", + ".golangci.json", + }, + url = "https://json.schemastore.org/golangci-lint.json", + }, + { + description = "JSON schema for the JSON Feed format", + fileMatch = { + "feed.json", + }, + url = "https://json.schemastore.org/feed.json", + versions = { + ["1"] = "https://json.schemastore.org/feed-1.json", + ["1.1"] = "https://json.schemastore.org/feed.json", + }, + }, + { + description = "Packer template JSON configuration", + fileMatch = { + "packer.json", + }, + url = "https://json.schemastore.org/packer.json", + }, + { + description = "NPM configuration file", + fileMatch = { + "package.json", + }, + url = "https://json.schemastore.org/package.json", + }, + { + description = "JSON schema for Visual Studio component configuration files", + fileMatch = { + "*.vsconfig", + }, + url = "https://json.schemastore.org/vsconfig.json", + }, + { + description = "Resume json", + fileMatch = { "resume.json" }, + url = "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json", + }, +} + +local function extend(tab1, tab2) + for _, value in ipairs(tab2) do + table.insert(tab1, value) + end + return tab1 +end + +local extended_schemas = extend(schemas, default_schemas) + +local opts = { + settings = { + json = { + schemas = extended_schemas, + }, + }, + setup = { + commands = { + Format = { + function() + vim.lsp.buf.range_formatting({}, { 0, 0 }, { vim.fn.line "$", 0 }) + end, + }, + }, + }, +} + +return opts diff --git a/lua/lsp/providers/sumneko_lua.lua b/lua/lsp/providers/sumneko_lua.lua new file mode 100644 index 00000000..4fee1fd1 --- /dev/null +++ b/lua/lsp/providers/sumneko_lua.lua @@ -0,0 +1,19 @@ +local opts = { + settings = { + Lua = { + diagnostics = { + globals = { "vim", "lvim" }, + }, + workspace = { + library = { + [require("utils").join_paths(get_runtime_dir(), "lvim", "lua")] = true, + [vim.fn.expand "$VIMRUNTIME/lua"] = true, + [vim.fn.expand "$VIMRUNTIME/lua/vim/lsp"] = true, + }, + maxPreload = 100000, + preloadFileSize = 10000, + }, + }, + }, +} +return opts diff --git a/lua/lsp/providers/vuels.lua b/lua/lsp/providers/vuels.lua new file mode 100644 index 00000000..3f44275d --- /dev/null +++ b/lua/lsp/providers/vuels.lua @@ -0,0 +1,26 @@ +local opts = { + setup = { + root_dir = function(fname) + local util = require "lspconfig/util" + return util.root_pattern "package.json"(fname) or util.root_pattern "vue.config.js"(fname) or vim.fn.getcwd() + end, + init_options = { + config = { + vetur = { + completion = { + autoImport = true, + tagCasing = "kebab", + useScaffoldSnippets = true, + }, + useWorkspaceDependencies = true, + validation = { + script = true, + style = true, + template = true, + }, + }, + }, + }, + }, +} +return opts diff --git a/lua/lsp/providers/yamlls.lua b/lua/lsp/providers/yamlls.lua new file mode 100644 index 00000000..156a35b0 --- /dev/null +++ b/lua/lsp/providers/yamlls.lua @@ -0,0 +1,30 @@ +local opts = { + settings = { + yaml = { + hover = true, + completion = true, + validate = true, + schemaStore = { + enable = true, + url = "https://www.schemastore.org/api/json/catalog.json", + }, + schemas = { + kubernetes = { + "daemon.{yml,yaml}", + "manager.{yml,yaml}", + "restapi.{yml,yaml}", + "role.{yml,yaml}", + "role_binding.{yml,yaml}", + "*onfigma*.{yml,yaml}", + "*ngres*.{yml,yaml}", + "*ecre*.{yml,yaml}", + "*eployment*.{yml,yaml}", + "*ervic*.{yml,yaml}", + "kubectl-edit*.yaml", + }, + }, + }, + }, +} + +return opts diff --git a/lua/lsp/templates.lua b/lua/lsp/templates.lua new file mode 100644 index 00000000..fbbc37f6 --- /dev/null +++ b/lua/lsp/templates.lua @@ -0,0 +1,98 @@ +local M = {} + +local Log = require "core.log" +local utils = require "utils" +local get_supported_filetypes = require("lsp.utils").get_supported_filetypes + +local ftplugin_dir = lvim.lsp.templates_dir + +local join_paths = _G.join_paths + +function M.remove_template_files() + -- remove any outdated files + for _, file in ipairs(vim.fn.glob(ftplugin_dir .. "/*.lua", 1, 1)) do + vim.fn.delete(file) + end +end + +---Checks if a server is ignored by default because of a conflict +---Only TSServer is enabled by default for the javascript-family +---@param server_name string +function M.is_ignored(server_name, filetypes) + --TODO: this is easy to be made configurable once stable + filetypes = filetypes or get_supported_filetypes(server_name) + + if vim.tbl_contains(filetypes, "javascript") then + if server_name == "tsserver" then + return false + else + return true + end + end + + local blacklist = { + "jedi_language_server", + "pylsp", + "sqlls", + "sqls", + "angularls", + "ansiblels", + } + return vim.tbl_contains(blacklist, server_name) +end + +---Generates an ftplugin file based on the server_name in the selected directory +---@param server_name string name of a valid language server, e.g. pyright, gopls, tsserver, etc. +---@param dir string the full path to the desired directory +function M.generate_ftplugin(server_name, dir) + -- we need to go through lspconfig to get the corresponding filetypes currently + local filetypes = get_supported_filetypes(server_name) or {} + if not filetypes then + return + end + + if M.is_ignored(server_name, filetypes) then + return + end + + -- print("got associated filetypes: " .. vim.inspect(filetypes)) + + for _, filetype in ipairs(filetypes) do + local filename = join_paths(dir, filetype .. ".lua") + local setup_cmd = string.format([[require("lsp.manager").setup(%q)]], server_name) + -- print("using setup_cmd: " .. setup_cmd) + -- overwrite the file completely + utils.write_file(filename, setup_cmd .. "\n", "a") + end +end + +---Generates ftplugin files based on a list of server_names +---The files are generated to a runtimepath: "$LUNARVIM_RUNTIME_DIR/site/after/ftplugin/template.lua" +---@param servers_names table list of servers to be enabled. Will add all by default +function M.generate_templates(servers_names) + servers_names = servers_names or {} + + Log:debug "Templates installation in progress" + + M.remove_template_files() + + if vim.tbl_isempty(servers_names) then + local available_servers = require("nvim-lsp-installer.servers").get_available_servers() + + for _, server in pairs(available_servers) do + table.insert(servers_names, server.name) + end + end + + -- create the directory if it didn't exist + if not utils.is_directory(lvim.lsp.templates_dir) then + vim.fn.mkdir(ftplugin_dir, "p") + end + + for _, server in ipairs(servers_names) do + M.generate_ftplugin(server, ftplugin_dir) + end + Log:debug "Templates installation is complete" +end + +return M diff --git a/lua/lsp/utils.lua b/lua/lsp/utils.lua index 17b9c3bc..59003406 100644 --- a/lua/lsp/utils.lua +++ b/lua/lsp/utils.lua @@ -1,28 +1,62 @@ local M = {} +local tbl = require "utils.table" + function M.is_client_active(name) local clients = vim.lsp.get_active_clients() + return tbl.find_first(clients, function(client) + return client.name == name + end) +end + +function M.get_active_clients_by_ft(filetype) + local matches = {} + local clients = vim.lsp.get_active_clients() for _, client in pairs(clients) do - if client.name == name then - return true, client + local supported_filetypes = client.config.filetypes or {} + if client.name ~= "null-ls" and vim.tbl_contains(supported_filetypes, filetype) then + table.insert(matches, client) end end - return false + return matches end --- FIXME: this should return a list instead -function M.get_active_client_by_ft(filetype) - if not lvim.lang[filetype] or not lvim.lang[filetype].lsp then - return nil +function M.get_client_capabilities(client_id) + if not client_id then + local buf_clients = vim.lsp.buf_get_clients() + for _, buf_client in ipairs(buf_clients) do + if buf_client.name ~= "null-ls" then + client_id = buf_client.id + break + end + end + end + if not client_id then + error "Unable to determine client_id" + return end - local clients = vim.lsp.get_active_clients() - for _, client in pairs(clients) do - if client.name == lvim.lang[filetype].lsp.provider then - return client + local client = vim.lsp.get_client_by_id(tonumber(client_id)) + + local enabled_caps = {} + for capability, status in pairs(client.resolved_capabilities) do + if status == true then + table.insert(enabled_caps, capability) + end + end + + return enabled_caps +end + +function M.get_supported_filetypes(server_name) + -- print("got filetypes query request for: " .. server_name) + local configs = require "lspconfig/configs" + pcall(require, ("lspconfig/" .. server_name)) + for _, config in pairs(configs) do + if config.name == server_name then + return config.document_config.default_config.filetypes or {} end end - return nil end return M |