Skip to content

Keep panes and gutters aligned under a winbar - #16

Merged
CoreyKaylor merged 3 commits into
mainfrom
winbar-alignment
Nov 4, 2026
Merged

CoreyKaylor merged 3 commits into
mainfrom
winbar-alignment

Conversation

@CoreyKaylor

@CoreyKaylor CoreyKaylor commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Fixes #14.

The problem

A winbar takes the top screen row of a window, which moves that window's text down one row.

  • Per-window bars (LspSaga breadcrumbs): only the source panes get a bar, so the code sits one row below its number/connector gutters.
  • Global winbar: every split gets one, so the cell gutters line up, but the Kitty canvas was placed at the connector window's top row, the bar row. That puts it one row above the code.

The fix

A global winbar can't be hidden on one window: an empty local value falls back to the global one. So instead of removing bars, DiffBandit keeps the bar row the same across the windows that must line up.

  • When any pane shows a winbar, the gutters, overview strips and panes without one get a blank bar, using the gutter background. Your own bars, like breadcrumbs, stay as they are.
  • The session, merge and folder views re-check whenever one of their windows is redrawn (a decoration provider's on_win, read-only, which schedules the realign). A bar a plugin sets later, even from inside its own autocmd where OptionSet doesn't fire, is picked up on the next redraw.
  • The Kitty canvas now starts below the connector's winbar, and viewport heights count only the rows that show code (nvim_win_get_height includes the winbar row).
  • With a global winbar, the one-row status headers grow to two rows (a blank bar above the header). Neovim forces this.

Testing

  • Added a session spec (suite 16g) for a bar on one pane, a global bar, removing the bar, and a bar set or cleared from inside another autocmd after the first render, plus a Kitty spec for the canvas row and height under a winbar.
  • nvim --headless -u tests/run.lua passes.

Please confirm

@tpodolak @linduxed, could you try this branch with your setup (LspSaga breadcrumbs / vim.opt.winbar = "%=%m %f%=", cells and Kitty renderers) and confirm the rows line up? With lazy.nvim:

{ "CoreyKaylor/diffbandit.nvim", branch = "winbar-alignment" }

linduxed: you can drop your autocmd that turns the winbar off.

A winbar takes the top row of a window. Breadcrumb plugins such as
LspSaga set one on the source panes only, shifting the code a row below
its number/connector gutters; a global 'winbar' reaches every split, but
the Kitty canvas was placed at the connector's top row (the bar), one
row above the code.

A global winbar cannot be hidden per window (an empty local value falls
back to it), so instead the bar row is kept uniform: when any aligned
window shows a winbar, gutters, overview strips and bar-less panes get a
blank one. The user's own bars are kept. Session, merge and folder hosts
re-align on OptionSet winbar and BufWinEnter (buffer swaps reset it).

The Kitty canvas now starts below the connector's winbar, and viewport
heights use text rows (winheight) instead of nvim_win_get_height, which
counts the bar row.

Fixes #14
@tpodolak

tpodolak commented Nov 4, 2026 •

Copy link
Copy Markdown

Thanks for looking into that @CoreyKaylor. It works when winbar is rendered immediately(focus on right diff panel from the start)

image

but when left panel is selected initially(e.g switching files with [f) and then right one is focused, then we are back to the offset issue

offset.mp4

Basically when winbar is rendered once diff lines were generated the issue remains. I dont know TBH why lspsaga defers winbar rendering, maybe I am able to somehow force it to be rendered eagerly

LspSaga renders breadcrumbs from its CursorMoved, BufEnter and
symbol-update callbacks. OptionSet does not fire there (autocmds are not
nested), and a bar appearing fires neither WinResized nor WinScrolled,
so a bar added after the first render (focus moving to the right pane,
]f/[f) left the gutters a row off.

Showing a winbar always redraws its window, so the watcher now rides on
a decoration provider: on_win checks (read-only) whether a host's bar
rows are in step and schedules the realign when they are not. This
covers any source of change: nested or top-level sets, global toggles,
and buffer swaps.
@CoreyKaylor

Copy link
Copy Markdown
Owner Author

Thanks for the video @tpodolak, that pinned it down.

LspSaga sets the breadcrumbs from inside its own CursorMoved/BufEnter/symbol-update callbacks. Neovim doesn't fire OptionSet from inside another autocmd, and a winbar appearing doesn't fire WinResized or WinScrolled either. So when the bar showed up after the first render (focus moving to the right pane, ]f/[f), nothing told DiffBandit about it. It only worked when the bar existed before the layout check.

5fcdd26 hooks the check into redraws instead: showing a winbar always redraws its window, so the next redraw re-aligns the gutters no matter how the bar was set. I reproduced your flow (left pane focused first, then a BufEnter hook sets the bar on the right pane) with both the cells and Kitty renderers, and the panes, gutters and image now move down together.

You shouldn't need to make LspSaga render eagerly. Could you update the branch and try again?

@tpodolak

tpodolak commented Nov 4, 2026 •

Copy link
Copy Markdown

Thanks. Works now as expected for both cases for cell rendering. For kitty(on ghosty), center gutter is offset though when right winbar is lazy loaded
image
it resets to normal once I scroll up and down though

Re-sending an image id with a new c/r does not resize its existing
virtual placement in Ghostty: the old grid is kept and the new image is
stretched over it. A winbar arriving after the first paint takes one row
from the connector, so the ribbons drifted further off the further down
the screen they were, until a later same-grid transmit happened to fix
it. Delete the old image and placement before transmitting a different
grid; same-grid repaints still replace the image in place.
@CoreyKaylor

Copy link
Copy Markdown
Owner Author

Thanks @tpodolak, glad cells is sorted. I reproduced the Kitty one in Ghostty.

The offset wasn't a whole row: it grew further down the screen, about half a row by line 15. DiffBandit was sending the right thing; when the lazy winbar takes a row, it sends a new image one row shorter. But Ghostty doesn't resize an image's existing virtual placement when the same image id is re-sent with a new cell grid. It keeps the old 35-row grid and stretches the 34-row image over it. Scrolling fixed it only because a later transmit happened to replace the placement.

7eb0b1e deletes the old image and placement before sending a different grid (same-grid repaints, like scrolling, still replace in place). With that, the change band and add wedges land on their rows in Ghostty after the bar loads lazily. This probably also fixes the same drift after resizing the terminal or a split.

Could you pull the branch once more?

@tpodolak

tpodolak commented Nov 4, 2026

Copy link
Copy Markdown

Works fine now with both Kitty and cell rendering. Thanks

@CoreyKaylor
CoreyKaylor merged commit ddf56b6 into main Nov 4, 2026
1 check passed
@CoreyKaylor
CoreyKaylor deleted the winbar-alignment branch November 4, 2026 19:16
@CoreyKaylor

Copy link
Copy Markdown
Owner Author

Thanks for all the feedback.

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.

Diff line rendering is offset when winbar is enabled

2 participants