jesseduffield.lazygit/pkg/gui/context/base_context.go
Stefan Haller 67ba1899b4 Build a custom patch from the focused main view of a commit's files
Pressing space in the commit-files focused main view now toggles the
selected line(s)/hunk into or out of the custom patch, instead of being a
no-op. It's the patch-building counterpart of working-tree staging from
the main view: the selection is resolved to change-line identities the
same way (ChangeLinesInViewRange), then mapped to the patch builder's
per-file line indices and added or removed.

The crux is that the commit's diff is unchanged by a toggle — only the
inclusion set changes — so unlike staging there's no async re-render to
ride. Membership is shown by an on-demand inclusion gutter: a reserved
left column painted with a marker on every change line currently in the
patch, recomputed synchronously after each toggle and on focusing the
main view, over the existing pager output. This is what lets patch
building work over a restructuring pager at all, where the old green
first-char overlay (which rewrites the rendered bytes) can't.

space routes to staging or patch building based on which handler the
panel beneath registers, via a new onTogglePatchFocusedMainView channel
(kept separate from staging because the post-action differs: sync gutter
repaint vs async reveal). Scoped to the commit-files panel for now;
commits/sub-commits/stash and the whole-commit multi-file diff follow.

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

288 lines
8.3 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
onTogglePatchFocusedMainViewFn onTogglePatchFocusedMainViewFn
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)
onTogglePatchFocusedMainViewFn = func(mainViewName string, firstLineIdx int, lastLineIdx 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.onTogglePatchFocusedMainViewFn = 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) AddOnTogglePatchFocusedMainViewFn(fn onTogglePatchFocusedMainViewFn) {
if fn != nil {
if self.onTogglePatchFocusedMainViewFn != nil {
panic("only one controller is allowed to set an onTogglePatchFocusedMainViewFn")
}
self.onTogglePatchFocusedMainViewFn = fn
}
}
func (self *BaseContext) GetOnTogglePatchFocusedMainView() onTogglePatchFocusedMainViewFn {
return self.onTogglePatchFocusedMainViewFn
}
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()
}