mirror of
https://github.com/neovim/neovim.git
synced 2026-08-26 17:11:48 +00:00
Merge #39488 from justinmk/uiselect
This commit is contained in:
@@ -216,10 +216,11 @@ TUI
|
||||
|
||||
UI
|
||||
|
||||
• |:tselect| delegates to |vim.ui.select()| instead of a bespoke internal
|
||||
selection routine.
|
||||
• |z=| (spell suggest) delegates to |vim.ui.select()| instead of a bespoke
|
||||
internal selection routine.
|
||||
• These builtin "picker" menus delegate to |vim.ui.select()|:
|
||||
• :browse oldfiles
|
||||
• |:recover|
|
||||
• |:tselect|
|
||||
• |z=| (spell suggest)
|
||||
|
||||
VIMSCRIPT
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
86
runtime/lua/vim/_core/swapfile.lua
Normal file
86
runtime/lua/vim/_core/swapfile.lua
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -7352,7 +7352,7 @@ static void f_substitute(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
static void f_swapfilelist(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
{
|
||||
tv_list_alloc_ret(rettv, kListLenUnknown);
|
||||
recover_names(NULL, false, rettv->vval.v_list, 0, NULL);
|
||||
recover_names(NULL, false, rettv->vval.v_list);
|
||||
}
|
||||
|
||||
/// "swapinfo(swap_filename)" function
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#include "nvim/highlight_group.h"
|
||||
#include "nvim/indent.h"
|
||||
#include "nvim/input.h"
|
||||
#include "nvim/lua/executor.h"
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/main.h"
|
||||
#include "nvim/mark.h"
|
||||
@@ -5033,54 +5034,8 @@ char *skip_vimgrep_pat(char *p, char **s, int *flags)
|
||||
return p;
|
||||
}
|
||||
|
||||
/// List v:oldfiles in a nice way.
|
||||
/// `:oldfiles` (sync) and `:browse oldfiles` (async).
|
||||
void ex_oldfiles(exarg_T *eap)
|
||||
{
|
||||
list_T *l = get_vim_var_list(VV_OLDFILES);
|
||||
int nr = 0;
|
||||
|
||||
if (l == NULL) {
|
||||
msg(_("No old files"), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
msg_start();
|
||||
msg_scroll = true;
|
||||
TV_LIST_ITER(l, li, {
|
||||
if (got_int) {
|
||||
break;
|
||||
}
|
||||
nr++;
|
||||
const char *fname = tv_get_string(TV_LIST_ITEM_TV(li));
|
||||
if (!message_filtered(fname)) {
|
||||
msg_outnum(nr);
|
||||
msg_puts(": ");
|
||||
msg_outtrans(tv_get_string(TV_LIST_ITEM_TV(li)), 0, false);
|
||||
msg_clr_eos();
|
||||
msg_putchar('\n');
|
||||
os_breakcheck();
|
||||
}
|
||||
});
|
||||
|
||||
// Assume "got_int" was set to truncate the listing.
|
||||
got_int = false;
|
||||
|
||||
// File selection prompt on ":browse oldfiles"
|
||||
if (cmdmod.cmod_flags & CMOD_BROWSE) {
|
||||
quit_more = false;
|
||||
nr = prompt_for_input(NULL, 0, false, NULL);
|
||||
msg_starthere();
|
||||
if (nr > 0 && nr <= tv_list_len(l)) {
|
||||
const char *const p = tv_list_find_str(l, nr - 1);
|
||||
if (p == NULL) {
|
||||
return;
|
||||
}
|
||||
char *const s = expand_env_save((char *)p);
|
||||
eap->arg = s;
|
||||
eap->cmdidx = CMD_edit;
|
||||
cmdmod.cmod_flags &= ~CMOD_BROWSE;
|
||||
do_exedit(eap, NULL);
|
||||
xfree(s);
|
||||
}
|
||||
}
|
||||
nlua_call_excmd("vim._core.ex_cmd", "ex_oldfiles", eap, &cmdmod, NULL);
|
||||
}
|
||||
|
||||
@@ -7639,8 +7639,7 @@ static void ex_tag_cmd(exarg_T *eap, const char *name)
|
||||
cmd = DT_LTAG;
|
||||
}
|
||||
|
||||
do_tag(eap->arg, cmd, eap->addr_count > 0 ? (int)eap->line2 : 1,
|
||||
eap->forceit, true);
|
||||
do_tag(eap, eap->arg, cmd, eap->addr_count > 0 ? (int)eap->line2 : 1, eap->forceit, true);
|
||||
}
|
||||
|
||||
enum {
|
||||
|
||||
@@ -212,7 +212,7 @@ void ex_help(exarg_T *eap)
|
||||
// It is needed for do_tag top open folds under the cursor.
|
||||
KeyTyped = old_KeyTyped;
|
||||
|
||||
do_tag(tag, DT_HELP, 1, false, true);
|
||||
do_tag(NULL, tag, DT_HELP, 1, false, true);
|
||||
|
||||
// Delete the empty buffer if we're not using it. Careful: autocommands
|
||||
// may have jumped to another window, check that the buffer is not in a
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "nvim/event/time.h"
|
||||
#include "nvim/ex_cmds.h"
|
||||
#include "nvim/ex_cmds_defs.h"
|
||||
#include "nvim/ex_docmd.h"
|
||||
#include "nvim/ex_getln.h"
|
||||
#include "nvim/garray.h"
|
||||
#include "nvim/garray_defs.h"
|
||||
@@ -167,11 +168,38 @@ static void nlua_push_cmdmod(lua_State *lstate, const cmdmod_T *cmod)
|
||||
lua_setfield(lstate, -2, "lockmarks");
|
||||
lua_pushboolean(lstate, cmod->cmod_flags & CMOD_NOSWAPFILE);
|
||||
lua_setfield(lstate, -2, "noswapfile");
|
||||
|
||||
// ":filter[!] /pattern/" modifier (same shape as `nvim_parse_cmd().mods.filter`).
|
||||
lua_newtable(lstate);
|
||||
lua_pushstring(lstate, cmod->cmod_filter_pat ? cmod->cmod_filter_pat : "");
|
||||
lua_setfield(lstate, -2, "pattern");
|
||||
lua_pushboolean(lstate, cmod->cmod_filter_force);
|
||||
lua_setfield(lstate, -2, "force");
|
||||
lua_setfield(lstate, -2, "filter");
|
||||
}
|
||||
|
||||
/// Pushes common exarg_T fields (bang, line1, line2, …) onto a table at the top of the stack.
|
||||
static void nlua_push_eap(lua_State *lstate, exarg_T *eap, const cmdmod_T *cmod)
|
||||
{
|
||||
// Canonical name (for builtin cmds); for usercmds `nlua_do_ucmd` sets "name" to the user-defined name.
|
||||
if (!IS_USER_CMDIDX(eap->cmdidx) && eap->cmdidx < CMD_SIZE) {
|
||||
lua_pushstring(lstate, get_command_name(NULL, eap->cmdidx));
|
||||
lua_setfield(lstate, -2, "name");
|
||||
}
|
||||
|
||||
// Modifier string (e.g. ":vert silent"). Same content as `nvim_parse_cmd().mods`.
|
||||
// Useful when forwarding the command verbatim, e.g. `feedkeys('<Cmd>'..eap.mods..' …<CR>')`.
|
||||
//
|
||||
// The size is chosen empirically to hold every modifier with room to spare; bump if more are added.
|
||||
char mods_buf[200] = { 0 };
|
||||
uc_mods(mods_buf, cmod, false);
|
||||
lua_pushstring(lstate, mods_buf);
|
||||
lua_setfield(lstate, -2, "mods");
|
||||
|
||||
// Structured form of `mods`.
|
||||
nlua_push_cmdmod(lstate, cmod);
|
||||
lua_setfield(lstate, -2, "smods");
|
||||
|
||||
lua_pushstring(lstate, eap->arg);
|
||||
lua_setfield(lstate, -2, "args");
|
||||
|
||||
@@ -203,9 +231,6 @@ static void nlua_push_eap(lua_State *lstate, exarg_T *eap, const cmdmod_T *cmod)
|
||||
}
|
||||
lua_setfield(lstate, -2, "fargs");
|
||||
}
|
||||
|
||||
nlua_push_cmdmod(lstate, cmod);
|
||||
lua_setfield(lstate, -2, "smods");
|
||||
}
|
||||
|
||||
#if __has_feature(address_sanitizer)
|
||||
@@ -2363,16 +2388,6 @@ int nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap, bool preview)
|
||||
lua_pushstring(lstate, nargs);
|
||||
lua_setfield(lstate, -2, "nargs");
|
||||
|
||||
// User commands also get a string "mods" field (in addition to "smods" from nlua_push_eap).
|
||||
//
|
||||
// The size of this buffer is chosen empirically to be large enough to hold
|
||||
// every possible modifier (with room to spare). If the list of possible
|
||||
// modifiers grows this may need to be updated.
|
||||
char buf[200] = { 0 };
|
||||
uc_mods(buf, &cmdmod, false);
|
||||
lua_pushstring(lstate, buf);
|
||||
lua_setfield(lstate, -2, "mods");
|
||||
|
||||
if (preview) {
|
||||
lua_pushinteger(lstate, cmdpreview_get_ns());
|
||||
|
||||
|
||||
@@ -511,10 +511,14 @@ int main(int argc, char **argv)
|
||||
// Decide about window layout for diff mode after reading vimrc.
|
||||
set_window_layout(¶ms);
|
||||
|
||||
// Recovery mode without a file name: List swap files.
|
||||
// Uses the 'dir' option, therefore it must be after the initializations.
|
||||
// "nvim -r" (recovery mode) without a file name: List swap files.
|
||||
if (recoverymode && fname == NULL) {
|
||||
recover_names(NULL, true, NULL, 0, NULL);
|
||||
typval_T items_tv;
|
||||
tv_list_alloc_ret(&items_tv, 0);
|
||||
recover_names(NULL, false, items_tv.vval.v_list);
|
||||
typval_T lua_args[] = { items_tv, { .v_type = VAR_UNKNOWN } };
|
||||
nlua_call_vimfn("vim._core.swapfile", "list_swaps", lua_args, NULL);
|
||||
tv_clear(&items_tv);
|
||||
os_exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
#include "nvim/globals.h"
|
||||
#include "nvim/highlight_defs.h"
|
||||
#include "nvim/input.h"
|
||||
#include "nvim/lua/executor.h"
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/main.h"
|
||||
#include "nvim/map_defs.h"
|
||||
@@ -789,28 +790,27 @@ void ml_recover(bool checkext)
|
||||
} else {
|
||||
directly = false;
|
||||
|
||||
// count the number of matching swapfiles
|
||||
len = recover_names(fname, false, NULL, 0, NULL);
|
||||
if (len == 0) { // no swapfiles found
|
||||
// Enumerate matching swapfiles into items_tv.
|
||||
typval_T items_tv;
|
||||
tv_list_alloc_ret(&items_tv, 0);
|
||||
recover_names(fname, true, items_tv.vval.v_list);
|
||||
int n_swaps = tv_list_len(items_tv.vval.v_list);
|
||||
|
||||
if (n_swaps == 0) {
|
||||
tv_clear(&items_tv);
|
||||
semsg(_("E305: No swap file found for %s"), fname);
|
||||
goto theend;
|
||||
}
|
||||
int i;
|
||||
if (len == 1) { // one swapfile found, use it
|
||||
i = 1;
|
||||
} else { // several swapfiles found, choose
|
||||
// list the names of the swapfiles
|
||||
recover_names(fname, true, NULL, 0, NULL);
|
||||
if (!ui_has(kUIMessages)) {
|
||||
msg_putchar('\n');
|
||||
}
|
||||
i = prompt_for_input(_("Enter number of swap file to use (0 to quit): "), 0, false, NULL);
|
||||
if (i < 1 || i > len) {
|
||||
goto theend;
|
||||
}
|
||||
if (n_swaps > 1) {
|
||||
// Several swapfiles found: prompt (async) via vim.ui.select().
|
||||
typval_T lua_args[] = { items_tv, { .v_type = VAR_UNKNOWN } };
|
||||
nlua_call_vimfn("vim._core.swapfile", "select_swap", lua_args, NULL);
|
||||
tv_clear(&items_tv);
|
||||
goto theend;
|
||||
}
|
||||
// get the swapfile name that will be used
|
||||
recover_names(fname, false, NULL, i, &fname_used);
|
||||
// One swapfile: use it directly.
|
||||
fname_used = xstrdup(tv_list_first(items_tv.vval.v_list)->li_tv.vval.v_string);
|
||||
tv_clear(&items_tv);
|
||||
}
|
||||
if (fname_used == NULL) {
|
||||
goto theend; // user chose invalid number.
|
||||
@@ -1274,28 +1274,22 @@ theend:
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the names of swapfiles in current directory and the directory given
|
||||
/// with the 'directory' option.
|
||||
/// Enumerate swapfiles for `fname` (or for the global swap dir if `fname` is NULL),
|
||||
/// appending each found path to `ret_list`.
|
||||
///
|
||||
/// Used to:
|
||||
/// - list the swapfiles for "nvim -r"
|
||||
/// - count the number of swapfiles when recovering
|
||||
/// - list the swapfiles when recovering
|
||||
/// - list the swapfiles for swapfilelist()
|
||||
/// - find the name of the n'th swapfile when recovering
|
||||
/// Used by `:recover` (`ml_recover()`), `nvim -r`, and `swapfilelist()`.
|
||||
///
|
||||
/// @param fname base for swapfile name
|
||||
/// @param do_list when true, list the swapfile names
|
||||
/// @param ret_list when not NULL add file names to it
|
||||
/// @param nr when non-zero, return nr'th swapfile name
|
||||
/// @param fname_out result when "nr" > 0
|
||||
int recover_names(char *fname, bool do_list, list_T *ret_list, int nr, char **fname_out)
|
||||
/// @param fname base for swapfile name, or NULL to list every swapfile in 'directory'.
|
||||
/// @param skip_curbuf exclude the current buffer's own active swapfile (used by `:recover`,
|
||||
/// not by `swapfilelist()`).
|
||||
/// @param ret_list receives the paths.
|
||||
void recover_names(char *fname, bool skip_curbuf, list_T *ret_list)
|
||||
FUNC_ATTR_NONNULL_ARG(3)
|
||||
{
|
||||
int num_names;
|
||||
char *(names[6]);
|
||||
char *tail;
|
||||
char *p;
|
||||
int file_count = 0;
|
||||
char **files;
|
||||
char *fname_res = NULL;
|
||||
#ifdef HAVE_READLINK
|
||||
@@ -1312,14 +1306,6 @@ int recover_names(char *fname, bool do_list, list_T *ret_list, int nr, char **fn
|
||||
#endif
|
||||
}
|
||||
|
||||
msg_ext_skip_flush = true;
|
||||
if (do_list) {
|
||||
// use msg() to start the scrolling properly
|
||||
msg_ext_set_kind("list_cmd");
|
||||
msg(_("Swap files found:"), 0);
|
||||
msg_putchar('\n');
|
||||
}
|
||||
|
||||
// Do the loop for every directory in 'directory'.
|
||||
// First allocate some memory to put the directory name in.
|
||||
String dir_name;
|
||||
@@ -1375,7 +1361,7 @@ int recover_names(char *fname, bool do_list, list_T *ret_list, int nr, char **fn
|
||||
// When no swapfile found, wildcard expansion might have failed (e.g.
|
||||
// not able to execute the shell).
|
||||
// Try finding a swapfile by simply adding ".swp" to the file name.
|
||||
if (*dirp == NUL && file_count + num_files == 0 && fname != NULL) {
|
||||
if (*dirp == NUL && tv_list_len(ret_list) + num_files == 0 && fname != NULL) {
|
||||
char *swapname = modname(fname_res, ".swp", true);
|
||||
if (swapname != NULL) {
|
||||
if (os_path_exists(swapname)) {
|
||||
@@ -1389,10 +1375,9 @@ int recover_names(char *fname, bool do_list, list_T *ret_list, int nr, char **fn
|
||||
}
|
||||
|
||||
// Remove swapfile name of the current buffer, it must be ignored.
|
||||
// But keep it for swapfilelist().
|
||||
if (curbuf->b_ml.ml_mfp != NULL
|
||||
&& (p = curbuf->b_ml.ml_mfp->mf_fname) != NULL
|
||||
&& ret_list == NULL) {
|
||||
if (skip_curbuf
|
||||
&& curbuf->b_ml.ml_mfp != NULL
|
||||
&& (p = curbuf->b_ml.ml_mfp->mf_fname) != NULL) {
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
// Do not expand wildcards, on Windows would try to expand
|
||||
// "%tmp%" in "%tmp%file"
|
||||
@@ -1411,50 +1396,9 @@ int recover_names(char *fname, bool do_list, list_T *ret_list, int nr, char **fn
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nr > 0) {
|
||||
file_count += num_files;
|
||||
if (nr <= file_count) {
|
||||
*fname_out = xstrdup(files[nr - 1 + num_files - file_count]);
|
||||
dirp = ""; // stop searching
|
||||
}
|
||||
} else if (do_list) {
|
||||
if (dir_name.data[0] == '.' && dir_name.data[1] == NUL) {
|
||||
if (fname == NULL) {
|
||||
msg_puts(_(" In current directory:\n"));
|
||||
} else {
|
||||
msg_puts(_(" Using specified name:\n"));
|
||||
}
|
||||
} else {
|
||||
msg_puts(_(" In directory "));
|
||||
msg_home_replace(dir_name.data);
|
||||
msg_puts(":\n");
|
||||
}
|
||||
|
||||
if (num_files) {
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
// print the swapfile name
|
||||
msg_outnum(++file_count);
|
||||
msg_puts(". ");
|
||||
msg_puts(path_tail(files[i]));
|
||||
msg_putchar('\n');
|
||||
StringBuilder msg = KV_INITIAL_VALUE;
|
||||
kv_resize(msg, IOSIZE);
|
||||
swapfile_info(files[i], &msg);
|
||||
bool need_clear = false;
|
||||
msg_multiline(cbuf_as_string(msg.items, msg.size), 0, false, false, &need_clear);
|
||||
kv_destroy(msg);
|
||||
}
|
||||
} else {
|
||||
msg_puts(_(" -- none --\n"));
|
||||
}
|
||||
ui_flush();
|
||||
} else if (ret_list != NULL) {
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
String name = concat_fnames(dir_name, cstr_as_string(files[i]), true);
|
||||
tv_list_append_allocated_string(ret_list, name.data);
|
||||
}
|
||||
} else {
|
||||
file_count += num_files;
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
// `files[i]` is already a full path (from `expand_wildcards`).
|
||||
tv_list_append_allocated_string(ret_list, xstrdup(files[i]));
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_names; i++) {
|
||||
@@ -1464,9 +1408,7 @@ int recover_names(char *fname, bool do_list, list_T *ret_list, int nr, char **fn
|
||||
FreeWild(num_files, files);
|
||||
}
|
||||
}
|
||||
msg_ext_skip_flush = false;
|
||||
xfree(dir_name.data);
|
||||
return file_count;
|
||||
}
|
||||
|
||||
/// Append the full path to name with path separators made into percent
|
||||
|
||||
@@ -3622,7 +3622,7 @@ bool get_visual_text(cmdarg_T *cap, char **pp, size_t *lenp)
|
||||
static void nv_tagpop(cmdarg_T *cap)
|
||||
{
|
||||
if (!checkclearopq(cap->oap)) {
|
||||
do_tag("", DT_POP, cap->count1, false, true);
|
||||
do_tag(NULL, "", DT_POP, cap->count1, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -433,10 +433,8 @@ int spell_check_sps(void)
|
||||
return OK;
|
||||
}
|
||||
|
||||
/// Let the user pick a spell suggestion. Delegates to `vim.ui.select()`.
|
||||
///
|
||||
/// @return 1-based index of the chosen suggestion, or 0 if cancelled.
|
||||
static int select_spell_suggestion(suginfo_T *sug)
|
||||
/// Let the user pick a spell suggestion. Delegates to (async) `vim.ui.select()`.
|
||||
static void select_spell_suggestion(suginfo_T *sug)
|
||||
{
|
||||
typval_T items_tv;
|
||||
tv_list_alloc_ret(&items_tv, sug->su_ga.ga_len);
|
||||
@@ -481,18 +479,10 @@ static int select_spell_suggestion(suginfo_T *sug)
|
||||
typval_T bad_tv = { .v_type = VAR_STRING,
|
||||
.vval.v_string = xstrnsave(sug->su_badptr, (size_t)sug->su_badlen) };
|
||||
typval_T lua_args[] = { items_tv, bad_tv, { .v_type = VAR_UNKNOWN } };
|
||||
typval_T rettv = TV_INITIAL_VALUE;
|
||||
nlua_call_vimfn("vim._core.spell", "suggest_select", lua_args, &rettv);
|
||||
|
||||
int idx = 0;
|
||||
if (rettv.v_type == VAR_NUMBER) {
|
||||
idx = (int)rettv.vval.v_number;
|
||||
}
|
||||
nlua_call_vimfn("vim._core.spell", "select_suggest", lua_args, NULL);
|
||||
|
||||
tv_clear(&items_tv);
|
||||
tv_clear(&bad_tv);
|
||||
tv_clear(&rettv);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/// "z=": Find badly spelled word under or after the cursor.
|
||||
@@ -583,8 +573,8 @@ void spell_suggest(int count)
|
||||
smsg(0, _("Only %" PRId64 " suggestions"), (int64_t)sug.su_ga.ga_len);
|
||||
}
|
||||
} else {
|
||||
// Ask the user (via vim.ui.select) to pick a suggestion.
|
||||
selected = select_spell_suggestion(&sug);
|
||||
// Hand off to (async) vim.ui.select().
|
||||
select_spell_suggestion(&sug);
|
||||
|
||||
lines_left = Rows; // avoid more prompt
|
||||
// don't delay for 'smd' in normal_cmd()
|
||||
|
||||
@@ -283,10 +283,11 @@ void set_buflocal_tfu_callback(buf_T *buf)
|
||||
/// type == DT_LTAG: use location list for displaying tag matches
|
||||
/// type == DT_FREE: free cached matches
|
||||
///
|
||||
/// @param eap excmd args (forwarded to Lua); may be NULL when the caller is not an excmd (e.g. `<C-T>`).
|
||||
/// @param tag tag (pattern) to jump to
|
||||
/// @param forceit :ta with !
|
||||
/// @param verbose print "tag not found" message
|
||||
void do_tag(char *tag, int type, int count, int forceit, bool verbose)
|
||||
void do_tag(exarg_T *eap, char *tag, int type, int count, int forceit, bool verbose)
|
||||
{
|
||||
taggy_T *tagstack = curwin->w_tagstack;
|
||||
int tagstackidx = curwin->w_tagstackidx;
|
||||
@@ -657,17 +658,13 @@ void do_tag(char *tag, int type, int count, int forceit, bool verbose)
|
||||
// jump to count'th matching tag.
|
||||
cur_match = count > 0 ? count - 1 : 0;
|
||||
} else if (type == DT_SELECT || (type == DT_JUMP && num_matches > 1)) {
|
||||
// Ask the user (via vim.ui.select) to pick a tag.
|
||||
int i = select_tag_match(new_tag, use_tagstack, num_matches, matches);
|
||||
if (i <= 0 || i > num_matches || got_int) {
|
||||
// no valid choice: don't change anything
|
||||
if (use_tagstack) {
|
||||
tagstack[tagstackidx].fmark = saved_fmark;
|
||||
tagstackidx = prevtagstackidx;
|
||||
}
|
||||
break;
|
||||
// Hand off to (async) vim.ui.select(). Roll back any pending tagstack changes.
|
||||
select_tag_match(eap, new_tag, use_tagstack, num_matches, matches, name);
|
||||
if (use_tagstack) {
|
||||
tagstack[tagstackidx].fmark = saved_fmark;
|
||||
tagstackidx = prevtagstackidx;
|
||||
}
|
||||
cur_match = i - 1;
|
||||
break;
|
||||
} else if (type == DT_LTAG) {
|
||||
if (add_llist_tags(tag, num_matches, matches) == FAIL) {
|
||||
goto end_do_tag;
|
||||
@@ -791,10 +788,9 @@ end_do_tag:
|
||||
xfree(tofree);
|
||||
}
|
||||
|
||||
/// Let the user pick from `matches`. Delegates to `vim.ui.select()`.
|
||||
///
|
||||
/// @return 1-based index of the chosen tag, or 0 if cancelled.
|
||||
static int select_tag_match(bool new_tag, bool use_tagstack, int num_matches, char **matches)
|
||||
/// Let the user pick from `matches`. Delegates to (async) `vim.ui.select()`.
|
||||
static void select_tag_match(exarg_T *eap, bool new_tag, bool use_tagstack, int num_matches,
|
||||
char **matches, const char *name)
|
||||
{
|
||||
taggy_T *tagstack = curwin->w_tagstack;
|
||||
int tagstackidx = curwin->w_tagstackidx;
|
||||
@@ -806,7 +802,7 @@ static int select_tag_match(bool new_tag, bool use_tagstack, int num_matches, ch
|
||||
os_breakcheck();
|
||||
if (got_int) {
|
||||
tv_clear(&items_tv);
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
tagptrs_T tagp;
|
||||
parse_match(matches[i], &tagp);
|
||||
@@ -830,18 +826,18 @@ static int select_tag_match(bool new_tag, bool use_tagstack, int num_matches, ch
|
||||
tv_list_append_tv(items_tv.vval.v_list, &item);
|
||||
}
|
||||
|
||||
typval_T lua_args[] = { items_tv, { .v_type = VAR_UNKNOWN } };
|
||||
typval_T rettv = TV_INITIAL_VALUE;
|
||||
nlua_call_vimfn("vim._core.tag", "select", lua_args, &rettv);
|
||||
// Pass items + tag name as a dict via the `extra` slot of nlua_call_excmd. Lua decides whether
|
||||
// to use `:tag` or `:stag` from `eap.name` (e.g. "tselect" vs "stselect").
|
||||
dict_T *extra_d = tv_dict_alloc();
|
||||
tv_dict_add_list(extra_d, S_LEN("items"), items_tv.vval.v_list);
|
||||
items_tv.vval.v_list->lv_refcount++; // dict keeps a ref
|
||||
tv_dict_add_str(extra_d, S_LEN("tagname"), name);
|
||||
typval_T extra_tv = { .v_type = VAR_DICT, .vval.v_dict = extra_d };
|
||||
|
||||
int idx = 0;
|
||||
if (rettv.v_type == VAR_NUMBER) {
|
||||
idx = (int)rettv.vval.v_number;
|
||||
}
|
||||
nlua_call_excmd("vim._core.tag", "select_tag", eap, &cmdmod, &extra_tv);
|
||||
|
||||
tv_clear(&items_tv);
|
||||
tv_clear(&rettv);
|
||||
return idx;
|
||||
tv_clear(&extra_tv);
|
||||
}
|
||||
|
||||
/// Add the matching tags to the location list for the current
|
||||
@@ -2321,7 +2317,7 @@ void free_tag_stuff(void)
|
||||
{
|
||||
ga_clear_strings(&tag_fnames);
|
||||
if (curwin != NULL) {
|
||||
do_tag(NULL, DT_FREE, 0, 0, 0);
|
||||
do_tag(NULL, NULL, DT_FREE, 0, 0, 0);
|
||||
}
|
||||
tag_freematch();
|
||||
|
||||
|
||||
@@ -319,6 +319,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -360,6 +361,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -401,6 +403,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -442,6 +445,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = true,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = true,
|
||||
keepalt = false,
|
||||
@@ -483,6 +487,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -524,6 +529,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -577,6 +583,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -619,6 +626,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -672,6 +680,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
@@ -713,6 +722,7 @@ describe('nvim_create_user_command', function()
|
||||
browse = false,
|
||||
confirm = false,
|
||||
emsg_silent = false,
|
||||
filter = { force = false, pattern = '' },
|
||||
hide = false,
|
||||
horizontal = false,
|
||||
keepalt = false,
|
||||
|
||||
@@ -232,6 +232,7 @@ describe('vim._core', function()
|
||||
'vim._core.shared',
|
||||
'vim._core.spell',
|
||||
'vim._core.stringbuffer',
|
||||
'vim._core.swapfile',
|
||||
'vim._core.system',
|
||||
'vim._core.table',
|
||||
'vim._core.tag',
|
||||
|
||||
@@ -85,6 +85,15 @@ describe(':oldfiles', function()
|
||||
|
||||
oldfiles = get_oldfiles('filter! file_ oldfiles')
|
||||
eq({ another }, oldfiles)
|
||||
|
||||
-- The original v:oldfiles index is preserved in the output (matches `message_filtered()` behavior).
|
||||
local v_oldfiles = api.nvim_get_vvar('oldfiles')
|
||||
local raw = eval([[split(execute('filter file_ oldfiles'), "\n")]])
|
||||
for _, line in ipairs(raw) do
|
||||
local idx, path = line:match('^(%d+):%s+(.+)$')
|
||||
ok(idx ~= nil, 'numbered', line)
|
||||
eq(path, v_oldfiles[tonumber(idx)])
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
@@ -160,24 +160,34 @@ describe("preserve and (R)ecover with custom 'directory'", function()
|
||||
{
|
||||
content = { { '' } },
|
||||
pos = 0,
|
||||
prompt = 'Enter number of swap file to use (0 to quit): ',
|
||||
-- Default vim.ui.select prompt.
|
||||
prompt = 'Type number and <Enter> or click with the mouse (q or empty cancels): ',
|
||||
},
|
||||
},
|
||||
condition = function()
|
||||
msg = msg or screen.messages[1]
|
||||
eq(true, msg.content[1][2]:match('Swap.*none --') ~= nil)
|
||||
eq('list_cmd', msg.kind)
|
||||
-- Concatenate all chunks (each chunk is { 'text' } or { hl_id, 'text', 'group' }).
|
||||
local text = ''
|
||||
for _, chunk in ipairs(msg.content) do
|
||||
text = text .. (#chunk >= 2 and chunk[2] or chunk[1])
|
||||
end
|
||||
-- New ui.select-driven prompt; rich info from format_item.
|
||||
eq(true, text:match('Enter number of swap file to use') ~= nil)
|
||||
eq(true, text:match('%.swo') ~= nil)
|
||||
eq(true, text:match('%.swp') ~= nil)
|
||||
eq(true, text:match('host name:') ~= nil)
|
||||
eq('confirm', msg.kind)
|
||||
screen.messages = {}
|
||||
end,
|
||||
})
|
||||
else
|
||||
screen:expect({
|
||||
any = {
|
||||
'\nSwap files found:',
|
||||
'\n In directory ',
|
||||
vim.pesc('\n1. '),
|
||||
vim.pesc('\n2. '),
|
||||
vim.pesc('\nEnter number of swap file to use (0 to quit): ^'),
|
||||
vim.pesc('Enter number of swap file to use (q or empty cancels):'),
|
||||
'\n1:.*%.swo',
|
||||
'\n2:.*%.swp',
|
||||
'host name:',
|
||||
vim.pesc('Type number and <Enter> or click with the mouse (q or empty cancels): ^'),
|
||||
},
|
||||
none = vim.pesc('{18:^@}'),
|
||||
})
|
||||
|
||||
@@ -1,15 +1,86 @@
|
||||
-- Tests for vim.ui.select(), including integration with builtins (:tselect, z=).
|
||||
|
||||
local t = require('test.testutil')
|
||||
local retry = t.retry
|
||||
local n = require('test.functional.testnvim')()
|
||||
local clear = n.clear
|
||||
local exec_lua = n.exec_lua
|
||||
local api = n.api
|
||||
local eq = t.eq
|
||||
local neq = t.neq
|
||||
local write_file = t.write_file
|
||||
|
||||
before_each(clear)
|
||||
|
||||
--- Mock async vim.ui.select impl. Imitates fzf-lua/telescope/snacks: opens a transient floating
|
||||
--- window, then schedules on_choice to fire on the next event-loop tick.
|
||||
---
|
||||
--- Sets `_G._captured` so tests can assert the user choice.
|
||||
--- @param pick integer|nil 1-based index to "pick" (nil cancels).
|
||||
local function setup_async_picker(pick)
|
||||
exec_lua(function()
|
||||
_G._captured = nil
|
||||
--- @diagnostic disable-next-line: duplicate-set-field
|
||||
vim.ui.select = function(items, opts, on_choice)
|
||||
_G._captured = { items = items, opts = opts }
|
||||
-- Open a floating window like a real picker would.
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
local win = vim.api.nvim_open_win(buf, false, {
|
||||
relative = 'editor',
|
||||
row = 1,
|
||||
col = 1,
|
||||
width = 30,
|
||||
height = math.min(#items, 5),
|
||||
})
|
||||
_G._captured.win = win
|
||||
-- Defer the choice so the wait actually has to pump events.
|
||||
vim.defer_fn(function()
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
vim.api.nvim_win_close(win, true)
|
||||
end
|
||||
if pick then
|
||||
on_choice(items[pick], pick)
|
||||
else
|
||||
on_choice(nil, nil)
|
||||
end
|
||||
end, 30)
|
||||
end
|
||||
end, pick)
|
||||
end
|
||||
|
||||
--- Mock fzf-lua-style picker: opens a floating window with a *terminal* buffer running a small
|
||||
--- shell command. When the command exits we treat the user as having "picked" `pick`.
|
||||
local function setup_term_picker(pick)
|
||||
exec_lua(function(pick_, prog)
|
||||
_G._captured = nil
|
||||
--- @diagnostic disable-next-line: duplicate-set-field
|
||||
vim.ui.select = function(items, opts, on_choice)
|
||||
_G._captured = { items = items, opts = opts }
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
local win = vim.api.nvim_open_win(buf, true, {
|
||||
relative = 'editor',
|
||||
row = 1,
|
||||
col = 1,
|
||||
width = 30,
|
||||
height = math.min(#items, 5),
|
||||
})
|
||||
vim.fn.jobstart({ prog }, {
|
||||
term = true,
|
||||
on_exit = function()
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
vim.api.nvim_win_close(win, true)
|
||||
end
|
||||
if pick_ then
|
||||
on_choice(items[pick_], pick_)
|
||||
else
|
||||
on_choice(nil, nil)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end, pick, n.testprg('shell-test'))
|
||||
end
|
||||
|
||||
describe('vim.ui.select()', function()
|
||||
it('can select an item', function()
|
||||
local result = exec_lua [[
|
||||
@@ -45,7 +116,7 @@ describe('vim.ui.select()', function()
|
||||
end)
|
||||
|
||||
describe('via :tselect', function()
|
||||
it('passes items and applies the chosen index', function()
|
||||
local function prepare_test()
|
||||
-- Create dummy source files so the jump succeeds.
|
||||
write_file('XselTagA.c', 'int foo;\n')
|
||||
write_file('XselTagB.c', 'int foo = 1;\n')
|
||||
@@ -60,49 +131,37 @@ describe('vim.ui.select()', function()
|
||||
.. 'foo\tXselTagA.c\t/^int foo;$/;"\tv\n'
|
||||
.. 'foo\tXselTagB.c\t/^int foo = 1;$/;"\tv\n'
|
||||
)
|
||||
api.nvim_set_option_value('tags', 'XselTags', {})
|
||||
end
|
||||
|
||||
it('passes items, gets user choice', function()
|
||||
prepare_test()
|
||||
|
||||
local got = exec_lua(function()
|
||||
vim.opt.tags = 'XselTags'
|
||||
local captured ---@type table?
|
||||
--- @diagnostic disable-next-line: duplicate-set-field
|
||||
vim.ui.select = function(items, opts, on_choice)
|
||||
captured = { items = items, kind = opts.kind }
|
||||
_G._captured = { items = items, kind = opts.kind }
|
||||
-- Pick the second match.
|
||||
on_choice(items[2], 2)
|
||||
end
|
||||
vim.cmd('tselect foo')
|
||||
return {
|
||||
kind = captured and captured.kind,
|
||||
nitems = captured and #captured.items,
|
||||
item1_tag = captured and captured.items[1].tag,
|
||||
item2_file = captured and captured.items[2].file,
|
||||
bufname = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t'),
|
||||
}
|
||||
end)
|
||||
-- on_choice queues `:[idx]tag` via feedkeys; let typeahead drain.
|
||||
retry(nil, 1000, function()
|
||||
eq('XselTagB.c', api.nvim_eval('expand("%:t")'))
|
||||
end)
|
||||
got = exec_lua(function()
|
||||
return _G._captured
|
||||
end)
|
||||
|
||||
eq('tag', got.kind)
|
||||
eq(2, got.nitems)
|
||||
eq('foo', got.item1_tag)
|
||||
eq('XselTagB.c', got.item2_file)
|
||||
-- Picking item 2 should land us in XselTagB.c.
|
||||
eq('XselTagB.c', got.bufname)
|
||||
eq(2, #got.items)
|
||||
eq('foo', got.items[1].tag)
|
||||
eq('XselTagB.c', got.items[2].file)
|
||||
end)
|
||||
|
||||
it('keeps the buffer unchanged when the user cancels', function()
|
||||
write_file('XselTagA.c', 'int foo;\n')
|
||||
write_file('XselTagB.c', 'int foo = 1;\n')
|
||||
finally(function()
|
||||
os.remove('XselTagA.c')
|
||||
os.remove('XselTagB.c')
|
||||
os.remove('XselTags')
|
||||
end)
|
||||
write_file(
|
||||
'XselTags',
|
||||
'!_TAG_FILE_FORMAT\t2\t/extended format/\n'
|
||||
.. 'foo\tXselTagA.c\t/^int foo;$/;"\tv\n'
|
||||
.. 'foo\tXselTagB.c\t/^int foo = 1;$/;"\tv\n'
|
||||
)
|
||||
|
||||
api.nvim_set_option_value('tags', 'XselTags', {})
|
||||
it('does nothing when the user cancels', function()
|
||||
prepare_test()
|
||||
|
||||
local before = api.nvim_buf_get_name(0)
|
||||
exec_lua(function()
|
||||
@@ -114,45 +173,63 @@ describe('vim.ui.select()', function()
|
||||
|
||||
eq(before, api.nvim_buf_get_name(0))
|
||||
end)
|
||||
|
||||
it('+ async picker', function()
|
||||
prepare_test()
|
||||
|
||||
setup_async_picker(2)
|
||||
exec_lua([[vim.cmd('tselect foo')]])
|
||||
retry(nil, 1000, function()
|
||||
eq('XselTagB.c', api.nvim_eval('expand("%:t")'))
|
||||
end)
|
||||
eq('tag', exec_lua([[return _G._captured and _G._captured.opts.kind]]))
|
||||
end)
|
||||
|
||||
it('+ async terminal-based picker', function()
|
||||
prepare_test()
|
||||
|
||||
setup_term_picker(2)
|
||||
exec_lua([[vim.cmd('tselect foo')]])
|
||||
retry(nil, 1000, function()
|
||||
eq('XselTagB.c', api.nvim_eval('expand("%:t")'))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('via z=', function()
|
||||
it('passes items and applies the chosen suggestion', function()
|
||||
local function prepare_test()
|
||||
api.nvim_set_option_value('spell', true, {})
|
||||
api.nvim_set_option_value('spelllang', 'en_us', {})
|
||||
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { 'helo' })
|
||||
end
|
||||
|
||||
local got = exec_lua(function()
|
||||
it('passes items, gets user choice', function()
|
||||
prepare_test()
|
||||
|
||||
exec_lua(function()
|
||||
vim.cmd('normal! gg0')
|
||||
local captured ---@type table?
|
||||
--- @diagnostic disable-next-line: duplicate-set-field
|
||||
vim.ui.select = function(items, opts, on_choice)
|
||||
captured = { items = items, kind = opts.kind, prompt = opts.prompt }
|
||||
_G._captured = { items = items, kind = opts.kind, prompt = opts.prompt }
|
||||
-- Pick the first suggestion.
|
||||
on_choice(items[1], 1)
|
||||
end
|
||||
vim.cmd('normal! z=')
|
||||
return {
|
||||
kind = captured and captured.kind,
|
||||
prompt = captured and captured.prompt,
|
||||
item1_word = captured and captured.items[1].word,
|
||||
line = vim.api.nvim_buf_get_lines(0, 0, -1, false)[1],
|
||||
}
|
||||
end)
|
||||
-- z= delegates to vim.ui.select, see `_core/spell:select_suggest`. on_choice queues
|
||||
-- `:normal! [idx]z=` via feedkeys; let typeahead drain.
|
||||
retry(nil, 1000, function()
|
||||
t.neq('helo', api.nvim_buf_get_lines(0, 0, -1, false)[1])
|
||||
end)
|
||||
local got = exec_lua([[return _G._captured]])
|
||||
|
||||
eq('spell', got.kind)
|
||||
-- prompt should contain the misspelled word
|
||||
t.matches('helo', got.prompt)
|
||||
-- The first suggestion replaced the bad word.
|
||||
t.neq('helo', got.line)
|
||||
eq(got.item1_word, got.line)
|
||||
eq(got.items[1].word, api.nvim_buf_get_lines(0, 0, -1, false)[1])
|
||||
end)
|
||||
|
||||
it('keeps the word unchanged when the user cancels', function()
|
||||
api.nvim_set_option_value('spell', true, {})
|
||||
api.nvim_set_option_value('spelllang', 'en_us', {})
|
||||
|
||||
api.nvim_buf_set_lines(0, 0, -1, false, { 'helo' })
|
||||
it('does nothing when the user cancels', function()
|
||||
prepare_test()
|
||||
|
||||
exec_lua(function()
|
||||
vim.cmd('normal! gg0')
|
||||
@@ -164,5 +241,49 @@ describe('vim.ui.select()', function()
|
||||
|
||||
eq('helo', api.nvim_buf_get_lines(0, 0, -1, false)[1])
|
||||
end)
|
||||
|
||||
it('+ async picker', function()
|
||||
prepare_test()
|
||||
|
||||
setup_async_picker(1)
|
||||
exec_lua([[vim.cmd('normal! gg0z=')]])
|
||||
retry(nil, 1000, function()
|
||||
neq('helo', api.nvim_buf_get_lines(0, 0, -1, false)[1])
|
||||
end)
|
||||
eq('spell', exec_lua([[return _G._captured and _G._captured.opts.kind]]))
|
||||
end)
|
||||
|
||||
it('+ async terminal-based picker', function()
|
||||
prepare_test()
|
||||
|
||||
setup_term_picker(1)
|
||||
exec_lua([[vim.cmd('normal! gg0z=')]])
|
||||
retry(nil, 1000, function()
|
||||
neq('helo', api.nvim_buf_get_lines(0, 0, -1, false)[1])
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('via ":browse oldfiles"', function()
|
||||
it('+ async picker', function()
|
||||
finally(function()
|
||||
os.remove('XselOldA')
|
||||
os.remove('XselOldB')
|
||||
end)
|
||||
write_file('XselOldA', 'a\n')
|
||||
write_file('XselOldB', 'b\n')
|
||||
local cwd = exec_lua([[return vim.uv.cwd()]])
|
||||
|
||||
setup_async_picker(2)
|
||||
exec_lua(function(cwd_)
|
||||
-- v:oldfiles is normally populated via shada; inject directly for the test.
|
||||
vim.v.oldfiles = { cwd_ .. '/XselOldA', cwd_ .. '/XselOldB' }
|
||||
vim.cmd('browse oldfiles')
|
||||
end, cwd)
|
||||
retry(nil, 1000, function()
|
||||
eq('XselOldB', api.nvim_eval('expand("%:t")'))
|
||||
end)
|
||||
eq('oldfiles', exec_lua([[return _G._captured and _G._captured.opts.kind]]))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user