mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-06 14:06:32 -04:00
The main view shows a diff renderer's picture of a diff, and that is not what you want on your clipboard: a renderer may drop the +/- column, move the line numbers into a gutter, group a hunk's deletions before its additions, or leave lines out altogether. So copying takes the lines from the diff itself, locating them by the identity of the selected rows. Only the panel that produced the diff can produce it again, though: the working tree's staged or unstaged side, a commit's, a stash entry's. The new seam is there for that. It asks per file, so that copying three lines of a commit's diff doesn't fetch the whole of it, and it will grow the actions on a selection as staging and patch building arrive. The clipboard gets the run of diff lines from the first selected line of a file to the last, so that lines the renderer hid come along and the result still reads as a diff. Headers count as selected lines too. A hunk header names the first line of its hunk, so it is looked for the way a line of the file is. A file header names no line at all, and a rendering may draw it over any number of rows, so a selection touching one of them takes the whole header. As in the patch explorers, a selection that is all additions or all deletions loses its +/- column, ready to be pasted into code. One that reaches into a header keeps the columns, and what comes out is a patch fragment rather than lines of code. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
271 lines
7.6 KiB
Go
271 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
|
|
focusedMainViewDiffSource types.FocusedMainViewDiffSource
|
|
onRenderToMainFn func()
|
|
onFocusFns []onFocusFn
|
|
onFocusLostFns []onFocusLostFn
|
|
onQuitFns []func()
|
|
|
|
focusable bool
|
|
transient bool
|
|
hasControlledBounds bool
|
|
needsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel
|
|
needsRerenderOnHeightChange bool
|
|
hasSelectableContent bool
|
|
|
|
*ParentContextMgr
|
|
}
|
|
|
|
type (
|
|
onFocusFn = func(types.OnFocusOpts)
|
|
onFocusLostFn = func(types.OnFocusLostOpts)
|
|
)
|
|
|
|
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
|
|
HasSelectableContent 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,
|
|
hasSelectableContent: opts.HasSelectableContent,
|
|
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) GetInputViewName() string {
|
|
return self.GetViewName()
|
|
}
|
|
|
|
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) HasSelectableContent() bool {
|
|
return self.hasSelectableContent
|
|
}
|
|
|
|
// SetHasSelectableContent is for the contexts whose answer isn't fixed and isn't a
|
|
// list length either: the main panes, which can only tell by reading the diff they
|
|
// have rendered. Whoever sets it re-derives the highlights that follow from it (see
|
|
// ContextMgr.UpdateSelectionHighlights).
|
|
func (self *BaseContext) SetHasSelectableContent(value bool) {
|
|
self.hasSelectableContent = value
|
|
}
|
|
|
|
func (self *BaseContext) GetKey() types.ContextKey {
|
|
return self.key
|
|
}
|
|
|
|
func (self *BaseContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
bindings := make([]*types.Binding, 0, len(self.keybindingsFns))
|
|
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.focusedMainViewDiffSource = 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) AddFocusedMainViewDiffSource(source types.FocusedMainViewDiffSource) {
|
|
if source != nil {
|
|
if self.focusedMainViewDiffSource != nil {
|
|
panic("only one controller is allowed to set the focused main view diff source")
|
|
}
|
|
self.focusedMainViewDiffSource = source
|
|
}
|
|
}
|
|
|
|
func (self *BaseContext) GetOnDoubleClick() func() error {
|
|
return self.onDoubleClickFn
|
|
}
|
|
|
|
func (self *BaseContext) GetOnClick() func(opts gocui.ViewMouseBindingOpts) error {
|
|
return self.onClickFn
|
|
}
|
|
|
|
func (self *BaseContext) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
|
return self.focusedMainViewDiffSource
|
|
}
|
|
|
|
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 := make([]*gocui.ViewMouseBinding, 0, len(self.mouseKeybindingsFns))
|
|
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()
|
|
}
|