vim-patch:9.1.0476: Cannot see matched text in popup menu

Problem:  Cannot see matched text in popup menu
Solution: Introduce 2 new highlighting groups: PmenuMatch and
          PmenuMatchSel (glepnir)

closes: vim/vim#14694

40c1c3317d

Co-authored-by: glepnir <glephunter@gmail.com>
This commit is contained in:
zeertzjq 2024-06-13 14:08:50 +08:00
parent fd950d4998
commit dc4037f612
12 changed files with 365 additions and 13 deletions

View File

@ -56,6 +56,8 @@ hi('CursorLineFold', { link = 'FoldColumn' })
hi('CurSearch', { link = 'Search' })
hi('PmenuKind', { link = 'Pmenu' })
hi('PmenuKindSel', { link = 'PmenuSel' })
hi('PmenuMatch', { link = 'Pmenu' })
hi('PmenuMatchSel', { link = 'PmenuSel' })
hi('PmenuExtra', { link = 'Pmenu' })
hi('PmenuExtraSel', { link = 'PmenuSel' })
hi('Substitute', { link = 'Search' })

View File

@ -5076,6 +5076,11 @@ PmenuExtraSel Popup menu: Selected item "extra text".
PmenuSbar Popup menu: Scrollbar.
*hl-PmenuThumb*
PmenuThumb Popup menu: Thumb of the scrollbar.
*hl-PmenuMatch*
PmenuMatch Popup menu: Matched text in normal item
*hl-PmenuMatchSel*
PmenuMatchSel Popup menu: Matched text in selected item
*hl-Question*
Question |hit-enter| prompt and yes/no questions.
*hl-QuickFixLine*

View File

@ -54,6 +54,8 @@ EXTERN const char *hlf_names[] INIT( = {
[HLF_SPL] = "SpellLocal",
[HLF_PNI] = "Pmenu",
[HLF_PSI] = "PmenuSel",
[HLF_PMNI] = "PmenuMatch",
[HLF_PMSI] = "PmenuMatchSel",
[HLF_PNK] = "PmenuKind",
[HLF_PSK] = "PmenuKindSel",
[HLF_PNX] = "PmenuExtra",

View File

@ -101,6 +101,8 @@ typedef enum {
HLF_SPL, ///< SpellLocal
HLF_PNI, ///< popup menu normal item
HLF_PSI, ///< popup menu selected item
HLF_PMNI, ///< popup menu matched text in normal item
HLF_PMSI, ///< popup menu matched text in selected item
HLF_PNK, ///< popup menu normal item "kind"
HLF_PSK, ///< popup menu selected item "kind"
HLF_PNX, ///< popup menu normal item "menu" (extra text)

View File

@ -169,6 +169,8 @@ static const char *highlight_init_both[] = {
"default link PmenuExtraSel PmenuSel",
"default link PmenuKind Pmenu",
"default link PmenuKindSel PmenuSel",
"default link PmenuMatch Pmenu",
"default link PmenuMatchSel PmenuSel",
"default link PmenuSbar Pmenu",
"default link Substitute Search",
"default link StatusLineTerm StatusLine",

View File

@ -1388,6 +1388,12 @@ bool compl_match_curr_select(int selected)
#define DICT_FIRST (1) ///< use just first element in "dict"
#define DICT_EXACT (2) ///< "dict" is the exact name of a file
/// Get current completion leader
char *ins_compl_leader(void)
{
return compl_leader;
}
/// Add any identifiers that match the given pattern "pat" in the list of
/// dictionary files "dict_start" to the list of completions.
///

View File

@ -55,13 +55,13 @@
#define HIGHLIGHT_INIT \
"8:SpecialKey,~:EndOfBuffer,z:TermCursor,Z:TermCursorNC,@:NonText,d:Directory,e:ErrorMsg," \
"i:IncSearch,l:Search,y:CurSearch,m:MoreMsg,M:ModeMsg,n:LineNr,a:LineNrAbove,b:LineNrBelow," \
"N:CursorLineNr,G:CursorLineSign,O:CursorLineFold" \
"r:Question,s:StatusLine,S:StatusLineNC,c:VertSplit,t:Title,v:Visual,V:VisualNOS,w:WarningMsg," \
"W:WildMenu,f:Folded,F:FoldColumn,A:DiffAdd,C:DiffChange,D:DiffDelete,T:DiffText,>:SignColumn," \
"-:Conceal,B:SpellBad,P:SpellCap,R:SpellRare,L:SpellLocal,+:Pmenu,=:PmenuSel," \
"[:PmenuKind,]:PmenuKindSel,{:PmenuExtra,}:PmenuExtraSel,x:PmenuSbar,X:PmenuThumb," \
"*:TabLine,#:TabLineSel,_:TabLineFill,!:CursorColumn,.:CursorLine,o:ColorColumn," \
"q:QuickFixLine,g:MsgArea,0:Whitespace,I:NormalNC"
"N:CursorLineNr,G:CursorLineSign,O:CursorLineFold,r:Question,s:StatusLine,S:StatusLineNC," \
"c:VertSplit,t:Title,v:Visual,V:VisualNOS,w:WarningMsg,W:WildMenu,f:Folded,F:FoldColumn," \
"A:DiffAdd,C:DiffChange,D:DiffDelete,T:DiffText,>:SignColumn,-:Conceal,B:SpellBad,P:SpellCap," \
"R:SpellRare,L:SpellLocal,+:Pmenu,=:PmenuSel,k:PmenuMatch,<:PmenuMatchSel,[:PmenuKind," \
"]:PmenuKindSel,{:PmenuExtra,}:PmenuExtraSel,x:PmenuSbar,X:PmenuThumb,*:TabLine,#:TabLineSel," \
"_:TabLineFill,!:CursorColumn,.:CursorLine,o:ColorColumn,q:QuickFixLine,z:StatusLineTerm," \
"Z:StatusLineTermNC,g:MsgArea,0:Whitespace,I:NormalNC"
// Default values for 'errorformat'.
// The "%f|%l| %m" one is used for when the contents of the quickfix window is

View File

@ -25,6 +25,7 @@
#include "nvim/ex_cmds_defs.h"
#include "nvim/extmark.h"
#include "nvim/extmark_defs.h"
#include "nvim/garray.h"
#include "nvim/getchar.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
@ -48,6 +49,7 @@
#include "nvim/plines.h"
#include "nvim/popupmenu.h"
#include "nvim/pos_defs.h"
#include "nvim/search.h"
#include "nvim/state_defs.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
@ -435,6 +437,74 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i
pum_redraw();
}
/// Displays text on the popup menu with specific attributes.
static void pum_puts_with_attr(int col, const char *text, int attr)
{
char *leader = ins_compl_leader();
if ((win_hl_attr(curwin, HLF_PMSI) == win_hl_attr(curwin, HLF_PSI)
&& win_hl_attr(curwin, HLF_PMNI) == win_hl_attr(curwin, HLF_PNI))) {
grid_line_puts(col, text, -1, attr);
return;
}
char *rt_leader = NULL;
if (leader != NULL && curwin->w_p_rl) {
rt_leader = reverse_text(leader);
}
char *match_leader = rt_leader != NULL ? rt_leader : leader;
size_t leader_len = match_leader ? strlen(match_leader) : 0;
const bool in_fuzzy = (get_cot_flags() & COT_FUZZY) != 0;
garray_T *ga = NULL;
if (match_leader != NULL && leader_len > 0 && in_fuzzy) {
ga = fuzzy_match_str_with_pos(text, match_leader);
}
// Render text with proper attributes
const char *ptr = text;
while (*ptr != NUL) {
int char_len = utfc_ptr2len(ptr);
int cells = utf_ptr2cells(ptr);
int new_attr = attr;
if (ga != NULL) {
// Handle fuzzy matching
for (int i = 0; i < ga->ga_len; i++) {
int *match_pos = ((int *)ga->ga_data) + i;
int actual_char_pos = 0;
const char *temp_ptr = text;
while (temp_ptr < ptr) {
temp_ptr += utfc_ptr2len(temp_ptr);
actual_char_pos++;
}
if (actual_char_pos == match_pos[0]) {
new_attr = win_hl_attr(curwin, (attr == win_hl_attr(curwin, HLF_PSI)
? HLF_PMSI : HLF_PMNI));
break;
}
}
} else if (!in_fuzzy && ptr < text + leader_len
&& strncmp(text, match_leader, leader_len) == 0) {
new_attr = win_hl_attr(curwin, (attr == win_hl_attr(curwin, HLF_PSI)
? HLF_PMSI : HLF_PMNI));
}
grid_line_puts(col, ptr, char_len, new_attr);
col += cells;
ptr += char_len;
}
if (ga != NULL) {
ga_clear(ga);
xfree(ga);
}
if (rt_leader) {
xfree(rt_leader);
}
}
/// Redraw the popup menu, using "pum_first" and "pum_selected".
void pum_redraw(void)
{
@ -593,13 +663,12 @@ void pum_redraw(void)
size++;
}
}
grid_line_puts(grid_col - size + 1, rt, -1, attr);
pum_puts_with_attr(grid_col - size + 1, rt, attr);
xfree(rt_start);
xfree(st);
grid_col -= width;
} else {
// use grid_line_puts() to truncate the text
grid_line_puts(grid_col, st, -1, attr);
pum_puts_with_attr(grid_col, st, attr);
xfree(st);
grid_col += width;
}

View File

@ -21,12 +21,14 @@
#include "nvim/errors.h"
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/ex_cmds.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_getln.h"
#include "nvim/fileio.h"
#include "nvim/fold.h"
#include "nvim/garray.h"
#include "nvim/getchar.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
@ -3254,7 +3256,7 @@ static void fuzzy_match_in_list(list_T *const l, char *const str, const bool mat
const char *const key, Callback *const item_cb,
const bool retmatchpos, list_T *const fmatchlist,
const int max_matches)
FUNC_ATTR_NONNULL_ARG(2, 5, 7)
FUNC_ATTR_NONNULL_ARG(2, 7)
{
int len = tv_list_len(l);
if (len == 0) {
@ -3542,6 +3544,84 @@ int fuzzy_match_str(char *const str, const char *const pat)
return score;
}
/// Fuzzy match the position of string "pat" in string "str".
/// @returns a dynamic array of matching positions. If there is no match, returns NULL.
garray_T *fuzzy_match_str_with_pos(const char *const str, char *const pat)
{
garray_T *match_positions = xmalloc(sizeof(garray_T));
ga_init(match_positions, sizeof(int), 10);
if (str == NULL || pat == NULL) {
ga_clear(match_positions);
return NULL;
}
list_T *l = tv_list_alloc(1);
typval_T tv_str = {
.v_type = VAR_STRING,
.vval.v_string = xstrdup(str),
};
tv_list_append_tv(l, &tv_str);
list_T *retlist = tv_list_alloc(3);
list_T *match_str_list = tv_list_alloc(1);
list_T *match_pos_list = tv_list_alloc(1);
list_T *match_score_list = tv_list_alloc(1);
tv_list_append_list(retlist, match_str_list);
tv_list_append_list(retlist, match_pos_list);
tv_list_append_list(retlist, match_score_list);
fuzzy_match_in_list(l, pat, false, NULL, NULL, true, retlist, 1);
varnumber_T score = 0;
listitem_T *score_item = tv_list_find(retlist, 2);
if (score_item != NULL && TV_LIST_ITEM_TV(score_item)->v_type == VAR_LIST) {
list_T *score_list = TV_LIST_ITEM_TV(score_item)->vval.v_list;
if (tv_list_len(score_list) > 0) {
listitem_T *first_score_item = tv_list_first(score_list);
if (first_score_item != NULL && TV_LIST_ITEM_TV(first_score_item)->v_type == VAR_NUMBER) {
score = TV_LIST_ITEM_TV(first_score_item)->vval.v_number;
}
}
}
if (score == 0) {
goto cleanup;
}
listitem_T *positions_item = tv_list_find(retlist, 1);
if (positions_item != NULL && TV_LIST_ITEM_TV(positions_item)->v_type == VAR_LIST) {
list_T *positions_outer_list = TV_LIST_ITEM_TV(positions_item)->vval.v_list;
if (tv_list_len(positions_outer_list) > 0) {
listitem_T *outer_li = tv_list_first(positions_outer_list);
if (outer_li != NULL && TV_LIST_ITEM_TV(outer_li)->v_type == VAR_LIST) {
list_T *positions_inner_list = TV_LIST_ITEM_TV(outer_li)->vval.v_list;
TV_LIST_ITER_CONST(positions_inner_list, li, {
if (TV_LIST_ITEM_TV(li)->v_type == VAR_NUMBER) {
varnumber_T pos = TV_LIST_ITEM_TV(li)->vval.v_number;
GA_APPEND(int, match_positions, (int)pos);
}
});
}
}
}
xfree(tv_str.vval.v_string);
tv_list_free(retlist);
tv_list_free(l);
return match_positions;
cleanup:
xfree(tv_str.vval.v_string);
tv_list_free(match_str_list);
tv_list_free(match_pos_list);
tv_list_free(match_score_list);
tv_list_free(retlist);
tv_list_free(l);
ga_clear(match_positions);
return NULL;
}
/// Copy a list of fuzzy matches into a string list after sorting the matches by
/// the fuzzy score. Frees the memory allocated for "fuzmatch".
void fuzzymatches_to_strmatches(fuzmatch_str_T *const fuzmatch, char ***const matches,

View File

@ -246,11 +246,11 @@ describe('ui/cursor', function()
end
end
if m.hl_id then
m.hl_id = 64
m.hl_id = 66
m.attr = { background = Screen.colors.DarkGray }
end
if m.id_lm then
m.id_lm = 71
m.id_lm = 73
end
end

View File

@ -1177,6 +1177,8 @@ describe('builtin popupmenu', function()
ks = { foreground = Screen.colors.Red, background = Screen.colors.Grey },
xn = { foreground = Screen.colors.White, background = Screen.colors.Magenta },
xs = { foreground = Screen.colors.Black, background = Screen.colors.Grey },
mn = { foreground = Screen.colors.Blue, background = Screen.colors.White },
ms = { foreground = Screen.colors.Green, background = Screen.colors.White },
})
screen:attach({ ext_multigrid = multigrid })
end)
@ -4585,6 +4587,121 @@ describe('builtin popupmenu', function()
]])
end)
end)
-- oldtest: Test_pum_highlights_match()
it('can highlight matched text', function()
exec([[
func Omni_test(findstart, base)
if a:findstart
return col(".")
endif
return {
\ 'words': [
\ { 'word': 'foo',},
\ { 'word': 'foobar',},
\ { 'word': 'fooBaz',},
\ { 'word': 'foobala',},
\ { 'word': '你好',},
\ { 'word': '你好吗',},
\ { 'word': '你不好吗',},
\ { 'word': '你可好吗',},
\]}
endfunc
set omnifunc=Omni_test
set completeopt=menu,noinsert,fuzzy
hi PmenuMatchSel guifg=Green guibg=White
hi PmenuMatch guifg=Blue guibg=White
]])
feed('i<C-X><C-O>')
local pum_start = [[
^ |
{s:foo }{1: }|
{n:foobar }{1: }|
{n:fooBaz }{1: }|
{n:foobala }{1: }|
{n: }{1: }|
{n: }{1: }|
{n: }{1: }|
{n: }{1: }|
{1:~ }|*10
{2:-- }{5:match 1 of 8} |
]]
screen:expect(pum_start)
feed('fo')
screen:expect([[
fo^ |
{ms:fo}{s:o }{1: }|
{mn:fo}{n:obar }{1: }|
{mn:fo}{n:oBaz }{1: }|
{mn:fo}{n:obala }{1: }|
{1:~ }|*14
{2:-- }{5:match 1 of 8} |
]])
feed('<Esc>S<C-X><C-O>')
screen:expect(pum_start)
feed('')
screen:expect([[
^ |
{ms:}{s: }{1: }|
{mn:}{n: }{1: }|
{mn:}{n: }{1: }|
{mn:}{n: }{1: }|
{1:~ }|*14
{2:-- }{5:match 1 of 8} |
]])
feed('')
screen:expect([[
^ |
{ms:}{s:}{ms:}{s: }{1: }|
{mn:}{n:}{mn:}{n: }{1: }|
{mn:}{n:}{mn:}{n: }{1: }|
{1:~ }|*15
{2:-- }{5:match 1 of 8} |
]])
feed('<C-E><Esc>')
command('set rightleft')
feed('S<C-X><C-O>')
screen:expect([[
^ |
{1: }{s: oof}|
{1: }{n: raboof}|
{1: }{n: zaBoof}|
{1: }{n: alaboof}|
{1: }{n: }|
{1: }{n: }|
{1: }{n: }|
{1: }{n: }|
{1: ~}|*10
{2:-- }{5:match 1 of 8} |
]])
feed('fo')
screen:expect([[
^ of|
{1: }{s: o}{ms:of}|
{1: }{n: rabo}{mn:of}|
{1: }{n: zaBo}{mn:of}|
{1: }{n: alabo}{mn:of}|
{1: ~}|*14
{2:-- }{5:match 1 of 8} |
]])
feed('<C-E><Esc>')
command('set norightleft')
command('set completeopt-=fuzzy')
feed('S<C-X><C-O>')
screen:expect(pum_start)
feed('fo')
screen:expect([[
fo^ |
{ms:fo}{s:o }{1: }|
{mn:fo}{n:obar }{1: }|
{mn:fo}{n:oBaz }{1: }|
{mn:fo}{n:obala }{1: }|
{1:~ }|*14
{2:-- }{5:match 1 of 8} |
]])
end)
end
end

View File

@ -1348,4 +1348,71 @@ func Test_pum_highlights_custom()
call StopVimInTerminal(buf)
endfunc
" Test match relate highlight group in pmenu
func Test_pum_highlights_match()
CheckScreendump
let lines =<< trim END
func Omni_test(findstart, base)
if a:findstart
return col(".")
endif
return {
\ 'words': [
\ { 'word': 'foo',},
\ { 'word': 'foobar',},
\ { 'word': 'fooBaz',},
\ { 'word': 'foobala',},
\ { 'word': '你好',},
\ { 'word': '你好吗',},
\ { 'word': '你不好吗',},
\ { 'word': '你可好吗',},
\]}
endfunc
set omnifunc=Omni_test
set completeopt=menu,noinsert,fuzzy
hi PmenuMatchSel ctermfg=6 ctermbg=225
hi PmenuMatch ctermfg=4 ctermbg=225
END
call writefile(lines, 'Xscript', 'D')
let buf = RunVimInTerminal('-S Xscript', {})
call TermWait(buf)
call term_sendkeys(buf, "i\<C-X>\<C-O>")
call TermWait(buf, 50)
call term_sendkeys(buf, "fo")
call TermWait(buf, 50)
call VerifyScreenDump(buf, 'Test_pum_highlights_03', {})
call term_sendkeys(buf, "\<ESC>S\<C-x>\<C-O>")
call TermWait(buf, 50)
call term_sendkeys(buf, "你")
call TermWait(buf, 50)
call VerifyScreenDump(buf, 'Test_pum_highlights_04', {})
call term_sendkeys(buf, "吗")
call TermWait(buf, 50)
call VerifyScreenDump(buf, 'Test_pum_highlights_05', {})
if has('rightleft')
call term_sendkeys(buf, "\<C-E>\<ESC>u:set rightleft\<CR>")
call TermWait(buf, 50)
call term_sendkeys(buf, "i\<C-X>\<C-O>")
call TermWait(buf, 50)
call term_sendkeys(buf, "fo")
call TermWait(buf, 50)
call VerifyScreenDump(buf, 'Test_pum_highlights_06', {})
call term_sendkeys(buf, "\<C-E>\<ESC>u:set norightleft\<CR>")
call TermWait(buf)
endif
call term_sendkeys(buf, ":set completeopt-=fuzzy\<CR>")
call TermWait(buf)
call term_sendkeys(buf, "\<ESC>S\<C-x>\<C-O>")
call TermWait(buf, 50)
call term_sendkeys(buf, "fo")
call TermWait(buf, 50)
call VerifyScreenDump(buf, 'Test_pum_highlights_07', {})
call term_sendkeys(buf, "\<C-E>\<Esc>u")
call TermWait(buf)
call StopVimInTerminal(buf)
endfunc
" vim: shiftwidth=2 sts=2 expandtab