From 08847a9ea15a50aba041ee621d71b9884f5fea97 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 8 Nov 2023 09:33:37 -0600 Subject: refactor: move defaults into separate module (#25929) Move default mappings and autocommands into a separate module and add comments and docstrings to document each of the defaults. --- runtime/lua/vim/_defaults.lua | 166 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 runtime/lua/vim/_defaults.lua (limited to 'runtime/lua/vim/_defaults.lua') diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua new file mode 100644 index 0000000000..c1b50e6950 --- /dev/null +++ b/runtime/lua/vim/_defaults.lua @@ -0,0 +1,166 @@ +--- Default mappings +do + --- Default maps for * and # in visual mode. + --- + --- See |v_star-default| and |v_#-default| + do + local function region_chunks(region) + local chunks = {} + local maxcol = vim.v.maxcol + for line, cols in vim.spairs(region) do + local endcol = cols[2] == maxcol and -1 or cols[2] + local chunk = vim.api.nvim_buf_get_text(0, line, cols[1], line, endcol, {})[1] + table.insert(chunks, chunk) + end + return chunks + end + + local function _visual_search(cmd) + assert(cmd == '/' or cmd == '?') + local region = vim.region( + 0, + '.', + 'v', + vim.api.nvim_get_mode().mode:sub(1, 1), + vim.o.selection == 'inclusive' + ) + local chunks = region_chunks(region) + 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' }) + + --- Map |gx| to call |vim.ui.open| on the identifier under the cursor + do + -- TODO: use vim.region() when it lands... #13896 #16843 + local function get_visual_selection() + local save_a = vim.fn.getreginfo('a') + vim.cmd([[norm! "ay]]) + local selection = vim.fn.getreg('a', 1) + vim.fn.setreg('a', save_a) + return selection + end + + local function do_open(uri) + local _, err = vim.ui.open(uri) + if err then + vim.notify(err, vim.log.levels.ERROR) + end + 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('')) + end, { desc = gx_desc }) + vim.keymap.set({ 'x' }, 'gx', function() + do_open(get_visual_selection()) + end, { desc = gx_desc }) + end +end + +--- Default menus +do + --- Right click popup menu + -- TODO VimScript, no l10n + vim.cmd([[ + aunmenu * + 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.-1- + anoremenu PopUp.How-to\ disable\ mouse help disable-mouse + ]]) +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, + 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 + 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 + vim.cmd({ cmd = 'bdelete', args = { args.buf }, bang = true }) + end + end + 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)) + end, + }) +end -- cgit From ab102f188e86bdbfce1d4de2ef633092a906e8fe Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 6 Nov 2023 15:46:44 -0600 Subject: refactor: move background color detection into Lua --- runtime/lua/vim/_defaults.lua | 130 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) (limited to 'runtime/lua/vim/_defaults.lua') diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index c1b50e6950..0d7b4f1884 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -164,3 +164,133 @@ do 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) + local len = #c + assert(len > 0 and len <= 4, 'Invalid hex color string') + if not c:match('^0x') then + c = string.format('0x%s', c) + end + + local max = tonumber(string.format('0x%s', string.rep('f', len))) + return tonumber(c) / 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 tty = false + for _, ui in ipairs(vim.api.nvim_list_uis()) do + if ui.chan == 1 and ui.stdout_tty then + tty = true + break + end + end + + if tty then + local timer = assert(vim.uv.new_timer()) + + local id = vim.api.nvim_create_autocmd('TermResponse', { + nested = true, + callback = function(args) + if vim.api.nvim_get_option_info2('background', {}).was_set then + -- Don't do anything if 'background' is already set + timer:stop() + timer:close() + return true + end + + 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' + if bg ~= vim.o.background then + vim.o.background = bg + end + end + + timer:stop() + timer:close() + + return true + end + end, + }) + + io.stdout:write('\027]11;?\027\\') + + timer:start(1000, 0, function() + -- No response received. Delete the autocommand + vim.schedule(function() + -- Suppress error if autocommand has already been deleted + pcall(vim.api.nvim_del_autocmd, id) + end) + timer:close() + end) + end +end -- cgit From ba58c6f8a44c9c37e97fce1d802dc5b5defceb3d Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:02:57 -0600 Subject: fix: only attempt to close timer if not already closing (#26047) This fixes an error that can occur in certain pathological cases when the autocommand fires at just the right time such that it attempts to close the timer after the timer has already exited, but before the scheduled callback has fired. We now let the timer continue to run even when the autocommand deletes itself to avoid having to repeat the cleanup code multiple times. There is no harm in letting the timer execute if the autocommand does not exist, as the pcall will catch the error. --- runtime/lua/vim/_defaults.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/_defaults.lua') diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 0d7b4f1884..870603c9f3 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -254,8 +254,6 @@ do callback = function(args) if vim.api.nvim_get_option_info2('background', {}).was_set then -- Don't do anything if 'background' is already set - timer:stop() - timer:close() return true end @@ -274,9 +272,6 @@ do end end - timer:stop() - timer:close() - return true end end, @@ -290,7 +285,10 @@ do -- Suppress error if autocommand has already been deleted pcall(vim.api.nvim_del_autocmd, id) end) - timer:close() + + if not timer:is_closing() then + timer:close() + end end) end end -- cgit From 2f64546dc1cf0816b032af2ce7ded72ce4e581b3 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:25:50 -0600 Subject: refactor: use optional base argument of tonumber (#26070) --- runtime/lua/vim/_defaults.lua | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/_defaults.lua') diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 870603c9f3..09d6d43e7a 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -190,14 +190,17 @@ do --- @param c string Color as a string of hex chars --- @return number? Intensity of the color local function parsecolor(c) - local len = #c - assert(len > 0 and len <= 4, 'Invalid hex color string') - if not c:match('^0x') then - c = string.format('0x%s', c) + if #c == 0 or #c > 4 then + return nil end - local max = tonumber(string.format('0x%s', string.rep('f', len))) - return tonumber(c) / max + 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 -- cgit From 9b4b23493d6976613fc3e8b561c34bb4b808399c Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:43:11 -0600 Subject: fix(defaults): wait until VimEnter to set background (#26284) The OptionSet autocommand does not fire until Vim has finished starting, so setting 'background' before the VimEnter event would not fire the OptionSet event. The prior implementation also waited until VimEnter to set 'background', so this was a regression introduced when moving background detection into Lua. --- runtime/lua/vim/_defaults.lua | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim/_defaults.lua') diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 09d6d43e7a..cc872dea83 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -252,14 +252,33 @@ do if tty then local timer = assert(vim.uv.new_timer()) + ---@param bg string New value of the 'background' option + local function setbg(bg) + if vim.api.nvim_get_option_info2('background', {}).was_set then + -- Don't do anything if 'background' is already set + return + end + + -- Wait until Nvim is finished starting to set 'background' to ensure the + -- OptionSet event fires. + if vim.v.vim_did_enter == 1 then + if vim.o.background ~= bg then + vim.o.background = bg + end + else + vim.api.nvim_create_autocmd('VimEnter', { + once = true, + nested = true, + callback = function() + setbg(bg) + end, + }) + end + end + local id = vim.api.nvim_create_autocmd('TermResponse', { nested = true, callback = function(args) - if vim.api.nvim_get_option_info2('background', {}).was_set then - -- Don't do anything if 'background' is already set - return true - end - local resp = args.data ---@type string local r, g, b = parseosc11(resp) if r and g and b then @@ -270,9 +289,7 @@ do 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' - if bg ~= vim.o.background then - vim.o.background = bg - end + setbg(bg) end return true -- cgit