From: Yasuhiro Matsumoto Date: Sun, 10 May 2026 18:53:57 +0000 (+0000) Subject: patch 9.2.0469: popup: textprop-anchored popups bleed past host window edges X-Git-Tag: v9.2.0469^0 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=e3d9929109dadef35624ea7a68a6dbf0ca013f46;p=thirdparty%2Fvim.git patch 9.2.0469: popup: textprop-anchored popups bleed past host window edges Problem: A popup anchored to a text property in a split window is positioned relative to the screen and may extend into adjacent splits or off-screen regions. There is no way to confine the popup to the window that contains the textprop. Solution: Add the "clipwindow" popup option to allow clipping the text property popup to the host window (Yasuhiro Matsumoto). Adds a "clipwindow" boolean option to popup_create()/popup_setoptions(). When set on a textprop-anchored popup, the popup's drawn extent is confined to its host (textprop) window's content rectangle so the popup no longer bleeds across a horizontal split's statusline (top/bottom) or a vsplit's separator (right) into another window. The popup keeps its full logical size and position; only the rows or columns that fall outside the host window's content area are skipped during drawing, so a popup that scrolls toward the host's edge looks visually "cut off" without its borders being relocated. popup_getoptions and popup_getpos continue to report the unclipped geometry. Implementation: - w_popup_topoff / w_popup_bottomoff record how many rows of the popup fall outside the host on each side. popup_adjust_position() computes them from the host rectangle after the logical layout is finalised, and update_popups() and the popup-mask builder subtract them when emitting cells/borders/scrollbar and when marking popup-owned cells. win_update() is bracketed by transient w_height/w_topline/w_winrow adjustments so the buffer's drawn content matches the visible row range. - w_popup_rightclip is the horizontal counterpart for the host's right edge: the right border, padding and content columns past the host are not drawn. win_update() is bracketed by a transient w_width reduction so the buffer text is not written past the host's right edge either. - When the textprop scrolls just above the host window's top, the popup is kept visible by extending the prop search above topline (new helper find_prop_in_lines) and synthesising a negative screen_row so the top-clip path can roll the popup off the top. When the textprop has scrolled far enough that even the bottom border would overlap the host edge -- or when the popup would overflow the host's left edge at all -- the popup is hidden, and unhidden again once it comes back within range. - The "reduce-height" / "clamp winrow to 0" fallbacks in popup_adjust_position are bypassed for host-clipped popups so the popup keeps its natural anchored position instead of being snapped to the screen edge. Left-edge partial clipping is intentionally not supported: it would require shrinking the buffer width during win_update, which reflows wrapped lines and corrupts the displayed content; the popup is hidden instead. closes: #20166 Signed-off-by: Yasuhiro Matsumoto Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/popup.txt b/runtime/doc/popup.txt index 250127b9b6..2afa93a756 100644 --- a/runtime/doc/popup.txt +++ b/runtime/doc/popup.txt @@ -1,4 +1,4 @@ -*popup.txt* For Vim version 9.2. Last change: 2026 May 01 +*popup.txt* For Vim version 9.2. Last change: 2026 May 10 VIM REFERENCE MANUAL by Bram Moolenaar @@ -712,6 +712,15 @@ The second argument of |popup_create()| is a dictionary with options: when "textprop" is present. textpropid Used to identify the text property when "textprop" is present. Use zero to reset. + clipwindow Only used when "textprop" is set. When TRUE the popup + is kept within the window containing the text + property: if the text property scrolls past that + window's top, bottom, left or right edge, the popup + is clipped at that edge instead of being drawn + outside it. Once the text property has scrolled out + of the window the popup is hidden. + Default FALSE. + See |popup-clipwindow|. fixed When FALSE (the default), and: - "pos" is "botleft" or "topleft", and - the popup would be truncated at the right edge of @@ -949,6 +958,31 @@ If the window for which the popup was defined is closed, the popup is closed. If the popup cannot fit in the desired position, it may show at a nearby position. + +CLIP TEXTPROP POPUP TO HOST WINDOW *popup-clipwindow* + +When the popup is anchored to a text property in a split window, the popup is +by default drawn relative to the whole screen and may extend past the edges of +the window that contains the text property (the "host window"). Setting +"clipwindow" to TRUE keeps the popup within window's content area: +parts of the popup that fall outside the window are clipped, and the popup is +hidden once the text property has scrolled entirely past one of the edges. + +Example: a tall popup anchored above the cursor that should never spill into +the window below the split: > + call popup_create(body, #{ + \ textprop: 'marker', + \ textpropid: id, + \ pos: 'topleft', + \ line: -1, col: 0, + \ posinvert: v:false, + \ clipwindow: v:true, + \ }) +< +With "posinvert" left at its default (TRUE) the popup may be flipped to the +opposite side of the text property when there is no room; set it to FALSE to +keep the requested side and rely on "clipwindow" to clip the overflow. + Some hints: - To avoid collision with other plugins the text property type name has to be unique. You can also use the "bufnr" item to make it local to a buffer. diff --git a/runtime/doc/tags b/runtime/doc/tags index 3168235dad..8eef8cf28b 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -9791,6 +9791,7 @@ popt-option print.txt /*popt-option* popup popup.txt /*popup* popup-buffer popup.txt /*popup-buffer* popup-callback popup.txt /*popup-callback* +popup-clipwindow popup.txt /*popup-clipwindow* popup-close popup.txt /*popup-close* popup-examples popup.txt /*popup-examples* popup-filter popup.txt /*popup-filter* diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index a1b9afce71..f4cdc702b7 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -1,4 +1,4 @@ -*version9.txt* For Vim version 9.2. Last change: 2026 May 05 +*version9.txt* For Vim version 9.2. Last change: 2026 May 10 VIM REFERENCE MANUAL by Bram Moolenaar @@ -52588,6 +52588,7 @@ Popups ~ - 'previewpopup' supports the same values as 'completepopup' (except for "align"). - Support "opacity" setting for 'completepopup' option. +- Support for clipping textproperty popups |popup-clipwindow|. Diff mode ~ --------- diff --git a/src/popupwin.c b/src/popupwin.c index dcbe0e854c..5bd156367e 100644 --- a/src/popupwin.c +++ b/src/popupwin.c @@ -70,6 +70,10 @@ typedef struct { int leftcol; int leftoff; int has_scrollbar; + int topoff; + int bottomoff; + int leftclip; + int rightclip; } popup_layout_T; static poppos_entry_T poppos_entries[] = { @@ -838,6 +842,15 @@ apply_general_options(win_T *wp, dict_T *dict) wp->w_popup_flags &= ~POPF_POSINVERT; } + nr = dict_get_bool(dict, "clipwindow", -1); + if (nr != -1) + { + if (nr) + wp->w_popup_flags |= POPF_CLIPWINDOW; + else + wp->w_popup_flags &= ~POPF_CLIPWINDOW; + } + nr = dict_get_bool(dict, "resize", -1); if (nr != -1) { @@ -1320,6 +1333,320 @@ popup_extra_width(win_T *wp) + wp->w_has_scrollbar; } +/* + * Return the host window used to clip popup "wp" when POPF_CLIPWINDOW is set, + * or NULL when no clipping should be applied (option off, or the host window + * is no longer valid). The textprop window is used as the host; popups not + * anchored to a textprop are not clipped. + */ + static win_T * +popup_get_clipwin(win_T *wp) +{ + if (!(wp->w_popup_flags & POPF_CLIPWINDOW)) + return NULL; + if (win_valid(wp->w_popup_prop_win)) + return wp->w_popup_prop_win; + return NULL; +} + +// Per-popup clip geometry derived from w_popup_{top,bottom}off and +// w_popup_{left,right}clip. Filled by popup_compute_clip(). +// +// *_extra : original border+padding at each edge. +// clip_*_content : how many *content* rows/cols are clipped at each edge +// (border/padding is consumed first; the rest comes off +// w_height/w_width). >= 0. +// eff_*_extra : 0 when that edge is clipped (border+padding gone), +// otherwise the original *_extra. +// eff_border[], +// eff_padding[] : per-edge border/padding sizes (indexed [top,right,bot,left] +// matching wp->w_popup_border / wp->w_popup_padding). At a +// clipped edge they collapse to 0; elsewhere they keep the +// original size. Drawing code can replace +// `wp->w_popup_border[N] > 0 && wp->w_popup_*clip == 0` +// with a single `cl.eff_border[N] > 0` test. +// eff_height : drawn extent = eff_top_extra + visible content + eff_bot_extra. +// eff_width : drawn extent = eff_left_extra + visible content + eff_right_extra +// (does NOT include w_leftcol or scrollbar; see callers). +typedef struct { + int top_extra; + int bot_extra; + int left_extra; + int right_extra; + + int clip_top_content; + int clip_bot_content; + int clip_left_content; + int clip_right_content; + + int eff_top_extra; + int eff_bot_extra; + int eff_left_extra; + int eff_right_extra; + + int eff_border[4]; + int eff_padding[4]; + + int eff_height; + int eff_width; +} popup_clip_T; + + static void +popup_compute_clip(win_T *wp, popup_clip_T *cl) +{ + int h, w; + + cl->top_extra = popup_top_extra(wp); + cl->bot_extra = wp->w_popup_padding[2] + wp->w_popup_border[2]; + cl->left_extra = wp->w_popup_border[3] + wp->w_popup_padding[3]; + cl->right_extra = wp->w_popup_border[1] + wp->w_popup_padding[1]; + + cl->clip_top_content = wp->w_popup_topoff - cl->top_extra; + if (cl->clip_top_content < 0) + cl->clip_top_content = 0; + cl->clip_bot_content = wp->w_popup_bottomoff - cl->bot_extra; + if (cl->clip_bot_content < 0) + cl->clip_bot_content = 0; + cl->clip_left_content = wp->w_popup_leftclip - cl->left_extra; + if (cl->clip_left_content < 0) + cl->clip_left_content = 0; + cl->clip_right_content = wp->w_popup_rightclip - cl->right_extra; + if (cl->clip_right_content < 0) + cl->clip_right_content = 0; + + cl->eff_top_extra = wp->w_popup_topoff > 0 ? 0 : cl->top_extra; + cl->eff_bot_extra = wp->w_popup_bottomoff > 0 ? 0 : cl->bot_extra; + cl->eff_left_extra = wp->w_popup_leftclip > 0 ? 0 : cl->left_extra; + cl->eff_right_extra = wp->w_popup_rightclip > 0 ? 0 : cl->right_extra; + + cl->eff_border[0] = wp->w_popup_topoff > 0 ? 0 : wp->w_popup_border[0]; + cl->eff_border[1] = wp->w_popup_rightclip > 0 ? 0 : wp->w_popup_border[1]; + cl->eff_border[2] = wp->w_popup_bottomoff > 0 ? 0 : wp->w_popup_border[2]; + cl->eff_border[3] = wp->w_popup_leftclip > 0 ? 0 : wp->w_popup_border[3]; + + cl->eff_padding[0] = wp->w_popup_topoff > 0 ? 0 : wp->w_popup_padding[0]; + cl->eff_padding[1] = wp->w_popup_rightclip > 0 ? 0 : wp->w_popup_padding[1]; + cl->eff_padding[2] = wp->w_popup_bottomoff > 0 ? 0 : wp->w_popup_padding[2]; + cl->eff_padding[3] = wp->w_popup_leftclip > 0 ? 0 : wp->w_popup_padding[3]; + + h = wp->w_height - cl->clip_top_content - cl->clip_bot_content; + if (h < 0) + h = 0; + cl->eff_height = cl->eff_top_extra + h + cl->eff_bot_extra; + + w = wp->w_width - cl->clip_left_content - cl->clip_right_content; + if (w < 0) + w = 0; + cl->eff_width = cl->eff_left_extra + w + cl->eff_right_extra; +} + +// Snapshot of the popup window geometry that update_popups() temporarily +// mutates so that win_update() draws within the host-window clip rectangle. +// Saved before the clip is applied, restored after win_update() returns so +// callers continue to see the popup's logical geometry. +// Field names omit the "w_" prefix to avoid clashing with struct-field +// macros like w_p_wrap (= w_onebuf_opt.wo_wrap). +typedef struct { + int height; + int width; + int winrow; + int wincol; + int leftcol; + int p_wrap; + linenr_T topline; +} popup_geom_save_T; + + static void +popup_geom_save(win_T *wp, popup_geom_save_T *sv) +{ + sv->height = wp->w_height; + sv->width = wp->w_width; + sv->winrow = wp->w_winrow; + sv->wincol = wp->w_wincol; + sv->leftcol = wp->w_leftcol; + sv->p_wrap = wp->w_p_wrap; + sv->topline = wp->w_topline; +} + + static void +popup_geom_restore(win_T *wp, popup_geom_save_T *sv) +{ + wp->w_p_wrap = sv->p_wrap; + wp->w_leftcol = sv->leftcol; + wp->w_wincol = sv->wincol; + wp->w_winrow = sv->winrow; + wp->w_topline = sv->topline; + wp->w_width = sv->width; + wp->w_height = sv->height; +} + +/* + * Compute a screen row for a textprop that has scrolled above the host + * window's top. textpos2screenpos() cannot return a row above topline, so we + * probe at topline to fill the screen_{scol,ccol,ecol} column mapping, then + * extrapolate a (possibly-negative) row by counting how many buffer lines lie + * between the prop and topline. The popup_topoff clip path then turns the + * negative row into a top-clip animation as the prop rolls off the top edge. + */ + static void +popup_screenpos_above_top( + win_T *prop_win, + pos_T *pos, + linenr_T prop_lnum, + int *screen_row, + int *screen_scol, + int *screen_ccol, + int *screen_ecol) +{ + pos_T probe = *pos; + + probe.lnum = prop_win->w_topline; + textpos2screenpos(prop_win, &probe, + screen_row, screen_scol, screen_ccol, screen_ecol); + *screen_row = prop_win->w_winrow + 1 + - (int)(prop_win->w_topline - prop_lnum); +} + +/* + * Hide popup "wp" because its anchoring textprop is no longer reachable. + * Marks the popup as POPF_HIDDEN (no-op when already hidden) and schedules a + * redraw of the host window so any leftover decorations are cleared. + */ + static void +popup_hide_for_textprop(win_T *wp) +{ + if ((wp->w_popup_flags & POPF_HIDDEN) != 0) + return; + wp->w_popup_flags |= POPF_HIDDEN; + if (win_valid(wp->w_popup_prop_win)) + redraw_win_later(wp->w_popup_prop_win, UPD_SOME_VALID); +} + +/* + * For "clipwindow" popups: search the lines above prop_win->w_topline for the + * popup's anchoring textprop and report whether one was found. When + * "max_reach" is > 0, only the last "max_reach" lines before topline are + * scanned; pass 0 to scan all lines from line 1. Returns false when the + * popup is not "clipwindow", topline is already at line 1, or no prop matches. + */ + static bool +popup_find_prop_above_top( + win_T *wp, + win_T *prop_win, + int max_reach, + textprop_T *prop, + linenr_T *found_lnum) +{ + linenr_T first; + + if (!(wp->w_popup_flags & POPF_CLIPWINDOW) || prop_win->w_topline <= 1) + return false; + + first = max_reach > 0 ? prop_win->w_topline - max_reach : 1; + if (first < 1) + first = 1; + return find_prop_in_lines(prop_win, + wp->w_popup_prop_type, wp->w_popup_prop_id, + prop, found_lnum, first, prop_win->w_topline - 1); +} + +/* + * Compute and assign w_popup_topoff/bottomoff/leftclip/rightclip from the + * host (textprop) window's content rectangle when POPF_CLIPWINDOW is set. + * The popup's logical geometry (w_winrow, w_height, w_width) is preserved; + * only the *off/clip fields record how much of each edge falls outside. + * Returns true when the popup has scrolled completely past one of the host + * edges, in which case the caller must hide it. + */ + static bool +popup_compute_clipwindow_offsets(win_T *wp) +{ + win_T *cw = popup_get_clipwin(wp); + int extra_h, extra_w; + int popup_top, popup_bottom, popup_left, popup_right; + int total_h, total_w; + + if (cw == NULL) + return false; + + extra_h = popup_top_extra(wp) + + wp->w_popup_padding[2] + wp->w_popup_border[2]; + extra_w = popup_extra_width(wp); + + popup_top = wp->w_winrow; + popup_bottom = wp->w_winrow + wp->w_height + extra_h; + popup_left = wp->w_wincol; + popup_right = wp->w_wincol + wp->w_width + extra_w; + total_h = wp->w_height + extra_h; + total_w = wp->w_width + extra_w; + + if (popup_top < cw->w_winrow) + wp->w_popup_topoff = cw->w_winrow - popup_top; + if (popup_bottom > cw->w_winrow + cw->w_height) + wp->w_popup_bottomoff = popup_bottom - (cw->w_winrow + cw->w_height); + if (popup_left < cw->w_wincol) + wp->w_popup_leftclip = cw->w_wincol - popup_left; + if (popup_right > cw->w_wincol + cw->w_width) + wp->w_popup_rightclip = popup_right - (cw->w_wincol + cw->w_width); + + return wp->w_popup_topoff >= total_h + || wp->w_popup_bottomoff >= total_h + || wp->w_popup_leftclip >= total_w + || wp->w_popup_rightclip >= total_w; +} + +/* + * Mutate "wp"'s window geometry so win_update() draws only the rows/columns + * that fit within the host-window clip rectangle for "clipwindow" popups. + * The caller must save the original geometry with popup_geom_save() before + * this call and restore it with popup_geom_restore() after win_update(). + * + * Vertical clip: shrink w_height by the clipped content rows; advance + * w_topline and w_winrow when rows are cut off the top so the first visible + * content row lands on the host's top edge. + * + * Horizontal clip: when the right side is clipped, just shrink w_width. + * When the left side is clipped, advance w_leftcol so the hidden buffer + * columns scroll off and shift w_wincol so the first visible column lands on + * the host's left edge. Disable wrap so the transient w_width reduction does + * not reflow wrapped lines: the popup's logical width is unchanged, we just + * want to truncate cells that fall outside the host at draw time. + */ + static void +popup_apply_winupdate_clip(win_T *wp, popup_clip_T *cl) +{ + if (wp->w_popup_topoff > 0 || wp->w_popup_bottomoff > 0) + { + wp->w_height -= cl->clip_top_content + cl->clip_bot_content; + if (wp->w_height < 0) + wp->w_height = 0; + if (cl->clip_top_content > 0) + { + wp->w_topline += cl->clip_top_content; + wp->w_winrow += cl->clip_top_content; + } + } + if (wp->w_popup_leftclip > 0 || wp->w_popup_rightclip > 0) + { + if (cl->clip_left_content > 0 || cl->clip_right_content > 0) + wp->w_p_wrap = FALSE; + if (cl->clip_right_content > 0) + { + wp->w_width -= cl->clip_right_content; + if (wp->w_width < 0) + wp->w_width = 0; + } + if (cl->clip_left_content > 0) + { + wp->w_leftcol += cl->clip_left_content; + wp->w_wincol += cl->clip_left_content; + wp->w_width -= cl->clip_left_content; + if (wp->w_width < 0) + wp->w_width = 0; + } + } +} + /* * Adjust the position and size of the popup to fit on the screen. */ @@ -1361,6 +1688,10 @@ popup_adjust_position(win_T *wp) wp->w_leftcol = 0; wp->w_popup_leftoff = 0; wp->w_popup_rightoff = 0; + wp->w_popup_topoff = 0; + wp->w_popup_bottomoff = 0; + wp->w_popup_leftclip = 0; + wp->w_popup_rightclip = 0; // May need to update the "cursorline" highlighting, which may also change // "topline" @@ -1378,20 +1709,24 @@ popup_adjust_position(win_T *wp) int screen_ccol; int screen_ecol; - // Popup window is positioned relative to a text property. + // Popup window is positioned relative to a text property. With + // "clipwindow", keep the popup visible while the textprop has just + // scrolled above the host's top: extrapolate a negative screen_row + // from a prop above topline so the top-clip path can roll the popup + // off the top edge. Unhiding is done in check_popup_unhidden(). + bool prop_above_top = false; if (!find_visible_prop(prop_win, wp->w_popup_prop_type, wp->w_popup_prop_id, &prop, &prop_lnum)) { - // Text property is no longer visible, hide the popup. - // Unhiding the popup is done in check_popup_unhidden(). - if ((wp->w_popup_flags & POPF_HIDDEN) == 0) + if (popup_find_prop_above_top(wp, prop_win, 0, + &prop, &prop_lnum)) + prop_above_top = true; + else { - wp->w_popup_flags |= POPF_HIDDEN; - if (win_valid(wp->w_popup_prop_win)) - redraw_win_later(wp->w_popup_prop_win, UPD_SOME_VALID); + popup_hide_for_textprop(wp); + return; } - return; } // Compute the desired position from the position of the text @@ -1401,7 +1736,11 @@ popup_adjust_position(win_T *wp) if (wp->w_popup_pos == POPPOS_TOPLEFT || wp->w_popup_pos == POPPOS_BOTLEFT) pos.col += prop.tp_len - 1; - textpos2screenpos(prop_win, &pos, &screen_row, + if (prop_above_top) + popup_screenpos_above_top(prop_win, &pos, prop_lnum, &screen_row, + &screen_scol, &screen_ccol, &screen_ecol); + else + textpos2screenpos(prop_win, &pos, &screen_row, &screen_scol, &screen_ccol, &screen_ecol); if (screen_scol == 0) @@ -1771,8 +2110,11 @@ popup_adjust_position(win_T *wp) else if (wp->w_popup_pos == POPPOS_BOTRIGHT || wp->w_popup_pos == POPPOS_BOTLEFT) { - if ((wp->w_height + extra_height) <= wantline) - // bottom aligned: may move down + if ((wp->w_height + extra_height) <= wantline + || (wp->w_popup_flags & POPF_CLIPWINDOW)) + // bottom aligned: may move down. With "clipwindow" the popup + // keeps its natural position even if it overflows the screen, + // because the clip logic handles the overflow. wp->w_winrow = wantline - (wp->w_height + extra_height); else if (wantline * 2 >= Rows || !(wp->w_popup_flags & POPF_POSINVERT)) { @@ -1838,7 +2180,7 @@ popup_adjust_position(win_T *wp) // make sure w_winrow is valid if (wp->w_winrow >= Rows) wp->w_winrow = Rows - 1; - else if (wp->w_winrow < 0) + else if (wp->w_winrow < 0 && !(wp->w_popup_flags & POPF_CLIPWINDOW)) wp->w_winrow = 0; if (wp->w_wincol + wp->w_width + extra_width @@ -1863,25 +2205,48 @@ popup_adjust_position(win_T *wp) // Same for the bottom edge: shift up so the border/padding/shadow stays // on screen, and clip the height if the popup is taller than the screen. - if (wp->w_winrow + wp->w_height + extra_height > Rows) - wp->w_winrow = Rows - wp->w_height - extra_height; - if (wp->w_winrow < 0) - wp->w_winrow = 0; - if (wp->w_winrow + wp->w_height + extra_height > Rows) - { - int avail = Rows - wp->w_winrow - extra_height; - wp->w_height = avail > 0 ? avail : 0; + // For "clipwindow" popups the host-window clip below handles overflow, so + // skip these screen-edge clamps -- otherwise a synthesised negative + // w_winrow (popup partially above the host's top edge) would be snapped + // back to 0 and defeat the top-clip animation. + if (!(wp->w_popup_flags & POPF_CLIPWINDOW)) + { + if (wp->w_winrow + wp->w_height + extra_height > Rows) + wp->w_winrow = Rows - wp->w_height - extra_height; + if (wp->w_winrow < 0) + wp->w_winrow = 0; + if (wp->w_winrow + wp->w_height + extra_height > Rows) + { + int avail = Rows - wp->w_winrow - extra_height; + wp->w_height = avail > 0 ? avail : 0; + } } if (wp->w_height != org_layout.height) win_comp_scroll(wp); + // Confine the popup to its host window for "clipwindow". The popup's + // logical geometry stays untouched; only w_popup_topoff/bottomoff/ + // leftclip/rightclip record how many rows/columns of each edge fall + // outside the host so the drawing code can skip them. When the popup + // has fully scrolled past one of the host edges, hide it instead of + // leaving stray decorations behind. + if (popup_compute_clipwindow_offsets(wp)) + { + popup_hide_for_textprop(wp); + return; + } + wp->w_popup_last_changedtick = CHANGEDTICK(wp->w_buffer); if (win_valid(wp->w_popup_prop_win)) { wp->w_popup_prop_changedtick = CHANGEDTICK(wp->w_popup_prop_win->w_buffer); wp->w_popup_prop_topline = wp->w_popup_prop_win->w_topline; + wp->w_popup_prop_winrow = wp->w_popup_prop_win->w_winrow; + wp->w_popup_prop_wincol = wp->w_popup_prop_win->w_wincol; + wp->w_popup_prop_width = wp->w_popup_prop_win->w_width; + wp->w_popup_prop_winheight = wp->w_popup_prop_win->w_height; } // Need to update popup_mask if the position or size changed. @@ -3554,6 +3919,10 @@ popup_save_layout(win_T *wp, popup_layout_T *layout) layout->leftcol = wp->w_leftcol; layout->leftoff = wp->w_popup_leftoff; layout->has_scrollbar = wp->w_has_scrollbar; + layout->topoff = wp->w_popup_topoff; + layout->bottomoff = wp->w_popup_bottomoff; + layout->leftclip = wp->w_popup_leftclip; + layout->rightclip = wp->w_popup_rightclip; } /* @@ -3568,7 +3937,11 @@ popup_layout_changed(win_T *wp, popup_layout_T *layout) || layout->leftoff != wp->w_popup_leftoff || layout->width != wp->w_width || layout->height != wp->w_height - || layout->has_scrollbar != wp->w_has_scrollbar; + || layout->has_scrollbar != wp->w_has_scrollbar + || layout->topoff != wp->w_popup_topoff + || layout->bottomoff != wp->w_popup_bottomoff + || layout->leftclip != wp->w_popup_leftclip + || layout->rightclip != wp->w_popup_rightclip; } /* @@ -4298,6 +4671,8 @@ f_popup_getoptions(typval_T *argvars, typval_T *rettv) dict_add_number(dict, "resize", (wp->w_popup_flags & POPF_RESIZE) != 0); dict_add_number(dict, "posinvert", (wp->w_popup_flags & POPF_POSINVERT) != 0); + dict_add_number(dict, "clipwindow", + (wp->w_popup_flags & POPF_CLIPWINDOW) != 0); // Return opacity (0-100) by converting from internal blend value dict_add_number(dict, "opacity", (wp->w_popup_flags & POPF_OPACITY) ? 100 - wp->w_popup_blend : 100); @@ -4765,11 +5140,23 @@ check_popup_unhidden(win_T *wp) { textprop_T prop; linenr_T lnum; + bool found = false; - if ((wp->w_popup_flags & POPF_HIDDEN_FORCE) == 0 - && find_visible_prop(wp->w_popup_prop_win, - wp->w_popup_prop_type, wp->w_popup_prop_id, - &prop, &lnum)) + if ((wp->w_popup_flags & POPF_HIDDEN_FORCE) != 0) + return FALSE; + if (find_visible_prop(wp->w_popup_prop_win, + wp->w_popup_prop_type, wp->w_popup_prop_id, + &prop, &lnum)) + found = true; + // The textprop may have scrolled just above the host window's top. + // Unhide the popup so popup_adjust_position() can roll it partially + // onto the host's top edge via the top-clip path. Limit the search + // to the popup's own height so we do not resurrect a popup whose + // prop is already further off-screen than the popup can extend. + else if (popup_find_prop_above_top(wp, wp->w_popup_prop_win, + popup_height(wp), &prop, &lnum)) + found = true; + if (found) { wp->w_popup_flags &= ~POPF_HIDDEN; wp->w_popup_prop_topline = 0; // force repositioning @@ -4793,7 +5180,11 @@ popup_need_position_adjust(win_T *wp) if (win_valid(wp->w_popup_prop_win) && (wp->w_popup_prop_changedtick != CHANGEDTICK(wp->w_popup_prop_win->w_buffer) - || wp->w_popup_prop_topline != wp->w_popup_prop_win->w_topline)) + || wp->w_popup_prop_topline != wp->w_popup_prop_win->w_topline + || wp->w_popup_prop_winrow != wp->w_popup_prop_win->w_winrow + || wp->w_popup_prop_wincol != wp->w_popup_prop_win->w_wincol + || wp->w_popup_prop_width != wp->w_popup_prop_win->w_width + || wp->w_popup_prop_winheight != wp->w_popup_prop_win->w_height)) return TRUE; // May need to adjust the width if the cursor moved. @@ -5086,7 +5477,18 @@ may_update_popup_mask(int type) } width = popup_width(wp); - height = popup_height(wp); + // Match the drawn extent computed by update_popups so that cells + // outside the clipped popup are not marked as popup-owned and the + // background window can draw through them. + if (wp->w_popup_topoff > 0 || wp->w_popup_bottomoff > 0) + { + popup_clip_T cl; + + popup_compute_clip(wp, &cl); + height = cl.eff_height; + } + else + height = popup_height(wp); popup_update_mask(wp, width, height); // Popup with partial transparency do not block lower layers from @@ -5095,18 +5497,25 @@ may_update_popup_mask(int type) if ((wp->w_popup_flags & POPF_OPACITY) && wp->w_popup_blend > 0) continue; - for (line = wp->w_winrow; - line < wp->w_winrow + height && line < screen_Rows; ++line) - for (col = wp->w_wincol; - col < wp->w_wincol + width - wp->w_popup_leftoff - && col < screen_Columns; ++col) - if (wp->w_zindex < POPUPMENU_ZINDEX - && pum_visible() - && pum_under_menu(line, col, FALSE)) - mask[line * screen_Columns + col] = POPUPMENU_ZINDEX; - else if (wp->w_popup_mask_cells == NULL - || !popup_masked(wp, width, height, col, line)) - mask[line * screen_Columns + col] = wp->w_zindex; + { + int mask_start = wp->w_winrow + wp->w_popup_topoff; + int mask_end = mask_start + height; + int mask_col_start = wp->w_wincol + wp->w_popup_leftclip; + int mask_col_end = wp->w_wincol + width - wp->w_popup_leftoff + - wp->w_popup_rightclip; + + for (line = mask_start; + line < mask_end && line < screen_Rows; ++line) + for (col = mask_col_start; + col < mask_col_end && col < screen_Columns; ++col) + if (wp->w_zindex < POPUPMENU_ZINDEX + && pum_visible() + && pum_under_menu(line, col, FALSE)) + mask[line * screen_Columns + col] = POPUPMENU_ZINDEX; + else if (wp->w_popup_mask_cells == NULL + || !popup_masked(wp, width, height, col, line)) + mask[line * screen_Columns + col] = wp->w_zindex; + } } // Only check which lines are to be updated if not already @@ -5528,6 +5937,14 @@ update_popups(void (*win_update)(win_T *wp)) { int title_len = 0; int title_wincol; + popup_clip_T cl; + + // Compute the clip geometry once per iteration; w_popup_*off/clip, + // w_height, w_width, w_popup_border and w_popup_padding are stable + // for the duration of this iteration (popup_apply_winupdate_clip() + // mutates w_height/w_width temporarily but the result is restored + // before any code below reads cl again). + popup_compute_clip(wp, &cl); override_success = push_highlight_overrides(wp->w_hl, wp->w_hl_len); @@ -5613,13 +6030,25 @@ update_popups(void (*win_update)(win_T *wp)) // Draw the popup text, unless it's off screen. if (wp->w_winrow < screen_Rows && wp->w_wincol < screen_Columns) { + popup_geom_save_T saved; + + popup_geom_save(wp, &saved); + // May need to update the "cursorline" highlighting, which may also // change "topline" if (wp->w_popup_last_curline != wp->w_cursor.lnum) popup_highlight_curline(wp); + // Clip the buffer's drawn extent to the host window when + // "clipwindow" is set. The transient mutations are reverted by + // popup_geom_restore() so callers continue to see the popup's + // logical geometry via popup_getoptions/popup_getpos. + popup_apply_winupdate_clip(wp, &cl); + win_update(wp); + popup_geom_restore(wp, &saved); + // move the cursor into the visible lines, otherwise executing // commands with win_execute() may cause the text to jump. if (wp->w_cursor.lnum < wp->w_topline) @@ -5631,6 +6060,12 @@ update_popups(void (*win_update)(win_T *wp)) wp->w_winrow -= top_off; wp->w_wincol -= left_extra; + // "clipwindow" with top-clip shifts all popup decorations down so the + // first visible row of the popup lands at the host window's top edge. + // Apply the shift before drawing borders/padding/etc. and restore at + // the end of this popup's iteration. + wp->w_winrow += wp->w_popup_topoff; + // Add offset for border and padding if not done already. if ((wp->w_flags & WFLAG_WCOL_OFF_ADDED) == 0) { @@ -5643,8 +6078,22 @@ update_popups(void (*win_update)(win_T *wp)) wp->w_flags |= WFLAG_WROW_OFF_ADDED; } - total_width = popup_width(wp) - wp->w_popup_rightoff; - total_height = popup_height(wp); + // When clipped by "clipwindow", drop the border/padding slot at the + // clipped edge that we will not render, so the popup ends exactly on + // the last visible content row (no empty trailing side-border row) + // and starts on the first visible row when top-clipped. When + // unclipped, fall back to the full popup geometry (cl.eff_width + // excludes w_leftcol and the scrollbar, which popup_width() folds in). + if (wp->w_popup_leftclip > 0 || wp->w_popup_rightclip > 0) + total_width = cl.eff_width; + else + total_width = popup_width(wp) - wp->w_popup_rightoff; + if (total_width < 0) + total_width = 0; + if (wp->w_popup_topoff > 0 || wp->w_popup_bottomoff > 0) + total_height = cl.eff_height; + else + total_height = popup_height(wp); popup_attr = get_win_attr(wp); if (wp->w_winrow + total_height > cmdline_row) @@ -5723,16 +6172,16 @@ update_popups(void (*win_update)(win_T *wp)) wp->w_popup_border[0] > 0 ? border_attr[0] : popup_attr); } - wincol = wp->w_wincol - wp->w_popup_leftoff; - top_padding = wp->w_popup_padding[0]; - if (wp->w_popup_border[0] > 0) + wincol = wp->w_wincol - wp->w_popup_leftoff + wp->w_popup_leftclip; + top_padding = cl.eff_padding[0]; + if (cl.eff_border[0] > 0) { // top border; do not draw over the title if (title_len > 0) { screen_fill(wp->w_winrow, wp->w_winrow + 1, wincol < 0 ? 0 : wincol, title_wincol, - wp->w_popup_border[3] != 0 && wp->w_popup_leftoff == 0 + cl.eff_border[3] != 0 && wp->w_popup_leftoff == 0 ? border_char[4] : border_char[0], border_char[0], border_attr[0]); screen_fill(wp->w_winrow, wp->w_winrow + 1, @@ -5743,18 +6192,19 @@ update_popups(void (*win_update)(win_T *wp)) { screen_fill(wp->w_winrow, wp->w_winrow + 1, wincol < 0 ? 0 : wincol, wincol + total_width, - wp->w_popup_border[3] != 0 && wp->w_popup_leftoff == 0 + cl.eff_border[3] != 0 && wp->w_popup_leftoff == 0 ? border_char[4] : border_char[0], border_char[0], border_attr[0]); } - if (wp->w_popup_border[1] > 0) + if (cl.eff_border[1] > 0) { buf[mb_char2bytes(border_char[5], buf)] = NUL; screen_puts(buf, wp->w_winrow, wincol + total_width - 1, border_attr[1]); } } - else if (wp->w_popup_padding[0] == 0 && popup_top_extra(wp) > 0) + else if (cl.eff_padding[0] == 0 && popup_top_extra(wp) > 0 + && wp->w_popup_topoff == 0) top_padding = 1; if (top_padding > 0 || wp->w_popup_padding[2] > 0) @@ -5844,25 +6294,26 @@ update_popups(void (*win_update)(win_T *wp)) attr_thumb = highlight_attr[HLF_PST]; } - for (i = wp->w_popup_border[0]; - i < total_height - wp->w_popup_border[2]; ++i) + // The side-border loop spans the popup's drawn extent. cl.eff_border + // and cl.eff_padding collapse the clipped edges to 0 so the loop + // covers the full visible area without leaving an empty trailing row. + for (i = cl.eff_border[0]; i < total_height - cl.eff_border[2]; ++i) { int pad_left; // left and right padding only needed next to the body int do_padding = - i >= wp->w_popup_border[0] + wp->w_popup_padding[0] - && i < total_height - wp->w_popup_border[2] - - wp->w_popup_padding[2]; + i >= cl.eff_border[0] + cl.eff_padding[0] + && i < total_height - cl.eff_border[2] - cl.eff_padding[2]; row = wp->w_winrow + i; // left border - if (wp->w_popup_border[3] > 0 && wincol >= 0) + if (cl.eff_border[3] > 0 && wincol >= 0) { buf[mb_char2bytes(border_char[3], buf)] = NUL; screen_puts(buf, row, wincol, border_attr[3]); } - if (do_padding && wp->w_popup_padding[3] > 0) + if (do_padding && cl.eff_padding[3] > 0) { int col = wincol + wp->w_popup_border[3]; @@ -5899,13 +6350,13 @@ update_popups(void (*win_update)(win_T *wp)) screen_putchar(' ', row, scroll_col, popup_attr); } // right border - if (wp->w_popup_border[1] > 0) + if (cl.eff_border[1] > 0) { buf[mb_char2bytes(border_char[1], buf)] = NUL; screen_puts(buf, row, wincol + total_width - 1, border_attr[1]); } // right padding - if (do_padding && wp->w_popup_padding[1] > 0) + if (do_padding && cl.eff_padding[1] > 0) { int pad_col_start = wincol + wp->w_popup_border[3] + wp->w_popup_padding[3] + wp->w_width + wp->w_leftcol; @@ -5932,7 +6383,7 @@ update_popups(void (*win_update)(win_T *wp)) } } - if (wp->w_popup_padding[2] > 0) + if (cl.eff_padding[2] > 0) { // bottom padding row = wp->w_winrow + wp->w_popup_border[0] @@ -5945,24 +6396,24 @@ update_popups(void (*win_update)(win_T *wp)) padcol, padendcol, ' ', ' ', popup_attr); } - if (wp->w_popup_border[2] > 0) + if (cl.eff_border[2] > 0) { // bottom border row = wp->w_winrow + total_height - 1; screen_fill(row, row + 1, wincol < 0 ? 0 : wincol, wincol + total_width, - wp->w_popup_border[3] != 0 && wp->w_popup_leftoff == 0 + cl.eff_border[3] != 0 && wp->w_popup_leftoff == 0 ? border_char[7] : border_char[2], border_char[2], border_attr[2]); - if (wp->w_popup_border[1] > 0) + if (cl.eff_border[1] > 0) { buf[mb_char2bytes(border_char[6], buf)] = NUL; screen_puts(buf, row, wincol + total_width - 1, border_attr[2]); } } - if (wp->w_popup_shadow) + if (wp->w_popup_shadow && wp->w_popup_bottomoff == 0) { // bottom shadow row = wp->w_winrow + total_height; @@ -5996,6 +6447,10 @@ update_popups(void (*win_update)(win_T *wp)) if (override_success) pop_highlight_overrides(); + + // Undo the topoff shift applied before drawing the borders so the + // next iteration sees the popup's logical winrow. + wp->w_winrow -= wp->w_popup_topoff; } #ifdef FEAT_PROP_POPUP diff --git a/src/proto/textprop.pro b/src/proto/textprop.pro index d3ecf6d14c..be9d80c6e3 100644 --- a/src/proto/textprop.pro +++ b/src/proto/textprop.pro @@ -16,6 +16,7 @@ int get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change); int prop_count_above_below(buf_T *buf, linenr_T lnum); int count_props(linenr_T lnum, int only_starting, int last_line); void sort_text_props(buf_T *buf, textprop_T *props, int *idxs, int count); +bool find_prop_in_lines(win_T *wp, int type_id, int id, textprop_T *prop, linenr_T *found_lnum, linenr_T first_lnum, linenr_T last_lnum); bool find_visible_prop(win_T *wp, int type_id, int id, textprop_T *prop, linenr_T *found_lnum); char_u *props_add_count_header(char_u *line, int line_len, int textlen, int *new_len); void add_text_props(linenr_T lnum, textprop_T *text_props, int text_prop_count); diff --git a/src/structs.h b/src/structs.h index 4d92ca75a1..6c6ea8c0e4 100644 --- a/src/structs.h +++ b/src/structs.h @@ -4197,6 +4197,14 @@ struct window_S int w_popup_leftoff; // columns left of the screen int w_popup_rightoff; // columns right of the screen + int w_popup_topoff; // rows above the host window's top + // when "clipwindow" is set + int w_popup_bottomoff; // rows below the host window's bottom + // when "clipwindow" is set + int w_popup_leftclip; // columns left of the host window's left + // when "clipwindow" is set + int w_popup_rightclip; // columns right of the host window's right + // when "clipwindow" is set varnumber_T w_popup_last_changedtick; // b:changedtick of popup buffer // when position was computed varnumber_T w_popup_prop_changedtick; // b:changedtick of buffer with @@ -4205,6 +4213,14 @@ struct window_S int w_popup_prop_topline; // w_topline of window with // w_popup_prop_type when position was // computed + int w_popup_prop_winrow; // w_winrow of host window when + // position was computed + int w_popup_prop_wincol; // w_wincol of host window when + // position was computed + int w_popup_prop_width; // w_width of host window when + // position was computed + int w_popup_prop_winheight; // w_height of host window when + // position was computed linenr_T w_popup_last_curline; // last known w_cursor.lnum of window // with "cursorline" set callback_T w_close_cb; // popup close callback diff --git a/src/testdir/dumps/Test_popup_clipwindow_bottom_clip.dump b/src/testdir/dumps/Test_popup_clipwindow_bottom_clip.dump new file mode 100644 index 0000000000..b5d79480fb --- /dev/null +++ b/src/testdir/dumps/Test_popup_clipwindow_bottom_clip.dump @@ -0,0 +1,14 @@ +>h+0&#ffffff0|o|s|t| |l|i|n|e| |1| @28 +|h|o|s|t| |l|i|n|e| |2| @28 +|h|o|s|t| |l|i|n|e| |3| @28 +|h|o|s|t|╔+0#0000001#e0e0e08|═@8|╗| +0#0000000#ffffff0@24 +|h|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |A| |║| +0#0000000#ffffff0@24 +|h|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |B| |║| +0#0000000#ffffff0@24 +|[+3&&|N|o| |N|a|m|e|]| |[|+|]| @8|1|,|1| @11|T|o|p +| +0&&@39 +|~+0#4040ff13&| @38 +|~| @38 +|~| @38 +|~| @38 +|[+1#0000000&|N|o| |N|a|m|e|]| @12|0|,|0|-|1| @9|A|l@1 +| +0&&@39 diff --git a/src/testdir/dumps/Test_popup_clipwindow_hidden.dump b/src/testdir/dumps/Test_popup_clipwindow_hidden.dump new file mode 100644 index 0000000000..c5f8930695 --- /dev/null +++ b/src/testdir/dumps/Test_popup_clipwindow_hidden.dump @@ -0,0 +1,14 @@ +|h+0&#ffffff0|o|s|t| |l|i|n|e| |4|5| @27 +|h|o|s|t| |l|i|n|e| |4|6| @27 +|h|o|s|t| |l|i|n|e| |4|7| @27 +|h|o|s|t| |l|i|n|e| |4|8| @27 +|h|o|s|t| |l|i|n|e| |4|9| @27 +>h|o|s|t| |l|i|n|e| |5|0| @27 +|[+3&&|N|o| |N|a|m|e|]| |[|+|]| @8|5|0|,|1| @10|B|o|t +| +0&&@39 +|~+0#4040ff13&| @38 +|~| @38 +|~| @38 +|~| @38 +|[+1#0000000&|N|o| |N|a|m|e|]| @12|0|,|0|-|1| @9|A|l@1 +| +0&&@39 diff --git a/src/testdir/dumps/Test_popup_clipwindow_left_clip.dump b/src/testdir/dumps/Test_popup_clipwindow_left_clip.dump new file mode 100644 index 0000000000..5d38f98d2a --- /dev/null +++ b/src/testdir/dumps/Test_popup_clipwindow_left_clip.dump @@ -0,0 +1,14 @@ +| +0&#ffffff0@26||+1&&>h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|═+0#0000001#e0e0e08@7|╗| +0#0000000#ffffff0|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&| +0&&|p+0#0000001#e0e0e08|o|p|u|p| |A|║| |n+0#0000000#ffffff0|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&| +0&&|p+0#0000001#e0e0e08|o|p|u|p| |B|║| |n+0#0000000#ffffff0|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&| +0&&|p+0#0000001#e0e0e08|o|p|u|p| |C|║| |n+0#0000000#ffffff0|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|═+0#0000001#e0e0e08@7|╝| +0#0000000#ffffff0|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d +| @31|1|,|1| @10|T|o|p| diff --git a/src/testdir/dumps/Test_popup_clipwindow_right_clip.dump b/src/testdir/dumps/Test_popup_clipwindow_right_clip.dump new file mode 100644 index 0000000000..6427f94a5a --- /dev/null +++ b/src/testdir/dumps/Test_popup_clipwindow_right_clip.dump @@ -0,0 +1,14 @@ +>h+0&#ffffff0|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&| +0&&@26 +|h|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|╔+0#0000001#e0e0e08|═@3||+1#0000000#ffffff0|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|║+0#0000001#e0e0e08| |p|o|p||+1#0000000#ffffff0|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|║+0#0000001#e0e0e08| |p|o|p||+1#0000000#ffffff0|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|║+0#0000001#e0e0e08| |p|o|p||+1#0000000#ffffff0|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|╚+0#0000001#e0e0e08|═@3||+1#0000000#ffffff0|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25 +|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25 +| +0#0000000&@31|1|,|1| @10|T|o|p| diff --git a/src/testdir/dumps/Test_popup_clipwindow_top_clip.dump b/src/testdir/dumps/Test_popup_clipwindow_top_clip.dump new file mode 100644 index 0000000000..9876f15cce --- /dev/null +++ b/src/testdir/dumps/Test_popup_clipwindow_top_clip.dump @@ -0,0 +1,14 @@ +| +0&#ffffff0@39 +|~+0#4040ff13&| @38 +|~| @38 +|~| @38 +|~| @38 +|[+1#0000000&|N|o| |N|a|m|e|]| @12|0|,|0|-|1| @9|A|l@1 +>h+0&&|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |B| |║| +0#0000000#ffffff0@24 +|h|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |C| |║| +0#0000000#ffffff0@24 +|h|o|s|t|╚+0#0000001#e0e0e08|═@8|╝| +0#0000000#ffffff0@24 +|h|o|s|t| |l|i|n|e| |4| @28 +|h|o|s|t| |l|i|n|e| |5| @28 +|h|o|s|t| |l|i|n|e| |6| @28 +|[+3&&|N|o| |N|a|m|e|]| |[|+|]| @8|1|,|1| @11|T|o|p +| +0&&@39 diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim index d6aa634c72..873840d8e8 100644 --- a/src/testdir/test_popupwin.vim +++ b/src/testdir/test_popupwin.vim @@ -4522,6 +4522,222 @@ func Test_popup_setoptions_other_tab() call prop_type_delete('textprop') endfunc +func Test_popup_clipwindow_option() + " Default: clipwindow is off. + let id = popup_create('TEST', #{}) + call assert_equal(0, popup_getoptions(id).clipwindow) + call popup_close(id) + + " popup_create() honours the option. + let id = popup_create('TEST', #{clipwindow: v:true}) + call assert_equal(1, popup_getoptions(id).clipwindow) + + " popup_setoptions() can toggle it off and on. + call popup_setoptions(id, #{clipwindow: v:false}) + call assert_equal(0, popup_getoptions(id).clipwindow) + call popup_setoptions(id, #{clipwindow: v:true}) + call assert_equal(1, popup_getoptions(id).clipwindow) + + call popup_close(id) +endfunc + +func Test_popup_clipwindow_hide_when_prop_off_screen() + " A "clipwindow" popup attached to a textprop should be hidden once the + " host window scrolls so the textprop is far enough off-screen that even + " the partially-clipped popup would no longer overlap, and unhidden again + " when the prop scrolls back into reach. + call prop_type_add('clipprop', {}) + new + call setline(1, range(1, 200)->mapnew({_, v -> 'line ' .. v})) + call prop_add(5, 1, #{type: 'clipprop', length: 5}) + let host = win_getid() + + let id = popup_create('attached', #{ + \ textprop: 'clipprop', + \ textpropwin: host, + \ line: -1, + \ wrap: v:false, + \ fixed: v:true, + \ clipwindow: v:true, + \ }) + redraw + call assert_equal(1, popup_getpos(id).visible) + + " Scroll the host so the prop is far below topline; popup hides. + call win_execute(host, 'normal! Gzb') + redraw + call assert_equal(0, popup_getpos(id).visible) + + " Scroll back so the prop is on the first visible line; popup unhides. + call win_execute(host, 'normal! ggzt') + redraw + call assert_equal(1, popup_getpos(id).visible) + + call popup_close(id) + bwipe! + call prop_type_delete('clipprop') +endfunc + +func Test_popup_clipwindow_top_clip() + CheckScreendump + + let lines =<< trim END + vim9script + set nowrap + :botright new + :resize 6 + setline(1, range(1, 30)->mapnew((_, v) => 'host line ' .. v)) + prop_type_add('clipprop', {}) + prop_add(2, 1, {type: 'clipprop', length: 4}) + popup_create(['popup A', 'popup B', 'popup C'], { + textprop: 'clipprop', + line: -4, + col: 0, + border: [], + padding: [0, 1, 0, 1], + highlight: 'PmenuSel', + wrap: false, + fixed: true, + posinvert: false, + clipwindow: true, + }) + END + call writefile(lines, 'XtestPopupClipwindowTop', 'D') + let buf = RunVimInTerminal('-S XtestPopupClipwindowTop', #{rows: 14, cols: 40}) + call VerifyScreenDump(buf, 'Test_popup_clipwindow_top_clip', {}) + + call StopVimInTerminal(buf) +endfunc + +func Test_popup_clipwindow_bottom_clip() + CheckScreendump + + let lines =<< trim END + vim9script + set nowrap + :topleft new + :resize 6 + setline(1, range(1, 30)->mapnew((_, v) => 'host line ' .. v)) + prop_type_add('clipprop', {}) + prop_add(2, 1, {type: 'clipprop', length: 4}) + popup_create(['popup A', 'popup B', 'popup C'], { + textprop: 'clipprop', + line: 1, + col: 0, + border: [], + padding: [0, 1, 0, 1], + highlight: 'PmenuSel', + wrap: false, + fixed: true, + posinvert: false, + clipwindow: true, + }) + END + call writefile(lines, 'XtestPopupClipwindowBottom', 'D') + let buf = RunVimInTerminal('-S XtestPopupClipwindowBottom', #{rows: 14, cols: 40}) + call VerifyScreenDump(buf, 'Test_popup_clipwindow_bottom_clip', {}) + + call StopVimInTerminal(buf) +endfunc + +func Test_popup_clipwindow_left_clip() + CheckScreendump + + let lines =<< trim END + vim9script + set nowrap + :vert botright new + :vert resize 22 + set laststatus=0 + setline(1, repeat(['host content line abcdef'], 20)) + prop_type_add('clipprop', {}) + prop_add(5, 6, {type: 'clipprop', length: 4}) + popup_create(['popup A', 'popup B', 'popup C'], { + textprop: 'clipprop', + line: 0, + col: -10, + border: [], + padding: [0, 1, 0, 1], + highlight: 'PmenuSel', + wrap: false, + fixed: true, + posinvert: false, + clipwindow: true, + }) + END + call writefile(lines, 'XtestPopupClipwindowLeft', 'D') + let buf = RunVimInTerminal('-S XtestPopupClipwindowLeft', #{rows: 14, cols: 50}) + call VerifyScreenDump(buf, 'Test_popup_clipwindow_left_clip', {}) + + call StopVimInTerminal(buf) +endfunc + +func Test_popup_clipwindow_right_clip() + CheckScreendump + + let lines =<< trim END + vim9script + set nowrap + :vert topleft new + :vert resize 22 + set laststatus=0 + setline(1, repeat(['host content line abcdef'], 20)) + prop_type_add('clipprop', {}) + prop_add(5, 14, {type: 'clipprop', length: 4}) + popup_create(['popup A', 'popup B', 'popup C'], { + textprop: 'clipprop', + line: 0, + col: 0, + border: [], + padding: [0, 1, 0, 1], + highlight: 'PmenuSel', + wrap: false, + fixed: true, + posinvert: false, + clipwindow: true, + }) + END + call writefile(lines, 'XtestPopupClipwindowRight', 'D') + let buf = RunVimInTerminal('-S XtestPopupClipwindowRight', #{rows: 14, cols: 50}) + call VerifyScreenDump(buf, 'Test_popup_clipwindow_right_clip', {}) + + call StopVimInTerminal(buf) +endfunc + +func Test_popup_clipwindow_hidden() + CheckScreendump + + let lines =<< trim END + vim9script + set nowrap + :topleft new + :resize 6 + setline(1, range(1, 50)->mapnew((_, v) => 'host line ' .. v)) + prop_type_add('clipprop', {}) + prop_add(2, 1, {type: 'clipprop', length: 4}) + popup_create(['popup A', 'popup B', 'popup C'], { + textprop: 'clipprop', + line: -4, + col: 0, + border: [], + padding: [0, 1, 0, 1], + highlight: 'PmenuSel', + wrap: false, + fixed: true, + posinvert: false, + clipwindow: true, + }) + # Scroll the host so the textprop is far below topline; the popup is + # then hidden because the prop has scrolled out of the host window. + win_execute(win_getid(), 'normal! Gzb') + END + call writefile(lines, 'XtestPopupClipwindowHidden', 'D') + let buf = RunVimInTerminal('-S XtestPopupClipwindowHidden', #{rows: 14, cols: 40}) + call VerifyScreenDump(buf, 'Test_popup_clipwindow_hidden', {}) + + call StopVimInTerminal(buf) +endfunc + func Test_popup_prop_not_visible() CheckScreendump diff --git a/src/textprop.c b/src/textprop.c index 48cbad4b6a..a049edec9b 100644 --- a/src/textprop.c +++ b/src/textprop.c @@ -1396,25 +1396,28 @@ sort_text_props( } /* - * Find text property "type_id" in the visible lines of window "wp". - * Match "id" when it is > 0. - * Returns false when not found. + * Find text property "type_id" in lines [first_lnum, last_lnum] of window + * "wp"'s buffer. Match "id" when it is > 0. Returns false when not found. */ bool -find_visible_prop( +find_prop_in_lines( win_T *wp, int type_id, int id, textprop_T *prop, - linenr_T *found_lnum) + linenr_T *found_lnum, + linenr_T first_lnum, + linenr_T last_lnum) { - // return when "type_id" no longer exists if (text_prop_type_by_id(wp->w_buffer, type_id) == NULL) return false; - // w_botline may not have been updated yet. - validate_botline_win(wp); - for (linenr_T lnum = wp->w_topline; lnum < wp->w_botline; ++lnum) + if (first_lnum < 1) + first_lnum = 1; + if (last_lnum > wp->w_buffer->b_ml.ml_line_count) + last_lnum = wp->w_buffer->b_ml.ml_line_count; + + for (linenr_T lnum = first_lnum; lnum <= last_lnum; ++lnum) { char_u *props; int count = get_text_props(wp->w_buffer, lnum, &props, FALSE); @@ -1432,6 +1435,25 @@ find_visible_prop( return false; } +/* + * Find text property "type_id" in the visible lines of window "wp". + * Match "id" when it is > 0. + * Returns false when not found. + */ + bool +find_visible_prop( + win_T *wp, + int type_id, + int id, + textprop_T *prop, + linenr_T *found_lnum) +{ + // w_botline may not have been updated yet. + validate_botline_win(wp); + return find_prop_in_lines(wp, type_id, id, prop, found_lnum, + wp->w_topline, wp->w_botline - 1); +} + /* * Set the text properties for line "lnum" to "tps" array with "count" entries. * If "count" is zero text properties are removed. diff --git a/src/version.c b/src/version.c index f1e40ddb20..2882be5795 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 469, /**/ 468, /**/ diff --git a/src/vim.h b/src/vim.h index ee67e4078a..d9a22ef018 100644 --- a/src/vim.h +++ b/src/vim.h @@ -690,6 +690,7 @@ extern int (*dyn_libintl_wputenv)(const wchar_t *envstring); #define POPF_INFO_MENU 0x400 // align info popup with popup menu #define POPF_POSINVERT 0x800 // vertical position can be inverted #define POPF_OPACITY 0x1000 // popup has opacity/transparency setting +#define POPF_CLIPWINDOW 0x2000 // confine popup to its host window's rect // flags used in w_popup_handled #define POPUP_HANDLED_1 0x01 // used by mouse_find_win()