2021-04-18 10:07:10 +02:00
|
|
|
package mergeconflicts
|
|
|
|
|
|
|
|
import (
|
2023-02-25 04:08:45 +02:00
|
|
|
"strings"
|
|
|
|
|
2021-04-18 10:07:10 +02:00
|
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
|
|
)
|
|
|
|
|
2022-05-07 07:42:36 +02:00
|
|
|
// State represents the selection state of the merge conflict context.
|
2021-04-18 10:07:10 +02:00
|
|
|
type State struct {
|
2022-01-25 16:20:19 +02:00
|
|
|
// path of the file with the conflicts
|
|
|
|
path string
|
|
|
|
|
|
|
|
// This is a stack of the file content. It is used to undo changes.
|
|
|
|
// The last item is the current file content.
|
|
|
|
contents []string
|
|
|
|
|
2021-08-25 12:43:57 +02:00
|
|
|
conflicts []*mergeConflict
|
|
|
|
// this is the index of the above `conflicts` field which is currently selected
|
|
|
|
conflictIndex int
|
|
|
|
|
|
|
|
// this is the index of the selected conflict's available selections slice e.g. [TOP, MIDDLE, BOTTOM]
|
|
|
|
// We use this to know which hunk of the conflict is selected.
|
2021-08-23 15:14:59 +02:00
|
|
|
selectionIndex int
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewState() *State {
|
|
|
|
return &State{
|
2021-08-23 15:14:59 +02:00
|
|
|
conflictIndex: 0,
|
|
|
|
selectionIndex: 0,
|
|
|
|
conflicts: []*mergeConflict{},
|
2022-01-25 16:20:19 +02:00
|
|
|
contents: []string{},
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-23 15:14:59 +02:00
|
|
|
func (s *State) setConflictIndex(index int) {
|
|
|
|
if len(s.conflicts) == 0 {
|
|
|
|
s.conflictIndex = 0
|
2021-08-25 12:43:57 +02:00
|
|
|
} else {
|
2022-01-30 05:46:46 +02:00
|
|
|
s.conflictIndex = utils.Clamp(index, 0, len(s.conflicts)-1)
|
2021-08-21 18:03:05 +02:00
|
|
|
}
|
2021-08-23 15:14:59 +02:00
|
|
|
s.setSelectionIndex(s.selectionIndex)
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2021-08-23 15:14:59 +02:00
|
|
|
func (s *State) setSelectionIndex(index int) {
|
|
|
|
if selections := s.availableSelections(); len(selections) != 0 {
|
2022-01-30 05:46:46 +02:00
|
|
|
s.selectionIndex = utils.Clamp(index, 0, len(selections)-1)
|
2021-08-21 18:03:05 +02:00
|
|
|
}
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2021-08-23 15:14:59 +02:00
|
|
|
func (s *State) SelectNextConflictHunk() {
|
|
|
|
s.setSelectionIndex(s.selectionIndex + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) SelectPrevConflictHunk() {
|
|
|
|
s.setSelectionIndex(s.selectionIndex - 1)
|
|
|
|
}
|
|
|
|
|
2021-04-18 10:07:10 +02:00
|
|
|
func (s *State) SelectNextConflict() {
|
2021-08-23 15:14:59 +02:00
|
|
|
s.setConflictIndex(s.conflictIndex + 1)
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) SelectPrevConflict() {
|
2021-08-23 15:14:59 +02:00
|
|
|
s.setConflictIndex(s.conflictIndex - 1)
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
func (s *State) currentConflict() *mergeConflict {
|
|
|
|
if len(s.conflicts) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.conflicts[s.conflictIndex]
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
// this is for starting a new merge conflict session
|
|
|
|
func (s *State) SetContent(content string, path string) {
|
|
|
|
if content == s.GetContent() && path == s.path {
|
|
|
|
return
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
s.path = path
|
|
|
|
s.contents = []string{}
|
|
|
|
s.PushContent(content)
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
// this is for when you've resolved a conflict. This allows you to undo to a previous
|
|
|
|
// state
|
|
|
|
func (s *State) PushContent(content string) {
|
|
|
|
s.contents = append(s.contents, content)
|
|
|
|
s.setConflicts(findConflicts(content))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) GetContent() string {
|
|
|
|
if len(s.contents) == 0 {
|
|
|
|
return ""
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
return s.contents[len(s.contents)-1]
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
func (s *State) GetPath() string {
|
|
|
|
return s.path
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) Undo() bool {
|
|
|
|
if len(s.contents) <= 1 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
s.contents = s.contents[:len(s.contents)-1]
|
|
|
|
|
|
|
|
newContent := s.GetContent()
|
|
|
|
// We could be storing the old conflicts and selected index on a stack too.
|
|
|
|
s.setConflicts(findConflicts(newContent))
|
|
|
|
|
|
|
|
return true
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) setConflicts(conflicts []*mergeConflict) {
|
|
|
|
s.conflicts = conflicts
|
2021-08-23 15:14:59 +02:00
|
|
|
s.setConflictIndex(s.conflictIndex)
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) NoConflicts() bool {
|
|
|
|
return len(s.conflicts) == 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) Selection() Selection {
|
2021-08-23 15:14:59 +02:00
|
|
|
if selections := s.availableSelections(); len(selections) > 0 {
|
|
|
|
return selections[s.selectionIndex]
|
|
|
|
}
|
|
|
|
return TOP
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) availableSelections() []Selection {
|
|
|
|
if conflict := s.currentConflict(); conflict != nil {
|
2021-08-25 12:43:57 +02:00
|
|
|
return availableSelections(conflict)
|
2021-08-23 15:14:59 +02:00
|
|
|
}
|
|
|
|
return nil
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
func (s *State) AllConflictsResolved() bool {
|
|
|
|
return len(s.conflicts) == 0
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) Reset() {
|
2022-01-25 16:20:19 +02:00
|
|
|
s.contents = []string{}
|
|
|
|
s.path = ""
|
|
|
|
}
|
|
|
|
|
2022-08-06 10:50:52 +02:00
|
|
|
// we're not resetting selectedIndex here because the user typically would want
|
|
|
|
// to pick either all top hunks or all bottom hunks so we retain that selection
|
|
|
|
func (s *State) ResetConflictSelection() {
|
|
|
|
s.conflictIndex = 0
|
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
func (s *State) Active() bool {
|
|
|
|
return s.path != ""
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) GetConflictMiddle() int {
|
2022-01-27 11:10:25 +02:00
|
|
|
currentConflict := s.currentConflict()
|
|
|
|
|
|
|
|
if currentConflict == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return currentConflict.target
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
2022-01-25 16:20:19 +02:00
|
|
|
func (s *State) ContentAfterConflictResolve(selection Selection) (bool, string, error) {
|
2021-04-18 10:07:10 +02:00
|
|
|
conflict := s.currentConflict()
|
|
|
|
if conflict == nil {
|
|
|
|
return false, "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
content := ""
|
2022-01-25 16:20:19 +02:00
|
|
|
err := utils.ForEachLineInFile(s.path, func(line string, i int) {
|
2021-08-25 12:43:57 +02:00
|
|
|
if selection.isIndexToKeep(conflict, i) {
|
2021-04-18 10:07:10 +02:00
|
|
|
content += line
|
|
|
|
}
|
2021-05-30 07:22:04 +02:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return false, "", err
|
2021-04-18 10:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return true, content, nil
|
|
|
|
}
|
2022-04-04 14:12:33 +02:00
|
|
|
|
|
|
|
func (s *State) GetSelectedLine() int {
|
|
|
|
conflict := s.currentConflict()
|
|
|
|
if conflict == nil {
|
2023-02-25 04:08:45 +02:00
|
|
|
// TODO: see why this is 1 and not 0
|
2022-04-04 14:12:33 +02:00
|
|
|
return 1
|
|
|
|
}
|
|
|
|
selection := s.Selection()
|
|
|
|
startIndex, _ := selection.bounds(conflict)
|
|
|
|
return startIndex + 1
|
|
|
|
}
|
2023-02-25 04:08:45 +02:00
|
|
|
|
|
|
|
func (s *State) GetSelectedRange() (int, int) {
|
|
|
|
conflict := s.currentConflict()
|
|
|
|
if conflict == nil {
|
|
|
|
return 0, 0
|
|
|
|
}
|
|
|
|
selection := s.Selection()
|
|
|
|
startIndex, endIndex := selection.bounds(conflict)
|
|
|
|
return startIndex, endIndex
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) PlainRenderSelected() string {
|
|
|
|
startIndex, endIndex := s.GetSelectedRange()
|
|
|
|
|
|
|
|
content := s.GetContent()
|
|
|
|
|
|
|
|
contentLines := utils.SplitLines(content)
|
|
|
|
|
|
|
|
return strings.Join(contentLines[startIndex:endIndex+1], "\n")
|
|
|
|
}
|