mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Extract the patch-toggle back end out of CommitFilesController
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>
This commit is contained in:
parent
e074c40cc9
commit
26b27ed69b
|
|
@ -593,164 +593,23 @@ func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName
|
|||
}
|
||||
|
||||
// GetOnTogglePatchFocusedMainView toggles the selected diff line(s) into or out of the
|
||||
// custom patch when space is pressed in the focused main view of a commit's files. It
|
||||
// is the patch-building counterpart of the files panel's staging handler: it resolves
|
||||
// the selection to change-line identities (shared with staging), maps them to the patch
|
||||
// builder's per-file line indices, and adds or removes them. The commit's diff is
|
||||
// unchanged by the toggle, but it refreshes so the browser indicator and the secondary
|
||||
// patch view update (and the layout splits); the selection is re-established afterwards
|
||||
// since that re-render (and its layout re-wrap) is in view-line space.
|
||||
// custom patch when space is pressed in the focused main view of a commit's files. The
|
||||
// per-file diff's patch target comes from the commit files context. It refreshes
|
||||
// normally afterwards so the file's patch-status indicator in the browser updates along
|
||||
// with the secondary patch view (the commits / sub-commits / stash panels, which build
|
||||
// from the whole-commit diff, have no such indicator and refresh more cheaply).
|
||||
func (self *CommitFilesController) GetOnTogglePatchFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) error {
|
||||
return func(mainViewName string, firstLineIdx int, lastLineIdx int) error {
|
||||
infos := self.c.Helpers().Staging.ChangeLinesInViewRange(mainViewName, firstLineIdx, lastLineIdx)
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Building a patch needs the patch builder started for this commit; if a patch
|
||||
// for a different commit is active, confirm before discarding it (as entering
|
||||
// the patch builder from the commit files panel does).
|
||||
from, to, reverse := self.c.Helpers().CommitFiles.CurrentFromToReverseForPatchBuilding()
|
||||
mustDiscardPatch := self.c.Git().Patch.PatchBuilder.Active() &&
|
||||
self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse)
|
||||
return self.c.ConfirmIf(mustDiscardPatch, types.ConfirmOpts{
|
||||
Title: self.c.Tr.DiscardPatch,
|
||||
Prompt: self.c.Tr.DiscardPatchConfirm,
|
||||
HandleConfirm: func() error {
|
||||
if mustDiscardPatch {
|
||||
self.c.Git().Patch.PatchBuilder.Reset()
|
||||
}
|
||||
if !self.c.Git().Patch.PatchBuilder.Active() {
|
||||
if err := self.c.Helpers().CommitFiles.StartPatchBuilder(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.togglePatchLines(infos); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Refresh normally: this updates the file's patch-status indicator in
|
||||
// the browser and the cumulative patch in the secondary view (splitting
|
||||
// the layout when the patch first becomes non-empty), and re-renders this
|
||||
// diff. The diff is unchanged, so the re-render keeps its scroll (same
|
||||
// command) and the inclusion gutter is recomputed over it (see
|
||||
// GetOnRenderToMain).
|
||||
canRebase := self.context().GetCanRebase()
|
||||
return togglePatchFromFocusedMainView(self.c, mainViewName, firstLineIdx, lastLineIdx,
|
||||
from, to, reverse, canRebase,
|
||||
func() {
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMIT_FILES}})
|
||||
|
||||
// The re-render — and the layout re-wrap when the secondary view first
|
||||
// appears — is in view-line space, which moves the selection. Re-select
|
||||
// the same content by its (unchanged, width-independent) change-line
|
||||
// ordinal once the re-render lands.
|
||||
self.revealSelectionAfterPatchToggle(mainViewName, firstLineIdx)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// togglePatchLines toggles the change lines identified by infos into or out of the
|
||||
// custom patch. The direction is decided once, from the first selected change line (in
|
||||
// view order) — already in the patch means remove the whole selection, otherwise add it
|
||||
// — and applied to every file the selection spans, mirroring how the patch explorer
|
||||
// toggles a range.
|
||||
func (self *CommitFilesController) togglePatchLines(infos []types.DiffLineInfo) error {
|
||||
patchBuilder := self.c.Git().Patch.PatchBuilder
|
||||
|
||||
// Group the selected change lines by file (in view order), then resolve each file's
|
||||
// identities to patch-line indices in one pass.
|
||||
var order []string
|
||||
identitiesByFile := map[string][]patch.LineIdentity{}
|
||||
for _, info := range infos {
|
||||
filename := self.patchFilename(info.Path)
|
||||
if filename == "" {
|
||||
continue
|
||||
}
|
||||
if _, seen := identitiesByFile[filename]; !seen {
|
||||
order = append(order, filename)
|
||||
}
|
||||
lineNumber, isDeletion := info.PatchSelectLine()
|
||||
identitiesByFile[filename] = append(identitiesByFile[filename],
|
||||
patch.LineIdentity{LineNumber: lineNumber, IsDeletion: isDeletion})
|
||||
}
|
||||
if len(order) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
indicesByFile := map[string][]int{}
|
||||
for filename, identities := range identitiesByFile {
|
||||
indices, err := patchBuilder.PatchLineIndicesForLines(filename, identities)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indicesByFile[filename] = indices
|
||||
}
|
||||
|
||||
// Decide the direction from the first selected change line.
|
||||
firstFile := order[0]
|
||||
included, err := patchBuilder.GetFileIncLineIndices(firstFile, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remove := len(indicesByFile[firstFile]) > 0 && lo.Contains(included, indicesByFile[firstFile][0])
|
||||
toggle := patchBuilder.AddFileLineRange
|
||||
if remove {
|
||||
toggle = patchBuilder.RemoveFileLineRange
|
||||
}
|
||||
|
||||
for _, filename := range order {
|
||||
indices := indicesByFile[filename]
|
||||
if len(indices) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := toggle(filename, "", indices); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// revealSelectionAfterPatchToggle re-establishes the selection after a toggle's
|
||||
// re-render. Toggling doesn't change the diff, so the same content is still there — but
|
||||
// the re-render (and the layout re-wrap when the secondary view first appears) is in
|
||||
// view-line space, which moves the selection. We preserve the selection's change-line
|
||||
// ordinal, which is unchanged (the line isn't consumed, unlike staging) and
|
||||
// width-independent, re-expanding the hunk in hunk mode. A range collapses to a line,
|
||||
// as a staged range does.
|
||||
func (self *CommitFilesController) revealSelectionAfterPatchToggle(mainViewName string, firstLineIdx int) {
|
||||
mainContext := self.c.Contexts().Normal
|
||||
if mainViewName == self.c.Contexts().NormalSecondary.GetViewName() {
|
||||
mainContext = self.c.Contexts().NormalSecondary
|
||||
}
|
||||
view := mainContext.GetView()
|
||||
|
||||
sel := mainContext.DiffSelectState()
|
||||
if sel.Mode == context.DiffSelectModeRange {
|
||||
sel.Mode = context.DiffSelectModeLine
|
||||
sel.RangeIsSticky = false
|
||||
}
|
||||
mode := sel.Mode
|
||||
|
||||
self.c.Helpers().Staging.RevealSelectionAfterStaging(view, view, firstLineIdx, func(viewLine int) {
|
||||
if mode == context.DiffSelectModeHunk {
|
||||
selectDiffHunk(self.c, mainContext, viewLine)
|
||||
} else {
|
||||
view.CancelRangeSelect()
|
||||
showSelectionAtLine(view, viewLine, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// patchFilename maps a diff line's absolute path to the key the patch builder stores
|
||||
// the file under — its repo-relative, slash-separated path.
|
||||
func (self *CommitFilesController) patchFilename(absPath string) string {
|
||||
relPath, err := filepath.Rel(self.c.Git().RepoPaths.WorktreePath(), absPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.ToSlash(relPath)
|
||||
}
|
||||
|
||||
// pathsForDiff returns the file paths to use for a diff command. When a text
|
||||
// filter is active and the node is a directory, only the visible (filtered)
|
||||
// file paths are returned so the diff reflects what the user sees.
|
||||
|
|
|
|||
168
pkg/gui/controllers/patch_building_from_main_view.go
Normal file
168
pkg/gui/controllers/patch_building_from_main_view.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// togglePatchFromFocusedMainView toggles the selected diff line(s) — a single line, a
|
||||
// range, or a hunk — into or out of the custom patch being built for (from, to,
|
||||
// reverse). It is the patch-building counterpart of the files panel's staging handler,
|
||||
// shared by the panels that build a patch from a focused main view: the commit files
|
||||
// panel (the per-file diff) and the commits / sub-commits / stash panels (the
|
||||
// whole-commit diff). The selection is resolved to change-line identities (shared with
|
||||
// staging) and mapped to the patch builder's per-file line indices.
|
||||
//
|
||||
// Building a patch needs the patch builder started for this target; if a patch for a
|
||||
// different target is active, we confirm before discarding it (as entering the patch
|
||||
// builder does). The diff itself is unchanged by a toggle, so refresh — supplied by the
|
||||
// caller, since what needs re-rendering differs per panel — re-renders the same diff
|
||||
// command (keeping its scroll) and the secondary patch view, and the inclusion gutter
|
||||
// rides that re-render. That re-render (and its layout re-wrap when the secondary view
|
||||
// first appears) is in view-line space, which moves the selection, so we re-establish it
|
||||
// afterwards by its width-independent change-line ordinal.
|
||||
func togglePatchFromFocusedMainView(
|
||||
c *ControllerCommon,
|
||||
mainViewName string,
|
||||
firstLineIdx int,
|
||||
lastLineIdx int,
|
||||
from string,
|
||||
to string,
|
||||
reverse bool,
|
||||
canRebase bool,
|
||||
refresh func(),
|
||||
) error {
|
||||
infos := c.Helpers().Staging.ChangeLinesInViewRange(mainViewName, firstLineIdx, lastLineIdx)
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
patchBuilder := c.Git().Patch.PatchBuilder
|
||||
mustDiscardPatch := patchBuilder.Active() && patchBuilder.NewPatchRequired(from, to, reverse)
|
||||
return c.ConfirmIf(mustDiscardPatch, types.ConfirmOpts{
|
||||
Title: c.Tr.DiscardPatch,
|
||||
Prompt: c.Tr.DiscardPatchConfirm,
|
||||
HandleConfirm: func() error {
|
||||
if mustDiscardPatch {
|
||||
patchBuilder.Reset()
|
||||
}
|
||||
if !patchBuilder.Active() {
|
||||
patchBuilder.Start(from, to, reverse, canRebase)
|
||||
}
|
||||
|
||||
if err := togglePatchLines(c, infos); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
refresh()
|
||||
revealSelectionAfterPatchToggle(c, mainViewName, firstLineIdx)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// togglePatchLines toggles the change lines identified by infos into or out of the
|
||||
// custom patch. The direction is decided once, from the first selected change line (in
|
||||
// view order) — already in the patch means remove the whole selection, otherwise add it
|
||||
// — and applied to every file the selection spans, mirroring how the patch explorer
|
||||
// toggles a range.
|
||||
func togglePatchLines(c *ControllerCommon, infos []types.DiffLineInfo) error {
|
||||
patchBuilder := c.Git().Patch.PatchBuilder
|
||||
|
||||
// Group the selected change lines by file (in view order), then resolve each file's
|
||||
// identities to patch-line indices in one pass.
|
||||
var order []string
|
||||
identitiesByFile := map[string][]patch.LineIdentity{}
|
||||
for _, info := range infos {
|
||||
filename := patchFilename(c, info.Path)
|
||||
if filename == "" {
|
||||
continue
|
||||
}
|
||||
if _, seen := identitiesByFile[filename]; !seen {
|
||||
order = append(order, filename)
|
||||
}
|
||||
lineNumber, isDeletion := info.PatchSelectLine()
|
||||
identitiesByFile[filename] = append(identitiesByFile[filename],
|
||||
patch.LineIdentity{LineNumber: lineNumber, IsDeletion: isDeletion})
|
||||
}
|
||||
if len(order) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
indicesByFile := map[string][]int{}
|
||||
for filename, identities := range identitiesByFile {
|
||||
indices, err := patchBuilder.PatchLineIndicesForLines(filename, identities)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indicesByFile[filename] = indices
|
||||
}
|
||||
|
||||
// Decide the direction from the first selected change line.
|
||||
firstFile := order[0]
|
||||
included, err := patchBuilder.GetFileIncLineIndices(firstFile, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remove := len(indicesByFile[firstFile]) > 0 && lo.Contains(included, indicesByFile[firstFile][0])
|
||||
toggle := patchBuilder.AddFileLineRange
|
||||
if remove {
|
||||
toggle = patchBuilder.RemoveFileLineRange
|
||||
}
|
||||
|
||||
for _, filename := range order {
|
||||
indices := indicesByFile[filename]
|
||||
if len(indices) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := toggle(filename, "", indices); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// revealSelectionAfterPatchToggle re-establishes the selection after a toggle's
|
||||
// re-render. Toggling doesn't change the diff, so the same content is still there — but
|
||||
// the re-render (and the layout re-wrap when the secondary view first appears) is in
|
||||
// view-line space, which moves the selection. We preserve the selection's change-line
|
||||
// ordinal, which is unchanged (the line isn't consumed, unlike staging) and
|
||||
// width-independent, re-expanding the hunk in hunk mode. A range collapses to a line,
|
||||
// as a staged range does.
|
||||
func revealSelectionAfterPatchToggle(c *ControllerCommon, mainViewName string, firstLineIdx int) {
|
||||
mainContext := c.Contexts().Normal
|
||||
if mainViewName == c.Contexts().NormalSecondary.GetViewName() {
|
||||
mainContext = c.Contexts().NormalSecondary
|
||||
}
|
||||
view := mainContext.GetView()
|
||||
|
||||
sel := mainContext.DiffSelectState()
|
||||
if sel.Mode == context.DiffSelectModeRange {
|
||||
sel.Mode = context.DiffSelectModeLine
|
||||
sel.RangeIsSticky = false
|
||||
}
|
||||
mode := sel.Mode
|
||||
|
||||
c.Helpers().Staging.RevealSelectionAfterStaging(view, view, firstLineIdx, func(viewLine int) {
|
||||
if mode == context.DiffSelectModeHunk {
|
||||
selectDiffHunk(c, mainContext, viewLine)
|
||||
} else {
|
||||
view.CancelRangeSelect()
|
||||
showSelectionAtLine(view, viewLine, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// patchFilename maps a diff line's absolute path to the key the patch builder stores
|
||||
// the file under — its repo-relative, slash-separated path.
|
||||
func patchFilename(c *ControllerCommon, absPath string) string {
|
||||
relPath, err := filepath.Rel(c.Git().RepoPaths.WorktreePath(), absPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.ToSlash(relPath)
|
||||
}
|
||||
Loading…
Reference in a new issue