winfloat.c (13781B)
1 #include <assert.h> 2 #include <stdbool.h> 3 #include <stdlib.h> 4 #include <string.h> 5 6 #include "klib/kvec.h" 7 #include "nvim/api/private/defs.h" 8 #include "nvim/api/private/helpers.h" 9 #include "nvim/api/vim.h" 10 #include "nvim/ascii_defs.h" 11 #include "nvim/autocmd.h" 12 #include "nvim/buffer_defs.h" 13 #include "nvim/drawscreen.h" 14 #include "nvim/errors.h" 15 #include "nvim/globals.h" 16 #include "nvim/grid.h" 17 #include "nvim/grid_defs.h" 18 #include "nvim/macros_defs.h" 19 #include "nvim/memory.h" 20 #include "nvim/message.h" 21 #include "nvim/mouse.h" 22 #include "nvim/move.h" 23 #include "nvim/option.h" 24 #include "nvim/option_defs.h" 25 #include "nvim/option_vars.h" 26 #include "nvim/optionstr.h" 27 #include "nvim/pos_defs.h" 28 #include "nvim/strings.h" 29 #include "nvim/types_defs.h" 30 #include "nvim/ui.h" 31 #include "nvim/ui_defs.h" 32 #include "nvim/vim_defs.h" 33 #include "nvim/window.h" 34 #include "nvim/winfloat.h" 35 36 #include "winfloat.c.generated.h" 37 38 /// Create a new float. 39 /// 40 /// @param wp if NULL, allocate a new window, otherwise turn existing window into a float. 41 /// It must then already belong to the current tabpage! 42 /// @param last make the window the last one in the window list. 43 /// Only used when allocating the autocommand window. 44 /// @param config must already have been validated! 45 win_T *win_new_float(win_T *wp, bool last, WinConfig fconfig, Error *err) 46 { 47 if (wp == NULL) { 48 tabpage_T *tp = NULL; 49 win_T *tp_last = last ? lastwin : lastwin_nofloating(); 50 if (fconfig.window != 0) { 51 assert(!last); 52 win_T *parent_wp = find_window_by_handle(fconfig.window, err); 53 if (!parent_wp) { 54 return NULL; 55 } 56 tp = win_find_tabpage(parent_wp); 57 if (!tp) { 58 return NULL; 59 } 60 tp_last = tp == curtab ? lastwin : tp->tp_lastwin; 61 while (tp_last->w_floating && tp_last->w_prev) { 62 tp_last = tp_last->w_prev; 63 } 64 } 65 wp = win_alloc(tp_last, false); 66 win_init(wp, curwin, 0); 67 if (wp->w_p_wbr != NULL && fconfig.height == 1) { 68 if (wp->w_p_wbr != empty_string_option) { 69 free_string_option(wp->w_p_wbr); 70 } 71 wp->w_p_wbr = empty_string_option; 72 } 73 if (wp->w_p_stl && wp->w_p_stl != empty_string_option) { 74 free_string_option(wp->w_p_stl); 75 wp->w_p_stl = empty_string_option; 76 } 77 } else { 78 assert(!last); 79 assert(!wp->w_floating); 80 if (firstwin == wp && lastwin_nofloating() == wp) { 81 // last non-float 82 api_set_error(err, kErrorTypeException, 83 "Cannot change last window into float"); 84 return NULL; 85 } else if (!win_valid(wp)) { 86 api_set_error(err, kErrorTypeException, 87 "Cannot change window from different tabpage into float"); 88 return NULL; 89 } else if (cmdwin_win != NULL && !cmdwin_win->w_floating) { 90 // cmdwin can't become the only non-float. Check for others. 91 bool other_nonfloat = false; 92 for (win_T *wp2 = firstwin; wp2 != NULL && !wp2->w_floating; wp2 = wp2->w_next) { 93 if (wp2 != wp && wp2 != cmdwin_win) { 94 other_nonfloat = true; 95 break; 96 } 97 } 98 if (!other_nonfloat) { 99 api_set_error(err, kErrorTypeException, "%s", e_cmdwin); 100 return NULL; 101 } 102 } 103 int dir; 104 winframe_remove(wp, &dir, NULL, NULL); 105 XFREE_CLEAR(wp->w_frame); 106 win_comp_pos(); // recompute window positions 107 win_remove(wp, NULL); 108 win_append(lastwin_nofloating(), wp, NULL); 109 } 110 wp->w_floating = true; 111 wp->w_status_height = wp->w_p_stl && *wp->w_p_stl != NUL 112 && (p_ls == 1 || p_ls == 2) ? STATUS_HEIGHT : 0; 113 wp->w_winbar_height = 0; 114 wp->w_hsep_height = 0; 115 wp->w_vsep_width = 0; 116 117 win_config_float(wp, fconfig); 118 redraw_later(wp, UPD_VALID); 119 return wp; 120 } 121 122 void win_set_minimal_style(win_T *wp) 123 { 124 wp->w_p_nu = false; 125 wp->w_p_rnu = false; 126 wp->w_p_cul = false; 127 wp->w_p_cuc = false; 128 wp->w_p_spell = false; 129 wp->w_p_list = false; 130 131 // Hide EOB region: use " " fillchar and cleared highlighting 132 if (wp->w_p_fcs_chars.eob != ' ') { 133 char *old = wp->w_p_fcs; 134 wp->w_p_fcs = ((*old == NUL) 135 ? xstrdup("eob: ") 136 : concat_str(old, ",eob: ")); 137 free_string_option(old); 138 } 139 140 // TODO(bfredl): this could use a highlight namespace directly, 141 // and avoid peculiarities around window options 142 char *old = wp->w_p_winhl; 143 wp->w_p_winhl = ((*old == NUL) 144 ? xstrdup("EndOfBuffer:") 145 : concat_str(old, ",EndOfBuffer:")); 146 free_string_option(old); 147 parse_winhl_opt(NULL, wp); 148 149 // signcolumn: use 'auto' 150 if (wp->w_p_scl[0] != 'a' || strlen(wp->w_p_scl) >= 8) { 151 free_string_option(wp->w_p_scl); 152 wp->w_p_scl = xstrdup("auto"); 153 } 154 155 // foldcolumn: use '0' 156 if (wp->w_p_fdc[0] != '0') { 157 free_string_option(wp->w_p_fdc); 158 wp->w_p_fdc = xstrdup("0"); 159 } 160 161 // colorcolumn: cleared 162 if (wp->w_p_cc != NULL && *wp->w_p_cc != NUL) { 163 free_string_option(wp->w_p_cc); 164 wp->w_p_cc = xstrdup(""); 165 } 166 167 // statuscolumn: cleared 168 if (wp->w_p_stc != NULL && *wp->w_p_stc != NUL) { 169 free_string_option(wp->w_p_stc); 170 wp->w_p_stc = empty_string_option; 171 } 172 173 // statusline: cleared (for floating windows) 174 if (wp->w_floating && wp->w_p_stl != NULL && *wp->w_p_stl != NUL) { 175 free_string_option(wp->w_p_stl); 176 wp->w_p_stl = empty_string_option; 177 if (wp->w_status_height > 0) { 178 win_config_float(wp, wp->w_config); 179 } 180 } 181 } 182 183 int win_border_height(win_T *wp) 184 { 185 return wp->w_border_adj[0] + wp->w_border_adj[2]; 186 } 187 188 int win_border_width(win_T *wp) 189 { 190 return wp->w_border_adj[1] + wp->w_border_adj[3]; 191 } 192 193 void win_config_float(win_T *wp, WinConfig fconfig) 194 { 195 // Process statusline changes before applying new height from config 196 bool show_stl = *wp->w_p_stl != NUL && (p_ls == 1 || p_ls == 2); 197 if (wp->w_status_height && !show_stl) { 198 win_remove_status_line(wp, false); 199 } else if (wp->w_status_height == 0 && show_stl) { 200 wp->w_status_height = STATUS_HEIGHT; 201 } 202 203 wp->w_width = MAX(fconfig.width, 1); 204 wp->w_height = MAX(fconfig.height, 1); 205 206 if (fconfig.relative == kFloatRelativeCursor) { 207 fconfig.relative = kFloatRelativeWindow; 208 fconfig.row += curwin->w_wrow; 209 fconfig.col += curwin->w_wcol; 210 fconfig.window = curwin->handle; 211 } else if (fconfig.relative == kFloatRelativeMouse) { 212 int row = mouse_row; 213 int col = mouse_col; 214 int grid = mouse_grid; 215 win_T *mouse_win = mouse_find_win_inner(&grid, &row, &col); 216 if (mouse_win != NULL) { 217 fconfig.relative = kFloatRelativeWindow; 218 fconfig.row += row; 219 fconfig.col += col; 220 fconfig.window = mouse_win->handle; 221 } 222 } 223 224 bool change_external = fconfig.external != wp->w_config.external; 225 bool change_border = (fconfig.border != wp->w_config.border 226 || memcmp(fconfig.border_hl_ids, 227 wp->w_config.border_hl_ids, 228 sizeof fconfig.border_hl_ids) != 0); 229 230 merge_win_config(&wp->w_config, fconfig); 231 232 bool has_border = wp->w_floating && wp->w_config.border; 233 for (int i = 0; i < 4; i++) { 234 int new_adj = has_border && wp->w_config.border_chars[2 * i + 1][0]; 235 if (new_adj != wp->w_border_adj[i]) { 236 change_border = true; 237 wp->w_border_adj[i] = new_adj; 238 } 239 } 240 241 if (!ui_has(kUIMultigrid)) { 242 int above_ch = wp->w_config.zindex < kZIndexMessages ? (int)p_ch : 0; 243 wp->w_height = MIN(wp->w_height, Rows - win_border_height(wp) - above_ch); 244 wp->w_width = MIN(wp->w_width, Columns - win_border_width(wp)); 245 } 246 247 win_set_inner_size(wp, true); 248 set_must_redraw(UPD_VALID); 249 wp->w_redr_status = wp->w_status_height; 250 wp->w_pos_changed = true; 251 if (change_external || change_border) { 252 wp->w_hl_needs_update = true; 253 redraw_later(wp, UPD_NOT_VALID); 254 } 255 256 // compute initial position 257 if (wp->w_config.relative == kFloatRelativeWindow) { 258 int row = (int)wp->w_config.row; 259 int col = (int)wp->w_config.col; 260 Error dummy = ERROR_INIT; 261 win_T *parent = find_window_by_handle(wp->w_config.window, &dummy); 262 if (parent) { 263 row += parent->w_winrow; 264 col += parent->w_wincol; 265 grid_adjust(&parent->w_grid, &row, &col); 266 if (wp->w_config.bufpos.lnum >= 0) { 267 pos_T pos = { MIN(wp->w_config.bufpos.lnum + 1, parent->w_buffer->b_ml.ml_line_count), 268 wp->w_config.bufpos.col, 0 }; 269 int trow, tcol, tcolc, tcole; 270 textpos2screenpos(parent, &pos, &trow, &tcol, &tcolc, &tcole, true); 271 row += trow - 1; 272 col += tcol - 1; 273 } 274 } 275 api_clear_error(&dummy); 276 wp->w_winrow = row; 277 wp->w_wincol = col; 278 } else { 279 wp->w_winrow = (int)fconfig.row; 280 wp->w_wincol = (int)fconfig.col; 281 } 282 283 // changing border style while keeping border only requires redrawing border 284 if (fconfig.border) { 285 wp->w_redr_border = true; 286 redraw_later(wp, UPD_VALID); 287 } 288 } 289 290 static int float_zindex_cmp(const void *a, const void *b) 291 { 292 int za = (*(win_T **)a)->w_config.zindex; 293 int zb = (*(win_T **)b)->w_config.zindex; 294 return za == zb ? 0 : za < zb ? 1 : -1; 295 } 296 297 void win_float_remove(bool bang, int count) 298 { 299 kvec_t(win_T *) float_win_arr = KV_INITIAL_VALUE; 300 for (win_T *wp = lastwin; wp && wp->w_floating; wp = wp->w_prev) { 301 kv_push(float_win_arr, wp); 302 } 303 if (float_win_arr.size > 0) { 304 qsort(float_win_arr.items, float_win_arr.size, sizeof(win_T *), float_zindex_cmp); 305 } 306 for (size_t i = 0; i < float_win_arr.size; i++) { 307 win_T *wp = float_win_arr.items[i]; 308 if (win_valid(wp) && win_close(wp, false, false) == FAIL) { 309 break; 310 } 311 if (!bang) { 312 count--; 313 if (count == 0) { 314 break; 315 } 316 } 317 } 318 kv_destroy(float_win_arr); 319 } 320 321 void win_check_anchored_floats(win_T *win) 322 { 323 for (win_T *wp = lastwin; wp && wp->w_floating; wp = wp->w_prev) { 324 // float might be anchored to moved window 325 if (wp->w_config.relative == kFloatRelativeWindow 326 && wp->w_config.window == win->handle) { 327 wp->w_pos_changed = true; 328 } 329 } 330 } 331 332 void win_float_update_statusline(void) 333 { 334 for (win_T *wp = lastwin; wp && wp->w_floating; wp = wp->w_prev) { 335 bool has_status = wp->w_status_height > 0; 336 bool should_show = *wp->w_p_stl != NUL && (p_ls == 1 || p_ls == 2); 337 if (should_show != has_status) { 338 win_config_float(wp, wp->w_config); 339 } 340 } 341 } 342 343 void win_float_anchor_laststatus(void) 344 { 345 FOR_ALL_WINDOWS_IN_TAB(win, curtab) { 346 if (win->w_config.relative == kFloatRelativeLaststatus) { 347 win->w_pos_changed = true; 348 } 349 } 350 } 351 352 void win_reconfig_floats(void) 353 { 354 for (win_T *wp = lastwin; wp && wp->w_floating; wp = wp->w_prev) { 355 win_config_float(wp, wp->w_config); 356 } 357 } 358 359 /// Return true if "win" is floating window in the current tab page. 360 /// 361 /// @param win window to check 362 bool win_float_valid(const win_T *win) 363 FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT 364 { 365 if (win == NULL) { 366 return false; 367 } 368 369 FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { 370 if (wp == win) { 371 return wp->w_floating; 372 } 373 } 374 return false; 375 } 376 377 win_T *win_float_find_preview(void) 378 { 379 for (win_T *wp = lastwin; wp && wp->w_floating; wp = wp->w_prev) { 380 if (wp->w_float_is_info) { 381 return wp; 382 } 383 } 384 return NULL; 385 } 386 387 /// Select an alternative window to `win` (assumed floating) in tabpage `tp`. 388 /// 389 /// Useful for finding a window to switch to if `win` is the current window, but is then closed or 390 /// moved to a different tabpage. 391 /// 392 /// @param tp `win`'s original tabpage, or NULL for current. 393 win_T *win_float_find_altwin(const win_T *win, const tabpage_T *tp) 394 FUNC_ATTR_NONNULL_ARG(1) 395 { 396 win_T *wp = prevwin; 397 if (tp == NULL) { 398 return (win_valid(wp) && wp != win && wp->w_config.focusable 399 && !wp->w_config.hide) ? wp : firstwin; 400 } 401 402 assert(tp != curtab); 403 wp = tabpage_win_valid(tp, tp->tp_prevwin) ? tp->tp_prevwin : tp->tp_firstwin; 404 return (wp->w_config.focusable && !wp->w_config.hide) ? wp : tp->tp_firstwin; 405 } 406 407 /// Inline helper function for handling errors and cleanup in win_float_create_preview. 408 static inline win_T *handle_error_and_cleanup(win_T *wp, Error *err) 409 { 410 if (ERROR_SET(err)) { 411 emsg(err->msg); 412 api_clear_error(err); 413 } 414 if (wp) { 415 win_remove(wp, NULL); 416 win_free(wp, NULL); 417 } 418 unblock_autocmds(); 419 return NULL; 420 } 421 422 /// create a floating preview window. 423 /// 424 /// @param[in] bool enter floating window. 425 /// @param[in] bool create a new buffer for window. 426 /// 427 /// @return win_T 428 win_T *win_float_create_preview(bool enter, bool new_buf) 429 { 430 WinConfig config = WIN_CONFIG_INIT; 431 config.col = curwin->w_wcol; 432 config.row = curwin->w_wrow; 433 config.relative = kFloatRelativeEditor; 434 config.focusable = false; 435 config.mouse = false; 436 config.anchor = 0; // NW 437 config.noautocmd = true; 438 config.hide = true; 439 config.style = kWinStyleMinimal; 440 Error err = ERROR_INIT; 441 442 block_autocmds(); 443 win_T *wp = win_new_float(NULL, false, config, &err); 444 if (!wp) { 445 return handle_error_and_cleanup(wp, &err); 446 } 447 448 if (new_buf) { 449 Buffer b = nvim_create_buf(false, true, &err); 450 if (!b) { 451 return handle_error_and_cleanup(wp, &err); 452 } 453 buf_T *buf = find_buffer_by_handle(b, &err); 454 if (!buf) { 455 return handle_error_and_cleanup(wp, &err); 456 } 457 buf->b_p_bl = false; // unlist 458 set_option_direct_for(kOptBufhidden, STATIC_CSTR_AS_OPTVAL("wipe"), OPT_LOCAL, 0, 459 kOptScopeBuf, buf); 460 win_set_buf(wp, buf, &err); 461 if (ERROR_SET(&err)) { 462 return handle_error_and_cleanup(wp, &err); 463 } 464 } 465 unblock_autocmds(); 466 wp->w_p_diff = false; 467 wp->w_float_is_info = true; 468 wp->w_p_wrap = true; // 'wrap' is default on 469 wp->w_p_so = 0; // 'scrolloff' zero 470 if (enter) { 471 win_enter(wp, false); 472 } 473 return wp; 474 }