mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
The focused main view already lets you point at a diff line and act on it (dive into staging, edit it, open it in a PR). This makes it a staging surface in its own right: press space to stage — or unstage — the selected line without diving into the separate staging view. To make that usable, the selection is now shown automatically whenever the main view holds a diff, anchored on the first change line already visible (so the view doesn't jump), rather than being toggled on demand from the middle of the view. That frees space for staging and means there's always a line to act on. Which contexts show a diff is marked by a new types.DiffMainViewContext, because "shows a diff" is the right signal, not "is stageable": reflog shows a diff (its selection drives edit/PR/navigation) but can't be staged, while a branch's log or the status dashboard show no diff and get no selection at all. Staging is delegated to the side panel beneath via a GetOnStageFocusedMainView handler mirroring GetOnClickFocusedMainView, so what "stage" means stays the panel's concern (the working tree stages/unstages; commits will later add to a custom patch). The files handler resolves the line's patch identity from the diff-line metadata, maps it to a patch line, and applies a one-line patch, choosing stage vs unstage from whether the shown diff is the unstaged or staged side (diffSplitState). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
272 lines
7.5 KiB
Go
272 lines
7.5 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, viewLineIdx int) 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()
|
|
}
|