mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 16:16:28 -04:00
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>
208 lines
6.6 KiB
Go
208 lines
6.6 KiB
Go
package controllers
|
|
|
|
import (
|
|
"path/filepath"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
)
|
|
|
|
// This controller is for all contexts that contain commit files.
|
|
|
|
var _ types.IController = &SwitchToDiffFilesController{}
|
|
|
|
type CanSwitchToDiffFiles interface {
|
|
types.IListContext
|
|
CanRebase() bool
|
|
GetSelectedRef() models.Ref
|
|
GetSelectedRefRangeForDiffFiles() *types.RefRange
|
|
}
|
|
|
|
// Not using our ListControllerTrait because we have our own way of working with
|
|
// range selections that's different from ListControllerTrait's
|
|
type SwitchToDiffFilesController struct {
|
|
baseController
|
|
c *ControllerCommon
|
|
context CanSwitchToDiffFiles
|
|
}
|
|
|
|
func NewSwitchToDiffFilesController(
|
|
c *ControllerCommon,
|
|
context CanSwitchToDiffFiles,
|
|
) *SwitchToDiffFilesController {
|
|
return &SwitchToDiffFilesController{
|
|
baseController: baseController{},
|
|
c: c,
|
|
context: context,
|
|
}
|
|
}
|
|
|
|
func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
bindings := []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.GoInto),
|
|
Handler: self.enter,
|
|
GetDisabledReason: self.canEnter,
|
|
Description: self.c.Tr.ViewItemFiles,
|
|
},
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func (self *SwitchToDiffFilesController) GetFocusedMainViewActions() types.FocusedMainViewActions {
|
|
return self
|
|
}
|
|
|
|
func (self *SwitchToDiffFilesController) OnClick(mainViewName string, clickedLineIdx int) error {
|
|
info, ok := self.c.Helpers().Staging.GetDiffLineInfo(mainViewName, clickedLineIdx)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
line, isDeletion := info.PatchSelectLine()
|
|
|
|
// Capture before self.enter() pushes the commit files panel, which
|
|
// re-renders the main view. We escape "all the way out" to this side
|
|
// panel (skipping the commit files panel), then focus the main view.
|
|
snapshot := focusedMainViewSnapshot(self.c, mainViewName, self.context)
|
|
|
|
if err := self.enter(); err != nil {
|
|
return err
|
|
}
|
|
|
|
context := self.c.Contexts().CommitFiles
|
|
var node *filetree.CommitFileNode
|
|
|
|
relativePath, err := filepath.Rel(self.c.Git().RepoPaths.WorktreePath(), info.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relativePath = "./" + relativePath
|
|
context.CommitFileTreeViewModel.ExpandToPath(relativePath)
|
|
self.c.PostRefreshUpdate(context)
|
|
|
|
idx, ok := context.CommitFileTreeViewModel.GetIndexForPath(relativePath)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
context.SetSelectedLineIdx(idx)
|
|
context.GetViewTrait().FocusPoint(
|
|
context.ModelIndexToViewIndex(idx), false)
|
|
node = context.GetSelected()
|
|
return self.c.Helpers().CommitFiles.EnterCommitFile(node, snapshot, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: line, ClickedViewRealLineIdx: line, ClickedViewRealLineIsDeletion: isDeletion, SelectLineInDefaultMode: true})
|
|
}
|
|
|
|
func (self *SwitchToDiffFilesController) Context() types.Context {
|
|
return self.context
|
|
}
|
|
|
|
func (self *SwitchToDiffFilesController) GetOnDoubleClick() func() error {
|
|
return func() error {
|
|
if self.canEnter() == nil {
|
|
return self.enter()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (self *SwitchToDiffFilesController) enter() error {
|
|
ref := self.context.GetSelectedRef()
|
|
refsRange := self.context.GetSelectedRefRangeForDiffFiles()
|
|
commitFilesContext := self.c.Contexts().CommitFiles
|
|
|
|
canRebase := self.canRebase(ref, refsRange)
|
|
|
|
commitFilesContext.ClearFilter()
|
|
commitFilesContext.ReInit(ref, refsRange)
|
|
commitFilesContext.SetSelection(0)
|
|
commitFilesContext.SetCanRebase(canRebase)
|
|
commitFilesContext.SetParentContext(self.context)
|
|
commitFilesContext.SetWindowName(self.context.GetWindowName())
|
|
commitFilesContext.GetView().TitlePrefix = self.context.GetView().TitlePrefix
|
|
|
|
self.c.Refresh(types.RefreshOptions{
|
|
Scope: []types.RefreshableView{types.COMMIT_FILES},
|
|
Then: func() error {
|
|
if filterPath := self.c.Modes().Filtering.GetPath(); filterPath != "" {
|
|
path, err := filepath.Rel(self.c.Git().RepoPaths.RepoPath(), filterPath)
|
|
if err != nil {
|
|
path = filterPath
|
|
}
|
|
commitFilesContext.CommitFileTreeViewModel.SelectPath(
|
|
filepath.ToSlash(path), self.c.UserConfig().Gui.ShowRootItemInFileTree)
|
|
}
|
|
self.c.Context().Push(commitFilesContext, types.OnFocusOpts{})
|
|
return nil
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// canRebase reports whether patches built from the selected ref may modify commits —
|
|
// true only for commits of the currently checked-out branch, and not while diffing a
|
|
// different ref or over a range. Shared by entering the commit files panel and toggling
|
|
// patch lines straight from the main view.
|
|
func (self *SwitchToDiffFilesController) canRebase(ref models.Ref, refsRange *types.RefRange) bool {
|
|
canRebase := self.context.CanRebase()
|
|
if canRebase {
|
|
if self.c.Modes().Diffing.Active() {
|
|
if self.c.Modes().Diffing.Ref != ref.RefName() {
|
|
canRebase = false
|
|
}
|
|
} else if refsRange != nil {
|
|
canRebase = false
|
|
}
|
|
}
|
|
return canRebase
|
|
}
|
|
|
|
// PrimaryAction toggles the selected line(s) of the whole-commit diff into or out of the
|
|
// custom patch when space is pressed in the focused main view of the commits / sub-commits
|
|
// / stash panels. The patch target is the panel's selected ref (or range), matching the
|
|
// diff the main view shows. Unlike the commit files panel there are no per-file patch
|
|
// indicators to update, so the toggle refreshes cheaply: it re-renders just this panel's
|
|
// main + secondary views (leaving the commit list untouched, which a list refresh would
|
|
// needlessly reload on every keystroke), re-running the same diff command (scroll
|
|
// preserved) and repainting the inclusion gutter.
|
|
func (self *SwitchToDiffFilesController) PrimaryAction(mainViewName string, firstLineIdx int, lastLineIdx int) error {
|
|
ref := self.context.GetSelectedRef()
|
|
if ref == nil {
|
|
return nil
|
|
}
|
|
refsRange := self.context.GetSelectedRefRangeForDiffFiles()
|
|
|
|
from, to := context.FromAndToForDiff(ref, refsRange)
|
|
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
|
canRebase := self.canRebase(ref, refsRange)
|
|
|
|
return togglePatchFromFocusedMainView(self.c, mainViewName, firstLineIdx, lastLineIdx,
|
|
from, to, reverse, canRebase,
|
|
func() {
|
|
self.c.OnUIThread(func() error {
|
|
self.c.PostRefreshUpdate(self.context)
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
func (self *SwitchToDiffFilesController) canEnter() *types.DisabledReason {
|
|
refRange := self.context.GetSelectedRefRangeForDiffFiles()
|
|
if refRange != nil {
|
|
return nil
|
|
}
|
|
ref := self.context.GetSelectedRef()
|
|
if ref == nil {
|
|
return &types.DisabledReason{Text: self.c.Tr.NoItemSelected}
|
|
}
|
|
if ref.RefName() == "" {
|
|
return &types.DisabledReason{Text: self.c.Tr.SelectedItemDoesNotHaveFiles}
|
|
}
|
|
|
|
return nil
|
|
}
|