feat(ui): 'pumborder' (popup menu border) #25541

Problem:
Popup menu cannot have a border.

Solution:
Support 'pumborder' option.
Generalize `win_redr_border` to `grid_redr_border`,
which redraws border for window grid and pum grid.
This commit is contained in:
glepnir
2025-10-10 22:14:50 +08:00
committed by GitHub
parent 9c89212de1
commit fafc329bbd
17 changed files with 649 additions and 133 deletions

View File

@@ -4908,6 +4908,12 @@ A jump table for the options with a short description can be found at |Q_op|.
< <
UI-dependent. Works best with RGB colors. 'termguicolors' UI-dependent. Works best with RGB colors. 'termguicolors'
*'pumborder'*
'pumborder' string (default "")
global
Defines the default border style of popupmenu windows. Same as
'winborder'.
*'pumheight'* *'ph'* *'pumheight'* *'ph'*
'pumheight' 'ph' number (default 0) 'pumheight' 'ph' number (default 0)
global global

View File

@@ -5129,6 +5129,13 @@ vim.o.pb = vim.o.pumblend
vim.go.pumblend = vim.o.pumblend vim.go.pumblend = vim.o.pumblend
vim.go.pb = vim.go.pumblend vim.go.pb = vim.go.pumblend
--- Defines the default border style of popupmenu windows. Same as
--- 'winborder'.
---
--- @type string
vim.o.pumborder = ""
vim.go.pumborder = vim.o.pumborder
--- Maximum number of items to show in the popup menu --- Maximum number of items to show in the popup menu
--- (`ins-completion-menu`). Zero means "use available screen space". --- (`ins-completion-menu`). Zero means "use available screen space".
--- ---

View File

@@ -961,7 +961,7 @@ static bool parse_bordertext_pos(win_T *wp, String bordertext_pos, BorderTextTyp
return true; return true;
} }
static void parse_border_style(Object style, WinConfig *fconfig, Error *err) void parse_border_style(Object style, WinConfig *fconfig, Error *err)
{ {
struct { struct {
const char *name; const char *name;
@@ -1079,14 +1079,14 @@ static void generate_api_error(win_T *wp, const char *attribute, Error *err)
} }
/// Parses a border style name or custom (comma-separated) style. /// Parses a border style name or custom (comma-separated) style.
bool parse_winborder(WinConfig *fconfig, Error *err) bool parse_winborder(WinConfig *fconfig, const char *border_opt, Error *err)
{ {
if (!fconfig) { if (!fconfig) {
return false; return false;
} }
Object style = OBJECT_INIT; Object style = OBJECT_INIT;
if (strchr(p_winborder, ',')) { if (strchr(border_opt, ',')) {
Array border_chars = ARRAY_DICT_INIT; Array border_chars = ARRAY_DICT_INIT;
char *p = p_winborder; char *p = p_winborder;
char part[MAX_SCHAR_SIZE] = { 0 }; char part[MAX_SCHAR_SIZE] = { 0 };
@@ -1364,7 +1364,7 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
} }
} }
} else if (*p_winborder != NUL && (wp == NULL || !wp->w_floating) } else if (*p_winborder != NUL && (wp == NULL || !wp->w_floating)
&& !parse_winborder(fconfig, err)) { && !parse_winborder(fconfig, p_winborder, err)) {
goto fail; goto fail;
} }

View File

@@ -111,6 +111,7 @@
#include "nvim/statusline.h" #include "nvim/statusline.h"
#include "nvim/strings.h" #include "nvim/strings.h"
#include "nvim/syntax.h" #include "nvim/syntax.h"
#include "nvim/syntax_defs.h"
#include "nvim/terminal.h" #include "nvim/terminal.h"
#include "nvim/types_defs.h" #include "nvim/types_defs.h"
#include "nvim/ui.h" #include "nvim/ui.h"
@@ -657,7 +658,8 @@ int update_screen(void)
win_grid_alloc(wp); win_grid_alloc(wp);
if (wp->w_redr_border || wp->w_redr_type >= UPD_NOT_VALID) { if (wp->w_redr_border || wp->w_redr_type >= UPD_NOT_VALID) {
win_redr_border(wp); grid_draw_border(&wp->w_grid_alloc, wp->w_config, wp->w_border_adj, (int)wp->w_p_winbl,
wp->w_ns_hl_attr);
} }
if (wp->w_redr_type != 0) { if (wp->w_redr_type != 0) {
@@ -743,112 +745,6 @@ void end_search_hl(void)
screen_search_hl.rm.regprog = NULL; screen_search_hl.rm.regprog = NULL;
} }
static void win_redr_bordertext(win_T *wp, VirtText vt, int col, BorderTextType bt)
{
for (size_t i = 0; i < kv_size(vt);) {
int attr = -1;
char *text = next_virt_text_chunk(vt, &i, &attr);
if (text == NULL) {
break;
}
if (attr == -1) { // No highlight specified.
attr = wp->w_ns_hl_attr[bt == kBorderTextTitle ? HLF_BTITLE : HLF_BFOOTER];
}
attr = hl_apply_winblend(wp, attr);
col += grid_line_puts(col, text, -1, attr);
}
}
int win_get_bordertext_col(int total_col, int text_width, AlignTextPos align)
{
switch (align) {
case kAlignLeft:
return 1;
case kAlignCenter:
return MAX((total_col - text_width) / 2 + 1, 1);
case kAlignRight:
return MAX(total_col - text_width + 1, 1);
}
UNREACHABLE;
}
static void win_redr_border(win_T *wp)
{
wp->w_redr_border = false;
if (!(wp->w_floating && wp->w_config.border)) {
return;
}
ScreenGrid *grid = &wp->w_grid_alloc;
schar_T chars[8];
for (int i = 0; i < 8; i++) {
chars[i] = schar_from_str(wp->w_config.border_chars[i]);
}
int *attrs = wp->w_config.border_attr;
int *adj = wp->w_border_adj;
int irow = wp->w_view_height + wp->w_winbar_height;
int icol = wp->w_view_width;
if (adj[0]) {
screengrid_line_start(grid, 0, 0);
if (adj[3]) {
grid_line_put_schar(0, chars[0], attrs[0]);
}
for (int i = 0; i < icol; i++) {
grid_line_put_schar(i + adj[3], chars[1], attrs[1]);
}
if (wp->w_config.title) {
int title_col = win_get_bordertext_col(icol, wp->w_config.title_width,
wp->w_config.title_pos);
win_redr_bordertext(wp, wp->w_config.title_chunks, title_col, kBorderTextTitle);
}
if (adj[1]) {
grid_line_put_schar(icol + adj[3], chars[2], attrs[2]);
}
grid_line_flush();
}
for (int i = 0; i < irow; i++) {
if (adj[3]) {
screengrid_line_start(grid, i + adj[0], 0);
grid_line_put_schar(0, chars[7], attrs[7]);
grid_line_flush();
}
if (adj[1]) {
int ic = (i == 0 && !adj[0] && chars[2]) ? 2 : 3;
screengrid_line_start(grid, i + adj[0], 0);
grid_line_put_schar(icol + adj[3], chars[ic], attrs[ic]);
grid_line_flush();
}
}
if (adj[2]) {
screengrid_line_start(grid, irow + adj[0], 0);
if (adj[3]) {
grid_line_put_schar(0, chars[6], attrs[6]);
}
for (int i = 0; i < icol; i++) {
int ic = (i == 0 && !adj[3] && chars[6]) ? 6 : 5;
grid_line_put_schar(i + adj[3], chars[ic], attrs[ic]);
}
if (wp->w_config.footer) {
int footer_col = win_get_bordertext_col(icol, wp->w_config.footer_width,
wp->w_config.footer_pos);
win_redr_bordertext(wp, wp->w_config.footer_chunks, footer_col, kBorderTextFooter);
}
if (adj[1]) {
grid_line_put_schar(icol + adj[3], chars[4], attrs[4]);
}
grid_line_flush();
}
}
/// Set cursor to its position in the current window. /// Set cursor to its position in the current window.
void setcursor(void) void setcursor(void)
{ {

View File

@@ -1084,6 +1084,112 @@ void grid_del_lines(ScreenGrid *grid, int row, int line_count, int end, int col,
} }
} }
static void grid_draw_bordertext(VirtText vt, int col, int winbl, const int *hl_attr,
BorderTextType bt)
{
for (size_t i = 0; i < kv_size(vt);) {
int attr = -1;
char *text = next_virt_text_chunk(vt, &i, &attr);
if (text == NULL) {
break;
}
if (attr == -1) { // No highlight specified.
attr = hl_attr[bt == kBorderTextTitle ? HLF_BTITLE : HLF_BFOOTER];
}
attr = hl_apply_winblend(winbl, attr);
col += grid_line_puts(col, text, -1, attr);
}
}
static int get_bordertext_col(int total_col, int text_width, AlignTextPos align)
{
switch (align) {
case kAlignLeft:
return 1;
case kAlignCenter:
return MAX((total_col - text_width) / 2 + 1, 1);
case kAlignRight:
return MAX(total_col - text_width + 1, 1);
}
UNREACHABLE;
}
/// draw border on floating window grid
void grid_draw_border(ScreenGrid *grid, WinConfig config, int *adj, int winbl, int *hl_attr)
{
int *attrs = config.border_attr;
int default_adj[4] = { 1, 1, 1, 1 };
if (adj == NULL) {
adj = default_adj;
}
schar_T chars[8];
if (!hl_attr) {
hl_attr = hl_attr_active;
}
for (int i = 0; i < 8; i++) {
chars[i] = schar_from_str(config.border_chars[i]);
}
int irow = grid->rows - adj[0] - adj[2];
int icol = grid->cols - adj[1] - adj[3];
if (adj[0]) {
screengrid_line_start(grid, 0, 0);
if (adj[3]) {
grid_line_put_schar(0, chars[0], attrs[0]);
}
for (int i = 0; i < icol; i++) {
grid_line_put_schar(i + adj[3], chars[1], attrs[1]);
}
if (config.title) {
int title_col = get_bordertext_col(icol, config.title_width, config.title_pos);
grid_draw_bordertext(config.title_chunks, title_col, winbl, hl_attr, kBorderTextTitle);
}
if (adj[1]) {
grid_line_put_schar(icol + adj[3], chars[2], attrs[2]);
}
grid_line_flush();
}
for (int i = 0; i < irow; i++) {
if (adj[3]) {
screengrid_line_start(grid, i + adj[0], 0);
grid_line_put_schar(0, chars[7], attrs[7]);
grid_line_flush();
}
if (adj[1]) {
int ic = (i == 0 && !adj[0] && chars[2]) ? 2 : 3;
screengrid_line_start(grid, i + adj[0], 0);
grid_line_put_schar(icol + adj[3], chars[ic], attrs[ic]);
grid_line_flush();
}
}
if (adj[2]) {
screengrid_line_start(grid, irow + adj[0], 0);
if (adj[3]) {
grid_line_put_schar(0, chars[6], attrs[6]);
}
for (int i = 0; i < icol; i++) {
int ic = (i == 0 && !adj[3] && chars[6]) ? 6 : 5;
grid_line_put_schar(i + adj[3], chars[ic], attrs[ic]);
}
if (config.footer) {
int footer_col = get_bordertext_col(icol, config.footer_width, config.footer_pos);
grid_draw_bordertext(config.footer_chunks, footer_col, winbl, hl_attr, kBorderTextFooter);
}
if (adj[1]) {
grid_line_put_schar(icol + adj[3], chars[4], attrs[4]);
}
grid_line_flush();
}
}
static void linecopy(ScreenGrid *grid, int to, int from, int col, int width) static void linecopy(ScreenGrid *grid, int to, int from, int col, int width)
{ {
unsigned off_to = (unsigned)(grid->line_offset[to] + (size_t)col); unsigned off_to = (unsigned)(grid->line_offset[to] + (size_t)col);

View File

@@ -3,6 +3,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> // IWYU pragma: keep #include <stddef.h> // IWYU pragma: keep
#include "nvim/buffer_defs.h" // IWYU pragma: keep
#include "nvim/grid_defs.h" // IWYU pragma: keep #include "nvim/grid_defs.h" // IWYU pragma: keep
#include "nvim/macros_defs.h" #include "nvim/macros_defs.h"
#include "nvim/pos_defs.h" #include "nvim/pos_defs.h"

View File

@@ -325,16 +325,16 @@ int hl_get_ui_attr(int ns_id, int idx, int final_id, bool optional)
/// Apply 'winblend' to highlight attributes. /// Apply 'winblend' to highlight attributes.
/// ///
/// @param wp The window to get 'winblend' value from. /// @param winbl The 'winblend' value.
/// @param attr The original attribute code. /// @param attr The original attribute code.
/// ///
/// @return The attribute code with 'winblend' applied. /// @return The attribute code with 'winblend' applied.
int hl_apply_winblend(win_T *wp, int attr) int hl_apply_winblend(int winbl, int attr)
{ {
HlEntry entry = attr_entry(attr); HlEntry entry = attr_entry(attr);
// if blend= attribute is not set, 'winblend' value overrides it. // if blend= attribute is not set, 'winblend' value overrides it.
if (entry.attr.hl_blend == -1 && wp->w_p_winbl > 0) { if (entry.attr.hl_blend == -1 && winbl > 0) {
entry.attr.hl_blend = (int)wp->w_p_winbl; entry.attr.hl_blend = winbl;
attr = get_attr_entry(entry); attr = get_attr_entry(entry);
} }
return attr; return attr;
@@ -379,7 +379,7 @@ void update_window_hl(win_T *wp, bool invalid)
} }
if (wp->w_floating) { if (wp->w_floating) {
wp->w_hl_attr_normal = hl_apply_winblend(wp, wp->w_hl_attr_normal); wp->w_hl_attr_normal = hl_apply_winblend((int)wp->w_p_winbl, wp->w_hl_attr_normal);
} }
wp->w_config.shadow = false; wp->w_config.shadow = false;
@@ -390,7 +390,7 @@ void update_window_hl(win_T *wp, bool invalid)
attr = hl_get_ui_attr(ns_id, HLF_BORDER, attr = hl_get_ui_attr(ns_id, HLF_BORDER,
wp->w_config.border_hl_ids[i], false); wp->w_config.border_hl_ids[i], false);
} }
attr = hl_apply_winblend(wp, attr); attr = hl_apply_winblend((int)wp->w_p_winbl, attr);
if (syn_attr2entry(attr).hl_blend > 0) { if (syn_attr2entry(attr).hl_blend > 0) {
wp->w_config.shadow = true; wp->w_config.shadow = true;
} }
@@ -411,7 +411,7 @@ void update_window_hl(win_T *wp, bool invalid)
} }
if (wp->w_floating) { if (wp->w_floating) {
wp->w_hl_attr_normalnc = hl_apply_winblend(wp, wp->w_hl_attr_normalnc); wp->w_hl_attr_normalnc = hl_apply_winblend((int)wp->w_p_winbl, wp->w_hl_attr_normalnc);
} }
} }

View File

@@ -86,6 +86,7 @@ EXTERN const char *hlf_names[] INIT( = {
[HLF_TS] = "StatusLineTerm", [HLF_TS] = "StatusLineTerm",
[HLF_TSNC] = "StatusLineTermNC", [HLF_TSNC] = "StatusLineTermNC",
[HLF_PRE] = "PreInsert", [HLF_PRE] = "PreInsert",
[HLF_PBR] = "PmenuBorder",
}); });
EXTERN int highlight_attr[HLF_COUNT]; // Highl. attr for each context. EXTERN int highlight_attr[HLF_COUNT]; // Highl. attr for each context.

View File

@@ -110,6 +110,7 @@ typedef enum {
HLF_PSX, ///< popup menu selected item "menu" (extra text) HLF_PSX, ///< popup menu selected item "menu" (extra text)
HLF_PSB, ///< popup menu scrollbar HLF_PSB, ///< popup menu scrollbar
HLF_PST, ///< popup menu scrollbar thumb HLF_PST, ///< popup menu scrollbar thumb
HLF_PBR, ///< popup menu border
HLF_TP, ///< tabpage line HLF_TP, ///< tabpage line
HLF_TPS, ///< tabpage line selected HLF_TPS, ///< tabpage line selected
HLF_TPF, ///< tabpage line filler HLF_TPF, ///< tabpage line filler

View File

@@ -174,6 +174,9 @@ static const char *highlight_init_both[] = {
"default link PmenuKind Pmenu", "default link PmenuKind Pmenu",
"default link PmenuKindSel PmenuSel", "default link PmenuKindSel PmenuSel",
"default link PmenuSbar Pmenu", "default link PmenuSbar Pmenu",
"default link PmenuBorder Pmenu",
"default link PmenuShadow FloatShadow",
"default link PmenuShadowThrough FloatShadowThrough",
"default link PreInsert Added", "default link PreInsert Added",
"default link ComplMatchIns NONE", "default link ComplMatchIns NONE",
"default link ComplHint NonText", "default link ComplHint NonText",

View File

@@ -308,6 +308,7 @@ EXTERN OptInt p_acl; ///< 'autocompletedelay'
#ifdef BACKSLASH_IN_FILENAME #ifdef BACKSLASH_IN_FILENAME
EXTERN char *p_csl; ///< 'completeslash' EXTERN char *p_csl; ///< 'completeslash'
#endif #endif
EXTERN char *p_pumborder; ///< 'pumborder'
EXTERN OptInt p_pb; ///< 'pumblend' EXTERN OptInt p_pb; ///< 'pumblend'
EXTERN OptInt p_ph; ///< 'pumheight' EXTERN OptInt p_ph; ///< 'pumheight'
EXTERN OptInt p_pw; ///< 'pumwidth' EXTERN OptInt p_pw; ///< 'pumwidth'

View File

@@ -6719,6 +6719,21 @@ local options = {
type = 'number', type = 'number',
varname = 'p_pb', varname = 'p_pb',
}, },
{
full_name = 'pumborder',
scope = { 'global' },
cb = 'did_set_pumborder',
defaults = { if_true = '' },
values = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'bold', 'none' },
desc = [=[
Defines the default border style of popupmenu windows. Same as
'winborder'.
]=],
short_desc = N_('border of popupmenu'),
type = 'string',
list = 'onecomma',
varname = 'p_pumborder',
},
{ {
abbreviation = 'ph', abbreviation = 'ph',
defaults = 0, defaults = 0,

View File

@@ -13,6 +13,7 @@
#include "nvim/cmdexpand_defs.h" #include "nvim/cmdexpand_defs.h"
#include "nvim/cursor.h" #include "nvim/cursor.h"
#include "nvim/cursor_shape.h" #include "nvim/cursor_shape.h"
#include "nvim/decoration.h"
#include "nvim/diff.h" #include "nvim/diff.h"
#include "nvim/digraph.h" #include "nvim/digraph.h"
#include "nvim/drawscreen.h" #include "nvim/drawscreen.h"
@@ -2124,16 +2125,32 @@ const char *did_set_winbar(optset_T *args)
return did_set_statustabline_rulerformat(args, false, false); return did_set_statustabline_rulerformat(args, false, false);
} }
/// The 'winborder' option is changed. static bool parse_border_opt(const char *border_opt)
const char *did_set_winborder(optset_T *args)
{ {
WinConfig fconfig = WIN_CONFIG_INIT; WinConfig fconfig = WIN_CONFIG_INIT;
Error err = ERROR_INIT; Error err = ERROR_INIT;
if (!parse_winborder(&fconfig, &err)) { bool result = true;
api_clear_error(&err); if (!parse_winborder(&fconfig, border_opt, &err)) {
return e_invarg; result = false;
} }
api_clear_error(&err); api_clear_error(&err);
return result;
}
/// The 'winborder' option is changed.
const char *did_set_winborder(optset_T *args)
{
if (!parse_border_opt(p_winborder)) {
return e_invarg;
}
return NULL;
}
const char *did_set_pumborder(optset_T *args)
{
if (!parse_border_opt(p_pumborder)) {
return e_invarg;
}
return NULL; return NULL;
} }

View File

@@ -10,12 +10,15 @@
#include "nvim/api/buffer.h" #include "nvim/api/buffer.h"
#include "nvim/api/private/defs.h" #include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h" #include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
#include "nvim/api/win_config.h"
#include "nvim/ascii_defs.h" #include "nvim/ascii_defs.h"
#include "nvim/autocmd.h" #include "nvim/autocmd.h"
#include "nvim/buffer.h" #include "nvim/buffer.h"
#include "nvim/buffer_defs.h" #include "nvim/buffer_defs.h"
#include "nvim/charset.h" #include "nvim/charset.h"
#include "nvim/cmdexpand.h" #include "nvim/cmdexpand.h"
#include "nvim/decoration.h"
#include "nvim/drawscreen.h" #include "nvim/drawscreen.h"
#include "nvim/errors.h" #include "nvim/errors.h"
#include "nvim/eval/typval.h" #include "nvim/eval/typval.h"
@@ -28,8 +31,10 @@
#include "nvim/gettext_defs.h" #include "nvim/gettext_defs.h"
#include "nvim/globals.h" #include "nvim/globals.h"
#include "nvim/grid.h" #include "nvim/grid.h"
#include "nvim/grid_defs.h"
#include "nvim/highlight.h" #include "nvim/highlight.h"
#include "nvim/highlight_defs.h" #include "nvim/highlight_defs.h"
#include "nvim/highlight_group.h"
#include "nvim/insexpand.h" #include "nvim/insexpand.h"
#include "nvim/keycodes.h" #include "nvim/keycodes.h"
#include "nvim/mbyte.h" #include "nvim/mbyte.h"
@@ -47,6 +52,7 @@
#include "nvim/pos_defs.h" #include "nvim/pos_defs.h"
#include "nvim/state_defs.h" #include "nvim/state_defs.h"
#include "nvim/strings.h" #include "nvim/strings.h"
#include "nvim/syntax.h"
#include "nvim/types_defs.h" #include "nvim/types_defs.h"
#include "nvim/ui.h" #include "nvim/ui.h"
#include "nvim/ui_compositor.h" #include "nvim/ui_compositor.h"
@@ -116,7 +122,7 @@ static void pum_compute_size(void)
/// Calculate vertical placement for popup menu. /// Calculate vertical placement for popup menu.
/// Sets pum_row and pum_height based on available space. /// Sets pum_row and pum_height based on available space.
static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_win_row, static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_win_row,
int above_row, int below_row) int above_row, int below_row, int pum_border_size)
{ {
int context_lines; int context_lines;
@@ -128,7 +134,7 @@ static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_
// Put the pum below "pum_win_row" if possible. // Put the pum below "pum_win_row" if possible.
// If there are few lines decide on where there is more room. // If there are few lines decide on where there is more room.
if (pum_win_row + 2 >= below_row - pum_height if (pum_win_row + 2 + pum_border_size >= below_row - pum_height
&& pum_win_row - above_row > (below_row - above_row) / 2) { && pum_win_row - above_row > (below_row - above_row) / 2) {
// pum above "pum_win_row" // pum above "pum_win_row"
pum_above = true; pum_above = true;
@@ -152,6 +158,14 @@ static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_
pum_row += pum_height - (int)p_ph; pum_row += pum_height - (int)p_ph;
pum_height = (int)p_ph; pum_height = (int)p_ph;
} }
if (pum_border_size > 0 && pum_border_size + pum_row + pum_height >= pum_win_row) {
if (pum_row < 2) {
pum_height -= pum_border_size;
} else {
pum_row -= pum_border_size;
}
}
} else { } else {
// pum below "pum_win_row" // pum below "pum_win_row"
pum_above = false; pum_above = false;
@@ -172,6 +186,10 @@ static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_
if (p_ph > 0 && pum_height > p_ph) { if (p_ph > 0 && pum_height > p_ph) {
pum_height = (int)p_ph; pum_height = (int)p_ph;
} }
if (pum_row + pum_height + pum_border_size >= cmdline_row) {
pum_height -= pum_border_size;
}
} }
// If there is a preview window above avoid drawing over it. // If there is a preview window above avoid drawing over it.
@@ -279,6 +297,8 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i
pum_rl = (State & MODE_CMDLINE) == 0 && curwin->w_p_rl; pum_rl = (State & MODE_CMDLINE) == 0 && curwin->w_p_rl;
int pum_border_size = *p_pumborder != NUL ? 2 : 0;
do { do {
// Mark the pum as visible already here, // Mark the pum as visible already here,
// to avoid that must_redraw is set when 'cursorcolumn' is on. // to avoid that must_redraw is set when 'cursorcolumn' is on.
@@ -366,10 +386,11 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i
} }
// Figure out the vertical size and position of the pum. // Figure out the vertical size and position of the pum.
pum_compute_vertical_placement(size, target_win, pum_win_row, above_row, below_row); pum_compute_vertical_placement(size, target_win, pum_win_row, above_row, below_row,
pum_border_size);
// don't display when we only have room for one line // don't display when we only have room for one line
if (pum_height < 1 || (pum_height == 1 && size > 1)) { if (pum_border_size == 0 && (pum_height < 1 || (pum_height == 1 && size > 1))) {
return; return;
} }
@@ -389,6 +410,10 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i
// Figure out the horizontal size and position of the pum. // Figure out the horizontal size and position of the pum.
pum_compute_horizontal_placement(target_win, cursor_col); pum_compute_horizontal_placement(target_win, cursor_col);
if (pum_col + pum_border_size + pum_width > Columns) {
pum_col -= pum_border_size;
}
// Set selected item and redraw. If the window size changed need to redo // Set selected item and redraw. If the window size changed need to redo
// the positioning. Limit this to two times, when there is not much // the positioning. Limit this to two times, when there is not much
// room the window size will keep changing. // room the window size will keep changing.
@@ -573,18 +598,43 @@ void pum_redraw(void)
} }
} }
WinConfig fconfig = WIN_CONFIG_INIT;
int pum_border_width = 0;
// setup popup menu border if 'pumborder' option is set
if (*p_pumborder != NUL) {
pum_border_width = 2;
fconfig.border = true;
Error err = ERROR_INIT;
parse_border_style(CSTR_AS_OBJ(p_pumborder), &fconfig, &err);
// shadow style uses different highlights for different positions
if (strcmp(p_pumborder, opt_winborder_values[3]) == 0) {
int blend = SYN_GROUP_STATIC("PmenuShadow");
int through = SYN_GROUP_STATIC("PmenuShadowThrough");
int attrs[] = { 0, 0, through, blend, blend, blend, through, 0 };
memcpy(fconfig.border_attr, attrs, sizeof(attrs));
} else {
// Non-shadow styles use PumBorder highlight for all border chars
int attr = hl_attr_active[HLF_PBR];
for (int i = 0; i < 8; i++) {
fconfig.border_attr[i] = attr;
}
}
api_clear_error(&err);
}
grid_assign_handle(&pum_grid); grid_assign_handle(&pum_grid);
pum_left_col = pum_col - col_off; pum_left_col = pum_col - col_off;
pum_right_col = pum_left_col + grid_width; pum_right_col = pum_left_col + grid_width;
bool moved = ui_comp_put_grid(&pum_grid, pum_row, pum_left_col, bool moved = ui_comp_put_grid(&pum_grid, pum_row, pum_left_col,
pum_height, grid_width, false, true); pum_height + pum_border_width, grid_width + pum_border_width, false,
true);
bool invalid_grid = moved || pum_invalid; bool invalid_grid = moved || pum_invalid;
pum_invalid = false; pum_invalid = false;
must_redraw_pum = false; must_redraw_pum = false;
if (!pum_grid.chars || pum_grid.rows != pum_height || pum_grid.cols != grid_width) { if (!pum_grid.chars || pum_grid.rows != pum_height || pum_grid.cols != grid_width) {
grid_alloc(&pum_grid, pum_height, grid_width, !invalid_grid, false); grid_alloc(&pum_grid, pum_height + pum_border_width, grid_width + pum_border_width,
!invalid_grid, false);
ui_call_grid_resize(pum_grid.handle, pum_grid.cols, pum_grid.rows); ui_call_grid_resize(pum_grid.handle, pum_grid.cols, pum_grid.rows);
} else if (invalid_grid) { } else if (invalid_grid) {
grid_invalidate(&pum_grid); grid_invalidate(&pum_grid);
@@ -599,6 +649,15 @@ void pum_redraw(void)
} }
int scroll_range = pum_size - pum_height; int scroll_range = pum_size - pum_height;
// avoid set border for mouse menu
int mouse_menu = State != MODE_CMDLINE && pum_grid.zindex == kZIndexCmdlinePopupMenu;
if (!mouse_menu && fconfig.border) {
grid_draw_border(&pum_grid, fconfig, NULL, 0, NULL);
row++;
col_off++;
}
// Never display more than we have // Never display more than we have
pum_first = MIN(pum_first, scroll_range); pum_first = MIN(pum_first, scroll_range);
@@ -871,7 +930,8 @@ static void pum_preview_set_text(buf_T *buf, char *info, linenr_T *lnum, int *ma
/// adjust floating info preview window position /// adjust floating info preview window position
static void pum_adjust_info_position(win_T *wp, int width) static void pum_adjust_info_position(win_T *wp, int width)
{ {
int col = pum_col + pum_width + pum_scrollbar + 1; int extra_width = *p_pumborder != NUL ? 2 : 0;
int col = pum_col + pum_width + pum_scrollbar + 1 + extra_width;
// TODO(glepnir): support config align border by using completepopup // TODO(glepnir): support config align border by using completepopup
// align menu // align menu
int right_extra = Columns - col; int right_extra = Columns - col;

View File

@@ -2,6 +2,7 @@
#include <stdbool.h> #include <stdbool.h>
#include "nvim/buffer_defs.h"
#include "nvim/eval/typval_defs.h" // IWYU pragma: keep #include "nvim/eval/typval_defs.h" // IWYU pragma: keep
#include "nvim/grid_defs.h" #include "nvim/grid_defs.h"
#include "nvim/macros_defs.h" #include "nvim/macros_defs.h"

View File

@@ -258,11 +258,11 @@ describe('ui/cursor', function()
end end
end end
if m.hl_id then if m.hl_id then
m.hl_id = 66 m.hl_id = 67
m.attr = { background = Screen.colors.DarkGray } m.attr = { background = Screen.colors.DarkGray }
end end
if m.id_lm then if m.id_lm then
m.id_lm = 77 m.id_lm = 78
m.attr_lm = {} m.attr_lm = {}
end end
end end

View File

@@ -2151,7 +2151,7 @@ describe('builtin popupmenu', function()
feed('<C-E><ESC>') feed('<C-E><ESC>')
end) end)
it('avoid modified original info text #test', function() it('avoid modified original info text', function()
command('call Append_multipe()') command('call Append_multipe()')
feed('S<C-x><C-o><C-P><C-P>') feed('S<C-x><C-o><C-P><C-P>')
if multigrid then if multigrid then
@@ -8730,6 +8730,407 @@ describe('builtin popupmenu', function()
]]) ]])
end) end)
end end
describe("'pumborder'", function()
before_each(function()
screen:try_resize(30, 11)
exec([[
funct Omni_test(findstart, base)
if a:findstart
return col(".") - 1
endif
return [#{word: "one", info: "1info"}, #{word: "two", info: "2info"}, #{word: "three", info: "3info"}]
endfunc
hi link PmenuBorder FloatBorder
set omnifunc=Omni_test
set completeopt-=preview
set pumborder=rounded
]])
end)
it('can set border', function()
feed('Gi<C-x><C-o>')
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:------------------------------]|*10
[3:------------------------------]|
## grid 2
one^ |
{1:~ }|*9
## grid 3
{5:-- }{6:match 1 of 3} |
## grid 4
{n:1info}|
## grid 5
╭───────────────╮|
│{12:one }│|
│{n:two }│|
│{n:three }│|
╰───────────────╯|
]],
win_pos = {
[2] = {
height = 10,
startcol = 0,
startrow = 0,
width = 30,
win = 1000,
},
},
float_pos = {
[5] = { -1, 'NW', 2, 1, 0, false, 100, 2, 1, 0 },
[4] = { 1001, 'NW', 1, 1, 17, false, 50, 1, 1, 17 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 2,
curline = 0,
curcol = 3,
linecount = 1,
sum_scroll_delta = 0,
},
[4] = {
win = 1001,
topline = 0,
botline = 1,
curline = 0,
curcol = 0,
linecount = 1,
sum_scroll_delta = 0,
},
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000,
},
[4] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1001,
},
},
})
else
screen:expect([[
one^ |
╭───────────────╮{n:1info}{1: }|
│{12:one }│{1: }|
│{n:two }│{1: }|
│{n:three }│{1: }|
╰───────────────╯{1: }|
{1:~ }|*4
{5:-- }{6:match 1 of 3} |
]])
end
-- avoid out of screen
feed(('a'):rep(25) .. '<C-x><C-o>')
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:------------------------------]|*10
[3:------------------------------]|
## grid 2
oneaaaaaaaaaaaaaaaaaaaaaaaaaon|
e^ |
{1:~ }|*8
## grid 3
{5:-- }{6:match 1 of 3} |
## grid 4
{n:1info}|
## grid 5
╭─────────────────╮|
│{12: one }│|
│{n: two }│|
│{n: three }│|
╰─────────────────╯|
]],
win_pos = {
[2] = {
height = 10,
startcol = 0,
startrow = 0,
width = 30,
win = 1000,
},
},
float_pos = {
[5] = { -1, 'NW', 2, 2, 11, false, 100, 2, 2, 11 },
[4] = { 1001, 'NW', 1, 2, 6, false, 50, 1, 2, 6 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 2,
curline = 0,
curcol = 31,
linecount = 1,
sum_scroll_delta = 0,
},
[4] = {
win = 1001,
topline = 0,
botline = 1,
curline = 0,
curcol = 0,
linecount = 1,
sum_scroll_delta = 0,
},
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000,
},
[4] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1001,
},
},
})
else
screen:expect([[
oneaaaaaaaaaaaaaaaaaaaaaaaaaon|
e^ |
{1:~ }{n:1info}╭─────────────────╮|
{1:~ }│{12: one }│|
{1:~ }│{n: two }│|
{1:~ }│{n: three }│|
{1:~ }╰─────────────────╯|
{1:~ }|*3
{5:-- }{6:match 1 of 3} |
]])
end
end)
it('adjust to above when the below row + border out of win height', function()
command('set completeopt+=menuone,noselect')
feed('<ESC>Stwo' .. ('<CR>'):rep(6) .. 'tw<C-N>')
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:------------------------------]|*10
[3:------------------------------]|
## grid 2
two |
|*5
tw^ |
{1:~ }|*3
## grid 3
{5:-- }{19:Back at original} |
## grid 4
╭───────────────╮|
│{n:two }│|
╰───────────────╯|
]],
win_pos = {
[2] = {
height = 10,
startcol = 0,
startrow = 0,
width = 30,
win = 1000,
},
},
float_pos = {
[4] = { -1, 'SW', 2, 4, 0, false, 100, 1, 3, 0 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 8,
curline = 6,
curcol = 2,
linecount = 7,
sum_scroll_delta = 0,
},
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000,
},
},
})
else
screen:expect([[
two |
|*2
╭───────────────╮ |
│{n:two }│ |
╰───────────────╯ |
tw^ |
{1:~ }|*3
{5:-- }{19:Back at original} |
]])
end
end)
it('pum border on cmdline', function()
command('set wildoptions=pum')
feed(':<TAB>')
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:------------------------------]|*10
[3:------------------------------]|
## grid 2
|
{1:~ }|*9
## grid 3
:!^ |
## grid 4
╭─────────────────╮|
│{12: ! }{c: }│|
│{n: # }{12: }│|
│{n: & }{12: }│|
│{n: < }{12: }│|
│{n: = }{12: }│|
│{n: > }{12: }│|
│{n: @ }{12: }│|
│{n: Next }{12: }│|
╰─────────────────╯|
]],
win_pos = {
[2] = {
height = 10,
startcol = 0,
startrow = 0,
width = 30,
win = 1000,
},
},
float_pos = {
[4] = { -1, 'SW', 1, 8, 0, false, 250, 2, 0, 0 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 2,
curline = 0,
curcol = 0,
linecount = 1,
sum_scroll_delta = 0,
},
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000,
},
},
})
else
screen:expect([[
╭─────────────────╮ |
│{12: ! }{c: }│{1: }|
│{n: # }{12: }│{1: }|
│{n: & }{12: }│{1: }|
│{n: < }{12: }│{1: }|
│{n: = }{12: }│{1: }|
│{n: > }{12: }│{1: }|
│{n: @ }{12: }│{1: }|
│{n: Next }{12: }│{1: }|
╰─────────────────╯{1: }|
:!^ |
]])
end
end)
it('reduce pum height when height is not enough', function()
command('set lines=7 laststatus=2')
feed('S<C-x><C-o>')
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:------------------------------]|*5
{3:[No Name] [+] }|
[3:------------------------------]|
## grid 2
one^ |
{1:~ }|*4
## grid 3
{5:-- }{6:match 1 of 3} |
## grid 4
╭────────────────╮|
│{12:one }{c: }│|
╰────────────────╯|
]],
win_pos = {
[2] = {
height = 5,
startcol = 0,
startrow = 0,
width = 30,
win = 1000,
},
},
float_pos = {
[4] = { -1, 'NW', 2, 1, 0, false, 100, 1, 1, 0 },
},
win_viewport = {
[2] = {
win = 1000,
topline = 0,
botline = 2,
curline = 0,
curcol = 3,
linecount = 1,
sum_scroll_delta = 0,
},
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000,
},
},
})
else
screen:expect([[
one^ |
╭────────────────╮{1: }|
│{12:one }{c: }│{1: }|
╰────────────────╯{1: }|
{1:~ }|
{3:[No Name] [+] }|
{5:-- }{6:match 1 of 3} |
]])
end
end)
end)
end end
describe('with ext_multigrid and actual mouse grid', function() describe('with ext_multigrid and actual mouse grid', function()