aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2026-04-30 13:42:53 -0400
committerGitHub <noreply@github.com>2026-04-30 13:42:53 -0400
commit83ee06463b94895d024a69f2d7a03abe16f6147c (patch)
tree5b013bf2aadba947952d3828c227f44138127cb2 /runtime/lua/vim
parent6a87ef75b3f141e175399e87c5518ec11102877d (diff)
parent7c4845ff463b63324c4d45f03fdd58b7f70e54ba (diff)
downloadrneovim-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.lua55
-rw-r--r--runtime/lua/vim/_core/spell.lua21
-rw-r--r--runtime/lua/vim/_core/swapfile.lua86
-rw-r--r--runtime/lua/vim/_core/tag.lua34
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