mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
Give the focused main view's selection a home outside the controllers
Where the selection starts out, and how it widens to a whole change block, are questions about what the view is showing — the same rendered diff the queries next door read. Nothing about them belongs to a keybinding, and the render funnel is about to need them too, from a layer that can reach a helper but not a controller. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4146c52f4f
commit
7df7bc6276
@@ -223,17 +223,6 @@ func (self *DiffLineHelper) ChangeBlockBounds(view *gocui.View, anchorViewLine i
|
||||
return startView, endView, true
|
||||
}
|
||||
|
||||
// SelectedHunkBounds returns the change block selected in hunk mode. The range
|
||||
// anchor stays on the block's far end when a click moves the cursor before its
|
||||
// handler runs, so it still identifies the selected block.
|
||||
func (self *DiffLineHelper) SelectedHunkBounds(view *gocui.View) (int, int, bool) {
|
||||
anchor := view.RangeSelectStartY()
|
||||
if anchor < 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return self.ChangeBlockBounds(view, anchor)
|
||||
}
|
||||
|
||||
// AdjacentChangeBlock returns the view line to move to for next/previous change-block
|
||||
// navigation in view's rendered diff, starting from anchorViewLine. A change block is
|
||||
// lazygit's notion of a hunk (see ChangeBlockBounds). forward=true targets the start
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Putting a selection in the focused main view: where it starts out, and how it is
|
||||
// widened to a whole change block. Both are answered from what the view is showing,
|
||||
// as recovered by the queries next door.
|
||||
|
||||
// EstablishSelection turns on the focused main view's selection once the view has
|
||||
// been focused. clickedViewLine is the view line a click pointed at, or -1 for
|
||||
// keyboard focus, which points at no particular line and so starts at the first
|
||||
// change line on screen.
|
||||
//
|
||||
// Focusing never moves the view: you focus the diff you are reading in order to point
|
||||
// at something in it, so the selection goes where you are looking rather than the
|
||||
// view going where the selection would like to be. With no change line on screen at
|
||||
// all — a long stretch of context — it lands on the middle visible line, the likeliest
|
||||
// one to be the one being read.
|
||||
//
|
||||
// With hunk mode configured as the default the selection widens to the whole change
|
||||
// block: keyboard focus lands on the first block on screen, and a click on a change
|
||||
// line selects that line's block, ready to act on. A click on context still selects
|
||||
// just that line — the click points at it precisely, so it stays editable.
|
||||
func (self *DiffLineHelper) EstablishSelection(mainContext *context.MainContext, clickedViewLine int) {
|
||||
mainContext.ResetDiffSelectMode()
|
||||
view := mainContext.GetView()
|
||||
|
||||
// The panel beneath renders a diff, but that diff may hold nothing to act on: a
|
||||
// binary file, or an empty commit. Rendering it worked that out, so the pane is
|
||||
// already showing no selection and there is nowhere to put one.
|
||||
if !self.ViewHasChangeLines(view) {
|
||||
return
|
||||
}
|
||||
|
||||
if clickedViewLine >= 0 {
|
||||
// Remember where the click landed so that a drag that follows anchors its range
|
||||
// there, even when this click selects a whole hunk.
|
||||
mainContext.SetDragAnchorViewLine(clickedViewLine)
|
||||
if self.hunkModeApplies(view, clickedViewLine) && self.IsChangeLine(view, clickedViewLine) {
|
||||
mainContext.DiffSelectState().Mode = types.DiffSelectModeHunk
|
||||
self.SelectChangeBlock(mainContext, clickedViewLine, false)
|
||||
return
|
||||
}
|
||||
self.ShowSelectionAtLine(view, clickedViewLine, false)
|
||||
return
|
||||
}
|
||||
|
||||
target, ok := self.changeToSelectOnScreen(view)
|
||||
if !ok {
|
||||
self.ShowSelectionAtLine(view, view.MiddleVisibleLineIdx(), false)
|
||||
return
|
||||
}
|
||||
if self.hunkModeApplies(view, target) {
|
||||
mainContext.DiffSelectState().Mode = types.DiffSelectModeHunk
|
||||
self.SelectChangeBlock(mainContext, target, false)
|
||||
return
|
||||
}
|
||||
self.ShowSelectionAtLine(view, target, false)
|
||||
}
|
||||
|
||||
// changeToSelectOnScreen returns the change line keyboard focus establishes the
|
||||
// selection on. In hunk mode that is the first block that begins on screen, so that
|
||||
// the block being offered up is one the user can see the extent of, falling back to a
|
||||
// block that reaches into the view from above — a change longer than the screen, where
|
||||
// there is nothing else to offer. Line by line it is simply the first change line on
|
||||
// screen. ok is false when the viewport shows no change at all.
|
||||
func (self *DiffLineHelper) changeToSelectOnScreen(view *gocui.View) (int, bool) {
|
||||
if self.c.UserConfig().Gui.UseHunkModeInStagingView {
|
||||
return self.FirstChangeBlockInView(view)
|
||||
}
|
||||
return self.FirstChangeLineInView(view)
|
||||
}
|
||||
|
||||
// hunkModeApplies reports whether an established selection should start out as the
|
||||
// whole change block around the given change line. That's what the config asks for,
|
||||
// except over a file shown as one solid block of changes, where it would select the
|
||||
// whole file — see IsSingleHunkForWholeFile.
|
||||
func (self *DiffLineHelper) hunkModeApplies(view *gocui.View, changeViewLine int) bool {
|
||||
return self.c.UserConfig().Gui.UseHunkModeInStagingView &&
|
||||
!self.IsSingleHunkForWholeFile(view, changeViewLine)
|
||||
}
|
||||
|
||||
// ShowSelectionAtLine moves the focused main view's selection to the given view line,
|
||||
// clamped to the content. scrollIntoView scrolls the line into view when it's
|
||||
// off-screen, for navigating to it; a click leaves it false, the clicked line being on
|
||||
// screen already.
|
||||
func (self *DiffLineHelper) ShowSelectionAtLine(view *gocui.View, lineIdx int, scrollIntoView bool) {
|
||||
view.FocusPoint(0, lo.Clamp(lineIdx, 0, max(0, view.ViewLinesHeight()-1)), scrollIntoView)
|
||||
|
||||
// A search carries on from where the selection now is, so that stepping to the
|
||||
// next match goes to the one after it rather than the one after the match the
|
||||
// user last stepped to.
|
||||
view.SetNearestSearchPosition()
|
||||
}
|
||||
|
||||
// SelectChangeBlock selects the whole change block around the given change line, for
|
||||
// hunk mode: the cursor goes to the block's first line and the range anchor to its
|
||||
// last, so the native range highlight spans the block. With no block to be found —
|
||||
// a diff with no changes in it — it falls back to a single-line selection.
|
||||
//
|
||||
// scrollIntoView brings the block's first line on screen, for the commands that mean
|
||||
// to go there; a click leaves it false, so that the view doesn't move under the mouse
|
||||
// when the block the click landed in starts above the viewport.
|
||||
func (self *DiffLineHelper) SelectChangeBlock(
|
||||
pane types.DiffPaneContext, changeViewLine int, scrollIntoView bool,
|
||||
) {
|
||||
view := pane.GetView()
|
||||
start, end, ok := self.ChangeBlockBounds(view, changeViewLine)
|
||||
if !ok {
|
||||
pane.DiffSelectState().Mode = types.DiffSelectModeLine
|
||||
view.CancelRangeSelect()
|
||||
self.ShowSelectionAtLine(view, changeViewLine, scrollIntoView)
|
||||
return
|
||||
}
|
||||
view.SetRangeSelectStart(end)
|
||||
self.ShowSelectionAtLine(view, start, scrollIntoView)
|
||||
}
|
||||
|
||||
// SelectedHunkBounds returns the change block selected in hunk mode. The range
|
||||
// anchor stays on the block's far end when a click moves the cursor before its
|
||||
// handler runs, so it still identifies the selected block.
|
||||
func (self *DiffLineHelper) SelectedHunkBounds(view *gocui.View) (int, int, bool) {
|
||||
anchor := view.RangeSelectStartY()
|
||||
if anchor < 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return self.ChangeBlockBounds(view, anchor)
|
||||
}
|
||||
@@ -207,7 +207,7 @@ func (self *MainViewController) togglePanel() error {
|
||||
isDiff := self.isDiffView()
|
||||
self.c.Context().Push(self.otherContext, types.OnFocusOpts{})
|
||||
if isDiff {
|
||||
establishDiffSelection(self.c, self.otherContext, -1)
|
||||
self.c.Helpers().DiffLine.EstablishSelection(self.otherContext, -1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -322,11 +322,11 @@ func revealSelectionAfterAction(c *ControllerCommon, pane types.DiffPaneContext,
|
||||
|
||||
c.Helpers().DiffLine.RevealChangeLineAtOrdinal(view, ordinal, func(viewLine int) {
|
||||
if selectHunk {
|
||||
selectDiffHunk(c, pane, viewLine, true)
|
||||
c.Helpers().DiffLine.SelectChangeBlock(pane, viewLine, true)
|
||||
return
|
||||
}
|
||||
view.CancelRangeSelect()
|
||||
showSelectionAtLine(view, viewLine, true)
|
||||
c.Helpers().DiffLine.ShowSelectionAtLine(view, viewLine, true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -539,7 +539,7 @@ func (self *MainViewController) selectClickedDiffLine(viewLine int) {
|
||||
if start, end, ok := self.c.Helpers().DiffLine.SelectedHunkBounds(view); ok &&
|
||||
viewLine >= start && viewLine <= end {
|
||||
self.context.ResetDiffSelectMode()
|
||||
showSelectionAtLine(view, viewLine, false)
|
||||
self.c.Helpers().DiffLine.ShowSelectionAtLine(view, viewLine, false)
|
||||
return
|
||||
}
|
||||
if self.c.Helpers().DiffLine.IsChangeLine(view, viewLine) {
|
||||
@@ -548,122 +548,11 @@ func (self *MainViewController) selectClickedDiffLine(viewLine int) {
|
||||
}
|
||||
}
|
||||
self.context.ResetDiffSelectMode()
|
||||
showSelectionAtLine(view, viewLine, false)
|
||||
}
|
||||
|
||||
// establishDiffSelection turns on the focused main view's selection once the view has
|
||||
// been focused. clickedViewLine is the view line a click pointed at, or -1 for
|
||||
// keyboard focus, which points at no particular line and so starts at the first
|
||||
// change line on screen.
|
||||
//
|
||||
// Focusing never moves the view: you focus the diff you are reading in order to point
|
||||
// at something in it, so the selection goes where you are looking rather than the
|
||||
// view going where the selection would like to be. With no change line on screen at
|
||||
// all — a long stretch of context — it lands on the middle visible line, the likeliest
|
||||
// one to be the one being read.
|
||||
//
|
||||
// With hunk mode configured as the default the selection widens to the whole change
|
||||
// block: keyboard focus lands on the first block on screen, and a click on a change
|
||||
// line selects that line's block, ready to act on. A click on context still selects
|
||||
// just that line — the click points at it precisely, so it stays editable.
|
||||
func establishDiffSelection(c *ControllerCommon, mainContext *context.MainContext, clickedViewLine int) {
|
||||
mainContext.ResetDiffSelectMode()
|
||||
view := mainContext.GetView()
|
||||
|
||||
// The panel beneath renders a diff, but that diff may hold nothing to act on: a
|
||||
// binary file, or an empty commit. Rendering it worked that out, so the pane is
|
||||
// already showing no selection and there is nowhere to put one.
|
||||
if !c.Helpers().DiffLine.ViewHasChangeLines(view) {
|
||||
return
|
||||
}
|
||||
|
||||
if clickedViewLine >= 0 {
|
||||
// Remember where the click landed so that a drag that follows anchors its range
|
||||
// there, even when this click selects a whole hunk.
|
||||
mainContext.SetDragAnchorViewLine(clickedViewLine)
|
||||
if hunkModeApplies(c, view, clickedViewLine) &&
|
||||
c.Helpers().DiffLine.IsChangeLine(view, clickedViewLine) {
|
||||
mainContext.DiffSelectState().Mode = types.DiffSelectModeHunk
|
||||
selectDiffHunk(c, mainContext, clickedViewLine, false)
|
||||
return
|
||||
}
|
||||
showSelectionAtLine(view, clickedViewLine, false)
|
||||
return
|
||||
}
|
||||
|
||||
target, ok := changeToSelectOnScreen(c, view)
|
||||
if !ok {
|
||||
showSelectionAtLine(view, view.MiddleVisibleLineIdx(), false)
|
||||
return
|
||||
}
|
||||
if hunkModeApplies(c, view, target) {
|
||||
mainContext.DiffSelectState().Mode = types.DiffSelectModeHunk
|
||||
selectDiffHunk(c, mainContext, target, false)
|
||||
return
|
||||
}
|
||||
showSelectionAtLine(view, target, false)
|
||||
}
|
||||
|
||||
// changeToSelectOnScreen returns the change line keyboard focus establishes the
|
||||
// selection on. In hunk mode that is the first block that begins on screen, so that
|
||||
// the block being offered up is one the user can see the extent of, falling back to a
|
||||
// block that reaches into the view from above — a change longer than the screen, where
|
||||
// there is nothing else to offer. Line by line it is simply the first change line on
|
||||
// screen. ok is false when the viewport shows no change at all.
|
||||
func changeToSelectOnScreen(c *ControllerCommon, view *gocui.View) (int, bool) {
|
||||
if c.UserConfig().Gui.UseHunkModeInStagingView {
|
||||
return c.Helpers().DiffLine.FirstChangeBlockInView(view)
|
||||
}
|
||||
return c.Helpers().DiffLine.FirstChangeLineInView(view)
|
||||
}
|
||||
|
||||
// hunkModeApplies reports whether an established selection should start out as the
|
||||
// whole change block around the given change line. That's what the config asks for,
|
||||
// except over a file shown as one solid block of changes, where it would select the
|
||||
// whole file — see DiffLineHelper.IsSingleHunkForWholeFile.
|
||||
func hunkModeApplies(c *ControllerCommon, view *gocui.View, changeViewLine int) bool {
|
||||
return c.UserConfig().Gui.UseHunkModeInStagingView &&
|
||||
!c.Helpers().DiffLine.IsSingleHunkForWholeFile(view, changeViewLine)
|
||||
}
|
||||
|
||||
// showSelectionAtLine moves the focused main view's selection to the given view line,
|
||||
// clamped to the content. scrollIntoView scrolls the line into view when it's
|
||||
// off-screen, for navigating to it; a click leaves it false, the clicked line being on
|
||||
// screen already.
|
||||
func showSelectionAtLine(view *gocui.View, lineIdx int, scrollIntoView bool) {
|
||||
view.FocusPoint(0, lo.Clamp(lineIdx, 0, max(0, view.ViewLinesHeight()-1)), scrollIntoView)
|
||||
|
||||
// A search carries on from where the selection now is, so that stepping to the
|
||||
// next match goes to the one after it rather than the one after the match the
|
||||
// user last stepped to.
|
||||
view.SetNearestSearchPosition()
|
||||
self.c.Helpers().DiffLine.ShowSelectionAtLine(view, viewLine, false)
|
||||
}
|
||||
|
||||
func (self *MainViewController) selectHunkAround(changeViewLine int, scrollIntoView bool) {
|
||||
selectDiffHunk(self.c, self.context, changeViewLine, scrollIntoView)
|
||||
}
|
||||
|
||||
// selectDiffHunk selects the whole change block around the given change line, for
|
||||
// hunk mode: the cursor goes to the block's first line and the range anchor to its
|
||||
// last, so the native range highlight spans the block. With no block to be found —
|
||||
// a diff with no changes in it — it falls back to a single-line selection.
|
||||
//
|
||||
// scrollIntoView brings the block's first line on screen, for the commands that mean
|
||||
// to go there; a click leaves it false, so that the view doesn't move under the mouse
|
||||
// when the block the click landed in starts above the viewport.
|
||||
func selectDiffHunk(
|
||||
c *ControllerCommon, pane types.DiffPaneContext, changeViewLine int, scrollIntoView bool,
|
||||
) {
|
||||
view := pane.GetView()
|
||||
start, end, ok := c.Helpers().DiffLine.ChangeBlockBounds(view, changeViewLine)
|
||||
if !ok {
|
||||
pane.DiffSelectState().Mode = types.DiffSelectModeLine
|
||||
view.CancelRangeSelect()
|
||||
showSelectionAtLine(view, changeViewLine, scrollIntoView)
|
||||
return
|
||||
}
|
||||
view.SetRangeSelectStart(end)
|
||||
showSelectionAtLine(view, start, scrollIntoView)
|
||||
self.c.Helpers().DiffLine.SelectChangeBlock(self.context, changeViewLine, scrollIntoView)
|
||||
}
|
||||
|
||||
// navigate moves the focused main view to the row find locates from the current
|
||||
@@ -762,7 +651,7 @@ func (self *MainViewController) placeNavigationTarget(target int, alignTop bool)
|
||||
}
|
||||
// Line mode leaves a single-line selection at the target; an active range extends
|
||||
// to it, the anchor being untouched.
|
||||
showSelectionAtLine(v, target, true)
|
||||
self.c.Helpers().DiffLine.ShowSelectionAtLine(v, target, true)
|
||||
}
|
||||
|
||||
// scrollTargetToTop scrolls the given row of the diff to the top of the view, leaving
|
||||
@@ -794,7 +683,7 @@ func (self *MainViewController) moveCursor(delta int) {
|
||||
} else if delta == 1 {
|
||||
checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig(), before, after)
|
||||
}
|
||||
showSelectionAtLine(v, after, true)
|
||||
self.c.Helpers().DiffLine.ShowSelectionAtLine(v, after, true)
|
||||
}
|
||||
|
||||
// collapseForLineMove drops hunk mode, and a non-sticky range, back to a single-line
|
||||
@@ -841,7 +730,7 @@ func (self *MainViewController) adjustSelection(delta int) {
|
||||
// of the diff — dropping hunk mode and a non-sticky range like a plain move does.
|
||||
func (self *MainViewController) selectAbsoluteLine(target int) {
|
||||
self.collapseForLineMove()
|
||||
showSelectionAtLine(self.context.GetView(), target, true)
|
||||
self.c.Helpers().DiffLine.ShowSelectionAtLine(self.context.GetView(), target, true)
|
||||
}
|
||||
|
||||
// selectingRange reports whether a range selection is currently active: we're in
|
||||
|
||||
@@ -84,7 +84,7 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *co
|
||||
self.c.Context().Push(mainViewContext, types.OnFocusOpts{})
|
||||
|
||||
if _, ok := self.context.(types.DiffMainViewContext); ok {
|
||||
establishDiffSelection(self.c, mainViewContext, clickedLineIdx)
|
||||
self.c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user