Until now space toggled lines into a custom patch only from the commit files
main view (the per-file diff). Register the same toggle handler on the commits,
sub-commits and stash panels, so a patch can be built straight from the
whole-commit (multi-file) diff their main view shows, without first diving into
the commit files panel.
The handler lives on SwitchToDiffFilesController, which is already bound to
exactly those three panels and already knows how to derive the patch target
(from/to/reverse/canRebase) for the selected ref — pulled out of enter() into a
shared canRebase helper and the reused FromAndToForDiff. The post-toggle refresh
is the cheap one: these panels have no per-file patch indicator to update, so we
just re-render their own main + secondary views rather than reloading the whole
commit list on every keystroke.
Sub-commits and stash didn't render the secondary patch view at all; give them
the same secondaryPatchPanelUpdateOpts the commits panel uses, and recompute the
inclusion gutter as their diff (re-)renders, so the cumulative patch and the
gutter track the toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Escaping a patch explorer (staging / patch building) back to the focused main
view it was entered from used to replay a numeric scroll position and selection
index captured on the way in. But the reason to escape after staging or dropping
a hunk is that the content changed, so a saved index points at the wrong line —
and the host auto-advances the explorer's selection to a still-valid line anyway,
which is the line the user actually cares about returning to.
Restore by *patch identity* instead. On escape, read the (file, type, source
line) the explorer currently has selected, then have the main view's re-render
land on the row that matches it: scan the incoming content as it loads (the
inverse of the diff-line primitive), and once the matching row plus a screenful
below it have loaded, swap the off-screen render in and scroll to / select that
row in one step. FocusPoint with scrollIntoView centres the row only if it's
off-screen, so the common unchanged-content escape — where the row is already
where it was — doesn't move at all. If the line is gone (the content really
changed), nothing is forced.
This generalizes the scroll restore from a fixed origin to a predicate
(RenderRestore: FirstPaintReady decides when the saved position is reachable,
Apply re-establishes it), folding the separate selection restore into the same
first paint — so it no longer rides a post-load callback that could fire early.
The restore also now survives task replacement, which the numeric version did
not: a periodic refresh can stop the escape's re-render before it first-paints.
The pending restore is held on the buffer manager and is *not* cleared when a
task starts, so the replacement task picks it up. It is not gated on the command
key — staging the last unstaged hunk re-renders `git diff` as `git diff --cached`,
a different command, yet the line to land on is still in the new content — but
validates itself: the scan finds the target line only when the content still
contains it, so applying it to a different item is a harmless no-op. A task
clears it once it has applied it (found or not), so it lives for exactly one
re-render. Because the restore is anchored on content identity and is idempotent,
"survive replacement" and "restore by identity" are one mechanism, not two.
With the identity in hand the snapshot no longer needs the captured scroll/index;
they're derived from the explorer's live selection.
The focused main view's click/enter/e/G handlers all need the same thing: given
a rendered diff row, the patch-space line it corresponds to. Until now that came
solely from delta's lazygit-edit:// hyperlinks, which only carry a path and a
single line number — no side. That's lossy: for a deletion the number is the old
line, but the consumers fed it into new-file lookups, and two consecutive
deletions (which share a new-file line number) couldn't be told apart at all.
Replace GetFileAndLineForClickedDiffLine with GetDiffLineInfo, returning the
fuller (file, type, new-line, old-line) record from diff-line-metadata-notes.md.
This is mechanism #1: parse the decolorized view buffer — walk up to the file's
"diff --git" section, reuse patch.Parse on it (splitting multi-file commit diffs
on the "diff --git" boundaries), and read the type and line numbers off the patch
arithmetic. It serves the structure-preserving renderings — no pager, git diff
--color, and delta --color-only without line numbers — with no external
dependency.
To avoid trusting a mis-parse, the parser bails when a hunk's body no longer
matches its header (Patch.IsWellFormed). That's what happens when a pager keeps
the diff/hunk headers but restructures the body: delta's line-number gutters push
the +/- marker off the start of each line, so every body line reads as context.
Such renderings fall through to the next backend rather than yielding a confident
wrong answer. (diff-so-fancy goes further and rewrites the headers too, so it
fails even earlier, on the missing "diff --git".)
GetDiffLineInfo is a seam with swappable backends: the buffer parser first, then
the old hyperlink reader as a fallback for renderings the parser can't handle
(delta's default mode, or delta with line-number gutters). The future #2 OSC
per-cell metadata reader plugs in ahead of both, behind the same record shape.
Wire the consumers to the record per that doc's field mapping:
- dive into staging/patch building lands on the exact patch line, looking a
deletion up by its old-file line number (PatchLineForOldLineNumber) so the
two-deletions case resolves correctly;
- `e` edits at the new-file line;
- `G` anchors the PR link on the left (old) side for a deletion, the right (new)
side otherwise.
The hyperlink fallback can't convey the side, so it reports DiffLineOther, which
the consumers treat as a non-deletion — i.e. exactly today's behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diving into staging or patch building from a focused main view (by
double-clicking a line, or pressing enter on the selected line) always
landed on a single-line selection. Entering the same views through the
side panel honours the UseHunkModeInStagingView config and selects the
whole hunk by default. The two ways in should agree, so that diving in
from the main view feels like the established flow.
A non-negative line index in NewState was overloaded for two intents:
clicking directly on the patch explorer view (where a single-line range
is the start of a drag) and diving in from the main view (where we want
the default select mode). Distinguish them with SelectLineInDefaultMode
on OnFocusOpts: the main-view entry points set it; the click-to-drag
path does not.
In hunk mode the selection covers the block of changes around the
clicked line. A context line has no surrounding changes, so we snap to
the next change line (as toggling hunk mode does); the clicked context
line itself is then not part of the selection.
This is a separate commit only because the branch is a throwaway
prototype; in a real history it would be folded into the commit that
introduces the focused-main-view enter behavior rather than landing on
top of it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SwitchToDiffFilesController.enter calls SelectPath and Context.Push
right after a (SYNC, by default) COMMIT_FILES refresh. This works today
because the model write currently happens synchronously in the worker
before Refresh's wg.Wait() returns, but an upcoming commit will bounce
that write onto the UI thread instead, at which point wg.Wait() no
longer guarantees it's been applied, and SelectPath would operate on a
stale tree.
Move both calls into Then ahead of that change, for the same reason as
the earlier FILES-scope commit: Then is already queued via OnUIThread,
so this is behavior-preserving on its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When this was originally introduced, it handled single clicks on a list entry
(treating them similar to a double-click by checking whether the click was on
the selected entry). Arguably it should have been called OnDoubleClick back then
already; but when we later changed it to do actual double-click detection (see
37197b8e9a), we should have renamed the methods.
Change working tree files and commit files panels to use filtering
(reducing the list) instead of search (highlighting matches). This
matches the behavior of other filterable views.
The text filter matches against the full file path, not just the
filename, which is more useful for navigating large directory trees.
When toggling a directory for a custom patch while a text filter is
active, only the visible filtered files in the directory are affected,
consistent with how staging a directory in the files panel works.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refresh is one of those functions that shouldn't require error handling (similar
to triggering a redraw of the UI, see
https://github.com/jesseduffield/lazygit/issues/3887).
As far as I see, the only reason why Refresh can currently return an error is
that the Then function returns one. The actual refresh errors, e.g. from the git
calls that are made to fetch data, are already logged and swallowed. Most of the
Then functions do only UI stuff such as selecting a list item, and always return
nil; there's only one that can return an error (updating the rebase todo file in
LocalCommitsController.startInteractiveRebaseWithEdit); it's not a critical
error if this fails, it is only used for setting rebase todo items to "edit"
when you start an interactive rebase by pressing 'e' on a range selection of
commits. We simply log this error instead of returning it.
Apparently this was an attempt at working around go's lack of default arguments,
but it's very unidiomatic and a bit confusing. Make it a normal parameter
instead, so all clients have to pass it explicitly.
We make the name of the GetSelectedRefRangeForDiffFiles very specific on purpose
to make it clear that this is only for switching to diff files, so the
implementations can make assumptions about that (unlike GetSelectedRef, which is
used for different purposes and needs to stay more generic).
Right now it doesn't do very much yet, but it's still worth it even in this
state, I'd say. The function is going to become a bit longer in the next commit,
though.
The three nested `if` statements may looks strange, and you might wonder why we
don't have single one with &&. The answer is that later in this branch we will
add an `else` block to the middle one.
viewFiles is only called from enter; it doesn't make much sense to fill in a
SwitchToCommitFilesContextOpts struct to pass it to viewFiles for this one call.
Simply inline viewFiles into enter and get rid of all that.
I found this indirection confusing when reading the code. It looks like
SwitchToDiffFilesController is instantiated with different such contexts, but
it's always Contexts.CommitFiles, so just use that directly.
Something dumb that we're currently doing is expecting list items
to define an ID method which returns a string. We use that when copying
items to clipboard with ctrl+o and when getting a ref name for diffing.
This commit gets us a little deeper into that hole by explicitly requiring
list items to implement that method so that we can easily use the new
helper functions in list_controller_trait.go.
In future we need to just remove the whole ID thing entirely but I'm too
lazy to do that right now.
We want to show an error when the user tries to invoke an action that expects only
a single item to be selected.
We're using the GetDisabledReason field to enforce this (as well as DisabledReason
on menu items).
I've created a ListControllerTrait to store some shared convenience functions for this.
The only time we should call SetSelectedLineIdx is when we are happy for a
select range to be retained which means things like moving the selected line
index to top top/bottom or up/down a page as the user navigates.
But in every other case we should now call SetSelection because that will
set the selected index and cancel the range which is almost always what we
want.
This is a pickle: initially I wanted it so that a filter would cancel automatically if the current context lost focus.
But there are situations where you want to retain the focus, e.g. when a popup appears, or when you view the commits
of a branch. The issue is that when you view the commits of a branch, the branches context is removed from the context
stack. Even if this were not the case, you could imagine going branches -> sub-commits -> files -> sub-commits, where
in that case branches would definitely be off the stack upon navigating to the files context.
So because I'm too lazy to find a proper solution to this problem, I'm just making it so that filters in side contexts
are retained unless explicitly cancelled.
There's another edge case this commit handles which is that if I'm in the sub-commits context via the branches context
and start a search, then navigate to the reflog context and hit enter to get to the sub-commits context again, I need
to cancel the search before I switch. Likewise with the commit files context.
We have not been good at consistent casing so far. Now we use 'Sentence case' everywhere. EVERYWHERE.
Also Removing 'Lc' prefix from i18n field names: the 'Lc' stood for lowercase but now that everything
is in 'Sentence case' there's no need for the distinction.
I've got a couple lower case things I've kept: namely, things that show up in parentheses.
This begins a big refactor of moving more code out of the Gui struct into contexts, controllers, and helpers. We also move some code into structs in the
gui package purely for the sake of better encapsulation