popupmenu.c (53244B)
1 /// @file popupmenu.c 2 /// 3 /// Popup menu (PUM) 4 5 #include <assert.h> 6 #include <stdbool.h> 7 #include <stdint.h> 8 #include <string.h> 9 10 #include "nvim/api/buffer.h" 11 #include "nvim/api/private/defs.h" 12 #include "nvim/api/private/helpers.h" 13 #include "nvim/api/vim.h" 14 #include "nvim/api/win_config.h" 15 #include "nvim/ascii_defs.h" 16 #include "nvim/autocmd.h" 17 #include "nvim/buffer.h" 18 #include "nvim/buffer_defs.h" 19 #include "nvim/charset.h" 20 #include "nvim/cmdexpand.h" 21 #include "nvim/decoration.h" 22 #include "nvim/drawscreen.h" 23 #include "nvim/errors.h" 24 #include "nvim/eval/typval.h" 25 #include "nvim/ex_cmds.h" 26 #include "nvim/ex_cmds_defs.h" 27 #include "nvim/fuzzy.h" 28 #include "nvim/garray.h" 29 #include "nvim/garray_defs.h" 30 #include "nvim/getchar.h" 31 #include "nvim/gettext_defs.h" 32 #include "nvim/globals.h" 33 #include "nvim/grid.h" 34 #include "nvim/grid_defs.h" 35 #include "nvim/highlight.h" 36 #include "nvim/highlight_defs.h" 37 #include "nvim/highlight_group.h" 38 #include "nvim/insexpand.h" 39 #include "nvim/keycodes.h" 40 #include "nvim/mbyte.h" 41 #include "nvim/memory.h" 42 #include "nvim/memory_defs.h" 43 #include "nvim/menu.h" 44 #include "nvim/message.h" 45 #include "nvim/mouse.h" 46 #include "nvim/move.h" 47 #include "nvim/option.h" 48 #include "nvim/option_defs.h" 49 #include "nvim/option_vars.h" 50 #include "nvim/plines.h" 51 #include "nvim/popupmenu.h" 52 #include "nvim/pos_defs.h" 53 #include "nvim/state_defs.h" 54 #include "nvim/strings.h" 55 #include "nvim/syntax.h" 56 #include "nvim/types_defs.h" 57 #include "nvim/ui.h" 58 #include "nvim/ui_compositor.h" 59 #include "nvim/ui_defs.h" 60 #include "nvim/vim_defs.h" 61 #include "nvim/window.h" 62 #include "nvim/winfloat.h" 63 64 static pumitem_T *pum_array = NULL; // items of displayed pum 65 static int pum_size; // nr of items in "pum_array" 66 static int pum_selected; // index of selected item or -1 67 static int pum_first = 0; // index of top item 68 69 static int pum_height; // nr of displayed pum items 70 static int pum_width; // width of displayed pum items 71 static int pum_base_width; // width of pum items base 72 static int pum_kind_width; // width of pum items kind column 73 static int pum_extra_width; // width of extra stuff 74 static int pum_scrollbar; // one when scrollbar present, else zero 75 static bool pum_rl; // true when popupmenu is drawn 'rightleft' 76 77 static int pum_anchor_grid; // grid where position is defined 78 static int pum_row; // top row of pum 79 static int pum_col; // left column of pum, right column if 'rightleft' 80 static int pum_win_row_offset; // The row offset needed to convert to window relative coordinates 81 static int pum_win_col_offset; // The column offset needed to convert to window relative coordinates 82 static int pum_left_col; // left column of pum, before padding or scrollbar 83 static int pum_right_col; // right column of pum, after padding or scrollbar 84 static bool pum_above; // pum is drawn above cursor line 85 86 static bool pum_is_visible = false; 87 static bool pum_is_drawn = false; 88 static bool pum_external = false; 89 static bool pum_invalid = false; // the screen was just cleared 90 91 #include "popupmenu.c.generated.h" 92 #define PUM_DEF_HEIGHT 10 93 94 static void pum_compute_size(void) 95 { 96 // Compute the width of the widest match and the widest extra. 97 pum_base_width = 0; 98 pum_kind_width = 0; 99 pum_extra_width = 0; 100 for (int i = 0; i < pum_size; i++) { 101 if (pum_array[i].pum_text != NULL) { 102 int w = vim_strsize(pum_array[i].pum_text); 103 if (pum_base_width < w) { 104 pum_base_width = w; 105 } 106 } 107 if (pum_array[i].pum_kind != NULL) { 108 int w = vim_strsize(pum_array[i].pum_kind) + 1; 109 if (pum_kind_width < w) { 110 pum_kind_width = w; 111 } 112 } 113 if (pum_array[i].pum_extra != NULL) { 114 int w = vim_strsize(pum_array[i].pum_extra) + 1; 115 if (pum_extra_width < w) { 116 pum_extra_width = w; 117 } 118 } 119 } 120 } 121 122 /// Calculate vertical placement for popup menu. 123 /// Sets pum_row and pum_height based on available space. 124 static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_win_row, 125 int above_row, int below_row, int pum_border_size) 126 { 127 int context_lines; 128 129 // Figure out the size and position of the pum. 130 pum_height = MIN(size, PUM_DEF_HEIGHT); 131 if (p_ph > 0 && pum_height > p_ph) { 132 pum_height = (int)p_ph; 133 } 134 135 // Put the pum below "pum_win_row" if possible. 136 // If there are few lines decide on where there is more room. 137 if (pum_win_row + 2 + pum_border_size >= below_row - pum_height 138 && pum_win_row - above_row > (below_row - above_row) / 2) { 139 // pum above "pum_win_row" 140 pum_above = true; 141 142 if ((State & MODE_CMDLINE) && target_win == NULL) { 143 // For cmdline pum, no need for context lines unless target_win is set 144 context_lines = 0; 145 } else { 146 // Leave two lines of context if possible 147 context_lines = MIN(2, target_win->w_wrow - target_win->w_cline_row); 148 } 149 150 if (pum_win_row >= size + context_lines) { 151 pum_row = pum_win_row - size - context_lines; 152 pum_height = size; 153 } else { 154 pum_row = 0; 155 pum_height = pum_win_row - context_lines; 156 } 157 if (p_ph > 0 && pum_height > p_ph) { 158 pum_row += pum_height - (int)p_ph; 159 pum_height = (int)p_ph; 160 } 161 162 if (pum_border_size > 0 && pum_border_size + pum_row + pum_height >= pum_win_row) { 163 if (pum_row < 2) { 164 pum_height -= pum_border_size; 165 } else { 166 pum_row -= pum_border_size; 167 } 168 } 169 } else { 170 // pum below "pum_win_row" 171 pum_above = false; 172 173 if ((State & MODE_CMDLINE) && target_win == NULL) { 174 // for cmdline pum, no need for context lines unless target_win is set 175 context_lines = 0; 176 } else { 177 // Leave three lines of context if possible 178 validate_cheight(target_win); 179 int cline_visible_offset = target_win->w_cline_row + 180 target_win->w_cline_height - target_win->w_wrow; 181 context_lines = MIN(3, cline_visible_offset); 182 } 183 184 pum_row = pum_win_row + context_lines; 185 pum_height = MIN(below_row - pum_row, size); 186 if (p_ph > 0 && pum_height > p_ph) { 187 pum_height = (int)p_ph; 188 } 189 190 if (pum_row + pum_height + pum_border_size >= cmdline_row) { 191 pum_height -= pum_border_size; 192 } 193 } 194 195 // If there is a preview window above avoid drawing over it. 196 if (above_row > 0 && pum_row < above_row && pum_height > above_row) { 197 pum_row = above_row; 198 pum_height = pum_win_row - above_row; 199 } 200 } 201 202 /// Try to set "pum_width" so that it fits within available_width. 203 /// Returns true if pum_width was successfully set, FALSE otherwise. 204 static bool set_pum_width_aligned_with_cursor(int width, int available_width) 205 { 206 bool end_padding = true; 207 208 if (width < p_pw) { 209 width = (int)p_pw; 210 end_padding = false; 211 } 212 if (p_pmw > 0 && width > p_pmw) { 213 width = (int)p_pmw; 214 end_padding = false; 215 } 216 217 pum_width = width + (end_padding && width >= p_pw ? 1 : 0); 218 return available_width >= pum_width; 219 } 220 221 /// Calculate horizontal placement for popup menu. Sets pum_col and pum_width 222 /// based on cursor position and available space. 223 static void pum_compute_horizontal_placement(win_T *target_win, int cursor_col) 224 { 225 int max_col = MAX(Columns, target_win ? (target_win->w_wincol + target_win->w_view_width) : 0); 226 int desired_width = pum_base_width + pum_kind_width + pum_extra_width; 227 int available_width; 228 229 if (pum_rl) { 230 available_width = cursor_col - pum_scrollbar + 1; 231 } else { 232 available_width = max_col - cursor_col - pum_scrollbar; 233 } 234 235 // Align pum with "cursor_col" 236 pum_col = cursor_col; 237 if (set_pum_width_aligned_with_cursor(desired_width, available_width)) { 238 return; 239 } 240 241 // Show the pum truncated, provided it is at least as wide as 'pum_width' 242 if (available_width > p_pw) { 243 pum_width = available_width; 244 return; 245 } 246 247 // Truncated pum is no longer aligned with "cursor_col" 248 if (pum_rl) { 249 available_width = max_col - pum_scrollbar; 250 } else { 251 available_width += cursor_col; 252 } 253 254 if (available_width > p_pw) { 255 pum_width = (int)p_pw + 1; // Truncate beyond 'pum_width' 256 if (pum_rl) { 257 pum_col = pum_width + pum_scrollbar; 258 } else { 259 pum_col = max_col - pum_width - pum_scrollbar; 260 } 261 return; 262 } 263 264 // Not enough room anywhere, use what we have 265 if (pum_rl) { 266 pum_col = max_col - 1; 267 } else { 268 pum_col = 0; 269 } 270 pum_width = max_col - pum_scrollbar; 271 } 272 273 static inline int pum_border_width(void) 274 { 275 if (*p_pumborder == NUL || strequal(p_pumborder, opt_winborder_values[7])) { 276 return 0; // No border 277 } 278 // Shadow (1) only has right+bottom, others (2) have full border 279 return strequal(p_pumborder, opt_winborder_values[3]) ? 1 : 2; 280 } 281 282 /// Show the popup menu with items "array[size]". 283 /// "array" must remain valid until pum_undisplay() is called! 284 /// When possible the leftmost character is aligned with cursor column. 285 /// The menu appears above the screen line "row" or at "row" + "height" - 1. 286 /// 287 /// @param array 288 /// @param size 289 /// @param selected index of initially selected item, -1 if out of range 290 /// @param array_changed if true, array contains different items since last call 291 /// if false, a new item is selected, but the array 292 /// is the same 293 /// @param cmd_startcol only for cmdline mode: column of completed match 294 void pum_display(pumitem_T *array, int size, int selected, bool array_changed, int cmd_startcol) 295 { 296 int redo_count = 0; 297 int pum_win_row; 298 int cursor_col; 299 300 if (!pum_is_visible) { 301 // To keep the code simple, we only allow changing the 302 // draw mode when the popup menu is not being displayed 303 pum_external = ui_has(kUIPopupmenu) 304 || ((State & MODE_CMDLINE) && ui_has(kUIWildmenu)); 305 } 306 307 pum_rl = (State & MODE_CMDLINE) == 0 && curwin->w_p_rl; 308 309 int border_width = pum_border_width(); 310 do { 311 // Mark the pum as visible already here, 312 // to avoid that must_redraw is set when 'cursorcolumn' is on. 313 pum_is_visible = true; 314 pum_is_drawn = true; 315 validate_cursor_col(curwin); 316 int above_row = 0; 317 int below_row = MAX(cmdline_row, curwin->w_winrow + curwin->w_view_height); 318 if (State & MODE_CMDLINE) { 319 below_row = cmdline_row; 320 } 321 win_T *target_win = (State & MODE_CMDLINE) ? cmdline_win : curwin; 322 pum_win_row_offset = 0; 323 pum_win_col_offset = 0; 324 325 // wildoptions=pum 326 if (State & MODE_CMDLINE) { 327 pum_win_row = cmdline_win ? cmdline_win->w_wrow : ui_has(kUICmdline) ? 0 : cmdline_row; 328 cursor_col = (cmdline_win ? cmdline_win->w_config._cmdline_offset : 0) + cmd_startcol; 329 cursor_col %= cmdline_win ? cmdline_win->w_view_width : Columns; 330 pum_anchor_grid = ui_has(kUICmdline) ? -1 : DEFAULT_GRID_HANDLE; 331 } else { 332 // anchor position: the start of the completed word 333 pum_win_row = curwin->w_wrow; 334 if (pum_rl) { 335 cursor_col = curwin->w_view_width - curwin->w_wcol - 1; 336 } else { 337 cursor_col = curwin->w_wcol; 338 } 339 } 340 341 if (target_win != NULL) { 342 // ext_popupmenu should always anchor to the default grid when multigrid is disabled 343 pum_anchor_grid = target_win->w_grid.target->handle; 344 pum_win_row += target_win->w_grid.row_offset; 345 cursor_col += target_win->w_grid.col_offset; 346 if (target_win->w_grid.target != &default_grid) { 347 pum_win_row += target_win->w_winrow; 348 cursor_col += target_win->w_wincol; 349 if (!ui_has(kUIMultigrid)) { 350 pum_anchor_grid = DEFAULT_GRID_HANDLE; 351 } else { 352 pum_win_row_offset = target_win->w_winrow; 353 pum_win_col_offset = target_win->w_wincol; 354 } 355 } 356 } 357 358 if (pum_external) { 359 if (array_changed) { 360 Arena arena = ARENA_EMPTY; 361 Array arr = arena_array(&arena, (size_t)size); 362 for (int i = 0; i < size; i++) { 363 Array item = arena_array(&arena, 4); 364 ADD_C(item, CSTR_AS_OBJ(array[i].pum_text)); 365 ADD_C(item, CSTR_AS_OBJ(array[i].pum_kind)); 366 ADD_C(item, CSTR_AS_OBJ(array[i].pum_extra)); 367 ADD_C(item, CSTR_AS_OBJ(array[i].pum_info)); 368 ADD_C(arr, ARRAY_OBJ(item)); 369 } 370 ui_call_popupmenu_show(arr, selected, pum_win_row - pum_win_row_offset, 371 cursor_col - pum_win_col_offset, 372 pum_anchor_grid); 373 arena_mem_free(arena_finish(&arena)); 374 } else { 375 ui_call_popupmenu_select(selected); 376 return; 377 } 378 } 379 380 win_T *pvwin = NULL; 381 FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { 382 if (wp->w_p_pvw) { 383 pvwin = wp; 384 break; 385 } 386 } 387 388 if (pvwin != NULL) { 389 if (pvwin->w_winrow < curwin->w_winrow) { 390 above_row = pvwin->w_winrow + pvwin->w_height; 391 } else if (pvwin->w_winrow > curwin->w_winrow + curwin->w_height) { 392 below_row = pvwin->w_winrow; 393 } 394 } 395 396 // Figure out the vertical size and position of the pum. 397 pum_compute_vertical_placement(size, target_win, pum_win_row, above_row, below_row, 398 border_width); 399 400 // don't display when we only have room for one line 401 if (border_width == 0 && (pum_height < 1 || (pum_height == 1 && size > 1))) { 402 return; 403 } 404 405 pum_array = array; 406 // Set "pum_size" before returning so that pum_set_event_info() gets the correct size. 407 pum_size = size; 408 409 if (pum_external) { 410 return; 411 } 412 413 pum_compute_size(); 414 415 // if there are more items than room we need a scrollbar 416 pum_scrollbar = (pum_height < size) ? 1 : 0; 417 418 // Figure out the horizontal size and position of the pum. 419 pum_compute_horizontal_placement(target_win, cursor_col); 420 421 if (pum_col + border_width + pum_width > Columns) { 422 pum_col -= border_width; 423 } 424 425 // Set selected item and redraw. If the window size changed need to redo 426 // the positioning. Limit this to two times, when there is not much 427 // room the window size will keep changing. 428 } while (pum_set_selected(selected, redo_count) && ++redo_count <= 2); 429 430 pum_grid.zindex = (State & MODE_CMDLINE) ? kZIndexCmdlinePopupMenu : kZIndexPopupMenu; 431 pum_redraw(); 432 } 433 434 /// Computes attributes of text on the popup menu. 435 /// Returns attributes for every cell, or NULL if all attributes are the same. 436 static int *pum_compute_text_attrs(char *text, hlf_T hlf, int user_hlattr) 437 { 438 if (*text == NUL || (hlf != HLF_PSI && hlf != HLF_PNI) 439 || (win_hl_attr(curwin, HLF_PMSI) == win_hl_attr(curwin, HLF_PSI) 440 && win_hl_attr(curwin, HLF_PMNI) == win_hl_attr(curwin, HLF_PNI))) { 441 return NULL; 442 } 443 444 char *leader = (State & MODE_CMDLINE) ? cmdline_compl_pattern() 445 : ins_compl_leader(); 446 if (leader == NULL || *leader == NUL) { 447 return NULL; 448 } 449 450 int *attrs = xmalloc(sizeof(int) * (size_t)vim_strsize(text)); 451 bool in_fuzzy = (State & MODE_CMDLINE) ? cmdline_compl_is_fuzzy() 452 : (get_cot_flags() & kOptCotFlagFuzzy) != 0; 453 size_t leader_len = strlen(leader); 454 455 garray_T *ga = NULL; 456 int matched_len = -1; 457 458 if (in_fuzzy) { 459 ga = fuzzy_match_str_with_pos(text, leader); 460 if (!ga) { 461 xfree(attrs); 462 return NULL; 463 } 464 } 465 466 const char *ptr = text; 467 int cell_idx = 0; 468 uint32_t char_pos = 0; 469 bool is_select = hlf == HLF_PSI; 470 471 while (*ptr != NUL) { 472 int new_attr = win_hl_attr(curwin, (int)hlf); 473 474 if (ga != NULL) { 475 // Handle fuzzy matching 476 for (int i = 0; i < ga->ga_len; i++) { 477 if (char_pos == ((uint32_t *)ga->ga_data)[i]) { 478 new_attr = win_hl_attr(curwin, is_select ? HLF_PMSI : HLF_PMNI); 479 new_attr = hl_combine_attr(win_hl_attr(curwin, HLF_PMNI), new_attr); 480 new_attr = hl_combine_attr(win_hl_attr(curwin, (int)hlf), new_attr); 481 break; 482 } 483 } 484 } else { 485 if (matched_len < 0 && mb_strnicmp(ptr, leader, leader_len) == 0) { 486 matched_len = (int)leader_len; 487 } 488 if (matched_len > 0) { 489 new_attr = win_hl_attr(curwin, is_select ? HLF_PMSI : HLF_PMNI); 490 new_attr = hl_combine_attr(win_hl_attr(curwin, HLF_PMNI), new_attr); 491 new_attr = hl_combine_attr(win_hl_attr(curwin, (int)hlf), new_attr); 492 matched_len--; 493 } 494 } 495 496 new_attr = hl_combine_attr(win_hl_attr(curwin, HLF_PNI), new_attr); 497 498 if (user_hlattr > 0) { 499 new_attr = hl_combine_attr(new_attr, user_hlattr); 500 } 501 502 int char_cells = utf_ptr2cells(ptr); 503 for (int i = 0; i < char_cells; i++) { 504 attrs[cell_idx + i] = new_attr; 505 } 506 cell_idx += char_cells; 507 508 MB_PTR_ADV(ptr); 509 char_pos++; 510 } 511 512 if (ga != NULL) { 513 ga_clear(ga); 514 xfree(ga); 515 } 516 return attrs; 517 } 518 519 /// Displays text on the popup menu with specific attributes. 520 static void pum_grid_puts_with_attrs(int col, int cells, const char *text, int textlen, 521 const int *attrs) 522 { 523 const int col_start = col; 524 const char *ptr = text; 525 526 // Render text with proper attributes 527 while (*ptr != NUL && (textlen < 0 || ptr < text + textlen)) { 528 int char_len = utfc_ptr2len(ptr); 529 int attr = attrs[pum_rl ? (col_start + cells - col - 1) : (col - col_start)]; 530 grid_line_puts(col, ptr, char_len, attr); 531 col += utf_ptr2cells(ptr); 532 ptr += char_len; 533 } 534 } 535 536 static inline void pum_align_order(int *order) 537 { 538 bool is_default = cia_flags == 0; 539 order[0] = is_default ? CPT_ABBR : cia_flags / 100; 540 order[1] = is_default ? CPT_KIND : (cia_flags / 10) % 10; 541 order[2] = is_default ? CPT_MENU : cia_flags % 10; 542 } 543 544 static inline char *pum_get_item(int index, int type) 545 { 546 switch (type) { 547 case CPT_ABBR: 548 return pum_array[index].pum_text; 549 case CPT_KIND: 550 return pum_array[index].pum_kind; 551 case CPT_MENU: 552 return pum_array[index].pum_extra; 553 } 554 return NULL; 555 } 556 557 static inline int pum_user_attr_combine(int idx, int type, int attr) 558 { 559 int user_attr[] = { 560 pum_array[idx].pum_user_abbr_hlattr, 561 pum_array[idx].pum_user_kind_hlattr, 562 }; 563 return user_attr[type] > 0 ? hl_combine_attr(attr, user_attr[type]) : attr; 564 } 565 566 /// Redraw the popup menu, using "pum_first" and "pum_selected". 567 void pum_redraw(void) 568 { 569 int row = 0; 570 int attr_scroll = win_hl_attr(curwin, HLF_PSB); 571 int attr_thumb = win_hl_attr(curwin, HLF_PST); 572 char *p = NULL; 573 int thumb_pos = 0; 574 int thumb_height = 1; 575 int n; 576 const schar_T fcs_trunc = pum_rl ? curwin->w_p_fcs_chars.truncrl 577 : curwin->w_p_fcs_chars.trunc; 578 579 // "word" "kind" "extra text" 580 const hlf_T hlfsNorm[3] = { HLF_PNI, HLF_PNK, HLF_PNX }; 581 const hlf_T hlfsSel[3] = { HLF_PSI, HLF_PSK, HLF_PSX }; 582 583 int grid_width = pum_width; 584 int col_off = 0; 585 bool extra_space = false; 586 if (pum_rl) { 587 col_off = pum_width - 1; 588 assert(!(State & MODE_CMDLINE)); 589 int win_end_col = W_ENDCOL(curwin); 590 if (pum_col < win_end_col - 1) { 591 grid_width += 1; 592 extra_space = true; 593 } 594 } else { 595 int min_col = 0; 596 if (pum_col > min_col) { 597 grid_width += 1; 598 col_off = 1; 599 extra_space = true; 600 } 601 } 602 WinConfig fconfig = WIN_CONFIG_INIT; 603 int border_width = pum_border_width(); 604 int border_attr = 0; 605 schar_T border_char = 0; 606 schar_T fill_char = schar_from_ascii(' '); 607 bool has_border = border_width > 0; 608 // setup popup menu border if 'pumborder' option is set 609 if (border_width > 0) { 610 Error err = ERROR_INIT; 611 if (!parse_winborder(&fconfig, p_pumborder, &err)) { 612 if (ERROR_SET(&err)) { 613 emsg(err.msg); 614 } 615 api_clear_error(&err); 616 return; 617 } 618 619 // Shadow style: only adds border on right and bottom edges 620 if (strequal(p_pumborder, opt_winborder_values[3])) { 621 fconfig.shadow = true; 622 int blend = SYN_GROUP_STATIC("PmenuShadow"); 623 int through = SYN_GROUP_STATIC("PmenuShadowThrough"); 624 fconfig.border_hl_ids[2] = through; 625 fconfig.border_hl_ids[3] = blend; 626 fconfig.border_hl_ids[4] = blend; 627 fconfig.border_hl_ids[5] = blend; 628 fconfig.border_hl_ids[6] = through; 629 } 630 631 // convert border highlight IDs to attributes, use PmenuBorder as default 632 for (int i = 0; i < 8; i++) { 633 int attr = hl_attr_active[HLF_PBR]; 634 if (fconfig.border_hl_ids[i]) { 635 attr = hl_get_ui_attr(-1, HLF_PBR, fconfig.border_hl_ids[i], false); 636 } 637 fconfig.border_attr[i] = attr; 638 } 639 api_clear_error(&err); 640 if (pum_scrollbar) { 641 border_char = schar_from_str(fconfig.border_chars[3]); 642 border_attr = fconfig.border_attr[3]; 643 } 644 } 645 646 if (pum_scrollbar > 0 && (!fconfig.border || fconfig.shadow)) { 647 grid_width++; 648 if (pum_rl) { 649 col_off++; 650 } 651 } 652 pum_grid.blending = p_pb > 0 || fconfig.shadow; 653 grid_assign_handle(&pum_grid); 654 655 pum_left_col = pum_col - col_off; 656 pum_right_col = pum_left_col + grid_width; 657 bool moved = ui_comp_put_grid(&pum_grid, pum_row, pum_left_col, 658 pum_height + border_width, grid_width + border_width, false, 659 true); 660 bool invalid_grid = moved || pum_invalid; 661 pum_invalid = false; 662 must_redraw_pum = false; 663 664 if (!pum_grid.chars || pum_grid.rows != pum_height + border_width 665 || pum_grid.cols != grid_width + border_width) { 666 grid_alloc(&pum_grid, pum_height + border_width, grid_width + border_width, 667 !invalid_grid, false); 668 ui_call_grid_resize(pum_grid.handle, pum_grid.cols, pum_grid.rows); 669 } else if (invalid_grid) { 670 grid_invalidate(&pum_grid); 671 } 672 if (ui_has(kUIMultigrid)) { 673 const char *anchor = pum_above ? "SW" : "NW"; 674 int row_off = pum_above ? -pum_height : 0; 675 ui_call_win_float_pos(pum_grid.handle, -1, cstr_as_string(anchor), pum_anchor_grid, 676 pum_row - row_off - pum_win_row_offset, pum_left_col - pum_win_col_offset, 677 false, pum_grid.zindex, (int)pum_grid.comp_index, pum_grid.comp_row, 678 pum_grid.comp_col); 679 } 680 681 int scroll_range = pum_size - pum_height; 682 683 // avoid set border for mouse menu 684 int mouse_menu = State != MODE_CMDLINE && pum_grid.zindex == kZIndexCmdlinePopupMenu; 685 if (!mouse_menu && fconfig.border) { 686 grid_draw_border(&pum_grid, &fconfig, NULL, 0, NULL); 687 if (!fconfig.shadow) { 688 row++; 689 col_off++; 690 } 691 } 692 693 // Never display more than we have 694 pum_first = MIN(pum_first, scroll_range); 695 696 if (pum_scrollbar) { 697 thumb_height = pum_height * pum_height / pum_size; 698 if (thumb_height == 0) { 699 thumb_height = 1; 700 } 701 thumb_pos = (pum_first * (pum_height - thumb_height) + scroll_range / 2) / scroll_range; 702 } 703 704 for (int i = 0; i < pum_height; i++) { 705 int idx = i + pum_first; 706 const bool selected = idx == pum_selected; 707 const hlf_T *const hlfs = selected ? hlfsSel : hlfsNorm; 708 const int trunc_attr = win_hl_attr(curwin, selected ? HLF_PSI : HLF_PNI); 709 hlf_T hlf = hlfs[0]; // start with "word" highlight 710 int attr = win_hl_attr(curwin, (int)hlf); 711 attr = hl_combine_attr(win_hl_attr(curwin, HLF_PNI), attr); 712 713 screengrid_line_start(&pum_grid, row, 0); 714 715 // prepend a space if there is room 716 if (extra_space) { 717 if (pum_rl) { 718 grid_line_puts(col_off + 1, " ", 1, attr); 719 } else { 720 grid_line_puts(col_off - 1, " ", 1, attr); 721 } 722 } 723 724 // Display each entry, use two spaces for a Tab. 725 // Do this 3 times and order from p_cia 726 int grid_col = col_off; 727 int totwidth = 0; 728 bool need_fcs_trunc = false; 729 int order[3]; 730 int items_width_array[3] = { pum_base_width, pum_kind_width, pum_extra_width }; 731 pum_align_order(order); 732 int basic_width = items_width_array[order[0]]; // first item width 733 bool last_isabbr = order[2] == CPT_ABBR; 734 int orig_attr = -1; 735 736 for (int j = 0; j < 3; j++) { 737 int item_type = order[j]; 738 hlf = hlfs[item_type]; 739 attr = win_hl_attr(curwin, (int)hlf); 740 attr = hl_combine_attr(win_hl_attr(curwin, HLF_PNI), attr); 741 orig_attr = attr; 742 if (item_type < 2) { // try combine attr with user custom 743 attr = pum_user_attr_combine(idx, item_type, attr); 744 } 745 int width = 0; 746 char *s = NULL; 747 p = pum_get_item(idx, item_type); 748 749 const bool next_isempty = j + 1 >= 3 || pum_get_item(idx, order[j + 1]) == NULL; 750 751 if (p != NULL) { 752 for (;; MB_PTR_ADV(p)) { 753 if (s == NULL) { 754 s = p; 755 } 756 int w = ptr2cells(p); 757 if (*p != NUL && *p != TAB && totwidth + w <= pum_width) { 758 width += w; 759 continue; 760 } 761 const int width_limit = pum_width; 762 763 // Display the text that fits or comes before a Tab. 764 // First convert it to printable characters. 765 char saved = *p; 766 767 if (saved != NUL) { 768 *p = NUL; 769 } 770 char *st = transstr(s, true); 771 if (saved != NUL) { 772 *p = saved; 773 } 774 775 int *attrs = NULL; 776 if (item_type == CPT_ABBR) { 777 attrs = pum_compute_text_attrs(st, hlf, 778 pum_array[idx].pum_user_abbr_hlattr); 779 } 780 781 if (pum_rl) { 782 char *rt = reverse_text(st); 783 char *rt_start = rt; 784 int cells = (int)mb_string2cells(rt); 785 int pad = next_isempty ? 0 : 2; 786 if (width_limit - totwidth < cells + pad) { 787 need_fcs_trunc = true; 788 } 789 790 // only draw the text that fits 791 if (grid_col - cells < col_off - width_limit) { 792 do { 793 cells -= utf_ptr2cells(rt); 794 MB_PTR_ADV(rt); 795 } while (grid_col - cells < col_off - width_limit); 796 797 if (grid_col - cells > col_off - width_limit) { 798 // Most left character requires 2-cells but only 1 cell is available on 799 // screen. Put a '<' on the left of the pum item. 800 *(--rt) = '<'; 801 cells++; 802 } 803 } 804 805 if (attrs == NULL) { 806 grid_line_puts(grid_col - cells + 1, rt, -1, attr); 807 } else { 808 pum_grid_puts_with_attrs(grid_col - cells + 1, cells, rt, -1, attrs); 809 } 810 811 xfree(rt_start); 812 xfree(st); 813 grid_col -= width; 814 } else { 815 int cells = (int)mb_string2cells(st); 816 int pad = next_isempty ? 0 : 2; 817 if (width_limit - totwidth < cells + pad) { 818 need_fcs_trunc = true; 819 } 820 if (need_fcs_trunc) { 821 int available_cells = width_limit - totwidth; 822 // Find the truncation point by counting display cells 823 char *p_end = st; 824 int displayed = 0; 825 while (*p_end != NUL) { 826 int char_cells = utf_ptr2cells(p_end); 827 if (displayed + char_cells > available_cells) { 828 break; 829 } 830 displayed += char_cells; 831 MB_PTR_ADV(p_end); 832 } 833 *p_end = NUL; 834 cells = displayed; 835 width = displayed; 836 } 837 838 if (attrs == NULL) { 839 grid_line_puts(grid_col, st, -1, attr); 840 } else { 841 pum_grid_puts_with_attrs(grid_col, cells, st, -1, attrs); 842 } 843 844 xfree(st); 845 grid_col += width; 846 } 847 848 if (attrs != NULL) { 849 XFREE_CLEAR(attrs); 850 } 851 852 if (*p != TAB) { 853 break; 854 } 855 856 // Display two spaces for a Tab. 857 if (pum_rl) { 858 grid_line_puts(grid_col - 1, " ", 2, attr); 859 grid_col -= 2; 860 } else { 861 grid_line_puts(grid_col, " ", 2, attr); 862 grid_col += 2; 863 } 864 totwidth += 2; 865 s = NULL; // start text at next char 866 width = 0; 867 } 868 } 869 870 if (j > 0) { 871 n = items_width_array[order[1]] + (last_isabbr ? 0 : 1); 872 } else { 873 n = order[j] == CPT_ABBR ? 1 : 0; 874 } 875 876 // Stop when there is nothing more to display. 877 if ((j == 2) 878 || (next_isempty && (j == 1 || (j == 0 && pum_get_item(idx, order[j + 2]) == NULL))) 879 || (basic_width + n >= pum_width)) { 880 break; 881 } 882 883 if (pum_rl) { 884 grid_line_fill(col_off - basic_width - n + 1, grid_col + 1, 885 schar_from_ascii(' '), orig_attr); 886 grid_col = col_off - basic_width - n; 887 } else { 888 grid_line_fill(grid_col, col_off + basic_width + n, 889 schar_from_ascii(' '), orig_attr); 890 grid_col = col_off + basic_width + n; 891 } 892 totwidth = basic_width + n; 893 } 894 895 if (pum_rl) { 896 const int lcol = col_off - pum_width + 1; 897 grid_line_fill(lcol, grid_col + 1, schar_from_ascii(' '), orig_attr); 898 if (need_fcs_trunc) { 899 linebuf_char[lcol] = fcs_trunc != NUL ? fcs_trunc : schar_from_ascii('<'); 900 linebuf_attr[lcol] = trunc_attr; 901 if (pum_width > 1 && linebuf_char[lcol + 1] == NUL) { 902 linebuf_char[lcol + 1] = schar_from_ascii(' '); 903 } 904 } 905 } else { 906 const int rcol = col_off + pum_width; 907 grid_line_fill(grid_col, rcol, schar_from_ascii(' '), orig_attr); 908 if (need_fcs_trunc) { 909 if (pum_width > 1 && linebuf_char[rcol - 1] == NUL) { 910 linebuf_char[rcol - 2] = schar_from_ascii(' '); 911 } 912 linebuf_char[rcol - 1] = fcs_trunc != NUL ? fcs_trunc : schar_from_ascii('>'); 913 linebuf_attr[rcol - 1] = trunc_attr; 914 } 915 } 916 917 if (pum_scrollbar > 0) { 918 bool thumb = i >= thumb_pos && i < thumb_pos + thumb_height; 919 int scrollbar_col = col_off + (pum_rl ? -pum_width : pum_width); 920 bool use_border_style = has_border && !fconfig.shadow; 921 grid_line_put_schar(scrollbar_col, 922 (use_border_style && !thumb) ? border_char : fill_char, 923 thumb ? attr_thumb : (use_border_style ? border_attr : attr_scroll)); 924 } 925 grid_line_flush(); 926 row++; 927 } 928 } 929 930 /// Set the informational text in the preview buffer when the completion 931 /// item does not include a dedicated preview or popup window. 932 /// 933 /// @param[in] win Window containing buffer where the text will be set. 934 /// @param[in] info Informational text to display in the preview buffer. 935 /// @param[in] lnum Where to start the text. Incremented for each added line. 936 /// @param[out] max_width Maximum width of the displayed text. 937 static void pum_preview_set_text(win_T *win, char *info, linenr_T *lnum, int *max_width) 938 { 939 Error err = ERROR_INIT; 940 Arena arena = ARENA_EMPTY; 941 Array replacement = ARRAY_DICT_INIT; 942 buf_T *buf = win->w_buffer; 943 buf->b_p_ma = true; 944 945 // Iterate through the string line by line by temporarily replacing newlines with NUL 946 for (char *curr = info, *next; curr; curr = next ? next + 1 : NULL) { 947 if ((next = strchr(curr, '\n'))) { 948 *next = NUL; // Temporarily replace the newline with a string terminator 949 } 950 // Only skip if this is an empty line AND it's the last line 951 if (*curr == '\0' && !next) { 952 break; 953 } 954 955 *max_width = MAX(*max_width, win_linetabsize(win, 0, curr, MAXCOL)); 956 ADD(replacement, STRING_OBJ(cstr_to_string(curr))); 957 (*lnum)++; 958 959 if (next) { 960 *next = '\n'; 961 } 962 } 963 964 int original_textlock = textlock; 965 textlock = 0; 966 nvim_buf_set_lines(0, buf->handle, 0, -1, false, replacement, &arena, &err); 967 textlock = original_textlock; 968 if (ERROR_SET(&err)) { 969 emsg(err.msg); 970 api_clear_error(&err); 971 } 972 arena_mem_free(arena_finish(&arena)); 973 api_free_array(replacement); 974 buf->b_p_ma = false; 975 } 976 977 /// adjust floating info preview window position 978 static bool pum_adjust_info_position(win_T *wp, int width) 979 { 980 int border_width = pum_border_width(); 981 int col = pum_col + pum_width + 1 + MAX(border_width, pum_scrollbar); 982 // TODO(glepnir): support config align border by using completepopup 983 // align menu 984 int right_extra = Columns - col; 985 int left_extra = pum_col - 2; 986 987 int max_extra = MAX(right_extra, left_extra); 988 // Close info window if there's insufficient space 989 // TODO(glepnir): Replace the hardcoded value (10) with values from the 'completepopup' width/height options. 990 if (max_extra < 10) { 991 wp->w_config.hide = true; 992 return false; 993 } 994 995 if (right_extra > width) { // place in right 996 wp->w_config.width = width; 997 wp->w_config.col = col - 1; 998 } else if (left_extra > width) { // place in left 999 wp->w_config.width = width; 1000 wp->w_config.col = pum_col - wp->w_config.width - 1; 1001 } else { // either width is enough just use the biggest one. 1002 const bool place_in_right = right_extra > left_extra; 1003 wp->w_config.width = max_extra; 1004 wp->w_config.col = place_in_right ? col - 1 : pum_col - wp->w_config.width - 1; 1005 } 1006 // when pum_above is SW otherwise is NW 1007 wp->w_config.anchor = pum_above ? kFloatAnchorSouth : 0; 1008 linenr_T count = wp->w_buffer->b_ml.ml_line_count; 1009 wp->w_view_width = wp->w_config.width; 1010 wp->w_config.height = plines_m_win(wp, wp->w_topline, count, Rows); 1011 wp->w_config.row = pum_above ? pum_row + wp->w_config.height : pum_row; 1012 wp->w_config.hide = false; 1013 win_config_float(wp, wp->w_config); 1014 return true; 1015 } 1016 1017 /// Used for nvim__complete_set 1018 /// 1019 /// @param selected the selected compl item. 1020 /// @param info Info string. 1021 /// @return a win_T pointer. 1022 win_T *pum_set_info(int selected, char *info) 1023 { 1024 if (!pum_is_visible || !compl_match_curr_select(selected)) { 1025 return NULL; 1026 } 1027 block_autocmds(); 1028 RedrawingDisabled++; 1029 no_u_sync++; 1030 win_T *wp = win_float_find_preview(); 1031 if (wp == NULL) { 1032 wp = win_float_create_preview(false, true); 1033 if (!wp) { 1034 return NULL; 1035 } 1036 wp->w_topline = 1; 1037 wp->w_p_wfb = true; 1038 } 1039 linenr_T lnum = 0; 1040 int max_info_width = 0; 1041 pum_preview_set_text(wp, info, &lnum, &max_info_width); 1042 no_u_sync--; 1043 RedrawingDisabled--; 1044 redraw_later(wp, UPD_NOT_VALID); 1045 1046 if (!pum_adjust_info_position(wp, max_info_width)) { 1047 wp = NULL; 1048 } 1049 unblock_autocmds(); 1050 return wp; 1051 } 1052 1053 /// Set the index of the currently selected item. The menu will scroll when 1054 /// necessary. When "n" is out of range don't scroll. 1055 /// This may be repeated when the preview window is used: 1056 /// "repeat" == 0: open preview window normally 1057 /// "repeat" == 1: open preview window but don't set the size 1058 /// "repeat" == 2: don't open preview window 1059 /// 1060 /// @param n 1061 /// @param repeat 1062 /// 1063 /// @returns true when the window was resized and the location of the popup 1064 /// menu must be recomputed. 1065 static bool pum_set_selected(int n, int repeat) 1066 { 1067 bool resized = false; 1068 int context = pum_height / 2; 1069 int prev_selected = pum_selected; 1070 1071 pum_selected = n; 1072 int scroll_offset = pum_selected - pum_height; 1073 unsigned cur_cot_flags = get_cot_flags(); 1074 bool use_float = (cur_cot_flags & kOptCotFlagPopup) != 0; 1075 1076 // Close the floating preview window if 'selected' is -1, indicating a return to the original 1077 // state. It is also closed when the selected item has no corresponding info item. 1078 if (use_float && (pum_selected < 0 || pum_array[pum_selected].pum_info == NULL)) { 1079 win_T *wp = win_float_find_preview(); 1080 if (wp) { 1081 wp->w_config.hide = true; 1082 win_config_float(wp, wp->w_config); 1083 } 1084 } 1085 1086 if ((pum_selected >= 0) && (pum_selected < pum_size)) { 1087 if (pum_first > pum_selected - 4) { 1088 // scroll down; when we did a jump it's probably a PageUp then 1089 // scroll a whole page 1090 if (pum_first > pum_selected - 2) { 1091 pum_first -= pum_height - 2; 1092 if (pum_first < 0) { 1093 pum_first = 0; 1094 } else if (pum_first > pum_selected) { 1095 pum_first = pum_selected; 1096 } 1097 } else { 1098 pum_first = pum_selected; 1099 } 1100 } else if (pum_first < scroll_offset + 5) { 1101 // scroll up; when we did a jump it's probably a PageDown then 1102 // scroll a whole page 1103 if (pum_first < scroll_offset + 3) { 1104 pum_first = MAX(pum_first + pum_height - 2, scroll_offset + 1); 1105 } else { 1106 pum_first = scroll_offset + 1; 1107 } 1108 } 1109 1110 // Give a few lines of context when possible. 1111 context = MIN(context, 3); 1112 1113 if (pum_height > 2) { 1114 if (pum_first > pum_selected - context) { 1115 pum_first = MAX(pum_selected - context, 0); // scroll down 1116 } else if (pum_first < pum_selected + context - pum_height + 1) { 1117 pum_first = pum_selected + context - pum_height + 1; // up 1118 } 1119 } 1120 // adjust for the number of lines displayed 1121 pum_first = MIN(pum_first, pum_size - pum_height); 1122 1123 // Show extra info in the preview window if there is something and 1124 // 'completeopt' contains "preview". 1125 // Skip this when tried twice already. 1126 // Skip this also when there is not much room. 1127 // Skip this for command-window when 'completeopt' contains "preview". 1128 // NOTE: Be very careful not to sync undo! 1129 if ((pum_array[pum_selected].pum_info != NULL) 1130 && (Rows > 10) 1131 && (repeat <= 1) 1132 && (cur_cot_flags & (kOptCotFlagPreview | kOptCotFlagPopup)) 1133 && !((cur_cot_flags & kOptCotFlagPreview) && cmdwin_type != 0)) { 1134 win_T *curwin_save = curwin; 1135 tabpage_T *curtab_save = curtab; 1136 1137 if (use_float) { 1138 block_autocmds(); 1139 } 1140 1141 // Open a preview window. 3 lines by default. Prefer 1142 // 'previewheight' if set and smaller. 1143 g_do_tagpreview = 3; 1144 1145 if ((p_pvh > 0) && (p_pvh < g_do_tagpreview)) { 1146 g_do_tagpreview = (int)p_pvh; 1147 } 1148 RedrawingDisabled++; 1149 // Prevent undo sync here, if an autocommand syncs undo weird 1150 // things can happen to the undo tree. 1151 no_u_sync++; 1152 1153 if (!use_float) { 1154 resized = prepare_tagpreview(false); 1155 } else { 1156 win_T *wp = win_float_find_preview(); 1157 if (wp) { 1158 win_enter(wp, false); 1159 } else { 1160 wp = win_float_create_preview(true, true); 1161 if (wp) { 1162 resized = true; 1163 } 1164 } 1165 } 1166 1167 no_u_sync--; 1168 RedrawingDisabled--; 1169 g_do_tagpreview = 0; 1170 1171 if (curwin->w_p_pvw || curwin->w_float_is_info) { 1172 int res = OK; 1173 if (!resized 1174 && (curbuf->b_nwindows == 1) 1175 && (curbuf->b_fname == NULL) 1176 && bt_nofile(curbuf) 1177 && (curbuf->b_p_bh[0] == 'w')) { 1178 // Already a "wipeout" buffer, make it empty. 1179 buf_clear(); 1180 } else { 1181 // Don't want to sync undo in the current buffer. 1182 no_u_sync++; 1183 res = do_ecmd(0, NULL, NULL, NULL, ECMD_ONE, 0, NULL); 1184 no_u_sync--; 1185 1186 if (res == OK) { 1187 // Edit a new, empty buffer. Set options for a "wipeout" 1188 // buffer. 1189 set_option_value_give_err(kOptSwapfile, BOOLEAN_OPTVAL(false), OPT_LOCAL); 1190 set_option_value_give_err(kOptBuflisted, BOOLEAN_OPTVAL(false), OPT_LOCAL); 1191 set_option_value_give_err(kOptBuftype, STATIC_CSTR_AS_OPTVAL("nofile"), OPT_LOCAL); 1192 set_option_value_give_err(kOptBufhidden, STATIC_CSTR_AS_OPTVAL("wipe"), OPT_LOCAL); 1193 set_option_value_give_err(kOptDiff, BOOLEAN_OPTVAL(false), OPT_LOCAL); 1194 } 1195 } 1196 1197 if (res == OK) { 1198 linenr_T lnum = 0; 1199 int max_info_width = 0; 1200 pum_preview_set_text(curwin, pum_array[pum_selected].pum_info, &lnum, &max_info_width); 1201 // Increase the height of the preview window to show the 1202 // text, but no more than 'previewheight' lines. 1203 if (repeat == 0 && !use_float) { 1204 lnum = MIN(lnum, (linenr_T)p_pvh); 1205 1206 if (curwin->w_height < lnum) { 1207 win_setheight((int)lnum); 1208 resized = true; 1209 } 1210 } 1211 1212 curbuf->b_changed = false; 1213 curbuf->b_p_ma = false; 1214 if (pum_selected != prev_selected) { 1215 curwin->w_topline = 1; 1216 } else if (curwin->w_topline > curbuf->b_ml.ml_line_count) { 1217 curwin->w_topline = curbuf->b_ml.ml_line_count; 1218 } 1219 curwin->w_cursor.lnum = 1; 1220 curwin->w_cursor.col = 0; 1221 1222 if (use_float) { 1223 // adjust floating window by actually height and max info text width 1224 if (!pum_adjust_info_position(curwin, max_info_width) && win_valid(curwin_save)) { 1225 win_enter(curwin_save, false); 1226 } 1227 } 1228 1229 if ((curwin != curwin_save && win_valid(curwin_save)) 1230 || (curtab != curtab_save && valid_tabpage(curtab_save))) { 1231 if (curtab != curtab_save && valid_tabpage(curtab_save)) { 1232 goto_tabpage_tp(curtab_save, false, false); 1233 } 1234 1235 // When the first completion is done and the preview 1236 // window is not resized, skip the preview window's 1237 // status line redrawing. 1238 if (ins_compl_active() && !resized) { 1239 curwin->w_redr_status = false; 1240 } 1241 1242 // Return cursor to where we were 1243 validate_cursor(curwin); 1244 redraw_later(curwin, UPD_SOME_VALID); 1245 1246 // When the preview window was resized we need to 1247 // update the view on the buffer. Only go back to 1248 // the window when needed, otherwise it will always be 1249 // redrawn. 1250 if (resized && win_valid(curwin_save)) { 1251 no_u_sync++; 1252 win_enter(curwin_save, true); 1253 no_u_sync--; 1254 update_topline(curwin); 1255 } 1256 1257 // Update the screen before drawing the popup menu. 1258 // Enable updating the status lines. 1259 // TODO(bfredl): can simplify, get rid of the flag munging? 1260 // or at least eliminate extra redraw before win_enter()? 1261 pum_is_visible = false; 1262 update_screen(); 1263 pum_is_visible = true; 1264 1265 if (!resized && win_valid(curwin_save)) { 1266 no_u_sync++; 1267 win_enter(curwin_save, true); 1268 no_u_sync--; 1269 } 1270 1271 // May need to update the screen again when there are 1272 // autocommands involved. 1273 pum_is_visible = false; 1274 update_screen(); 1275 pum_is_visible = true; 1276 } 1277 } 1278 } 1279 1280 if (use_float) { 1281 unblock_autocmds(); 1282 } 1283 } 1284 } 1285 1286 return resized; 1287 } 1288 1289 /// Undisplay the popup menu (later). 1290 void pum_undisplay(bool immediate) 1291 { 1292 pum_is_visible = false; 1293 pum_array = NULL; 1294 must_redraw_pum = false; 1295 1296 if (immediate) { 1297 pum_check_clear(); 1298 } 1299 } 1300 1301 void pum_check_clear(void) 1302 { 1303 if (!pum_is_visible && pum_is_drawn) { 1304 if (pum_external) { 1305 ui_call_popupmenu_hide(); 1306 } else { 1307 ui_comp_remove_grid(&pum_grid); 1308 if (ui_has(kUIMultigrid)) { 1309 ui_call_win_close(pum_grid.handle); 1310 ui_call_grid_destroy(pum_grid.handle); 1311 } 1312 // TODO(bfredl): consider keeping float grids allocated. 1313 grid_free(&pum_grid); 1314 } 1315 pum_is_drawn = false; 1316 pum_external = false; 1317 win_T *wp = win_float_find_preview(); 1318 if (wp != NULL) { 1319 win_close(wp, false, false); 1320 } 1321 } 1322 } 1323 1324 /// Clear the popup menu. Currently only resets the offset to the first 1325 /// displayed item. 1326 void pum_clear(void) 1327 { 1328 pum_first = 0; 1329 } 1330 1331 /// @return true if the popup menu is displayed. 1332 bool pum_visible(void) 1333 { 1334 return pum_is_visible; 1335 } 1336 1337 /// @return true if the popup menu is displayed and drawn on the grid. 1338 bool pum_drawn(void) 1339 { 1340 return pum_visible() && !pum_external; 1341 } 1342 1343 /// Screen was cleared, need to redraw next time 1344 void pum_invalidate(void) 1345 { 1346 pum_invalid = true; 1347 } 1348 1349 void pum_recompose(void) 1350 { 1351 ui_comp_compose_grid(&pum_grid); 1352 } 1353 1354 void pum_ext_select_item(int item, bool insert, bool finish) 1355 { 1356 if (!pum_visible() || item < -1 || item >= pum_size) { 1357 return; 1358 } 1359 pum_want.active = true; 1360 pum_want.item = item; 1361 pum_want.insert = insert; 1362 pum_want.finish = finish; 1363 } 1364 1365 /// Gets the height of the menu. 1366 /// 1367 /// @return the height of the popup menu, the number of entries visible. 1368 /// Only valid when pum_visible() returns true! 1369 int pum_get_height(void) 1370 { 1371 if (pum_external) { 1372 int ui_pum_height = ui_pum_get_height(); 1373 if (ui_pum_height) { 1374 return ui_pum_height; 1375 } 1376 } 1377 return pum_height; 1378 } 1379 1380 /// Add size information about the pum to "dict". 1381 void pum_set_event_info(dict_T *dict) 1382 { 1383 if (!pum_visible()) { 1384 return; 1385 } 1386 double w, h, r, c; 1387 if (!ui_pum_get_pos(&w, &h, &r, &c)) { 1388 w = (double)pum_width; 1389 h = (double)pum_height; 1390 r = (double)pum_row; 1391 c = (double)pum_col; 1392 } 1393 tv_dict_add_float(dict, S_LEN("height"), h); 1394 tv_dict_add_float(dict, S_LEN("width"), w); 1395 tv_dict_add_float(dict, S_LEN("row"), r); 1396 tv_dict_add_float(dict, S_LEN("col"), c); 1397 tv_dict_add_nr(dict, S_LEN("size"), pum_size); 1398 tv_dict_add_bool(dict, S_LEN("scrollbar"), 1399 pum_scrollbar ? kBoolVarTrue : kBoolVarFalse); 1400 } 1401 1402 static void pum_position_at_mouse(int min_width) 1403 { 1404 int min_row = 0; 1405 int min_col = 0; 1406 int max_row = Rows; 1407 int max_col = Columns; 1408 int grid = mouse_grid; 1409 int row = mouse_row; 1410 int col = mouse_col; 1411 pum_win_row_offset = 0; 1412 pum_win_col_offset = 0; 1413 1414 if (ui_has(kUIMultigrid) && grid == 0) { 1415 mouse_find_win_outer(&grid, &row, &col); 1416 } 1417 if (grid > 1) { 1418 win_T *wp = get_win_by_grid_handle(grid); 1419 if (wp != NULL) { 1420 row += wp->w_winrow; 1421 col += wp->w_wincol; 1422 pum_win_row_offset = wp->w_winrow; 1423 pum_win_col_offset = wp->w_wincol; 1424 1425 if (wp->w_view_height > 0 || wp->w_view_width > 0) { 1426 // When the user has requested a different grid size, let the popupmenu extend to the size 1427 // of it. 1428 max_row = MAX(Rows - wp->w_winrow, wp->w_winrow + wp->w_view_height); 1429 max_col = MAX(Columns - wp->w_wincol, wp->w_wincol + wp->w_view_width); 1430 } 1431 } 1432 } 1433 if (pum_grid.handle != 0 && grid == pum_grid.handle) { 1434 // Repositioning the menu by right-clicking on itself 1435 row += pum_row; 1436 col += pum_left_col; 1437 } else { 1438 pum_anchor_grid = grid; 1439 } 1440 1441 if (max_row - row > pum_size || max_row - row > row - min_row) { 1442 // Enough space below the mouse row, 1443 // or there is more space below the mouse row than above. 1444 pum_above = false; 1445 pum_row = row + 1; 1446 if (pum_height > max_row - pum_row) { 1447 pum_height = max_row - pum_row; 1448 } 1449 } else { 1450 // Show above the mouse row, reduce height if it does not fit. 1451 pum_above = true; 1452 pum_row = row - pum_size; 1453 if (pum_row < min_row) { 1454 pum_height += pum_row - min_row; 1455 pum_row = min_row; 1456 } 1457 } 1458 1459 if (pum_rl) { 1460 if (col - min_col + 1 >= pum_base_width 1461 || col - min_col + 1 > min_width) { 1462 // Enough space to show at mouse column. 1463 pum_col = col; 1464 } else { 1465 // Not enough space, left align with window. 1466 pum_col = min_col + MIN(pum_base_width, min_width) - 1; 1467 } 1468 pum_width = pum_col - min_col + 1; 1469 } else { 1470 if (max_col - col >= pum_base_width 1471 || max_col - col > min_width) { 1472 // Enough space to show at mouse column. 1473 pum_col = col; 1474 } else { 1475 // Not enough space, right align with window. 1476 pum_col = max_col - MIN(pum_base_width, min_width); 1477 } 1478 pum_width = max_col - pum_col; 1479 } 1480 1481 pum_width = MIN(pum_width, pum_base_width + 1); 1482 } 1483 1484 /// Select the pum entry at the mouse position. 1485 static void pum_select_mouse_pos(void) 1486 { 1487 int grid = mouse_grid; 1488 int row = mouse_row; 1489 int col = mouse_col; 1490 1491 if (grid == 0) { 1492 mouse_find_win_outer(&grid, &row, &col); 1493 } 1494 1495 if (grid == pum_grid.handle) { 1496 pum_selected = row; 1497 return; 1498 } 1499 1500 if (grid != pum_anchor_grid 1501 || col < pum_left_col - pum_win_col_offset 1502 || col >= pum_right_col - pum_win_col_offset) { 1503 pum_selected = -1; 1504 return; 1505 } 1506 1507 int idx = row - (pum_row - pum_win_row_offset); 1508 1509 if (idx < 0 || idx >= pum_height) { 1510 pum_selected = -1; 1511 } else if (*pum_array[idx].pum_text != NUL) { 1512 pum_selected = idx; 1513 } 1514 } 1515 1516 /// Execute the currently selected popup menu item. 1517 static void pum_execute_menu(vimmenu_T *menu, int mode) 1518 { 1519 int idx = 0; 1520 exarg_T ea; 1521 1522 for (vimmenu_T *mp = menu->children; mp != NULL; mp = mp->next) { 1523 if ((mp->modes & mp->enabled & mode) && idx++ == pum_selected) { 1524 CLEAR_FIELD(ea); 1525 execute_menu(&ea, mp, -1); 1526 break; 1527 } 1528 } 1529 } 1530 1531 /// Open the terminal version of the popup menu and don't return until it is closed. 1532 void pum_show_popupmenu(vimmenu_T *menu) 1533 { 1534 pum_undisplay(true); 1535 pum_size = 0; 1536 int mode = get_menu_mode_flag(); 1537 1538 for (vimmenu_T *mp = menu->children; mp != NULL; mp = mp->next) { 1539 if (menu_is_separator(mp->dname) || (mp->modes & mp->enabled & mode)) { 1540 pum_size++; 1541 } 1542 } 1543 1544 // When there are only Terminal mode menus, using "popup Edit" results in 1545 // pum_size being zero. 1546 if (pum_size <= 0) { 1547 emsg(_(e_menu_only_exists_in_another_mode)); 1548 return; 1549 } 1550 1551 int idx = 0; 1552 pumitem_T *array = (pumitem_T *)xcalloc((size_t)pum_size, sizeof(pumitem_T)); 1553 1554 for (vimmenu_T *mp = menu->children; mp != NULL; mp = mp->next) { 1555 char *s = NULL; 1556 // Make a copy of the text, the menu may be redefined in a callback. 1557 if (menu_is_separator(mp->dname)) { 1558 s = ""; 1559 } else if (mp->modes & mp->enabled & mode) { 1560 s = mp->dname; 1561 } 1562 if (s != NULL) { 1563 s = xstrdup(s); 1564 array[idx++].pum_text = s; 1565 } 1566 } 1567 1568 pum_array = array; 1569 pum_compute_size(); 1570 pum_scrollbar = 0; 1571 pum_height = pum_size; 1572 pum_rl = curwin->w_p_rl; 1573 pum_position_at_mouse(20); 1574 1575 pum_selected = -1; 1576 pum_first = 0; 1577 if (!p_mousemev) { 1578 // Pretend 'mousemoveevent' is set. 1579 ui_call_option_set(STATIC_CSTR_AS_STRING("mousemoveevent"), BOOLEAN_OBJ(true)); 1580 } 1581 1582 while (true) { 1583 pum_is_visible = true; 1584 pum_is_drawn = true; 1585 pum_grid.zindex = kZIndexCmdlinePopupMenu; // show above cmdline area #23275 1586 pum_redraw(); 1587 setcursor_mayforce(curwin, true); 1588 1589 int c = vgetc(); 1590 1591 // Bail out when typing Esc, CTRL-C or some callback or <expr> mapping 1592 // closed the popup menu. 1593 if (c == ESC || c == Ctrl_C || pum_array == NULL) { 1594 break; 1595 } else if (c == CAR || c == NL) { 1596 // enter: select current item, if any, and close 1597 pum_execute_menu(menu, mode); 1598 break; 1599 } else if (c == 'k' || c == K_UP || c == K_MOUSEUP) { 1600 // cursor up: select previous item 1601 while (pum_selected > 0) { 1602 pum_selected--; 1603 if (*array[pum_selected].pum_text != NUL) { 1604 break; 1605 } 1606 } 1607 } else if (c == 'j' || c == K_DOWN || c == K_MOUSEDOWN) { 1608 // cursor down: select next item 1609 while (pum_selected < pum_size - 1) { 1610 pum_selected++; 1611 if (*array[pum_selected].pum_text != NUL) { 1612 break; 1613 } 1614 } 1615 } else if (c == K_RIGHTMOUSE) { 1616 // Right mouse down: reposition the menu. 1617 vungetc(c); 1618 break; 1619 } else if (c == K_LEFTDRAG || c == K_RIGHTDRAG || c == K_MOUSEMOVE) { 1620 // mouse moved: select item in the mouse row 1621 pum_select_mouse_pos(); 1622 } else if (c == K_LEFTMOUSE || c == K_LEFTMOUSE_NM || c == K_RIGHTRELEASE) { 1623 // left mouse click: select clicked item, if any, and close; 1624 // right mouse release: select clicked item, close if any 1625 pum_select_mouse_pos(); 1626 if (pum_selected >= 0) { 1627 pum_execute_menu(menu, mode); 1628 break; 1629 } 1630 if (c == K_LEFTMOUSE || c == K_LEFTMOUSE_NM) { 1631 break; 1632 } 1633 } 1634 } 1635 1636 for (idx = 0; idx < pum_size; idx++) { 1637 xfree(array[idx].pum_text); 1638 } 1639 xfree(array); 1640 pum_undisplay(true); 1641 if (!p_mousemev) { 1642 ui_call_option_set(STATIC_CSTR_AS_STRING("mousemoveevent"), BOOLEAN_OBJ(false)); 1643 } 1644 } 1645 1646 void pum_make_popup(const char *path_name, int use_mouse_pos) 1647 { 1648 if (!use_mouse_pos) { 1649 // Hack: set mouse position at the cursor so that the menu pops up 1650 // around there. 1651 mouse_row = curwin->w_grid.row_offset + curwin->w_wrow; 1652 mouse_col = curwin->w_grid.col_offset 1653 + (curwin->w_p_rl ? curwin->w_view_width - curwin->w_wcol - 1 1654 : curwin->w_wcol); 1655 if (ui_has(kUIMultigrid)) { 1656 mouse_grid = curwin->w_grid.target->handle; 1657 } else if (curwin->w_grid.target != &default_grid) { 1658 mouse_grid = 0; 1659 mouse_row += curwin->w_winrow; 1660 mouse_col += curwin->w_wincol; 1661 } 1662 } 1663 1664 vimmenu_T *menu = menu_find(path_name); 1665 if (menu != NULL) { 1666 pum_show_popupmenu(menu); 1667 } 1668 } 1669 1670 void pum_ui_flush(void) 1671 { 1672 if (ui_has(kUIMultigrid) && pum_is_drawn && !pum_external && pum_grid.handle != 0 1673 && pum_grid.pending_comp_index_update) { 1674 const char *anchor = pum_above ? "SW" : "NW"; 1675 int row_off = pum_above ? -pum_height : 0; 1676 ui_call_win_float_pos(pum_grid.handle, -1, cstr_as_string(anchor), pum_anchor_grid, 1677 pum_row - row_off - pum_win_row_offset, pum_left_col - pum_win_col_offset, 1678 false, pum_grid.zindex, (int)pum_grid.comp_index, pum_grid.comp_row, 1679 pum_grid.comp_col); 1680 pum_grid.pending_comp_index_update = false; 1681 } 1682 }