Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

glepnir
Copy link
Member

@glepnir glepnir commented Sep 2, 2025

Problem:

  1. Rightmost window retained vsep after adjacent window closed
  2. Explicit width/height ignored due to win_equal() override
  3. winfixwidth windows expanded when receiving space from closed windows

Solution:

  1. Clear vsep_width for new lastwin in win_remove()
  2. Add WSP_NOEQUAL flag to skip equalization when size explicitly set
  3. Skip frame resizing for winfixwidth windows in winframe_remove()

Fix #32854

@zeertzjq zeertzjq requested a review from seandewar September 3, 2025 01:39
@seandewar
Copy link
Member

seandewar commented Sep 3, 2025

This is a Vim bug: #32854 (comment)
I'm not sure about this fix.

@glepnir
Copy link
Member Author

glepnir commented Sep 3, 2025

It might be a bit different — in nvim_open_win the window width is set directly, rather than resized🤔. I’m not completely sure about this. The fix for win_remove should also apply to Vim. I’ll try it out there.

@seandewar
Copy link
Member

seandewar commented Sep 3, 2025

Looks like the vsep bug didn't exist back in v9.0.0000; gonna bisect to see what introduced it (hopefully it wasn't me 😇)

@glepnir
Copy link
Member Author

glepnir commented Sep 3, 2025

because in win_split_ins:1437 set 1 for oldwin but when new_win removed the vsep_width of oldwin not reset. 🤔

      wp->w_vsep_width = oldwin->w_vsep_width;
      oldwin->w_vsep_width = 1;

what's wrong of element ..always offline for me 😮‍💨

@seandewar
Copy link
Member

Yup, matrix servers are super duper offline at the moment 😁

@glepnir
Copy link
Member Author

glepnir commented Sep 3, 2025

ohhhh 💣 💥

@seandewar
Copy link
Member

seandewar commented Sep 3, 2025

Okay after bisecting looks like it was indeed me that caused the vsep issue in vim/vim@5866bc3 🚀

@seandewar
Copy link
Member

seandewar commented Sep 3, 2025

Okay, seems this isn't actually a bug in my commit (phew 😌); the vsep issue reproduces in versions like v9.0.0000 (probably even earlier), but only if &ea is unset.

It just so happens that before that commit, the frame layout is slightly different such that win_equal_req calls frame_new_width on a leaf frame for the last window, which removes the vsep if &ea is set.

@glepnir
Copy link
Member Author

glepnir commented Sep 3, 2025

Okay, seems this isn't actually a bug in my commit (phew 😌)

😊 that clears up my doubt. Since your commit didn’t change it, I was curious why it was that commit.

Comment on lines 5417 to 5425
if (tp == NULL) {
lastwin = curtab->tp_lastwin = wp->w_prev;
} else {
tp->tp_lastwin = wp->w_prev;
}
win_T *new_last = (tp == NULL) ? lastwin : tp->tp_lastwin;
if (new_last != NULL && !new_last->w_floating) {
new_last->w_vsep_width = 0;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this doesn't fix the vsep issue if the window with the vsep isn't the last window:

botright vsplit
set winfixwidth
botright vsplit
botright split
wincmd p
quit

I think it's probably more appropriate to fixup the vsep somewhere like winframe_remove instead anyway? win_remove relates moreso to the window list.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

range FOR_ALL_WINDOWS_IN_TAB(fix_wp, target_tp) and find if frp->fr_parent == NULL then reset vsep_width ?

Copy link
Member Author

@glepnir glepnir Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh it works .. do you have any other more smart way 🤔

  if (*dirp == 'h') {
    tabpage_T *target_tp = tp ? tp : curtab;
    FOR_ALL_WINDOWS_IN_TAB(fix_wp, target_tp) {
      if (!fix_wp->w_floating) {
        // Check if this window is now the rightmost in its row
        frame_T *frp;
        for (frp = fix_wp->w_frame; frp->fr_parent != NULL; frp = frp->fr_parent) {
          if (frp->fr_parent->fr_layout == FR_ROW && frp->fr_next != NULL) {
            break;
          }
        }
        if (frp->fr_parent == NULL) {
          fix_wp->w_vsep_width = 0;  // rightmost window doesn't need separator
        }
      }
    }
  }

Copy link
Member

@seandewar seandewar Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'll work (though the w_width will be wrong), but it'll maybe be more efficient to check if the closed window was a rightmost window, and then recursively remove vseps of the windows to the left.

Might be a bit tricky; I'll see if I can come up with something.

Copy link
Member

@seandewar seandewar Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this'll work (and shouldn't cause issues with any unflattened frames after the remove):

diff --git a/src/nvim/window.c b/src/nvim/window.c
index d5f325bf4f..97fafcf3d4 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -1440,7 +1440,7 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir, frame_T *to_fl
     }
     if (toplevel) {
       if (flags & WSP_BOT) {
-        frame_add_vsep(curfrp);
+        frame_set_vsep(curfrp, true);
       }
       // Set width of neighbor frame
       frame_new_width(curfrp, curfrp->fr_width
@@ -3214,6 +3214,12 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp, frame_T **unflat_al
   int row = topleft->w_winrow;
   int col = topleft->w_wincol;
 
+  // If this is a rightmost window, remove vertical separators to the left.
+  if (win->w_vsep_width == 0 && frp_close->fr_parent->fr_layout == FR_ROW
+      && frp_close->fr_prev != NULL) {
+    frame_set_vsep(frp_close->fr_prev, false);
+  }
+
   // Remove this frame from the list of frames.
   frame_remove(frp_close);
 
@@ -3405,7 +3411,7 @@ void winframe_restore(win_T *wp, int dir, frame_T *unflat_altfr)
 
   // Vertical separators to the left may have been lost.  Restore them.
   if (wp->w_vsep_width == 0 && frp->fr_parent->fr_layout == FR_ROW && frp->fr_prev != NULL) {
-    frame_add_vsep(frp->fr_prev);
+    frame_set_vsep(frp->fr_prev, true);
   }
 
   // Statuslines or horizontal separators above may have been lost.  Restore them.
@@ -3851,23 +3857,26 @@ static void frame_new_width(frame_T *topfrp, int width, bool leftfirst, bool wfw
   topfrp->fr_width = width;
 }
 
-/// Add the vertical separator to windows at the right side of "frp".
+/// Add or remove the vertical separator to windows at the right side of "frp".
 /// Note: Does not check if there is room!
-static void frame_add_vsep(const frame_T *frp)
+static void frame_set_vsep(const frame_T *frp, bool add)
   FUNC_ATTR_NONNULL_ARG(1)
 {
   if (frp->fr_layout == FR_LEAF) {
     win_T *wp = frp->fr_win;
-    if (wp->w_vsep_width == 0) {
+    if (add && wp->w_vsep_width == 0) {
       if (wp->w_width > 0) {            // don't make it negative
         wp->w_width--;
       }
       wp->w_vsep_width = 1;
+    } else if (!add && wp->w_vsep_width == 1) {
+      win_new_width(wp, wp->w_width + 1);
+      wp->w_vsep_width = 0;
     }
   } else if (frp->fr_layout == FR_COL) {
     // Handle all the frames in the column.
     FOR_ALL_FRAMES(frp, frp->fr_child) {
-      frame_add_vsep(frp);
+      frame_set_vsep(frp, add);
     }
   } else {
     assert(frp->fr_layout == FR_ROW);
@@ -3876,7 +3885,7 @@ static void frame_add_vsep(const frame_T *frp)
     while (frp->fr_next != NULL) {
       frp = frp->fr_next;
     }
-    frame_add_vsep(frp);
+    frame_set_vsep(frp, add);
   }
 }

This assumes that if the window doesn't have a vsep, then it must already be a rightmost window (so it avoids the loop from before).
Similar to the same assumption I make in winframe_restore, but the inverse.

win_new_width is needed otherwise the last window column is wrong due to w_view_width being out-of-date (same as this PR, but w_width was also wrong). Not sure why it's not used when add is true; might be correct to use it there too if it doesn't regress anything...

Copy link
Member

@seandewar seandewar Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't call winframe_restore and fails on master and v0.9 (maybe other versions too). Don't think it's worth looking into here.

You need to trigger E36 from moving (not splitting) a window with some 'wfw' windows nearby. Maybe run the test via gdb or add some logging to check you're hitting it. There's some "no room" tests in that file that you can copy and adjust to make that easier.

I can write one later if you're stuck, but it might be a while as today's looking busy. 🥲

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe i can use help win for test 🤔

I can write one later if you're stuck, but it might be a while as today's looking busy.

no worry :)

Copy link
Member

@seandewar seandewar Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh, we also shouldn't resize the frame if the 'wfw' window will get the space anyway, otherwise it'll be off by one when it does...

Plus, we should only be checking if there is a 'wfw' window in a rightmost leaf child in the left frame; frame_fixed_width doesn't do that... (need to write a test specifically for this; I haven't done that here)

I think the following should fix those, but I'm starting to think fixing the resizing isn't worth all the complexity: 🫠

diff --git a/src/nvim/window.c b/src/nvim/window.c
index 2cc3ea4ddb..7b540ecef0 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -3221,10 +3221,10 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp, frame_T **unflat_al
   // If this is a rightmost window, remove vertical separators to the left.
   if (win->w_vsep_width == 0 && frp_close->fr_parent->fr_layout == FR_ROW
       && frp_close->fr_prev != NULL) {
-    frame_set_vsep(frp_close->fr_prev, false);
-    // Windows resized to fill their vseps.  If one is 'winfixwidth', resize the frame to maintain
+    // Windows resize to fill their vseps.  If one is 'winfixwidth', resize the frame to maintain
     // its previous width.
-    if (frame_fixed_width(frp_close->fr_prev)) {
+    bool right_has_wfw = frame_set_vsep(frp_close->fr_prev, false);
+    if (right_has_wfw && frp_close->fr_prev != altfr) {
       frame_new_width(frp_close->fr_prev, frp_close->fr_prev->fr_width - 1, false, false);
     }
   }
@@ -3422,10 +3422,10 @@ void winframe_restore(win_T *wp, int dir, frame_T *unflat_altfr)
   if (wp->w_vsep_width == 0 && frp->fr_parent->fr_layout == FR_ROW && frp->fr_prev != NULL) {
     // Windows resize to fit their vseps.  If one is 'winfixwidth', resize the frame to maintain its
     // previous width.
-    if (frame_fixed_width(frp->fr_prev)) {
+    bool right_had_wfw = frame_set_vsep(frp->fr_prev, true);
+    if (right_had_wfw && frp->fr_prev != unflat_altfr) {
       frame_new_width(frp->fr_prev, frp->fr_prev->fr_width + 1, false, false);
     }
-    frame_set_vsep(frp->fr_prev, true);
   }
 
   // Statuslines or horizontal separators above may have been lost.  Restore them.
@@ -3873,7 +3873,8 @@ static void frame_new_width(frame_T *topfrp, int width, bool leftfirst, bool wfw
 
 /// Add or remove the vertical separator to windows at the right side of "frp".
 /// Note: Does not check if there is room!
-static void frame_set_vsep(const frame_T *frp, bool add)
+/// @return whether any rightmost window has 'winfixwidth' set.
+static bool frame_set_vsep(const frame_T *frp, bool add)
   FUNC_ATTR_NONNULL_ARG(1)
 {
   if (frp->fr_layout == FR_LEAF) {
@@ -3887,11 +3888,14 @@ static void frame_set_vsep(const frame_T *frp, bool add)
       win_new_width(wp, wp->w_width + 1);
       wp->w_vsep_width = 0;
     }
+    return wp->w_p_wfw;
   } else if (frp->fr_layout == FR_COL) {
     // Handle all the frames in the column.
+    bool has_wfw = false;
     FOR_ALL_FRAMES(frp, frp->fr_child) {
-      frame_set_vsep(frp, add);
+      has_wfw = frame_set_vsep(frp, add) || has_wfw;
     }
+    return has_wfw;
   } else {
     assert(frp->fr_layout == FR_ROW);
     // Only need to handle the last frame in the row.
@@ -3899,7 +3903,7 @@ static void frame_set_vsep(const frame_T *frp, bool add)
     while (frp->fr_next != NULL) {
       frp = frp->fr_next;
     }
-    frame_set_vsep(frp, add);
+    return frame_set_vsep(frp, add);
   }
 }
 
diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua
index 0962547197..1177c0fca9 100644
--- a/test/functional/api/window_spec.lua
+++ b/test/functional/api/window_spec.lua
@@ -2296,6 +2296,15 @@ describe('API/win', function()
         { 20, 20 },
         { api.nvim_win_get_width(wins[#wins]), api.nvim_win_get_width(wins[#wins - 1]) }
       )
+      -- If a 'winfixwidth' window gets the space anyway, check it has the correct size.
+      command('set winfixwidth | only | mode')
+      screen:expect([[
+        {5:1000                                              }|
+        ^                                                  |
+        {1:~                                                 }|*7
+                                                          |
+      ]])
+      eq(eval('&columns'), api.nvim_win_get_width(0))
     end)
     it('opens a large right split window without crashing', function()
       api.nvim_command('vsplit')

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect the resize case can be done in anther PR 🤔 ?

Copy link
Member

@seandewar seandewar Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you've already applied the change it can stay here, but it could do with a test where 'wfw' doesn't cause the frame to resize if it's not the altfr, if that's possible. (Also a winframe_restore test as mentioned earlier)

@glepnir glepnir force-pushed the 32854 branch 2 times, most recently from 021a17b to 0412048 Compare September 3, 2025 11:55
@glepnir glepnir force-pushed the 32854 branch 3 times, most recently from ad18b3f to 2d41c77 Compare September 4, 2025 06:38
@glepnir glepnir force-pushed the 32854 branch 4 times, most recently from 5e78e95 to 64ac485 Compare September 6, 2025 05:54
Problem:
1. Rightmost window retained vsep after adjacent window closed
2. Explicit width/height ignored due to win_equal() override
3. winfixwidth windows expanded when receiving space from closed windows

Solution:
1. Clear vsep_width for new lastwin in win_remove()
2. Add WSP_NOEQUAL flag to skip equalization when size explicitly set
3. Skip frame resizing for winfixwidth windows in winframe_remove()

Co-authored-by: Sean Dewar <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

An unintended window border has appeared on the right side of the screen.
2 participants