diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2023-03-08 16:55:44 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2023-03-19 16:30:39 +1100 |
commit | 73c7dc9c5d00408e156724ff5b9bd792b4d17273 (patch) | |
tree | 634d633fec7c788672e407f8f63b08efef1776dd /pkg/commands/patch | |
parent | b542579db31f160a8d13d255b447d654d253db17 (diff) |
refactor patch code
Diffstat (limited to 'pkg/commands/patch')
-rw-r--r-- | pkg/commands/patch/format.go | 169 | ||||
-rw-r--r-- | pkg/commands/patch/hunk.go | 159 | ||||
-rw-r--r-- | pkg/commands/patch/parse.go | 85 | ||||
-rw-r--r-- | pkg/commands/patch/patch.go | 151 | ||||
-rw-r--r-- | pkg/commands/patch/patch_line.go | 30 | ||||
-rw-r--r-- | pkg/commands/patch/patch_manager.go | 52 | ||||
-rw-r--r-- | pkg/commands/patch/patch_modifier.go | 187 | ||||
-rw-r--r-- | pkg/commands/patch/patch_parser.go | 230 | ||||
-rw-r--r-- | pkg/commands/patch/patch_test.go (renamed from pkg/commands/patch/patch_modifier_test.go) | 134 | ||||
-rw-r--r-- | pkg/commands/patch/transform.go | 156 |
10 files changed, 770 insertions, 583 deletions
diff --git a/pkg/commands/patch/format.go b/pkg/commands/patch/format.go new file mode 100644 index 000000000..d04b6bec1 --- /dev/null +++ b/pkg/commands/patch/format.go @@ -0,0 +1,169 @@ +package patch + +import ( + "strings" + + "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/theme" + "github.com/samber/lo" +) + +type patchPresenter struct { + patch *Patch + // if true, all following fields are ignored + plain bool + + isFocused bool + // first line index for selected cursor range + firstLineIndex int + // last line index for selected cursor range + lastLineIndex int + // line indices for tagged lines (e.g. lines added to a custom patch) + incLineIndices *set.Set[int] +} + +// formats the patch as a plain string +func formatPlain(patch *Patch) string { + presenter := &patchPresenter{ + patch: patch, + plain: true, + incLineIndices: set.New[int](), + } + return presenter.format() +} + +func formatRangePlain(patch *Patch, startIdx int, endIdx int) string { + lines := patch.Lines()[startIdx : endIdx+1] + return strings.Join( + lo.Map(lines, func(line *PatchLine, _ int) string { + return line.Content + "\n" + }), + "", + ) +} + +type FormatViewOpts struct { + IsFocused bool + // first line index for selected cursor range + FirstLineIndex int + // last line index for selected cursor range + LastLineIndex int + // line indices for tagged lines (e.g. lines added to a custom patch) + IncLineIndices *set.Set[int] +} + +// formats the patch for rendering within a view, meaning it's coloured and +// highlights selected items +func formatView(patch *Patch, opts FormatViewOpts) string { + includedLineIndices := opts.IncLineIndices + if includedLineIndices == nil { + includedLineIndices = set.New[int]() + } + presenter := &patchPresenter{ + patch: patch, + plain: false, + isFocused: opts.IsFocused, + firstLineIndex: opts.FirstLineIndex, + lastLineIndex: opts.LastLineIndex, + incLineIndices: includedLineIndices, + } + return presenter.format() +} + +func (self *patchPresenter) format() string { + // if we have no changes in our patch (i.e. no additions or deletions) then + // the patch is effectively empty and we can return an empty string + if !self.patch.ContainsChanges() { + return "" + } + + stringBuilder := &strings.Builder{} + lineIdx := 0 + appendLine := func(line string) { + _, _ = stringBuilder.WriteString(line + "\n") + + lineIdx++ + } + appendFormattedLine := func(line string, style style.TextStyle) { + formattedLine := self.formatLine( + line, + style, + lineIdx, + ) + + appendLine(formattedLine) + } + + for _, line := range self.patch.header { + appendFormattedLine(line, theme.DefaultTextColor.SetBold()) + } + + for _, hunk := range self.patch.hunks { + appendLine( + self.formatLine( + hunk.formatHeaderStart(), + style.FgCyan, + lineIdx, + ) + + // we're splitting the line into two parts: the diff header and the context + // We explicitly pass 'included' as false here so that we're only tagging the + // first half of the line as included if the line is indeed included. + self.formatLineAux( + hunk.headerContext, + theme.DefaultTextColor, + lineIdx, + false, + ), + ) + + for _, line := range hunk.bodyLines { + appendFormattedLine(line.Content, self.patchLineStyle(line)) + } + } + + return stringBuilder.String() +} + +func (self *patchPresenter) patchLineStyle(patchLine *PatchLine) style.TextStyle { + switch patchLine.Kind { + case ADDITION: + return style.FgGreen + case DELETION: + return style.FgRed + default: + return theme.DefaultTextColor + } +} + +func (self *patchPresenter) formatLine(str string, textStyle style.TextStyle, index int) string { + included := self.incLineIndices.Includes(index) + + return self.formatLineAux(str, textStyle, index, included) +} + +// 'selected' means you've got it highlighted with your cursor +// 'included' means the line has been included in the patch (only applicable when +// building a patch) +func (self *patchPresenter) formatLineAux(str string, textStyle style.TextStyle, index int, included bool) string { + if self.plain { + return str + } + + selected := self.isFocused && index >= self.firstLineIndex && index <= self.lastLineIndex + + if selected { + textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor) + } + + firstCharStyle := textStyle + if included { + firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen) + } + + if len(str) < 2 { + return firstCharStyle.Sprint(str) + } + + return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:]) +} diff --git a/pkg/commands/patch/hunk.go b/pkg/commands/patch/hunk.go index 605c473c1..6d0177d05 100644 --- a/pkg/commands/patch/hunk.go +++ b/pkg/commands/patch/hunk.go @@ -1,130 +1,67 @@ package patch -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/samber/lo" -) - -type PatchHunk struct { - FirstLineIdx int - oldStart int - newStart int - heading string - bodyLines []string -} - -func (hunk *PatchHunk) LastLineIdx() int { - return hunk.FirstLineIdx + len(hunk.bodyLines) +import "fmt" + +// Example hunk: +// @@ -16,2 +14,3 @@ func (f *CommitFile) Description() string { +// return f.Name +// -} +// + +// +// test + +type Hunk struct { + // the line number of the first line in the old file ('16' in the above example) + oldStart int + // the line number of the first line in the new file ('14' in the above example) + newStart int + // the context at the end of the header line (' func (f *CommitFile) Description() string {' in the above example) + headerContext string + // the body of the hunk, excluding the header line + bodyLines []*PatchLine } -func newHunk(lines []string, firstLineIdx int) *PatchHunk { - header := lines[0] - bodyLines := lines[1:] - - oldStart, newStart, heading := headerInfo(header) - - return &PatchHunk{ - oldStart: oldStart, - newStart: newStart, - heading: heading, - FirstLineIdx: firstLineIdx, - bodyLines: bodyLines, - } +// Returns the number of lines in the hunk in the original file ('2' in the above example) +func (self *Hunk) oldLength() int { + return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, DELETION}) } -func headerInfo(header string) (int, int, string) { - match := hunkHeaderRegexp.FindStringSubmatch(header) - - oldStart := utils.MustConvertToInt(match[1]) - newStart := utils.MustConvertToInt(match[2]) - heading := match[3] - - return oldStart, newStart, heading +// Returns the number of lines in the hunk in the new file ('3' in the above example) +func (self *Hunk) newLength() int { + return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, ADDITION}) } -func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string { - skippedNewlineMessageIndex := -1 - newLines := []string{} - - lineIdx := hunk.FirstLineIdx - for _, line := range hunk.bodyLines { - lineIdx++ // incrementing at the start to skip the header line - if line == "" { - break - } - isLineSelected := lo.Contains(lineIndices, lineIdx) - - firstChar, content := line[:1], line[1:] - transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected) - - if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " { - newLines = append(newLines, transformedFirstChar+content) - continue - } - - if transformedFirstChar == "+" { - // we don't want to include the 'newline at end of file' line if it involves an addition we're not including - skippedNewlineMessageIndex = lineIdx + 1 - } - } - - return newLines +// Returns true if the hunk contains any changes (i.e. if it's not just a context hunk). +// We'll end up with a context hunk if we're transforming a patch and one of the hunks +// has no selected lines. +func (self *Hunk) containsChanges() bool { + return nLinesWithKind(self.bodyLines, []PatchLineKind{ADDITION, DELETION}) > 0 } -func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string { - linesToKeepInPatchContext := "-" - if reverse { - linesToKeepInPatchContext = "+" - } - if !isLineSelected && firstChar == linesToKeepInPatchContext { - return " " - } - - return firstChar +// Returns the number of lines in the hunk, including the header line +func (self *Hunk) lineCount() int { + return len(self.bodyLines) + 1 } -func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string { - return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading) +// Returns all lines in the hunk, including the header line +func (self *Hunk) allLines() []*PatchLine { + lines := []*PatchLine{{Content: self.formatHeaderLine(), Kind: HUNK_HEADER}} + lines = append(lines, self.bodyLines...) + return lines } -func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) { - bodyLines := hunk.updatedLines(lineIndices, reverse) - startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset) - if !ok { - return startOffset, "" - } - return startOffset, header + strings.Join(bodyLines, "") +// Returns the header line, including the unified diff header and the context +func (self *Hunk) formatHeaderLine() string { + return fmt.Sprintf("%s%s", self.formatHeaderStart(), self.headerContext) } -func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int) (int, string, bool) { - changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"}) - oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"}) - newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "}) - - if changeCount == 0 { - // if nothing has changed we just return nothing - return startOffset, "", false - } - - oldStart := hunk.oldStart - - var newStartOffset int - // if the hunk went from zero to positive length, we need to increment the starting point by one - // if the hunk went from positive to zero length, we need to decrement the starting point by one - if oldLength == 0 { - newStartOffset = 1 - } else if newLength == 0 { - newStartOffset = -1 - } else { - newStartOffset = 0 +// Returns the first part of the header line i.e. the unified diff part (excluding any context) +func (self *Hunk) formatHeaderStart() string { + newLengthDisplay := "" + newLength := self.newLength() + // if the new length is 1, it's omitted + if newLength != 1 { + newLengthDisplay = fmt.Sprintf(",%d", newLength) } - newStart := oldStart + startOffset + newStartOffset - - newStartOffset = startOffset + newLength - oldLength - formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading) - return newStartOffset, formattedHeader, true + return fmt.Sprintf("@@ -%d,%d +%d%s @@", self.oldStart, self.oldLength(), self.newStart, newLengthDisplay) } diff --git a/pkg/commands/patch/parse.go b/pkg/commands/patch/parse.go new file mode 100644 index 000000000..fee7d2918 --- /dev/null +++ b/pkg/commands/patch/parse.go @@ -0,0 +1,85 @@ +package patch + +import ( + "regexp" + "strings" + + "github.com/jesseduffield/lazygit/pkg/utils" +) + +var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`) + +func Parse(patchStr string) *Patch { + // ignore trailing newline. + lines := strings.Split(strings.TrimSuffix(patchStr, "\n"), "\n") + + hunks := []*Hunk{} + patchHeader := []string{} + + var currentHunk *Hunk + for _, line := range lines { + if strings.HasPrefix(line, "@@") { + oldStart, newStart, headerContext := headerInfo(line) + + currentHunk = &Hunk{ + oldStart: oldStart, + newStart: newStart, + headerContext: headerContext, + bodyLines: []*PatchLine{}, + } + hunks = append(hunks, currentHunk) + } else if currentHunk != nil { + currentHunk.bodyLines = append(currentHunk.bodyLines, newHunkLine(line)) + } else { + patchHeader = append(patchHeader, line) + } + } + + return &Patch{ + hunks: hunks, + header: patchHeader, + } +} + +func headerInfo(header string) (int, int, string) { + match := hunkHeaderRegexp.FindStringSubmatch(header) + + oldStart := utils.MustConvertToInt(match[1]) + newStart := utils.MustConvertToInt(match[2]) + headerContext := match[3] + + return oldStart, newStart, headerContext +} + +func newHunkLine(line string) *PatchLine { + if line == "" { + return &PatchLine{ + Kind: CONTEXT, + Content: "", + } + } + + firstChar := line[:1] + + kind := parseFirstChar(firstChar) + + return &PatchLine{ + Kind: kind, + Content: line, + } +} + +func parseFirstChar(firstChar string) PatchLineKind { + switch firstChar { + case " ": + return CONTEXT + case "+": + return ADDITION + case "-": + return DELETION + case "\\": + return NEWLINE_MESSAGE + } + + return CONTEXT +} diff --git a/pkg/commands/patch/patch.go b/pkg/commands/patch/patch.go new file mode 100644 index 000000000..5275fb613 --- /dev/null +++ b/pkg/commands/patch/patch.go @@ -0,0 +1,151 @@ +package patch + +import ( + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" +) + +type Patch struct { + // header of the patch (split on newlines) e.g. + // diff --git a/filename b/filename + // index dcd3485..1ba5540 100644 + // --- a/filename + // +++ b/filename + header []string + // hunks of the patch + hunks []*Hunk +} + +// Returns a new patch with the specified transformation applied (e.g. +// only selecting a subset of changes). +// Leaves the original patch unchanged. +func (self *Patch) Transform(opts TransformOpts) *Patch { + return transform(self, opts) +} + +// Returns the patch as a plain string +func (self *Patch) FormatPlain() string { + return formatPlain(self) +} + +// Returns a range of lines from the patch as a plain string (range is inclusive) +func (self *Patch) FormatRangePlain(startIdx int, endIdx int) string { + return formatRangePlain(self, startIdx, endIdx) +} + +// Returns the patch as a string with ANSI color codes for displaying in a view +func (self *Patch) FormatView(opts FormatViewOpts) string { + return formatView(self, opts) +} + +// Returns the lines of the patch +func (self *Patch) Lines() []*PatchLine { + lines := []*PatchLine{} + for _, line := range self.header { + lines = append(lines, &PatchLine{Content: line, Kind: PATCH_HEADER}) + } + + for _, hunk := range self.hunks { + lines = append(lines, hunk.allLines()...) + } + + return lines +} + +// Returns the patch line index of the first line in the given hunk +func (self *Patch) HunkStartIdx(hunkIndex int) int { + hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1) + + result := len(self.header) + for i := 0; i < hunkIndex; i++ { + result += self.hunks[i].lineCount() + } + return result +} + +// Returns the patch line index of the last line in the given hunk +func (self *Patch) HunkEndIdx(hunkIndex int) int { + hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1) + + return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1 +} + +func (self *Patch) ContainsChanges() bool { + return lo.SomeBy(self.hunks, func(hunk *Hunk) bool { + return hunk.containsChanges() + }) +} + +// Takes a line index in the patch and returns the line number in the new file. +// If the line is a header line, returns 1. +// If the line is a hunk header line, returns the first file line number in that hunk. +// If the line is out of range below, returns the last file line number in the last hunk. +func (self *Patch) LineNumberOfLine(idx int) int { + if idx < len(self.header) || len(self.hunks) == 0 { + return 1 + } + + hunkIdx := self.HunkContainingLine(idx) + // cursor out of range, return last file line number + if hunkIdx == -1 { + lastHunk := self.hunks[len(self.hunks)-1] + return lastHunk.newStart + lastHunk.newLength() - 1 + } + + hunk := self.hunks[hunkIdx] + hunkStartIdx := self.HunkStartIdx(hunkIdx) + idxInHunk := idx - hunkStartIdx + + if idxInHunk == 0 { + return hunk.oldStart + } + + lines := hunk.bodyLines[:idxInHunk-1] + offset := nLinesWithKind(lines, []PatchLineKind{ADDITION, CONTEXT}) + return hunk.oldStart + offset +} + +// Returns hunk index containing the line at the given patch line index +func (self *Patch) HunkContainingLine(idx int) int { + for hunkIdx, hunk := range self.hunks { + hunkStartIdx := self.HunkStartIdx(hunkIdx) + if idx >= hunkStartIdx && idx < hunkStartIdx+hunk.lineCount() { + return hunkIdx + } + } + return -1 +} + +// Returns the patch line index of the next change (i.e. addition or deletion). +func (self *Patch) GetNextChangeIdx(idx int) int { + idx = utils.Clamp(idx, 0, self.LineCount()-1) + + lines := self.Lines() + + for i, line := range lines[idx:] { + if line.isChange() { + return i + idx + } + } + + // there are no changes from the cursor onwards so we'll instead + // return the index of the last change + for i := len(lines) - 1; i >= 0; i-- { + line := lines[i] + if line.isChange() { + return i + } + } + + // should not be possible + return 0 +} + +// Returns the length of the patch in lines +func (self *Patch) LineCount() int { + count := len(self.header) + for _, hunk := range self.hunks { + count += hunk.lineCount() + } + return count +} diff --git a/pkg/commands/patch/patch_line.go b/pkg/commands/patch/patch_line.go new file mode 100644 index 000000000..994d7c4e9 --- /dev/null +++ b/pkg/commands/patch/patch_line.go @@ -0,0 +1,30 @@ +package patch + +import "github.com/samber/lo" + +type PatchLineKind int + +const ( + PATCH_HEADER PatchLineKind = iota + HUNK_HEADER + ADDITION + DELETION + CONTEXT + NEWLINE_MESSAGE +) + +type PatchLine struct { + Kind PatchLineKind + Content string // something like '+ hello' (note the first character is not removed) +} + +func (self *PatchLine) isChange() bool { + return self.Kind == ADDITION || self.Kind == DELETION +} + +// Returns the number of lines in the given slice that have one of the given kinds +func nLinesWithKind(lines []*PatchLine, kinds []PatchLineKind) int { + return lo.CountBy(lines, func(line *PatchLine) bool { + return lo.Contains(kinds, line.Kind) + }) +} diff --git a/pkg/commands/patch/patch_manager.go b/pkg/commands/patch/patch_manager.go index 84db20be6..8e06dccd7 100644 --- a/pkg/commands/patch/patch_manager.go +++ b/pkg/commands/patch/patch_manager.go @@ -162,39 +162,37 @@ func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLi return nil } -func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool) string { +func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool) string { info, err := p.getFileInfo(filename) if err != nil { p.Log.Error(err) return "" } - switch info.mode { - case WHOLE: - // use the whole diff - // the reverse flag is only for part patches so we're ignoring it here - return info.diff - case PART: - // generate a new diff with just the selected lines - return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, - PatchOptions{ - Reverse: reverse, - KeepOriginalHeader: true, - }) - default: + if info.mode == UNSELECTED { return "" } -} -func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool) string { - patch := p.renderPlainPatchForFile(filename, reverse) - if plain { - return patch + if info.mode == WHOLE && plain { + // Use the whole diff (spares us parsing it and then formatting it). + // TODO: see if this is actually noticeably faster. + // The reverse flag is only for part patches so we're ignoring it here. + return info.diff } - parser := NewPatchParser(p.Log, patch) - // not passing included lines because we don't want to see them in the secondary panel - return parser.Render(false, -1, -1, nil) + patch := Parse(info.diff). + Transform(TransformOpts{ + Reverse: reverse, + IncludedLineIndices: info.includedLineIndices, + }) + + if plain { + return patch.FormatPlain() + } else { + return patch.FormatView(FormatViewOpts{ + IsFocused: false, + }) + } } func (p *PatchManager) renderEachFilePatch(plain bool) []string { @@ -212,14 +210,8 @@ func (p *PatchManager) renderEachFilePatch(plain bool) []string { return output } -func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string { - result := "" - for _, patch := range p.renderEachFilePatch(plain) { - if patch != "" { - result += patch + "\n" - } - } - return result +func (p *PatchManager) RenderAggregatedPatch(plain bool) string { + return strings.Join(p.renderEachFilePatch(plain), "") } func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus { diff --git a/pkg/commands/patch/patch_modifier.go b/pkg/commands/patch/patch_modifier.go deleted file mode 100644 index 79f7b7d31..000000000 --- a/pkg/commands/patch/patch_modifier.go +++ /dev/null @@ -1,187 +0,0 @@ -package patch - -import ( - "fmt" - "regexp" - "strings" - - "github.com/sirupsen/logrus" -) - -var ( - hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`) - patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`) -) - -type PatchOptions struct { - // Create a patch that will applied in reverse with `git apply --reverse`. - // This affects how unselected lines are treated when only parts of a hunk - // are selected: usually, for unselected lines we change '-' lines to - // context lines and remove '+' lines, but when Reverse is true we need to - // turn '+' lines into context lines and remove '-' lines. - Reverse bool - - // Whether to keep or discard the original diff header including the - // "index deadbeef..fa1afe1 100644" line. - KeepOriginalHeader bool -} - -func GetHeaderFromDiff(diff string) string { - match := patchHeaderRegexp.FindStringSubmatch(diff) - if len(match) <= 1 { - return "" - } - return match[1] -} - -func GetHunksFromDiff(diff string) []*PatchHunk { - hunks := []*PatchHunk{} - firstLineIdx := -1 - var hunkLines []string //nolint:prealloc - pastDiffHeader := false - - lines := strings.SplitAfter(diff, "\n") - - for lineIdx, line := range lines { - isHunkHeader := strings.HasPrefix(line, "@@ -") - - if isHunkHeader { - if pastDiffHeader { // we need to persist the current hunk - hunks = append(hunks, newHunk(hunkLines, firstLineIdx)) - } - pastDiffHeader = true - firstLineIdx = lineIdx - hunkLines = []string{line} - continue - } - - if !pastDiffHeader { // skip through the stuff that precedes the first hunk - continue - } - - if lineIdx == len(lines)-1 && line == "" { // skip the trailing newline - continue - } - - hunkLines = append(hunkLines, line) - } - - if pastDiffHeader { - hunks = append(hunks, newHunk(hunkLines, firstLineIdx)) - } - - return hunks -} - -type PatchModifier struct { - Log *logrus.Entry - filename string - hunks []*PatchHunk - header string -} - -func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier { - return &PatchModifier{ - Log: log, - filename: filename, - hunks: GetHunksFromDiff(diffText), - header: GetHeaderFromDiff(diffText), - } -} - -func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, opts PatchOptions) string { - // step one is getting only those hunks which we care about - hunksInRange := []*PatchHunk{} -outer: - for _, hunk := range d.hunks { - // if there is any line in our lineIndices array that the hunk contains, we append it - for _, lineIdx := range lineIndices { - if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() { - hunksInRange = append(hunksInRange, hunk) - continue outer - } - } - } - - // step 2 is collecting all the hunks with new headers - startOffset := 0 - formattedHunks := "" - var formattedHunk string - for _, hunk := range hunksInRange { - startOffset, formattedHunk = hunk.formatWithChanges( - lineIndices, opts.Reverse, startOffset) - formattedHunks += formattedHunk - } - - if formattedHunks == "" { - return "" - } - - var fileHeader string - // for staging/unstaging lines we don't want the original header because - // it makes git confused e.g. when dealing with deleted/added files - // but with building and applying patches the original header gives git - // information it needs to cleanly apply patches - if opts.KeepOriginalHeader { - fileHeader = d.header - } else { - fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename) - } - - return fileHeader + formattedHunks -} - -func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, opts PatchOptions) string { - // generate array of consecutive line indices from our range - selectedLines := []int{} - for i := firstLineIdx; i <= lastLineIdx; i++ { - selectedLines = append(selectedLines, i) - } - return d.ModifiedPatchForLines(selectedLines, opts) -} - -func (d *PatchModifier) OriginalPatchLength() int { - if len(d.hunks) == 0 { - return 0 - } - - return d.hunks[len(d.hunks)-1].LastLineIdx() -} - -func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, opts PatchOptions) string { - p := NewPatchModifier(log, filename, diffText) - return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, opts) -} - -func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, opts PatchOptions) string { - p := NewPatchModifier(log, filename, diffText) - return p.ModifiedPatchForLines(includedLineIndices, opts) -} - -// I want to know, given a hunk, what line a given index is on -func (hunk *PatchHunk) LineNumberOfLine(idx int) int { - n := idx - hunk.FirstLineIdx - 1 - if n < 0 { - n = 0 - } else if n >= len(hunk.bodyLines) { - n = len(hunk.bodyLines) - 1 - } - - lines := hunk.bodyLines[0:n] - - offset := nLinesWithPrefix(lines, []string{"+", " "}) - - return hunk.newStart + offset -} - -func nLinesWithPrefix(lines []string, chars []string) int { - result := 0 - for _, line := range lines { - for _, char := range chars { - if line[:1] == char { - result++ - } - } - } - return result -} diff --git a/pkg/commands/patch/patch_parser.go b/pkg/commands/patch/patch_parser.go deleted file mode 100644 index 3f191f40a..000000000 --- a/pkg/commands/patch/patch_parser.go +++ /dev/null @@ -1,230 +0,0 @@ -package patch - -import ( - "regexp" - "strings" - - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/theme" - "github.com/samber/lo" - "github.com/sirupsen/logrus" -) - -type PatchLineKind int - -const ( - PATCH_HEADER PatchLineKind = iota - COMMIT_SHA - COMMIT_DESCRIPTION - HUNK_HEADER - ADDITION - DELETION - CONTEXT - NEWLINE_MESSAGE -) - -// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position. - -type PatchLine struct { - Kind PatchLineKind - Content string // something like '+ hello' (note the first character is not removed) -} - -type PatchParser struct { - Log *logrus.Entry - PatchLines []*PatchLine - PatchHunks []*PatchHunk - HunkStarts []int - StageableLines []int // rename to mention we're talking about indexes -} - |