diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 4d593dbd0..a74f3c68c 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -1485,14 +1485,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { ViewName: "main", Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.PrevItem), - Handler: gui.handleSelectTop, + Handler: gui.handleSelectPrevConflictHunk, Description: gui.Tr.SelectTop, }, { ViewName: "main", Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.NextItem), - Handler: gui.handleSelectBottom, + Handler: gui.handleSelectNextConflictHunk, Description: gui.Tr.SelectBottom, }, { @@ -1500,14 +1500,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, - Handler: gui.handleSelectTop, + Handler: gui.handleSelectPrevConflictHunk, }, { ViewName: "main", Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, - Handler: gui.handleSelectBottom, + Handler: gui.handleSelectNextConflictHunk, }, { ViewName: "main", @@ -1528,14 +1528,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.PrevItemAlt), Modifier: gocui.ModNone, - Handler: gui.handleSelectTop, + Handler: gui.handleSelectPrevConflictHunk, }, { ViewName: "main", Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.NextItemAlt), Modifier: gocui.ModNone, - Handler: gui.handleSelectBottom, + Handler: gui.handleSelectNextConflictHunk, }, { ViewName: "main", diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 51376ccf8..0206ba164 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -14,18 +14,18 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" ) -func (gui *Gui) handleSelectTop() error { +func (gui *Gui) handleSelectPrevConflictHunk() error { return gui.withMergeConflictLock(func() error { gui.takeOverMergeConflictScrolling() - gui.State.Panels.Merging.SelectTopOption() + gui.State.Panels.Merging.SelectPrevConflictHunk() return gui.refreshMergePanel() }) } -func (gui *Gui) handleSelectBottom() error { +func (gui *Gui) handleSelectNextConflictHunk() error { return gui.withMergeConflictLock(func() error { gui.takeOverMergeConflictScrolling() - gui.State.Panels.Merging.SelectBottomOption() + gui.State.Panels.Merging.SelectNextConflictHunk() return gui.refreshMergePanel() }) } @@ -135,6 +135,8 @@ func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error switch selection { case mergeconflicts.TOP: logStr = "Picking top hunk" + case mergeconflicts.MIDDLE: + logStr = "Picking middle hunk" case mergeconflicts.BOTTOM: logStr = "Picking bottom hunk" case mergeconflicts.BOTH: diff --git a/pkg/gui/mergeconflicts/find_conflicts.go b/pkg/gui/mergeconflicts/find_conflicts.go index dc6e99189..80e487f85 100644 --- a/pkg/gui/mergeconflicts/find_conflicts.go +++ b/pkg/gui/mergeconflicts/find_conflicts.go @@ -12,7 +12,8 @@ type LineType int const ( START LineType = iota - MIDDLE + ANCESTOR + TARGET END NOT_A_MARKER ) @@ -28,10 +29,14 @@ func findConflicts(content string) []*mergeConflict { for i, line := range utils.SplitLines(content) { switch determineLineType(line) { case START: - newConflict = &mergeConflict{start: i} - case MIDDLE: + newConflict = &mergeConflict{start: i, ancestor: -1} + case ANCESTOR: if newConflict != nil { - newConflict.middle = i + newConflict.ancestor = i + } + case TARGET: + if newConflict != nil { + newConflict.target = i } case END: if newConflict != nil { @@ -54,8 +59,10 @@ func determineLineType(line string) LineType { switch { case strings.HasPrefix(trimmedLine, "<<<<<<< "): return START + case strings.HasPrefix(trimmedLine, "||||||| "): + return ANCESTOR case trimmedLine == "=======": - return MIDDLE + return TARGET case strings.HasPrefix(trimmedLine, ">>>>>>> "): return END default: diff --git a/pkg/gui/mergeconflicts/find_conflicts_test.go b/pkg/gui/mergeconflicts/find_conflicts_test.go index c0a8e3e5b..bcc70ce73 100644 --- a/pkg/gui/mergeconflicts/find_conflicts_test.go +++ b/pkg/gui/mergeconflicts/find_conflicts_test.go @@ -43,12 +43,16 @@ func TestDetermineLineType(t *testing.T) { }, { line: "=======", - expected: MIDDLE, + expected: TARGET, }, { line: ">>>>>>> blah", expected: END, }, + { + line: "||||||| adf33b9", + expected: ANCESTOR, + }, } for _, s := range scenarios { diff --git a/pkg/gui/mergeconflicts/rendering.go b/pkg/gui/mergeconflicts/rendering.go index 45fff634a..8255b6ce9 100644 --- a/pkg/gui/mergeconflicts/rendering.go +++ b/pkg/gui/mergeconflicts/rendering.go @@ -16,11 +16,11 @@ func ColoredConflictFile(content string, state *State, hasFocus bool) string { var outputBuffer bytes.Buffer for i, line := range utils.SplitLines(content) { textStyle := theme.DefaultTextColor - if i == conflict.start || i == conflict.middle || i == conflict.end { + if i == conflict.start || i == conflict.ancestor || i == conflict.target || i == conflict.end { textStyle = style.FgRed } - if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.conflictTop) { + if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.conflictSelection) { textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor).SetBold() } if i == conflict.end && len(remainingConflicts) > 0 { @@ -35,6 +35,19 @@ func shiftConflict(conflicts []*mergeConflict) (*mergeConflict, []*mergeConflict return conflicts[0], conflicts[1:] } -func shouldHighlightLine(index int, conflict *mergeConflict, top bool) bool { - return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top) +func shouldHighlightLine(index int, conflict *mergeConflict, selection Selection) bool { + switch selection { + case TOP: + if conflict.ancestor >= 0 { + return index >= conflict.start && index <= conflict.ancestor + } else { + return index >= conflict.start && index <= conflict.target + } + case MIDDLE: + return index >= conflict.ancestor && index <= conflict.target + case BOTTOM: + return index >= conflict.target && index <= conflict.end + default: + return false + } } diff --git a/pkg/gui/mergeconflicts/state.go b/pkg/gui/mergeconflicts/state.go index 9d8320ffb..343f282ad 100644 --- a/pkg/gui/mergeconflicts/state.go +++ b/pkg/gui/mergeconflicts/state.go @@ -11,42 +11,62 @@ type Selection int const ( TOP Selection = iota + MIDDLE BOTTOM BOTH ) -// mergeConflict : A git conflict with a start middle and end corresponding to line +// mergeConflict : A git conflict with a start, ancestor (if exists), target, and end corresponding to line // numbers in the file where the conflict markers appear type mergeConflict struct { - start int - middle int - end int + start int + ancestor int + target int + end int } type State struct { sync.Mutex - conflictIndex int - conflictTop bool - conflicts []*mergeConflict - EditHistory *stack.Stack + conflictIndex int + conflictSelection Selection + conflicts []*mergeConflict + EditHistory *stack.Stack } func NewState() *State { return &State{ - Mutex: sync.Mutex{}, - conflictIndex: 0, - conflictTop: true, - conflicts: []*mergeConflict{}, - EditHistory: stack.New(), + Mutex: sync.Mutex{}, + conflictIndex: 0, + conflictSelection: TOP, + conflicts: []*mergeConflict{}, + EditHistory: stack.New(), } } -func (s *State) SelectTopOption() { - s.conflictTop = true +func (s *State) SelectPrevConflictHunk() { + switch s.conflictSelection { + case MIDDLE: + s.conflictSelection = TOP + case BOTTOM: + if s.currentConflict().ancestor >= 0 { + s.conflictSelection = MIDDLE + } else { + s.conflictSelection = TOP + } + } } -func (s *State) SelectBottomOption() { - s.conflictTop = false +func (s *State) SelectNextConflictHunk() { + switch s.conflictSelection { + case TOP: + if s.currentConflict().ancestor >= 0 { + s.conflictSelection = MIDDLE + } else { + s.conflictSelection = BOTTOM + } + case MIDDLE: + s.conflictSelection = BOTTOM + } } func (s *State) SelectNextConflict() { @@ -100,11 +120,7 @@ func (s *State) NoConflicts() bool { } func (s *State) Selection() Selection { - if s.conflictTop { - return TOP - } else { - return BOTTOM - } + return s.conflictSelection } func (s *State) IsFinalConflict() bool { @@ -116,7 +132,7 @@ func (s *State) Reset() { } func (s *State) GetConflictMiddle() int { - return s.currentConflict().middle + return s.currentConflict().target } func (s *State) ContentAfterConflictResolve(path string, selection Selection) (bool, string, error) { @@ -141,13 +157,23 @@ func (s *State) ContentAfterConflictResolve(path string, selection Selection) (b func isIndexToDelete(i int, conflict *mergeConflict, selection Selection) bool { isMarkerLine := - i == conflict.middle || - i == conflict.start || + i == conflict.start || + i == conflict.ancestor || + i == conflict.target || i == conflict.end - isUnwantedContent := - (selection == BOTTOM && conflict.start < i && i < conflict.middle) || - (selection == TOP && conflict.middle < i && i < conflict.end) - - return isMarkerLine || isUnwantedContent + var isWantedContent bool + switch selection { + case TOP: + if conflict.ancestor >= 0 { + isWantedContent = conflict.start < i && i < conflict.ancestor + } else { + isWantedContent = conflict.start < i && i < conflict.target + } + case MIDDLE: + isWantedContent = conflict.ancestor < i && i < conflict.target + case BOTTOM: + isWantedContent = conflict.target < i && i < conflict.end + } + return isMarkerLine || !isWantedContent } diff --git a/pkg/gui/mergeconflicts/state_test.go b/pkg/gui/mergeconflicts/state_test.go index a71d05a7a..861833d32 100644 --- a/pkg/gui/mergeconflicts/state_test.go +++ b/pkg/gui/mergeconflicts/state_test.go @@ -58,37 +58,57 @@ bar ======= baz >>>>>>> branch + +<<<<<<< HEAD +foo +||||||| fffffff +bar +======= +baz +>>>>>>> branch `, expected: []*mergeConflict{ { - start: 0, - middle: 2, - end: 4, + start: 0, + ancestor: -1, + target: 2, + end: 4, }, { - start: 6, - middle: 9, - end: 11, + start: 6, + ancestor: -1, + target: 9, + end: 11, }, { - start: 13, - middle: 15, - end: 17, + start: 13, + ancestor: -1, + target: 15, + end: 17, }, { - start: 19, - middle: 21, - end: 23, + start: 19, + ancestor: -1, + target: 21, + end: 23, }, { - start: 25, - middle: 27, - end: 29, + start: 25, + ancestor: -1, + target: 27, + end: 29, }, { - start: 31, - middle: 34, - end: 36, + start: 31, + ancestor: -1, + target: 34, + end: 36, + }, + { + start: 38, + ancestor: 40, + target: 42, + end: 44, }, }, },