]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0469: popup: textprop-anchored popups bleed past host window edges v9.2.0469
authorYasuhiro Matsumoto <mattn.jp@gmail.com>
Sun, 10 May 2026 18:53:57 +0000 (18:53 +0000)
committerChristian Brabandt <cb@256bit.org>
Sun, 10 May 2026 19:02:38 +0000 (19:02 +0000)
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 <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
15 files changed:
runtime/doc/popup.txt
runtime/doc/tags
runtime/doc/version9.txt
src/popupwin.c
src/proto/textprop.pro
src/structs.h
src/testdir/dumps/Test_popup_clipwindow_bottom_clip.dump [new file with mode: 0644]
src/testdir/dumps/Test_popup_clipwindow_hidden.dump [new file with mode: 0644]
src/testdir/dumps/Test_popup_clipwindow_left_clip.dump [new file with mode: 0644]
src/testdir/dumps/Test_popup_clipwindow_right_clip.dump [new file with mode: 0644]
src/testdir/dumps/Test_popup_clipwindow_top_clip.dump [new file with mode: 0644]
src/testdir/test_popupwin.vim
src/textprop.c
src/version.c
src/vim.h

index 250127b9b62e86daee37aef0fe7f6e0846634d4c..2afa93a7565f3253adf826546140be9cadde5bab 100644 (file)
@@ -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.
index 3168235dad021d321ac9967dcc850b1053ceaf96..8eef8cf28b99fc665d2bfd45e8a70442d4b266e9 100644 (file)
@@ -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*
index a1b9afce713e036af31d9eb5a898ad1d54f581b8..f4cdc702b72cac436d5e130b16c6e365bef1ff5d 100644 (file)
@@ -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 ~
 ---------
index dcbe0e854c485c6c45e5950dbdf5818bc12cae2d..5bd156367e4d9ba4e4f08f974c53a299444b640e 100644 (file)
@@ -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
index d3ecf6d14c8e573d8dcc47ddfc6a0171637d37fe..be9d80c6e3f09a5003d13f7625419de77731da84 100644 (file)
@@ -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);
index 4d92ca75a1893f02d50e8cf434bb231d9abfbd62..6c6ea8c0e48183c431ec28546d76a3c694f3c66a 100644 (file)
@@ -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 (file)
index 0000000..b5d7948
--- /dev/null
@@ -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 (file)
index 0000000..c5f8930
--- /dev/null
@@ -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 (file)
index 0000000..5d38f98
--- /dev/null
@@ -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 (file)
index 0000000..6427f94
--- /dev/null
@@ -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 (file)
index 0000000..9876f15
--- /dev/null
@@ -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
index d6aa634c72c9f12b6c2ed4425c81a65fce2c9abd..873840d8e80bae9f64bb2277f6f1d6e30d218f56 100644 (file)
@@ -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
 
index 48cbad4b6a6f9d92a076418afee767bd7971ed88..a049edec9b9fc4136dab0eacd742a4347e879010 100644 (file)
@@ -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.
index f1e40ddb201677ed113e01ac99ae151f32695bd4..2882be579592433338566b0a3d382c38557361e8 100644 (file)
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    469,
 /**/
     468,
 /**/
index ee67e4078ae0268a7f47fd91f8105057e68b4d18..d9a22ef018ca2973f1ac4dcb151570aa58f8da45 100644 (file)
--- 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()