1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-03 13:21:56 +02:00

refactor patch code

This commit is contained in:
Jesse Duffield 2023-03-08 16:55:44 +11:00
parent b542579db3
commit 73c7dc9c5d
14 changed files with 834 additions and 622 deletions

View File

@ -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:])
}

View File

@ -1,130 +1,67 @@
package patch
import (
"fmt"
"strings"
import "fmt"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
// Example hunk:
// @@ -16,2 +14,3 @@ func (f *CommitFile) Description() string {
// return f.Name
// -}
// +
// +// test
type PatchHunk struct {
FirstLineIdx int
oldStart int
newStart int
heading string
bodyLines []string
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 (hunk *PatchHunk) LastLineIdx() int {
return hunk.FirstLineIdx + len(hunk.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 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 new file ('3' in the above example)
func (self *Hunk) newLength() int {
return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, ADDITION})
}
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 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 (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
// Returns the number of lines in the hunk, including the header line
func (self *Hunk) lineCount() int {
return len(self.bodyLines) + 1
}
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)
// 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
}
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
// 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)
}
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
}
// 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)
}
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
linesToKeepInPatchContext := "-"
if reverse {
linesToKeepInPatchContext = "+"
}
if !isLineSelected && firstChar == linesToKeepInPatchContext {
return " "
}
return firstChar
}
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)
}
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, "")
}
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
}
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)
}

View File

@ -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
}

151
pkg/commands/patch/patch.go Normal file
View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
hunkStarts, stageableLines, patchLines := parsePatch(patch)
patchHunks := GetHunksFromDiff(patch)
return &PatchParser{
Log: log,
HunkStarts: hunkStarts, // deprecated
StageableLines: stageableLines,
PatchLines: patchLines,
PatchHunks: patchHunks,
}
}
// GetHunkContainingLine takes a line index and an offset and finds the hunk
// which contains the line index, then returns the hunk considering the offset.
// e.g. if the offset is 1 it will return the next hunk.
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
if len(p.PatchHunks) == 0 {
return nil
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
} else if resultIndex > len(p.PatchHunks)-1 {
resultIndex = len(p.PatchHunks) - 1
}
return p.PatchHunks[resultIndex]
}
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
return p.PatchHunks[len(p.PatchHunks)-1]
}
// otherwise select the first
return p.PatchHunks[0]
}
// 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 (l *PatchLine) render(selected bool, included bool) string {
content := l.Content
if len(content) == 0 {
content = " " // using the space so that we can still highlight if necessary
}
// for hunk headers we need to start off cyan and then use white for the message
if l.Kind == HUNK_HEADER {
re := regexp.MustCompile("(@@.*?@@)(.*)")
match := re.FindStringSubmatch(content)
return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
}
var textStyle style.TextStyle
switch l.Kind {
case PATCH_HEADER:
textStyle = textStyle.SetBold()
case ADDITION:
textStyle = style.FgGreen
case DELETION:
textStyle = style.FgRed
case COMMIT_SHA:
textStyle = style.FgYellow
default:
textStyle = theme.DefaultTextColor
}
return coloredString(textStyle, content, selected, included)
}
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
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:])
}
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
// ignore trailing newline.
lines := strings.Split(strings.TrimSuffix(patch, "\n"), "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines))
var lineKind PatchLineKind
var firstChar string
for index, line := range lines {
firstChar = " "
if len(line) > 0 {
firstChar = line[:1]
}
if index == 0 && strings.HasPrefix(line, "commit") {
lineKind = COMMIT_SHA
pastCommitDescription = false
} else if !pastCommitDescription {
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
pastCommitDescription = true
lineKind = PATCH_HEADER
} else {
lineKind = COMMIT_DESCRIPTION
}
} else if firstChar == "@" {
pastFirstHunkHeader = true
hunkStarts = append(hunkStarts, index)
lineKind = HUNK_HEADER
} else if pastFirstHunkHeader {
switch firstChar {
case "-":
lineKind = DELETION
stageableLines = append(stageableLines, index)
case "+":
lineKind = ADDITION
stageableLines = append(stageableLines, index)
case "\\":
lineKind = NEWLINE_MESSAGE
case " ":
lineKind = CONTEXT
}
} else {
lineKind = PATCH_HEADER
}
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
}
return hunkStarts, stageableLines, patchLines
}
// Render returns the coloured string of the diff with any selected lines highlighted
func (p *PatchParser) Render(isFocused bool, firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
contentToDisplay := slices.Some(p.PatchLines, func(line *PatchLine) bool {
return line.Content != ""
})
if !contentToDisplay {
return ""
}
renderedLines := slices.MapWithIndex(p.PatchLines, func(patchLine *PatchLine, index int) string {
selected := isFocused && index >= firstLineIndex && index <= lastLineIndex
included := lo.Contains(incLineIndices, index)
return patchLine.render(selected, included)
})
result := strings.Join(renderedLines, "\n")
return result
}
// RenderLinesPlain returns the non-coloured string of diff part from firstLineIndex to
// lastLineIndex
func (p *PatchParser) RenderLinesPlain(firstLineIndex, lastLineIndex int) string {
return renderLinesPlain(p.PatchLines[firstLineIndex : lastLineIndex+1])
}
func renderLinesPlain(lines []*PatchLine) string {
renderedLines := slices.Map(lines, func(line *PatchLine) string {
return line.Content + "\n"
})
return strings.Join(renderedLines, "")
}
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
// note this will actually include the current index if it is stageable
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
for _, lineIndex := range p.StageableLines {
if lineIndex >= currentIndex {
return lineIndex
}
}
return p.StageableLines[len(p.StageableLines)-1]
}

View File

@ -1,8 +1,6 @@
package patch
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -112,8 +110,7 @@ const exampleHunk = `@@ -1,5 +1,5 @@
...
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
func TestTransform(t *testing.T) {
type scenario struct {
testName string
filename string
@ -431,7 +428,7 @@ func TestModifyPatchForRange(t *testing.T) {
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,1 @@
@@ -0,0 +1 @@
+new line
\ No newline at end of file
`,
@ -445,7 +442,7 @@ func TestModifyPatchForRange(t *testing.T) {
reverse: true,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,1 @@
@@ -0,0 +1 @@
+new line
\ No newline at end of file
`,
@ -491,41 +488,128 @@ func TestModifyPatchForRange(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex,
PatchOptions{
Reverse: s.reverse,
KeepOriginalHeader: false,
})
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
lineIndices := ExpandRange(s.firstLineIndex, s.lastLineIndex)
result := Parse(s.diffText).
Transform(TransformOpts{
Reverse: s.reverse,
FileNameOverride: s.filename,
IncludedLineIndices: lineIndices,
}).
FormatPlain()
assert.Equal(t, s.expected, result)
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
func TestParseAndFormatPlain(t *testing.T) {
scenarios := []struct {
testName string
hunk *PatchHunk
idx int
expected int
}
scenarios := []scenario{
patchStr string
}{
{
testName: "nothing selected",
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
idx: 15,
expected: 3,
testName: "simpleDiff",
patchStr: simpleDiff,
},
{
testName: "addNewlineToEndOfFile",
patchStr: addNewlineToEndOfFile,
},
{
testName: "removeNewlinefromEndOfFile",
patchStr: removeNewlinefromEndOfFile,
},
{
testName: "twoHunks",
patchStr: twoHunks,
},
{
testName: "twoChangesInOneHunk",
patchStr: twoChangesInOneHunk,
},
{
testName: "newFile",
patchStr: newFile,
},
{
testName: "addNewlineToPreviouslyEmptyFile",
patchStr: addNewlineToPreviouslyEmptyFile,
},
{
testName: "exampleHunk",
patchStr: exampleHunk,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
result := s.hunk.LineNumberOfLine(s.idx)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
// here we parse the patch, then format it, and ensure the result
// matches the original patch. Note that unified diffs allow omitting
// the new length in a hunk header if the value is 1, and currently we always
// omit the new length in such cases.
patch := Parse(s.patchStr)
result := formatPlain(patch)
assert.Equal(t, s.patchStr, result)
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
testName string
patchStr string
indexes []int
expecteds []int
}
scenarios := []scenario{
{
testName: "twoHunks",
patchStr: twoHunks,
// this is really more of a characteristic test than anything.
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
expecteds: []int{1, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 8, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
result := patch.LineNumberOfLine(idx)
assert.Equal(t, s.expecteds[i], result)
}
})
}
}
func TestGetNextStageableLineIndex(t *testing.T) {
type scenario struct {
testName string
patchStr string
indexes []int
expecteds []int
}
scenarios := []scenario{
{
testName: "twoHunks",
patchStr: twoHunks,
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
expecteds: []int{6, 6, 6, 6, 6, 6, 6, 7, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
result := patch.GetNextChangeIdx(idx)
assert.Equal(t, s.expecteds[i], result)
}
})
}

View File

@ -0,0 +1,156 @@
package patch
import "github.com/samber/lo"
type patchTransformer struct {
patch *Patch
opts TransformOpts
}
type TransformOpts 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
// If set, we will replace the original header with one referring to this file name.
// 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
FileNameOverride string
// The indices of lines that should be included in the patch.
IncludedLineIndices []int
}
func transform(patch *Patch, opts TransformOpts) *Patch {
transformer := &patchTransformer{
patch: patch,
opts: opts,
}
return transformer.transform()
}
// helper function that takes a start and end index and returns a slice of all
// indexes inbetween (inclusive)
func ExpandRange(start int, end int) []int {
expanded := []int{}
for i := start; i <= end; i++ {
expanded = append(expanded, i)
}
return expanded
}
func (self *patchTransformer) transform() *Patch {
header := self.transformHeader()
hunks := self.transformHunks()
return &Patch{
header: header,
hunks: hunks,
}
}
func (self *patchTransformer) transformHeader() []string {
if self.opts.FileNameOverride != "" {
return []string{
"--- a/" + self.opts.FileNameOverride,
"+++ b/" + self.opts.FileNameOverride,
}
} else {
return self.patch.header
}
}
func (self *patchTransformer) transformHunks() []*Hunk {
newHunks := make([]*Hunk, 0, len(self.patch.hunks))
startOffset := 0
var formattedHunk *Hunk
for i, hunk := range self.patch.hunks {
startOffset, formattedHunk = self.transformHunk(
hunk,
startOffset,
self.patch.HunkStartIdx(i),
)
if formattedHunk.containsChanges() {
newHunks = append(newHunks, formattedHunk)
}
}
return newHunks
}
func (self *patchTransformer) transformHunk(hunk *Hunk, startOffset int, firstLineIdx int) (int, *Hunk) {
newLines := self.transformHunkLines(hunk, firstLineIdx)
newNewStart, newStartOffset := self.transformHunkHeader(newLines, hunk.oldStart, startOffset)
newHunk := &Hunk{
bodyLines: newLines,
oldStart: hunk.oldStart,
newStart: newNewStart,
headerContext: hunk.headerContext,
}
return newStartOffset, newHunk
}
func (self *patchTransformer) transformHunkLines(hunk *Hunk, firstLineIdx int) []*PatchLine {
skippedNewlineMessageIndex := -1
newLines := []*PatchLine{}
for i, line := range hunk.bodyLines {
lineIdx := i + firstLineIdx + 1 // plus one for header line
if line.Content == "" {
break
}
isLineSelected := lo.Contains(self.opts.IncludedLineIndices, lineIdx)
if isLineSelected || (line.Kind == NEWLINE_MESSAGE && skippedNewlineMessageIndex != lineIdx) || line.Kind == CONTEXT {
newLines = append(newLines, line)
continue
}
if (line.Kind == DELETION && !self.opts.Reverse) || (line.Kind == ADDITION && self.opts.Reverse) {
content := " " + line.Content[1:]
newLines = append(newLines, &PatchLine{
Kind: CONTEXT,
Content: content,
})
continue
}
if line.Kind == ADDITION {
// 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
}
func (self *patchTransformer) transformHunkHeader(newBodyLines []*PatchLine, oldStart int, startOffset int) (int, int) {
oldLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, DELETION})
newLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, ADDITION})
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
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
return newStart, newStartOffset
}

View File

@ -52,7 +52,7 @@ func (gui *Gui) branchCommitsRenderToMain() error {
func (gui *Gui) secondaryPatchPanelUpdateOpts() *types.ViewUpdateOpts {
if gui.git.Patch.PatchManager.Active() {
patch := gui.git.Patch.PatchManager.RenderAggregatedPatchColored(false)
patch := gui.git.Patch.PatchManager.RenderAggregatedPatch(false)
return &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(patch),

View File

@ -181,10 +181,16 @@ func (self *StagingController) applySelection(reverse bool) error {
}
firstLineIdx, lastLineIdx := state.SelectedRange()
patch := patch.ModifiedPatchForRange(self.c.Log, path, state.GetDiff(), firstLineIdx, lastLineIdx,
patch.PatchOptions{Reverse: reverse, KeepOriginalHeader: false})
patchToApply := patch.
Parse(state.GetDiff()).
Transform(patch.TransformOpts{
Reverse: reverse,
IncludedLineIndices: patch.ExpandRange(firstLineIdx, lastLineIdx),
FileNameOverride: path,
}).
FormatPlain()
if patch == "" {
if patchToApply == "" {
return nil
}
@ -198,7 +204,7 @@ func (self *StagingController) applySelection(reverse bool) error {
applyFlags = append(applyFlags, "cached")
}
self.c.LogAction(self.c.Tr.Actions.ApplyPatch)
err := self.git.WorkingTree.ApplyPatch(patch, applyFlags...)
err := self.git.WorkingTree.ApplyPatch(patchToApply, applyFlags...)
if err != nil {
return self.c.Error(err)
}
@ -229,18 +235,23 @@ func (self *StagingController) editHunk() error {
return nil
}
hunk := state.CurrentHunk()
patchText := patch.ModifiedPatchForRange(
self.c.Log, path, state.GetDiff(), hunk.FirstLineIdx, hunk.LastLineIdx(),
patch.PatchOptions{Reverse: self.staged, KeepOriginalHeader: false},
)
hunkStartIdx, hunkEndIdx := state.CurrentHunkBounds()
patchText := patch.
Parse(state.GetDiff()).
Transform(patch.TransformOpts{
Reverse: self.staged,
IncludedLineIndices: patch.ExpandRange(hunkStartIdx, hunkEndIdx),
FileNameOverride: path,
}).
FormatPlain()
patchFilepath, err := self.git.WorkingTree.SaveTemporaryPatch(patchText)
if err != nil {
return err
}
lineOffset := 3
lineIdxInHunk := state.GetSelectedLineIdx() - hunk.FirstLineIdx
lineIdxInHunk := state.GetSelectedLineIdx() - hunkStartIdx
if err := self.helpers.Files.EditFileAtLine(patchFilepath, lineIdxInHunk+lineOffset); err != nil {
return err
}
@ -253,10 +264,13 @@ func (self *StagingController) editHunk() error {
self.c.LogAction(self.c.Tr.Actions.ApplyPatch)
lineCount := strings.Count(editedPatchText, "\n") + 1
newPatchText := patch.ModifiedPatchForRange(
self.c.Log, path, editedPatchText, 0, lineCount,
patch.PatchOptions{KeepOriginalHeader: false},
)
newPatchText := patch.
Parse(editedPatchText).
Transform(patch.TransformOpts{
IncludedLineIndices: patch.ExpandRange(0, lineCount),
FileNameOverride: path,
}).
FormatPlain()
applyFlags := []string{"cached"}
if self.staged {

View File

@ -202,7 +202,7 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
}
func (gui *Gui) copyPatchToClipboard() error {
patch := gui.git.Patch.PatchManager.RenderAggregatedPatchColored(true)
patch := gui.git.Patch.PatchManager.RenderAggregatedPatch(true)
gui.c.LogAction(gui.c.Tr.Actions.CopyPatchToClipboard)
if err := gui.os.CopyToClipboard(patch); err != nil {

View File

@ -1,6 +1,7 @@
package patch_exploring
import (
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/sirupsen/logrus"
)
@ -12,7 +13,7 @@ type State struct {
selectedLineIdx int
rangeStartLineIdx int
diff string
patchParser *patch.PatchParser
patch *patch.Patch
selectMode selectMode
}
@ -33,9 +34,9 @@ func NewState(diff string, selectedLineIdx int, oldState *State, log *logrus.Ent
return oldState
}
patchParser := patch.NewPatchParser(log, diff)
patch := patch.Parse(diff)
if len(patchParser.StageableLines) == 0 {
if !patch.ContainsChanges() {
return nil
}
@ -54,13 +55,13 @@ func NewState(diff string, selectedLineIdx int, oldState *State, log *logrus.Ent
if oldState.selectMode == HUNK {
selectMode = HUNK
}
selectedLineIdx = patchParser.GetNextStageableLineIndex(oldState.selectedLineIdx)
selectedLineIdx = patch.GetNextChangeIdx(oldState.selectedLineIdx)
} else {
selectedLineIdx = patchParser.StageableLines[0]
selectedLineIdx = patch.GetNextChangeIdx(0)
}
return &State{
patchParser: patchParser,
patch: patch,
selectedLineIdx: selectedLineIdx,
selectMode: selectMode,
rangeStartLineIdx: rangeStartLineIdx,
@ -112,8 +113,8 @@ func (s *State) SetLineSelectMode() {
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
} else if newSelectedLineIdx > s.patch.LineCount()-1 {
newSelectedLineIdx = s.patch.LineCount() - 1
}
s.selectedLineIdx = newSelectedLineIdx
@ -141,8 +142,9 @@ func (s *State) CycleHunk(forward bool) {
change = -1
}
newHunk := s.patchParser.GetHunkContainingLine(s.selectedLineIdx, change)
s.selectedLineIdx = s.patchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
hunkIdx := s.patch.HunkContainingLine(s.selectedLineIdx)
start := s.patch.HunkStartIdx(hunkIdx + change)
s.selectedLineIdx = s.patch.GetNextChangeIdx(start)
}
func (s *State) CycleLine(forward bool) {
@ -154,15 +156,18 @@ func (s *State) CycleLine(forward bool) {
s.SelectLine(s.selectedLineIdx + change)
}
func (s *State) CurrentHunk() *patch.PatchHunk {
return s.patchParser.GetHunkContainingLine(s.selectedLineIdx, 0)
// returns first and last patch line index of current hunk
func (s *State) CurrentHunkBounds() (int, int) {
hunkIdx := s.patch.HunkContainingLine(s.selectedLineIdx)
start := s.patch.HunkStartIdx(hunkIdx)
end := s.patch.HunkEndIdx(hunkIdx)
return start, end
}
func (s *State) SelectedRange() (int, int) {
switch s.selectMode {
case HUNK:
hunk := s.CurrentHunk()
return hunk.FirstLineIdx, hunk.LastLineIdx()
return s.CurrentHunkBounds()
case RANGE:
if s.rangeStartLineIdx > s.selectedLineIdx {
return s.selectedLineIdx, s.rangeStartLineIdx
@ -178,7 +183,7 @@ func (s *State) SelectedRange() (int, int) {
}
func (s *State) CurrentLineNumber() int {
return s.CurrentHunk().LineNumberOfLine(s.selectedLineIdx)
return s.patch.LineNumberOfLine(s.selectedLineIdx)
}
func (s *State) AdjustSelectedLineIdx(change int) {
@ -187,17 +192,23 @@ func (s *State) AdjustSelectedLineIdx(change int) {
func (s *State) RenderForLineIndices(isFocused bool, includedLineIndices []int) string {
firstLineIdx, lastLineIdx := s.SelectedRange()
return s.patchParser.Render(isFocused, firstLineIdx, lastLineIdx, includedLineIndices)
includedLineIndicesSet := set.NewFromSlice(includedLineIndices)
return s.patch.FormatView(patch.FormatViewOpts{
IsFocused: isFocused,
FirstLineIndex: firstLineIdx,
LastLineIndex: lastLineIdx,
IncLineIndices: includedLineIndicesSet,
})
}
func (s *State) PlainRenderSelected() string {
firstLineIdx, lastLineIdx := s.SelectedRange()
return s.patchParser.RenderLinesPlain(firstLineIdx, lastLineIdx)
return s.patch.FormatRangePlain(firstLineIdx, lastLineIdx)
}
func (s *State) SelectBottom() {
s.SetLineSelectMode()
s.SelectLine(len(s.patchParser.PatchLines) - 1)
s.SelectLine(s.patch.LineCount() - 1)
}
func (s *State) SelectTop() {