git status doesn't descend into a nested repository, such as the
directory that a removed submodule leaves behind. It reports the
directory as a whole, with a trailing slash. The file tree splits paths
at slashes, so it showed a directory with a file without a name in it.
Discarding the directory removed it, but then the cleanup of emptied
directories took the parent of "modules/sub/" to be "modules/sub"
itself, and failed to read it.
Take the slash off when loading the files. This also covers any
untracked directory if status.showUntrackedFiles is "normal", and linked
worktrees inside the repo. Their slash was taken off when they were
marked as worktrees; this is no longer needed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If the repo or linked worktree that lazygit has open is deleted while
lazygit is running, every git command fails. lazygit no longer panics
since the previous commit, but it shows empty lists, and there's
nothing useful left to do in it.
Check at the start of every refresh whether the worktree and its git
dir still exist. If one of them is gone, quit and print its path in
the terminal. The background files refresh does this check too, so
lazygit quits within ten seconds even if nobody is using it. To quit
from a refresh, return an error that wraps gocui.ErrQuit; this keeps
it out of the error popup.
Switching to another worktree or to a recent repo would be an
alternative, but it would be more surprising than quitting. Also,
there isn't always a main worktree to switch to.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The next commit makes performRefresh return early right after the
capture, and it must not leave the input blocked when it does.
This doesn't let any keypress slip through before the block begins.
Only refreshes on the UI thread block input, and there the capture
runs synchronously.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
time.AfterFunc and errgroup.Group.Go run the function they are given
on a goroutine of their own, so a panic in it leaves the terminal in
raw mode, like a panic in any other goroutine that doesn't use
utils.Safe.
Add utils.SafeFunc and utils.SafeFuncWithError, which return a copy
of a function that runs through utils.Safe or utils.SafeWithError, and
use them for the functions given to time.AfterFunc and errgroup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Most goroutines run their function through utils.Safe, which closes
the screen if the function panics. Without it, the panic is printed to
a terminal that is still in raw mode and on the alternate screen, and
the terminal needs a reset afterwards. Go has no process-wide hook for
panics, so every goroutine has to do this itself.
Use utils.Safe for the goroutines that don't use it yet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under a diff renderer that states which diff line each row shows, such
as delta, the queries about the lines of the main view read the rows by
those records alone. Still, they took the text of every row as well,
and for a diff of 300,000 lines this was most of the time each of them
took. Moving the selection by change block runs two of them for every
key press.
Take the records of the rows first, and their text only if no row
states a diff line.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every query about the lines of the main view, such as moving the
selection to the next change block, finds out which line of which file
each row shows. For every row, this joined the path that the diff
states with the path of the worktree. In a diff of 300,000 lines, this
took more than half of the time of the query.
The lines of a diff come file by file, so remember the last path that
was made absolute and reuse it while the path stays the same. Do the
same in the other places that go through every line of a diff. A query
over that diff now takes about 27 milliseconds instead of 63.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no diff renderer configured, opening the jump-to-file menu for the
diff of a commit that adds or deletes a file of 100,000 lines takes
more than 20 seconds, and so does picking a file from it. In such a
diff, moving the selection by change block takes as long for each key
press. All this time the UI is frozen.
To find out which line of which file each row of the main view shows,
lazygit parses the view's content as a diff. For every line of a file's
diff it asks the parsed patch for the line's number in the new and in
the old file, and each of these calls counts the lines of the hunk
before it. So parsing takes time quadratic in the length of a hunk, and
the diff of a new or deleted file is a single hunk. For a diff of
200,000 lines, one parse took about 10 seconds, and opening the menu
parses it twice. A diff renderer that states which line each row shows,
such as delta, doesn't go through this parse.
Number all lines of the patch in one pass instead. Parsing that diff
now takes about 40 milliseconds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With gui.scrollPastBottom set to false, picking the last file of a long
diff from the jump-to-file menu of the commits panel scrolls that file
to the top of the view. This leaves blank space below the end of the
diff.
A jump in a pane that isn't focused puts the file at the top of the view
and leaves it to the layout to stop at the end of the diff. But the
layout doesn't do this while the view is loading. Picking an item from
the menu closes the menu first, and the commits panel starts a re-render
of the diff as it gets the focus back. So the view is loading when the
jump happens, and the layout corrects the position only in a later pass
after the re-render has finished. For a long diff the re-render never
finishes; a later commit fixes this. Clicking a file in the diffstat
doesn't close a menu, so it doesn't have the problem.
Stop the jump at the last screenful of the diff, as a jump in the
focused pane does. Like that one, it stops there whatever
gui.scrollPastBottom is set to.
The diff in click_a_file_in_a_diff_stat_that_comes_first fits on
screen, so the jump doesn't scroll it any more. Make it longer than the
view.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A jump in a pane that isn't focused is going to stop at the last
screenful of the diff as well.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If you press `e` on a file of a commit, and a later commit deleted that
file, the editor is opened on a path that doesn't exist. Most editors
then create it as a new, empty file. This is confusing. The same
happens with `o`, and with any other file that the worktree it is
opened from doesn't have. An example is a file that is added in a branch
that isn't checked out anywhere, since it is opened from the current
worktree.
Show an error instead of opening the editor. For a range selection in
the commit files panel, show it only if none of the selected files
exist. Otherwise open the existing ones and leave out the others
silently; an error shown only after returning from the editor would
come too late to be useful.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If the commit files panel was entered from a branch that another
worktree has checked out, "Copy absolute path" in its copy menu gives
the file's path in the current worktree. Make it give the path in the
worktree that has the branch checked out. This is the file that `e` and
`o` open, and the one users most likely want the path of.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Do the same for `o` in the commit files panel as the previous commits
do for `e`. If the panel was entered from a branch that another worktree
has checked out, open the file from that worktree.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If you enter a branch in the branches panel and press `e` on a line of
a commit's diff, or on a file in the commit files panel entered from
there, lazygit opens the file in the current worktree. If another
worktree has the branch checked out, this is the wrong copy of the
file. The current worktree may have it in a different state, or not
have it at all. Clicking a lazygit-edit:// hyperlink of a diff renderer
in such a diff has the same problem.
Open the file from the worktree that has the branch checked out
instead, and carry the line number forward to the file as that worktree
has it.
If no other worktree has the branch checked out, the file is still
opened from the current worktree.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three places open a file of a diff in the editor: `e` and alt-click in
the main view, the lazygit-edit:// hyperlinks of a diff renderer, and
`e` in the commit files panel. The first two also each carry a line of
the diff forward to the file's current line before opening it.
Give them one pair of methods to call. The next commit changes where
these files are opened from, and can then do so in one place.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Going to the next or previous file of a diff, or picking one from the
jump-to-file menu or the diffstat, brings that file to the top of the
view. The user goes there to read the file from its start, so scroll the
view back to the left edge as well. Jumps between the hunks of a diff
keep the horizontal scroll position, as moving the selection does.
Nothing scrolls the main view horizontally yet, so this changes nothing
on its own. It prepares for scrolling the focused main view sideways
with H and L.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two places that position the main view after a re-render of the same
diff call SetOrigin(0, y), although they only mean to move the view
vertically. One is the restore after a change of the context size, of
whitespace handling or of the diff renderer. The other is the clamp for
content that ends above the scroll position, for example after staging a
large part of the diff. Use SetOriginY in both, so that they keep the
horizontal scroll position.
Nothing scrolls the main view horizontally yet, so this changes nothing
on its own. It prepares for scrolling the focused main view sideways
with H and L.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If diff.mnemonicPrefix is set, pressing space on a line in the focused
main view does nothing. The command log shows "Apply patch", but the
line is not staged. Other commands that read a path out of the working
tree's diff go wrong as well, such as jumping to a file. Building a
custom patch from a commit's diff still works, because git uses
mnemonic prefixes only when one side of the diff is the index or the
working tree.
With this config, the paths in the working tree's diff start with i/
and w/ instead of a/ and b/. We only strip a/ and b/, so the path we
read out of the diff header is not a file of the files panel, and we
skip the line without a message.
Users who set the config want to see these prefixes, so keep them.
Strip the first component of the path instead, whatever it is. `git
apply` does the same by default, and ParseableDiffPrefixes makes sure
there is such a component.
A section without +++ and --- lines needs more than that, because we
find the new path on its "diff --git" line by searching for " b/".
Take the path from the "rename to" or "copy to" line instead where
there is one. Otherwise the two paths on the "diff --git" line are the
same. Split the line where the two halves match after stripping their
prefixes, like `git apply` does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The "rename to" and "copy to" lines of a diff header spell a path the
same way as the other header lines, but without a prefix. The next
commit reads paths from these lines too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The rule below the header of a branch's pull request is drawn as wide as
the main view. Its width is read when the render is asked for, before
the layout has settled it. If the layout then changes the width of the
main view, for example because it unsplits the view for the content
that comes next, the rule comes out as wide as the view used to be.
Make a prefix a function that is called with the width the render is
laid out to, once the layout has run. The function it returns produces
the text on the render's own goroutine before the command starts. This
way a prefix that takes a while to produce, such as one that runs
commands of its own, holds up only the render and not the UI.
All prefixes are still static here; the next commit lays out the rule
of the pull request header to the width.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A diff opens with a diffstat naming every file in it, right above the
diff of each of them. Selecting a commit puts that list in front of you,
naming the same files the menu of the diff's files offers, and the menu
is still the only way to any of them.
Make each of those names a link that goes to where that file's diff
begins, as picking it from the menu does. The panel keeps the focus, so
a file of the commit being read is a click away and the selection stays
on the commit.
The names are found in the output as it is written to the pane, for next
to nothing. The diffstat is git's own text whichever renderer the diff
goes through — delta, diff-so-fancy and difftastic all pass it on
untouched — and it comes first, so the scan for it ends with it and
nothing below is looked at.
The link states the name as the diffstat does, and which file that names
is worked out on the click, against the files the diff turned out to
hold. That is the point at which a name the diffstat cut off behind
"..." or compacted to the "{old => new}" form of a rename can be
recognized at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
n and N step through the files of a multi-file diff one at a time, which
is a long way to the far end of a commit touching a hundred files.
Bind ctrl+g to a menu of the diff's files, in the order the diff shows
them and by the paths the repo knows them by; picking one goes to where
its diff begins, exactly where stepping to it with n would have left
you. The menu filters as you type, so the file you have in mind is a few
characters away however many the commit touches.
The key works in the panel the diff belongs to as well as in the diff
itself. A commit is read from the commits panel, so being able to jump
from there saves focusing the diff and leaving it again for the next
commit; the diff scrolls to the file and the focus stays in the panel.
It is ctrl+g rather than f because a key that works in every panel has
to be free in all of them, and f is fetch in the files panel and fixup
in the commits panel.
The command applies only while the main view is showing a diff, and is
left out of the keybindings menu where it isn't: over a branch's commit
log, or while a conflicted file has given the main section over to the
merge conflicts view.
The diff is read to the end before the menu is built, as searching it
does: a file below the part that has been read is in neither the list
nor the view. Each item names its file rather than the row that file
begins at, so that a diff re-rendered while the menu is up is jumped
into at the row the file begins at now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Where a focus or a click puts the focused main view's selection is
worked out in the diff line helper; where a jump puts it is worked out
on the main view controller, though it is the same question about the
same pane. A jump asked for from anywhere else — from a panel below the
pane, or from a click on a link in the diff — has no way to reach that
answer.
Move it over. The controller keeps placeNavigationTarget as the short
way to say it for the jumps it makes itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
File navigation works out where the neighbouring file begins by walking
the rows itself, forwards or, more laboriously, backwards. A menu of the
diff's files needs the same rows, all of them at once.
Extract fileStarts, which answers that for the whole diff, and have
navigation pick its neighbour out of the answer. The two then agree on
where a file begins by construction, and the walk backwards over a file
goes away.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The identity of a row is reported in the repo's terms already: the pane
previewing the custom patch shows a diff of the two trees the patch was
materialized into, and the paths of those trees are mapped back to the
repo's files before anything sees them. Asking which file a row belongs
to, which file navigation does, was the one query that skipped that step
and answered with the tree's path.
Pull the mapping out of inRepoTerms so that it can be applied to a bare
path, and put filePaths through it. A menu listing the files of the diff
will want to show them by name; where a renderer states the path of each
side of a change, this also has both halves belong to one file rather
than to the two trees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two panels offer a branch's pull request today, and each looks it up in
the model's map itself and builds the same disabled reason from the same
string. The focused main view is about to offer a line of the diff in
that pull request, which would make three.
Put the lookup and the disabled reason on the host helper, beside the
pull request URL it already builds, and have both panels ask it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hunk defaults and wrapping no longer belong to a staging panel. Rename both
public keys with automatic migration, and remove panel-era English strings so
config, UI, schema, and generated docs describe the surviving interface.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Nothing routes to the explorer contexts after line actions and file entry
moved into the normal main pair. Delete their contexts, views, main-pair
wiring, discovery API, test drivers, and selection-state package so the
surviving diff view is the only implementation.
Their names go from the contexts a custom command may bind to as well, so
that a config still naming one is reported as the config error it now is
rather than taken as a context we simply failed to find.
Co-Authored-By: GitHub Copilot <copilot@github.com>
Focused main views now own selection, staging, patch construction, copying,
and context-size rerenders. Remove the parallel controller/helper stack and
its refresh scopes, while retaining custom-patch reset as mode behavior.
Co-Authored-By: GitHub Copilot <copilot@github.com>
The hint was raised on entering the staging panel, once, to explain that
the selection mode there had changed and how to get the old one back. That
panel is gone, and with it the moment the hint was tied to; what is left is
a string nothing prints and a flag nothing reads.
The advice it carried is not lost: the option it names is documented, and
the key that switches modes is on screen while a diff is focused.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Space in the pane previewing the patch had nothing to act on: everything
there is in the patch already, so it can only mean taking those lines back
out, in the way that space in the staged side of the working tree's diff
only unstages.
A line of the patch can't be found by its number: the patch renumbers
whatever follows a change it leaves out, so a line of it names a line of
the commit's diff that isn't the one shown. A line is identified instead
by its position among its file's changes, counted in the diff the patch is
shown as. That is the same position it has among the changes the patch
holds for the file, since both are in the order the file has them. This
holds however the rendering came out, so a renderer that groups a hunk's
deletions before its additions, or leaves a change out altogether, no
longer matters.
The pane's rows are stated in terms of the trees the patch was
materialized into, so the paths come back to the repo's own before
anything is looked up in them. This also lets the lines there be copied
and edited, as the lines of a commit's diff can be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A patch built from what is on screen has to show what is in it, over the
diff those lines were taken from — which may be a diff renderer's
rendering of it, whose bytes are none of ours to touch.
So the marks are drawn in a gutter over the content, from the lines the
patch holds rather than from where they were drawn last. They are worked
out again whenever a pane's content settles and whenever the patch
itself changes. Working them out as the content settles keeps them right
across a renderer switch, a change of context size, or a walk through
the commits.
They are shown whenever the main view shows the diff the patch is built
from, whether or not it has the focus. The pane beside the diff previews
the patch all the while, and marks that came and went with the focus
would look like a bug while browsing. A diff of any other commit gets
none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The five panels that show a commit's diff now offer on it what the patch
explorer offers on the one file it can show at a time: space takes the
selected lines into the custom patch being built, or back out of it again,
and d takes them out of the commit itself.
So a patch can be built straight from the diff already on screen, over
several files at once and without entering the commit's files first — and
from a stash entry or a reflog entry too, whose diffs the explorer could
reach but which had no patch preview of their own to show for it.
The two arrive together because they are one answer to what a commit's diff
offers, and because d needs exactly what space builds: a patch of the
selection, which the rebase then removes from the commit. Which of the two
space is doing is decided by the first selected line, as toggling a file in
the commit files panel is, and the selection then moves past the lines
dealt with — they are still in the diff, a patch leaving the commit alone,
so a second press would otherwise take them straight back out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing lines from a commit, moving a custom patch out of one, undoing
either — none of it goes through the focused main view, so a selection over
that commit's diff was left where it was, painted over a rendering of a diff
that no longer exists.
Every render now asks whether it is showing a different diff than the one on
screen, which is what a rewritten commit looks like, and puts the selection
back on the change that has taken its place — the same reveal an action in
the view does for itself. It stands down for a render of the same diff,
where a selection, perhaps a range still being made, is exactly right as it
is, and for one that something more precise is already waiting to place.
The key a render is remembered under moves ahead of anything that might
change the command's arguments, so that it says which diff is being
rendered and nothing else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It is built entirely on that helper, and the render chokepoint that is about
to want it lives in the gui package, which cannot reach a controller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Editing a hunk points an editor at a patch we wrote to a temp file and
waits. Nothing about the repo has changed when the editor returns: the
change lands when the caller applies what came back. But the suspend path
this went through refreshes as soon as the subprocess exits, so it reads
the state from before the patch is applied and then races the caller's own
refresh to publish it. Whichever lands last wins, and when it is the stale
one the files panel goes on showing the file as it was before the edit
until something else refreshes.
The caller is the one that knows when there is something new to see, and
both callers already refresh once they have applied the patch, so the
refresh in the middle only ever had a wrong answer to give.
Being a publish-order race, it doesn't reproduce reliably enough for a
test; holding a background refresh between its read and its publish shows
it every time.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two space presses in quick succession only staged one hunk. Input is
withheld until the refresh has landed, which is enough where the diff is
rebuilt on the spot, but the main view re-renders asynchronously: the
selection only moves to the next change once that render is on screen, so
the second press acted on lines that were no longer in the diff, and
staged nothing.
So the wait is now for the selection to be where the work carries on from,
rather than for the model. A restore therefore has to say when it is done —
which it can be either way, since a view given a message rather than a
re-render now gives up the restore it was holding instead of leaving it to
claim some later render.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
EndBlockingEvents dispatches the keys buffered during a block from inside its
own call, so a keybinding handler runs in the middle of whatever the caller was
doing. If a caller ends the block partway through updating the screen, that
handler acts on state the caller has yet to finish writing.
No caller does that today; both of them end their block from a UI-thread
callback of their own, with nothing left to do afterwards. The commit after this
one adds a caller that can. It ends the block from a render restore, and a
render of the two main panes resolves that restore halfway through laying the
panes out.
Queue the replay through Update instead, so the buffered keys arrive on a later
pass of the loop, as they would have if the user had pressed them then. Input
stays withheld until that pass runs. Gui events are dispatched in preference to
queued work, so a key pressed in between would otherwise be handled ahead of the
keys buffered before it.
The replay's error now reaches the error handler along with every other
handler's, and EndBlockingEvents has nothing left to return.
Some merge conflicts can only be resolved by picking a side. The files
panel explains those rather than diffing them, and where one side deleted
the file and the other modified it, git's diff of that modification is
shown below the explanation. The focused main view took those change lines
for a diff of its own. It drew a selection over them and offered to stage
hunks of a file whose conflict staging can't resolve.
So have a render say whether it holds the diff the panel offers in the
main view, and put a selection only on one that does. Every diff render
already goes through NewMainViewDiffTask, so it says so for itself; the
custom patch preview, assembled as text rather than run as a command, says
so through NewMainViewDiffStringTask. Establishing a selection asks the
pane the same question rather than looking for change lines itself.
The selection has been wrong over this content since "Show a selection in
the focused main view" introduced it, and the fix belongs there. It lands
here instead because a render had no way to say what it holds until "Show
git's own diff when the renderer's can't be acted on" gave every diff
render one constructor to go through.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A diff renderer may lay a diff out however it likes: line numbers in a
gutter, the +/- column replaced by colour, the two sides in columns. Once
it has, we can only tell which line of which file a row shows if the
renderer says so. Under a renderer that doesn't say, the main view holds
a diff that can be read but not staged, edited or copied from. That is no
good now that the main view is where you stage.
So focusing it brings git's own diff instead, and every re-render while it
stays focused keeps to that, so staging a hunk doesn't flip back. Browsing
is untouched: you see what the renderer produced until you focus the view
to act on it.
Whether the renderer says anything is settled by asking it rather than by
watching it work: run it on empty input and see whether it announces the
protocol. Announcing is a property of the renderer, so the answer is known
before we render anything, and a diff with no lines to describe can't fool
it. Watching would have to see a diff go by first, and a binary file's diff
holds nothing that would tell the two cases apart. The answer is remembered
until the renderer changes. git itself is asked the same question, with the
renderer's own arguments, since it announces itself for exactly the formats
whose output can't be read back as a diff.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things about a diff command follow from what its output is for: whether
the diff renderer produces it, and whether it is coloured. That was a
`plain` flag, which covers two of the three cases — the diff as configured,
and git's own uncoloured diff for building patches out of — and leaves no
room for the third, which is about to be needed: git's own diff, coloured,
for showing where the renderer's version of it can't be acted on.
So the flag becomes a mode. It also takes over deciding the colour, which
each command spelled out for itself, and it settles a question the flag
couldn't put: whether ignoring whitespace applies. It is about what the user
wants to see, so it holds for anything shown, and not for a diff a patch is
built from, which has to describe every change.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each side of a file's diff has a pane of its own, but a pane is only shown
while its side has something in it: staging the last unstaged change takes
the upper pane away, and unstaging the last staged one takes the lower one
away. The focus follows into the pane that is left, and the selection has to
be waiting there when it arrives — on the lines just acted on, which is where
they are now, unless they were discarded rather than moved, in which case on
what is left of the file. The pane being moved to shows no selection until
the restore places one, so that the selection it was left with the last time
it was used doesn't appear for a frame.
Whether the acted-on side still holds anything is a question the model can't
answer: a refresh only queues its update, and by the time it lands the
re-render this has to ride is already under way. So the answer is worked out
from what we just did — the files we changed report whether the selection
covered all of their changes, and the ones it didn't touch are as the model
describes them.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing has read that since the paint started settling the scroll position
before consulting the restore: what the answer was for was deciding whether
the reset the new content was owed still had to happen, and by then it has.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Where the selection starts out, and how it widens to a whole change block,
are questions about what the view is showing — the same rendered diff the
queries next door read. Nothing about them belongs to a keybinding, and the
render funnel is about to need them too, from a layer that can reach a helper
but not a controller.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staging takes the lines it acted on out of the diff, so the selection has
nothing to sit on afterwards and would be left wherever those lines used to
be. What the user wants is the change that moved up into their place, so
that pressing the key again goes on to the next one — which is how staging
line by line through a file works.
The line acted on is gone, so it can't be remembered by identity the way a
re-render of the same diff remembers one; what is remembered instead is its
place in the sequence of the diff's changes, which the change after it
inherits. Staging the last change is the one case with nothing to inherit
it, and there the selection stays on the last change there is.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Putting a view back on a remembered line as it re-renders is two things: the
plumbing that watches the content arrive, reveals it at the right moment and
places the view, and the search that says which row to land on. Only the
second is specific to what is being remembered, and a second kind of it is
about to arrive — the change line an action leaves the selection on, which
is found by counting rather than by identity.
Behaviour-preserving.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Space in the focused main view now acts on the selected lines the way it
does in the staging view: over the working tree's unstaged changes it puts
them into the index, and over the staged ones it takes them back out. Which
of the two it does is a property of the pane, each side of the diff having
one of its own. Nothing has to be entered first, and a selection reaching
across several files of a directory's diff is applied as one patch per file.
The rows on screen are only a picture of the diff, so the patch is built
from the diff itself: each selected row's identity — which file, which line
of it, and whether it is a deletion — is looked for in the file's own diff,
and the lines that match are the ones the patch includes. Matching by
position rather than by counting rows is what tells the two halves of a
modified line apart, since the deletion and the addition replacing it sit at
the same place in the new file.
Panels other than the working tree offer no action on their diff yet, so
space says nothing and does nothing there.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>