--- 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. --- --- See |v_star-default| and |v_#-default| do local function _visual_search(cmd) assert(cmd == '/' or cmd == '?') local chunks = vim.fn.getregion(vim.fn.getpos('.'), vim.fn.getpos('v'), { type = vim.fn.mode() }) local esc_chunks = vim .iter(chunks) :map(function(v) return vim.fn.escape(v, cmd == '/' and [[/\]] or [[?\]]) end) :totable() local esc_pat = table.concat(esc_chunks, [[\n]]) local search_cmd = ([[%s\V%s%s]]):format(cmd, esc_pat, '\n') return '\27' .. search_cmd end vim.keymap.set('x', '*', function() return _visual_search('/') end, { desc = ':help v_star-default', expr = true, silent = true }) vim.keymap.set('x', '#', function() return _visual_search('?') end, { desc = ':help v_#-default', expr = true, silent = true }) end --- Map Y to y$. This mimics the behavior of D and C. See |Y-default| vim.keymap.set('n', 'Y', 'y$', { desc = ':help Y-default' }) --- Use normal! to prevent inserting raw when using i_. #17473 --- --- See |CTRL-L-default| vim.keymap.set('n', '', 'nohlsearchdiffupdatenormal! ', { desc = ':help CTRL-L-default', }) --- Set undo points when deleting text in insert mode. --- --- See |i_CTRL-U-default| and |i_CTRL-W-default| vim.keymap.set('i', '', 'u', { desc = ':help i_CTRL-U-default' }) vim.keymap.set('i', '', 'u', { desc = ':help i_CTRL-W-default' }) --- Use the same flags as the previous substitution with &. --- --- Use : instead of so that ranges are supported. #19365 --- --- See |&-default| vim.keymap.set('n', '&', ':&&', { desc = ':help &-default' }) --- 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', "mode() ==# 'V' ? ':normal! @=reg_recorded()' : 'Q'", { silent = true, expr = true, desc = ':help v_Q-default' } ) vim.keymap.set( 'x', '@', "mode() ==# 'V' ? ':normal! @'.getcharstr().'' : '@'", { silent = true, expr = true, desc = ':help v_@-default' } ) --- Map |gx| to call |vim.ui.open| on the at cursor. do local function do_open(uri) 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() for _, url in ipairs(require('vim.ui')._get_urls()) do local err = do_open(url) if err then vim.notify(err, vim.log.levels.ERROR) end 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. 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|, |gri|, |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('n', 'gri', function() vim.lsp.buf.implementation() end, { desc = 'vim.lsp.buf.implementation()' }) vim.keymap.set('i', '', 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 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.jump({ count = vim.v.count1 }) end, { desc = 'Jump to the next diagnostic in the current buffer' }) vim.keymap.set('n', '[d', function() vim.diagnostic.jump({ count = -vim.v.count1 }) end, { desc = 'Jump to the previous diagnostic in the current buffer' }) vim.keymap.set('n', ']D', function() vim.diagnostic.jump({ count = math.huge, wrap = false }) end, { desc = 'Jump to the last diagnostic in the current buffer' }) vim.keymap.set('n', '[D', function() vim.diagnostic.jump({ count = -math.huge, wrap = false }) end, { desc = 'Jump to the first diagnostic in the current buffer' }) vim.keymap.set('n', 'd', function() vim.diagnostic.open_float() end, { desc = 'Show diagnostics under the cursor' }) vim.keymap.set( 'n', '', 'd', { remap = true, desc = 'Show diagnostics under the cursor' } ) end --- vim-unimpaired style mappings. See: https://github.com/tpope/vim-unimpaired do -- Quickfix mappings vim.keymap.set('n', '[q', function() vim.cmd.cprevious({ count = vim.v.count1 }) end, { desc = ':cprevious', }) vim.keymap.set('n', ']q', function() vim.cmd.cnext({ count = vim.v.count1 }) end, { desc = ':cnext', }) vim.keymap.set('n', '[Q', function() vim.cmd.crewind({ count = vim.v.count ~= 0 and vim.v.count or nil }) end, { desc = ':crewind', }) vim.keymap.set('n', ']Q', function() vim.cmd.clast({ count = vim.v.count ~= 0 and vim.v.count or nil }) end, { desc = ':clast', }) vim.keymap.set('n', '[', function() vim.cmd.cpfile({ count = vim.v.count1 }) end, { desc = ':cpfile', }) vim.keymap.set('n', ']', function() vim.cmd.cnfile({ count = vim.v.count1 }) end, { desc = ':cnfile', }) -- Location list mappings vim.keymap.set('n', '[l', function() vim.cmd.lprevious({ count = vim.v.count1 }) end, { desc = ':lprevious', }) vim.keymap.set('n', ']l', function() vim.cmd.lnext({ count = vim.v.count1 }) end, { desc = ':lnext', }) vim.keymap.set('n', '[L', function() vim.cmd.lrewind({ count = vim.v.count ~= 0 and vim.v.count or nil }) end, { desc = ':lrewind', }) vim.keymap.set('n', ']L', function() vim.cmd.llast({ count = vim.v.count ~= 0 and vim.v.count or nil }) end, { desc = ':llast', }) vim.keymap.set('n', '[', function() vim.cmd.lpfile({ count = vim.v.count1 }) end, { desc = ':lpfile', }) vim.keymap.set('n', ']', function() vim.cmd.lnfile({ count = vim.v.count1 }) end, { desc = ':lnfile', }) -- Argument list vim.keymap.set('n', '[a', function() vim.cmd.previous({ count = vim.v.count1 }) end, { desc = ':previous', }) vim.keymap.set('n', ']a', function() -- count doesn't work with :next, must use range. See #30641. vim.cmd.next({ range = { vim.v.count1 } }) end, { desc = ':next', }) vim.keymap.set('n', '[A', function() if vim.v.count ~= 0 then vim.cmd.argument({ count = vim.v.count }) else vim.cmd.rewind() end end, { desc = ':rewind', }) vim.keymap.set('n', ']A', function() if vim.v.count ~= 0 then vim.cmd.argument({ count = vim.v.count }) else vim.cmd.last() end end, { desc = ':last', }) -- Tags vim.keymap.set('n', '[t', function() -- count doesn't work with :tprevious, must use range. See #30641. vim.cmd.tprevious({ range = { vim.v.count1 } }) end, { desc = ':tprevious' }) vim.keymap.set('n', ']t', function() -- count doesn't work with :tnext, must use range. See #30641. vim.cmd.tnext({ range = { vim.v.count1 } }) end, { desc = ':tnext' }) vim.keymap.set('n', '[T', function() -- count doesn't work with :trewind, must use range. See #30641. vim.cmd.trewind({ range = vim.v.count ~= 0 and { vim.v.count } or nil }) end, { desc = ':trewind' }) vim.keymap.set('n', ']T', function() -- :tlast does not accept a count, so use :trewind if count given if vim.v.count ~= 0 then vim.cmd.trewind({ range = { vim.v.count } }) else vim.cmd.tlast() end end, { desc = ':tlast' }) vim.keymap.set('n', '[', function() -- count doesn't work with :ptprevious, must use range. See #30641. vim.cmd.ptprevious({ range = { vim.v.count1 } }) end, { desc = ' :ptprevious' }) vim.keymap.set('n', ']', function() -- count doesn't work with :ptnext, must use range. See #30641. vim.cmd.ptnext({ range = { vim.v.count1 } }) end, { desc = ':ptnext' }) -- Buffers vim.keymap.set('n', '[b', function() vim.cmd.bprevious({ count = vim.v.count1 }) end, { desc = ':bprevious' }) vim.keymap.set('n', ']b', function() vim.cmd.bnext({ count = vim.v.count1 }) end, { desc = ':bnext' }) vim.keymap.set('n', '[B', function() if vim.v.count ~= 0 then vim.cmd.buffer({ count = vim.v.count }) else vim.cmd.brewind() end end, { desc = ':brewind' }) vim.keymap.set('n', ']B', function() if vim.v.count ~= 0 then vim.cmd.buffer({ count = vim.v.count }) else vim.cmd.blast() end end, { desc = ':blast' }) end end --- Default menus do --- Right click popup menu vim.cmd([[ anoremenu PopUp.Go\ to\ definition lua vim.lsp.buf.definition() amenu PopUp.Open\ in\ web\ browser gx anoremenu PopUp.Inspect Inspect anoremenu PopUp.-1- vnoremenu PopUp.Cut "+x vnoremenu PopUp.Copy "+y anoremenu PopUp.Paste "+gP vnoremenu PopUp.Paste "+P vnoremenu PopUp.Delete "_x nnoremenu PopUp.Select\ All ggVG vnoremenu PopUp.Select\ All gg0oG$ inoremenu PopUp.Select\ All VG anoremenu PopUp.-2- anoremenu PopUp.How-to\ disable\ mouse help disable-mouse ]]) local function enable_ctx_menu(ctx) vim.cmd([[ amenu disable PopUp.Go\ to\ definition amenu disable PopUp.Open\ in\ web\ browser ]]) if ctx == 'url' then vim.cmd([[amenu enable PopUp.Open\ in\ web\ browser]]) elseif ctx == 'lsp' then vim.cmd([[anoremenu enable PopUp.Go\ to\ definition]]) end end local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim_popupmenu', {}) vim.api.nvim_create_autocmd('MenuPopup', { pattern = '*', group = nvim_popupmenu_augroup, desc = 'Mouse popup menu', -- nested = true, callback = function() local urls = require('vim.ui')._get_urls() local url = vim.startswith(urls[1], 'http') local ctx = url and 'url' or (vim.lsp.get_clients({ bufnr = 0 })[1] and 'lsp' or nil) enable_ctx_menu(ctx) end, }) end --- Default autocommands. See |default-autocmds| do local nvim_terminal_augroup = vim.api.nvim_create_augroup('nvim_terminal', {}) vim.api.nvim_create_autocmd('BufReadCmd', { pattern = 'term://*', group = nvim_terminal_augroup, desc = 'Treat term:// buffers as terminal buffers', nested = true, command = "if !exists('b:term_title')|call termopen(matchstr(expand(\"\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'cwd': expand(get(matchlist(expand(\"\"), '\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))})", }) vim.api.nvim_create_autocmd({ 'TermClose' }, { group = nvim_terminal_augroup, nested = true, desc = 'Automatically close terminal buffers when started with no arguments and exiting without an error', callback = function(args) if vim.v.event.status ~= 0 then return end local info = vim.api.nvim_get_chan_info(vim.bo[args.buf].channel) local argv = info.argv or {} if table.concat(argv, ' ') == vim.o.shell then vim.api.nvim_buf_delete(args.buf, { force = true }) end end, }) vim.api.nvim_create_autocmd('TermRequest', { group = nvim_terminal_augroup, desc = 'Handles OSC foreground/background color requests', callback = function(args) --- @type integer local channel = vim.bo[args.buf].channel if channel == 0 then return end local fg_request = args.data == '\027]10;?' local bg_request = args.data == '\027]11;?' if fg_request or bg_request then -- WARN: This does not return the actual foreground/background color, -- but rather returns: -- - fg=white/bg=black when Nvim option 'background' is 'dark' -- - fg=black/bg=white when Nvim option 'background' is 'light' local red, green, blue = 0, 0, 0 local bg_option_dark = vim.o.background == 'dark' if (fg_request and bg_option_dark) or (bg_request and not bg_option_dark) then red, green, blue = 65535, 65535, 65535 end local command = fg_request and 10 or 11 local data = string.format('\027]%d;rgb:%04x/%04x/%04x\007', command, red, green, blue) vim.api.nvim_chan_send(channel, data) end end, }) vim.api.nvim_create_autocmd('TermOpen', { group = nvim_terminal_augroup, desc = 'Default settings for :terminal buffers', callback = function() vim.bo.modifiable = false vim.bo.undolevels = -1 vim.bo.scrollback = vim.o.scrollback < 0 and 10000 or math.max(1, vim.o.scrollback) vim.bo.textwidth = 0 vim.wo[0][0].wrap = false vim.wo[0][0].list = false -- This is gross. Proper list options support when? local winhl = vim.o.winhighlight if winhl ~= '' then winhl = winhl .. ',' end vim.wo[0][0].winhighlight = winhl .. 'StatusLine:StatusLineTerm,StatusLineNC:StatusLineTermNC' end, }) vim.api.nvim_create_autocmd('CmdwinEnter', { pattern = '[:>]', desc = 'Limit syntax sync to maxlines=1 in the command window', group = vim.api.nvim_create_augroup('nvim_cmdwin', {}), command = 'syntax sync minlines=1 maxlines=1', }) vim.api.nvim_create_autocmd('SwapExists', { pattern = '*', desc = 'Skip the swapfile prompt when the swapfile is owned by a running Nvim process', group = vim.api.nvim_create_augroup('nvim_swapfile', {}), callback = function() local info = vim.fn.swapinfo(vim.v.swapname) local user = vim.uv.os_get_passwd().username local iswin = 1 == vim.fn.has('win32') if info.error or info.pid <= 0 or (not iswin and info.user ~= user) then vim.v.swapchoice = '' -- Show the prompt. return end vim.v.swapchoice = 'e' -- Choose "(E)dit". vim.notify( ('W325: Ignoring swapfile from Nvim process %d'):format(info.pid), vim.log.levels.WARN ) 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, }) 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. --- --- 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 --- 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 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) end return true end end, }) io.stdout:write('\027]11;?\007') 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 --- 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 local colorterm = os.getenv('COLORTERM') if tty.rgb or colorterm == 'truecolor' or colorterm == '24bit' then -- The TUI was able to determine truecolor support or $COLORTERM explicitly indicates -- truecolor support setoption('termguicolors', true) elseif colorterm == nil or colorterm == '' then -- Neither the TUI nor $COLORTERM indicate that truecolor is supported, so query the -- terminal 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 options do --- Default 'grepprg' to ripgrep if available. if vim.fn.executable('rg') == 1 then -- Use -uu to make ripgrep not check ignore files/skip dot-files vim.o.grepprg = 'rg --vimgrep -uu ' vim.o.grepformat = '%f:%l:%c:%m' end end