@@ -42,6 +42,10 @@ type DiffView struct {
4242 err error
4343 unified udiff.UnifiedDiff
4444 edits []udiff.Edit
45+
46+ splitHunks []splitHunk
47+ beforeCodeWidth int
48+ afterCodeWidth int
4549}
4650
4751// New creates a new DiffView with default settings.
@@ -124,7 +128,20 @@ func (dv *DiffView) String() string {
124128 if err := dv.computeDiff(); err != nil {
125129 return err.Error()
126130 }
127− dv.detectWidth()
131+
132+ switch dv.layout {
133+ case layoutUnified:
134+ return dv.renderUnified()
135+ case layoutSplit:
136+ return dv.renderSplit()
137+ default:
138+ panic("unknown diffview layout")
139+ }
140+}
141+
142+// renderUnified renders the unified diff view as a string.
143+func (dv *DiffView) renderUnified() string {
144+ dv.detectUnifiedWidth()
128145
129146 codeWidth := dv.width - leadingSymbolsSize
130147 beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
@@ -178,6 +195,88 @@ func (dv *DiffView) String() string {
178195 return b.String()
179196}
180197
198+// renderSplit renders the split (side-by-side) diff view as a string.
199+func (dv *DiffView) renderSplit() string {
200+ dv.convertDiffToSplit()
201+ dv.detectSplitWidth()
202+
203+ beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
204+
205+ var b strings.Builder
206+
207+ for i, h := range dv.splitHunks {
208+ if dv.lineNumbers {
209+ b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
210+ }
211+ b.WriteString(dv.style.DividerLine.Code.Width(dv.beforeCodeWidth + leadingSymbolsSize).Render(dv.hunkLineFor(dv.unified.Hunks[i])))
212+ if dv.lineNumbers {
213+ b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
214+ }
215+ b.WriteString(dv.style.DividerLine.Code.Width(dv.afterCodeWidth + leadingSymbolsSize).Render(" "))
216+ b.WriteRune('\n')
217+
218+ beforeLine := h.fromLine
219+ afterLine := h.toLine
220+
221+ for _, l := range h.lines {
222+ var beforeContent string
223+ var afterContent string
224+ if l.before != nil {
225+ beforeContent = strings.TrimSuffix(l.before.Content, "\n")
226+ }
227+ if l.after != nil {
228+ afterContent = strings.TrimSuffix(l.after.Content, "\n")
229+ }
230+
231+ switch {
232+ case l.before == nil:
233+ if dv.lineNumbers {
234+ b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", beforeNumDigits)))
235+ }
236+ b.WriteString(dv.style.MissingLine.Code.Width(dv.beforeCodeWidth + leadingSymbolsSize).Render(" "))
237+ case l.before.Kind == udiff.Equal:
238+ if dv.lineNumbers {
239+ b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
240+ }
241+ b.WriteString(dv.style.EqualLine.Code.Width(dv.beforeCodeWidth + leadingSymbolsSize).Render(" " + beforeContent))
242+ beforeLine++
243+ case l.before.Kind == udiff.Delete:
244+ if dv.lineNumbers {
245+ b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
246+ }
247+ b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
248+ b.WriteString(dv.style.DeleteLine.Code.Width(dv.beforeCodeWidth).Render(beforeContent))
249+ beforeLine++
250+ }
251+
252+ switch {
253+ case l.after == nil:
254+ if dv.lineNumbers {
255+ b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", afterNumDigits)))
256+ }
257+ b.WriteString(dv.style.MissingLine.Code.Width(dv.afterCodeWidth + leadingSymbolsSize).Render(" "))
258+ case l.after.Kind == udiff.Equal:
259+ if dv.lineNumbers {
260+ b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
261+ }
262+ b.WriteString(dv.style.EqualLine.Code.Width(dv.afterCodeWidth + leadingSymbolsSize).Render(" " + afterContent))
263+ afterLine++
264+ case l.after.Kind == udiff.Insert:
265+ if dv.lineNumbers {
266+ b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
267+ }
268+ b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
269+ b.WriteString(dv.style.InsertLine.Code.Width(dv.afterCodeWidth).Render(afterContent))
270+ afterLine++
271+ }
272+
273+ b.WriteRune('\n')
274+ }
275+ }
276+
277+ return b.String()
278+}
279+
181280func (dv *DiffView) computeDiff() error {
182281 if dv.isComputed {
183282 return dv.err
@@ -236,7 +335,7 @@ func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
236335 return
237336}
238337
239−func (dv *DiffView) detectWidth() {
338+func (dv *DiffView) detectUnifiedWidth() {
240339 if dv.width > 0 {
241340 return
242341 }
@@ -251,3 +350,36 @@ func (dv *DiffView) detectWidth() {
251350 }
252351 }
253352}
353+
354+func (dv *DiffView) convertDiffToSplit() {
355+ dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
356+ for i, h := range dv.unified.Hunks {
357+ dv.splitHunks[i] = hunkToSplit(h)
358+ }
359+}
360+
361+func (dv *DiffView) detectSplitWidth() {
362+ if dv.width > 0 {
363+ return
364+ }
365+
366+ for i, h := range dv.splitHunks {
367+ shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
368+
369+ for _, l := range h.lines {
370+ var lineWidth int
371+ if l.before != nil {
372+ codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
373+ dv.beforeCodeWidth = max(dv.beforeCodeWidth, codeWidth, shownLines)
374+ lineWidth += codeWidth
375+ }
376+ if l.after != nil {
377+ codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
378+ dv.afterCodeWidth = max(dv.afterCodeWidth, codeWidth, shownLines)
379+ lineWidth += codeWidth
380+ }
381+ lineWidth += leadingSymbolsSize * 2
382+ dv.width = max(dv.width, lineWidth, shownLines)
383+ }
384+ }
385+}