summaryrefslogtreecommitdiffstats
path: root/pkg/gui/patch_exploring
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/gui/patch_exploring')
-rw-r--r--pkg/gui/patch_exploring/focus.go53
-rw-r--r--pkg/gui/patch_exploring/focus_test.go100
-rw-r--r--pkg/gui/patch_exploring/state.go212
3 files changed, 365 insertions, 0 deletions
diff --git a/pkg/gui/patch_exploring/focus.go b/pkg/gui/patch_exploring/focus.go
new file mode 100644
index 000000000..cf0908999
--- /dev/null
+++ b/pkg/gui/patch_exploring/focus.go
@@ -0,0 +1,53 @@
+package patch_exploring
+
+import "github.com/jesseduffield/lazygit/pkg/utils"
+
+func calculateOrigin(currentOrigin int, bufferHeight int, firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) int {
+ needToSeeIdx, wantToSeeIdx := getNeedAndWantLineIdx(firstLineIdx, lastLineIdx, selectedLineIdx, mode)
+
+ return calculateNewOriginWithNeededAndWantedIdx(currentOrigin, bufferHeight, needToSeeIdx, wantToSeeIdx)
+}
+
+// we want to scroll our origin so that the index we need to see is in view
+// and the other index we want to see (e.g. the other side of a line range)
+// is in as close to being in view as possible.
+func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, needToSeeIdx int, wantToSeeIdx int) int {
+ origin := currentOrigin
+ if needToSeeIdx < currentOrigin {
+ origin = needToSeeIdx
+ } else if needToSeeIdx > currentOrigin+bufferHeight {
+ origin = needToSeeIdx - bufferHeight
+ }
+
+ bottom := origin + bufferHeight
+
+ if wantToSeeIdx < origin {
+ requiredChange := origin - wantToSeeIdx
+ allowedChange := bottom - needToSeeIdx
+ return origin - utils.Min(requiredChange, allowedChange)
+ } else if wantToSeeIdx > origin+bufferHeight {
+ requiredChange := wantToSeeIdx - bottom
+ allowedChange := needToSeeIdx - origin
+ return origin + utils.Min(requiredChange, allowedChange)
+ } else {
+ return origin
+ }
+}
+
+func getNeedAndWantLineIdx(firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) (int, int) {
+ switch mode {
+ case LINE:
+ return selectedLineIdx, selectedLineIdx
+ case RANGE:
+ if selectedLineIdx == firstLineIdx {
+ return firstLineIdx, lastLineIdx
+ } else {
+ return lastLineIdx, firstLineIdx
+ }
+ case HUNK:
+ return firstLineIdx, lastLineIdx
+ default:
+ // we should never land here
+ panic("unknown mode")
+ }
+}
diff --git a/pkg/gui/patch_exploring/focus_test.go b/pkg/gui/patch_exploring/focus_test.go
new file mode 100644
index 000000000..eb3ed7c66
--- /dev/null
+++ b/pkg/gui/patch_exploring/focus_test.go
@@ -0,0 +1,100 @@
+package patch_exploring
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewOrigin(t *testing.T) {
+ type scenario struct {
+ name string
+ origin int
+ bufferHeight int
+ firstLineIdx int
+ lastLineIdx int
+ selectedLineIdx int
+ selectMode selectMode
+ expected int
+ }
+
+ scenarios := []scenario{
+ {
+ name: "selection above scroll window",
+ origin: 50,
+ bufferHeight: 100,
+ firstLineIdx: 10,
+ lastLineIdx: 10,
+ selectedLineIdx: 10,
+ selectMode: LINE,
+ expected: 10,
+ },
+ {
+ name: "selection below scroll window",
+ origin: 0,
+ bufferHeight: 100,
+ firstLineIdx: 150,
+ lastLineIdx: 150,
+ selectedLineIdx: 150,
+ selectMode: LINE,
+ expected: 50,
+ },
+ {
+ name: "selection within scroll window",
+ origin: 0,
+ bufferHeight: 100,
+ firstLineIdx: 50,
+ lastLineIdx: 50,
+ selectedLineIdx: 50,
+ selectMode: LINE,
+ expected: 0,
+ },
+ {
+ name: "range ending below scroll window with selection at end of range",
+ origin: 0,
+ bufferHeight: 100,
+ firstLineIdx: 40,
+ lastLineIdx: 150,
+ selectedLineIdx: 150,
+ selectMode: RANGE,
+ expected: 50,
+ },
+ {
+ name: "range ending below scroll window with selection at beginning of range",
+ origin: 0,
+ bufferHeight: 100,
+ firstLineIdx: 40,
+ lastLineIdx: 150,
+ selectedLineIdx: 40,
+ selectMode: RANGE,
+ expected: 40,
+ },
+ {
+ name: "range starting above scroll window with selection at beginning of range",
+ origin: 50,
+ bufferHeight: 100,
+ firstLineIdx: 40,
+ lastLineIdx: 150,
+ selectedLineIdx: 40,
+ selectMode: RANGE,
+ expected: 40,
+ },
+ {
+ name: "hunk extending beyond both bounds of scroll window",
+ origin: 50,
+ bufferHeight: 100,
+ firstLineIdx: 40,
+ lastLineIdx: 200,
+ selectedLineIdx: 70,
+ selectMode: HUNK,
+ expected: 40,
+ },
+ }
+
+ for _, s := range scenarios {
+ s := s
+ t.Run(s.name, func(t *testing.T) {
+ assert.EqualValues(t, s.expected, calculateOrigin(s.origin, s.bufferHeight, s.firstLineIdx, s.lastLineIdx, s.selectedLineIdx, s.selectMode))
+ })
+ }
+}
diff --git a/pkg/gui/patch_exploring/state.go b/pkg/gui/patch_exploring/state.go
new file mode 100644
index 000000000..008338326
--- /dev/null
+++ b/pkg/gui/patch_exploring/state.go
@@ -0,0 +1,212 @@
+package patch_exploring
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/commands/patch"
+ "github.com/sirupsen/logrus"
+)
+
+// State represents the current state of the patch explorer context i.e. when
+// you're staging a file or you're building a patch from an existing commit
+// this struct holds the info about the diff you're interacting with and what's currently selected.
+type State struct {
+ selectedLineIdx int
+ rangeStartLineIdx int
+ diff string
+ patchParser *patch.PatchParser
+ selectMode selectMode
+}
+
+// these represent what select mode we're in
+type selectMode int
+
+const (
+ LINE selectMode = iota
+ RANGE
+ HUNK
+)
+
+func NewState(diff string, selectedLineIdx int, oldState *State, log *logrus.Entry) *State {
+ if oldState != nil && diff == oldState.diff && selectedLineIdx == -1 {
+ // if we're here then we can return the old state. If selectedLineIdx was not -1
+ // then that would mean we were trying to click and potentiall drag a range, which
+ // is why in that case we continue below
+ return oldState
+ }
+
+ patchParser := patch.NewPatchParser(log, diff)
+
+ if len(patchParser.StageableLines) == 0 {
+ return nil
+ }
+
+ rangeStartLineIdx := 0
+ if oldState != nil {
+ rangeStartLineIdx = oldState.rangeStartLineIdx
+ }
+
+ selectMode := LINE
+ // if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
+ if selectedLineIdx >= 0 {
+ selectMode = RANGE
+ rangeStartLineIdx = selectedLineIdx
+ } else if oldState != nil {
+ // if we previously had a selectMode of RANGE, we want that to now be line again
+ if oldState.selectMode == HUNK {
+ selectMode = HUNK
+ }
+ selectedLineIdx = patchParser.GetNextStageableLineIndex(oldState.selectedLineIdx)
+ } else {
+ selectedLineIdx = patchParser.StageableLines[0]
+ }
+
+ return &State{
+ patchParser: patchParser,
+ selectedLineIdx: selectedLineIdx,
+ selectMode: selectMode,
+ rangeStartLineIdx: rangeStartLineIdx,
+ diff: diff,
+ }
+}
+
+func (s *State) GetSelectedLineIdx() int {
+ return s.selectedLineIdx
+}
+
+func (s *State) GetDiff() string {
+ return s.diff
+}
+
+func (s *State) ToggleSelectHunk() {
+ if s.selectMode == HUNK {
+ s.selectMode = LINE
+ } else {
+ s.selectMode = HUNK
+ }
+}
+
+func (s *State) ToggleSelectRange() {
+ if s.selectMode == RANGE {
+ s.selectMode = LINE
+ } else {
+ s.selectMode = RANGE
+ s.rangeStartLineIdx = s.selectedLineIdx
+ }
+}
+
+func (s *State) SelectingHunk() bool {
+ return s.selectMode == HUNK
+}
+
+func (s *State) SelectingRange() bool {
+ return s.selectMode == RANGE
+}
+
+func (s *State) SelectingLine() bool {
+ return s.selectMode == LINE
+}
+
+func (s *State) SetLineSelectMode() {
+ s.selectMode = LINE
+}
+
+func (s *State) SelectLine(newSelectedLineIdx int) {
+ if newSelectedLineIdx < 0 {
+ newSelectedLineIdx = 0
+ } else if newSelectedLineIdx > len(s.patchParser.PatchLines)-1 {
+ newSelectedLineIdx = len(s.patchParser.PatchLines) - 1
+ }
+
+ s.selectedLineIdx = newSelectedLineIdx
+}
+
+func (s *State) SelectNewLineForRange(newSelectedLineIdx int) {
+ s.rangeStartLineIdx = newSelectedLineIdx
+
+ s.selectMode = RANGE
+
+ s.SelectLine(newSelectedLineIdx)
+}
+
+func (s *State) CycleSelection(forward bool) {
+ if s.SelectingHunk() {
+ s.CycleHunk(forward)
+ } else {
+ s.CycleLine(forward)
+ }
+}
+
+func (s *State) CycleHunk(forward bool) {
+ change := 1
+ if !forward {
+ change = -1
+ }
+
+ newHunk := s.patchParser.GetHunkContainingLine(s.selectedLineIdx, change)
+ s.selectedLineIdx = s.patchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
+}
+
+func (s *State) CycleLine(forward bool) {
+ change := 1
+ if !forward {
+ change = -1
+ }
+
+ s.SelectLine(s.selectedLineIdx + change)
+}
+
+func (s *State) CurrentHunk() *patch.PatchHunk {
+ return s.patchParser.GetHunkContainingLine(s.selectedLineIdx, 0)
+}
+
+func (s *State) SelectedRange() (int, int) {
+ switch s.selectMode {
+ case HUNK:
+ hunk := s.CurrentHunk()
+ return hunk.FirstLineIdx, hunk.LastLineIdx()
+ case RANGE:
+ if s.rangeStartLineIdx > s.selectedLineIdx {
+ return s.selectedLineIdx, s.rangeStartLineIdx
+ } else {
+ return s.rangeStartLineIdx, s.selectedLineIdx
+ }
+ case LINE:
+ return s.selectedLineIdx, s.selectedLineIdx
+ default:
+ // should never happen
+ return 0, 0
+ }
+}
+
+func (s *State) CurrentLineNumber() int {
+ return s.CurrentHunk().LineNumberOfLine(s.selectedLineIdx)
+}
+
+func (s *State) AdjustSelectedLineIdx(change int) {
+ s.SelectLine(s.selectedLineIdx + change)
+}
+
+func (s *State) RenderForLineIndices(isFocused bool, includedLineIndices []int) string {
+ firstLineIdx, lastLineIdx := s.SelectedRange()
+ return s.patchParser.Render(isFocused, firstLineIdx, lastLineIdx, includedLineIndices)
+}
+
+func (s *State) PlainRenderSelected() string {
+ firstLineIdx, lastLineIdx := s.SelectedRange()
+ return s.patchParser.RenderLinesPlain(firstLineIdx, lastLineIdx)
+}
+
+func (s *State) SelectBottom() {
+ s.SetLineSelectMode()
+ s.SelectLine(len(s.patchParser.PatchLines) - 1)
+}
+
+func (s *State) SelectTop() {
+ s.SetLineSelectMode()
+ s.SelectLine(0)
+}
+
+func (s *State) CalculateOrigin(currentOrigin int, bufferHeight int) int {
+ firstLineIdx, lastLineIdx := s.SelectedRange()
+
+ return calculateOrigin(currentOrigin, bufferHeight, firstLineIdx, lastLineIdx, s.GetSelectedLineIdx(), s.selectMode)
+}