From e6569c18c21be5166e4b9cc7530e828b8285c84e Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 2 Oct 2024 06:57:18 -0700 Subject: [PATCH] feat(lspinfo): replace :LspInfo with :checkhealth #3339 Problem: :LspInfo has its own "inner platlform" of highlights, mappings etc. And it doesn't integrate with :checkhealth. Solution: - Move the lspinfo code to a healthcheck. - LspInfo features such as highlights, "floating window" presentation, etc., should be added to :checkhealth in Nvim core, if they are really needed. - Define a "q" mapping until Nvim stable has that in :checkhealth. --- .editorconfig | 2 +- .stylua.toml | 2 +- README.md | 2 +- doc/lspconfig.txt | 24 +- lua/lspconfig.lua | 2 +- lua/lspconfig/{ui/lspinfo.lua => health.lua} | 245 ++++++------------- lua/lspconfig/ui/windows.lua | 124 +--------- plugin/lspconfig.lua | 16 +- 8 files changed, 98 insertions(+), 319 deletions(-) rename lua/lspconfig/{ui/lspinfo.lua => health.lua} (50%) diff --git a/.editorconfig b/.editorconfig index 26f3db25..edf2a6d8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ root = true [*] -max_line_length = 100 +max_line_length = 120 indent_style = space indent_size = 2 tab_width = 8 diff --git a/.stylua.toml b/.stylua.toml index 78c5507c..aaebb284 100644 --- a/.stylua.toml +++ b/.stylua.toml @@ -3,4 +3,4 @@ line_endings = "Unix" indent_type = "Spaces" indent_width = 2 quote_style = "AutoPreferSingle" -call_parentheses = "None" +call_parentheses = "Input" diff --git a/README.md b/README.md index 29ba336b..29c68dcc 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Most of the time, the reason for failure is present in the logs. ## Commands -* `:LspInfo` shows the status of active and configured language servers. +* `:LspInfo` (deprecated alias to `:che lspconfig`) shows the status of active and configured language servers. * `:LspStart ` Start the requested server name. Will only successfully start if the command detects a root directory matching the current config. Pass `autostart = false` to your `.setup{}` call for a language server if you would like to launch clients solely with this command. Defaults to all servers matching current buffer filetype. * `:LspStop ` Defaults to stopping all buffer clients. * `:LspRestart ` Defaults to restarting all buffer clients. diff --git a/doc/lspconfig.txt b/doc/lspconfig.txt index d5a44170..0ba19021 100644 --- a/doc/lspconfig.txt +++ b/doc/lspconfig.txt @@ -107,8 +107,7 @@ overrides to `setup{}` are: `pyright.disableOrganizeImports`: `boolean` Nested keys need to be translated into a nested table and passed to - the settings field in `setup{}` as follows: -> + the settings field in `setup{}` as follows: >lua require('lspconfig').pyright.setup{ settings = { pyright = { @@ -317,8 +316,8 @@ The `root_dir` key in `config` and `setup` can hold any function of the form returns nil | string > This allows for rich composition of root directory patterns which is necessary -for some project structures. Example (for Kotlin): -> +for some project structures. Example (for Kotlin): >lua + local root_files = { 'settings.gradle', -- Gradle (multi-project) 'settings.gradle.kts', -- Gradle (multi-project) @@ -369,6 +368,7 @@ standard for single-file mode, lspconfig will adopt that standard. COMMANDS *lspconfig-commands* :LspInfo *:LspInfo* +Deprecated alias to `:check lspconfig`. Shows the status of all configured language servers. Note that client id refers to the Nvim RPC channel connected to a given language server. Also lists deprecated servers. @@ -459,8 +459,8 @@ Note that it will not report any configuration changes applied in LOGGING *lspconfig-logging* When debugging language servers, it is helpful to enable additional logging in -the built-in client, specifically considering the RPC logs. Example: -> +the built-in client, specifically considering the RPC logs. Example: >lua + vim.lsp.set_log_level 'trace' if vim.fn.has 'nvim-0.5.1' == 1 then require('vim.lsp.log').set_format_func(vim.inspect) @@ -486,14 +486,10 @@ from `lspconfig` in time. ============================================================================== Highlights *lspconfig-highlight* -LspInfoTitle Client name -LspInfoList Server name list -LspInfoFiletype `filetypes` area -LspInfoTip Tip -LspInfoBorder Window border - To set the border use: > - require('lspconfig.ui.windows').default_options.border = 'single' -< Accepts the same values as the `border` option to |nvim_open_win()| +WARNING: The `require('lspconfig.ui.windows')` API was removed. +LspInfo is provided by a healthcheck instead: > + :checkhealth lspconfig +< ============================================================================== diff --git a/lua/lspconfig.lua b/lua/lspconfig.lua index 9c1ef7e3..3382259f 100644 --- a/lua/lspconfig.lua +++ b/lua/lspconfig.lua @@ -9,7 +9,7 @@ local M = { ---@class Alias ---@field to string The new name of the server ---@field version string The version that the alias will be removed in ----@field inconfig? boolean need shown in lspinfo +---@field inconfig? boolean should display in healthcheck (`:checkhealth lspconfig`) local aliases = { ['fennel-ls'] = { to = 'fennel_ls', diff --git a/lua/lspconfig/ui/lspinfo.lua b/lua/lspconfig/health.lua similarity index 50% rename from lua/lspconfig/ui/lspinfo.lua rename to lua/lspconfig/health.lua index ac8e6c2b..24d3444f 100644 --- a/lua/lspconfig/ui/lspinfo.lua +++ b/lua/lspconfig/health.lua @@ -1,12 +1,14 @@ +local M = {} +local health = require('vim.health') + local api, fn = vim.api, vim.fn -local windows = require 'lspconfig.ui.windows' local util = require 'lspconfig.util' local error_messages = { - cmd_not_found = 'Unable to find executable. Please check your path and ensure the server is installed', - no_filetype_defined = 'No filetypes defined, Please define filetypes in setup()', + cmd_not_found = 'Unable to find executable. Check your $PATH and ensure the server is installed.', + no_filetype_defined = 'No filetypes defined. Define filetypes in setup().', root_dir_not_found = 'Not found.', - async_root_dir_function = 'Asynchronous root_dir functions are not supported in :LspInfo', + async_root_dir_function = 'Asynchronous root_dir functions are not supported by `:checkhealth lspconfig`', } local helptags = { @@ -22,12 +24,6 @@ local function trim_blankspace(cmd) return trimmed_cmd end -local function indent_lines(lines, offset) - return vim.tbl_map(function(val) - return offset .. val - end, lines) -end - local function remove_newlines(cmd) cmd = trim_blankspace(cmd) cmd = table.concat(cmd, ' ') @@ -78,7 +74,7 @@ local function make_config_info(config, bufnr) end) coroutine.resume(co) if root_dir then - config_info.root_dir = root_dir + config_info.root_dir = vim.fn.fnamemodify(root_dir, ':~') elseif coroutine.status(co) == 'suspended' then config_info.root_dir = error_messages.async_root_dir_function else @@ -115,9 +111,8 @@ local function make_config_info(config, bufnr) }, info_lines) end - vim.list_extend(lines, indent_lines(info_lines, '\t')) - - return lines + vim.list_extend(lines, info_lines) + return table.concat(lines, '\n') end ---@param client vim.lsp.Client @@ -128,16 +123,12 @@ local function make_client_info(client, fname) client_info.cmd = cmd_type[type(client.config.cmd)](client.config) local workspace_folders = fn.has 'nvim-0.9' == 1 and client.workspace_folders or client.workspaceFolders local uv = vim.uv - local is_windows = uv.os_uname().version:match 'Windows' - fname = uv.fs_realpath(fname) or fn.fnamemodify(fn.resolve(fname), ':p') - if is_windows then - fname:gsub('%/', '%\\') - end + fname = vim.fs.normalize(uv.fs_realpath(fname) or fn.fnamemodify(fn.resolve(fname), ':p')) if workspace_folders then for _, schema in ipairs(workspace_folders) do local matched = true - local root_dir = uv.fs_realpath(schema.name) + local root_dir = vim.fn.fnamemodify(uv.fs_realpath(schema.name), ':~') if root_dir == nil or fname:sub(1, root_dir:len()) ~= root_dir then matched = false end @@ -157,7 +148,6 @@ local function make_client_info(client, fname) client_info.attached_buffers_list = table.concat(vim.lsp.get_buffers_by_client_id(client.id), ', ') local lines = { - '', 'Client: ' .. client.name .. ' (id: ' @@ -179,30 +169,30 @@ local function make_client_info(client, fname) info_lines = vim.list_extend(info_lines, server_specific_info) end - vim.list_extend(lines, indent_lines(info_lines, '\t')) + vim.list_extend(lines, info_lines) - return lines + return table.concat(lines, '\n') end -return function() - -- These options need to be cached before switching to the floating - -- buffer. - local original_bufnr = api.nvim_get_current_buf() - local buf_clients = util.get_lsp_clients { bufnr = original_bufnr } - local clients = util.get_lsp_clients() - local buffer_filetype = vim.bo.filetype - local fname = api.nvim_buf_get_name(original_bufnr) +local function check_lspconfig(bufnr) + bufnr = (bufnr and bufnr ~= -1) and bufnr or nil - windows.default_options.wrap = true - windows.default_options.breakindent = true - windows.default_options.breakindentopt = 'shift:25' - windows.default_options.showbreak = 'NONE' + health.start('LSP configs active in this session (globally)') + health.info('Configured servers: ' .. table.concat(util.available_servers(), ', ')) + local deprecated_servers = {} + for server_name, deprecate in pairs(require('lspconfig').server_aliases()) do + table.insert(deprecated_servers, ('%s -> %s'):format(server_name, deprecate.to)) + end + if #deprecated_servers == 0 then + health.ok('Deprecated servers: (none)') + else + health.warn('Deprecated servers: ' .. table.concat(deprecated_servers, ', ')) + end - local win_info = windows.percentage_range_window(0.8, 0.7) - local bufnr, win_id = win_info.bufnr, win_info.win_id - vim.bo.bufhidden = 'wipe' - - local buf_lines = {} + local buf_clients = not bufnr and {} or vim.lsp.get_clients { bufnr = bufnr } + local clients = vim.lsp.get_clients() + local buffer_filetype = bufnr and vim.fn.getbufvar(bufnr, '&filetype') or '(invalid buffer)' + local fname = bufnr and api.nvim_buf_get_name(bufnr) or '(invalid buffer)' local buf_client_ids = {} for _, client in ipairs(buf_clients) do @@ -216,91 +206,29 @@ return function() end end - -- insert the tips at the top of window - buf_lines[#buf_lines + 1] = 'Press q or to close this window. Press to view server doc.' - - local header = { - '', - 'Language client log: ' .. (vim.lsp.get_log_path()), - 'Detected filetype: ' .. buffer_filetype, - } - vim.list_extend(buf_lines, header) - - local buffer_clients_header = { - '', - tostring(#vim.tbl_keys(buf_clients)) .. ' client(s) attached to this buffer: ', - } - - vim.list_extend(buf_lines, buffer_clients_header) + health.start(('LSP configs active in this buffer (id=%s)'):format(bufnr or '(invalid buffer)')) + health.info('Language client log: ' .. (vim.fn.fnamemodify(vim.lsp.get_log_path(), ':~'))) + health.info(('Detected filetype: `%s`'):format(buffer_filetype)) + health.info(('%d client(s) attached to this buffer'):format(#vim.tbl_keys(buf_clients))) for _, client in ipairs(buf_clients) do - local client_info = make_client_info(client, fname) - vim.list_extend(buf_lines, client_info) + health.info(make_client_info(client, fname)) end - local other_active_section_header = { - '', - tostring(#other_active_clients) .. ' active client(s) not attached to this buffer: ', - } if not vim.tbl_isempty(other_active_clients) then - vim.list_extend(buf_lines, other_active_section_header) - end - for _, client in ipairs(other_active_clients) do - local client_info = make_client_info(client, fname) - vim.list_extend(buf_lines, client_info) + health.info(('%s active client(s) not attached to this buffer:'):format(#other_active_clients)) + for _, client in ipairs(other_active_clients) do + health.info(make_client_info(client, fname)) + end end - local other_matching_configs_header = { - '', - 'Other clients that match the filetype: ' .. buffer_filetype, - '', - } - - local other_matching_configs = util.get_other_matching_providers(buffer_filetype) - + local other_matching_configs = not bufnr and {} or util.get_other_matching_providers(buffer_filetype) if not vim.tbl_isempty(other_matching_configs) then - vim.list_extend(buf_lines, other_matching_configs_header) + health.info(('Other clients that match the "%s" filetype: '):format(buffer_filetype)) for _, config in ipairs(other_matching_configs) do - vim.list_extend(buf_lines, make_config_info(config, original_bufnr)) + health.info(make_config_info(config, bufnr)) end end - local matching_config_header = { - '', - 'Configured servers list: ' .. table.concat(util.available_servers(), ', '), - } - local deprecated_servers = {} - vim.list_extend(buf_lines, matching_config_header) - for server_name, deprecate in pairs(require('lspconfig').server_aliases()) do - table.insert(deprecated_servers, ('%s -> %s'):format(server_name, deprecate.to)) - end - if #deprecated_servers > 0 then - vim.list_extend(buf_lines, { '', 'Deprecated servers: ' .. table.concat(deprecated_servers, ' ') }) - end - - local fmt_buf_lines = indent_lines(buf_lines, ' ') - - api.nvim_buf_set_lines(bufnr, 0, -1, true, fmt_buf_lines) - vim.bo.modifiable = false - vim.bo.filetype = 'lspinfo' - - local augroup = api.nvim_create_augroup('lspinfo', { clear = false }) - - local function close() - api.nvim_clear_autocmds { group = augroup, buffer = bufnr } - if api.nvim_win_is_valid(win_id) then - api.nvim_win_close(win_id, true) - end - end - - vim.keymap.set('n', '', close, { buffer = bufnr, nowait = true }) - vim.keymap.set('n', 'q', close, { buffer = bufnr, nowait = true }) - api.nvim_create_autocmd({ 'BufDelete', 'BufHidden' }, { - once = true, - buffer = bufnr, - callback = close, - group = augroup, - }) - vim.fn.matchadd( 'Error', error_messages.no_filetype_defined @@ -311,65 +239,52 @@ return function() .. error_messages.root_dir_not_found ) + -- TODO(justimk): enhance :checkhealth's highlighting instead of doing this only for lspconfig. vim.cmd [[ syn keyword String true syn keyword Error false - syn match LspInfoFiletypeList /\markdown', config.name) + lines[#lines + 1] = '' + vim.list_extend(lines, vim.split(desc, '\n')) + lines[#lines + 1] = '' end - - for _, config in ipairs(other_matching_configs) do - append_lines(config) - end - - local info = windows.percentage_range_window(0.8, 0.7) - lines = indent_lines(lines, ' ') - api.nvim_buf_set_lines(info.bufnr, 0, -1, false, lines) - api.nvim_buf_add_highlight(info.bufnr, 0, 'LspInfoTip', 0, 0, -1) - - vim.bo[info.bufnr].filetype = 'markdown' - vim.bo[info.bufnr].syntax = 'on' - vim.wo[info.win_id].concealcursor = 'niv' - vim.wo[info.win_id].conceallevel = 2 - vim.wo[info.win_id].breakindent = false - vim.wo[info.win_id].breakindentopt = '' - - local function close_doc_win() - if api.nvim_win_is_valid(info.win_id) then - api.nvim_win_close(info.win_id, true) - end - end - - vim.keymap.set('n', '', close_doc_win, { buffer = info.bufnr }) end - vim.keymap.set('n', '', show_doc, { buffer = true, nowait = true }) + for _, client in ipairs(buf_clients) do + local config = require('lspconfig.configs')[client.name] + append_lines(config) + end + + for _, config in ipairs(other_matching_configs) do + append_lines(config) + end + + health.info(table.concat(lines, '\n')) end + +function M.check() + -- XXX: :checkhealth switches to its buffer before invoking the healthcheck(s). + local orig_bufnr = vim.fn.bufnr('#') + local buf_clients, other_matching_configs = check_lspconfig(orig_bufnr) + check_lspdocs(buf_clients, other_matching_configs) + + -- XXX: create "q" mapping until :checkhealth has this feature in Nvim stable. + vim.cmd [[nnoremap q q]] +end + +return M diff --git a/lua/lspconfig/ui/windows.lua b/lua/lspconfig/ui/windows.lua index dc02c9ef..616d30cd 100644 --- a/lua/lspconfig/ui/windows.lua +++ b/lua/lspconfig/ui/windows.lua @@ -1,129 +1,11 @@ --- The following is extracted and modified from plenary.vnim by --- TJ Devries. It is not a stable API, and is expected to change --- -local api = vim.api - -local function apply_defaults(original, defaults) - if original == nil then - original = {} - end - - original = vim.deepcopy(original) - - for k, v in pairs(defaults) do - if original[k] == nil then - original[k] = v - end - end - - return original -end - local win_float = {} win_float.default_options = {} -function win_float.default_opts(options) - options = apply_defaults(options, { percentage = 0.9 }) - - local width = math.floor(vim.o.columns * options.percentage) - local height = math.floor(vim.o.lines * options.percentage) - - local top = math.floor(((vim.o.lines - height) / 2)) - local left = math.floor((vim.o.columns - width) / 2) - - local opts = { - relative = 'editor', - row = top, - col = left, - width = width, - height = height, - style = 'minimal', - border = { - { ' ', 'NormalFloat' }, - { ' ', 'NormalFloat' }, - { ' ', 'NormalFloat' }, - { ' ', 'NormalFloat' }, - { ' ', 'NormalFloat' }, - { ' ', 'NormalFloat' }, - { ' ', 'NormalFloat' }, - { ' ', 'NormalFloat' }, - }, - } - - opts.border = options.border and options.border - - return opts +function win_float.default_opts() + return {} end ---- Create window that takes up certain percentags of the current screen. ---- ---- Works regardless of current buffers, tabs, splits, etc. ---@param col_range number | Table: --- If number, then center the window taking up this percentage of the screen. --- If table, first index should be start, second_index should be end ---@param row_range number | Table: --- If number, then center the window taking up this percentage of the screen. --- If table, first index should be start, second_index should be end -function win_float.percentage_range_window(col_range, row_range, options) - options = apply_defaults(options, win_float.default_options) - - local win_opts = win_float.default_opts(options) - win_opts.relative = 'editor' - - local height_percentage, row_start_percentage - if type(row_range) == 'number' then - assert(row_range <= 1) - assert(row_range > 0) - height_percentage = row_range - row_start_percentage = (1 - height_percentage) / 3 - elseif type(row_range) == 'table' then - height_percentage = row_range[2] - row_range[1] - row_start_percentage = row_range[1] - else - error(string.format("Invalid type for 'row_range': %p", row_range)) - end - - win_opts.height = math.ceil(vim.o.lines * height_percentage) - win_opts.row = math.ceil(vim.o.lines * row_start_percentage) - win_opts.border = options.border or 'none' - - local width_percentage, col_start_percentage - if type(col_range) == 'number' then - assert(col_range <= 1) - assert(col_range > 0) - width_percentage = col_range - col_start_percentage = (1 - width_percentage) / 2 - elseif type(col_range) == 'table' then - width_percentage = col_range[2] - col_range[1] - col_start_percentage = col_range[1] - else - error(string.format("Invalid type for 'col_range': %p", col_range)) - end - - win_opts.col = math.floor(vim.o.columns * col_start_percentage) - win_opts.width = math.floor(vim.o.columns * width_percentage) - - local bufnr = options.bufnr or api.nvim_create_buf(false, true) - local win_id = api.nvim_open_win(bufnr, true, win_opts) - api.nvim_win_set_option(win_id, 'winhl', 'FloatBorder:LspInfoBorder') - - for k, v in pairs(win_float.default_options) do - if k ~= 'border' then - vim.opt_local[k] = v - end - end - - api.nvim_win_set_buf(win_id, bufnr) - - api.nvim_win_set_option(win_id, 'cursorcolumn', false) - api.nvim_buf_set_option(bufnr, 'tabstop', 2) - api.nvim_buf_set_option(bufnr, 'shiftwidth', 2) - - return { - bufnr = bufnr, - win_id = win_id, - } -end +function win_float.percentage_range_window() end return win_float diff --git a/plugin/lspconfig.lua b/plugin/lspconfig.lua index e5d924cc..fd4631d1 100644 --- a/plugin/lspconfig.lua +++ b/plugin/lspconfig.lua @@ -48,23 +48,9 @@ local get_clients_from_cmd_args = function(arg) return result end -for group, hi in pairs { - LspInfoBorder = { link = 'Label', default = true }, - LspInfoList = { link = 'Function', default = true }, - LspInfoTip = { link = 'Comment', default = true }, - LspInfoTitle = { link = 'Title', default = true }, - LspInfoFiletype = { link = 'Type', default = true }, -} do - api.nvim_set_hl(0, group, hi) -end - -- Called from plugin/lspconfig.vim because it requires knowing that the last -- script in scriptnames to be executed is lspconfig. -api.nvim_create_user_command('LspInfo', function() - require 'lspconfig.ui.lspinfo'() -end, { - desc = 'Displays attached, active, and configured language servers', -}) +api.nvim_create_user_command('LspInfo', ':che lspconfig', { desc = 'Deprecated alias to `:che lspconfig`' }) api.nvim_create_user_command('LspStart', function(info) local server_name = string.len(info.args) > 0 and info.args or nil