Fall back to the raw diff when focusing a main view under an unresolvable pager

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>
This commit is contained in:
Stefan Haller 2026-06-20 08:21:01 +02:00
parent 482acff66d
commit 2a69d4c140
10 changed files with 389 additions and 24 deletions

View file

@ -2,8 +2,11 @@ package git_commands
import (
"fmt"
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/mgutz/str"
)
type DiffCommands struct {
@ -16,6 +19,69 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
}
}
// metadataHandshake is the OSC sequence a metadata-aware pager emits, as its first
// output, to announce that it speaks the diff-line-metadata protocol: a version-only
// OSC 1717 record (no fields). See ProbePagerEmitsDiffMetadata and, for how it's
// swallowed on a real render, escapeInterpreter.dropMetadataIfHandshake.
const metadataHandshake = "\x1b]1717"
// ProbePagerEmitsDiffMetadata reports whether the configured pager speaks the
// diff-line-metadata protocol, by running it on empty input and checking for its
// handshake. It's the focused main view's signal for whether it can act on the
// pager's rendered diff or must fall back to the raw diff (see
// StagingHelper.DiffMainViewShouldRenderRaw). The verdict is content-independent —
// the handshake is the pager's first output regardless of the diff — so the caller
// caches it per pager.
//
// No PTY is needed: git needs a terminal to decide to invoke a pager, but the pager
// itself emits the handshake whenever EMIT_OSC1717_METADATA is set, so we can run it
// directly with empty input.
//
// 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;
// we conservatively report false (the focused main view then always renders raw).
func (self *DiffCommands) ProbePagerEmitsDiffMetadata() bool {
if extDiffCmd := self.diffRendererConfigManager.GetExternalDiffCommand(3); extDiffCmd != "" {
return self.externalDiffEmitsMetadata(extDiffCmd)
}
if pagerCmd := self.diffRendererConfigManager.GetStdinFilterCommand(0); pagerCmd != "" {
return self.probeEmitsMetadata(self.cmd.NewShell(pagerCmd, ""))
}
return false
}
// externalDiffEmitsMetadata probes an external diff command, invoking it the way git
// invokes a diff.external driver — with 7 positional args
// (path old-file old-hex old-mode new-file new-hex new-mode) — but on two empty temp
// files, so it emits its handshake without there being a real diff to render.
func (self *DiffCommands) externalDiffEmitsMetadata(extDiffCmd string) bool {
oldFile, err := os.CreateTemp("", "lazygit-probe-old-*")
if err != nil {
return false
}
defer os.Remove(oldFile.Name())
oldFile.Close()
newFile, err := os.CreateTemp("", "lazygit-probe-new-*")
if err != nil {
return false
}
defer os.Remove(newFile.Name())
newFile.Close()
args := append(str.ToArgv(extDiffCmd),
"probe", oldFile.Name(), "0000000", "100644", newFile.Name(), "0000000", "100644")
return self.probeEmitsMetadata(self.cmd.New(args))
}
func (self *DiffCommands) probeEmitsMetadata(cmdObj *oscommands.CmdObj) bool {
cmdObj.AddEnvVars("EMIT_OSC1717_METADATA=V1")
// The pager may exit non-zero on the synthetic input; we only care about whether
// it emitted the handshake first, and the output is captured either way.
output, _ := cmdObj.RunWithOutput()
return strings.Contains(output, metadataHandshake)
}
// This is for generating diffs to be shown in the UI (e.g. rendering a range
// diff to the main view). It uses a custom diff renderer if one is configured.
func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj {

View file

@ -385,14 +385,17 @@ func (self *WorkingTreeCommands) Exclude(filename string) error {
// WorktreeFileDiff returns the diff of a file
func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
// for now we assume an error means the file was deleted
s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, nil).RunWithOutput()
s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, false, nil).RunWithOutput()
return s
}
// WorktreeFileDiffCmdObj returns a command object for diffing a file or directory
// in the working tree. When pathOverrides is non-empty, those paths are used instead of
// the node's path (used to diff only filtered/visible files within a directory).
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, pathOverrides []string) *oscommands.CmdObj {
// ignoreExternalDiff forces git's own (coloured) diff regardless of a configured
// external diff command, for the focused main view's raw-diff fallback (see
// StagingHelper.DiffMainViewShouldRenderRaw); unlike plain it keeps the colour.
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreExternalDiff bool, pathOverrides []string) *oscommands.CmdObj {
colorArg := self.diffRendererConfigManager.GetColorArg()
if plain {
colorArg = "never"
@ -407,7 +410,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
}
cmdArgs := NewGitCmd("diff").
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !plain).
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !plain && !ignoreExternalDiff).
Arg("--submodule").
Arg(fmt.Sprintf("--color=%s", colorArg)).
ArgIf(cached, "--cached").

View file

@ -373,8 +373,13 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) {
split, mainShowsStaged := self.diffSplitState(node)
// When the focused main view needs to act on a diff the configured pager
// can't resolve, render it raw (no pager) so it's stageable; browsing keeps
// the pretty pager output. See StagingHelper.DiffMainViewShouldRenderRaw.
renderRaw := self.c.Helpers().Staging.DiffMainViewShouldRenderRaw()
pathOverrides := self.pathOverridesForDiff(node)
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, pathOverrides)
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, renderRaw, pathOverrides)
title := self.c.Tr.UnstagedChanges
if mainShowsStaged {
title = self.c.Tr.StagedChanges
@ -382,14 +387,14 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) {
refreshOpts := types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
Task: diffMainViewTask(renderRaw, cmdObj.GetCmd()),
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Title: title,
},
}
if split {
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, pathOverrides)
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, renderRaw, pathOverrides)
title := self.c.Tr.StagedChanges
if mainShowsStaged {
@ -399,7 +404,7 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) {
refreshOpts.Secondary = &types.ViewUpdateOpts{
Title: title,
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
Task: diffMainViewTask(renderRaw, cmdObj.GetCmd()),
}
}

View file

@ -1,11 +1,13 @@
package helpers
import (
"fmt"
"path/filepath"
"regexp"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types"
@ -18,6 +20,14 @@ var lazygitEditURLRegexp = regexp.MustCompile(`^lazygit-edit://(.+?):(\d+)$`)
type StagingHelper struct {
c *HelperCommon
windowHelper *WindowHelper
// pagerMetadataSupport caches whether the configured pager speaks the
// diff-line-metadata protocol (nil until first probed), so the focused main view
// knows whether it can act on the pager's diff or must render it raw (see
// DiffMainViewShouldRenderRaw). Keyed on pagerSupportSig; re-probed when the pager
// changes (a cycle or a config reload).
pagerMetadataSupport *bool
pagerSupportSig string
}
func NewStagingHelper(
@ -784,3 +794,106 @@ func (self *StagingHelper) diffLineInfoFromHyperlink(hyperlink string) (types.Di
NewLine: utils.MustConvertToInt(matches[2]),
}, true
}
// --- The raw-diff fallback for unsupported pagers ---
//
// The focused main view is the staging surface, so it must be able 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 we
// can't resolve, so when such a diff is focused we 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 a pager is usable is decided by probing it for the metadata handshake (see
// DiffCommands.ProbePagerEmitsDiffMetadata) — a pager-level, content-independent fact,
// so the verdict is stable (a binary file, which has no change lines under any pager,
// can't mislead it) and known before we render, so we never render pretty only to
// discover mid-flight that we should have rendered raw.
// pagerSupportsMetadata returns whether the current pager speaks the metadata
// protocol, probing it once and caching the result, re-probing when the pager changes.
func (self *StagingHelper) pagerSupportsMetadata() bool {
sig := self.currentPagerSignature()
if self.pagerMetadataSupport == nil || sig != self.pagerSupportSig {
result := self.c.Git().Diff.ProbePagerEmitsDiffMetadata()
self.pagerMetadataSupport = &result
self.pagerSupportSig = sig
}
return *self.pagerMetadataSupport
}
// currentPagerSignature identifies the current pager, so the cached verdict resets
// when it changes (the user cycles pagers, or reloads a changed config). The pager
// command's width placeholder is irrelevant to its identity, so a fixed width is used.
func (self *StagingHelper) currentPagerSignature() string {
pc := self.c.State().GetDiffRendererConfigManager()
index, _ := pc.CurrentDiffRendererIndex()
return fmt.Sprintf("%d\x00%s\x00%s",
index, pc.GetExternalDiffCommand(3), pc.GetStdinFilterCommand(0))
}
// MainViewPagerConfigured reports whether any custom pager is in effect for the
// focused main view's diff — an external diff command, git's diff.external config,
// or a stdin pager. With no pager the diff is already raw and resolvable, so the
// raw-diff fallback only applies when one is configured.
func (self *StagingHelper) MainViewPagerConfigured() bool {
pc := self.c.State().GetDiffRendererConfigManager()
return pc.GetDiffRendererType() != config.DiffRendererType_RawGit
}
func (self *StagingHelper) rendererIsRawGitWithArgs() bool {
pc := self.c.State().GetDiffRendererConfigManager()
return pc.GetDiffRendererType() == config.DiffRendererType_RawGit &&
len(pc.GetRawGitArgs()) > 0
}
// DiffMainViewShouldRenderRaw reports whether a side panel rendering its diff into
// the focused main view should bypass the pager and render the raw (git-coloured)
// diff. This holds while the main view holds focus and the configured pager doesn't
// speak the metadata protocol. Side panels consult it when building their main-view
// diff (see e.g. FilesController.GetOnRenderToMain) so that a re-render while focused
// — after staging a hunk, say — stays raw.
func (self *StagingHelper) DiffMainViewShouldRenderRaw() bool {
return self.focusedOnMainView() && ((self.MainViewPagerConfigured() && !self.pagerSupportsMetadata()) ||
self.rendererIsRawGitWithArgs())
}
func (self *StagingHelper) focusedOnMainView() bool {
current := self.c.Context().CurrentStatic().GetKey()
return current == self.c.Contexts().Normal.GetKey() ||
current == self.c.Contexts().NormalSecondary.GetKey()
}
// RenderFocusedMainViewRaw installs a restore that calls place once the focused main
// view's raw re-render lands, then triggers that re-render via the side panel beneath.
// It's used when focusing a diff under a pager that doesn't speak the protocol:
// DiffMainViewShouldRenderRaw is already true (the verdict is probed, not derived from
// this render), so the side panel renders raw; place then establishes the selection on
// the result.
func (self *StagingHelper) RenderFocusedMainViewRaw(view *gocui.View, sidePanel types.Context, place func()) {
manager := self.c.GetOrCreateViewBufferManagerForView(view)
if manager == nil {
return
}
manager.SetRestoreForNextTask(&tasks.RenderRestore{
// Hold the first paint until end of input so the whole diff is read before we
// place the selection (a change line near the end would otherwise be missed).
FirstPaintReady: func() bool { return false },
Apply: func(swapIn func()) {
swapIn()
manager.ClearRestoreForNextTask()
// Apply runs on the task's goroutine; hop to the UI thread to place the
// selection, which reads the swapped-in content and touches view state.
self.c.OnUIThread(func() error {
place()
return nil
})
},
})
if sidePanel != nil {
sidePanel.HandleRenderToMain()
}
}

View file

@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"os/exec"
"path/filepath"
"strings"
@ -249,21 +250,53 @@ func (self *MainViewController) togglePanel() error {
return nil
}
// showInitialDiffSelection turns on the focused main view's selection when entering
// a diff view without pointing at a specific line, anchored on the first change line
// already visible (so the view barely moves), falling back to the current top line
// when none is visible (scrolled into trailing context, or not loaded that far yet).
// With hunk mode configured as the default it selects the whole change block around
// that line, like entering the staging view does; otherwise a single line.
// showInitialDiffSelection turns on the focused main view's selection when entering a
// diff view by keyboard, without pointing at a specific line. See
// establishFocusedDiffSelection, which it defers to (clicks go through there too,
// passing the clicked line).
func showInitialDiffSelection(c *ControllerCommon, mainContext *context.MainContext) {
establishFocusedDiffSelection(c, mainContext, -1)
}
// establishFocusedDiffSelection turns on the focused main view's selection after the
// view is focused. Under a pager that doesn't speak the metadata protocol (probed, so
// known up front; see StagingHelper.DiffMainViewShouldRenderRaw) the diff on screen was
// rendered pretty for browsing and isn't resolvable, so it re-renders it raw and places
// the selection once that lands; otherwise it places the selection on the diff directly.
// clickedViewLine is the view line a click pointed at, or -1 for keyboard focus (start
// at the first change block).
func establishFocusedDiffSelection(c *ControllerCommon, mainContext *context.MainContext, clickedViewLine int) {
staging := c.Helpers().Staging
resetDiffSelectMode(mainContext)
if staging.DiffMainViewShouldRenderRaw() {
sidePanel := c.Context().NextInStack(mainContext)
staging.RenderFocusedMainViewRaw(mainContext.GetView(), sidePanel, func() {
placeOrHideInitialDiffSelection(c, mainContext, clickedViewLine, true)
})
return
}
placeOrHideInitialDiffSelection(c, mainContext, clickedViewLine, clickedViewLine < 0)
}
// placeOrHideInitialDiffSelection puts the focused main view's selection on the clicked
// line (clickedViewLine >= 0) or, for keyboard focus, on the first change line at or
// below the top of the viewport — so the view barely moves — falling back to the top
// line when none is visible. With hunk mode configured as the default, keyboard focus
// selects the whole change block around that line, like entering the staging view does.
// When the diff has nothing to act on (a placeholder, a binary file, an all-context
// diff) it shows no selection rather than highlighting a stray line.
func placeOrHideInitialDiffSelection(c *ControllerCommon, mainContext *context.MainContext, clickedViewLine int, scrollIntoView bool) {
view := mainContext.GetView()
// Nothing to act on (the main view shows "No changed files" or another non-diff
// placeholder): show no selection at all rather than highlighting a stray line.
if !c.Helpers().Staging.ViewHasChangeLines(view) {
view.Highlight = false
return
}
if clickedViewLine >= 0 {
showSelectionAtLine(view, clickedViewLine, scrollIntoView)
return
}
target, ok := c.Helpers().Staging.FirstChangeLineInView(view)
if !ok {
showSelectionAtLine(view, view.OriginY(), true)
@ -277,6 +310,20 @@ func showInitialDiffSelection(c *ControllerCommon, mainContext *context.MainCont
showSelectionAtLine(view, target, true)
}
// diffMainViewTask builds the task a side panel uses to render its diff into the main
// view, choosing between the normal pty task and the raw-diff fallback. When renderRaw
// is set (the focused main view needs to act on a diff the configured pager can't
// resolve, see StagingHelper.DiffMainViewShouldRenderRaw) it uses a plain command task,
// which — unlike the pty task — doesn't pipe the diff through a stdin pager (GIT_PAGER);
// the external diff command, if any, is suppressed in the cmd itself. The caller passes
// the same renderRaw to the diff-cmd builder so the two stay in step.
func diffMainViewTask(renderRaw bool, cmd *exec.Cmd) types.UpdateTask {
if renderRaw {
return types.NewRunCommandTask(cmd)
}
return types.NewRunPtyTask(cmd)
}
// updateFocusedMainViewSelectionVisibility shows or hides the focused-main-view selection
// to match what a side panel is rendering into the main view, called from the panel's
// render-to-main so the selection tracks content changes (a refresh after the last change

View file

@ -123,7 +123,7 @@ func (self *SubmodulesController) GetOnRenderToMain() func() {
if file == nil {
task = types.NewRenderStringTask(prefix)
} else {
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, nil)
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, false, nil)
task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
}
}

View file

@ -88,14 +88,10 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *co
return nil
}
if clickedLineIdx >= 0 {
// A click points at a specific line, so select it directly (in the default
// single-line mode).
resetDiffSelectMode(mainViewContext)
showSelectionAtLine(mainViewContext.GetView(), clickedLineIdx, false)
} else {
showInitialDiffSelection(self.c, mainViewContext)
}
// A click points at a specific line (clickedLineIdx); keyboard focus (-1) starts at
// the first change block. Either way, if the pager produced an unresolvable diff
// this re-renders it raw first (see establishFocusedDiffSelection).
establishFocusedDiffSelection(self.c, mainViewContext, clickedLineIdx)
// The inclusion gutter is refreshed by the main view's focus handler (it's shown
// only while focused, so it tracks focus changes there).

View file

@ -0,0 +1,65 @@
package staging
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var StageFromMainViewWithConformingPager = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Focus the main view under a pager that announces the diff-metadata protocol (a handshake); its output is trusted, so the diff is not re-rendered raw and staging works on it",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
// A fake conforming pager: it emits the version-only OSC 1717 handshake as its
// first output (announcing it speaks the protocol), prints a marker line so the
// test can tell its output apart from the raw diff, then passes the diff through
// unchanged. The probe finds the handshake and trusts the pager, so focusing
// does not fall back to the raw diff. (The passed-through diff is structurally
// intact, so the selection still resolves via the buffer parser.)
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
{Command: "printf '\\033]1717;1\\007CONFORMING-PAGER\\n'; cat"},
}
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n")
shell.Commit("one")
shell.UpdateFile("file1", "one\ntwo\nTHREE\nfour\nfive\nsix\nseven\neight\nNINE\nten\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Contains("file1").IsSelected(),
)
// The handshake is swallowed (it leaves no visible bytes), but the pager's
// marker line proves its output is what's shown.
t.Views().Main().Content(Contains("CONFORMING-PAGER"))
t.Views().Files().Press(keys.Universal.FocusMainView)
// Focusing did not re-render raw — the marker is still there — and the selection
// resolved on the pager's (structure-preserving) output.
t.Views().Main().
IsFocused().
Content(Contains("CONFORMING-PAGER")).
SelectedLines(
Contains("-three"),
).
Press(keys.Main.ToggleSelectHunk).
SelectedLines(
Contains("-three"),
Contains("+THREE"),
).
PressPrimaryAction().
Tap(func() {
t.Views().Secondary().
ContainsLines(
Contains("-three"),
Contains("+THREE"),
)
})
},
})

View file

@ -0,0 +1,68 @@
package staging
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var StageFromMainViewWithUnsupportedPager = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Focus the main view under a pager that restructures the diff without emitting metadata; it falls back to the raw diff so the selection is still stageable",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
// `cat -n` prepends a line number to every line, which pushes the diff's +/-
// markers off the start of the line: the buffer parser can't resolve it, and
// cat emits no metadata, so the focused main view must fall back to the raw diff.
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
{Command: "cat -n"},
}
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n")
shell.Commit("one")
// Two separate change blocks, so staging one leaves the file split (the other
// stays unstaged) and the staged side renders into the secondary view.
shell.UpdateFile("file1", "one\ntwo\nTHREE\nfour\nfive\nsix\nseven\neight\nNINE\nten\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// While browsing, the main view shows the pager's output: `cat -n` has numbered
// every line (the first being the `diff --git` header), so the diff isn't in
// its raw form.
t.Views().Files().
IsFocused().
Lines(
Contains("file1").IsSelected(),
)
t.Views().Main().Content(Contains("1 diff --git a/file1 b/file1"))
// Focusing the main view falls back to the raw diff, so the change line is
// resolved and selected (the pager's line numbers are gone).
t.Views().Files().Press(keys.Universal.FocusMainView)
t.Views().Main().
IsFocused().
SelectedLines(
Contains("-three"),
).
Press(keys.Main.ToggleSelectHunk).
SelectedLines(
Contains("-three"),
Contains("+THREE"),
).
PressPrimaryAction().
Tap(func() {
t.Views().Secondary().
ContainsLines(
Contains("-three"),
Contains("+THREE"),
)
}).
// The other block stays unstaged, also shown raw.
ContainsLines(
Contains("-nine"),
Contains("+NINE"),
)
},
})

View file

@ -431,6 +431,8 @@ var tests = []*components.IntegrationTest{
staging.SelectNextLineAfterStagingInTwoHunkDiff,
staging.SelectNextLineAfterStagingIsolatedAddedLine,
staging.SelectNextLineAfterStagingLineFromMainView,
staging.StageFromMainViewWithConformingPager,
staging.StageFromMainViewWithUnsupportedPager,
staging.StageHunkFromMainView,
staging.StageHunks,
staging.StageHunksWithRapidKeypresses,