jesseduffield.lazygit/pkg/gui/context/base_context.go
Stefan Haller 05c7b82a84 Follow the acted-on side to the right pane after staging from the main view
After staging or unstaging from the focused main view, focus stayed on the
pane the user acted in even when the side they were acting on moved to the
other pane. Two cases got it wrong:

- Unstaging the first hunk of an only-staged file splits the diff: the main
  half flips to show the just-unstaged change, and the staged remainder jumps
  to the secondary half — but focus stayed on the main half, away from the
  staged content the user was working through.
- Unstaging the last staged hunk from the secondary half empties the staged
  side and collapses the split, hiding the secondary half — leaving focus
  stranded on a hidden pane.

The rule is the same in both: focus the staged side while it survives. The
stage handler now reports which focused-main pane should hold focus —
the secondary half when unstaging leaves the file split, the main half
otherwise — and the controller re-selects the revealed change in that pane
and focuses it. The split is read from the model, which Refresh has already
updated synchronously by the time the handler returns; the re-render it
queues is what the reveal rides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 12:59:00 +02:00

272 lines
7.6 KiB
Go

package context
import (
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type BaseContext struct {
kind types.ContextKind
key types.ContextKey
view *gocui.View
viewTrait types.IViewTrait
windowName string
onGetOptionsMap func() map[string]string
keybindingsFns []types.KeybindingsFn
mouseKeybindingsFns []types.MouseKeybindingsFn
onDoubleClickFn func() error
onClickFn func(opts gocui.ViewMouseBindingOpts) error
onClickFocusedMainViewFn onClickFocusedMainViewFn
onStageFocusedMainViewFn onStageFocusedMainViewFn
onRenderToMainFn func()
onFocusFns []onFocusFn
onFocusLostFns []onFocusLostFn
onQuitFns []func()
focusable bool
transient bool
hasControlledBounds bool
needsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel
needsRerenderOnHeightChange bool
highlightOnFocus bool
*ParentContextMgr
}
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)
)
var _ types.IBaseContext = &BaseContext{}
type NewBaseContextOpts struct {
Kind types.ContextKind
Key types.ContextKey
View *gocui.View
WindowName string
Focusable bool
Transient bool
HasUncontrolledBounds bool // negating for the sake of making false the default
HighlightOnFocus bool
NeedsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel
NeedsRerenderOnHeightChange bool
OnGetOptionsMap func() map[string]string
}
func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
viewTrait := NewViewTrait(opts.View)
hasControlledBounds := !opts.HasUncontrolledBounds
return &BaseContext{
kind: opts.Kind,
key: opts.Key,
view: opts.View,
windowName: opts.WindowName,
onGetOptionsMap: opts.OnGetOptionsMap,
focusable: opts.Focusable,
transient: opts.Transient,
hasControlledBounds: hasControlledBounds,
highlightOnFocus: opts.HighlightOnFocus,
needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
needsRerenderOnHeightChange: opts.NeedsRerenderOnHeightChange,
ParentContextMgr: &ParentContextMgr{},
viewTrait: viewTrait,
}
}
func (self *BaseContext) GetOptionsMap() map[string]string {
if self.onGetOptionsMap != nil {
return self.onGetOptionsMap()
}
return nil
}
func (self *BaseContext) SetWindowName(windowName string) {
self.windowName = windowName
}
func (self *BaseContext) GetWindowName() string {
return self.windowName
}
func (self *BaseContext) GetViewName() string {
// for the sake of the global context which has no view
if self.view == nil {
return ""
}
return self.view.Name()
}
func (self *BaseContext) GetView() *gocui.View {
return self.view
}
func (self *BaseContext) GetViewTrait() types.IViewTrait {
return self.viewTrait
}
func (self *BaseContext) GetKind() types.ContextKind {
return self.kind
}
func (self *BaseContext) GetKey() types.ContextKey {
return self.key
}
func (self *BaseContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{}
for i := range self.keybindingsFns {
// the first binding in the bindings array takes precedence but we want the
// last keybindingsFn to take precedence to we add them in reverse
bindings = append(bindings, self.keybindingsFns[len(self.keybindingsFns)-1-i](opts)...)
}
return bindings
}
func (self *BaseContext) AddKeybindingsFn(fn types.KeybindingsFn) {
self.keybindingsFns = append(self.keybindingsFns, fn)
}
func (self *BaseContext) AddMouseKeybindingsFn(fn types.MouseKeybindingsFn) {
self.mouseKeybindingsFns = append(self.mouseKeybindingsFns, fn)
}
func (self *BaseContext) ClearAllAttachedControllerFunctions() {
self.keybindingsFns = nil
self.mouseKeybindingsFns = nil
self.onFocusFns = nil
self.onFocusLostFns = nil
self.onQuitFns = nil
self.onDoubleClickFn = nil
self.onClickFn = nil
self.onClickFocusedMainViewFn = nil
self.onStageFocusedMainViewFn = nil
self.onRenderToMainFn = nil
}
func (self *BaseContext) AddOnDoubleClickFn(fn func() error) {
if fn != nil {
if self.onDoubleClickFn != nil {
panic("only one controller is allowed to set an onDoubleClickFn")
}
self.onDoubleClickFn = fn
}
}
func (self *BaseContext) AddOnClickFn(fn func(opts gocui.ViewMouseBindingOpts) error) {
if fn != nil {
if self.onClickFn != nil {
panic("only one controller is allowed to set an onClickFn")
}
self.onClickFn = fn
}
}
func (self *BaseContext) AddOnClickFocusedMainViewFn(fn onClickFocusedMainViewFn) {
if fn != nil {
if self.onClickFocusedMainViewFn != nil {
panic("only one controller is allowed to set an onClickFocusedMainViewFn")
}
self.onClickFocusedMainViewFn = fn
}
}
func (self *BaseContext) GetOnDoubleClick() func() error {
return self.onDoubleClickFn
}
func (self *BaseContext) GetOnClick() func(opts gocui.ViewMouseBindingOpts) error {
return self.onClickFn
}
func (self *BaseContext) GetOnClickFocusedMainView() onClickFocusedMainViewFn {
return self.onClickFocusedMainViewFn
}
func (self *BaseContext) AddOnStageFocusedMainViewFn(fn onStageFocusedMainViewFn) {
if fn != nil {
if self.onStageFocusedMainViewFn != nil {
panic("only one controller is allowed to set an onStageFocusedMainViewFn")
}
self.onStageFocusedMainViewFn = fn
}
}
func (self *BaseContext) GetOnStageFocusedMainView() onStageFocusedMainViewFn {
return self.onStageFocusedMainViewFn
}
func (self *BaseContext) AddOnRenderToMainFn(fn func()) {
if fn != nil {
if self.onRenderToMainFn != nil {
panic("only one controller is allowed to set an onRenderToMainFn")
}
self.onRenderToMainFn = fn
}
}
func (self *BaseContext) AddOnFocusFn(fn onFocusFn) {
if fn != nil {
self.onFocusFns = append(self.onFocusFns, fn)
}
}
func (self *BaseContext) AddOnFocusLostFn(fn onFocusLostFn) {
if fn != nil {
self.onFocusLostFns = append(self.onFocusLostFns, fn)
}
}
func (self *BaseContext) AddOnQuitFn(fn func()) {
if fn != nil {
self.onQuitFns = append(self.onQuitFns, fn)
}
}
func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
bindings := []*gocui.ViewMouseBinding{}
for i := range self.mouseKeybindingsFns {
// the first binding in the bindings array takes precedence but we want the
// last keybindingsFn to take precedence to we add them in reverse
bindings = append(bindings, self.mouseKeybindingsFns[len(self.mouseKeybindingsFns)-1-i](opts)...)
}
return bindings
}
func (self *BaseContext) IsFocusable() bool {
return self.focusable
}
func (self *BaseContext) IsTransient() bool {
return self.transient
}
func (self *BaseContext) HasControlledBounds() bool {
return self.hasControlledBounds
}
func (self *BaseContext) NeedsRerenderOnWidthChange() types.NeedsRerenderOnWidthChangeLevel {
return self.needsRerenderOnWidthChange
}
func (self *BaseContext) NeedsRerenderOnHeightChange() bool {
return self.needsRerenderOnHeightChange
}
func (self *BaseContext) Title() string {
return ""
}
func (self *BaseContext) TotalContentHeight() int {
return self.view.ViewLinesHeight()
}