diff options
Diffstat (limited to 'pkg/gui/patch_exploring')
-rw-r--r-- | pkg/gui/patch_exploring/focus.go | 53 | ||||
-rw-r--r-- | pkg/gui/patch_exploring/focus_test.go | 100 | ||||
-rw-r--r-- | pkg/gui/patch_exploring/state.go | 212 |
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) +} |