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 // 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 { // 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, 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++ } for _, line := range self.patch.header { // always passing false for 'included' here because header lines are not part of the patch appendLine(self.formatLineAux(line, theme.DefaultTextColor.SetBold(), false)) } for _, hunk := range self.patch.hunks { appendLine( self.formatLineAux( hunk.formatHeaderStart(), style.FgCyan, false, ) + // we're splitting the line into two parts: the diff header and the context // We explicitly pass 'included' as false for both because these are not part // of the actual patch self.formatLineAux( hunk.headerContext, theme.DefaultTextColor, false, ), ) for _, line := range hunk.bodyLines { style := self.patchLineStyle(line) if line.isChange() { appendLine(self.formatLine(line.Content, style, lineIdx)) } else { appendLine(self.formatLineAux(line.Content, style, false)) } } } 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, 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, included bool) string { if self.plain { return str } 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:]) }