diff options
Diffstat (limited to 'runtime/lua/vim')
79 files changed, 41231 insertions, 5684 deletions
diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua index 3e370c0a84..5ed60ca8ab 100644 --- a/runtime/lua/vim/F.lua +++ b/runtime/lua/vim/F.lua @@ -1,18 +1,30 @@ local F = {} ---- Returns {a} if it is not nil, otherwise returns {b}. +--- Returns the first argument which is not nil. --- ----@generic A ----@generic B +--- If all arguments are nil, returns nil. --- ----@param a A ----@param b B ----@return A | B -function F.if_nil(a, b) - if a == nil then - return b +--- Examples: +--- +--- ```lua +--- local a = nil +--- local b = nil +--- local c = 42 +--- local d = true +--- assert(vim.F.if_nil(a, b, c, d) == 42) +--- ``` +--- +---@param ... any +---@return any +function F.if_nil(...) + local nargs = select('#', ...) + for i = 1, nargs do + local v = select(i, ...) + if v ~= nil then + return v + end end - return a + return nil end -- Use in combination with pcall diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua new file mode 100644 index 0000000000..cc872dea83 --- /dev/null +++ b/runtime/lua/vim/_defaults.lua @@ -0,0 +1,314 @@ +--- 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! <C-L> to prevent inserting raw <C-L> when using i_<C-O>. #17473 + --- + --- See |CTRL-L-default| + vim.keymap.set('n', '<C-L>', '<Cmd>nohlsearch<Bar>diffupdate<Bar>normal! <C-L><CR>', { + 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', '<C-U>', '<C-G>u<C-U>', { desc = ':help i_CTRL-U-default' }) + vim.keymap.set('i', '<C-W>', '<C-G>u<C-W>', { desc = ':help i_CTRL-W-default' }) + + --- Use the same flags as the previous substitution with &. + --- + --- Use : instead of <Cmd> so that ranges are supported. #19365 + --- + --- See |&-default| + vim.keymap.set('n', '&', ':&&<CR>', { 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('<cfile>')) + 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 <C-Home><C-O>VG + anoremenu PopUp.-1- <Nop> + anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR> + ]]) +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(\"<amatch>\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'cwd': expand(get(matchlist(expand(\"<amatch>\"), '\\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 + +--- 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:<red>/<green>/<blue> + --- + --- or + --- + --- OSC 11 ; rgba:<red>/<green>/<blue>/<alpha> + --- + --- where + --- + --- <red>, <green>, <blue>, <alpha> := 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()) + + ---@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) + 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' + setbg(bg) + end + + 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) + + if not timer:is_closing() then + timer:close() + end + end) + end +end diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index da8764fbd4..6cccbe8313 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -3,7 +3,7 @@ -- Lua code lives in one of three places: -- 1. runtime/lua/vim/ (the runtime): For "nice to have" features, e.g. the -- `inspect` and `lpeg` modules. --- 2. runtime/lua/vim/shared.lua: pure lua functions which always +-- 2. runtime/lua/vim/shared.lua: pure Lua functions which always -- are available. Used in the test runner, as well as worker threads -- and processes launched from Nvim. -- 3. runtime/lua/vim/_editor.lua: Code which directly interacts with @@ -28,6 +28,8 @@ for k, v in pairs({ treesitter = true, filetype = true, + loader = true, + func = true, F = true, lsp = true, highlight = true, @@ -35,12 +37,26 @@ for k, v in pairs({ keymap = true, ui = true, health = true, - fs = true, secure = true, + snippet = true, + _watch = true, }) do vim._submodules[k] = v end +-- There are things which have special rules in vim._init_packages +-- for legacy reasons (uri) or for performance (_inspector). +-- most new things should go into a submodule namespace ( vim.foobar.do_thing() ) +vim._extra = { + uri_from_fname = true, + uri_from_bufnr = true, + uri_to_fname = true, + uri_to_bufnr = true, + show_pos = true, + inspect_pos = true, +} + +--- @private vim.log = { levels = { TRACE = 0, @@ -52,13 +68,78 @@ vim.log = { }, } --- Internal-only until comments in #8107 are addressed. --- Returns: --- {errcode}, {output} -function vim._system(cmd) - local out = vim.fn.system(cmd) - local err = vim.v.shell_error - return err, out +-- TODO(lewis6991): document that the signature is system({cmd}, [{opts},] {on_exit}) +--- Runs a system command or throws an error if {cmd} cannot be run. +--- +--- Examples: +--- +--- ```lua +--- +--- local on_exit = function(obj) +--- print(obj.code) +--- print(obj.signal) +--- print(obj.stdout) +--- print(obj.stderr) +--- end +--- +--- -- Runs asynchronously: +--- vim.system({'echo', 'hello'}, { text = true }, on_exit) +--- +--- -- Runs synchronously: +--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait() +--- -- { code = 0, signal = 0, stdout = 'hello', stderr = '' } +--- +--- ``` +--- +--- See |uv.spawn()| for more details. Note: unlike |uv.spawn()|, vim.system +--- throws an error if {cmd} cannot be run. +--- +--- @param cmd (string[]) Command to execute +--- @param opts (SystemOpts|nil) Options: +--- - cwd: (string) Set the current working directory for the sub-process. +--- - env: table<string,string> Set environment variables for the new process. Inherits the +--- current environment with `NVIM` set to |v:servername|. +--- - clear_env: (boolean) `env` defines the job environment exactly, instead of merging current +--- environment. +--- - stdin: (string|string[]|boolean) If `true`, then a pipe to stdin is opened and can be written +--- to via the `write()` method to SystemObj. If string or string[] then will be written to stdin +--- and closed. Defaults to `false`. +--- - stdout: (boolean|function) +--- Handle output from stdout. When passed as a function must have the signature `fun(err: string, data: string)`. +--- Defaults to `true` +--- - stderr: (boolean|function) +--- Handle output from stderr. When passed as a function must have the signature `fun(err: string, data: string)`. +--- Defaults to `true`. +--- - text: (boolean) Handle stdout and stderr as text. Replaces `\r\n` with `\n`. +--- - timeout: (integer) Run the command with a time limit. Upon timeout the process is sent the +--- TERM signal (15) and the exit code is set to 124. +--- - detach: (boolean) If true, spawn the child process in a detached state - this will make it +--- a process group leader, and will effectively enable the child to keep running after the +--- parent exits. Note that the child process will still keep the parent's event loop alive +--- unless the parent process calls |uv.unref()| on the child's process handle. +--- +--- @param on_exit (function|nil) Called when subprocess exits. When provided, the command runs +--- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait(). +--- +--- @return vim.SystemObj Object with the fields: +--- - pid (integer) Process ID +--- - wait (fun(timeout: integer|nil): SystemCompleted) Wait for the process to complete. Upon +--- timeout the process is sent the KILL signal (9) and the exit code is set to 124. Cannot +--- be called in |api-fast|. +--- - SystemCompleted is an object with the fields: +--- - code: (integer) +--- - signal: (integer) +--- - stdout: (string), nil if stdout argument is passed +--- - stderr: (string), nil if stderr argument is passed +--- - kill (fun(signal: integer|string)) +--- - write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to close the stream. +--- - is_closing (fun(): boolean) +function vim.system(cmd, opts, on_exit) + if type(opts) == 'function' then + on_exit = opts |