From 688860741589b4583129e426f4df0523f9213275 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:12:49 -0500 Subject: [PATCH] feat(lsp): add more LSP defaults (#28500) - crn for rename - crr for code actions - gr for references - (in Insert mode) for signature help --- runtime/doc/change.txt | 4 + runtime/doc/lsp.txt | 42 ++-- runtime/doc/news.txt | 11 +- runtime/doc/vim_diff.txt | 4 + runtime/lua/vim/_defaults.lua | 450 ++++++++++++++++++---------------- runtime/lua/vim/lsp.lua | 2 +- 6 files changed, 274 insertions(+), 239 deletions(-) diff --git a/runtime/doc/change.txt b/runtime/doc/change.txt index 6d3efb4167..0f362c0cd4 100644 --- a/runtime/doc/change.txt +++ b/runtime/doc/change.txt @@ -281,6 +281,10 @@ gr{char} Replace the virtual characters under the cursor with that have a special meaning in Insert mode, such as most CTRL-keys, cannot be used. + *gr-default* + Mapped to |vim.lsp.buf.references()| by default. + |default-mappings| + *digraph-arg* The argument for Normal mode commands like |r| and |t| is a single character. When 'cpo' doesn't contain the 'D' flag, this character can also be entered diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index a0d0b75407..b3f60ed41f 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -61,47 +61,41 @@ options are not restored when the LSP client is stopped or detached. - |K| is mapped to |vim.lsp.buf.hover()| unless |'keywordprg'| is customized or a custom keymap for `K` exists. + *crr* *v_crr* *crn* *i_CTRL-S* +Some keymaps are created unconditionally when Nvim starts: +- "crn" is mapped in Normal mode to |vim.lsp.buf.rename()| +- "crr" is mapped in Normal and Visual mode to |vim.lsp.buf.code_action()| +- "gr" is mapped in Normal mode to |vim.lsp.buf.references()| |gr-default| +- CTRL-S is mapped in Insert mode to |vim.lsp.buf.signature_help()| + +If not wanted, these keymaps can be removed at any time using +|vim.keymap.del()| or |:unmap|. + *lsp-defaults-disable* To override the above defaults, set or unset the options on |LspAttach|: >lua + vim.api.nvim_create_autocmd('LspAttach', { callback = function(ev) vim.bo[ev.buf].formatexpr = nil vim.bo[ev.buf].omnifunc = nil - vim.keymap.del("n", "K", { buffer = ev.buf }) + vim.keymap.del('n', 'K', { buffer = ev.buf }) end, }) -To use other LSP features like hover, rename, etc. you can set other keymaps -on |LspAttach|. Example: >lua - vim.api.nvim_create_autocmd('LspAttach', { - callback = function(args) - vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = args.buf }) - end, - }) +To use other LSP features, set keymaps on |LspAttach|. Not all language +servers provide the same capabilities. To ensure you only set keymaps if the +language server supports a feature, guard keymaps behind capability checks. +Example: >lua -The most common functions are: - -- |vim.lsp.buf.hover()| -- |vim.lsp.buf.format()| -- |vim.lsp.buf.references()| -- |vim.lsp.buf.implementation()| -- |vim.lsp.buf.code_action()| - - -Not all language servers provide the same capabilities. To ensure you only set -keymaps if the language server supports a feature, you can guard the keymap -calls behind capability checks: ->lua vim.api.nvim_create_autocmd('LspAttach', { callback = function(args) local client = vim.lsp.get_client_by_id(args.data.client_id) - if client.server_capabilities.hoverProvider then - vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = args.buf }) + if client.supports_method('textDocument/implementation') then + vim.keymap.set('n', 'g', vim.lsp.buf.implementation, { buffer = args.buf }) end end, }) < - To learn what capabilities are available you can run the following command in a buffer with a started LSP client: >vim diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 5881d90358..edf0b8fb7c 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -408,6 +408,10 @@ The following changes to existing APIs or features add new behavior. • 'comments' includes "fb:•". • 'shortmess' includes the "C" flag. • 'grepprg' defaults to using ripgrep if available. + • |crn| in Normal mode maps to |vim.lsp.buf.rename()|. + • |crr| in Normal and Visual mode maps to |vim.lsp.buf.code_action()|. + • "gr" in Normal mode maps to |vim.lsp.buf.references()| |gr-default| + • |i_CTRL-S| in Insert mode maps to |vim.lsp.buf.signature_help()| • Automatic linting of treesitter query files (see |ft-query-plugin|). Can be disabled via: >lua vim.g.query_lint_on = {} @@ -438,9 +442,10 @@ The following changes to existing APIs or features add new behavior. :call netrw#BrowseX(expand(exists("g:netrw_gx")? g:netrw_gx : ''), netrw#CheckIfRemote()) -• |vim.lsp.start()| now maps |K| to use |vim.lsp.buf.hover()| if the server - supports it, unless |'keywordprg'| was customized before calling - |vim.lsp.start()|. +• |vim.lsp.start()| now creates the following default keymaps (assuming the + server supports the feature): + - |K| in Normal mode maps to |vim.lsp.buf.hover()|, unless |'keywordprg'| + was customized before calling |vim.lsp.start()|. • Terminal buffers started with no arguments (and use 'shell') close automatically if the job exited without error, eliminating the (often diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index d426f8151f..c767c71523 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -137,6 +137,10 @@ of these in your config by simply removing the mapping, e.g. ":unmap Y". - * |v_star-default| - gc |gc-default| |v_gc-default| |o_gc-default| - gcc |gcc-default| +- |crn| +- |crr| +- gr |gr-default| +- |i_CTRL-S| - Nvim LSP client defaults |lsp-defaults| - K |K-lsp-default| diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 25bef2352c..b1cd4f6ec6 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -144,6 +144,31 @@ do end vim.keymap.set({ 'o' }, 'gc', textobject_rhs, { desc = 'Comment textobject' }) end + + --- Default maps for LSP functions. + --- + --- These are mapped unconditionally to avoid confusion. If no server is attached, or if a server + --- does not support a capability, an error message is displayed rather than exhibiting different + --- behavior. + --- + --- See |gr-default|, |crn|, |crr|, |i_CTRL-S|. + do + vim.keymap.set('n', 'crn', function() + vim.lsp.buf.rename() + end, { desc = 'vim.lsp.buf.rename()' }) + + vim.keymap.set({ 'n', 'v' }, 'crr', function() + vim.lsp.buf.code_action() + end, { desc = 'vim.lsp.buf.code_action()' }) + + vim.keymap.set('n', 'gr', function() + vim.lsp.buf.references() + end, { desc = 'vim.lsp.buf.references()' }) + + vim.keymap.set('i', '', function() + vim.lsp.buf.signature_help() + end, { desc = 'vim.lsp.buf.signature_help()' }) + end end --- Default menus @@ -243,230 +268,140 @@ do vim.notify(('W325: Ignoring swapfile from Nvim process %d'):format(info.pid)) end, }) -end --- Only do the following when the TUI is attached -local tty = nil -for _, ui in ipairs(vim.api.nvim_list_uis()) do - if ui.chan == 1 and ui.stdout_tty then - tty = ui - break - end -end - -if tty then - local group = vim.api.nvim_create_augroup('nvim_tty', {}) - - --- Set an option after startup (so that OptionSet is fired), but only if not - --- already set by the user. - --- - --- @param option string Option name - --- @param value any Option value - local function setoption(option, value) - if vim.api.nvim_get_option_info2(option, {}).was_set then - -- Don't do anything if option is already set - return - end - - -- Wait until Nvim is finished starting to set the option to ensure the - -- OptionSet event fires. - if vim.v.vim_did_enter == 1 then - --- @diagnostic disable-next-line:no-unknown - vim.o[option] = value - else - vim.api.nvim_create_autocmd('VimEnter', { - group = group, - once = true, - nested = true, - callback = function() - setoption(option, value) - end, - }) + -- Only do the following when the TUI is attached + local tty = nil + for _, ui in ipairs(vim.api.nvim_list_uis()) do + if ui.chan == 1 and ui.stdout_tty then + tty = ui + break end end - --- Guess value of 'background' based on terminal color. - --- - --- We write Operating System Command (OSC) 11 to the terminal to request the - --- terminal's background color. We then wait for a response. If the response - --- matches `rgba:RRRR/GGGG/BBBB/AAAA` where R, G, B, and A are hex digits, then - --- compute the luminance[1] of the RGB color and classify it as light/dark - --- accordingly. Note that the color components may have anywhere from one to - --- four hex digits, and require scaling accordingly as values out of 4, 8, 12, - --- or 16 bits. Also note the A(lpha) component is optional, and is parsed but - --- ignored in the calculations. - --- - --- [1] https://en.wikipedia.org/wiki/Luma_%28video%29 - do - --- Parse a string of hex characters as a color. + if tty then + local group = vim.api.nvim_create_augroup('nvim_tty', {}) + + --- Set an option after startup (so that OptionSet is fired), but only if not + --- already set by the user. --- - --- The string can contain 1 to 4 hex characters. The returned value is - --- between 0.0 and 1.0 (inclusive) representing the intensity of the color. - --- - --- For instance, if only a single hex char "a" is used, then this function - --- returns 0.625 (10 / 16), while a value of "aa" would return 0.664 (170 / - --- 256). - --- - --- @param c string Color as a string of hex chars - --- @return number? Intensity of the color - local function parsecolor(c) - if #c == 0 or #c > 4 then - return nil + --- @param option string Option name + --- @param value any Option value + local function setoption(option, value) + if vim.api.nvim_get_option_info2(option, {}).was_set then + -- Don't do anything if option is already set + return end - local val = tonumber(c, 16) - if not val then - return nil + -- Wait until Nvim is finished starting to set the option to ensure the + -- OptionSet event fires. + if vim.v.vim_did_enter == 1 then + --- @diagnostic disable-next-line:no-unknown + vim.o[option] = value + else + vim.api.nvim_create_autocmd('VimEnter', { + group = group, + once = true, + nested = true, + callback = function() + setoption(option, value) + end, + }) end - - local max = tonumber(string.rep('f', #c), 16) - return val / max end - --- Parse an OSC 11 response + --- Guess value of 'background' based on terminal color. --- - --- Either of the two formats below are accepted: + --- We write Operating System Command (OSC) 11 to the terminal to request the + --- terminal's background color. We then wait for a response. If the response + --- matches `rgba:RRRR/GGGG/BBBB/AAAA` where R, G, B, and A are hex digits, then + --- compute the luminance[1] of the RGB color and classify it as light/dark + --- accordingly. Note that the color components may have anywhere from one to + --- four hex digits, and require scaling accordingly as values out of 4, 8, 12, + --- or 16 bits. Also note the A(lpha) component is optional, and is parsed but + --- ignored in the calculations. --- - --- OSC 11 ; rgb:// - --- - --- or - --- - --- OSC 11 ; rgba:/// - --- - --- where - --- - --- , , , := h | hh | hhh | hhhh - --- - --- The alpha component is ignored, if present. - --- - --- @param resp string OSC 11 response - --- @return string? Red component - --- @return string? Green component - --- @return string? Blue component - local function parseosc11(resp) - local r, g, b - r, g, b = resp:match('^\027%]11;rgb:(%x+)/(%x+)/(%x+)$') - if not r and not g and not b then - local a - r, g, b, a = resp:match('^\027%]11;rgba:(%x+)/(%x+)/(%x+)/(%x+)$') - if not a or #a > 4 then - return nil, nil, nil + --- [1] https://en.wikipedia.org/wiki/Luma_%28video%29 + do + --- Parse a string of hex characters as a color. + --- + --- The string can contain 1 to 4 hex characters. The returned value is + --- between 0.0 and 1.0 (inclusive) representing the intensity of the color. + --- + --- For instance, if only a single hex char "a" is used, then this function + --- returns 0.625 (10 / 16), while a value of "aa" would return 0.664 (170 / + --- 256). + --- + --- @param c string Color as a string of hex chars + --- @return number? Intensity of the color + local function parsecolor(c) + if #c == 0 or #c > 4 then + return nil end + + local val = tonumber(c, 16) + if not val then + return nil + end + + local max = tonumber(string.rep('f', #c), 16) + return val / max end - if r and g and b and #r <= 4 and #g <= 4 and #b <= 4 then - return r, g, b - end - - return nil, nil, nil - end - - local timer = assert(vim.uv.new_timer()) - - local id = vim.api.nvim_create_autocmd('TermResponse', { - group = group, - nested = true, - callback = function(args) - local resp = args.data ---@type string - local r, g, b = parseosc11(resp) - if r and g and b then - local rr = parsecolor(r) - local gg = parsecolor(g) - local bb = parsecolor(b) - - if rr and gg and bb then - local luminance = (0.299 * rr) + (0.587 * gg) + (0.114 * bb) - local bg = luminance < 0.5 and 'dark' or 'light' - setoption('background', bg) + --- Parse an OSC 11 response + --- + --- Either of the two formats below are accepted: + --- + --- OSC 11 ; rgb:// + --- + --- or + --- + --- OSC 11 ; rgba:/// + --- + --- where + --- + --- , , , := h | hh | hhh | hhhh + --- + --- The alpha component is ignored, if present. + --- + --- @param resp string OSC 11 response + --- @return string? Red component + --- @return string? Green component + --- @return string? Blue component + local function parseosc11(resp) + local r, g, b + r, g, b = resp:match('^\027%]11;rgb:(%x+)/(%x+)/(%x+)$') + if not r and not g and not b then + local a + r, g, b, a = resp:match('^\027%]11;rgba:(%x+)/(%x+)/(%x+)/(%x+)$') + if not a or #a > 4 then + return nil, nil, nil end - - return true end - end, - }) - io.stdout:write('\027]11;?\007') + if r and g and b and #r <= 4 and #g <= 4 and #b <= 4 then + return r, g, b + end - timer:start(1000, 0, function() - -- Delete the autocommand if no response was received - vim.schedule(function() - -- Suppress error if autocommand has already been deleted - pcall(vim.api.nvim_del_autocmd, id) - end) - - if not timer:is_closing() then - timer:close() + return nil, nil, nil end - end) - end - - --- If the TUI (term_has_truecolor) was able to determine that the host - --- terminal supports truecolor, enable 'termguicolors'. Otherwise, query the - --- terminal (using both XTGETTCAP and SGR + DECRQSS). If the terminal's - --- response indicates that it does support truecolor enable 'termguicolors', - --- but only if the user has not already disabled it. - do - if tty.rgb then - -- The TUI was able to determine truecolor support - setoption('termguicolors', true) - else - local caps = {} ---@type table - require('vim.termcap').query({ 'Tc', 'RGB', 'setrgbf', 'setrgbb' }, function(cap, found) - if not found then - return - end - - caps[cap] = true - if caps.Tc or caps.RGB or (caps.setrgbf and caps.setrgbb) then - setoption('termguicolors', true) - end - end) local timer = assert(vim.uv.new_timer()) - -- Arbitrary colors to set in the SGR sequence - local r = 1 - local g = 2 - local b = 3 - local id = vim.api.nvim_create_autocmd('TermResponse', { group = group, nested = true, callback = function(args) local resp = args.data ---@type string - local decrqss = resp:match('^\027P1%$r([%d;:]+)m$') + local r, g, b = parseosc11(resp) + if r and g and b then + local rr = parsecolor(r) + local gg = parsecolor(g) + local bb = parsecolor(b) - if decrqss then - -- The DECRQSS SGR response first contains attributes separated by - -- semicolons, followed by the SGR itself with parameters separated - -- by colons. Some terminals include "0" in the attribute list - -- unconditionally; others do not. Our SGR sequence did not set any - -- attributes, so there should be no attributes in the list. - local attrs = vim.split(decrqss, ';') - if #attrs ~= 1 and (#attrs ~= 2 or attrs[1] ~= '0') then - return false - end - - -- The returned SGR sequence should begin with 48:2 - local sgr = attrs[#attrs]:match('^48:2:([%d:]+)$') - if not sgr then - return false - end - - -- The remaining elements of the SGR sequence should be the 3 colors - -- we set. Some terminals also include an additional parameter - -- (which can even be empty!), so handle those cases as well - local params = vim.split(sgr, ':') - if #params ~= 3 and (#params ~= 4 or (params[1] ~= '' and params[1] ~= '1')) then - return true - end - - if - tonumber(params[#params - 2]) == r - and tonumber(params[#params - 1]) == g - and tonumber(params[#params]) == b - then - setoption('termguicolors', true) + if rr and gg and bb then + local luminance = (0.299 * rr) + (0.587 * gg) + (0.114 * bb) + local bg = luminance < 0.5 and 'dark' or 'light' + setoption('background', bg) end return true @@ -474,16 +409,7 @@ if tty then end, }) - -- Write SGR followed by DECRQSS. This sets the background color then - -- immediately asks the terminal what the background color is. If the - -- terminal responds to the DECRQSS with the same SGR sequence that we - -- sent then the terminal supports truecolor. - local decrqss = '\027P$qm\027\\' - if os.getenv('TMUX') then - decrqss = string.format('\027Ptmux;%s\027\\', decrqss:gsub('\027', '\027\027')) - end - -- Reset attributes first, as other code may have set attributes. - io.stdout:write(string.format('\027[0m\027[48;2;%d;%d;%dm%s', r, g, b, decrqss)) + io.stdout:write('\027]11;?\007') timer:start(1000, 0, function() -- Delete the autocommand if no response was received @@ -497,13 +423,115 @@ if tty then end end) end + + --- If the TUI (term_has_truecolor) was able to determine that the host + --- terminal supports truecolor, enable 'termguicolors'. Otherwise, query the + --- terminal (using both XTGETTCAP and SGR + DECRQSS). If the terminal's + --- response indicates that it does support truecolor enable 'termguicolors', + --- but only if the user has not already disabled it. + do + if tty.rgb then + -- The TUI was able to determine truecolor support + setoption('termguicolors', true) + else + local caps = {} ---@type table + require('vim.termcap').query({ 'Tc', 'RGB', 'setrgbf', 'setrgbb' }, function(cap, found) + if not found then + return + end + + caps[cap] = true + if caps.Tc or caps.RGB or (caps.setrgbf and caps.setrgbb) then + setoption('termguicolors', true) + end + end) + + local timer = assert(vim.uv.new_timer()) + + -- Arbitrary colors to set in the SGR sequence + local r = 1 + local g = 2 + local b = 3 + + local id = vim.api.nvim_create_autocmd('TermResponse', { + group = group, + nested = true, + callback = function(args) + local resp = args.data ---@type string + local decrqss = resp:match('^\027P1%$r([%d;:]+)m$') + + if decrqss then + -- The DECRQSS SGR response first contains attributes separated by + -- semicolons, followed by the SGR itself with parameters separated + -- by colons. Some terminals include "0" in the attribute list + -- unconditionally; others do not. Our SGR sequence did not set any + -- attributes, so there should be no attributes in the list. + local attrs = vim.split(decrqss, ';') + if #attrs ~= 1 and (#attrs ~= 2 or attrs[1] ~= '0') then + return false + end + + -- The returned SGR sequence should begin with 48:2 + local sgr = attrs[#attrs]:match('^48:2:([%d:]+)$') + if not sgr then + return false + end + + -- The remaining elements of the SGR sequence should be the 3 colors + -- we set. Some terminals also include an additional parameter + -- (which can even be empty!), so handle those cases as well + local params = vim.split(sgr, ':') + if #params ~= 3 and (#params ~= 4 or (params[1] ~= '' and params[1] ~= '1')) then + return true + end + + if + tonumber(params[#params - 2]) == r + and tonumber(params[#params - 1]) == g + and tonumber(params[#params]) == b + then + setoption('termguicolors', true) + end + + return true + end + end, + }) + + -- Write SGR followed by DECRQSS. This sets the background color then + -- immediately asks the terminal what the background color is. If the + -- terminal responds to the DECRQSS with the same SGR sequence that we + -- sent then the terminal supports truecolor. + local decrqss = '\027P$qm\027\\' + if os.getenv('TMUX') then + decrqss = string.format('\027Ptmux;%s\027\\', decrqss:gsub('\027', '\027\027')) + end + -- Reset attributes first, as other code may have set attributes. + io.stdout:write(string.format('\027[0m\027[48;2;%d;%d;%dm%s', r, g, b, decrqss)) + + timer:start(1000, 0, function() + -- Delete the autocommand if no response was received + vim.schedule(function() + -- Suppress error if autocommand has already been deleted + pcall(vim.api.nvim_del_autocmd, id) + end) + + if not timer:is_closing() then + timer:close() + end + end) + end + end end end ---- Default 'grepprg' to ripgrep if available. -if vim.fn.executable('rg') == 1 then - -- Match :grep default, otherwise rg searches cwd by default - -- Use -uuu to make ripgrep not do its default filtering - vim.o.grepprg = 'rg --vimgrep -uuu $* ' .. (vim.fn.has('unix') == 1 and '/dev/null' or 'nul') - vim.o.grepformat = '%f:%l:%c:%m' +--- Default options +do + --- Default 'grepprg' to ripgrep if available. + if vim.fn.executable('rg') == 1 then + -- Match :grep default, otherwise rg searches cwd by default + -- Use -uuu to make ripgrep not do its default filtering + vim.o.grepprg = 'rg --vimgrep -uuu $* ' .. (vim.fn.has('unix') == 1 and '/dev/null' or 'nul') + vim.o.grepformat = '%f:%l:%c:%m' + end end diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 25feeb0e8d..3f459491f3 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -348,7 +348,7 @@ function lsp._set_defaults(client, bufnr) and is_empty_or_default(bufnr, 'keywordprg') and vim.fn.maparg('K', 'n', false, false) == '' then - vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr }) + vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr, desc = 'vim.lsp.buf.hover()' }) end end) if client.supports_method(ms.textDocument_diagnostic) then