diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2026-04-30 13:42:53 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-30 13:42:53 -0400 |
| commit | 83ee06463b94895d024a69f2d7a03abe16f6147c (patch) | |
| tree | 5b013bf2aadba947952d3828c227f44138127cb2 /runtime/lua/vim | |
| parent | 6a87ef75b3f141e175399e87c5518ec11102877d (diff) | |
| parent | 7c4845ff463b63324c4d45f03fdd58b7f70e54ba (diff) | |
| download | rneovim-83ee06463b94895d024a69f2d7a03abe16f6147c.tar.gz rneovim-83ee06463b94895d024a69f2d7a03abe16f6147c.tar.bz2 rneovim-83ee06463b94895d024a69f2d7a03abe16f6147c.zip | |
Merge #39488 from justinmk/uiselect
Diffstat (limited to 'runtime/lua/vim')
| -rw-r--r-- | runtime/lua/vim/_core/ex_cmd.lua | 55 | ||||
| -rw-r--r-- | runtime/lua/vim/_core/spell.lua | 21 | ||||
| -rw-r--r-- | runtime/lua/vim/_core/swapfile.lua | 86 | ||||
| -rw-r--r-- | runtime/lua/vim/_core/tag.lua | 34 |
4 files changed, 178 insertions, 18 deletions
diff --git a/runtime/lua/vim/_core/ex_cmd.lua b/runtime/lua/vim/_core/ex_cmd.lua index 81232abf74..a1a7ddb07d 100644 --- a/runtime/lua/vim/_core/ex_cmd.lua +++ b/runtime/lua/vim/_core/ex_cmd.lua @@ -6,11 +6,25 @@ local uv = vim.uv local N_ = vim.fn.gettext --- Parsed ex command arguments for builtin commands, passed from C via `nlua_call_excmd`. ---- Inherits fields from user command args: args, bang, line1, line2, range, count, reg, smods. +--- Inherits fields from user command args: name, args, bang, line1, line2, range, count, reg, smods. +--- Note: For builtin commands `name` is the canonical command name. --- @class vim._core.ExCmdArgs : vim.api.keyset.create_user_command.command_args local M = {} +--- Apply the `:filter[!] /pattern/` modifier to a single message. See also `message_filtered()`. +--- +--- @param filter vim.api.keyset.cmd_mods_filter ":filter" mod. +--- @param msg string Message to test. +--- @return boolean # True if `msg` should be skipped (not displayed). +function M.filter(filter, msg) + if not filter or filter.pattern == '' then + return false + end + local match = vim.regex(filter.pattern):match_str(msg) ~= nil + return match == filter.force +end + --- @param msg string local function echo_err(msg) api.nvim_echo({ { msg } }, true, { err = true }) @@ -266,4 +280,43 @@ function M.ex_uptime() api.nvim_echo({ { N_('Up %s'):format(uptime_display) } }, true, {}) end +--- `:oldfiles` and `:browse oldfiles`. Lists v:oldfiles (plain `:oldfiles`) or shows (async) +--- vim.ui.select() picker (`:browse oldfiles`) and edits the chosen file. +--- @param eap vim._core.ExCmdArgs +function M.ex_oldfiles(eap) + local files = vim.v.oldfiles + if not files or #files == 0 then + api.nvim_echo({ { N_('No old files') } }, false, {}) + return + end + + if eap.smods.browse then + vim.ui.select(files, { + prompt = N_('Select an oldfile:'), + kind = 'oldfiles', + }, function(_, idx) + if idx then + api.nvim_cmd({ + cmd = 'edit', + args = { vim.fn.expand(files[idx]) }, + magic = { file = false, bar = true }, -- May contain '%' (e.g. swapfiles), don't expand. + }, {}) + end + end) + return + end + + -- `:oldfiles`: list the entries. Honor `:filter /pat/[!]` per entry. + local lines = {} ---@type [string][] + for i, f in ipairs(files) do + if not M.filter(eap.smods.filter, f) then + lines[#lines + 1] = { ('%d: %s\n'):format(i, f) } + end + end + if #lines == 0 then + return + end + api.nvim_echo(lines, false, {}) +end + return M diff --git a/runtime/lua/vim/_core/spell.lua b/runtime/lua/vim/_core/spell.lua index d6bdc010a3..888c28da32 100644 --- a/runtime/lua/vim/_core/spell.lua +++ b/runtime/lua/vim/_core/spell.lua @@ -1,4 +1,3 @@ -local select_blocking = require('vim._core.ui').select_blocking local N_ = vim.fn.gettext local M = {} @@ -10,14 +9,15 @@ local M = {} --- @field altscore? integer Secondary score (only set when 'spellsuggest' contains "double" or "best"). --- @field salscore? boolean True if the score came from sound-alike comparison (only set alongside `altscore`). ---- Called from `spell_suggest()` (`z=`) to let the user pick from `items` via ---- |vim.ui.select()|. +--- Implements `spell_suggest()` (`z=`) via vim.ui.select(). +--- +--- async: returns immediately, the chosen suggestion is applied later +--- by re-running `:normal! [idx]z=` from `on_choice`. --- --- @param items vim._core.spell.Suggestion[] --- @param bad string The misspelled word being replaced. ---- @return integer? # 1-based index of the chosen suggestion, or nil if cancelled. -function M.suggest_select(items, bad) - return select_blocking(items, { +function M.select_suggest(items, bad) + vim.ui.select(items, { prompt = N_('Change "%s" to:'):format(bad), kind = 'spell', format_item = function(s) @@ -30,7 +30,14 @@ function M.suggest_select(items, bad) end return ('"%s"%s%s'):format(s.word, extra, score) end, - }) + }, function(_, idx) + if not idx then + return + end + -- Queue ":normal! [idx]z=" as user input, so the recursive spell_suggest runs via the normal + -- input-dispatch loop. Using vim.schedule + vim.cmd can hang bc of "Press ENTER". + vim.fn.feedkeys(vim.keycode(('<Cmd>normal! %dz=<CR>'):format(idx)), 'in') + end) end return M diff --git a/runtime/lua/vim/_core/swapfile.lua b/runtime/lua/vim/_core/swapfile.lua new file mode 100644 index 0000000000..7e3bee993f --- /dev/null +++ b/runtime/lua/vim/_core/swapfile.lua @@ -0,0 +1,86 @@ +local api = vim.api +local N_ = vim.fn.gettext + +local M = {} + +--- Renders a swap file as a multi-line block: +--- ``` +--- %home%foo%bar%README.md.swl +--- dated: Thu Apr 23 17:25:52 2026 +--- file name: ~foo/bar/README.md +--- modified: no +--- user name: justin host name: minime +--- process ID: 10521 (STILL RUNNING) +--- ``` +--- @param path string +--- @return string +local function format_swap(path) + local info = vim.fn.swapinfo(path) + local mtime = info.mtime and vim.fn.strftime('%a %b %d %H:%M:%S %Y', info.mtime) or '?' + local lines = { + vim.fs.basename(path), + (' dated: %s'):format(mtime), + } + if info.error then + lines[#lines + 1] = (' [%s]'):format(info.error) + else + lines[#lines + 1] = (' file name: %s'):format( + info.fname == '' and '[No Name]' or info.fname + ) + lines[#lines + 1] = (' modified: %s'):format(info.dirty == 1 and 'YES' or 'no') + if info.user ~= '' or info.host ~= '' then + local parts = {} ---@type string[] + if info.user ~= '' then + parts[#parts + 1] = ('user name: %s'):format(info.user) + end + if info.host ~= '' then + parts[#parts + 1] = ('host name: %s'):format(info.host) + end + lines[#lines + 1] = (' %s'):format(table.concat(parts, ' ')) + end + if info.pid > 0 then + lines[#lines + 1] = (' process ID: %d (STILL RUNNING)'):format(info.pid) + end + end + return table.concat(lines, '\n') +end + +--- Implements `:recover` (when there are multiple swap files): let the user pick via vim.ui.select(). +--- +--- async: returns immediately, then schedules `:recover {path}` on the chosen swapfile. +--- +--- @param items string[] List of swapfile paths. +function M.select_swap(items) + vim.ui.select(items, { + prompt = N_('Enter number of swap file to use (q or empty cancels):'), + kind = 'swap', + format_item = format_swap, + }, function(_, idx) + if not idx then + return + end + -- Queue ":recover! <swapfile>" as user input, so the recursive recovery runs via the normal + -- input-dispatch loop. Using vim.schedule + vim.cmd can hang bc of "Press ENTER". + vim.fn.feedkeys( + vim.keycode(('<Cmd>recover! %s<CR>'):format(vim.fn.fnameescape(items[idx]))), + 'in' + ) + end) +end + +--- Implements `nvim -r` (no arg): list every swapfile found in 'directory'. +--- +--- @param items string[] List of swapfile paths. +function M.list_swaps(items) + local lines = { { N_('Swap files found:') .. '\n' } } ---@type [string][] + if #items == 0 then + lines[#lines + 1] = { ' ' .. N_('-- none --') } + else + for i, path in ipairs(items) do + lines[#lines + 1] = { ('%d. %s\n'):format(i, format_swap(path)) } + end + end + api.nvim_echo(lines, false, {}) +end + +return M diff --git a/runtime/lua/vim/_core/tag.lua b/runtime/lua/vim/_core/tag.lua index 9d6c3cffbd..3c0c2f544f 100644 --- a/runtime/lua/vim/_core/tag.lua +++ b/runtime/lua/vim/_core/tag.lua @@ -1,4 +1,3 @@ -local select_blocking = require('vim._core.ui').select_blocking local N_ = vim.fn.gettext local M = {} @@ -11,34 +10,49 @@ local M = {} --- @field extra? string --- @field cur boolean True if this is the currently-active tagstack match. ---- Called from `do_tag()` (`:tselect`, ambiguous `:tag`, etc.) to let the user ---- pick from `matches` via |vim.ui.select()|. +--- Implements `do_tag()` (`:tselect`, ambiguous `:tag`, …) via vim.ui.select(). --- ---- @param items vim._core.tag.Match[] One per matching tag. ---- @return integer? # 1-based index of the chosen tag, or nil if cancelled. -function M.select(items) +--- async: returns immediately, the chosen tag is applied later by re-running +--- `:[mods] [idx]tag {tagname}` (or `stag`) from `on_choice`. +--- +--- @param eap vim._core.ExCmdArgs Original :tselect/:stselect/… invocation. +--- @param extra { items: vim._core.tag.Match[], tagname: string } +function M.select_tag(eap, extra) + local items, tagname = extra.items, extra.tagname + -- :stag/:stselect/:stjump need a split when re-invoked. + local stag = eap.name:sub(1, 1) == 's' + -- `eap.mods` is the raw modifier string (e.g. ":vert silent"). + local mods_str = eap.mods ~= '' and (eap.mods .. ' ') or '' + local taglen = 18 for _, m in ipairs(items) do taglen = math.max(taglen, vim.fn.strdisplaywidth(m.tag) + 2) end - return select_blocking(items, { + vim.ui.select(items, { prompt = N_('Type number and <Enter> (q or empty cancels):'), kind = 'tag', format_item = function(m) local marker = m.cur and '>' or ' ' local kind = m.kind or '' - local extra = m.extra and (' ' .. m.extra) or '' return ('%s %s %-4s %-' .. taglen .. 's %s%s'):format( marker, m.pri, kind, m.tag, m.file, - extra + m.extra and (' ' .. m.extra) or '' ) end, - }) + }, function(_, idx) + if not idx then + return + end + -- Queue ":[mods] [idx](s)tag {tagname}" as user input, so the recursive do_tag runs via the + -- normal input-dispatch loop. Using vim.schedule + vim.cmd can hang bc of "Press ENTER". + local cmd = stag and 'stag' or 'tag' + vim.fn.feedkeys(vim.keycode(('<Cmd>%s%d%s %s<CR>'):format(mods_str, idx, cmd, tagname)), 'in') + end) end return M |