diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2024-05-24 19:18:11 +0000 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2024-05-24 19:18:11 +0000 |
| commit | ff7ed8f586589d620a806c3758fac4a47a8e7e15 (patch) | |
| tree | 729bbcb92231538fa61dab6c3d890b025484b7f5 /runtime/lua/vim | |
| parent | 376914f419eb08fdf4c1a63a77e1f035898a0f10 (diff) | |
| parent | 28c04948a1c887a1cc0cb64de79fa32631700466 (diff) | |
| download | rneovim-ff7ed8f586589d620a806c3758fac4a47a8e7e15.tar.gz rneovim-ff7ed8f586589d620a806c3758fac4a47a8e7e15.tar.bz2 rneovim-ff7ed8f586589d620a806c3758fac4a47a8e7e15.zip | |
Merge remote-tracking branch 'upstream/master' into mix_20240309
Diffstat (limited to 'runtime/lua/vim')
62 files changed, 4707 insertions, 2154 deletions
diff --git a/runtime/lua/vim/_comment.lua b/runtime/lua/vim/_comment.lua new file mode 100644 index 0000000000..044cd69716 --- /dev/null +++ b/runtime/lua/vim/_comment.lua @@ -0,0 +1,263 @@ +---@nodoc +---@class vim._comment.Parts +---@field left string Left part of comment +---@field right string Right part of comment + +--- Get 'commentstring' at cursor +---@param ref_position integer[] +---@return string +local function get_commentstring(ref_position) + local buf_cs = vim.bo.commentstring + + local has_ts_parser, ts_parser = pcall(vim.treesitter.get_parser) + if not has_ts_parser then + return buf_cs + end + + -- Try to get 'commentstring' associated with local tree-sitter language. + -- This is useful for injected languages (like markdown with code blocks). + local row, col = ref_position[1] - 1, ref_position[2] + local ref_range = { row, col, row, col + 1 } + + -- - Get 'commentstring' from the deepest LanguageTree which both contains + -- reference range and has valid 'commentstring' (meaning it has at least + -- one associated 'filetype' with valid 'commentstring'). + -- In simple cases using `parser:language_for_range()` would be enough, but + -- it fails for languages without valid 'commentstring' (like 'comment'). + local ts_cs, res_level = nil, 0 + + ---@param lang_tree vim.treesitter.LanguageTree + local function traverse(lang_tree, level) + if not lang_tree:contains(ref_range) then + return + end + + local lang = lang_tree:lang() + local filetypes = vim.treesitter.language.get_filetypes(lang) + for _, ft in ipairs(filetypes) do + local cur_cs = vim.filetype.get_option(ft, 'commentstring') + if cur_cs ~= '' and level > res_level then + ts_cs = cur_cs + end + end + + for _, child_lang_tree in pairs(lang_tree:children()) do + traverse(child_lang_tree, level + 1) + end + end + traverse(ts_parser, 1) + + return ts_cs or buf_cs +end + +--- Compute comment parts from 'commentstring' +---@param ref_position integer[] +---@return vim._comment.Parts +local function get_comment_parts(ref_position) + local cs = get_commentstring(ref_position) + + if cs == nil or cs == '' then + vim.api.nvim_echo({ { "Option 'commentstring' is empty.", 'WarningMsg' } }, true, {}) + return { left = '', right = '' } + end + + if not (type(cs) == 'string' and cs:find('%%s') ~= nil) then + error(vim.inspect(cs) .. " is not a valid 'commentstring'.") + end + + -- Structure of 'commentstring': <left part> <%s> <right part> + local left, right = cs:match('^(.-)%%s(.-)$') + return { left = left, right = right } +end + +--- Make a function that checks if a line is commented +---@param parts vim._comment.Parts +---@return fun(line: string): boolean +local function make_comment_check(parts) + local l_esc, r_esc = vim.pesc(parts.left), vim.pesc(parts.right) + + -- Commented line has the following structure: + -- <whitespace> <trimmed left> <anything> <trimmed right> <whitespace> + local regex = '^%s-' .. vim.trim(l_esc) .. '.*' .. vim.trim(r_esc) .. '%s-$' + + return function(line) + return line:find(regex) ~= nil + end +end + +--- Compute comment-related information about lines +---@param lines string[] +---@param parts vim._comment.Parts +---@return string indent +---@return boolean is_commented +local function get_lines_info(lines, parts) + local comment_check = make_comment_check(parts) + + local is_commented = true + local indent_width = math.huge + ---@type string + local indent + + for _, l in ipairs(lines) do + -- Update lines indent: minimum of all indents except blank lines + local _, indent_width_cur, indent_cur = l:find('^(%s*)') + + -- Ignore blank lines completely when making a decision + if indent_width_cur < l:len() then + -- NOTE: Copying actual indent instead of recreating it with `indent_width` + -- allows to handle both tabs and spaces + if indent_width_cur < indent_width then + ---@diagnostic disable-next-line:cast-local-type + indent_width, indent = indent_width_cur, indent_cur + end + + -- Update comment info: commented if every non-blank line is commented + if is_commented then + is_commented = comment_check(l) + end + end + end + + -- `indent` can still be `nil` in case all `lines` are empty + return indent or '', is_commented +end + +--- Compute whether a string is blank +---@param x string +---@return boolean is_blank +local function is_blank(x) + return x:find('^%s*$') ~= nil +end + +--- Make a function which comments a line +---@param parts vim._comment.Parts +---@param indent string +---@return fun(line: string): string +local function make_comment_function(parts, indent) + local prefix, nonindent_start, suffix = indent .. parts.left, indent:len() + 1, parts.right + local blank_comment = indent .. vim.trim(parts.left) .. vim.trim(parts.right) + + return function(line) + if is_blank(line) then + return blank_comment + end + return prefix .. line:sub(nonindent_start) .. suffix + end +end + +--- Make a function which uncomments a line +---@param parts vim._comment.Parts +---@return fun(line: string): string +local function make_uncomment_function(parts) + local l_esc, r_esc = vim.pesc(parts.left), vim.pesc(parts.right) + local regex = '^(%s*)' .. l_esc .. '(.*)' .. r_esc .. '(%s-)$' + local regex_trimmed = '^(%s*)' .. vim.trim(l_esc) .. '(.*)' .. vim.trim(r_esc) .. '(%s-)$' + + return function(line) + -- Try regex with exact comment parts first, fall back to trimmed parts + local indent, new_line, trail = line:match(regex) + if new_line == nil then + indent, new_line, trail = line:match(regex_trimmed) + end + + -- Return original if line is not commented + if new_line == nil then + return line + end + + -- Prevent trailing whitespace + if is_blank(new_line) then + indent, trail = '', '' + end + + return indent .. new_line .. trail + end +end + +--- Comment/uncomment buffer range +---@param line_start integer +---@param line_end integer +---@param ref_position? integer[] +local function toggle_lines(line_start, line_end, ref_position) + ref_position = ref_position or { line_start, 0 } + local parts = get_comment_parts(ref_position) + local lines = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end, false) + local indent, is_comment = get_lines_info(lines, parts) + + local f = is_comment and make_uncomment_function(parts) or make_comment_function(parts, indent) + + -- Direct `nvim_buf_set_lines()` essentially removes both regular and + -- extended marks (squashes to empty range at either side of the region) + -- inside region. Use 'lockmarks' to preserve regular marks. + -- Preserving extmarks is not a universally good thing to do: + -- - Good for non-highlighting in text area extmarks (like showing signs). + -- - Debatable for highlighting in text area (like LSP semantic tokens). + -- Mostly because it causes flicker as highlighting is preserved during + -- comment toggling. + package.loaded['vim._comment']._lines = vim.tbl_map(f, lines) + local lua_cmd = string.format( + 'vim.api.nvim_buf_set_lines(0, %d, %d, false, package.loaded["vim._comment"]._lines)', + line_start - 1, + line_end + ) + vim.cmd.lua({ lua_cmd, mods = { lockmarks = true } }) + package.loaded['vim._comment']._lines = nil +end + +--- Operator which toggles user-supplied range of lines +---@param mode string? +---|"'line'" +---|"'char'" +---|"'block'" +local function operator(mode) + -- Used without arguments as part of expression mapping. Otherwise it is + -- called as 'operatorfunc'. + if mode == nil then + vim.o.operatorfunc = "v:lua.require'vim._comment'.operator" + return 'g@' + end + + -- Compute target range + local mark_from, mark_to = "'[", "']" + local lnum_from, col_from = vim.fn.line(mark_from), vim.fn.col(mark_from) + local lnum_to, col_to = vim.fn.line(mark_to), vim.fn.col(mark_to) + + -- Do nothing if "from" mark is after "to" (like in empty textobject) + if (lnum_from > lnum_to) or (lnum_from == lnum_to and col_from > col_to) then + return + end + + -- NOTE: use cursor position as reference for possibly computing local + -- tree-sitter-based 'commentstring'. Recompute every time for a proper + -- dot-repeat. In Visual and sometimes Normal mode it uses start position. + toggle_lines(lnum_from, lnum_to, vim.api.nvim_win_get_cursor(0)) + return '' +end + +--- Select contiguous commented lines at cursor +local function textobject() + local lnum_cur = vim.fn.line('.') + local parts = get_comment_parts({ lnum_cur, vim.fn.col('.') }) + local comment_check = make_comment_check(parts) + + if not comment_check(vim.fn.getline(lnum_cur)) then + return + end + + -- Compute commented range + local lnum_from = lnum_cur + while (lnum_from >= 2) and comment_check(vim.fn.getline(lnum_from - 1)) do + lnum_from = lnum_from - 1 + end + + local lnum_to = lnum_cur + local n_lines = vim.api.nvim_buf_line_count(0) + while (lnum_to <= n_lines - 1) and comment_check(vim.fn.getline(lnum_to + 1)) do + lnum_to = lnum_to + 1 + end + + -- Select range linewise for operator to act upon + vim.cmd('normal! ' .. lnum_from .. 'GV' .. lnum_to .. 'G') +end + +return { operator = operator, textobject = textobject, toggle_lines = toggle_lines } diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 91baee1a1e..5b964b84a0 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -1,3 +1,31 @@ +--- Default user commands +do + vim.api.nvim_create_user_command('Inspect', function(cmd) + if cmd.bang then + vim.print(vim.inspect_pos()) + else + vim.show_pos() + end + end, { desc = 'Inspect highlights and extmarks at the cursor', bang = true }) + + vim.api.nvim_create_user_command('InspectTree', function(cmd) + if cmd.mods ~= '' or cmd.count ~= 0 then + local count = cmd.count ~= 0 and cmd.count or '' + local new = cmd.mods ~= '' and 'new' or 'vnew' + + vim.treesitter.inspect_tree({ + command = ('%s %s%s'):format(cmd.mods, count, new), + }) + else + vim.treesitter.inspect_tree() + end + end, { desc = 'Inspect treesitter language tree for buffer', count = true }) + + vim.api.nvim_create_user_command('EditQuery', function(cmd) + vim.treesitter.query.edit(cmd.fargs[1]) + end, { desc = 'Edit treesitter query', nargs = '?' }) +end + --- Default mappings do --- Default maps for * and # in visual mode. @@ -50,42 +78,126 @@ do --- See |&-default| vim.keymap.set('n', '&', ':&&<CR>', { desc = ':help &-default' }) - --- Use Q in visual mode to execute a macro on each line of the selection. #21422 + --- Use Q in Visual mode to execute a macro on each line of the selection. #21422 + --- This only make sense in linewise Visual mode. #28287 --- --- Applies to @x and includes @@ too. vim.keymap.set( 'x', 'Q', - ':normal! @<C-R>=reg_recorded()<CR><CR>', - { silent = true, desc = ':help v_Q-default' } + "mode() == 'V' ? ':normal! @<C-R>=reg_recorded()<CR><CR>' : 'Q'", + { silent = true, expr = true, desc = ':help v_Q-default' } ) vim.keymap.set( 'x', '@', - "':normal! @'.getcharstr().'<CR>'", + "mode() == 'V' ? ':normal! @'.getcharstr().'<CR>' : '@'", { silent = true, expr = true, desc = ':help v_@-default' } ) - --- Map |gx| to call |vim.ui.open| on the identifier under the cursor + + --- Map |gx| to call |vim.ui.open| on the <cfile> at cursor. do local function do_open(uri) - local _, err = vim.ui.open(uri) - if err then - vim.notify(err, vim.log.levels.ERROR) + local cmd, err = vim.ui.open(uri) + local rv = cmd and cmd:wait(1000) or nil + if cmd and rv and rv.code ~= 0 then + err = ('vim.ui.open: command %s (%d): %s'):format( + (rv.code == 124 and 'timeout' or 'failed'), + rv.code, + vim.inspect(cmd.cmd) + ) end + return err end local gx_desc = 'Opens filepath or URI under cursor with the system handler (file explorer, web browser, …)' vim.keymap.set({ 'n' }, 'gx', function() - do_open(vim.fn.expand('<cfile>')) + local err = do_open(require('vim.ui')._get_url()) + if err then + vim.notify(err, vim.log.levels.ERROR) + end end, { desc = gx_desc }) vim.keymap.set({ 'x' }, 'gx', function() local lines = vim.fn.getregion(vim.fn.getpos('.'), vim.fn.getpos('v'), { type = vim.fn.mode() }) -- Trim whitespace on each line and concatenate. - do_open(table.concat(vim.iter(lines):map(vim.trim):totable())) + local err = do_open(table.concat(vim.iter(lines):map(vim.trim):totable())) + if err then + vim.notify(err, vim.log.levels.ERROR) + end end, { desc = gx_desc }) end + + --- Default maps for built-in commenting. + --- + --- See |gc-default| and |gcc-default|. + do + local operator_rhs = function() + return require('vim._comment').operator() + end + vim.keymap.set({ 'n', 'x' }, 'gc', operator_rhs, { expr = true, desc = 'Toggle comment' }) + + local line_rhs = function() + return require('vim._comment').operator() .. '_' + end + vim.keymap.set('n', 'gcc', line_rhs, { expr = true, desc = 'Toggle comment line' }) + + local textobject_rhs = function() + require('vim._comment').textobject() + end + vim.keymap.set({ 'o' }, 'gc', textobject_rhs, { desc = 'Comment textobject' }) + end + + --- Default maps for LSP functions. + --- + --- These are mapped unconditionally to avoid different behavior depending on whether an LSP + --- client is attached. If no client is attached, or if a server does not support a capability, an + --- error message is displayed rather than exhibiting different behavior. + --- + --- See |grr|, |grn|, |gra|, |i_CTRL-S|. + do + vim.keymap.set('n', 'grn', function() + vim.lsp.buf.rename() + end, { desc = 'vim.lsp.buf.rename()' }) + + vim.keymap.set({ 'n', 'x' }, 'gra', function() + vim.lsp.buf.code_action() + end, { desc = 'vim.lsp.buf.code_action()' }) + + vim.keymap.set('n', 'grr', function() + vim.lsp.buf.references() + end, { desc = 'vim.lsp.buf.references()' }) + + vim.keymap.set('i', '<C-S>', function() + vim.lsp.buf.signature_help() + end, { desc = 'vim.lsp.buf.signature_help()' }) + end + + --- Map [d and ]d to move to the previous/next diagnostic. Map <C-W>d to open a floating window + --- for the diagnostic under the cursor. + --- + --- See |[d-default|, |]d-default|, and |CTRL-W_d-default|. + do + vim.keymap.set('n', ']d', function() + vim.diagnostic.goto_next({ float = false }) + end, { desc = 'Jump to the next diagnostic' }) + + vim.keymap.set('n', '[d', function() + vim.diagnostic.goto_prev({ float = false }) + end, { desc = 'Jump to the previous diagnostic' }) + + vim.keymap.set('n', '<C-W>d', function() + vim.diagnostic.open_float() + end, { desc = 'Show diagnostics under the cursor' }) + + vim.keymap.set( + 'n', + '<C-W><C-D>', + '<C-W>d', + { remap = true, desc = 'Show diagnostics under the cursor' } + ) + end end --- Default menus @@ -93,7 +205,6 @@ do --- Right click popup menu -- TODO VimScript, no l10n vim.cmd([[ - aunmenu * vnoremenu PopUp.Cut "+x vnoremenu PopUp.Copy "+y anoremenu PopUp.Paste "+gP @@ -102,6 +213,7 @@ do nnoremenu PopUp.Select\ All ggVG vnoremenu PopUp.Select\ All gg0oG$ inoremenu PopUp.Select\ All <C-Home><C-O>VG + anoremenu PopUp.Inspect <Cmd>Inspect<CR> anoremenu PopUp.-1- <Nop> anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR> ]]) @@ -128,7 +240,7 @@ do end local info = vim.api.nvim_get_chan_info(vim.bo[args.buf].channel) local argv = info.argv or {} - if #argv == 1 and argv[1] == vim.o.shell then + if table.concat(argv, ' ') == vim.o.shell then vim.api.nvim_buf_delete(args.buf, { force = true }) end end, @@ -136,8 +248,9 @@ do vim.api.nvim_create_autocmd('TermRequest', { group = nvim_terminal_augroup, - desc = 'Respond to OSC foreground/background color requests', + desc = 'Handles OSC foreground/background color requests', callback = function(args) + --- @type integer local channel = vim.bo[args.buf].channel if channel == 0 then return @@ -181,232 +294,146 @@ do return end vim.v.swapchoice = 'e' -- Choose "(E)dit". - vim.notify(('W325: Ignoring swapfile from Nvim process %d'):format(info.pid)) + vim.notify( + ('W325: Ignoring swapfile from Nvim process %d'):format(info.pid), + vim.log.levels.WARN + ) 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 - 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 digi |