aboutsummaryrefslogtreecommitdiff
path: root/test/functional/api/vim_spec.lua
Commit message (Collapse)AuthorAge
* fix(options): out-of-range number in 'iskeyword' aborts Nvim (#42410)ump45nose3 days
| | | | | | | | | | | | Problem: A number in 'iskeyword', 'isfname' or 'isident' that overflows (e.g. via a modeline) hits abort() or an assert in getdigits_int(). Solution: Parse the numbers non-strictly so an out-of-range value gives E474. Fixes #42409 AI-assisted
* fix(options): extend dry-run validationLewis Russell4 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: nvim_set_option_value() dry runs skip checks in did_set_* callbacks. This logic also prepares values needed to apply the option, such as tab widths or a compiled regexp. Moving the checks into validation callbacks needs a way to save that work for did_set_*. Solution: Store prepared values in optset_T so did_set_* can consume them after the option is stored. Dry runs perform the same validation, then free the prepared values without changing live state. This extends dry-run validation for: - 'cedit': reject invalid command-line window keys. - 'completeitemalign': require each completion column exactly once. - 'cursorlineopt': reject empty values and conflicting line modes. - 'encoding': accept only UTF-8 and its aliases. - 'fileencoding': reject lists and local changes with 'nomodifiable'. - 'messagesopt': require mandatory fields and enforce numeric limits. - 'spellcapcheck': return regexp errors, including under :silent!. - 'spellsuggest': reject conflicting suggestion modes. - 'varsofttabstop' and 'vartabstop': reject invalid tab widths. - 'wildmode': reject more than four completion stages. 'makeencoding' also uses the shared encoding preparation. Separating value preparation exposed a bug in 'vartabstop' and 'varsofttabstop': failed assignments could discard local tab widths, and global assignments could overwrite them. Fix this by preparing new widths separately and applying them only after a valid local assignment. AI-assisted
* fix(api): nvim_set_option_value() "remove" ignores non-adjacent items #42351Justin M. Keyes5 days
| | | | | | | | | | | | | Problem: `nvim_set_option_value('fo', {'c','r','o'}, {operation='remove'})` does nothing on "jcqrol": the items are joined into one `:set-=` value, which only matches a contiguous substring. Removing map keys, e.g. {'eol','space'} from 'listchars', never matches. `vim.opt:remove{…}` inherits this. (Regression from f34ee3da80e8; 429338e74bf4 fixed the case where the flags are adjacent and in order.) Solution: Remove each item separately.
* fix(api): crash on invalid :tab address #42328glepnir7 days
| | | | | | | | Problem: nvim_parse_cmd("'btab", {}) and typing ":'btab" crash, parse_cmdline() continues with a NULL eap->cmd after an invalid :tab address. Solution: Fail if parse_command_modifiers() set an error.
* feat(api): nvim_chan_set()Justin M. Keyes10 days
| | | | | | | | | | | | | | | | | | | Problem: Only the stdio channel can "own" a server (self-exit on close). After :restart the TUI reconnects over a socket, so the restarted server does not have "self-exit" behavior when the channel closes (e.g. by UI disconnect, host-terminal close, etc.), which leaves a dangling server. Solution: - Introduce `nvim_chan_set`. (`nvim_set_client_info` doesn't fit: exposing it to Lua would require a `channel` param, which is not backwards-compatible, plus it's awkward because it doesn't merge info.) - Broadcast per-channel properties in the "restart" UI event as an opaque dict (a potentially useful pattern for other UI-events in the future). - UIs are expected to restore the channel properties on restart. - `nvim_get_chan_info()` reports "detach". - Remove `nvim__chan_set_detach()`.
* fix(terminal): nvim_open_term crash in zero-size window #42248Justin M. Keyes13 days
| | | | | | | | | | | | | Problem: nvim_open_term() sizes the terminal from the current window, which may have 0 rows (bordered float, 'cmdheight' taking all rows). libvterm aborts when resizing from 0 rows. Solution: In `terminal_alloc()`, treat zero width/height as the pty default (80x24), like jobstart(term=true). Open question: nvim_win_get_height() on such a float returns -1 (winheight() returns 0).
* test: simplify functional test expectationsLewis Russell2026-10-05
| | | | | | | | | | | | Problem: Repeated default fields and field-by-field assertions obscure the values that each functional test is meant to check. Solution: Add eq_partial() to check selected record fields while preserving list length and order. Use local helpers to fill fixed expected defaults where tests need full equality. Keep explicit absence checks and test-specific values at the call sites. AI-assisted
* Merge pull request #41971 from bfredl/noisprintbfredl2026-09-22
|\ | | | | refactor(charset)!: delete 'isprint' and cleanup 'iskeyword' and friends
| * refactor(charset)!: delete 'isprint' and cleanup 'iskeyword' and friendsbfredl2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | In the `enc=utf-8` only world, the 'isprint' option is quite obsolete. The function is only consulted for decoded U+0080 to U+00FF chars whose printability is defined by the unicode charset, just like higher codepoints. Meanwhile, invalid `\x80` to `\xFF` bytes are always unprintable. Disentangle 'iskeyword', 'isfname' and 'isident' from each other by giving them all separate bitsets. Now these can be updated individually. These updaters can back out of errors gracefully, no need for extra panic logic in option[str].c to restore partially updated chartabs.
* | fix(api): preserve option trust during mergesLewis Russell2026-09-22
|/ | | | | | | | | | | | | Problem: API append, prepend, and remove operations reach the option setter as full replacements. This can clear an expression's untrusted flag and allow it to write files, unlike equivalent :set operations. Solution: Pass replacement status through the option setters so merges preserve the existing value's trust restrictions. AI-assisted
* refactor(api): reuse parse_cmdline in nvim_cmd #38508glepnir2026-09-22
| | | | | | | | | Problem: nvim_cmd() duplicates Ex parsing: it sets the address type and default range itself and builds the whole command line in build_cmdline_str(). Solution: Build only the range, command name and arguments, and let parse_cmdline() fill in exarg_T.
* refactor(option): simplify error buffer handlingLewis Russell2026-09-21
| | | | | | | | | | | | | | | | | | | | | | | | | | Problem: Option validators and setters repeatedly pass error buffers and their sizes separately. Some setters return messages in static storage. Solution: - Add CharBuf for borrowed writable storage, with its length including space for a terminating NUL. - Move option error buffers to the callers that report errors. Preserve existing buffer sizes, truncation, and calls that do not need a message. - Consolidate option error formatting and the duplicated code that lists valid option values. - Return error messages directly from do_one_set_option(). - Remove unused arguments from get_option_newval() and set_option_value_for(). - Handle legacy TTY assignments in the existing callers and remove the redundant compatibility wrapper. - Reuse CharBuf for Tree-sitter query errors. - Pass CharBuf through the statusline and statuscolumn builders, retaining the buffer sizes provided by their callers, including title formatting. AI-assisted
* refactor(options): move schema checks into validate_option_value()Lewis Russell2026-09-18
| | | | | | | | | | | | | Problem: Dict schema checks are mixed into individual option validators. Solution: Check schemas before calling option validators. Remove the redundant checks and callback entries. AI-assisted
* fix(options): extend nvim_set_option_value() dry-run validationLewis Russell2026-09-18
| | | | | | | | | | | | | | | | Problem: nvim_set_option_value() dry runs skip checks in did_set_* callbacks. Solution: Split validation out of these callbacks so dry runs and normal assignments use the same checks. Read 'mkspellmem' limits when building spell files. Keep the 'spellsuggest' limit unchanged when an assignment fails. AI-assisted
* fix(options): validate nvim_set_option_value() dry runsLewis Russell2026-09-18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: nvim_set_option_value() with dry_run=true accepts invalid values such as foldmethod='nonsense'. Validation is mixed with side effects in the did_set_* callbacks, so dry runs skip it. Solution: Split value validation out of the did_set_* callbacks into separate validation_cb callbacks. Use these checks for both normal assignments and nvim_set_option_value() dry runs. Keep side effects in did_set_*. Replace direct foldenable/foldlevel assignments in C folding commands with setters. This gives C commands the same OptionSet and synchronization behavior as Lua assignments. Use normal setters for 'tabstop' and 'vartabstop' in :retab so C code indentation and folds based on indentation use the new tab widths. Direct assignments could leave them using the old widths. :retab temporarily turns off 'list' to calculate tab widths. Previously, :retab -1 reported an error and returned without restoring 'list'. Check the argument before turning 'list' off, so an invalid argument leaves the option unchanged. AI-assisted
* fix(lua): add missing param annotations (#41911)Lewis Russell2026-09-15
|
* fix(statusline): "%.0" renders garbage #41685Volodymyr Chernetskyi2026-09-06
| | | | | | | | | | | | | | | | | | | Problem: `getdigits_int(pp, strict, def)` returns `def` only when the number fails to parse or overflows, never when the text parses to a literal `0`, so `%.0` leaves `maxwid` at 0. Both render paths then break down: `l > maxwid` is always true and `while (l >= maxwid)` consumes the whole string, so `%.0f` comes out as `<` and `%.0l` as `0>3`. Vim guards for this right after the same call. Nvim had the guard too until 3344cffe7b ("getdigits: introduce `strict`, `def` parameters"), which folded it into the new `def` argument and so changed what a literal zero means. The neighbouring `minwid` call was translated correctly. Solution: Fall back to 50 when the parsed width is not positive, as Vim does. AI-assisted
* fix(api): leaked error message in nvim_eval_statusline() #41690Volodymyr Chernetskyi2026-09-04
| | | | | | | | | | | | Problem: `find_window_by_handle()` returns NULL only after `VALIDATE_INT` has already set `err`, so the `api_set_error()` that followed allocated a second message over the first pointer and leaked it. Solution: Drop the redundant call and keep the helper's message, which is already set and better worded. AI-assisted
* fix(editor): shada "force" load clears v:oldfiles #41662Justin M. Keyes2026-09-03
| | | | | | | | | | | | Problem: `shada_read()` rebuilds `v:oldfiles` for any "forced" read, which first *clears* the list. For an in-memory caller such as multicursor, the list is never repopulated. So `v:oldfiles` is empty after `Q` + edit. Solution: Rebuild `v:oldfiles` only when `kShaDaGetOldfiles` is requested. `shada_read_everything()` (`:rshada[!]`, startup) always requests it. old bug from 411a06c8b6e9
* feat(multicursor): MC HAMMER #41587Justin M. Keyes2026-09-01
| | | | | | | | | | | | | | | | | | | | | Other (squashed) commits: fix(tui): emit ui_send output atomically with the frame Problem: tui_ui_send() writes directly to the TTY, bypassing the output buffer. Sequences sent via nvim_ui_send() (e.g. kitty multiple-cursors, or visual-dot-repeat) always arrive in a separate TTY write from the frame they were computed for. This manifests as "tearing", or e.g. in the case of multicursor the terminal renders text with stale cursor overlays. Solution: - tui_ui_send(): while a frame is being assembled (pending invalid regions or buffered output), buffer instead of writing directly. - Out-of-frame sends (tty queries, clear-on-disable) still write immediately. - mcursor.lua: emit the terminal-cursor update at the end of the redraw cycle (`on_end`, when screen positions are final) instead of vim.schedule().
* fix(lifecycle): late signal kills Nvim mid-teardownJustin M. Keyes2026-08-19
| | | | | | | | | | | | | | | | | | | | Problem: A deadly signal arriving during teardown can kill Nvim while it is preserving swapfiles. `os_exit()` ignores deadly signals via `signal_reject_deadly()`, but `signal_teardown()` => `uv_signal_stop()` resets them to the default behavior, so SIGHUP arriving after that kills the process: [Process exited 129] // 128 + SIGHUP This is a race when closing a pty: kernel sends SIGHUP to foreground process group *and* the reads return EOF, so `chanclose()` on a TUI job prepares to exit twice. This means it is unpredictable whether Nvim exits 1 or is terminated. Solution: Ignore deadly signals once the watchers are closed. Only SIGKILL interrupts it now.
* feat(options): lua closure/function optionsJustin M. Keyes2026-07-31
| | | | | | | | | | | | | | | Problem: Cannot assign Lua functions/closures to "func" ('completefunc', 'tagfun', …) or "expr" ('foldexpr', 'indentexpr', …) options. Solution: - Store "func"/"expr" options as `Callback` instead of string. - Delete oceans of copy-pasted code. - BREAKING: LuaRef returned via RPC/Vimscript is now represented as `"<Lua N: file:line>"` (like what `:map` shows) instead of `nil`. - Note: `man.vim` still uses `v:lua` string, bc it's a vimscript ftplugin. Helped-by: Lewis Russell <lewis6991@gmail.com>
* feat(exmode): "1q:", :exmode #41010Justin M. Keyes2026-07-27
| | | | | | | | | Problem: Want `gQ` for _le multicursor_. Solution: - Don't use `gQ` for exmode. - Introduce `:exmode`. - Introduce `[count]q:` as an alias to `:exmode`.
* refactor(exmode): Ex-mode as cmdwin + Lua #40991Justin M. Keyes2026-07-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: POSIX-compatible Ex-mode requires special-cases all over the codebase to match various quirks that don't actually matter to users. - The main utility of *interactive* Ex-mode is its REPL behavior, and that can be achieved with `cmdwin`, which also gains extra UX benefits. - The main utility of *non-interactive* `nvim -es` is for shell scripting, where Ex-mode quirks are mostly unhelpful (e.g. the "Entering Ex mode" message). Solution: - Reimplement *interactive* Ex-mode as a "persistent, insert-mode cmdwin" in Lua. - "nvim -e/-E" is simply an alias to "gQ". - Reframe *non-interactive* Ex-mode (`nvim -es`) as "script mode". - Drop POSIX Ex-mode quirks. Improvements: - "nvim -V1 -es" output ends with a final newline! - "nvim -V1 -es" no longer shows the "Entering Ex mode" msg. (This was pointless noise, unwanted for scripting purposes.) - stdin is no longer typeahead. Scripts (":lua io.read()") can read stdin as data. - Empty line is a no-op: a stray blank line no longer moves the cursor (deviates from POSIX ex "+1"), no longer exits 1 at EOF (E501). Preserved behavior: - cursor starts at "$" - mode()=="cv" (for non-interactive) - multiline commands (:append/:function/heredoc pull continuation lines) - bare-range print - :print=>stdout - -V1=>stderr - CRLF input - continue-after-error and exit codes Dropped (regressed) POSIX behavior (non-interactive): - Event loop only ticks while/between commands, not while blocked waiting for a stdin line. - ":g/pat/visual...Q" - input()/getchar()/":s/x/y/c" no longer consume stdin lines as answers: Nvim stops at end-of-input, skipping the rest of the script, exit 0. Use ":lua io.read()" instead. - If users care about this they should use interactive Ex-mode (`gQ`). - ":@r" stops at end of the register instead of continuing to read cmdline input from stdin.
* test(harness): migrate away from magic globalsJustin M. Keyes2026-07-21
| | | | | | | | | | | | | | | | | | | | | Problem: The magic globals `it`, `describe`, etc., are more trouble than they are worth. - Hooking into `after_each` requires `getfenv()` hacks. - They confuse luals/emmylua, because the top-level `.luarc.json` isn't merged with `test/.luarc.json` (apparently a luals limitation?) - They totally defeat discoverability because the user just has to "know" about the various magic symbols. So they harm DX, which means they serve no purpose at all. Solution: - Expose the test API from `testutil`, so tests can call `t.it()`, `t.describe()`, etc., in the conventional way. - Drop `getfenv()` hacks. - Drop the `setfenv()` injection in `load_chunk`. - Drop `test/_meta.lua`.
* fix(lua): vim.keycode cleanup #40817Justin M. Keyes2026-07-18
|
* fix(showcmd): avoid stale %S contents #40747Barrett Ruth2026-07-15
| | | Co-authored-by: zeertzjq <zeertzjq@outlook.com>
* fix(path): normalize separators (slashes) #39155tao2026-07-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: Continue separators normalization, and try to keep it at the nvim <-> external boundary, e.g., `fn.xxx`, `api.xxx`, `:xxx` Solution: some key changes - normalize `$HOME-windows` - normalize entry points of changing directory - `nvim_set_current_dir()`, `chdir()` - normalize the named pipe - `--listen` arg, `--server` arg, `:restart` - `serverstart()`, `sockconnect()`, `serverstop()` - make `expand()` respect 'shellslash' again - clean up `did_set_shellslash` - replace `forward_slash` with `TO_SLASH` - remove obsolete `TMP_PATHSEPSTR` - remove `slash_adjust` in `do_autocmd_dirchanged`? - make `fnamemodify()` always return `/` (except when substituting separators via `:s`, `:gs` ?) Note: - these funcs still apply `slash_adjust` on return, as before: `:pwd`, `chdir()`, `exepath()`, `getcwd()` - these funcs alwarys return `/`, unlike before the normalization PRs - `getcompletion()`, `finddir()`, `findfile()` Also - clean up code, comments, formatting and tests - move `TO_SLASH` from `f_chdir` to `changedir_func` - move `TO_SLASH` from `f_bufadd` to `buflist_new` Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
* test: unreliable "API nvim_list_chans, nvim_get_chan_info stream=job ↵Justin M. Keyes2026-07-11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | :terminal channel" FAILED ...vim/build/Xtest_xdg_api/test/functional/api/vim_spec.lua @ 3256: API nvim_list_chans, nvim_get_chan_info stream=job :terminal channel Expected values to be equal. Expected: { argv = { "/home/runner/work/neovim/neovim/build/bin/nvim", "-u", "NONE", "-i", "NONE" }, buf = 2, buffer = 2, exitcode = 1, id = 4, mode = "terminal", pty = "", stream = "job" } Actual: { argv = { "/home/runner/work/neovim/neovim/build/bin/nvim", "-u", "NONE", "-i", "NONE" }, buf = 2, buffer = 2, exitcode = 129, id = 4, mode = "terminal", pty = "", stream = "job", <metatable> = { __tostring = <function 1> } } stack traceback: ...vim/build/Xtest_xdg_api/test/functional/api/vim_spec.lua:3300: in function <...vim/build/Xtest_xdg_api/test/functional/api/vim_spec.lua:3256>
* fix(input): stops accepting input after a large paste/input burst #40585prwang2026-07-06
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: Neovim can permanently stop accepting keyboard input after a large paste, or after any sufficiently large input burst. The screen still redraws and honours window resize, but typed keys have no effect and the session must be killed. A paste that triggers the freeze is applied only partially. Root cause: `src/nvim/os/input.c` holds `input_buffer[INPUT_BUFFER_SIZE]` (16386 bytes), compacted left rather than used as a ring: - `input_get()` drains the buffer by advancing `input_read_pos`, but never rewinds the cursors when it empties (`input_read_pos == input_write_pos`). - The only rewind lives in `input_enqueue_raw()`, which `input_enqueue()` reaches only inside `while (input_space() >= 19 && ptr < end)` (19 is the maximum expansion of one `<x>` key form). So once input fills the buffer to within 19 bytes of the top and is then fully drained, the cursors are pinned near the top with `input_space() < 19`. The gate never reopens, `input_enqueue()` never rewinds, and all further input is silently dropped: `input_available()` stays 0 and the editor blocks forever in `state_enter()` → `input_get()`. Redraw and resize run on independent paths, which is why the UI looks alive while the keyboard is ignored. Solution: Rewind the read/write cursors when the buffer is empty, at the start of `input_enqueue()`. The reset moves no data in the empty case and guarantees the space gate can reopen; the non-empty case self-heals as the editor drains. Test case: A reproducer (no terminal required; drives `nvim --embed` over msgpack-RPC via `nvim_input`) floods the input buffer, lets the editor drain it, then probes with `:qa!`. It shows a sharp threshold at `INPUT_BUFFER_SIZE − 18`: | `--fill` | master | with this fix | |----------|-----------|---------------| | ≤ 16367 | quits | quits | | ≥ 16368 | **hangs** | quits | - On current `master`, the freeze reproduces at `fill=16368`; the patched build quits for every fill up to 100000. - Interactive: a ~40 kB bracketed paste into Insert mode or a `:terminal` no longer freezes.
* fix(options): crash when setting option to empty dict #40492zeertzjq2026-06-30
|
* perf(options): avoid Lua<=>API roundtrips #40477Justin M. Keyes2026-06-29
| | | | | | | | | | | | | | | | Problem: - Lua<=>API roundtrips - Although we prefer Lua for most business-logic code, doing this conversion in C makes sense in this case because: 1. setting options is a hot path 2. most of the options logic lives in C 3. the current arrangement is MORE verbose and requires MORE code Solution: Move conversion to a C util. - nvim_set_option_value passes the raw Object (scalar, Array, or Dict) to `object_as_optval_for()` which flattens it to the canonical `:set` string and validates the type. - drop `convert_value_to_vim`, eliminate its roundtrip.
* feat(api): nvim_set_option_value(operation=...) #39849Kyle2026-06-28
| | | | | | | | | | | | | Problem: `nvim_set_option_value` cannot "update" options similar to `:set opt=`, `:set opt+=`, etc. The Lua impls of "vim.opt" / "vim.o" have incomplete, bespoke reimplementations of those operations. ref #38420 Solution: - Add `operation` param to `nvim_set_option_value`, which may be "set", "append", "prepend", or "remove". - Use this feature to implement `vim.opt` / `vim.o`.
* fix(stdpath): avoid DOS 8.3 filenames for "cache", "run" #40201tao2026-06-12
| | | | | | | | | | | | Problem: stdpath() may return a DOS 8.3 "shortened" filename, because Windows truncates long usernames into `6ch~N` names in `TEMP/TMP` env vars. We don't want to "leak" them into Nvim. Solution: For "run", pass `true` to `vim_FullName` to expand 8.3 filenames. For "cache", call `os_realpath` to expand 8.3 filenames. Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
* fix(path): `nvim_get_runtime_file` fails on DOS 8.3 filename #40144tao2026-06-10
| | | | | | | | | | | | | | | | | | Problem: stdpath() may return a DOS 8.3 "shortened" filename, because Windows truncates some long usernames into `6ch~N` names. Then features such as `nvim_get_runtime_file` fail to find the file. Analysis: When expanding an 8.3 filename path like `C:/Users/ADMINI~1/AppData/*`, we treat `~` as a special character and first check whether a directory named `ADMINI~1` exists under `Users`. Since no such directory actually exists, the expansion fails. Solution: Treat `~` as a literal character in `do_path_expand`. Since the `~/` case is already handled in `gen_expand_wildcards`, any remaining `~` is just a literal character and will later be escaped to `\~` by `file_pat_to_reg_pat` if needed.
* feat(api): tab-local option scope #39811Justin M. Keyes2026-05-17
| | | | | | | | | | | | | | | | | | Problem: 2d795face6e4 added support for tab-local options ('cmdheight') to `nvim_get_option_value`, but not to: nvim_get_option_info2() nvim_set_option_value(…, { tab = … }) gettabwinvar() Solution: - Update `options.lua` to model tab-local options. Introduce `kOptScopeTab`. - Handle tab scope in the options layer so it works for all options APIs. Note: - No change to `gettabvar()`. Not sure if needed/wanted. fix https://github.com/neovim/neovim/issues/31140
* refactor(api): tab-local nvim_get_option_value #39810Justin M. Keyes2026-05-16
|
* fix(api): nvim_get_option_value tab-local 'cmdheight' #39259EliWiegman2026-05-16
| | | | | | | Problem: API clients cannot query the tab-local value of 'cmdheight'. Solution: Allow nvim_get_option_value() to accept { tab = <tab-ID> } for 'cmdheight'.
* refactor: make nvim_put() trigger TextPut* with '_' as regnamezeertzjq2026-05-13
|
* fix(messages): reset redirection message column after :echon #39472luukvbaal2026-05-11
| | | | | | Problem: Message redirection column for captured output is not reset after :echon since (4260f73, e63346df). Solution: Ensure msg_ext_append is set before the kind with :echon.
* fix(messages): disallow source="nvim" progress msg #39315luukvbaal2026-05-06
| | | | | | | | | Problem: Internal progress messages use the "nvim" source (since ff68fd6b), plugins shouldn't be allowed to set the progress message source to "nvim". The message ID used for internal progress messages is not identifiable as such. Solution: Disallow setting opts->source to "nvim" with nvim_echo(). Refactor msg_progress() and callees to bypass nvim_echo(). Prepend message id for internal progress messages with "nvim.".