diff options
Diffstat (limited to 'runtime/lua/vim')
45 files changed, 1091 insertions, 550 deletions
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 0b8a54e957..28f1542f64 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -224,7 +224,7 @@ do local function cmd(opts) local ok, err = pcall(vim.api.nvim_cmd, opts, {}) if not ok then - vim.api.nvim_err_writeln(err:sub(#'Vim:' + 1)) + vim.api.nvim_echo({ { err:sub(#'Vim:' + 1) } }, true, { err = true }) end end @@ -412,7 +412,7 @@ do end end - local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim_popupmenu', {}) + local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim.popupmenu', {}) vim.api.nvim_create_autocmd('MenuPopup', { pattern = '*', group = nvim_popupmenu_augroup, @@ -429,13 +429,13 @@ end --- Default autocommands. See |default-autocmds| do - local nvim_terminal_augroup = vim.api.nvim_create_augroup('nvim_terminal', {}) + 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, ''))})", + command = "if !exists('b:term_title')|call jobstart(matchstr(expand(\"<amatch>\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'term': v:true, 'cwd': expand(get(matchlist(expand(\"<amatch>\"), '\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))})", }) vim.api.nvim_create_autocmd({ 'TermClose' }, { @@ -509,14 +509,14 @@ do 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', {}), + 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', {}), + 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 @@ -543,7 +543,7 @@ do end if tty then - local group = vim.api.nvim_create_augroup('nvim_tty', {}) + 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. diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 44f17b3f85..4b28b63746 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -58,6 +58,7 @@ vim._extra = { --- @private vim.log = { + --- @enum vim.log.levels levels = { TRACE = 0, DEBUG = 1, @@ -92,7 +93,7 @@ local utfs = { --- --- -- Runs synchronously: --- local obj = vim.system({'echo', 'hello'}, { text = true }):wait() ---- -- { code = 0, signal = 0, stdout = 'hello', stderr = '' } +--- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' } --- --- ``` --- @@ -390,7 +391,7 @@ end local VIM_CMD_ARG_MAX = 20 ---- Executes Vim script commands. +--- Executes Vimscript (|Ex-commands|). --- --- Note that `vim.cmd` can be indexed with a command name to return a callable function to the --- command. @@ -425,7 +426,7 @@ local VIM_CMD_ARG_MAX = 20 --- ``` --- ---@param command string|table Command(s) to execute. ---- If a string, executes multiple lines of Vim script at once. In this +--- If a string, executes multiple lines of Vimscript at once. In this --- case, it is an alias to |nvim_exec2()|, where `opts.output` is set --- to false. Thus it works identical to |:source|. --- If a table, executes a single command. In this case, it is an alias @@ -620,13 +621,8 @@ end ---@param opts table|nil Optional parameters. Unused by default. ---@diagnostic disable-next-line: unused-local function vim.notify(msg, level, opts) -- luacheck: no unused args - if level == vim.log.levels.ERROR then - vim.api.nvim_err_writeln(msg) - elseif level == vim.log.levels.WARN then - vim.api.nvim_echo({ { msg, 'WarningMsg' } }, true, {}) - else - vim.api.nvim_echo({ { msg } }, true, {}) - end + local chunks = { { msg, level == vim.log.levels.WARN and 'WarningMsg' or nil } } + vim.api.nvim_echo(chunks, true, { err = level == vim.log.levels.ERROR }) end do diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index b2385197bd..670e867c1e 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -272,12 +272,12 @@ function vim.api.nvim_buf_attach(buffer, send_buffer, opts) end --- This temporarily switches current buffer to "buffer". --- If the current window already shows "buffer", the window is not switched. --- If a window inside the current tabpage (including a float) already shows the ---- buffer, then one of these windows will be set as current window temporarily. +--- buffer, then one of those windows will be set as current window temporarily. --- Otherwise a temporary scratch window (called the "autocmd window" for --- historical reasons) will be used. --- --- This is useful e.g. to call Vimscript functions that only work with the ---- current buffer/window currently, like `termopen()`. +--- current buffer/window currently, like `jobstart(…, {'term': v:true})`. --- --- @param buffer integer Buffer handle, or 0 for current buffer --- @param fun function Function to call inside the buffer (currently Lua callable @@ -452,7 +452,7 @@ function vim.api.nvim_buf_get_extmarks(buffer, ns_id, start, end_, opts) end --- --- @param buffer integer Buffer handle, or 0 for current buffer --- @param mode string Mode short-name ("n", "i", "v", ...) ---- @return vim.api.keyset.keymap[] # Array of |maparg()|-like dictionaries describing mappings. +--- @return vim.api.keyset.get_keymap[] # Array of |maparg()|-like dictionaries describing mappings. --- The "buffer" key holds the associated buffer handle. function vim.api.nvim_buf_get_keymap(buffer, mode) end @@ -885,10 +885,8 @@ function vim.api.nvim_cmd(cmd, opts) end --- --- On execution error: fails with Vimscript error, updates v:errmsg. --- ---- Prefer using `nvim_cmd()` or `nvim_exec2()` over this. To evaluate multiple lines of Vim script ---- or an Ex command directly, use `nvim_exec2()`. To construct an Ex command using a structured ---- format and then execute it, use `nvim_cmd()`. To modify an Ex command before evaluating it, use ---- `nvim_parse_cmd()` in conjunction with `nvim_cmd()`. +--- Prefer `nvim_cmd()` or `nvim_exec2()` instead. To modify an Ex command in a structured way +--- before executing it, modify the result of `nvim_parse_cmd()` then pass it to `nvim_cmd()`. --- --- @param command string Ex command string function vim.api.nvim_command(command) end @@ -1099,29 +1097,28 @@ function vim.api.nvim_del_user_command(name) end --- @param name string Variable name function vim.api.nvim_del_var(name) end ---- Echo a message. +--- Prints a message given by a list of `[text, hl_group]` "chunks". --- ---- @param chunks any[] A list of `[text, hl_group]` arrays, each representing a ---- text chunk with specified highlight group name or ID. ---- `hl_group` element can be omitted for no highlight. +--- Example: +--- ```lua +--- vim.api.nvim_echo({ { 'chunk1-line1\nchunk1-line2\n' }, { 'chunk2-line1' } }, true, {}) +--- ``` +--- +--- @param chunks any[] List of `[text, hl_group]` pairs, where each is a `text` string highlighted by +--- the (optional) name or ID `hl_group`. --- @param history boolean if true, add to `message-history`. --- @param opts vim.api.keyset.echo_opts Optional parameters. ---- - verbose: Message is printed as a result of 'verbose' option. ---- If Nvim was invoked with -V3log_file, the message will be ---- redirected to the log_file and suppressed from direct output. +--- - err: Treat the message like `:echoerr`. Sets `hl_group` to `hl-ErrorMsg` by default. +--- - verbose: Message is controlled by the 'verbose' option. Nvim invoked with `-V3log` +--- will write the message to the "log" file instead of standard output. function vim.api.nvim_echo(chunks, history, opts) end ---- Writes a message to the Vim error buffer. Does not append "\n", the ---- message is buffered (won't display) until a linefeed is written. ---- ---- @param str string Message +--- @deprecated +--- @param str string function vim.api.nvim_err_write(str) end ---- Writes a message to the Vim error buffer. Appends "\n", so the buffer is ---- flushed (and displayed). ---- ---- @see vim.api.nvim_err_write ---- @param str string Message +--- @deprecated +--- @param str string function vim.api.nvim_err_writeln(str) end --- Evaluates a Vimscript `expression`. Dicts and Lists are recursively expanded. @@ -1281,6 +1278,8 @@ function vim.api.nvim_get_autocmds(opts) end --- Gets information about a channel. --- +--- See `nvim_list_uis()` for an example of how to get channel info. +--- --- @param chan integer channel_id, or 0 for current channel --- @return table<string,any> # Channel info dict with these keys: --- - "id" Channel id. @@ -1298,8 +1297,8 @@ function vim.api.nvim_get_autocmds(opts) end --- "/dev/pts/1". If unknown, the key will still be present if a pty is used (e.g. --- for conpty on Windows). --- - "buffer" (optional) Buffer connected to |terminal| instance. ---- - "client" (optional) Info about the peer (client on the other end of the RPC channel), ---- which it provided via |nvim_set_client_info()|. +--- - "client" (optional) Info about the peer (client on the other end of the channel), as set +--- by |nvim_set_client_info()|. --- function vim.api.nvim_get_chan_info(chan) end @@ -1416,7 +1415,7 @@ function vim.api.nvim_get_hl_ns(opts) end --- Gets a list of global (non-buffer-local) `mapping` definitions. --- --- @param mode string Mode short-name ("n", "i", "v", ...) ---- @return vim.api.keyset.keymap[] # Array of |maparg()|-like dictionaries describing mappings. +--- @return vim.api.keyset.get_keymap[] # Array of |maparg()|-like dictionaries describing mappings. --- The "buffer" key is always zero. function vim.api.nvim_get_keymap(mode) end @@ -1621,6 +1620,14 @@ function vim.api.nvim_list_tabpages() end --- Gets a list of dictionaries representing attached UIs. --- +--- Example: The Nvim builtin `TUI` sets its channel info as described in `startup-tui`. In +--- particular, it sets `client.name` to "nvim-tui". So you can check if the TUI is running by +--- inspecting the client name of each UI: +--- +--- ```lua +--- vim.print(vim.api.nvim_get_chan_info(vim.api.nvim_list_uis()[1].chan).client.name) +--- ``` +--- --- @return any[] # Array of UI dictionaries, each with these keys: --- - "height" Requested height of the UI --- - "width" Requested width of the UI @@ -1640,14 +1647,10 @@ function vim.api.nvim_list_wins() end --- @return any function vim.api.nvim_load_context(dict) end ---- Notify the user with a message ---- ---- Relays the call to vim.notify . By default forwards your message in the ---- echo area but can be overridden to trigger desktop notifications. ---- ---- @param msg string Message to display to the user ---- @param log_level integer The log level ---- @param opts table<string,any> Reserved for future use. +--- @deprecated +--- @param msg string +--- @param log_level integer +--- @param opts table<string,any> --- @return any function vim.api.nvim_notify(msg, log_level, opts) end @@ -1666,7 +1669,8 @@ function vim.api.nvim_notify(msg, log_level, opts) end --- in a virtual terminal having the intended size. --- --- Example: this `TermHl` command can be used to display and highlight raw ANSI termcodes, so you ---- can use Nvim as a "scrollback pager" (for terminals like kitty): [terminal-scrollback-pager]() +--- can use Nvim as a "scrollback pager" (for terminals like kitty): [ansi-colorize]() +--- [terminal-scrollback-pager]() --- --- ```lua --- vim.api.nvim_create_user_command('TermHl', function() @@ -1750,10 +1754,12 @@ function vim.api.nvim_open_term(buffer, opts) end --- @param config vim.api.keyset.win_config Map defining the window configuration. Keys: --- - relative: Sets the window layout to "floating", placed at (row,col) --- coordinates relative to: ---- - "editor" The global editor grid ---- - "win" Window given by the `win` field, or current window. ---- - "cursor" Cursor position in current window. ---- - "mouse" Mouse position +--- - "cursor" Cursor position in current window. +--- - "editor" The global editor grid. +--- - "laststatus" 'laststatus' if present, or last row. +--- - "mouse" Mouse position. +--- - "tabline" Tabline if present, or first row. +--- - "win" Window given by the `win` field, or current window. --- - win: `window-ID` window to split, or relative window when creating a --- float (relative="win"). --- - anchor: Decides which corner of the float to place at (row,col): @@ -1861,10 +1867,8 @@ function vim.api.nvim_open_term(buffer, opts) end --- @return integer # Window handle, or 0 on error function vim.api.nvim_open_win(buffer, enter, config) end ---- Writes a message to the Vim output buffer. Does not append "\n", the ---- message is buffered (won't display) until a linefeed is written. ---- ---- @param str string Message +--- @deprecated +--- @param str string function vim.api.nvim_out_write(str) end --- Parse command line. @@ -2136,8 +2140,8 @@ function vim.api.nvim_set_current_win(window) end --- ``` --- ["start", tick] --- ``` ---- - on_buf: called for each buffer being redrawn (before ---- window callbacks) +--- - on_buf: called for each buffer being redrawn (once per edit, +--- before window callbacks) --- ``` --- ["buf", bufnr, tick] --- ``` diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index e11dddb2d3..98e916115e 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -88,6 +88,7 @@ error('Cannot require a meta file') --- @field pattern? string|string[] --- @class vim.api.keyset.echo_opts +--- @field err? boolean --- @field verbose? boolean --- @class vim.api.keyset.empty @@ -227,10 +228,10 @@ error('Cannot require a meta file') --- @field do_source? boolean --- @class vim.api.keyset.set_decoration_provider ---- @field on_start? fun(_: "start", tick: integer) +--- @field on_start? fun(_: "start", tick: integer): boolean? --- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer) ---- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer) ---- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer) +--- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer): boolean? +--- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer): boolean? --- @field on_end? fun(_: "end", tick: integer) --- @field _on_hl_def? fun(_: "hl_def") --- @field _on_spell_nav? fun(_: "spell_nav") diff --git a/runtime/lua/vim/_meta/api_keysets_extra.lua b/runtime/lua/vim/_meta/api_keysets_extra.lua index 806b3e49c0..fbef6fa3bc 100644 --- a/runtime/lua/vim/_meta/api_keysets_extra.lua +++ b/runtime/lua/vim/_meta/api_keysets_extra.lua @@ -173,6 +173,26 @@ error('Cannot require a meta file') --- @field force? true --- @field cterm? vim.api.keyset.hl_info.cterm +--- @class vim.api.keyset.get_keymap +--- @field abbr? 0|1 +--- @field buffer? 0|1 +--- @field callback? function +--- @field desc? string +--- @field expr? 0|1 +--- @field lhs? string +--- @field lhsraw? string +--- @field lhsrawalt? string +--- @field lnum? integer +--- @field mode? string +--- @field mode_bits? integer +--- @field noremap? 0|1 +--- @field nowait? 0|1 +--- @field rhs? string +--- @field script? 0|1 +--- @field scriptversion? integer +--- @field sid? integer +--- @field silent? 0|1 + --- @class vim.api.keyset.get_mode --- @field blocking boolean --- @field mode string diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index b8779b66fe..9fa2e242c4 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -233,9 +233,8 @@ function vim.wait(time, callback, interval, fast_only) end --- {callback} receives event name plus additional parameters. See |ui-popupmenu| --- and the sections below for event format for respective events. --- ---- Callbacks for `msg_show` events are executed in |api-fast| context unless ---- Nvim will wait for input, in which case messages should be shown ---- immediately. +--- Callbacks for `msg_show` events are executed in |api-fast| context; showing +--- the message should be scheduled. --- --- Excessive errors inside the callback will result in forced detachment. --- diff --git a/runtime/lua/vim/_meta/json.lua b/runtime/lua/vim/_meta/json.lua index 1a7e87db9c..0d59de5fa6 100644 --- a/runtime/lua/vim/_meta/json.lua +++ b/runtime/lua/vim/_meta/json.lua @@ -25,18 +25,18 @@ vim.json = {} --- ---@param str string Stringified JSON data. ---@param opts? table<string,any> Options table with keys: ---- - luanil: (table) Table with keys: ---- * object: (boolean) When true, converts `null` in JSON objects ---- to Lua `nil` instead of |vim.NIL|. ---- * array: (boolean) When true, converts `null` in JSON arrays ---- to Lua `nil` instead of |vim.NIL|. +--- - luanil: (table) Table with keys: +--- - object: (boolean) When true, converts `null` in JSON objects +--- to Lua `nil` instead of |vim.NIL|. +--- - array: (boolean) When true, converts `null` in JSON arrays +--- to Lua `nil` instead of |vim.NIL|. ---@return any function vim.json.decode(str, opts) end --- Encodes (or "packs") Lua object {obj} as JSON in a Lua string. ---@param obj any ---@param opts? table<string,any> Options table with keys: ---- - escape_slash: (boolean) (default false) When true, escapes `/` ---- character in JSON strings +--- - escape_slash: (boolean) (default false) Escape slash +--- characters "/" in string values. ---@return string function vim.json.encode(obj, opts) end diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 63bf0df5f6..107b1ffdfb 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -52,7 +52,7 @@ vim.go.ari = vim.go.allowrevins --- set to one of CJK locales. See Unicode Standard Annex #11 --- (https://www.unicode.org/reports/tr11). --- ---- @type string +--- @type 'single'|'double' vim.o.ambiwidth = "single" vim.o.ambw = vim.o.ambiwidth vim.go.ambiwidth = vim.o.ambiwidth @@ -208,7 +208,7 @@ vim.go.awa = vim.go.autowriteall --- will change. To use other settings, place ":highlight" commands AFTER --- the setting of the 'background' option. --- ---- @type string +--- @type 'light'|'dark' vim.o.background = "dark" vim.o.bg = vim.o.background vim.go.background = vim.o.background @@ -595,7 +595,7 @@ vim.wo.briopt = vim.wo.breakindentopt --- This option is used together with 'buftype' and 'swapfile' to specify --- special kinds of buffers. See `special-buffers`. --- ---- @type string +--- @type ''|'hide'|'unload'|'delete'|'wipe' vim.o.bufhidden = "" vim.o.bh = vim.o.bufhidden vim.bo.bufhidden = vim.o.bufhidden @@ -658,7 +658,7 @@ vim.bo.bl = vim.bo.buflisted --- without saving. For writing there must be matching `BufWriteCmd|, --- |FileWriteCmd` or `FileAppendCmd` autocommands. --- ---- @type string +--- @type ''|'acwrite'|'help'|'nofile'|'nowrite'|'quickfix'|'terminal'|'prompt' vim.o.buftype = "" vim.o.bt = vim.o.buftype vim.bo.buftype = vim.o.buftype @@ -1087,8 +1087,8 @@ vim.go.cia = vim.go.completeitemalign --- "menu" or "menuone". No effect if "longest" is present. --- --- noselect Same as "noinsert", except that no menu item is ---- pre-selected. If both "noinsert" and "noselect" are present, ---- "noselect" has precedence. +--- pre-selected. If both "noinsert" and "noselect" are +--- present, "noselect" has precedence. --- --- fuzzy Enable `fuzzy-matching` for completion candidates. This --- allows for more flexible and intuitive matching, where @@ -1118,7 +1118,7 @@ vim.go.cot = vim.go.completeopt --- For Insert mode completion the buffer-local value is used. For --- command line completion the global value is used. --- ---- @type string +--- @type ''|'slash'|'backslash' vim.o.completeslash = "" vim.o.csl = vim.o.completeslash vim.bo.completeslash = vim.o.completeslash @@ -1824,7 +1824,7 @@ vim.go.dy = vim.go.display --- hor horizontally, height of windows is not affected --- both width and height of windows is affected --- ---- @type string +--- @type 'both'|'ver'|'hor' vim.o.eadirection = "both" vim.o.ead = vim.o.eadirection vim.go.eadirection = vim.o.eadirection @@ -2126,7 +2126,7 @@ vim.go.fencs = vim.go.fileencodings --- option is set, because the file would be different when written. --- This option cannot be changed when 'modifiable' is off. --- ---- @type string +--- @type 'unix'|'dos'|'mac' vim.o.fileformat = "unix" vim.o.ff = vim.o.fileformat vim.bo.fileformat = vim.o.fileformat @@ -2382,7 +2382,7 @@ vim.go.fcl = vim.go.foldclose --- "[1-9]": to display a fixed number of columns --- See `folding`. --- ---- @type string +--- @type 'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9' vim.o.foldcolumn = "0" vim.o.fdc = vim.o.foldcolumn vim.wo.foldcolumn = vim.o.foldcolumn @@ -2479,7 +2479,7 @@ vim.wo.fmr = vim.wo.foldmarker --- `fold-syntax` syntax Syntax highlighting items specify folds. --- `fold-diff` diff Fold text that is not changed. --- ---- @type string +--- @type 'manual'|'expr'|'marker'|'indent'|'syntax'|'diff' vim.o.foldmethod = "manual" vim.o.fdm = vim.o.foldmethod vim.wo.foldmethod = vim.o.foldmethod @@ -2783,6 +2783,7 @@ vim.go.gp = vim.go.grepprg --- ci Command-line Insert mode --- cr Command-line Replace mode --- sm showmatch in Insert mode +--- t Terminal mode --- a all modes --- The argument-list is a dash separated list of these arguments: --- hor{N} horizontal bar, {N} percent of the character height @@ -2802,7 +2803,8 @@ vim.go.gp = vim.go.grepprg --- ```vim --- set guicursor=n:blinkon0 --- ``` ---- - Default is "blinkon0" for each mode. +--- +--- Default is "blinkon0" for each mode. --- {group-name} --- Highlight group that decides the color and font of the --- cursor. @@ -2848,7 +2850,7 @@ vim.go.gp = vim.go.grepprg --- --- --- @type string -vim.o.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20" +vim.o.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20,t:block-blinkon500-blinkoff500-TermCursor" vim.o.gcr = vim.o.guicursor vim.go.guicursor = vim.o.guicursor vim.go.gcr = vim.go.guicursor @@ -3142,7 +3144,7 @@ vim.bo.ims = vim.bo.imsearch --- 'redrawtime') then 'inccommand' is automatically disabled until --- `Command-line-mode` is done. --- ---- @type string +--- @type 'nosplit'|'split'|'' vim.o.inccommand = "nosplit" vim.o.icm = vim.o.inccommand vim.go.inccommand = vim.o.inccommand @@ -4352,7 +4354,7 @@ vim.go.mh = vim.go.mousehide --- "g<LeftMouse>" is "<C-LeftMouse> (jump to tag under mouse click) --- "g<RightMouse>" is "<C-RightMouse> ("CTRL-T") --- ---- @type string +--- @type 'extend'|'popup'|'popup_setpos' vim.o.mousemodel = "popup_setpos" vim.o.mousem = vim.o.mousemodel vim.go.mousemodel = vim.o.mousemodel @@ -4843,8 +4845,8 @@ vim.go.redrawdebug = vim.o.redrawdebug vim.go.rdb = vim.go.redrawdebug --- Time in milliseconds for redrawing the display. Applies to ---- 'hlsearch', 'inccommand', `:match` highlighting and syntax ---- highlighting. +--- 'hlsearch', 'inccommand', `:match` highlighting, syntax highlighting, +--- and async `LanguageTree:parse()`. --- When redrawing takes more than this many milliseconds no further --- matches will be highlighted. --- For syntax highlighting the time applies per window. When over the @@ -5220,7 +5222,7 @@ vim.go.sect = vim.go.sections --- backwards, you cannot include the last character of a line, when --- starting in Normal mode and 'virtualedit' empty. --- ---- @type string +--- @type 'inclusive'|'exclusive'|'old' vim.o.selection = "inclusive" vim.o.sel = vim.o.selection vim.go.selection = vim.o.selection @@ -5786,7 +5788,7 @@ vim.go.sc = vim.go.showcmd --- place the text. Without a custom 'statusline' or 'tabline' it will be --- displayed in a convenient location. --- ---- @type string +--- @type 'last'|'statusline'|'tabline' vim.o.showcmdloc = "last" vim.o.sloc = vim.o.showcmdloc vim.go.showcmdloc = vim.o.showcmdloc @@ -5918,7 +5920,7 @@ vim.go.siso = vim.go.sidescrolloff --- "number" display signs in the 'number' column. If the number --- column is not present, then behaves like "auto". --- ---- @type string +--- @type 'yes'|'no'|'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'yes:1'|'yes:2'|'yes:3'|'yes:4'|'yes:5'|'yes:6'|'yes:7'|'yes:8'|'yes:9'|'number' vim.o.signcolumn = "auto" vim.o.scl = vim.o.signcolumn vim.wo.signcolumn = vim.o.signcolumn @@ -6226,7 +6228,7 @@ vim.go.sb = vim.go.splitbelow --- with the previous cursor position. For "screen", the text cannot always --- be kept on the same screen line when 'wrap' is enabled. --- ---- @type string +--- @type 'cursor'|'screen'|'topline' vim.o.splitkeep = "cursor" vim.o.spk = vim.o.splitkeep vim.go.splitkeep = vim.o.splitkeep @@ -6874,7 +6876,7 @@ vim.go.tbs = vim.go.tagbsearch --- match Match case --- smart Ignore case unless an upper case letter is used --- ---- @type string +--- @type 'followic'|'ignore'|'match'|'followscs'|'smart' vim.o.tagcase = "followic" vim.o.tc = vim.o.tagcase vim.bo.tagcase = vim.o.tagcase @@ -7756,7 +7758,7 @@ vim.go.wop = vim.go.wildoptions --- key is never used for the menu. --- This option is not used for <F10>; on Win32. --- ---- @type string +--- @type 'yes'|'menu'|'no' vim.o.winaltkeys = "menu" vim.o.wak = vim.o.winaltkeys vim.go.winaltkeys = vim.o.winaltkeys diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index cf1beda15f..4b5b276023 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -1023,16 +1023,22 @@ function vim.fn.complete_check() end --- See |complete_info_mode| for the values. --- pum_visible |TRUE| if popup menu is visible. --- See |pumvisible()|. ---- items List of completion matches. Each item is a ---- dictionary containing the entries "word", +--- items List of all completion candidates. Each item +--- is a dictionary containing the entries "word", --- "abbr", "menu", "kind", "info" and "user_data". --- See |complete-items|. +--- matches Same as "items", but only returns items that +--- are matching current query. If both "matches" +--- and "items" are in "what", the returned list +--- will still be named "items", but each item +--- will have an additional "match" field. --- selected Selected item index. First index is zero. --- Index is -1 if no item is selected (showing --- typed text only, or the last completion after --- no item is selected when using the <Up> or --- <Down> keys) ---- inserted Inserted string. [NOT IMPLEMENTED YET] +--- completed Return a dictionary containing the entries of +--- the currently selected index item. --- preview_winid Info floating preview window id. --- preview_bufnr Info floating preview buffer id. --- @@ -3765,6 +3771,20 @@ function vim.fn.getregtype(regname) end --- @return vim.fn.getscriptinfo.ret[] function vim.fn.getscriptinfo(opts) end +--- Returns the current stack trace of Vim scripts. +--- Stack trace is a |List|, of which each item is a |Dictionary| +--- with the following items: +--- funcref The funcref if the stack is at a function, +--- otherwise this item is omitted. +--- event The string of the event description if the +--- stack is at an autocmd event, otherwise this +--- item is omitted. +--- lnum The line number in the script on the stack. +--- filepath The file path of the script on the stack. +--- +--- @return table[] +function vim.fn.getstacktrace() end + --- If {tabnr} is not specified, then information about all the --- tab pages is returned as a |List|. Each List item is a --- |Dictionary|. Otherwise, {tabnr} specifies the tab page @@ -4805,7 +4825,7 @@ function vim.fn.jobresize(job, width, height) end --- @return any function vim.fn.jobsend(...) end ---- Note: Prefer |vim.system()| in Lua (unless using the `pty` option). +--- Note: Prefer |vim.system()| in Lua (unless using `rpc`, `pty`, or `term`). --- --- Spawns {cmd} as a job. --- If {cmd} is a List it runs directly (no 'shell'). @@ -4813,8 +4833,11 @@ function vim.fn.jobsend(...) end --- call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}']) --- <(See |shell-unquoting| for details.) --- ---- Example: >vim ---- call jobstart('nvim -h', {'on_stdout':{j,d,e->append(line('.'),d)}}) +--- Example: start a job and handle its output: >vim +--- call jobstart(['nvim', '-h'], {'on_stdout':{j,d,e->append(line('.'),d)}}) +--- < +--- Example: start a job in a |terminal| connected to the current buffer: >vim +--- call jobstart(['nvim', '-h'], {'term':v:true}) --- < --- Returns |job-id| on success, 0 on invalid arguments (or job --- table is full), -1 if {cmd}[0] or 'shell' is not executable. @@ -4879,6 +4902,10 @@ function vim.fn.jobsend(...) end --- stdin: (string) Either "pipe" (default) to connect the --- job's stdin to a channel or "null" to disconnect --- stdin. +--- term: (boolean) Spawns {cmd} in a new pseudo-terminal session +--- connected to the current (unmodified) buffer. Implies "pty". +--- Default "height" and "width" are set to the current window +--- dimensions. |jobstart()|. Defaults $TERM to "xterm-256color". --- width: (number) Width of the `pty` terminal. --- --- {opts} is passed as |self| dictionary to the callback; the @@ -7511,7 +7538,7 @@ function vim.fn.screenstring(row, col) end --- @param stopline? integer --- @param timeout? integer --- @param skip? string|function ---- @return any +--- @return integer function vim.fn.search(pattern, flags, stopline, timeout, skip) end --- Get or update the last search count, like what is displayed @@ -10168,19 +10195,8 @@ function vim.fn.tanh(expr) end --- @return string function vim.fn.tempname() end ---- Spawns {cmd} in a new pseudo-terminal session connected ---- to the current (unmodified) buffer. Parameters and behavior ---- are the same as |jobstart()| except "pty", "width", "height", ---- and "TERM" are ignored: "height" and "width" are taken from ---- the current window. Note that termopen() implies a "pty" arg ---- to jobstart(), and thus has the implications documented at ---- |jobstart()|. ---- ---- Returns the same values as jobstart(). ---- ---- Terminal environment is initialized as in |jobstart-env|, ---- except $TERM is set to "xterm-256color". Full behavior is ---- described in |terminal|. +--- @deprecated +--- Use |jobstart()| with `{term: v:true}` instead. --- --- @param cmd string|string[] --- @param opts? table diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua index 445da4e02f..c1b8695bbf 100644 --- a/runtime/lua/vim/_meta/vvars.lua +++ b/runtime/lua/vim/_meta/vvars.lua @@ -203,7 +203,8 @@ vim.v.errors = ... vim.v.event = ... --- The value of the exception most recently caught and not ---- finished. See also `v:throwpoint` and `throw-variables`. +--- finished. See also `v:stacktrace`, `v:throwpoint`, and +--- `throw-variables`. --- Example: --- --- ```vim @@ -616,6 +617,13 @@ vim.v.servername = ... --- @type integer vim.v.shell_error = ... +--- The stack trace of the exception most recently caught and +--- not finished. Refer to `getstacktrace()` for the structure of +--- stack trace. See also `v:exception`, `v:throwpoint`, and +--- `throw-variables`. +--- @type table[] +vim.v.stacktrace = ... + --- Last given status message. --- Modifiable (can be set). --- @type string @@ -718,7 +726,7 @@ vim.v.this_session = ... --- The point where the exception most recently caught and not --- finished was thrown. Not set when commands are typed. See ---- also `v:exception` and `throw-variables`. +--- also `v:exception`, `v:stacktrace`, and `throw-variables`. --- Example: --- --- ```vim diff --git a/runtime/lua/vim/_options.lua b/runtime/lua/vim/_options.lua index 77d7054626..8338c5ead7 100644 --- a/runtime/lua/vim/_options.lua +++ b/runtime/lua/vim/_options.lua @@ -229,10 +229,8 @@ end --- global value of a |global-local| option, see |:setglobal|. --- </pre> ---- Get or set |options|. Like `:set`. Invalid key is an error. ---- ---- Note: this works on both buffer-scoped and window-scoped options using the ---- current buffer and window. +--- Get or set |options|. Works like `:set`, so buffer/window-scoped options target the current +--- buffer/window. Invalid key is an error. --- --- Example: --- @@ -770,7 +768,7 @@ end --- --- --- A special interface |vim.opt| exists for conveniently interacting with list- ---- and map-style option from Lua: It allows accessing them as Lua tables and +--- and map-style options from Lua: It allows accessing them as Lua tables and --- offers object-oriented method for adding and removing entries. --- --- Examples: ~ diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index c0a0570e13..157172447a 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -79,7 +79,8 @@ function SystemObj:_timeout(signal) self:kill(signal or SIG.TERM) end -local MAX_TIMEOUT = 2 ^ 31 +-- Use max 32-bit signed int value to avoid overflow on 32-bit systems. #31633 +local MAX_TIMEOUT = 2 ^ 31 - 1 --- @param timeout? integer --- @return vim.SystemCompleted diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index ded7a8f89d..ead75f7d51 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -70,7 +70,7 @@ end --- Use virtual text for diagnostics. If multiple diagnostics are set for a --- namespace, one prefix per diagnostic + the last diagnostic message are --- shown. ---- (default: `true`) +--- (default: `false`) --- @field virtual_text? boolean|vim.diagnostic.Opts.VirtualText|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.VirtualText --- --- Use signs for diagnostics |diagnostic-signs|. @@ -312,7 +312,7 @@ M.severity = { local global_diagnostic_options = { signs = true, underline = true, - virtual_text = true, + virtual_text = false, float = true, update_in_insert = false, severity_sort = false, @@ -356,7 +356,7 @@ local bufnr_and_namespace_cacher_mt = { -- bufnr -> ns -> Diagnostic[] local diagnostic_cache = {} --- @type table<integer,table<integer,vim.Diagnostic[]>> do - local group = api.nvim_create_augroup('DiagnosticBufWipeout', {}) + local group = api.nvim_create_augroup('nvim.diagnostic.buf_wipeout', {}) setmetatable(diagnostic_cache, { --- @param t table<integer,vim.Diagnostic[]> --- @param bufnr integer @@ -871,10 +871,10 @@ local function set_list(loclist, opts) end if open then - if qf_id then + if not loclist then -- First navigate to the diagnostics quickfix list. local nr = vim.fn.getqflist({ id = qf_id, nr = 0 }).nr - api.nvim_command(nr .. 'chistory') + api.nvim_command(('silent %dchistory'):format(nr)) -- Now open the quickfix list. api.nvim_command('botright cwindow') @@ -1395,10 +1395,6 @@ M.handlers.signs = { return end - if opts.signs and opts.signs.severity then - diagnostics = filter_by_severity(opts.signs.severity, diagnostics) - end - -- 10 is the default sign priority when none is explicitly specified local priority = opts.signs and opts.signs.priority or 10 local get_priority = severity_to_extmark_priority(priority, opts) @@ -1406,7 +1402,7 @@ M.handlers.signs = { local ns = M.get_namespace(namespace) if not ns.user_data.sign_ns then ns.user_data.sign_ns = - api.nvim_create_namespace(string.format('%s/diagnostic/signs', ns.name)) + api.nvim_create_namespace(string.format('nvim.%s.diagnostic.signs', ns.name)) end -- Handle legacy diagnostic sign definitions @@ -1501,14 +1497,10 @@ M.handlers.underline = { return end - if opts.underline and opts.underline.severity then - diagnostics = filter_by_severity(opts.underline.severity, diagnostics) - end - local ns = M.get_namespace(namespace) if not ns.user_data.underline_ns then ns.user_data.underline_ns = - api.nvim_create_namespace(string.format('%s/diagnostic/underline', ns.name)) + api.nvim_create_namespace(string.format('nvim.%s.diagnostic.underline', ns.name)) end local underline_ns = ns.user_data.underline_ns @@ -1565,7 +1557,6 @@ M.handlers.virtual_text = { return end - local severity --- @type vim.diagnostic.SeverityFilter? if opts.virtual_text then if opts.virtual_text.format then diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics) @@ -1576,23 +1567,17 @@ M.handlers.virtual_text = { then diagnostics = prefix_source(diagnostics) end - if opts.virtual_text.severity then - severity = opts.virtual_text.severity - end end local ns = M.get_namespace(namespace) if not ns.user_data.virt_text_ns then ns.user_data.virt_text_ns = - api.nvim_create_namespace(string.format('%s/diagnostic/virtual_text', ns.name)) + api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_text', ns.name)) end local virt_text_ns = ns.user_data.virt_text_ns local buffer_line_diagnostics = diagnostic_lines(diagnostics) for line, line_diagnostics in pairs(buffer_line_diagnostics) do - if severity then - line_diagnostics = filter_by_severity(severity, line_diagnostics) - end local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text) if virt_texts then @@ -1797,7 +1782,8 @@ function M.show(namespace, bufnr, diagnostics, opts) for handler_name, handler in pairs(M.handlers) do if handler.show and opts_res[handler_name] then - handler.show(namespace, bufnr, diagnostics, opts_res) + local filtered = filter_by_severity(opts_res[handler_name].severity, diagnostics) + handler.show(namespace, bufnr, filtered, opts_res) end end end diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 83e82392de..efc41269f8 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -353,6 +353,7 @@ local extension = { cql = 'cqlang', crm = 'crm', cr = 'crystal', + cake = 'cs', csx = 'cs', cs = 'cs', csc = 'csc', @@ -504,6 +505,7 @@ local extension = { gdshader = 'gdshader', shader = 'gdshader', ged = 'gedcom', + gel = 'gel', gmi = 'gemtext', gemini = 'gemtext', gift = 'gift', @@ -592,6 +594,7 @@ local extension = { hw = detect.hw, module = detect.hw, pkg = detect.hw, + hy = 'hy', iba = 'ibasic', ibi = 'ibasic', icn = 'icon', @@ -616,6 +619,7 @@ local extension = { janet = 'janet', jav = 'java', java = 'java', + jsh = 'java', jj = 'javacc', jjt = 'javacc', es = 'javascript', @@ -628,7 +632,7 @@ local extension = { clp = 'jess', jgr = 'jgraph', jinja = 'jinja', - jjdescription = 'jj', + jjdescription = 'jjdescription', j73 = 'jovial', jov = 'jovial', jovial = 'jovial', @@ -698,7 +702,6 @@ local extension = { el = 'lisp', lsp = 'lisp', asd = 'lisp', - stsg = 'lisp', lt = 'lite', lite = 'lite', livemd = 'livebook', @@ -734,7 +737,7 @@ local extension = { mk = detect.make, mak = detect.make, page = 'mallard', - map = 'map', + map = detect_line1('^%*+$', 'lnkmap', 'map'), mws = 'maple', mpl = 'maple', mv = 'maple', @@ -1061,16 +1064,17 @@ local extension = { builder = 'ruby', rake = 'ruby', rs = 'rust', + sa = detect.sa, sage = 'sage', sls = 'salt', sas = 'sas', sass = 'sass', - sa = 'sather', sbt = 'sbt', scala = 'scala', ss = 'scheme', scm = 'scheme', sld = 'scheme', + stsg = 'scheme', sce = 'scilab', sci = 'scilab', scss = 'scss', @@ -1093,6 +1097,7 @@ local extension = { la = 'sh', lai = 'sh', mdd = 'sh', + slang = 'shaderslang', sieve = 'sieve', siv = 'sieve', sig = detect.sig, @@ -1334,6 +1339,7 @@ local extension = { xlb = 'xml', xlc = 'xml', xba = 'xml', + slnx = 'xml', xpm = detect_line1('XPM2', 'xpm2', 'xpm'), xpm2 = 'xpm2', xqy = 'xquery', @@ -1389,7 +1395,7 @@ local extension = { txt = detect.txt, xml = detect.xml, y = detect.y, - cmd = detect_line1('^/%*', 'rexx', 'dosbatch'), + cmd = detect.cmd, rul = detect.rul, cpy = detect_line1('^##', 'python', 'cobol'), dsl = detect_line1('^%s*<!', 'dsl', 'structurizr'), @@ -1438,6 +1444,7 @@ local filename = { ['/etc/asound.conf'] = 'alsaconf', ['build.xml'] = 'ant', ['.htaccess'] = 'apache', + APKBUILD = 'apkbuild', ['apt.conf'] = 'aptconf', ['/.aptitude/config'] = 'aptconf', ['=tagging-method'] = 'arch', @@ -1544,6 +1551,8 @@ local filename = { ['filter-rules'] = 'elmfilt', ['exim.conf'] = 'exim', exports = 'exports', + fennelrc = 'fennel', + ['.fennelrc'] = 'fennel', ['.fetchmailrc'] = 'fetchmail', fvSchemes = detect.foam, fvSolution = detect.foam, @@ -1565,6 +1574,12 @@ local filename = { ['.gitmodules'] = 'gitconfig', ['.gitattributes'] = 'gitattributes', ['.gitignore'] = 'gitignore', + ['.ignore'] = 'gitignore', + ['.dockerignore'] = 'gitignore', + ['.fdignore'] = 'gitignore', + ['.npmignore'] = 'gitignore', + ['.rgignore'] = 'gitignore', + ['.vscodeignore'] = 'gitignore', ['gitolite.conf'] = 'gitolite', ['git-rebase-todo'] = 'gitrebase', gkrellmrc = 'gkrellmrc', @@ -1599,6 +1614,7 @@ local filename = { ['/etc/host.conf'] = 'hostconf', ['/etc/hosts.allow'] = 'hostsaccess', ['/etc/hosts.deny'] = 'hostsaccess', + ['.hy-history'] = 'hy', ['hyprland.conf'] = 'hyprlang', ['hyprpaper.conf'] = 'hyprlang', ['hypridle.conf'] = 'hyprlang', @@ -1620,6 +1636,7 @@ local filename = { ['.lintstagedrc'] = 'json', ['deno.lock'] = 'json', ['flake.lock'] = 'json', + ['.swcrc'] = 'json', ['.babelrc'] = 'jsonc', ['.eslintrc'] = 'jsonc', ['.hintrc'] = 'jsonc', @@ -1629,6 +1646,7 @@ local filename = { ['.luaurc'] = 'jsonc', ['.swrc'] = 'jsonc', ['.vsconfig'] = 'jsonc', + ['bun.lock'] = 'jsonc', ['.justfile'] = 'just', ['justfile'] = 'just', ['Justfile'] = 'just', @@ -1774,6 +1792,7 @@ local filename = { ['Rantfile'] = 'ruby', Vagrantfile = 'ruby', ['smb.conf'] = 'samba', + ['.lips_repl_history'] = 'scheme', screenrc = 'screen', ['.screenrc'] = 'screen', ['/etc/sensors3.conf'] = 'sensors', @@ -1795,7 +1814,6 @@ local filename = { ['.kshrc'] = detect.ksh, ['.profile'] = detect.sh, ['/etc/profile'] = detect.sh, - APKBUILD = detect.bash, PKGBUILD = detect.bash, ['.tcshrc'] = detect.tcsh, ['tcsh.login'] = detect.tcsh, @@ -1872,6 +1890,9 @@ local filename = { ['/etc/blkid.tab'] = 'xml', ['/etc/blkid.tab.old'] = 'xml', ['fonts.conf'] = 'xml', + ['Directory.Packages.props'] = 'xml', + ['Directory.Build.props'] = 'xml', + ['Directory.Build.targets'] = 'xml', ['.clangd'] = 'yaml', ['.clang-format'] = 'yaml', ['.clang-tidy'] = 'yaml', @@ -2156,8 +2177,8 @@ local pattern = { ['/gitolite%-admin/conf/'] = starsetf('gitolite'), ['/%.i3/config$'] = 'i3config', ['/i3/config$'] = 'i3config', - ['/supertux2/config$'] = 'lisp', ['/%.mplayer/config$'] = 'mplayerconf', + ['/supertux2/config$'] = 'scheme', ['/neofetch/config%.conf$'] = 'sh', ['/%.ssh/config$'] = 'sshconfig', ['/%.sway/config$'] = 'swayconfig', @@ -2225,6 +2246,7 @@ local pattern = { ['^dictd.*%.conf$'] = 'dictdconf', ['/lxqt/.*%.conf$'] = 'dosini', ['/screengrab/.*%.conf$'] = 'dosini', + ['/%.config/fd/ignore$'] = 'gitignore', ['^${GNUPGHOME}/gpg%.conf$'] = 'gpg', ['/boot/grub/grub%.conf$'] = 'grub', ['/hypr/.*%.conf$'] = 'hyprlang', @@ -2355,6 +2377,8 @@ local pattern = { ['%.html%.m4$'] = 'htmlm4', ['^JAM.*%.'] = starsetf('jam'), ['^Prl.*%.'] = starsetf('jam'), + ['^${HOME}/.*/Code/User/.*%.json$'] = 'jsonc', + ['^${HOME}/.*/VSCodium/User/.*%.json$'] = 'jsonc', ['%.properties_..$'] = 'jproperties', ['%.properties_.._..$'] = 'jproperties', ['%.properties_.._.._'] = starsetf('jproperties'), diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index 4f2fef5b1f..31c88c80bd 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -34,6 +34,12 @@ local matchregex = vim.filetype._matchregex -- can be detected from the first five lines of the file. --- @type vim.filetype.mapfn function M.asm(path, bufnr) + -- tiasm uses `* commment` + local lines = table.concat(getlines(bufnr, 1, 10), '\n') + if findany(lines, { '^%*', '\n%*', 'Texas Instruments Incorporated' }) then + return 'tiasm' + end + local syntax = vim.b[bufnr].asmsyntax if not syntax or syntax == '' then syntax = M.asm_syntax(path, bufnr) @@ -219,6 +225,24 @@ function M.cls(_, bufnr) return 'st' end +--- *.cmd is close to a Batch file, but on OS/2 Rexx files and TI linker command files also use *.cmd. +--- lnk: `/* comment */`, `// comment`, and `--linker-option=value` +--- rexx: `/* comment */`, `-- comment` +--- @type vim.filetype.mapfn +function M.cmd(_, bufnr) + local lines = table.concat(getlines(bufnr, 1, 20)) + if matchregex(lines, [[MEMORY\|SECTIONS\|\%(^\|\n\)--\S\|\%(^\|\n\)//]]) then + return 'lnk' + else + local line1 = getline(bufnr, 1) + if line1:find('^/%*') then + return 'rexx' + else + return 'dosbatch' + end + end +end + --- @type vim.filetype.mapfn function M.conf(path, bufnr) if fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then @@ -733,7 +757,7 @@ function M.html(_, bufnr) if matchregex( line, - [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content\|{{.*}}]] + [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content]] ) then return 'htmlangular' @@ -1406,6 +1430,15 @@ function M.sig(_, bufnr) end end +--- @type vim.filetype.mapfn +function M.sa(_, bufnr) + local lines = table.concat(getlines(bufnr, 1, 4), '\n') + if findany(lines, { '^;', '\n;' }) then + return 'tiasm' + end + return 'sather' +end + -- This function checks the first 25 lines of file extension "sc" to resolve -- detection between scala and SuperCollider --- @type vim.filetype.mapfn diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 2f007d97c3..5940fa4386 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -105,14 +105,23 @@ function M.basename(file) return file:match('/$') and '' or (file:match('[^/]*$')) end ---- Concatenate directories and/or file paths into a single path with normalization ---- (e.g., `"foo/"` and `"bar"` get joined to `"foo/bar"`) +--- Concatenates partial paths (one absolute or relative path followed by zero or more relative +--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are +--- replaced with forward-slashes. +--- +--- Examples: +--- - "foo/", "/bar" => "foo/bar" +--- - Windows: "a\foo\", "\bar" => "a/foo/bar" --- ---@since 12 ---@param ... string ---@return string function M.joinpath(...) - return (table.concat({ ... }, '/'):gsub('//+', '/')) + local path = table.concat({ ... }, '/') + if iswin then + path = path:gsub('\\', '/') + end + return (path:gsub('//+', '/')) end ---@alias Iterator fun(): string?, string? @@ -127,6 +136,7 @@ end --- - skip: (fun(dir_name: string): boolean)|nil Predicate --- to control traversal. Return false to stop searching the current directory. --- Only useful when depth > 1 +--- - follow: boolean|nil Follow symbolic links. (default: true) --- ---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type". --- "name" is the basename of the item relative to {path}. @@ -138,6 +148,7 @@ function M.dir(path, opts) vim.validate('path', path, 'string') vim.validate('depth', opts.depth, 'number', true) vim.validate('skip', opts.skip, 'function', true) + vim.validate('follow', opts.follow, 'boolean', true) path = M.normalize(path) if not opts.depth or opts.depth == 1 then @@ -168,7 +179,9 @@ function M.dir(path, opts) if opts.depth and level < opts.depth - and t == 'directory' + and (t == 'directory' or (t == 'link' and opts.follow ~= false and (vim.uv.fs_stat( + M.joinpath(path, f) + ) or {}).type == 'directory')) and (not opts.skip or opts.skip(f) ~= false) then dirs[#dirs + 1] = { f, level + 1 } @@ -202,6 +215,10 @@ end --- Use `math.huge` to place no limit on the number of matches. --- (default: `1`) --- @field limit? number +--- +--- Follow symbolic links. +--- (default: `true`) +--- @field follow? boolean --- Find files or directories (or other items as specified by `opts.type`) in the given path. --- @@ -225,7 +242,7 @@ end --- --- -- get all files ending with .cpp or .hpp inside lib/ --- local cpp_hpp = vim.fs.find(function(name, path) ---- return name:match('.*%.[ch]pp$') and path:match('[/\\\\]lib$') +--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$') --- end, {limit = math.huge, type = 'file'}) --- ``` --- @@ -235,6 +252,7 @@ end --- If {names} is a function, it is called for each traversed item with args: --- - name: base name of the current item --- - path: full path of the current item +--- --- The function should return `true` if the given item is considered a match. --- ---@param opts vim.fs.find.Opts Optional keyword arguments: @@ -247,6 +265,7 @@ function M.find(names, opts) vim.validate('stop', opts.stop, 'string', true) vim.validate('type', opts.type, 'string', true) vim.validate('limit', opts.limit, 'number', true) + vim.validate('follow', opts.follow, 'boolean', true) if type(names) == 'string' then names = { names } @@ -336,7 +355,14 @@ function M.find(names, opts) end end - if type_ == 'directory' then + if + type_ == 'directory' + or ( + type_ == 'link' + and opts.follow ~= false + and (vim.uv.fs_stat(f) or {}).type == 'directory' + ) + then dirs[#dirs + 1] = f end end @@ -505,6 +531,27 @@ local function path_resolve_dot(path) return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/') end +--- Expand tilde (~) character at the beginning of the path to the user's home directory. +--- +--- @param path string Path to expand. +--- @param sep string|nil Path separator to use. Uses os_sep by default. +--- @return string Expanded path. +local function expand_home(path, sep) + sep = sep or os_sep + + if vim.startswith(path, '~') then + local home = uv.os_homedir() or '~' --- @type string + + if home:sub(-1) == sep then + home = home:sub(1, -2) + end + + path = home .. path:sub(2) + end + + return path +end + --- @class vim.fs.normalize.Opts --- @inlinedoc --- @@ -568,14 +615,8 @@ function M.normalize(path, opts) return '' end - -- Expand ~ to users home directory - if vim.startswith(path, '~') then - local home = uv.os_homedir() or '~' - if home:sub(-1) == os_sep_local then - home = home:sub(1, -2) - end - path = home .. path:sub(2) - end + -- Expand ~ to user's home directory + path = expand_home(path, os_sep_local) -- Expand environment variables if `opts.expand_env` isn't `false` if opts.expand_env == nil or opts.expand_env then @@ -605,8 +646,8 @@ function M.normalize(path, opts) return prefix .. path end - -- Remove extraneous slashes from the prefix - prefix = prefix:gsub('/+', '/') + -- Ensure capital drive and remove extraneous slashes from the prefix + prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/') end if not opts._fast then @@ -679,4 +720,75 @@ function M.rm(path, opts) end end +--- Convert path to an absolute path. A tilde (~) character at the beginning of the path is expanded +--- to the user's home directory. Does not check if the path exists, normalize the path, resolve +--- symlinks or hardlinks (including `.` and `..`), or expand environment variables. If the path is +--- already absolute, it is returned unchanged. Also converts `\` path separators to `/`. +--- +--- @param path string Path +--- @return string Absolute path +function M.abspath(path) + vim.validate('path', path, 'string') + + -- Expand ~ to user's home directory + path = expand_home(path) + + -- Convert path separator to `/` + path = path:gsub(os_sep, '/') + + local prefix = '' + + if iswin then + prefix, path = split_windows_path(path) + end + + if prefix == '//' or vim.startswith(path, '/') then + -- Path is already absolute, do nothing + return prefix .. path + end + + -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory + -- of the drive specified in the path + local cwd = (iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or uv.cwd() + assert(cwd ~= nil) + -- Convert cwd path separator to `/` + cwd = cwd:gsub(os_sep, '/') + + -- Prefix is not needed for expanding relative paths, as `cwd` already contains it. + return M.joinpath(cwd, path) +end + +--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor. +--- +--- Example: +--- +--- ```lua +--- vim.fs.relpath('/var', '/var/lib') -- 'lib' +--- vim.fs.relpath('/var', '/usr/bin') -- nil +--- ``` +--- +--- @param base string +--- @param target string +--- @param opts table? Reserved for future use +--- @return string|nil +function M.relpath(base, target, opts) + vim.validate('base', base, 'string') + vim.validate('target', target, 'string') + vim.validate('opts', opts, 'table', true) + + base = vim.fs.normalize(vim.fs.abspath(base)) + target = vim.fs.normalize(vim.fs.abspath(target)) + if base == target then + return '.' + end + + local prefix = '' + if iswin then + prefix, base = split_windows_path(base) + end + base = prefix .. base .. (base ~= '/' and '/' or '') + + return vim.startswith(target, base) and target:sub(#base + 1) or nil +end + return M diff --git a/runtime/lua/vim/func.lua b/runtime/lua/vim/func.lua index f71659ffb4..fc8fa62c71 100644 --- a/runtime/lua/vim/func.lua +++ b/runtime/lua/vim/func.lua @@ -3,9 +3,6 @@ local M = {} -- TODO(lewis6991): Private for now until: -- - There are other places in the codebase that could benefit from this -- (e.g. LSP), but might require other changes to accommodate. --- - Invalidation of the cache needs to be controllable. Using weak tables --- is an acceptable invalidation policy, but it shouldn't be the only --- one. -- - I don't think the story around `hash` is completely thought out. We -- may be able to have a good default hash by hashing each argument, -- so basically a better 'concat'. @@ -17,6 +14,10 @@ local M = {} --- Internally uses a |lua-weaktable| to cache the results of {fn} meaning the --- cache will be invalidated whenever Lua does garbage collection. --- +--- The cache can also be manually invalidated by calling `:clear()` on the returned object. +--- Calling this function with no arguments clears the entire cache; otherwise, the arguments will +--- be interpreted as function inputs, and only the cache entry at their hash will be cleared. +--- --- The memoized function returns shared references so be wary about --- mutating return values. --- @@ -32,11 +33,12 @@ local M = {} --- first n arguments passed to {fn}. --- --- @param fn F Function to memoize. ---- @param strong? boolean Do not use a weak table +--- @param weak? boolean Use a weak table (default `true`) --- @return F # Memoized version of {fn} --- @nodoc -function M._memoize(hash, fn, strong) - return require('vim.func._memoize')(hash, fn, strong) +function M._memoize(hash, fn, weak) + -- this is wrapped in a function to lazily require the module + return require('vim.func._memoize')(hash, fn, weak) end return M diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua index 6e557905a7..c46f878067 100644 --- a/runtime/lua/vim/func/_memoize.lua +++ b/runtime/lua/vim/func/_memoize.lua @@ -1,5 +1,7 @@ --- Module for private utility functions +--- @alias vim.func.MemoObj { _hash: (fun(...): any), _weak: boolean?, _cache: table<any> } + --- @param argc integer? --- @return fun(...): any local function concat_hash(argc) @@ -33,29 +35,49 @@ local function resolve_hash(hash) return hash end +--- @param weak boolean? +--- @return table +local create_cache = function(weak) + return setmetatable({}, { + __mode = weak ~= false and 'kv', + }) +end + --- @generic F: function --- @param hash integer|string|fun(...): any --- @param fn F ---- @param strong? boolean +--- @param weak? boolean --- @return F -return function(hash, fn, strong) +return function(hash, fn, weak) vim.validate('hash', hash, { 'number', 'string', 'function' }) vim.validate('fn', fn, 'function') + vim.validate('weak', weak, 'boolean', true) - ---@type table<any,table<any,any>> - local cache = {} - if not strong then - setmetatable(cache, { __mode = 'kv' }) - end - - hash = resolve_hash(hash) + --- @type vim.func.MemoObj + local obj = { + _cache = create_cache(weak), + _hash = resolve_hash(hash), + _weak = weak, + --- @param self vim.func.MemoObj + clear = function(self, ...) + if select('#', ...) == 0 then + self._cache = create_cache(self._weak) + return + end + local key = self._hash(...) + self._cache[key] = nil + end, + } - return function(...) - local key = hash(...) - if cache[key] == nil then - cache[key] = vim.F.pack_len(fn(...)) - end - - return vim.F.unpack_len(cache[key]) - end + return setmetatable(obj, { + --- @param self vim.func.MemoObj + __call = function(self, ...) + local key = self._hash(...) + local cache = self._cache + if cache[key] == nil then + cache[key] = vim.F.pack_len(fn(...)) + end + return vim.F.unpack_len(cache[key]) + end, + }) end diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua index 52a7a13966..ee376f3a11 100644 --- a/runtime/lua/vim/health.lua +++ b/runtime/lua/vim/health.lua @@ -11,7 +11,7 @@ --- < --- Plugin authors are encouraged to write new healthchecks. |health-dev| --- ---- Commands *health-commands* +--- COMMANDS *health-commands* --- --- *:che* *:checkhealth* --- :che[ckhealth] Run all healthchecks. @@ -39,6 +39,23 @@ --- :checkhealth vim* --- < --- +--- USAGE *health-usage* +--- +--- Local mappings in the healthcheck buffer: +--- +--- q Closes the window. +--- +--- Global configuration: +--- +--- *g:health* +--- g:health Dictionary with the following optional keys: +--- - `style` (`'float'|nil`) Set to "float" to display :checkhealth in +--- a floating window instead of the default behavior. +--- +--- Example: >lua +--- vim.g.health = { style = 'float' } +--- +--- -------------------------------------------------------------------------------- --- Create a healthcheck *health-dev* --- --- Healthchecks are functions that check the user environment, configuration, or @@ -331,13 +348,31 @@ function M._check(mods, plugin_names) local emptybuf = vim.fn.bufnr('$') == 1 and vim.fn.getline(1) == '' and 1 == vim.fn.line('$') - -- When no command modifiers are used: - -- - If the current buffer is empty, open healthcheck directly. - -- - If not specified otherwise open healthcheck in a tab. - local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer' - local bufnr = vim.api.nvim_create_buf(true, true) - vim.cmd(buf_cmd .. ' ' .. bufnr) + if + vim.g.health + and type(vim.g.health) == 'table' + and vim.tbl_get(vim.g.health, 'style') == 'float' + then + local max_height = math.floor(vim.o.lines * 0.8) + local max_width = 80 + local float_bufnr, float_winid = vim.lsp.util.open_floating_preview({}, '', { + height = max_height, + width = max_width, + offset_x = math.floor((vim.o.columns - max_width) / 2), + offset_y = math.floor((vim.o.lines - max_height) / 2) - 1, + relative = 'editor', + }) + vim.api.nvim_set_current_win(float_winid) + vim.bo[float_bufnr].modifiable = true + vim.wo[float_winid].list = false + else + -- When no command modifiers are used: + -- - If the current buffer is empty, open healthcheck directly. + -- - If not specified otherwise open healthcheck in a tab. + local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer' + vim.cmd(buf_cmd .. ' ' .. bufnr) + end if vim.fn.bufexists('health://') == 1 then vim.cmd.bwipe('health://') @@ -407,6 +442,17 @@ function M._check(mods, plugin_names) -- Clear the 'Running healthchecks...' message. vim.cmd.redraw() vim.print('') + + -- Quit with 'q' inside healthcheck buffers. + vim.keymap.set('n', 'q', function() + local ok, _ = pcall(vim.cmd.close) + if not ok then + vim.cmd.bdelete() + end + end, { buffer = bufnr, silent = true, noremap = true, nowait = true }) + + -- Once we're done writing checks, set nomodifiable. + vim.bo[bufnr].modifiable = false end return M diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua index 099efa3c61..f5ace7fdc5 100644 --- a/runtime/lua/vim/hl.lua +++ b/runtime/lua/vim/hl.lua @@ -115,7 +115,7 @@ function M.range(bufnr, ns, higroup, start, finish, opts) end end -local yank_ns = api.nvim_create_namespace('hlyank') +local yank_ns = api.nvim_create_namespace('nvim.hlyank') local yank_timer --- @type uv.uv_timer_t? local yank_cancel --- @type fun()? diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6a8c3d1ff3..5226c8ae37 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -211,7 +211,7 @@ local function reuse_client_default(client, config) for _, config_folder in ipairs(config_folders) do local found = false - for _, client_folder in ipairs(client.workspace_folders) do + for _, client_folder in ipairs(client.workspace_folders or {}) do if config_folder.uri == client_folder.uri then found = true break @@ -334,6 +334,11 @@ end --- rootUri, and rootPath on initialization. Unused if `root_dir` is provided. --- @field root_markers? string[] --- +--- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on +--- initialization. If a function, it accepts a single callback argument which must be called with +--- the value of root_dir to use. The LSP server will not be started until the callback is called. +--- @field root_dir? string|fun(cb:fun(string)) +--- --- Predicate used to decide if a client should be re-used. Used on all --- running clients. The default implementation re-uses a client if name and --- root_dir matches. @@ -415,9 +420,33 @@ lsp.config = setmetatable({ _configs = {} }, { --- @return vim.lsp.Config __index = function(self, name) validate('name', name, 'string') - invalidate_enabled_config(name) + + local rconfig = lsp._enabled_configs[name] or {} self._configs[name] = self._configs[name] or {} - return self._configs[name] + + if not rconfig.resolved_config then + -- Resolve configs from lsp/*.lua + -- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites. + local rtp_config = {} ---@type vim.lsp.Config + for _, v in ipairs(api.nvim_get_runtime_file(('lsp/%s.lua'):format(name), true)) do + local config = assert(loadfile(v))() ---@type any? + if type(config) == 'table' then + rtp_config = vim.tbl_deep_extend('force', rtp_config, config) + else + log.warn(string.format('%s does not return a table, ignoring', v)) + end + end + + rconfig.resolved_config = vim.tbl_deep_extend( + 'force', + lsp.config._configs['*'] or {}, + rtp_config, + lsp.config._configs[name] or {} + ) + rconfig.resolved_config.name = name + end + + return rconfig.resolved_config end, --- @param self vim.lsp.config @@ -441,40 +470,6 @@ lsp.config = setmetatable({ _configs = {} }, { end, }) ---- @private ---- @param name string ---- @return vim.lsp.Config -function lsp._resolve_config(name) - local econfig = lsp._enabled_configs[name] or {} - - if not econfig.resolved_config then - -- Resolve configs from lsp/*.lua - -- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites. - local orig_configs = lsp.config._configs - lsp.config._configs = {} - pcall(vim.cmd.runtime, { ('lsp/%s.lua'):format(name), bang = true }) - local rtp_configs = lsp.config._configs - lsp.config._configs = orig_configs - - local config = vim.tbl_deep_extend( - 'force', - lsp.config._configs['*'] or {}, - rtp_configs[name] or {}, - lsp.config._configs[name] or {} - ) - - config.name = name - - validate('cmd', config.cmd, { 'function', 'table' }) - validate('cmd', config.reuse_client, 'function', true) - -- All other fields are validated in client.create - - econfig.resolved_config = config - end - - return assert(econfig.resolved_config) -end - local lsp_enable_autocmd_id --- @type integer? --- @param bufnr integer @@ -495,19 +490,36 @@ local function lsp_enable_callback(bufnr) return true end + --- @param config vim.lsp.Config + local function start(config) + return vim.lsp.start(config, { + bufnr = bufnr, + reuse_client = config.reuse_client, + _root_markers = config.root_markers, + }) + end + for name in vim.spairs(lsp._enabled_configs) do - local config = lsp._resolve_config(name) + local config = lsp.config[name] + validate('cmd', config.cmd, { 'function', 'table' }) + validate('cmd', config.reuse_client, 'function', true) if can_start(config) then -- Deepcopy config so changes done in the client -- do not propagate back to the enabled configs. config = vim.deepcopy(config) - vim.lsp.start(config, { - bufnr = bufnr, - reuse_client = config.reuse_client, - _root_markers = config.root_markers, - }) + if type(config.root_dir) == 'function' then + ---@param root_dir string + config.root_dir(function(root_dir) + config.root_dir = root_dir + vim.schedule(function() + start(config) + end) + end) + else + start(config) + end end end end @@ -534,7 +546,7 @@ function lsp.enable(name, enable) if nm == '*' then error('Invalid name') end - lsp._enabled_configs[nm] = enable == false and nil or {} + lsp._enabled_configs[nm] = enable ~= false and {} or nil end if not next(lsp._enabled_configs) then @@ -831,7 +843,7 @@ local function buf_attach(bufnr) attached_buffers[bufnr] = true local uri = vim.uri_from_bufnr(bufnr) - local augroup = ('lsp_b_%d_save'):format(bufnr) + local augroup = ('nvim.lsp.b_%d_save'):format(bufnr) local group = api.nvim_create_augroup(augroup, { clear = true }) api.nvim_create_autocmd('BufWritePre', { group = group, @@ -1379,7 +1391,7 @@ end --- |LspAttach| autocommand. Example: --- --- ```lua ---- vim.api.nvim_create_autocommand('LspAttach', { +--- vim.api.nvim_create_autocmd('LspAttach', { --- callback = function(args) --- local client = vim.lsp.get_client_by_id(args.data.client_id) --- if client:supports_method('textDocument/foldingRange') then diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua index 2f1767aaf5..66eb81db6e 100644 --- a/runtime/lua/vim/lsp/_folding_range.lua +++ b/runtime/lua/vim/lsp/_folding_range.lua @@ -171,7 +171,7 @@ end -- 1. Implement clearing `bufstate` and event hooks -- when no clients in the buffer support the corresponding method. -- 2. Then generalize this state management to other LSP modules. -local augroup_setup = api.nvim_create_augroup('vim_lsp_folding_range/setup', {}) +local augroup_setup = api.nvim_create_augroup('nvim.lsp.folding_range.setup', {}) --- Initialize `bufstate` and event hooks, then request folding ranges. --- Manage their lifecycle within this function. diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 248969885c..4711b3cc9b 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -174,6 +174,7 @@ function M.cancel(client_id) cancel() end end + cancels[client_id] = nil end return M diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 1926a0228d..8efc6996dd 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -20,7 +20,7 @@ local function client_positional_params(params) end end -local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range') +local hover_ns = api.nvim_create_namespace('nvim.lsp.hover_range') --- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts --- @field silent? boolean @@ -252,13 +252,13 @@ end --- vim.lsp.buf.definition({ on_list = on_list }) --- vim.lsp.buf.references(nil, { on_list = on_list }) --- ``` +--- @field on_list? fun(t: vim.lsp.LocationOpts.OnList) --- ---- If you prefer loclist instead of qflist: +--- Whether to use the |location-list| or the |quickfix| list. --- ```lua --- vim.lsp.buf.definition({ loclist = true }) ---- vim.lsp.buf.references(nil, { loclist = true }) +--- vim.lsp.buf.references(nil, { loclist = false }) --- ``` ---- @field on_list? fun(t: vim.lsp.LocationOpts.OnList) --- @field loclist? boolean --- @class vim.lsp.LocationOpts.OnList @@ -324,12 +324,11 @@ local function process_signature_help_results(results) return signatures end -local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') +local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help') --- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts --- @field silent? boolean --- TODO(lewis6991): support multiple clients --- Displays signature information about the symbol under the cursor in a --- floating window. --- @param config? vim.lsp.buf.signature_help.Opts @@ -356,6 +355,7 @@ function M.signature_help(config) local ft = vim.bo[ctx.bufnr].filetype local total = #signatures + local can_cycle = total > 1 and config.focusable local idx = 0 --- @param update_win? integer @@ -371,7 +371,7 @@ function M.signature_help(config) return end - local sfx = total > 1 and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or '' + local sfx = can_cycle and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or '' local title = string.format('Signature Help: %s%s', client.name, sfx) if config.border then config.title = title @@ -402,7 +402,7 @@ function M.signature_help(config) local fbuf, fwin = show_signature() - if total > 1 then + if can_cycle then vim.keymap.set('n', '<C-s>', function() show_signature(fwin) end, { @@ -796,9 +796,10 @@ function M.references(context, opts) end end ---- Lists all symbols in the current buffer in the quickfix window. +--- Lists all symbols in the current buffer in the |location-list|. --- @param opts? vim.lsp.ListOpts function M.document_symbol(opts) + opts = vim.tbl_deep_extend('keep', opts or {}, { loclist = true }) local params = { textDocument = util.make_text_document_params() } request_with_opts(ms.textDocument_documentSymbol, params, opts) end diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 72043c18dd..a082613bb0 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -174,6 +174,10 @@ local validate = vim.validate --- capabilities. --- @field server_capabilities lsp.ServerCapabilities? --- +--- Response from the server sent on `initialize` describing information about +--- the server. +--- @field server_info lsp.ServerInfo? +--- --- A ring buffer (|vim.ringbuf()|) containing progress messages --- sent by the server. --- @field progress vim.lsp.Client.Progress @@ -556,6 +560,8 @@ function Client:initialize() self.offset_encoding = self.server_capabilities.positionEncoding end + self.server_info = result.serverInfo + if next(self.settings) then self:notify(ms.workspace_didChangeConfiguration, { settings = self.settings }) end @@ -696,14 +702,14 @@ local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'err --- --- @param ... string List to write to the buffer local function err_message(...) - local message = table.concat(vim.iter({ ... }):flatten():totable()) + local chunks = { { table.concat(vim.iter({ ... }):flatten():totable()) } } if vim.in_fast_event() then vim.schedule(function() - api.nvim_err_writeln(message) + api.nvim_echo(chunks, true, { err = true }) api.nvim_command('redraw') end) else - api.nvim_err_writeln(message) + api.nvim_echo(chunks, true, { err = true }) api.nvim_command('redraw') end end @@ -799,6 +805,8 @@ function Client:stop(force) return end + vim.lsp._watchfiles.cancel(self.id) + if force or not self.initialized or self._graceful_shutdown_failed then rpc.terminate() return @@ -813,7 +821,6 @@ function Client:stop(force) rpc.terminate() self._graceful_shutdown_failed = true end - vim.lsp._watchfiles.cancel(self.id) end) end diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 3ccd165d0b..e36d8fee27 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -21,7 +21,7 @@ local lens_cache_by_buf = setmetatable({}, { ---client_id -> namespace local namespaces = setmetatable({}, { __index = function(t, key) - local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key) + local value = api.nvim_create_namespace('nvim.lsp.codelens:' .. key) rawset(t, key, value) return value end, @@ -30,7 +30,7 @@ local namespaces = setmetatable({}, { ---@private M.__namespaces = namespaces -local augroup = api.nvim_create_augroup('vim_lsp_codelens', {}) +local augroup = api.nvim_create_augroup('nvim.lsp.codelens', {}) api.nvim_create_autocmd('LspDetach', { group = augroup, diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index dbf0a62eeb..9902c52c33 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -127,8 +127,10 @@ end --- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion --- --- @param item lsp.CompletionItem +--- @param prefix string +--- @param match fun(text: string, prefix: string):boolean --- @return string -local function get_completion_word(item) +local function get_completion_word(item, prefix, match) if item.insertTextFormat == protocol.InsertTextFormat.Snippet then if item.textEdit then -- Use label instead of text if text has different starting characters. @@ -146,7 +148,12 @@ local function get_completion_word(item) -- -- Typing `i` would remove the candidate because newText starts with `t`. local text = parse_snippet(item.insertText or item.textEdit.newText) - return #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label + local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label + if item.filterText and not match(word, prefix) then + return item.filterText + else + return word + end elseif item.insertText and item.insertText ~= '' then return parse_snippet(item.insertText) else @@ -224,6 +231,9 @@ end ---@param prefix string ---@return boolean local function match_item_by_value(value, prefix) + if prefix == '' then + return true + end if vim.o.completeopt:find('fuzzy') ~= nil then return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil end @@ -276,7 +286,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert') for _, item in ipairs(items) do if matches(item) then - local word = get_completion_word(item) + local word = get_completion_word(item, prefix, match_item_by_value) local hl_group = '' if item.deprecated @@ -630,7 +640,7 @@ local function enable_completions(client_id, bufnr, opts) -- Set up autocommands. local group = - api.nvim_create_augroup(string.format('vim/lsp/completion-%d', bufnr), { clear = true }) + api.nvim_create_augroup(string.format('nvim.lsp.completion_%d', bufnr), { clear = true }) api.nvim_create_autocmd('CompleteDone', { group = group, buffer = bufnr, diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 9a879d9f38..cf39338cc1 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -5,7 +5,7 @@ local api = vim.api local M = {} -local augroup = api.nvim_create_augroup('vim_lsp_diagnostic', {}) +local augroup = api.nvim_create_augroup('nvim.lsp.diagnostic', {}) local DEFAULT_CLIENT_ID = -1 @@ -208,7 +208,7 @@ end --- @param uri string --- @param client_id? integer ---- @param diagnostics vim.Diagnostic[] +--- @param diagnostics lsp.Diagnostic[] --- @param is_pull boolean local function handle_diagnostics(uri, client_id, diagnostics, is_pull) local fname = vim.uri_to_fname(uri) diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 1945040bda..5da4033f89 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -382,7 +382,7 @@ end --- @diagnostic disable-next-line: deprecated RCS[ms.textDocument_hover] = M.hover -local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') +local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help') --- @deprecated remove in 0.13 --- |lsp-handler| for the method "textDocument/signatureHelp". @@ -582,9 +582,8 @@ NSC['window/showMessage'] = function(_, params, ctx) if message_type == protocol.MessageType.Error then err_message('LSP[', client_name, '] ', message) else - --- @type string - local message_type_name = protocol.MessageType[message_type] - api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message)) + message = ('LSP[%s][%s] %s\n'):format(client_name, protocol.MessageType[message_type], message) + api.nvim_echo({ { message } }, true, {}) end return params end diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua index d2cf888d89..8af9f2f791 100644 --- a/runtime/lua/vim/lsp/health.lua +++ b/runtime/lua/vim/lsp/health.lua @@ -40,6 +40,8 @@ local function check_active_clients() local clients = vim.lsp.get_clients() if next(clients) then for _, client in pairs(clients) do + local server_version = vim.tbl_get(client, 'server_info', 'version') + or '? (no serverInfo.version response)' local cmd ---@type string local ccmd = client.config.cmd if type(ccmd) == 'table' then @@ -62,6 +64,7 @@ local function check_active_clients() end report_info(table.concat({ string.format('%s (id: %d)', client.name, client.id), + string.format('- Version: %s', server_version), dirs_info, string.format('- Command: %s', cmd), string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })), @@ -181,7 +184,7 @@ local function check_enabled_configs() vim.health.start('vim.lsp: Enabled Configurations') for name in vim.spairs(vim.lsp._enabled_configs) do - local config = vim.lsp._resolve_config(name) + local config = vim.lsp.config[name] local text = {} --- @type string[] text[#text + 1] = ('%s:'):format(name) for k, v in diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index 50cf9f5f29..37e1202d1d 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -29,8 +29,8 @@ local bufstates = vim.defaulttable(function(_) }) end) -local namespace = api.nvim_create_namespace('vim_lsp_inlayhint') -local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {}) +local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint') +local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {}) --- |lsp-handler| for the method `textDocument/inlayHint` --- Store hints for a specific buffer and client diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 7cc3f95aed..a31202553b 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -166,7 +166,7 @@ function STHighlighter.new(bufnr) local self = setmetatable({}, { __index = STHighlighter }) self.bufnr = bufnr - self.augroup = api.nvim_create_augroup('vim_lsp_semantic_tokens:' .. bufnr, { clear = true }) + self.augroup = api.nvim_create_augroup('nvim.lsp.semantic_tokens:' .. bufnr, { clear = true }) self.client_state = {} STHighlighter.active[bufnr] = self @@ -225,7 +225,7 @@ function STHighlighter:attach(client_id) local state = self.client_state[client_id] if not state then state = { - namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens:' .. client_id), + namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens:' .. client_id), active_request = {}, current_result = {}, } @@ -805,7 +805,7 @@ function M._refresh(err, _, ctx) return vim.NIL end -local namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens') +local namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens') api.nvim_set_decoration_provider(namespace, { on_win = function(_, _, bufnr, topline, botline) local highlighter = STHighlighter.active[bufnr] diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6bee5bc31f..4e0adf3366 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -875,15 +875,16 @@ function M.make_floating_popup_options(width, height, opts) return { anchor = anchor, + row = row + (opts.offset_y or 0), col = col + (opts.offset_x or 0), height = height, focusable = opts.focusable, - relative = opts.relative == 'mouse' and 'mouse' or 'cursor', - row = row + (opts.offset_y or 0), + relative = (opts.relative == 'mouse' or opts.relative == 'editor') and opts.relative + or 'cursor', style = 'minimal', width = width, border = opts.border or default_border, - zindex = opts.zindex or 50, + zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1, title = title, title_pos = title_pos, } @@ -1356,7 +1357,7 @@ end ---@param bufnrs table list of buffers where the preview window will remain visible ---@see autocmd-events local function close_preview_autocmd(events, winnr, bufnrs) - local augroup = api.nvim_create_augroup('preview_window_' .. winnr, { + local augroup = api.nvim_create_augroup('nvim.preview_window_' .. winnr, { clear = true, }) @@ -1431,7 +1432,7 @@ function M._make_floating_popup_size(contents, opts) if vim.tbl_isempty(line_widths) then for _, line in ipairs(contents) do local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n')) - height = height + math.ceil(line_width / wrap_at) + height = height + math.max(1, math.ceil(line_width / wrap_at)) end else for i = 1, #contents do @@ -1494,7 +1495,7 @@ end --- @field title_pos? 'left'|'center'|'right' --- --- (default: `'cursor'`) ---- @field relative? 'mouse'|'cursor' +--- @field relative? 'mouse'|'cursor'|'editor' --- --- - "auto": place window based on which side of the cursor has more lines --- - "above": place the window above the cursor unless there are not enough lines @@ -1618,7 +1619,7 @@ function M.open_floating_preview(contents, syntax, opts) api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) end - local augroup_name = ('closing_floating_preview_%d'):format(floating_winnr) + local augroup_name = ('nvim.closing_floating_preview_%d'):format(floating_winnr) local ok = pcall(api.nvim_get_autocmds, { group = augroup_name, pattern = tostring(floating_winnr) }) if not ok then @@ -1649,7 +1650,7 @@ function M.open_floating_preview(contents, syntax, opts) end do --[[ References ]] - local reference_ns = api.nvim_create_namespace('vim_lsp_references') + local reference_ns = api.nvim_create_namespace('nvim.lsp.references') --- Removes document highlights from a buffer. --- diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 24c3f243e5..02b12490af 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -959,7 +959,7 @@ do --- function vim.startswith(s, prefix) --- vim.validate('s', s, 'string') --- vim.validate('prefix', prefix, 'string') - --- ... + --- -- ... --- end --- ``` --- @@ -979,7 +979,7 @@ do --- age={age, 'number'}, --- hobbies={hobbies, 'table'}, --- } - --- ... + --- -- ... --- end --- ``` --- diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua index af7e3c6d33..bfd439181e 100644 --- a/runtime/lua/vim/snippet.lua +++ b/runtime/lua/vim/snippet.lua @@ -1,6 +1,6 @@ local G = vim.lsp._snippet_grammar -local snippet_group = vim.api.nvim_create_augroup('vim/snippet', {}) -local snippet_ns = vim.api.nvim_create_namespace('vim/snippet') +local snippet_group = vim.api.nvim_create_augroup('nvim.snippet', {}) +local snippet_ns = vim.api.nvim_create_namespace('nvim.snippet') local hl_group = 'SnippetTabstop' local jump_forward_key = '<tab>' local jump_backward_key = '<s-tab>' diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 89dc4e289a..0269699dfd 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -61,8 +61,6 @@ function M._create_parser(bufnr, lang, opts) { on_bytes = bytes_cb, on_detach = detach_cb, on_reload = reload_cb, preview = true } ) - self:parse() - return self end @@ -397,6 +395,8 @@ end --- Note: By default, disables regex syntax highlighting, which may be required for some plugins. --- In this case, add `vim.bo.syntax = 'on'` after the call to `start`. --- +--- Note: By default, the highlighter parses code asynchronously, using a segment time of 3ms. +--- --- Example: --- --- ```lua @@ -408,8 +408,8 @@ end --- }) --- ``` --- ----@param bufnr (integer|nil) Buffer to be highlighted (default: current buffer) ----@param lang (string|nil) Language of the parser (default: from buffer filetype) +---@param bufnr integer? Buffer to be highlighted (default: current buffer) +---@param lang string? Language of the parser (default: from buffer filetype) function M.start(bufnr, lang) bufnr = vim._resolve_bufnr(bufnr) local parser = assert(M.get_parser(bufnr, lang, { error = false })) diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 0cb5b497c7..4a571bbaf7 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -19,14 +19,19 @@ local api = vim.api ---The range on which to evaluate foldexpr. ---When in insert mode, the evaluation is deferred to InsertLeave. ---@field foldupdate_range? Range2 +--- +---The treesitter parser associated with this buffer. +---@field parser? vim.treesitter.LanguageTree local FoldInfo = {} FoldInfo.__index = FoldInfo ---@private -function FoldInfo.new() +---@param bufnr integer +function FoldInfo.new(bufnr) return setmetatable({ levels0 = {}, levels = {}, + parser = ts.get_parser(bufnr, nil, { error = false }), }, FoldInfo) end @@ -64,111 +69,122 @@ end ---@param info TS.FoldInfo ---@param srow integer? ---@param erow integer? 0-indexed, exclusive ----@param parse_injections? boolean -local function compute_folds_levels(bufnr, info, srow, erow, parse_injections) +---@param callback function? +local function compute_folds_levels(bufnr, info, srow, erow, callback) srow = srow or 0 erow = erow or api.nvim_buf_line_count(bufnr) - local parser = assert(ts.get_parser(bufnr, nil, { error = false })) - - parser:parse(parse_injections and { srow, erow } or nil) - - local enter_counts = {} ---@type table<integer, integer> - local leave_counts = {} ---@type table<integer, integer> - local prev_start = -1 - local prev_stop = -1 + local parser = info.parser + if not parser then + return + end - parser:for_each_tree(function(tree, ltree) - local query = ts.query.get(ltree:lang(), 'folds') - if not query then + parser:parse(nil, function(_, trees) + if not trees then return end - -- Collect folds starting from srow - 1, because we should first subtract the folds that end at - -- srow - 1 from the level of srow - 1 to get accurate level of srow. - for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do - for id, nodes in pairs(match) do - if query.captures[id] == 'fold' then - local range = ts.get_range(nodes[1], bufnr, metadata[id]) - local start, _, stop, stop_col = Range.unpack4(range) - - if #nodes > 1 then - -- assumes nodes are ordered by range - local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id]) - local _, _, end_stop, end_stop_col = Range.unpack4(end_range) - stop = end_stop - stop_col = end_stop_col - end + local enter_counts = {} ---@type table<integer, integer> + local leave_counts = {} ---@type table<integer, integer> + local prev_start = -1 + local prev_stop = -1 - if stop_col == 0 then - stop = stop - 1 - end + parser:for_each_tree(function(tree, ltree) + local query = ts.query.get(ltree:lang(), 'folds') + if not query then + return + end - local fold_length = stop - start + 1 - - -- Fold only multiline nodes that are not exactly the same as previously met folds - -- Checking against just the previously found fold is sufficient if nodes - -- are returned in preorder or postorder when traversing tree - if - fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop) - then - enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1 - leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1 - prev_start = start - prev_stop = stop + -- Collect folds starting from srow - 1, because we should first subtract the folds that end at + -- srow - 1 from the level of srow - 1 to get accurate level of srow. + for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do + for id, nodes in pairs(match) do + if query.captures[id] == 'fold' then + local range = ts.get_range(nodes[1], bufnr, metadata[id]) + local start, _, stop, stop_col = Range.unpack4(range) + + if #nodes > 1 then + -- assumes nodes are ordered by range + local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id]) + local _, _, end_stop, end_stop_col = Range.unpack4(end_range) + stop = end_stop + stop_col = end_stop_col + end + + if stop_col == 0 then + stop = stop - 1 + end + + local fold_length = stop - start + 1 + + -- Fold only multiline nodes that are not exactly the same as previously met folds + -- Checking against just the previously found fold is sufficient if nodes + -- are returned in preorder or postorder when traversing tree + if + fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop) + then + enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1 + leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1 + prev_start = start + prev_stop = stop + end end end end - end - end) + end) - local nestmax = vim.wo.foldnestmax - local level0_prev = info.levels0[srow] or 0 - local leave_prev = leave_counts[srow] or 0 - - -- We now have the list of fold opening and closing, fill the gaps and mark where fold start - for lnum = srow + 1, erow do - local enter_line = enter_counts[lnum] or 0 - local leave_line = leave_counts[lnum] or 0 - local level0 = level0_prev - leave_prev + enter_line - - -- Determine if it's the start/end of a fold - -- NB: vim's fold-expr interface does not have a mechanism to indicate that - -- two (or more) folds start at this line, so it cannot distinguish between - -- ( \n ( \n )) \n (( \n ) \n ) - -- versus - -- ( \n ( \n ) \n ( \n ) \n ) - -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and - -- vim interprets as the second case. - -- If it did have such a mechanism, (clamped - clamped_prev) - -- would be the correct number of starts to pass on. - local adjusted = level0 ---@type integer - local prefix = '' - if enter_line > 0 then - prefix = '>' - if leave_line > 0 then - -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line - -- so that f2 gets the correct level on this line. This may reduce the size of f1 below - -- foldminlines, but we don't handle it for simplicity. - adjusted = level0 - leave_line - leave_line = 0 + local nestmax = vim.wo.foldnestmax + local level0_prev = info.levels0[srow] or 0 + local leave_prev = leave_counts[srow] or 0 + + -- We now have the list of fold opening and closing, fill the gaps and mark where fold start + for lnum = srow + 1, erow do + local enter_line = enter_counts[lnum] or 0 + local leave_line = leave_counts[lnum] or 0 + local level0 = level0_prev - leave_prev + enter_line + + -- Determine if it's the start/end of a fold + -- NB: vim's fold-expr interface does not have a mechanism to indicate that + -- two (or more) folds start at this line, so it cannot distinguish between + -- ( \n ( \n )) \n (( \n ) \n ) + -- versus + -- ( \n ( \n ) \n ( \n ) \n ) + -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and + -- vim interprets as the second case. + -- If it did have such a mechanism, (clamped - clamped_prev) + -- would be the correct number of starts to pass on. + local adjusted = level0 ---@type integer + local prefix = '' + if enter_line > 0 then + prefix = '>' + if leave_line > 0 then + -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line + -- so that f2 gets the correct level on this line. This may reduce the size of f1 below + -- foldminlines, but we don't handle it for simplicity. + adjusted = level0 - leave_line + leave_line = 0 + end end - end - -- Clamp at foldnestmax. - local clamped = adjusted - if adjusted > nestmax then - prefix = '' - clamped = nestmax - end + -- Clamp at foldnestmax. + local clamped = adjusted + if adjusted > nestmax then + prefix = '' + clamped = nestmax + end - -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels(). - info.levels0[lnum] = adjusted - info.levels[lnum] = prefix .. tostring(clamped) + -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels(). + info.levels0[lnum] = adjusted + info.levels[lnum] = prefix .. tostring(clamped) - leave_prev = leave_line - level0_prev = adjusted - end + leave_prev = leave_line + level0_prev = adjusted + end + + if callback then + callback() + end + end) end local M = {} @@ -176,7 +192,7 @@ local M = {} ---@type table<integer,TS.FoldInfo> local foldinfos = {} -local group = api.nvim_create_augroup('treesitter/fold', {}) +local group = api.nvim_create_augroup('nvim.treesitter.fold', {}) --- Update the folds in the windows that contain the buffer and use expr foldmethod (assuming that --- the user doesn't use different foldexpr for the same buffer). @@ -259,6 +275,8 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) schedule_if_loaded(bufnr, function() local srow_upd, erow_upd ---@type integer?, integer? local max_erow = api.nvim_buf_line_count(bufnr) + -- TODO(ribru17): Replace this with a proper .all() awaiter once #19624 is resolved + local iterations = 0 for _, change in ipairs(tree_changes) do local srow, _, erow, ecol = Range.unpack4(change) -- If a parser doesn't have any ranges explicitly set, treesitter will @@ -272,12 +290,14 @@ local function on_changedtree(bufnr, foldinfo, tree_changes) end -- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit. srow = math.max(srow - vim.wo.foldminlines, 0) - compute_folds_levels(bufnr, foldinfo, srow, erow) srow_upd = srow_upd and math.min(srow_upd, srow) or srow erow_upd = erow_upd and math.max(erow_upd, erow) or erow - end - if #tree_changes > 0 then - foldinfo:foldupdate(bufnr, srow_upd, erow_upd) + compute_folds_levels(bufnr, foldinfo, srow, erow, function() + iterations = iterations + 1 + if iterations == #tree_changes then + foldinfo:foldupdate(bufnr, srow_upd, erow_upd) + end + end) end end) end @@ -335,8 +355,9 @@ local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col, foldinfo.on_bytes_range = nil -- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit. srow = math.max(srow - vim.wo.foldminlines, 0) - compute_folds_levels(bufnr, foldinfo, srow, erow) - foldinfo:foldupdate(bufnr, srow, erow) + compute_folds_levels(bufnr, foldinfo, srow, erow, function() + foldinfo:foldupdate(bufnr, srow, erow) + end) end) end end @@ -347,13 +368,21 @@ function M.foldexpr(lnum) lnum = lnum or vim.v.lnum local bufnr = api.nvim_get_current_buf() - local parser = ts.get_parser(bufnr, nil, { error = false }) - if not parser then - return '0' - end - if not foldinfos[bufnr] then - foldinfos[bufnr] = FoldInfo.new() + foldinfos[bufnr] = FoldInfo.new(bufnr) + api.nvim_create_autocmd('BufUnload', { + buffer = bufnr, + once = true, + callback = function() + foldinfos[bufnr] = nil + end, + }) + + local parser = foldinfos[bufnr].parser + if not parser then + return '0' + end + compute_folds_levels(bufnr, foldinfos[bufnr]) parser:register_cbs({ @@ -378,10 +407,17 @@ api.nvim_create_autocmd('OptionSet', { pattern = { 'foldminlines', 'foldnestmax' }, desc = 'Refresh treesitter folds', callback = function() - for bufnr, _ in pairs(foldinfos) do - foldinfos[bufnr] = FoldInfo.new() - compute_folds_levels(bufnr, foldinfos[bufnr]) - foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr)) + local buf = api.nvim_get_current_buf() + local bufs = vim.v.option_type == 'global' and vim.tbl_keys(foldinfos) + or foldinfos[buf] and { buf } + or {} + for _, bufnr in ipairs(bufs) do + foldinfos[bufnr] = FoldInfo.new(bufnr) + api.nvim_buf_call(bufnr, function() + compute_folds_levels(bufnr, foldinfos[bufnr], nil, nil, function() + foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr)) + end) + end) end end, }) diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index a825505378..f6645beb28 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -1,6 +1,6 @@ local api = vim.api -local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') +local namespace = api.nvim_create_namespace('nvim.treesitter.query_linter') local M = {} diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 26817cdba5..42c25dbdad 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -119,7 +119,7 @@ function TSTreeView:new(bufnr, lang) end local t = { - ns = api.nvim_create_namespace('treesitter/dev-inspect'), + ns = api.nvim_create_namespace('nvim.treesitter.dev_inspect'), nodes = nodes, named = named, ---@type vim.treesitter.dev.TSTreeViewOpts @@ -135,7 +135,7 @@ function TSTreeView:new(bufnr, lang) return t end -local decor_ns = api.nvim_create_namespace('ts.dev') +local decor_ns = api.nvim_create_namespace('nvim.treesitter.dev') ---@param range Range4 ---@return string @@ -442,7 +442,7 @@ function M.inspect_tree(opts) end, }) - local group = api.nvim_create_augroup('treesitter/dev', {}) + local group = api.nvim_create_augroup('nvim.treesitter.dev', {}) api.nvim_create_autocmd('CursorMoved', { group = group, @@ -547,7 +547,7 @@ function M.inspect_tree(opts) }) end -local edit_ns = api.nvim_create_namespace('treesitter/dev-edit') +local edit_ns = api.nvim_create_namespace('nvim.treesitter.dev_edit') ---@param query_win integer ---@param base_win integer @@ -633,7 +633,7 @@ function M.edit_query(lang) -- can infer the language later. api.nvim_buf_set_name(query_buf, string.format('%s/query_editor.scm', lang)) - local group = api.nvim_create_augroup('treesitter/dev-edit', {}) + local group = api.nvim_create_augroup('nvim.treesitter.dev_edit', {}) api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { group = group, buffer = query_buf, diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 8ce8652f7d..c11fa1999d 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -2,7 +2,7 @@ local api = vim.api local query = vim.treesitter.query local Range = require('vim.treesitter._range') -local ns = api.nvim_create_namespace('treesitter/highlighter') +local ns = api.nvim_create_namespace('nvim.treesitter.highlighter') ---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch @@ -69,6 +69,7 @@ end ---@field private _queries table<string,vim.treesitter.highlighter.Query> ---@field tree vim.treesitter.LanguageTree ---@field private redraw_count integer +---@field parsing boolean true if we are parsing asynchronously local TSHighlighter = { active = {}, } @@ -147,8 +148,6 @@ function TSHighlighter.new(tree, opts) vim.opt_local.spelloptions:append('noplainbuffer') end) - self.tree:parse() - return self end @@ -299,6 +298,8 @@ local function on_line_impl(self, buf, line, is_spell_nav) state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) end + local captures = state.highlighter_query:query().captures + while line >= state.next_row do local capture, node, metadata, match = state.iter(line) @@ -311,7 +312,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) if capture then local hl = state.highlighter_query:get_hl_from_capture(capture) - local capture_name = state.highlighter_query:query().captures[capture] + local capture_name = captures[capture] local spell, spell_pri_offset = get_spell(capture_name) @@ -382,19 +383,23 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _) end ---@private ----@param _win integer ---@param buf integer ---@param topline integer ---@param botline integer -function TSHighlighter._on_win(_, _win, buf, topline, botline) +function TSHighlighter._on_win(_, _, buf, topline, botline) local self = TSHighlighter.active[buf] - if not self then + if not self or self.parsing then return false end - self.tree:parse({ topline, botline + 1 }) - self:prepare_highlight_states(topline, botline + 1) + self.parsing = self.tree:parse({ topline, botline + 1 }, function(_, trees) + if trees and self.parsing then + self.parsing = false + api.nvim__redraw({ buf = buf, valid = false, flush = false }) + end + end) == nil self.redraw_count = self.redraw_count + 1 - return true + self:prepare_highlight_states(topline, botline) + return #self._highlight_states > 0 end api.nvim_set_decoration_provider(ns, { diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 446051dfd7..238a078703 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -133,8 +133,9 @@ function M.add(lang, opts) path = paths[1] end - return loadparser(path, lang, symbol_name) or nil, - string.format('Cannot load parser %s for language "%s"', path, lang) + local res = loadparser(path, lang, symbol_name) + return res, + res == nil and string.format('Cannot load parser %s for language "%s"', path, lang) or nil end --- @param x string|string[] diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4b42164dc8..35a77f1afc 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -44,6 +44,8 @@ local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local Range = require('vim.treesitter._range') +local default_parse_timeout_ms = 3 + ---@alias TSCallbackName ---| 'changedtree' ---| 'bytes' @@ -76,6 +78,10 @@ local TSCallbackNames = { ---@field private _injections_processed boolean ---@field private _opts table Options ---@field private _parser TSParser Parser for language +---Table of regions for which the tree is currently running an async parse +---@field private _ranges_being_parsed table<string, boolean> +---Table of callback queues, keyed by each region for which the callbacks should be run +---@field private _cb_queues table<string, fun(err?: string, trees?: table<integer, TSTree>)[]> ---@field private _has_regions boolean ---@field private _regions table<integer, Range6[]>? ---List of regions this tree should manage and parse. If nil then regions are @@ -130,6 +136,8 @@ function LanguageTree.new(source, lang, opts) _injections_processed = false, _valid = false, _parser = vim._create_ts_parser(lang), + _ranges_being_parsed = {}, + _cb_queues = {}, _callbacks = {}, _callbacks_rec = {}, } @@ -232,6 +240,7 @@ end ---@param reload boolean|nil function LanguageTree:invalidate(reload) self._valid = false + self._parser:reset() -- buffer was reloaded, reparse all trees if reload then @@ -334,10 +343,12 @@ end --- @private --- @param range boolean|Range? +--- @param timeout integer? --- @return Range6[] changes --- @return integer no_regions_parsed --- @return number total_parse_time -function LanguageTree:_parse_regions(range) +--- @return boolean finished whether async parsing still needs time +function LanguageTree:_parse_regions(range, timeout) local changes = {} local no_regions_parsed = 0 local total_parse_time = 0 @@ -357,9 +368,14 @@ function LanguageTree:_parse_regions(range) ) then self._parser:set_included_ranges(ranges) + self._parser:set_timeout(timeout and timeout * 1000 or 0) -- ms -> micros local parse_time, tree, tree_changes = tcall(self._parser.parse, self._parser, self._trees[i], self._source, true) + if not tree then + return changes, no_regions_parsed, total_parse_time, false + end + -- Pass ranges if this is an initial parse local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true) @@ -373,7 +389,7 @@ function LanguageTree:_parse_regions(range) end end - return changes, no_regions_parsed, total_parse_time + return changes, no_regions_parsed, total_parse_time, true end --- @private @@ -409,6 +425,82 @@ function LanguageTree:_add_injections() return query_time end +--- @param range boolean|Range? +--- @return string +local function range_to_string(range) + return type(range) == 'table' and table.concat(range, ',') or tostring(range) +end + +--- @private +--- @param range boolean|Range? +--- @param callback fun(err?: string, trees?: table<integer, TSTree>) +function LanguageTree:_push_async_callback(range, callback) + local key = range_to_string(range) + self._cb_queues[key] = self._cb_queues[key] or {} + local queue = self._cb_queues[key] + queue[#queue + 1] = callback +end + +--- @private +--- @param range boolean|Range? +--- @param err? string +--- @param trees? table<integer, TSTree> +function LanguageTree:_run_async_callbacks(range, err, trees) + local key = range_to_string(range) + for _, cb in ipairs(self._cb_queues[key]) do + cb(err, trees) + end + self._ranges_being_parsed[key] = nil + self._cb_queues[key] = nil +end + +--- Run an asynchronous parse, calling {on_parse} when complete. +--- +--- @private +--- @param range boolean|Range? +--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>) +--- @return table<integer, TSTree>? trees the list of parsed trees, if parsing completed synchronously +function LanguageTree:_async_parse(range, on_parse) + self:_push_async_callback(range, on_parse) + + -- If we are already running an async parse, just queue the callback. + local range_string = range_to_string(range) + if not self._ranges_being_parsed[range_string] then + self._ranges_being_parsed[range_string] = true + else + return + end + + local buf = vim.b[self._source] + local ct = buf.changedtick + local total_parse_time = 0 + local redrawtime = vim.o.redrawtime + local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil + + local function step() + -- If buffer was changed in the middle of parsing, reset parse state + if buf.changedtick ~= ct then + ct = buf.changedtick + total_parse_time = 0 + end + + local parse_time, trees, finished = tcall(self._parse, self, range, timeout) + total_parse_time = total_parse_time + parse_time + + if finished then + self:_run_async_callbacks(range, nil, trees) + return trees + elseif total_parse_time > redrawtime then + self:_run_async_callbacks(range, 'TIMEOUT', nil) + return nil + else + vim.schedule(step) + end + end + + return step() +end + --- Recursively parse all regions in the language tree using |treesitter-parsers| --- for the corresponding languages and run injection queries on the parsed trees --- to determine whether child trees should be created and parsed. @@ -420,11 +512,33 @@ end --- Set to `true` to run a complete parse of the source (Note: Can be slow!) --- Set to `false|nil` to only parse regions with empty ranges (typically --- only the root tree without injections). ---- @return table<integer, TSTree> -function LanguageTree:parse(range) +--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)? Function invoked when parsing completes. +--- When provided and `vim.g._ts_force_sync_parsing` is not set, parsing will run +--- asynchronously. The first argument to the function is a string respresenting the error type, +--- in case of a failure (currently only possible for timeouts). The second argument is the list +--- of trees returned by the parse (upon success), or `nil` if the parse timed out (determined +--- by 'redrawtime'). +--- +--- If parsing was still able to finish synchronously (within 3ms), `parse()` returns the list +--- of trees. Otherwise, it returns `nil`. +--- @return table<integer, TSTree>? +function LanguageTree:parse(range, on_parse) + if on_parse then + return self:_async_parse(range, on_parse) + end + local trees, _ = self:_parse(range) + return trees +end + +--- @private +--- @param range boolean|Range|nil +--- @param timeout integer? +--- @return table<integer, TSTree> trees +--- @return boolean finished +function LanguageTree:_parse(range, timeout) if self:is_valid() then self:_log('valid') - return self._trees + return self._trees, true end local changes --- @type Range6[]? @@ -433,17 +547,22 @@ function LanguageTree:parse(range) local no_regions_parsed = 0 local query_time = 0 local total_parse_time = 0 + local is_finished --- @type boolean -- At least 1 region is invalid if not self:is_valid(true) then - changes, no_regions_parsed, total_parse_time = self:_parse_regions(range) + changes, no_regions_parsed, total_parse_time, is_finished = self:_parse_regions(range, timeout) + timeout = timeout and math.max(timeout - total_parse_time, 0) + if not is_finished then + return self._trees, is_finished + end -- Need to run injections when we parsed something if no_regions_parsed > 0 then self._injections_processed = false end end - if not self._injections_processed and range ~= false and range ~= nil then + if not self._injections_processed and range then query_time = self:_add_injections() self._injections_processed = true end @@ -457,10 +576,17 @@ function LanguageTree:parse(range) }) for _, child in pairs(self._children) do - child:parse(range) + if timeout == 0 then + return self._trees, false + end + local ctime, _, child_finished = tcall(child._parse, child, range, timeout) + timeout = timeout and math.max(timeout - ctime, 0) + if not child_finished then + return self._trees, child_finished + end end - return self._trees + return self._trees, true end --- Invokes the callback for each |LanguageTree| recursively. @@ -907,6 +1033,7 @@ function LanguageTree:_edit( ) end + self._parser:reset() self._regions = nil local changed_range = { diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index dbe3d54c2f..e43d0a8ad4 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,17 +1,74 @@ +--- @brief This Lua |treesitter-query| interface allows you to create queries and use them to parse +--- text. See |vim.treesitter.query.parse()| for a working example. + local api = vim.api local language = require('vim.treesitter.language') local memoize = vim.func._memoize local M = {} +local function is_directive(name) + return string.sub(name, -1) == '!' +end + +---@nodoc +---@class vim.treesitter.query.ProcessedPredicate +---@field [1] string predicate name +---@field [2] boolean should match +---@field [3] (integer|string)[] the original predicate + +---@alias vim.treesitter.query.ProcessedDirective (integer|string)[] + +---@nodoc +---@class vim.treesitter.query.ProcessedPattern { +---@field predicates vim.treesitter.query.ProcessedPredicate[] +---@field directives vim.treesitter.query.ProcessedDirective[] + +--- Splits the query patterns into predicates and directives. +---@param patterns table<integer, (integer|string)[][]> +---@return table<integer, vim.treesitter.query.ProcessedPattern> +local function process_patterns(patterns) + ---@type table<integer, vim.treesitter.query.ProcessedPattern> + local processed_patterns = {} + + for k, pattern_list in pairs(patterns) do + ---@type vim.treesitter.query.ProcessedPredicate[] + local predicates = {} + ---@type vim.treesitter.query.ProcessedDirective[] + local directives = {} + + for _, pattern in ipairs(pattern_list) do + -- Note: tree-sitter strips the leading # from predicates for us. + local pred_name = pattern[1] + ---@cast pred_name string + + if is_directive(pred_name) then + table.insert(directives, pattern) + else + local should_match = true + if pred_name:match('^not%-') then + pred_name = pred_name:sub(5) + should_match = false + end + table.insert(predicates, { pred_name, should_match, pattern }) + end + end + + processed_patterns[k] = { predicates = predicates, directives = directives } + end + + return processed_patterns +end + ---@nodoc ---Parsed query, see |vim.treesitter.query.parse()| --- ---@class vim.treesitter.Query ----@field lang string name of the language for this parser +---@field lang string parser language name ---@field captures string[] list of (unique) capture names defined in query ----@field info vim.treesitter.QueryInfo contains information used in the query (e.g. captures, predicates, directives) +---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives) ---@field query TSQuery userdata query object +---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern> local Query = {} Query.__index = Query @@ -30,6 +87,7 @@ function Query.new(lang, ts_query) patterns = query_info.patterns, } self.captures = self.info.captures + self._processed_patterns = process_patterns(self.info.patterns) return self end @@ -204,6 +262,7 @@ local explicit_queries = setmetatable({}, { ---@param query_name string Name of the query (e.g., "highlights") ---@param text string Query text (unparsed). function M.set(lang, query_name, text) + M.get:clear(lang, query_name) explicit_queries[lang][query_name] = M.parse(lang, text) end @@ -226,22 +285,38 @@ M.get = memoize('concat-2', function(lang, query_name) end return M.parse(lang, query_string) -end) +end, false) ---- Parse {query} as a string. (If the query is in a file, the caller ---- should read the contents into a string before calling). ---- ---- Returns a `Query` (see |lua-treesitter-query|) object which can be used to ---- search nodes in the syntax tree for the patterns defined in {query} ---- using the `iter_captures` and `iter_matches` methods. +api.nvim_create_autocmd('OptionSet', { + pattern = { 'runtimepath' }, + group = api.nvim_create_augroup('nvim.treesitter.query_cache_reset', { clear = true }), + callback = function() + M.get:clear() + end, +}) + +--- Parses a {query} string and returns a `Query` object (|lua-treesitter-query|), which can be used +--- to search the tree for the query patterns (via |Query:iter_captures()|, |Query:iter_matches()|), +--- or inspect the query via these fields: +--- - `captures`: a list of unique capture names defined in the query (alias: `info.captures`). +--- - `info.patterns`: information about predicates. --- ---- Exposes `info` and `captures` with additional context about {query}. ---- - `captures` contains the list of unique capture names defined in {query}. ---- - `info.captures` also points to `captures`. ---- - `info.patterns` contains information about predicates. +--- Example (to try it, use `g==` or select the code then run `:'<,'>lua`): +--- ```lua +--- local query = vim.treesitter.query.parse('vimdoc', [[ +--- ; query +--- ((h1) @str +--- (#trim! @str 1 1 1 1)) +--- ]]) +--- local tree = vim.treesitter.get_parser():parse()[1] +--- for id, node, metadata in query:iter_captures(tree:root(), 0) do +--- -- Print the node name and source text. +--- vim.print({node:type(), vim.treesitter.get_node_text(node, vim.api.nvim_get_current_buf())}) +--- end +--- ``` --- ---@param lang string Language to use for the query ----@param query string Query in s-expr syntax +---@param query string Query text, in s-expr syntax --- ---@return vim.treesitter.Query : Parsed query --- @@ -250,7 +325,7 @@ M.parse = memoize('concat-2', function(lang, query) assert(language.add(lang)) local ts_query = vim._ts_parse_query(lang, query) return Query.new(lang, ts_query) -end) +end, false) --- Implementations of predicates that can optionally be prefixed with "any-". --- @@ -740,84 +815,50 @@ function M.list_predicates() return vim.tbl_keys(predicate_handlers) end -local function xor(x, y) - return (x or y) and not (x and y) -end - -local function is_directive(name) - return string.sub(name, -1) == '!' -end - ---@private ----@param match TSQueryMatch +---@param pattern_i integer +---@param predicates vim.treesitter.query.ProcessedPredicate[] +---@param captures table<integer, TSNode[]> ---@param source integer|string -function Query:match_preds(match, source) - local _, pattern = match:info() - local preds = self.info.patterns[pattern] - - if not preds then - return true - end - - local captures = match:captures() - - for _, pred in pairs(preds) do - -- Here we only want to return if a predicate DOES NOT match, and - -- continue on the other case. This way unknown predicates will not be considered, - -- which allows some testing and easier user extensibility (#12173). - -- Also, tree-sitter strips the leading # from predicates for us. - local is_not = false - - -- Skip over directives... they will get processed after all the predicates. - if not is_directive(pred[1]) then - local pred_name = pred[1] - if pred_name:match('^not%-') then - pred_name = pred_name:sub(5) - is_not = true - end - - local handler = predicate_handlers[pred_name] - - if not handler then - error(string.format('No handler for %s', pred[1])) - return false - end - - local pred_matches = handler(captures, pattern, source, pred) +---@return boolean whether the predicates match +function Query:_match_predicates(predicates, pattern_i, captures, source) + for _, predicate in ipairs(predicates) do + local processed_name = predicate[1] + local should_match = predicate[2] + local orig_predicate = predicate[3] + + local handler = predicate_handlers[processed_name] + if not handler then + error(string.format('No handler for %s', orig_predicate[1])) + return false + end - if not xor(is_not, pred_matches) then - return false - end + local does_match = handler(captures, pattern_i, source, orig_predicate) + if does_match ~= should_match then + return false end end return true end ---@private ----@param match TSQueryMatch +---@param pattern_i integer +---@param directives vim.treesitter.query.ProcessedDirective[] +---@param source integer|string +---@param captures table<integer, TSNode[]> ---@return vim.treesitter.query.TSMetadata metadata -function Query:apply_directives(match, source) +function Query:_apply_directives(directives, pattern_i, captures, source) ---@type vim.treesitter.query.TSMetadata local metadata = {} - local _, pattern = match:info() - local preds = self.info.patterns[pattern] - - if not preds then - return metadata - end - local captures = match:captures() + for _, directive in pairs(directives) do + local handler = directive_handlers[directive[1]] - for _, pred in pairs(preds) do - if is_directive(pred[1]) then - local handler = directive_handlers[pred[1]] - - if not handler then - error(string.format('No handler for %s', pred[1])) - end - - handler(captures, pattern, source, pred, metadata) + if not handler then + error(string.format('No handler for %s', directive[1])) end + + handler(captures, pattern_i, source, directive, metadata) end return metadata @@ -841,26 +882,22 @@ local function value_or_node_range(start, stop, node) return start, stop end ---- @param match TSQueryMatch ---- @return integer -local function match_id_hash(_, match) - return (match:info()) -end - ---- Iterate over all captures from all matches inside {node} +--- Iterates over all captures from all matches in {node}. --- ---- {source} is needed if the query contains predicates; then the caller +--- {source} is required if the query contains predicates; then the caller --- must ensure to use a freshly parsed tree consistent with the current --- text of the buffer (if relevant). {start} and {stop} can be used to limit --- matches inside a row range (this is typically used with root node --- as the {node}, i.e., to get syntax highlight matches in the current --- viewport). When omitted, the {start} and {stop} row values are used from the given node. --- ---- The iterator returns four values: a numeric id identifying the capture, ---- the captured node, metadata from any directives processing the match, ---- and the match itself. ---- The following example shows how to get captures by name: +--- The iterator returns four values: +--- 1. the numeric id identifying the capture +--- 2. the captured node +--- 3. metadata from any directives processing the match +--- 4. the match itself --- +--- Example: how to get captures by name: --- ```lua --- for id, node, metadata, match in query:iter_captures(tree:root(), bufnr, first, last) do --- local name = query.captures[id] -- name of the capture in the query @@ -876,8 +913,8 @@ end ---@param start? integer Starting line for the search. Defaults to `node:start()`. ---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`. --- ----@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch): ---- capture id, capture node, metadata, match +---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree): +--- capture id, capture node, metadata, match, tree --- ---@note Captures are only returned if the query pattern of a specific capture contained predicates. function Query:iter_captures(node, source, start, stop) @@ -887,10 +924,14 @@ function Query:iter_captures(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) + -- Copy the tree to ensure it is valid during the entire lifetime of the iterator + local tree = node:tree():copy() local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 }) - local apply_directives = memoize(match_id_hash, self.apply_directives, true) - local match_preds = memoize(match_id_hash, self.match_preds, true) + -- For faster checks that a match is not in the cache. + local highest_cached_match_id = -1 + ---@type table<integer, vim.treesitter.query.TSMetadata> + local match_cache = {} local function iter(end_line) local capture, captured_node, match = cursor:next_capture() @@ -899,18 +940,39 @@ function Query:iter_captures(node, source, start, stop) return end - if not match_preds(self, match, source) then - local match_id = match:info() - cursor:remove_match(match_id) - if end_line and captured_node:range() > end_line then - return nil, captured_node, nil, nil - end - return iter(end_line) -- tail call: try next match + local match_id, pattern_i = match:info() + + --- @type vim.treesitter.query.TSMetadata + local metadata + if match_id <= highest_cached_match_id then + metadata = match_cache[match_id] end - local metadata = apply_directives(self, match, source) + if not metadata then + metadata = {} + + local processed_pattern = self._processed_patterns[pattern_i] + if processed_pattern then + local captures = match:captures() - return capture, captured_node, metadata, match + local predicates = processed_pattern.predicates + if not self:_match_predicates(predicates, pattern_i, captures, source) then + cursor:remove_match(match_id) + if end_line and captured_node:range() > end_line then + return nil, captured_node, nil, nil + end + return iter(end_line) -- tail call: try next match + end + + local directives = processed_pattern.directives + metadata = self:_apply_directives(directives, pattern_i, captures, source) + end + + highest_cached_match_id = math.max(highest_cached_match_id, match_id) + match_cache[match_id] = metadata + end + + return capture, captured_node, metadata, match, tree end return iter end @@ -932,7 +994,7 @@ end --- -- `node` was captured by the `name` capture in the match --- --- local node_data = metadata[id] -- Node level metadata ---- ... use the info here ... +--- -- ... use the info here ... --- end --- end --- end @@ -951,7 +1013,7 @@ end --- (last) node instead of the full list of matching nodes. This option is only for backward --- compatibility and will be removed in a future release. --- ----@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata): pattern id, match, metadata +---@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree): pattern id, match, metadata, tree function Query:iter_matches(node, source, start, stop, opts) opts = opts or {} opts.match_limit = opts.match_limit or 256 @@ -962,6 +1024,8 @@ function Query:iter_matches(node, source, start, stop, opts) start, stop = value_or_node_range(start, stop, node) + -- Copy the tree to ensure it is valid during the entire lifetime of the iterator + local tree = node:tree():copy() local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts) local function iter() @@ -971,17 +1035,22 @@ function Query:iter_matches(node, source, start, stop, opts) return end - local match_id, pattern = match:info() + local match_id, pattern_i = match:info() + local processed_pattern = self._processed_patterns[pattern_i] + local captures = match:captures() - if not self:match_preds(match, source) then - cursor:remove_match(match_id) - return iter() -- tail call: try next match + --- @type vim.treesitter.query.TSMetadata + local metadata = {} + if processed_pattern then + local predicates = processed_pattern.predicates + if not self:_match_predicates(predicates, pattern_i, captures, source) then + cursor:remove_match(match_id) + return iter() -- tail call: try next match + end + local directives = processed_pattern.directives + metadata = self:_apply_directives(directives, pattern_i, captures, source) end - local metadata = self:apply_directives(match, source) - - local captures = match:captures() - if opts.all == false then -- Convert the match table into the old buggy version for backward -- compatibility. This is slow, but we only do it when the caller explicitly opted into it by @@ -990,11 +1059,11 @@ function Query:iter_matches(node, source, start, stop, opts) for k, v in pairs(captures or {}) do old_match[k] = v[#v] end - return pattern, old_match, metadata + return pattern_i, old_match, metadata end -- TODO(lewis6991): create a new function that returns {match, metadata} - return pattern, captures, metadata + return pattern_i, captures, metadata, tree end return iter end diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua index d64ef98d2d..06c54ac033 100644 --- a/runtime/lua/vim/version.lua +++ b/runtime/lua/vim/version.lua @@ -227,8 +227,7 @@ end ---@field to? vim.Version local VersionRange = {} ---- @private ---- +---@nodoc ---@param version string|vim.Version function VersionRange:has(version) if type(version) == 'string' then diff --git a/runtime/lua/vim/vimhelp.lua b/runtime/lua/vim/vimhelp.lua index 5579cc0174..a494d311b1 100644 --- a/runtime/lua/vim/vimhelp.lua +++ b/runtime/lua/vim/vimhelp.lua @@ -7,7 +7,7 @@ local M = {} --- Note: {patterns} is assumed to be sorted by occurrence in the file. --- @param patterns {start:string,stop:string,match:string}[] function M.highlight_groups(patterns) - local ns = vim.api.nvim_create_namespace('vimhelp') + local ns = vim.api.nvim_create_namespace('nvim.vimhelp') vim.api.nvim_buf_clear_namespace(0, ns, 0, -1) local save_cursor = vim.fn.getcurpos() |