The focused-main-view rework made the secondary pane actionable, but the old
patch-building explorer's secondary was inert, so its actions were never thought
through. Pressing space there routed through the same toggle handler as the main
pane, resolving the selection against the secondary's diff and mapping it to
patch-builder indices by line number. But the secondary shows the *aggregated*
custom patch, which renumbers included additions whenever an earlier addition in
the same hunk is excluded (Transform recomputes each hunk's +start). So the
shifted number resolved to the wrong line in the original diff — often adding an
unrelated line instead of removing the selected one.
Resolve the secondary selection by its *ordinal* among the change lines shown
instead: the custom-patch view renders exactly the included change lines in
order, so the k-th change line of a file is that file's k-th included change line
(PatchBuilder.IncludedChangeLineIndices), independent of the renumbering. Space
in the secondary now only ever removes, mirroring how space in the staging view's
staged pane unstages.
Discarding from the commit (the remove key) makes no sense in the custom-patch
preview — it would act on lines shown only as the patch, and space already
removes them — so it's disabled there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discarding lines from a commit via the focused main view installed its own
revealSelectionAfterPrimaryAction before the rebase. That rebase rewrites
the commit, so the selection-preserve net now re-establishes the selection
as the diff re-renders — with the same anchor (the selection's first line),
making this install redundant. Files-discard keeps its own reveal: its diff
command is stable, so the net never fires there, and it has staging's
focus-follow to the secondary pane besides.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dropping a hunk with `d` re-establishes the focused main view's selection
on the next surviving change, which feels great. But other operations that
rewrite the commit under the focused main view — moving a custom patch out
into the index, undoing right after a discard or a patch move — don't run
through the focused-main-view action handlers, so nothing was preserving
the selection. The stale gocui selection was left painted over the new
content, often as a large, now-meaningless range.
Rather than teach every such command to capture and restore the selection
(move-patch, undo, redo, and any future one), the focused main view now
preserves it itself, by its change-line ordinal, as the diff re-renders —
the command-agnostic counterpart of revealSelectionAfterPrimaryAction. The
diff side panels call it from their render-to-main before triggering the
render, so the restore rides the re-render.
It stands down unless the focused main view is current and shows a
selection, no precise restore is already pending (escape / post-stage
reveal / context-size place the selection more precisely), and the diff
command is actually changing. That last gate matters: a plain background
refresh re-renders the same commit's diff unchanged, and there the
selection — range and all — must be left alone; only a command change
(e.g. a rebase rewriting the commit's hash) means the content moved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SHAs go stale on every history rewrite of this throwaway branch; describe
commits by what they did instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver fix is a no-op for the old staging panel, not a tightening: that
panel anchors its drag at the click on mouse-down, so it never exposed the
bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Click-and-drag in the focused main view turns a hunk selection into a range,
but it was anchored at the change block's far end — where selecting a hunk
leaves the range anchor — rather than the clicked line. And on a context
line, where the click leaves no range anchor at all, dragging just moved the
single selected line instead of opening a range. Remember the line each
mouse-down lands on (the click can show a whole hunk, so it can't be read
back from the view) and, as the drag proceeds, anchor the range there while
the cursor end follows the mouse as gocui already moves it. Works for a click
that focuses the view and for one while it's already focused, on change and
context lines alike.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the diff re-renders for a new pager (or a changed context size),
PreserveDiffPositionOnRerender restored the cursor by patch identity but left
the selection's other end — the range anchor — pinned to its old view line. A
pager that restructures the diff (delta side-by-side is the clearest case)
then left the selection spanning the wrong range of patch lines. Remember the
far end by patch identity too and put it back the same way, so the selection
covers the same lines however the new pager lays them out. One mechanism now
restores both ends, covering hunk and range selections alike; a single-line
selection still needs only the cursor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking the staged/unstaged pane you weren't focused on selected a single
line the first time, even in hunk mode, because that pane's select mode was
still its default until it had been focused once (tabbing to it and back was
the workaround). Seed the clicked pane's mode from the one we're leaving so
the very first click there behaves like every later one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Once in hunk mode, clicking another change line reset the selection to a
single line, so you had to press `a` again for each block you wanted to
stage (a behaviour the main view inherited from the staging and patch-
building panels). Preserve hunk mode across clicks instead: a click on a
change line re-selects that whole block, while a click on context — or any
click when we weren't in hunk mode — drops to a single line, where the click
points precisely (e.g. to edit it).
The two click handlers shared an identical selection body, so unify them
into one helper and make the change in a single place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Focusing the main view by keyboard already selects a whole change block in
hunk mode; a click, though, only ever placed a single-line selection, so the
common "click the diff to stage this block" gesture still needed a follow-up
`a`. Now a click on a change line selects that line's block too. A click on
context still selects just that line, since the click points at it precisely
(e.g. to edit it with `e`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§10 now lists diff-so-fancy alongside delta and difftastic: the same #2 case as
delta's default (strips +/- markers, conveys side by color) but a line-oriented
Perl filter, unified-only, and -- the one wrinkle worth stating in the spec --
it strips terminal escapes from its content, so the record is prepended to the
line rather than embedded. Fix the now-stale "all three"/"both pagers" counts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the third pager emitter (diff-so-fancy, commit c397cd6 on its
prototype-osc-metadata branch): the simplest of the three -- a line-oriented
Perl filter, unified single-column only, the same #2 category as delta's
default. Captures the diff-so-fancy-specific findings: sanitize_display strips
OSC so the record is prepended not embedded; the path is derived from
$file_1/$file_2 (not $last_file_seen, which is empty for a noprefix deletion);
combined diffs skipped; counters are file-scope globals so a chunk-spanning hunk
keeps counting; classification mirrors strip_leading_indicators so the
no-newline marker is correctly skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add §4.4: a conforming pager emits a version-only OSC 1717 record (no further
fields) as its first output, so a host can probe it on an empty diff — which
emits no per-line records — and tell "speaks the protocol" apart from
"unsupported pager", content-independently. Used by the focused main view's
raw-diff fallback to decide whether a pager's output is actionable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A metadata-aware pager emits a version-only OSC 1717 record (no fields) as its
first output, to announce it speaks the diff-line-metadata protocol so lazygit
can probe for it. It isn't per-line metadata, so on a real render it must be
swallowed whole rather than lingering in the accumulator and attaching to the
following diff-header line (or, if newline-terminated, producing a phantom blank
line). Drop any OSC 1717 payload with no fields at its terminator; per-line
payloads always have fields, so they're kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the remaining diff panels — local commits, sub-commits, commit files,
stash, and reflog — into the focused main view's raw-diff fallback, the same way
the files panel already is: each computes DiffMainViewShouldRenderRaw and renders
its diff raw (no pager) when focused under a pager whose output we can't resolve,
so its selection stays toggleable into a custom patch (or, for reflog, navigable
and copyable).
The commit-diff panels build their diff through the diff helper, which can't
reach the staging helper, so the panel computes renderRaw and passes it in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prep for extending the focused main view's raw-diff fallback beyond the files
panel. ShowCmdObj, ShowFileDiffCmdObj, ShowStashEntryCmdObj and DiffCmdObj gain
the ignoreExternalDiff arg WorktreeFileDiffCmdObj already has — forcing git's own
coloured diff regardless of a configured external diff command. All callers pass
false, so behaviour is unchanged.
Also lift the pty-vs-command task choice out of the files controller into
types.NewMainViewDiffTask(WithPrefix), so the commit/stash/patch-building panels
(whose diffs are built in the diff helper, which can't reach the staging helper)
can select the same raw-fallback task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view is the staging surface, so it has to resolve the diff it
shows to patch-space. A pager that restructures the diff without emitting our
metadata (stock delta-default, plain difftastic, `cat -n`) produces output the
buffer parser can't read, which would leave its diff unstageable.
When such a diff is focused, re-render it raw — git's own colour, no pager — the
same content a no-pager setup shows, which the buffer parser handles. Browsing
keeps the pretty pager output; only focusing to act switches to raw.
Whether the pager is usable is decided by probing it: run it on empty input and
look for the version-only OSC 1717 handshake a metadata-aware pager emits first.
This is a pager-level, content-independent fact (a binary file, which has no
change lines under any pager, can't mislead it) and it's known before we render,
so we never render pretty only to discover mid-flight that we should have rendered
raw. The verdict is cached per pager (reset when the pager changes). No PTY is
needed — git needs a terminal to decide to invoke a pager, but the pager emits the
handshake regardless. A git-config external diff driver (useExternalDiffGitConfig)
is chosen per file via .gitattributes and a single diff can mix drivers, so
there's no one pager to probe; it's treated as unsupported (always raw).
Bypassing the pager needs two things, since a pager reaches the diff by two
routes: an external diff command (suppressed in the cmd via the new
ignoreExternalDiff arg, keeping git's colour, unlike plain) and a stdin pager
(GIT_PAGER, applied by the pty task — so the raw render uses a plain command
task instead).
This wires the files panel; the commit/stash/patch-building panels follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ctrl+o` in the focused main view copies the selection to the clipboard,
mirroring the staging view's copy. Unlike stage / discard, copying is the same
for every diff panel — it just reads the text shown — so it's a direct
MainViewController command rather than a FocusedMainViewActions method. That
keeps it free of per-panel duplication and lets it work over panels that have
no actions, notably the reflog.
The +/- prefix handling is metadata-aware where it matters: with no pager the
main view shows the raw diff, so a homogeneous selection has its +/-/space
column stripped (dropDiffPrefix) to ease pasting into code. With a pager
configured the rendered lines carry no such column, so stripping a leading
character would eat real content — copy verbatim instead. (Refining the raw
case to classify header lines via the diff-line metadata, fixing the
'--- a/file' edge case noted in 159bbb0825, is left for later.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view always showed a selection while it held focus, even when
its content was a placeholder ("No changed files", a merge-conflict message)
rather than a diff — so pressing a navigation key conjured a highlighted line
over "No changed files", and discarding the last change (or changes vanishing
outside lazygit) left a stale selection behind.
A selection should only show when there are change lines to act on. Two
moments establish or change the content:
- Focus: showInitialDiffSelection now leaves the selection off when the view
has no change lines (ViewHasChangeLines), rather than highlighting a stray
line at the top.
- Render: the side panel's render-to-main is where it decides between a diff
and a placeholder, so that's where the selection's visibility is set —
updateFocusedMainViewSelectionVisibility shows it only on the focused pane
and only when a diff is being rendered. This covers the refresh cases that
focus can't: discarding the last change, and changes disappearing or
reappearing outside lazygit, all hide or restore the selection on the next
refresh. (Focusing reuses the already-rendered content rather than
re-rendering, which is why focus needs its own check.)
Wired into the files panel, where placeholders occur; the commit panels always
render a diff. Adds a SelectionIsShown/SelectionIsHidden test assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the focused main view's `d` binding to a DiscardSelection action on the
side panel beneath, the discard counterpart of the primary action. As with
stage/toggle, what discard means is the panel's business:
- the files panel discards from the working tree, mirroring the staging
view's `d` — reverse-apply not cached on the unstaged side (destructive, so
confirmed), reverse-apply cached on the staged side (i.e. unstage);
- the commit panels (commit files, and the whole-commit diff of the local
commits / sub-commits / stash panels) remove the selected lines from the
commit via a rebase, mirroring the patch builder's "discard lines from
commit" — building a one-off patch from the selection and deleting it.
The commit-discard backend and its guard are shared free functions in
patch_building_from_main_view.go, paralleling the patch toggle. Discarding
from a commit is only possible on a local branch, so DiscardSelection grows a
disabled-reason companion on the FocusedMainViewActions interface: the `d`
binding is greyed with a reason on stash and other-branch sub-commits (never
rebaseable) and while a rebase is in progress, exactly as the patch builder
greys it. The files panel is always available (a zero-context diff is the one
error it reports inline, matching the staging view).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view's discard command (coming next) needs to read either
side's diff and apply a patch with an arbitrary direction and target: discard
from the working tree reads the unstaged diff but applies a reverse patch that
is not cached. stageDiffLines couldn't express that — it used a single reverse
flag both to pick which diff to read and to reverse the apply, and hardcoded
Cached: true.
The read side and the apply direction are independent (they coincide only for
staging and unstaging), so split them: applyDiffLines now takes sourceCached
(which diff to read) and a git_commands.ApplyPatchOpts (how to apply). Renamed
from stageDiffLines since it now covers stage, unstage, and discard. Staging
passes the same values as before, preserving behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each command a side panel handles in the focused main view needed its own
delegation channel: a HasKeybindings getter, an IBaseContext Add method, a
BaseContext field + getter, a baseController nil default, and an attach.go
registration — roughly five touch points per command. With three commands
(click, stage, toggle-patch) that was already a lot of boilerplate, and it
doesn't scale to the discard / copy commands coming next.
Replace the three channels with one: a side panel exposes a single
FocusedMainViewActions interface via GetFocusedMainViewActions (nil when its
diff offers no actions), and the controllers implement that interface
directly. The stage and toggle-patch handlers, already unified to a plain
error return, become one PrimaryAction method whose meaning is the panel's
business (stage for the files panel, patch toggle for the commit panels);
the click handler becomes OnClick. MainViewController is now a thin
dispatcher: fetch the actions from the panel beneath, call the method.
Adding a command is now one interface method, an implementation in the two
or three controllers, and a keybinding — no plumbing.
The toggle-patch channel did double duty as the "this panel builds a custom
patch" signal for the inclusion gutter (shown only beneath a patch-building
panel, not the staging files panel). Collapsing the channels removes that
proxy, so make the classification explicit on DiffMainViewContext, whose
marker method now returns a DiffMainViewType (none / staging / patch-building)
instead of being a bare marker. The gutter shows only beneath a panel whose
type is patch-building (commit files, local commits, sub-commits, stash).
Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view's space binding delegates to the side panel beneath
it, but the two handlers were asymmetric: the patch-toggle handler did its
own re-render and re-established the selection itself, while the staging
handler returned a focusViewName so the dispatcher (stageRange) could do the
reveal-and-focus dance on its behalf. That split blocks collapsing the
per-command handler channels into one, since the two have different return
types.
Make staging match the toggle: GetOnStageFocusedMainView returns plain
error, and the FilesController handler does the post-staging reveal and pane
focus itself. The reveal/select-mode logic the two handlers shared (collapse
a range to a line, preserve the change-line ordinal across the re-render,
re-expand a hunk) is extracted into revealSelectionAfterPrimaryAction, with
mainContextForViewName resolving a main view name to its context. The
dispatcher is now uniform: read the selected range, hand it to whichever
handler the panel registered.
Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The next commit adds patch building from the commits / sub-commits / stash
main views, which toggle the same way as the commit files main view; only the
patch target and the post-toggle refresh differ. Pull the panel-agnostic back
end (the toggle skeleton, the line toggle, the post-toggle reveal, the patch
filename mapping) into shared free functions so both controllers reuse it.
The skeleton now takes the patch target (from/to/reverse/canRebase) and a
refresh callback as parameters rather than reading them from the commit files
context and hardcoding Refresh({COMMIT_FILES}). The commit files handler passes
exactly what it did before, so behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the visibility model (gutter visible iff patch-active AND main
focused), the selection re-select after the split re-render, the
all-wrapped-segments marker, and the deferred pager-switch bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the fix for the §21.20 live-update gap: the patch toggle now
refreshes (updating the browser indicators + secondary patch summary +
the split), scroll/selection are preserved automatically because the
re-render uses the same diff command, and the gutter rides the same
re-render via the pipeline's own content-equality test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records what landed, the confirmed sync (no re-render) insight, what
needs interactive sign-off, the known limitations, and the 6c/step-7
next steps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pressing space in the commit-files focused main view now toggles the
selected line(s)/hunk into or out of the custom patch, instead of being a
no-op. It's the patch-building counterpart of working-tree staging from
the main view: the selection is resolved to change-line identities the
same way (ChangeLinesInViewRange), then mapped to the patch builder's
per-file line indices and added or removed.
The crux is that the commit's diff is unchanged by a toggle — only the
inclusion set changes — so unlike staging there's no async re-render to
ride. Membership is shown by an on-demand inclusion gutter: a reserved
left column painted with a marker on every change line currently in the
patch, recomputed synchronously after each toggle and on focusing the
main view, over the existing pager output. This is what lets patch
building work over a restructuring pager at all, where the old green
first-char overlay (which rewrites the rendered bytes) can't.
space routes to staging or patch building based on which handler the
panel beneath registers, via a new onTogglePatchFocusedMainView channel
(kept separate from staging because the post-action differs: sync gutter
repaint vs async reveal). Scoped to the commit-files panel for now;
commits/sub-commits/stash and the whole-commit multi-file diff follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prep for routing space to custom-patch building when the panel beneath
the focused main view is a commits/commit-files panel. stageSelectedLine
becomes the router; the staging-specific body (async re-render + reveal)
moves to stageRange. Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The patch builder works in patch-line-index space, which the patch
explorer can supply directly from its parsed-patch state. The focused
main view can't: it only knows a selection as diff-line metadata
identities (file line number + deletion?), and those indices differ
between the raw diff and however a pager renders it.
Add the conversion at the boundary so the main view stays in identity
space: PatchLineIndicesForLines maps identities to indices (for
toggling) and IncludedLineIdentities reports the included change lines
as identities (for the inclusion gutter to match rendered rows against).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A reserved left-hand column that draws a per-line marker glyph and shifts
the content right past it, for the custom-patch inclusion overlay (which
needs to show which change lines are in the patch over arbitrary pager
output, where it can't touch the rendered bytes).
It's pure draw-time decoration: the content buffer, and so the diff-line
metadata, click resolution and wrapping inputs, are untouched. The marker
is drawn on a line's first wrapped segment only, and the gutter narrows
the content wrap width while shown. Off by default; SetInclusionGutter
turns it on per view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>