diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go index e2f228c21..8c92b240f 100644 --- a/pkg/gui/context/base_context.go +++ b/pkg/gui/context/base_context.go @@ -39,7 +39,7 @@ type ( onFocusFn = func(types.OnFocusOpts) onFocusLostFn = func(types.OnFocusLostOpts) onClickFocusedMainViewFn = func(mainViewName string, clickedLineIdx int) error - onStageFocusedMainViewFn = func(mainViewName string, firstLineIdx int, lastLineIdx int) (focusViewName string, err error) + onStageFocusedMainViewFn = func(mainViewName string, firstLineIdx int, lastLineIdx int) error onTogglePatchFocusedMainViewFn = func(mainViewName string, firstLineIdx int, lastLineIdx int) error ) diff --git a/pkg/gui/controllers/base_controller.go b/pkg/gui/controllers/base_controller.go index 0b8a38438..2b9e8803e 100644 --- a/pkg/gui/controllers/base_controller.go +++ b/pkg/gui/controllers/base_controller.go @@ -23,7 +23,7 @@ func (self *baseController) GetOnClickFocusedMainView() func(mainViewName string return nil } -func (self *baseController) GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) (focusViewName string, err error) { +func (self *baseController) GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) error { return nil } diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index bd10d939b..305bbdd46 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -457,21 +457,21 @@ func (self *FilesController) diffSplitState(node *filetree.FileNode) (split bool return split, mainShowsStaged } -func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) (string, error) { - return func(mainViewName string, firstLineIdx int, lastLineIdx int) (string, error) { +func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) error { + return func(mainViewName string, firstLineIdx int, lastLineIdx int) error { if self.c.UserConfig().Git.DiffContextSize == 0 { - return "", fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage, + return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage, self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) } node := self.context().GetSelected() if node == nil { - return "", nil + return nil } infos := self.c.Helpers().Staging.ChangeLinesInViewRange(mainViewName, firstLineIdx, lastLineIdx) if len(infos) == 0 { - return "", nil + return nil } // The whole diff shown in the main view is on one side — the staged diff in @@ -492,7 +492,7 @@ func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName strin continue } if err := self.stageDiffLines(file, fileInfos, reverse); err != nil { - return "", err + return err } } @@ -513,7 +513,15 @@ func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName strin } } } - return focusViewName, nil + + // The staging Refresh above queued the main-view re-render; re-establish the + // selection in whichever pane now holds the acted-on side once that render lands, + // and focus that pane if staging moved it there. + revealSelectionAfterPrimaryAction(self.c, mainViewName, focusViewName, firstLineIdx) + if focusViewName != mainViewName { + self.c.Context().Push(mainContextForViewName(self.c, focusViewName), types.OnFocusOpts{}) + } + return nil } } diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index d70897a0b..7bad13182 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -280,80 +280,59 @@ func (self *MainViewController) isDiffView() bool { } // stageSelectedLine acts on the selected diff line(s) — a single line, a range, or a -// hunk — delegating to the side panel beneath the focused main view, since what the -// action means is the panel's business: the working tree stages, while commits build a -// custom patch. Panels whose diff supports neither register no handler, so this is a -// no-op there. +// hunk — delegating the primary action to the side panel beneath the focused main view, +// since what the action means is the panel's business: the working tree stages, while +// commits toggle the selection into a custom patch. Each handler does its own re-render +// and re-establishes the selection afterwards (see revealSelectionAfterPrimaryAction), so +// the dispatcher just hands over the selected range. Panels whose diff supports neither +// register no handler, so this is a no-op there. func (self *MainViewController) stageSelectedLine() error { sidePanelContext := self.c.Context().NextInStack(self.context) if sidePanelContext == nil { return nil } + v := self.context.GetView() + first, last := v.SelectedLineRange() if handler := sidePanelContext.GetOnStageFocusedMainView(); handler != nil { - return self.stageRange(handler) + return handler(self.context.GetViewName(), first, last) } if handler := sidePanelContext.GetOnTogglePatchFocusedMainView(); handler != nil { - // Toggling the selection into the custom patch leaves the diff unchanged (the - // handler repaints the inclusion gutter itself), so unlike staging there's no - // re-render to ride and the selection stays where it is. - v := self.context.GetView() - first, last := v.SelectedLineRange() return handler(self.context.GetViewName(), first, last) } return nil } -// stageRange stages (or unstages) the current selection through the side panel's -// staging handler. Staging mutates the working tree, so the diff re-renders -// asynchronously and the selection is re-revealed once it lands. -func (self *MainViewController) stageRange(handler func(mainViewName string, firstLineIdx int, lastLineIdx int) (string, error)) error { - v := self.context.GetView() - first, last := v.SelectedLineRange() +// revealSelectionAfterPrimaryAction re-establishes the focused-main-view selection after a +// primary action (staging or a patch toggle) re-renders the diff. The selection's +// change-line ordinal is read from the source pane (still showing the pre-action diff +// until the queued re-render) and re-applied once the target pane re-renders — so the +// selection lands on the change nearest the one acted on rather than at a stale position. +// sourceViewName and targetViewName are usually the same pane, but staging can move the +// acted-on side to the other pane (passing that pane as the target). The target inherits +// the (collapsed) select mode; a range collapses back to a single line, hunk mode stays +// on to land on the next hunk. +func revealSelectionAfterPrimaryAction(c *ControllerCommon, sourceViewName string, targetViewName string, firstLineIdx int) { + sourceContext := mainContextForViewName(c, sourceViewName) + targetContext := mainContextForViewName(c, targetViewName) - // Staging consumes the selected range, so a range selection collapses back to a - // single line; hunk mode stays on, to land on the next hunk. - sel := self.sel() + sel := sourceContext.DiffSelectState() if sel.Mode == context.DiffSelectModeRange { sel.Mode = context.DiffSelectModeLine sel.RangeIsSticky = false } - - // Staging updates the model synchronously and queues the main-view re-render, and - // reports which pane should hold focus afterwards (staging/unstaging can move the - // acted-on side to the other pane). "" means nothing was staged. - focusViewName, err := handler(self.context.GetViewName(), first, last) - if err != nil { - return err - } - if focusViewName == "" { - return nil - } - - // Re-select in whichever pane now holds the acted-on side, and focus it. The - // acted-on line's change-line ordinal is read from the pane we acted in (its - // content is still the pre-staging diff until the queued re-render) and re-applied - // in the target pane's re-render. The target inherits our select mode (line/hunk). - targetContext := self.context - if focusViewName == self.otherContext.GetViewName() { - targetContext = self.otherContext - } *targetContext.DiffSelectState() = *sel - targetView := targetContext.GetView() + mode := sel.Mode - self.c.Helpers().Staging.RevealSelectionAfterStaging(v, targetView, first, func(viewLine int) { - if sel.Mode == context.DiffSelectModeHunk { - selectDiffHunk(self.c, targetContext, viewLine) + sourceView := sourceContext.GetView() + targetView := targetContext.GetView() + c.Helpers().Staging.RevealSelectionAfterStaging(sourceView, targetView, firstLineIdx, func(viewLine int) { + if mode == context.DiffSelectModeHunk { + selectDiffHunk(c, targetContext, viewLine) } else { targetView.CancelRangeSelect() showSelectionAtLine(targetView, viewLine, true) } }) - - if targetContext != self.context { - self.c.Context().Push(targetContext, types.OnFocusOpts{}) - } - - return nil } func (self *MainViewController) enter() error { @@ -674,15 +653,22 @@ func sidePanelShowsDiff(sidePanel types.Context) bool { return ok } -// focusedMainViewContextForViewName maps a focused main view's view name (as -// passed to GetOnClickFocusedMainView) to its context. -func focusedMainViewContextForViewName(c *ControllerCommon, viewName string) types.Context { +// mainContextForViewName maps a focused main view's view name (as passed to the +// side-panel handlers) to its main context — the secondary pane for the secondary +// view name, the primary pane otherwise. +func mainContextForViewName(c *ControllerCommon, viewName string) *context.MainContext { if viewName == c.Contexts().NormalSecondary.GetViewName() { return c.Contexts().NormalSecondary } return c.Contexts().Normal } +// focusedMainViewContextForViewName is mainContextForViewName as a types.Context, for +// callers that only need the interface. +func focusedMainViewContextForViewName(c *ControllerCommon, viewName string) types.Context { + return mainContextForViewName(c, viewName) +} + // focusedMainViewSnapshot records the focused main view to return to when diving // into a patch explorer from it, so escaping can come back with the main view // focused. sidePanel is the panel to land on first (which re-renders the diff --git a/pkg/gui/controllers/patch_building_from_main_view.go b/pkg/gui/controllers/patch_building_from_main_view.go index 880e51dcc..525df27ff 100644 --- a/pkg/gui/controllers/patch_building_from_main_view.go +++ b/pkg/gui/controllers/patch_building_from_main_view.go @@ -4,7 +4,6 @@ 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" ) @@ -59,7 +58,10 @@ func togglePatchFromFocusedMainView( } refresh() - revealSelectionAfterPatchToggle(c, mainViewName, firstLineIdx) + // A toggle doesn't change the diff, so source and target are the same pane; + // the re-render (and the layout re-wrap when the secondary view first appears) + // still moves the selection in view-line space, so re-establish it. + revealSelectionAfterPrimaryAction(c, mainViewName, mainViewName, firstLineIdx) return nil }, }) @@ -126,37 +128,6 @@ func togglePatchLines(c *ControllerCommon, infos []types.DiffLineInfo) error { 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 { diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 16bbb66ad..1ea6ff759 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -102,7 +102,7 @@ type IBaseContext interface { // (space), delegated to the side panel that owns the diff being shown. The // inclusive view-line range is the current selection (a single line, a range, or // a hunk). - AddOnStageFocusedMainViewFn(func(mainViewName string, firstLineIdx int, lastLineIdx int) (focusViewName string, err error)) + AddOnStageFocusedMainViewFn(func(mainViewName string, firstLineIdx int, lastLineIdx int) error) // And for toggling the selected line(s) into/out of the custom patch from the // focused main view (space), when the panel beneath builds a patch rather than // staging. @@ -338,14 +338,13 @@ type HasKeybindings interface { // that belongs to your panel while the main view is already focused. GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error - // Implement this in a side-panel controller to stage/unstage (or, later, add to - // the custom patch) the selected diff line(s) when the user presses space in the - // focused main view. The inclusive view-line range is the current selection (a - // single line, a range, or a hunk). It returns the name of the focused main view - // that should hold focus afterwards — staging/unstaging can move the acted-on - // side to the other pane — or "" when nothing was done. Return a nil func to do - // nothing. - GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) (focusViewName string, err error) + // Implement this in a side-panel controller to stage/unstage the selected diff + // line(s) when the user presses space in the focused main view. The inclusive + // view-line range is the current selection (a single line, a range, or a hunk). + // The handler re-renders the diff and re-establishes the selection itself + // (staging/unstaging can move the acted-on side to the other pane, which the + // handler then focuses). Return a nil func to do nothing. + GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) error // Implement this in a side-panel controller to toggle the selected diff line(s) // into or out of the custom patch when the user presses space in the focused main