mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-14 17:46:26 -04:00
Step 1 made the focused main view a staging surface for a single line. This adds the range and hunk selection the staging view has, so you can stage (or unstage) a range or a whole hunk in place — `v` starts a range, shift-up/down extend one, `a` toggles hunk selection — without diving into the staging view. The selection mode (line / range / hunk, plus sticky-range and "the user turned hunk mode on themselves") lives on MainContext rather than the controller, because three places need a pane's mode: the main view controller that drives it, the focus controller that resets it when you focus the view, and togglePanel which sets it on the *other* pane. The selected line and the range anchor stay in the gocui view itself (its cursor and rangeSelectStartY), so the existing native range-select rendering draws the highlight — no new highlight machinery. Hunk bounds come from the diff-line metadata (the isChange run around the cursor, via ChangeBlockBounds), not from a parsed patch, so they work over any conforming rendering. The up/down keys are mode-aware: hunk mode steps hunk to hunk, a non-sticky range collapses on a plain move, a sticky range extends. GetOnStageFocusedMainView now takes the selected view-line range instead of one line; the files handler collects the change lines in the range and applies a single patch. It identifies each change line's patch line by scanning the parsed patch and matching (file line number, deletion?) identities, rather than looking each number up with PatchLineForLineNumber. That lookup can't tell an addition at the very start of a hunk from the deletion above it, nor an addition from the deletion it replaces on a modified line — both of which a range routinely spans. As a side effect this also fixes staging a change on the first line of a file. Co-Authored-By: Claude Opus 4.8 (1M context) <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, 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.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()
|
|
}
|