Select, navigate and edit diff lines in the focused main view - #6031
Open
stefanhaller wants to merge 16 commits into
Open
stefanhaller wants to merge 16 commits into
stefanhaller wants to merge 16 commits into
Conversation
stefanhaller
added this pull request to stack #6027
September 20, 2026 05:39
stefanhaller
force-pushed
the
select-diff-lines-in-main-view
branch
from
September 20, 2026 17:51
a05ddb0 to
8d1134e
Compare
stefanhaller
force-pushed
the
select-diff-lines-in-main-view
branch
from
September 21, 2026 12:44
8d1134e to
c0acd8d
Compare
stefanhaller
force-pushed
the
select-diff-lines-in-main-view
branch
2 times, most recently
from
September 25, 2026 09:32
4a24367 to
fdf0d40
Compare
The focused main view is about to get a real diff selection, which makes its up/down/page/top/bottom keys mode-aware: what they do depends on the select state the main view controller owns. Keeping them in a separate controller would mean either duplicating that state or reaching across controllers for it, so move them to where the state will live. The two controllers were attached to the same pair of contexts and nothing else, so nothing else can notice. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The focused main view is about to show a selection, but only where there are diff lines to act on: a branch's commit log or the status dashboard has nothing to select. Rather than have the main view guess from the rendered content, let the side panels say so, since each of them knows what it renders. The classification is finer than a yes/no because acting on a selection means different things per panel — staging into the working tree for the files panel, taking lines into a custom patch for the commit panels — and those actions want the same one answer as this. Only whether a panel shows a diff at all is read for now. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking a line of the focused main view entered the staging or patch building panel at that line. The focused main view is about to gain a selection of its own, which is what a click there should set — and with the explorers on their way out, the dive has nowhere to go. Nothing replaces the gesture yet, so for the next couple of commits a click in the focused main view does nothing. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
With both implementations gone, nothing is left that lets a side panel handle a click in the focused main view, so the mechanism for attaching one to a context can go as well. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both callers pass a main context, and focusing one is about to need more of it than the Context interface offers. Saying so in the signature also retires the type assertion that was there only to reach ClearSearchString. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The count comes from the view's buffer, which a rendering task appends to on its own goroutine, so reading it without the write mutex is a data race. Nobody called it until now, which is why nothing has tripped over it; the next commit does, from the UI thread while a render is still loading. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ReadToEnd holds a gocui task while it reads, so that lazygit doesn't count as idle while something waits on the result. That has nothing to do with reading all the way to the end, and the next commit needs it for a bounded read too, so the two are pulled apart. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The focused main view could be scrolled but not pointed at: there was no way to say "this line" or "these lines", which is what every line-level interaction needs — editing a line, copying part of a diff, and, later, staging. So a diff main view now always carries a selection while focused, starting at the first change already on screen so that focusing doesn't move the view. The mode of the selection lives on the main context (a single line, a range from a fixed anchor, or the change block around the cursor); the selected line and the range anchor stay in the view itself, whose native range select draws them, so there is no new highlight machinery. The modes and their keys are the ones the staging view has, including that the arrow keys step from block to block in hunk mode. A pane holds something to select only when what it is showing is a diff with changes in it, which the render works out once its content is final: a placeholder message is not a diff, and neither is a diff with nothing in it at all, such as a binary file's or an empty commit's. Whether a selection is then drawn there follows from the context stack, as it does for every other view. A click in the focused main view now has a meaning again: it selects the line it points at. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mode of a diff view's selection lives on the context of the pane showing it, and until now only the controllers that drive the selection needed to name that pane, all of which have it as the concrete context it is. A side panel is about to be handed the pane a command was invoked in, so that it can act on what is selected there — and it can't be handed a context, the interface it is handed one through being declared where a context's concrete type isn't available. So the state moves to where such an interface can speak of it, and the panes gain one: a context that has a diff selection. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Focusing the main view is about to establish a hunk selection, and that happens in a free function shared with the controller that focuses the main view from a side panel, which has no main view controller to hand. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Users who set hunk mode as their default get it when entering the staging view, and the focused main view is on its way to replacing that view, so it has to behave the same: focusing selects a whole change block, and clicking a change line selects that line's block, ready to act on. A click on a context line still selects only that line, since the click points at it precisely — you may well want to edit it. A click inside the block that is already selected does the same, giving hunk mode up and going back to line by line. That keeps a single line reachable with the mouse in a block too long to see the end of, and matches how a click inside a range selection already collapses it. The block offered up is the first one that begins on screen, so that its whole extent can be seen before acting on it. Only when none does — a change too long to fit on the screen — is the block that reaches into the view from above taken instead, and the selection then extends to its first line off screen: focusing a diff must not move it, so nothing here scrolls. The staging view makes one exception, and so must we, or a file that is one solid block of changes (a file you just created, or deleted) would come up with all of it selected. The staging view asks the parsed patch; we ask the rendered diff the same question, which needs no second git invocation and works over a whole commit's diff, where the answer differs per file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selecting a range with the mouse is the obvious gesture once clicking selects a line, and it's the only way to get a range without knowing the keybindings. The range has to be anchored where the mouse went down, which the view can't tell us: a click in hunk mode selects a whole block, leaving the view's own range anchor at the block's far end, so a drag from there would grow the selection from the wrong end. So the clicked line is remembered when the click happens, and the drag anchors there. A drag that reaches the edge of the view keeps scrolling, using the same autoscroller the staging view does — mouse capture means the pointer can be dragged past the edge, and there is more diff down there than fits on screen. Unlike the staging view, whose content is a string that is always there in full, this diff loads lazily, so scrolling down has to keep reading it in or the autoscroll would stop at the loaded edge. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading a diff of any size means moving in bigger steps than a line at a time, which the staging view offers and the main view didn't. So the same hunk keys work here, and n / N step from file to file — worth having only here, since a diff spanning several files is something the staging view never showed. Where "the next file" begins isn't in the text: a diff renderer may print whatever it likes above a file's content. So navigation lands on the first row that states which file it belongs to, which is the file's header when the source says so and its first content line otherwise. The anchor's own file is found by scanning down rather than to the nearest row either way — having just landed on a header, the nearest row above belongs to the file we came from, and taking it would send the next press back where we started. A file you go to is brought to the top of the view, since the file is what you went there for and the more of it is on screen the better. That only applies where the view has to scroll at all: a file already on screen leaves it where it is. In hunk mode what ends up selected is the file's first change rather than the row the file begins at, and a large context size can put that change further down than a screenful; the selection is scrolled into view afterwards as any other jump's is, and the alignment gives way where the two can't both hold. The diff loads lazily, so a target below the loaded portion isn't there to be found; rather than doing nothing, we read the rest in and look again. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading a diff is often how you notice something to fix, and the file and line are right there in front of you — so pressing edit opens the file at the line under the selection, as it does in the staging view. The line number the diff shows is the line number in the version the diff is of, which for a commit's diff is not where that line sits today, so it is carried forward the same way clicking a diff-renderer hyperlink already does. A file header names no line, so it just opens the file. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Now that a diff view always carries a selection, the highlight fights the diff itself for the line's colours. Painting the selection across the whole line takes over the background, and a selected hunk becomes one solid block with no boundary between what was removed and what replaced it — the more lines you select, the less you can read. This isn't specific to renderers like delta that say which side of the diff a line is on by colouring its background, though they suffer most: git's own output puts red and green text on that background, which reads badly too. Since there is no rendering of a diff that a full-width highlight doesn't degrade, there is nothing here worth configuring — the bar is simply what diff views use. Left edge only, and two columns wide: every convention for marking a row of coloured content — change bars, diff gutters, selection gutters — puts the marker on the left, and bracketing both edges reads as framing instead. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Search the diff, then move the selection with the arrow keys, and pressing "n" goes to the match after the one the search was last on rather than to the one after the selection. So walking through the diff and then picking up the search where you got to takes as many presses as there are matches behind you. List views and the staging view keep the current match in step with their selection; do the same here. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
stefanhaller
force-pushed
the
select-diff-lines-in-main-view
branch
from
September 25, 2026 10:14
fdf0d40 to
81d206f
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third PR in the stack of PRs towards staging hunks directly from the main view. This one is stacked on #6029.
The focused main view is already reachable with
0or by clicking it. This PR gives it a real selection and line-level commands, so that a diff is something you move around in rather than only scroll.A diff shown in a focused main view now always carries a selection, anchored at the first visible change line when you focus it. There is no toggle and no config for it. Non-diff main views, such as a branch's log, keep no selection.
Keys in the focused main view:
↑/↓move the selection,vstarts a range,atoggles between hunk and line-by-line selection,shift+↑/shift+↓extend. These all work the same as in the staging panel.←/→jump by hunk (change block),n/Nby file. Both new keys have config entries,keybinding.main.nextFileandkeybinding.main.prevFile, defaulting tonandN.eopens the selected line in your editor. Over a file header it opens the file without a line.The selection is drawn as a narrow bar at the left edge, two columns wide, not across the whole line. Now that a diff view always carries a selection, a full-width highlight takes over the line's background, and a selected hunk becomes one solid block with no boundary between what was removed and what replaced it. Renderers that say which side of the diff a line is on by colouring its background suffer most, but git's own red and green text reads badly that way too. Since there is no rendering of a diff that a full-width highlight doesn't degrade, there is nothing here worth configuring.
This PR by itself is not very useful yet, so it shouldn't be released on its own; you can't do anything with the selection except press
efor edit. It will take us several more PRs until you can actually stage the selected hunk.