mirror of
https://github.com/neovim/neovim.git
synced 2026-05-06 16:29:57 -04:00
feat(ui): use vim.ui.select for :tselect, z= #39478
Problem: `:tselect` and `z=` (spell suggest) have their own bespoke select menus. Solution: - Delegate to `vim.ui.select` instead. - Bonus: - `:tselect` gains mouse support. `print_tag_list` didn't suport mouseclick. This causes some minor regressions, which are not blockers: - `z=` no longer draws the list right-left if 'rightleft' is set. - TODO: can/should `vim.ui.select` / `vim.fn.inputlist()` handle that? - `:tselect` - No "column" headings (`# pri kind tag file`). - No highlighting: (HLF_T: tag name, HLF_D: file, HLF_CM: extra fields). - TODO: can `vim.ui.select()` support highlighted chunks (`[[text, hl_id], ...]`) ? fix https://github.com/neovim/neovim/issues/25814 fix https://github.com/neovim/neovim/issues/31987
This commit is contained in:
+6
-5
@@ -5165,12 +5165,13 @@ vim.ui.progress_status() *vim.ui.progress_status()*
|
||||
|
||||
vim.ui.select({items}, {opts}, {on_choice}) *vim.ui.select()*
|
||||
Prompts the user to pick from a list of items, allowing arbitrary
|
||||
(potentially asynchronous) work until `on_choice`.
|
||||
(potentially asynchronous) work until `on_choice`. This is the standard
|
||||
"picker" interface, used by |z=|, |:tselect|, etc.
|
||||
|
||||
Plugins may override `vim.ui.select` to provide a custom "picker"
|
||||
interface; they are expected to call the `format_item` and `preview_item`
|
||||
handlers (if any) provided by the caller. They may also use the `kind`
|
||||
hint (if provided by the caller) to decide how to handle some items.
|
||||
Plugins may override `vim.ui.select` to provide a custom picker; they are
|
||||
expected to call the `format_item` and `preview_item` handlers (if any)
|
||||
provided by the caller. They may also use the `kind` hint (if provided by
|
||||
the caller) to decide how to handle some items.
|
||||
|
||||
Note: the default `vim.ui.select` currently doesn't support preview.
|
||||
|
||||
|
||||
@@ -216,7 +216,10 @@ TUI
|
||||
|
||||
UI
|
||||
|
||||
• todo
|
||||
• |: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.
|
||||
|
||||
VIMSCRIPT
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
local select_blocking = require('vim._core.ui').select_blocking
|
||||
local N_ = vim.fn.gettext
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @class vim._core.spell.Suggestion
|
||||
--- @field word string The suggested replacement.
|
||||
--- @field extra? string Text replaced when wider than the bad span.
|
||||
--- @field score integer Primary score.
|
||||
--- @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()|.
|
||||
---
|
||||
--- @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, {
|
||||
prompt = N_('Change "%s" to:'):format(bad),
|
||||
kind = 'spell',
|
||||
format_item = function(s)
|
||||
local extra = s.extra and (' < "' .. s.extra .. '"') or ''
|
||||
local score = ''
|
||||
if vim.o.verbose > 0 then
|
||||
score = s.altscore
|
||||
and (' (%s%d - %d)'):format(s.salscore and 's ' or '', s.score, s.altscore)
|
||||
or (' (%d)'):format(s.score)
|
||||
end
|
||||
return ('"%s"%s%s'):format(s.word, extra, score)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,44 @@
|
||||
local select_blocking = require('vim._core.ui').select_blocking
|
||||
local N_ = vim.fn.gettext
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @class vim._core.tag.Match
|
||||
--- @field tag string
|
||||
--- @field kind? string
|
||||
--- @field pri string Priority code, e.g. "FSC" — see `:h tag-priority`.
|
||||
--- @field file string
|
||||
--- @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()|.
|
||||
---
|
||||
--- @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)
|
||||
local taglen = 18
|
||||
for _, m in ipairs(items) do
|
||||
taglen = math.max(taglen, vim.fn.strdisplaywidth(m.tag) + 2)
|
||||
end
|
||||
|
||||
return select_blocking(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
|
||||
)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,23 @@
|
||||
local M = {}
|
||||
|
||||
--- Wait for |vim.ui.select()| and return the selected index. The default vim.ui.select impl
|
||||
--- (inputlist()) is synchronous, but this also handles async pickers (fzf-lua, telescope, …).
|
||||
---
|
||||
--- @param items table Items to choose from.
|
||||
--- @param opts table Forwarded to |vim.ui.select()|.
|
||||
--- @return integer? # 1-based index of the chosen item, or nil if cancelled/interrupted.
|
||||
function M.select_blocking(items, opts)
|
||||
local choice ---@type integer?
|
||||
local done = false
|
||||
vim.ui.select(items, opts or {}, function(_, idx)
|
||||
choice = idx
|
||||
done = true
|
||||
end)
|
||||
-- vim.wait returns false on timeout (math.huge means never) or interrupt (-2).
|
||||
vim.wait(math.huge, function()
|
||||
return done
|
||||
end)
|
||||
return choice
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -26,11 +26,11 @@ local M = vim._defer_require('vim.ui', {
|
||||
---@field kind? string
|
||||
|
||||
--- Prompts the user to pick from a list of items, allowing arbitrary (potentially asynchronous)
|
||||
--- work until `on_choice`.
|
||||
--- work until `on_choice`. This is the standard "picker" interface, used by |z=|, |:tselect|, etc.
|
||||
---
|
||||
--- Plugins may override `vim.ui.select` to provide a custom "picker" interface; they are expected
|
||||
--- to call the `format_item` and `preview_item` handlers (if any) provided by the caller. They may
|
||||
--- also use the `kind` hint (if provided by the caller) to decide how to handle some items.
|
||||
--- Plugins may override `vim.ui.select` to provide a custom picker; they are expected to call the
|
||||
--- `format_item` and `preview_item` handlers (if any) provided by the caller. They may also use the
|
||||
--- `kind` hint (if provided by the caller) to decide how to handle some items.
|
||||
---
|
||||
--- Note: the default `vim.ui.select` currently doesn't support preview.
|
||||
---
|
||||
|
||||
+68
-80
@@ -28,6 +28,7 @@
|
||||
#include "nvim/hashtab_defs.h"
|
||||
#include "nvim/highlight_defs.h"
|
||||
#include "nvim/input.h"
|
||||
#include "nvim/lua/executor.h"
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mbyte_defs.h"
|
||||
@@ -432,6 +433,68 @@ 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)
|
||||
{
|
||||
typval_T items_tv;
|
||||
tv_list_alloc_ret(&items_tv, sug->su_ga.ga_len);
|
||||
|
||||
for (int i = 0; i < sug->su_ga.ga_len; i++) {
|
||||
suggest_T *stp = &SUG(sug->su_ga, i);
|
||||
|
||||
dict_T *d = tv_dict_alloc();
|
||||
|
||||
// The suggested word may replace only part of the bad word; append the
|
||||
// unreplaced tail to form the user-visible "word".
|
||||
int el = sug->su_badlen - stp->st_orglen;
|
||||
if (el > 0) {
|
||||
char *word = xmallocz((size_t)stp->st_wordlen + (size_t)el);
|
||||
memcpy(word, stp->st_word, (size_t)stp->st_wordlen);
|
||||
memcpy(word + stp->st_wordlen, sug->su_badptr + stp->st_orglen, (size_t)el);
|
||||
tv_dict_add_allocated_str(d, S_LEN("word"), word);
|
||||
} else {
|
||||
tv_dict_add_str(d, S_LEN("word"), stp->st_word);
|
||||
}
|
||||
|
||||
// The suggestion may replace MORE than su_badlen of the bad text;
|
||||
// capture that wider span as `extra`.
|
||||
if (sug->su_badlen < stp->st_orglen) {
|
||||
char *extra = xstrnsave(sug->su_badptr, (size_t)stp->st_orglen);
|
||||
tv_dict_add_allocated_str(d, S_LEN("extra"), extra);
|
||||
}
|
||||
|
||||
// Pass raw scoring data; Lua decides whether/how to render.
|
||||
// `altscore`/`salscore` are only meaningful with SPS_DOUBLE|SPS_BEST.
|
||||
tv_dict_add_nr(d, S_LEN("score"), stp->st_score);
|
||||
if (sps_flags & (SPS_DOUBLE | SPS_BEST)) {
|
||||
tv_dict_add_nr(d, S_LEN("altscore"), stp->st_altscore);
|
||||
tv_dict_add_bool(d, S_LEN("salscore"),
|
||||
stp->st_salscore ? kBoolVarTrue : kBoolVarFalse);
|
||||
}
|
||||
|
||||
typval_T item = { .v_type = VAR_DICT, .vval.v_dict = d };
|
||||
tv_list_append_tv(items_tv.vval.v_list, &item);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
tv_clear(&items_tv);
|
||||
tv_clear(&bad_tv);
|
||||
tv_clear(&rettv);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/// "z=": Find badly spelled word under or after the cursor.
|
||||
/// Give suggestions for the properly spelled word.
|
||||
/// In Visual mode use the highlighted word as the bad word.
|
||||
@@ -439,7 +502,6 @@ int spell_check_sps(void)
|
||||
void spell_suggest(int count)
|
||||
{
|
||||
pos_T prev_cursor = curwin->w_cursor;
|
||||
bool mouse_used = false;
|
||||
int badlen = 0;
|
||||
int msg_scroll_save = msg_scroll;
|
||||
const int wo_spell_save = curwin->w_p_spell;
|
||||
@@ -512,91 +574,17 @@ void spell_suggest(int count)
|
||||
true, need_cap, true);
|
||||
|
||||
int selected = count;
|
||||
msg_ext_set_kind("confirm");
|
||||
if (GA_EMPTY(&sug.su_ga)) {
|
||||
msg_ext_set_kind("wmsg");
|
||||
msg(_("No suggestions"), 0);
|
||||
} else if (count > 0) {
|
||||
if (count > sug.su_ga.ga_len) {
|
||||
smsg(0, _("Only %" PRId64 " suggestions"),
|
||||
(int64_t)sug.su_ga.ga_len);
|
||||
msg_ext_set_kind("wmsg");
|
||||
smsg(0, _("Only %" PRId64 " suggestions"), (int64_t)sug.su_ga.ga_len);
|
||||
}
|
||||
} else {
|
||||
// When 'rightleft' is set the list is drawn right-left.
|
||||
cmdmsg_rl = curwin->w_p_rl;
|
||||
|
||||
// List the suggestions.
|
||||
msg_start();
|
||||
msg_row = Rows - 1; // for when 'cmdheight' > 1
|
||||
lines_left = Rows; // avoid more prompt
|
||||
char *fmt = _("Change \"%.*s\" to:");
|
||||
if (cmdmsg_rl && strncmp(fmt, "Change", 6) == 0) {
|
||||
// And now the rabbit from the high hat: Avoid showing the
|
||||
// untranslated message rightleft.
|
||||
fmt = ":ot \"%.*s\" egnahC";
|
||||
}
|
||||
vim_snprintf(IObuff, IOSIZE, fmt, sug.su_badlen, sug.su_badptr);
|
||||
msg_puts(IObuff);
|
||||
msg_clr_eos();
|
||||
msg_putchar('\n');
|
||||
|
||||
msg_scroll = true;
|
||||
for (int i = 0; i < sug.su_ga.ga_len; i++) {
|
||||
suggest_T *stp = &SUG(sug.su_ga, i);
|
||||
|
||||
// The suggested word may replace only part of the bad word, add
|
||||
// the not replaced part. But only when it's not getting too long.
|
||||
char wcopy[MAXWLEN + 2];
|
||||
xstrlcpy(wcopy, stp->st_word, MAXWLEN + 1);
|
||||
int el = sug.su_badlen - stp->st_orglen;
|
||||
if (el > 0 && stp->st_wordlen + el <= MAXWLEN) {
|
||||
assert(sug.su_badptr != NULL);
|
||||
xmemcpyz(wcopy + stp->st_wordlen, sug.su_badptr + stp->st_orglen, (size_t)el);
|
||||
}
|
||||
vim_snprintf(IObuff, IOSIZE, "%2d", i + 1);
|
||||
if (cmdmsg_rl) {
|
||||
rl_mirror_ascii(IObuff, NULL);
|
||||
}
|
||||
msg_puts(IObuff);
|
||||
|
||||
vim_snprintf(IObuff, IOSIZE, " \"%s\"", wcopy);
|
||||
msg_puts(IObuff);
|
||||
|
||||
// The word may replace more than "su_badlen".
|
||||
if (sug.su_badlen < stp->st_orglen) {
|
||||
vim_snprintf(IObuff, IOSIZE, _(" < \"%.*s\""),
|
||||
stp->st_orglen, sug.su_badptr);
|
||||
msg_puts(IObuff);
|
||||
}
|
||||
|
||||
if (p_verbose > 0) {
|
||||
// Add the score.
|
||||
if (sps_flags & (SPS_DOUBLE | SPS_BEST)) {
|
||||
vim_snprintf(IObuff, IOSIZE, " (%s%d - %d)",
|
||||
stp->st_salscore ? "s " : "",
|
||||
stp->st_score, stp->st_altscore);
|
||||
} else {
|
||||
vim_snprintf(IObuff, IOSIZE, " (%d)",
|
||||
stp->st_score);
|
||||
}
|
||||
if (cmdmsg_rl) {
|
||||
// Mirror the numbers, but keep the leading space.
|
||||
rl_mirror_ascii(IObuff + 1, NULL);
|
||||
}
|
||||
msg_advance(30);
|
||||
msg_puts(IObuff);
|
||||
}
|
||||
if (!ui_has(kUIMessages) || i < sug.su_ga.ga_len - 1) {
|
||||
msg_putchar('\n');
|
||||
}
|
||||
}
|
||||
|
||||
cmdmsg_rl = false;
|
||||
msg_col = 0;
|
||||
// Ask for choice.
|
||||
selected = prompt_for_input(NULL, 0, false, &mouse_used);
|
||||
if (mouse_used) {
|
||||
selected = sug.su_ga.ga_len + 1 - (cmdline_row - mouse_row);
|
||||
}
|
||||
// Ask the user (via vim.ui.select) to pick a suggestion.
|
||||
selected = select_spell_suggestion(&sug);
|
||||
|
||||
lines_left = Rows; // avoid more prompt
|
||||
// don't delay for 'smd' in normal_cmd()
|
||||
|
||||
+52
-180
@@ -38,6 +38,7 @@
|
||||
#include "nvim/highlight_defs.h"
|
||||
#include "nvim/input.h"
|
||||
#include "nvim/insexpand.h"
|
||||
#include "nvim/lua/executor.h"
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mark.h"
|
||||
#include "nvim/mark_defs.h"
|
||||
@@ -651,26 +652,13 @@ void do_tag(char *tag, int type, int count, int forceit, bool verbose)
|
||||
}
|
||||
g_do_tagpreview = 0;
|
||||
} else {
|
||||
bool ask_for_selection = false;
|
||||
|
||||
if (type == DT_TAG && *tag != NUL) {
|
||||
// If a count is supplied to the ":tag <name>" command, then
|
||||
// jump to count'th matching tag.
|
||||
cur_match = count > 0 ? count - 1 : 0;
|
||||
} else if (type == DT_SELECT || (type == DT_JUMP && num_matches > 1)) {
|
||||
print_tag_list(new_tag, use_tagstack, num_matches, matches);
|
||||
ask_for_selection = true;
|
||||
} else if (type == DT_LTAG) {
|
||||
if (add_llist_tags(tag, num_matches, matches) == FAIL) {
|
||||
goto end_do_tag;
|
||||
}
|
||||
|
||||
cur_match = 0; // Jump to the first tag
|
||||
}
|
||||
|
||||
if (ask_for_selection) {
|
||||
// Ask to select a tag from the list.
|
||||
int i = prompt_for_input(NULL, 0, false, NULL);
|
||||
// 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) {
|
||||
@@ -680,6 +668,12 @@ void do_tag(char *tag, int type, int count, int forceit, bool verbose)
|
||||
break;
|
||||
}
|
||||
cur_match = i - 1;
|
||||
} else if (type == DT_LTAG) {
|
||||
if (add_llist_tags(tag, num_matches, matches) == FAIL) {
|
||||
goto end_do_tag;
|
||||
}
|
||||
|
||||
cur_match = 0; // Jump to the first tag
|
||||
}
|
||||
|
||||
if (cur_match >= num_matches) {
|
||||
@@ -797,179 +791,57 @@ end_do_tag:
|
||||
xfree(tofree);
|
||||
}
|
||||
|
||||
// List all the matching tags.
|
||||
static void print_tag_list(bool new_tag, bool use_tagstack, int num_matches, char **matches)
|
||||
/// 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)
|
||||
{
|
||||
taggy_T *tagstack = curwin->w_tagstack;
|
||||
int tagstackidx = curwin->w_tagstackidx;
|
||||
tagptrs_T tagp;
|
||||
|
||||
// Assume that the first match indicates how long the tags can
|
||||
// be, and align the file names to that.
|
||||
parse_match(matches[0], &tagp);
|
||||
int taglen = MAX((int)(tagp.tagname_end - tagp.tagname + 2), 18);
|
||||
if (taglen > Columns - 25) {
|
||||
taglen = MAXCOL;
|
||||
}
|
||||
if (msg_col == 0) {
|
||||
msg_didout = false; // overwrite previous message
|
||||
}
|
||||
msg_ext_set_kind("confirm");
|
||||
msg_start();
|
||||
msg_puts_hl(_(" # pri kind tag"), HLF_T, false);
|
||||
msg_clr_eos();
|
||||
taglen_advance(taglen);
|
||||
msg_puts_hl(_("file\n"), HLF_T, false);
|
||||
typval_T items_tv;
|
||||
tv_list_alloc_ret(&items_tv, num_matches);
|
||||
|
||||
for (int i = 0; i < num_matches && !got_int; i++) {
|
||||
parse_match(matches[i], &tagp);
|
||||
if (!new_tag && (
|
||||
(g_do_tagpreview != 0
|
||||
&& i == ptag_entry.cur_match)
|
||||
|| (use_tagstack
|
||||
&& i == tagstack[tagstackidx].cur_match))) {
|
||||
*IObuff = '>';
|
||||
} else {
|
||||
*IObuff = ' ';
|
||||
}
|
||||
vim_snprintf(IObuff + 1, IOSIZE - 1,
|
||||
"%2d %s ", i + 1,
|
||||
mt_names[matches[i][0] & MT_MASK]);
|
||||
msg_puts(IObuff);
|
||||
if (tagp.tagkind != NULL) {
|
||||
msg_outtrans_len(tagp.tagkind, (int)(tagp.tagkind_end - tagp.tagkind), 0, false);
|
||||
}
|
||||
msg_advance(13);
|
||||
msg_outtrans_len(tagp.tagname, (int)(tagp.tagname_end - tagp.tagname), HLF_T, false);
|
||||
msg_putchar(' ');
|
||||
taglen_advance(taglen);
|
||||
|
||||
// Find out the actual file name. If it is long, truncate
|
||||
// it and put "..." in the middle
|
||||
const char *p = tag_full_fname(&tagp);
|
||||
if (p != NULL) {
|
||||
msg_outtrans(p, HLF_D, false);
|
||||
XFREE_CLEAR(p);
|
||||
}
|
||||
if (msg_col > 0) {
|
||||
msg_putchar('\n');
|
||||
}
|
||||
if (got_int) {
|
||||
break;
|
||||
}
|
||||
msg_advance(15);
|
||||
|
||||
// print any extra fields
|
||||
const char *command_end = tagp.command_end;
|
||||
if (command_end != NULL) {
|
||||
p = command_end + 3;
|
||||
while (*p && *p != '\r' && *p != '\n') {
|
||||
while (*p == TAB) {
|
||||
p++;
|
||||
}
|
||||
|
||||
// skip "file:" without a value (static tag)
|
||||
if (strncmp(p, "file:", 5) == 0 && ascii_isspace(p[5])) {
|
||||
p += 5;
|
||||
continue;
|
||||
}
|
||||
// skip "kind:<kind>" and "<kind>"
|
||||
if (p == tagp.tagkind
|
||||
|| (p + 5 == tagp.tagkind
|
||||
&& strncmp(p, "kind:", 5) == 0)) {
|
||||
p = tagp.tagkind_end;
|
||||
continue;
|
||||
}
|
||||
// print all other extra fields
|
||||
int hl_id = HLF_CM;
|
||||
while (*p && *p != '\r' && *p != '\n') {
|
||||
if (msg_col + ptr2cells(p) >= Columns) {
|
||||
msg_putchar('\n');
|
||||
if (got_int) {
|
||||
break;
|
||||
}
|
||||
msg_advance(15);
|
||||
}
|
||||
p = msg_outtrans_one(p, hl_id, false);
|
||||
if (*p == TAB) {
|
||||
msg_puts_hl(" ", hl_id, false);
|
||||
break;
|
||||
}
|
||||
if (*p == ':') {
|
||||
hl_id = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg_col > 15) {
|
||||
msg_putchar('\n');
|
||||
if (got_int) {
|
||||
break;
|
||||
}
|
||||
msg_advance(15);
|
||||
}
|
||||
} else {
|
||||
for (p = tagp.command;
|
||||
*p && *p != '\r' && *p != '\n';
|
||||
p++) {}
|
||||
command_end = p;
|
||||
}
|
||||
|
||||
// Put the info (in several lines) at column 15.
|
||||
// Don't display "/^" and "?^".
|
||||
p = tagp.command;
|
||||
if (*p == '/' || *p == '?') {
|
||||
p++;
|
||||
if (*p == '^') {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
// Remove leading whitespace from pattern
|
||||
while (p != command_end && ascii_isspace(*p)) {
|
||||
p++;
|
||||
}
|
||||
|
||||
while (p != command_end) {
|
||||
if (msg_col + (*p == TAB ? 1 : ptr2cells(p)) > Columns) {
|
||||
msg_putchar('\n');
|
||||
}
|
||||
if (got_int) {
|
||||
break;
|
||||
}
|
||||
msg_advance(15);
|
||||
|
||||
// skip backslash used for escaping a command char or
|
||||
// a backslash
|
||||
if (*p == '\\' && (*(p + 1) == *tagp.command
|
||||
|| *(p + 1) == '\\')) {
|
||||
p++;
|
||||
}
|
||||
|
||||
if (*p == TAB) {
|
||||
msg_putchar(' ');
|
||||
p++;
|
||||
} else {
|
||||
p = msg_outtrans_one(p, 0, false);
|
||||
}
|
||||
|
||||
// don't display the "$/;\"" and "$?;\""
|
||||
if (p == command_end - 2 && *p == '$'
|
||||
&& *(p + 1) == *tagp.command) {
|
||||
break;
|
||||
}
|
||||
// don't display matching '/' or '?'
|
||||
if (p == command_end - 1 && *p == *tagp.command
|
||||
&& (*p == '/' || *p == '?')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (msg_col && (!ui_has(kUIMessages) || i < num_matches - 1)) {
|
||||
msg_putchar('\n');
|
||||
}
|
||||
for (int i = 0; i < num_matches; i++) {
|
||||
os_breakcheck();
|
||||
if (got_int) {
|
||||
tv_clear(&items_tv);
|
||||
return 0;
|
||||
}
|
||||
tagptrs_T tagp;
|
||||
parse_match(matches[i], &tagp);
|
||||
|
||||
bool cur = !new_tag
|
||||
&& ((g_do_tagpreview != 0 && i == ptag_entry.cur_match)
|
||||
|| (use_tagstack && i == tagstack[tagstackidx].cur_match));
|
||||
|
||||
dict_T *d = tv_dict_alloc();
|
||||
tv_dict_add_str_len(d, S_LEN("tag"), tagp.tagname, (int)(tagp.tagname_end - tagp.tagname));
|
||||
tv_dict_add_str(d, S_LEN("pri"), mt_names[matches[i][0] & MT_MASK]);
|
||||
if (tagp.tagkind != NULL) {
|
||||
tv_dict_add_str_len(d, S_LEN("kind"), tagp.tagkind, (int)(tagp.tagkind_end - tagp.tagkind));
|
||||
}
|
||||
char *fname = tag_full_fname(&tagp);
|
||||
tv_dict_add_str(d, S_LEN("file"), fname != NULL ? fname : "");
|
||||
xfree(fname);
|
||||
tv_dict_add_bool(d, S_LEN("cur"), cur ? kBoolVarTrue : kBoolVarFalse);
|
||||
|
||||
typval_T item = { .v_type = VAR_DICT, .vval.v_dict = d };
|
||||
tv_list_append_tv(items_tv.vval.v_list, &item);
|
||||
}
|
||||
if (got_int) {
|
||||
got_int = false; // only stop the listing
|
||||
|
||||
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);
|
||||
|
||||
int idx = 0;
|
||||
if (rettv.v_type == VAR_NUMBER) {
|
||||
idx = (int)rettv.vval.v_number;
|
||||
}
|
||||
|
||||
tv_clear(&items_tv);
|
||||
tv_clear(&rettv);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/// Add the matching tags to the location list for the current
|
||||
|
||||
@@ -4,7 +4,6 @@ local Screen = require('test.functional.ui.screen')
|
||||
local uv = vim.uv
|
||||
|
||||
local eq = t.eq
|
||||
local pcall_err = t.pcall_err
|
||||
local matches = t.matches
|
||||
local feed = n.feed
|
||||
local eval = n.eval
|
||||
@@ -231,10 +230,13 @@ describe('vim._core', function()
|
||||
'vim._core.options',
|
||||
'vim._core.server',
|
||||
'vim._core.shared',
|
||||
'vim._core.spell',
|
||||
'vim._core.stringbuffer',
|
||||
'vim._core.system',
|
||||
'vim._core.table',
|
||||
'vim._core.tag',
|
||||
'vim._core.time',
|
||||
'vim._core.ui',
|
||||
'vim._core.ui2',
|
||||
'vim._core.util',
|
||||
'vim._core.vimfn',
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
-- Tests for vim.ui.select(), including integration with builtins (:tselect, z=).
|
||||
|
||||
local t = require('test.testutil')
|
||||
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 write_file = t.write_file
|
||||
|
||||
before_each(clear)
|
||||
|
||||
describe('vim.ui.select()', function()
|
||||
it('can select an item', function()
|
||||
local result = exec_lua [[
|
||||
local items = {
|
||||
{ name = 'Item 1' },
|
||||
{ name = 'Item 2' },
|
||||
}
|
||||
local opts = {
|
||||
format_item = function(entry)
|
||||
return entry.name
|
||||
end
|
||||
}
|
||||
local selected
|
||||
local cb = function(item)
|
||||
selected = item
|
||||
end
|
||||
-- inputlist would require input and block the test;
|
||||
local choices
|
||||
vim.fn.inputlist = function(x)
|
||||
choices = x
|
||||
return 1
|
||||
end
|
||||
vim.ui.select(items, opts, cb)
|
||||
vim.wait(100, function() return selected ~= nil end)
|
||||
return {selected, choices}
|
||||
]]
|
||||
eq({ name = 'Item 1' }, result[1])
|
||||
eq({
|
||||
'Select one of:',
|
||||
'1: Item 1',
|
||||
'2: Item 2',
|
||||
}, result[2])
|
||||
end)
|
||||
|
||||
describe('via :tselect', function()
|
||||
it('passes items and applies the chosen index', function()
|
||||
-- Create dummy source files so the jump succeeds.
|
||||
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'
|
||||
)
|
||||
|
||||
local got = exec_lua(function()
|
||||
vim.opt.tags = 'XselTags'
|
||||
local captured ---@type table?
|
||||
vim.ui.select = function(items, opts, on_choice)
|
||||
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)
|
||||
|
||||
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)
|
||||
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', {})
|
||||
|
||||
local before = api.nvim_buf_get_name(0)
|
||||
exec_lua(function()
|
||||
vim.ui.select = function(_, _, on_choice)
|
||||
on_choice(nil, nil)
|
||||
end
|
||||
vim.cmd('tselect foo')
|
||||
end)
|
||||
|
||||
eq(before, api.nvim_buf_get_name(0))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('via z=', function()
|
||||
it('passes items and applies the chosen suggestion', 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' })
|
||||
|
||||
local got = exec_lua(function()
|
||||
vim.cmd('normal! gg0')
|
||||
local captured ---@type table?
|
||||
vim.ui.select = function(items, opts, on_choice)
|
||||
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)
|
||||
|
||||
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)
|
||||
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' })
|
||||
|
||||
exec_lua(function()
|
||||
vim.cmd('normal! gg0')
|
||||
vim.ui.select = function(_, _, on_choice)
|
||||
on_choice(nil, nil)
|
||||
end
|
||||
vim.cmd('normal! z=')
|
||||
end)
|
||||
|
||||
eq('helo', api.nvim_buf_get_lines(0, 0, -1, false)[1])
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
@@ -2,7 +2,6 @@ local t = require('test.testutil')
|
||||
local n = require('test.functional.testnvim')()
|
||||
|
||||
local eq = t.eq
|
||||
local pcall_err = t.pcall_err
|
||||
local ok = t.ok
|
||||
local exec_lua = n.exec_lua
|
||||
local clear = n.clear
|
||||
@@ -17,41 +16,6 @@ describe('vim.ui', function()
|
||||
clear({ args_rm = { '-u' }, args = { '--clean' } })
|
||||
end)
|
||||
|
||||
describe('select()', function()
|
||||
it('can select an item', function()
|
||||
local result = exec_lua [[
|
||||
local items = {
|
||||
{ name = 'Item 1' },
|
||||
{ name = 'Item 2' },
|
||||
}
|
||||
local opts = {
|
||||
format_item = function(entry)
|
||||
return entry.name
|
||||
end
|
||||
}
|
||||
local selected
|
||||
local cb = function(item)
|
||||
selected = item
|
||||
end
|
||||
-- inputlist would require input and block the test;
|
||||
local choices
|
||||
vim.fn.inputlist = function(x)
|
||||
choices = x
|
||||
return 1
|
||||
end
|
||||
vim.ui.select(items, opts, cb)
|
||||
vim.wait(100, function() return selected ~= nil end)
|
||||
return {selected, choices}
|
||||
]]
|
||||
eq({ name = 'Item 1' }, result[1])
|
||||
eq({
|
||||
'Select one of:',
|
||||
'1: Item 1',
|
||||
'2: Item 2',
|
||||
}, result[2])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('input()', function()
|
||||
it('can input text', function()
|
||||
local result = exec_lua [[
|
||||
|
||||
@@ -400,7 +400,8 @@ describe('ui/ext_messages', function()
|
||||
{
|
||||
content = { { '' } },
|
||||
pos = 0,
|
||||
prompt = 'Type number and <Enter> (q or empty cancels): ',
|
||||
-- Default vim.ui.select uses this prompt.
|
||||
prompt = 'Type number and <Enter> or click with the mouse (q or empty cancels): ',
|
||||
},
|
||||
},
|
||||
-- Message depends on runtimepath, only test the static text...
|
||||
@@ -408,13 +409,12 @@ describe('ui/ext_messages', function()
|
||||
for _, msg in ipairs(screen.messages) do
|
||||
eq(false, msg.history)
|
||||
eq('confirm', msg.kind)
|
||||
eq(' # pri kind tag', msg.content[1][2])
|
||||
eq('\n ', msg.content[2][2])
|
||||
eq('file\n', msg.content[3][2])
|
||||
eq('> 1 F ', msg.content[4][2])
|
||||
eq('help.txt', msg.content[5][2])
|
||||
eq(' \n ', msg.content[6][2])
|
||||
eq('\n *help.txt*', msg.content[#msg.content][2])
|
||||
local text = '' -- Concatenate all chunks.
|
||||
for _, chunk in ipairs(msg.content) do
|
||||
text = text .. (#chunk >= 2 and chunk[2] or chunk[1])
|
||||
end
|
||||
t.matches('^Type number and <Enter> %(q or empty cancels%):\n', text)
|
||||
t.matches('1: > F%s+help%.txt%s+', text)
|
||||
end
|
||||
screen.messages = {}
|
||||
end,
|
||||
@@ -1286,7 +1286,7 @@ stack traceback:
|
||||
},
|
||||
messages = {
|
||||
{
|
||||
content = { { 'Change "helllo" to:\n 1 "Hello"\n 2 "Hallo"\n 3 "Hullo"' } },
|
||||
content = { { 'Change "helllo" to:\n1: "Hello"\n2: "Hallo"\n3: "Hullo"' } },
|
||||
kind = 'confirm',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -466,13 +466,12 @@ func Test_spellsuggest_option_number()
|
||||
call assert_equal('A baord', getline(1))
|
||||
|
||||
let a = execute('norm $z=')
|
||||
" Nvim: z= goes through vim.ui.select().
|
||||
call assert_equal(
|
||||
\ "\n"
|
||||
\ .. "Change \"baord\" to:\n"
|
||||
\ .. " 1 \"board\"\n"
|
||||
\ .. " 2 \"bard\"\n"
|
||||
"\ Nvim: Prompt message is sent to cmdline prompt.
|
||||
"\ .. "Type number and <Enter> or click with the mouse (q or empty cancels): ", a)
|
||||
\ .. "1: \"board\"\n"
|
||||
\ .. "2: \"bard\"\n"
|
||||
\ , a)
|
||||
|
||||
set spell spellsuggest=0
|
||||
@@ -505,14 +504,13 @@ func Test_spellsuggest_option_expr()
|
||||
new
|
||||
call setline(1, 'baord')
|
||||
let a = execute('norm z=')
|
||||
" Nvim: z= goes through vim.ui.select().
|
||||
call assert_equal(
|
||||
\ "\n"
|
||||
\ .. "Change \"baord\" to:\n"
|
||||
\ .. " 1 \"BARD\"\n"
|
||||
\ .. " 2 \"BOARD\"\n"
|
||||
\ .. " 3 \"BROAD\"\n"
|
||||
"\ Nvim: Prompt message is sent to cmdline prompt.
|
||||
"\ .. "Type number and <Enter> or click with the mouse (q or empty cancels): ", a)
|
||||
\ .. "1: \"BARD\"\n"
|
||||
\ .. "2: \"BOARD\"\n"
|
||||
\ .. "3: \"BROAD\"\n"
|
||||
\ , a)
|
||||
|
||||
" With verbose, z= should show the score i.e. word length with
|
||||
@@ -522,11 +520,9 @@ func Test_spellsuggest_option_expr()
|
||||
call assert_equal(
|
||||
\ "\n"
|
||||
\ .. "Change \"baord\" to:\n"
|
||||
\ .. " 1 \"BARD\" (4 - 0)\n"
|
||||
\ .. " 2 \"BOARD\" (5 - 0)\n"
|
||||
\ .. " 3 \"BROAD\" (5 - 0)\n"
|
||||
"\ Nvim: Prompt message is sent to cmdline prompt.
|
||||
"\ .. "Type number and <Enter> or click with the mouse (q or empty cancels): ", a)
|
||||
\ .. "1: \"BARD\" (4 - 0)\n"
|
||||
\ .. "2: \"BOARD\" (5 - 0)\n"
|
||||
\ .. "3: \"BROAD\" (5 - 0)\n"
|
||||
\ , a)
|
||||
|
||||
set spell& spellsuggest& verbose&
|
||||
|
||||
@@ -1239,17 +1239,12 @@ func Test_tselect_listing()
|
||||
|
||||
call feedkeys("\<CR>", "t")
|
||||
let l = split(execute("tselect first"), "\n")
|
||||
" Nvim: :tselect goes through vim.ui.select().
|
||||
let expected =<< [DATA]
|
||||
# pri kind tag file
|
||||
1 FS v first Xfoo
|
||||
typeref:typename:int
|
||||
1
|
||||
2 FS v first Xfoo
|
||||
typeref:typename:char
|
||||
2
|
||||
Type number and <Enter> (q or empty cancels):
|
||||
1: FS v first Xfoo
|
||||
2: FS v first Xfoo
|
||||
[DATA]
|
||||
" Type number and <Enter> (q or empty cancels):
|
||||
" Nvim: Prompt message is sent to cmdline prompt.
|
||||
|
||||
call assert_equal(expected, l)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user