on main help sign in
feat(diffview): add support for setting vertical offset
commit45ddc6680a2b07d18b49d2877350fde783aea5f8 authorAndrey Nering <andreynering@users.noreply.github.com> dateWed 11 Jun 2025, 15:04 (1 year ago) parent3c80193
3 files changed+114−52
internal/exp/diffview/Taskfile.yaml+20 internal/exp/diffview/diffview.go+71−52 internal/exp/diffview/diffview_test.go+23
internal/exp/diffview/Taskfile.yaml+20
@@ -80,3 +80,23 @@ tasks:
8080 - for: sources
8181 cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
8282 silent: true
83+
84+ test:print:yoffset:unified:
85+ desc: Print golden files for debugging
86+ method: none
87+ sources:
88+ - ./testdata/TestDiffViewYOffset/Unified/*.golden
89+ cmds:
90+ - for: sources
91+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
92+ silent: true
93+
94+ test:print:yoffset:split:
95+ desc: Print golden files for debugging
96+ method: none
97+ sources:
98+ - ./testdata/TestDiffViewYOffset/Split/*.golden
99+ cmds:
100+ - for: sources
101+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
102+ silent: true
internal/exp/diffview/diffview.go+71−52
@@ -40,6 +40,7 @@ type DiffView struct {
4040 height int
4141 width int
4242 xOffset int
43+ yOffset int
4344 style Style
4445
4546 isComputed bool
@@ -137,6 +138,12 @@ func (dv *DiffView) XOffset(xOffset int) *DiffView {
137138 return dv
138139}
139140
141+// YOffset sets the vertical offset for the DiffView.
142+func (dv *DiffView) YOffset(yOffset int) *DiffView {
143+ dv.yOffset = yOffset
144+ return dv
145+}
146+
140147// String returns the string representation of the DiffView.
141148func (dv *DiffView) String() string {
142149 if err := dv.computeDiff(); err != nil {
@@ -296,17 +303,23 @@ func (dv *DiffView) renderUnified() string {
296303 var b strings.Builder
297304
298305 fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
299− printedLines := 0
306+ printedLines := -dv.yOffset
307+
308+ write := func(s string) {
309+ if printedLines >= 0 {
310+ b.WriteString(s)
311+ }
312+ }
300313
301314outer:
302315 for i, h := range dv.unified.Hunks {
303316 if dv.lineNumbers {
304− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
305− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
317+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
318+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
306319 }
307320 content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
308− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
309− b.WriteRune('\n')
321+ write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
322+ write("\n")
310323 printedLines++
311324
312325 beforeLine := h.FromLine
@@ -319,13 +332,13 @@ outer:
319332 isLastLine := j+1 == len(h.Lines)
320333 if hasReachedHeight && (!isLastHunk || !isLastLine) {
321334 if dv.lineNumbers {
322− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
323− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
335+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
336+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
324337 }
325− b.WriteString(fullContentStyle.Render(
338+ write(fullContentStyle.Render(
326339 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
327340 ))
328− b.WriteRune('\n')
341+ write("\n")
329342 break outer
330343 }
331344
@@ -338,36 +351,36 @@ outer:
338351 switch l.Kind {
339352 case udiff.Equal:
340353 if dv.lineNumbers {
341− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
342− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
354+ write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
355+ write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
343356 }
344− b.WriteString(fullContentStyle.Render(
357+ write(fullContentStyle.Render(
345358 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
346359 ))
347360 beforeLine++
348361 afterLine++
349362 case udiff.Insert:
350363 if dv.lineNumbers {
351− b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
352− b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
364+ write(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
365+ write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
353366 }
354− b.WriteString(fullContentStyle.Render(
367+ write(fullContentStyle.Render(
355368 dv.style.InsertLine.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
356369 dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content),
357370 ))
358371 afterLine++
359372 case udiff.Delete:
360373 if dv.lineNumbers {
361− b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
362− b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
374+ write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
375+ write(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
363376 }
364− b.WriteString(fullContentStyle.Render(
377+ write(fullContentStyle.Render(
365378 dv.style.DeleteLine.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
366379 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
367380 ))
368381 beforeLine++
369382 }
370− b.WriteRune('\n')
383+ write("\n")
371384
372385 printedLines++
373386 }
@@ -375,11 +388,11 @@ outer:
375388
376389 for printedLines < dv.height {
377390 if dv.lineNumbers {
378− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
379− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
391+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
392+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
380393 }
381− b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
382− b.WriteRune('\n')
394+ write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
395+ write("\n")
383396 printedLines++
384397 }
385398
@@ -392,20 +405,26 @@ func (dv *DiffView) renderSplit() string {
392405
393406 beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
394407 afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
395− printedLines := 0
408+ printedLines := -dv.yOffset
409+
410+ write := func(s string) {
411+ if printedLines >= 0 {
412+ b.WriteString(s)
413+ }
414+ }
396415
397416outer:
398417 for i, h := range dv.splitHunks {
399418 if dv.lineNumbers {
400− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
419+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
401420 }
402421 content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
403− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
422+ write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
404423 if dv.lineNumbers {
405− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
424+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
406425 }
407− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
408− b.WriteRune('\n')
426+ write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
427+ write("\n")
409428 printedLines++
410429
411430 beforeLine := h.fromLine
@@ -418,18 +437,18 @@ outer:
418437 isLastLine := j+1 == len(h.lines)
419438 if hasReachedHeight && (!isLastHunk || !isLastLine) {
420439 if dv.lineNumbers {
421− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
440+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
422441 }
423− b.WriteString(beforeFullContentStyle.Render(
442+ write(beforeFullContentStyle.Render(
424443 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
425444 ))
426445 if dv.lineNumbers {
427− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
446+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
428447 }
429− b.WriteString(afterFullContentStyle.Render(
448+ write(afterFullContentStyle.Render(
430449 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
431450 ))
432− b.WriteRune('\n')
451+ write("\n")
433452 break outer
434453 }
435454
@@ -452,24 +471,24 @@ outer:
452471 switch {
453472 case l.before == nil:
454473 if dv.lineNumbers {
455− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
474+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
456475 }
457− b.WriteString(beforeFullContentStyle.Render(
476+ write(beforeFullContentStyle.Render(
458477 dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "),
459478 ))
460479 case l.before.Kind == udiff.Equal:
461480 if dv.lineNumbers {
462− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
481+ write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
463482 }
464− b.WriteString(beforeFullContentStyle.Render(
483+ write(beforeFullContentStyle.Render(
465484 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingBeforeEllipsis, " …", " ") + beforeContent),
466485 ))
467486 beforeLine++
468487 case l.before.Kind == udiff.Delete:
469488 if dv.lineNumbers {
470− b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
489+ write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
471490 }
472− b.WriteString(beforeFullContentStyle.Render(
491+ write(beforeFullContentStyle.Render(
473492 dv.style.DeleteLine.Symbol.Render(ternary(leadingBeforeEllipsis, "-…", "- ")) +
474493 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent),
475494 ))
@@ -479,31 +498,31 @@ outer:
479498 switch {
480499 case l.after == nil:
481500 if dv.lineNumbers {
482− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
501+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
483502 }
484− b.WriteString(afterFullContentStyle.Render(
503+ write(afterFullContentStyle.Render(
485504 dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
486505 ))
487506 case l.after.Kind == udiff.Equal:
488507 if dv.lineNumbers {
489− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
508+ write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
490509 }
491− b.WriteString(afterFullContentStyle.Render(
510+ write(afterFullContentStyle.Render(
492511 dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingAfterEllipsis, " …", " ") + afterContent),
493512 ))
494513 afterLine++
495514 case l.after.Kind == udiff.Insert:
496515 if dv.lineNumbers {
497− b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
516+ write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
498517 }
499− b.WriteString(afterFullContentStyle.Render(
518+ write(afterFullContentStyle.Render(
500519 dv.style.InsertLine.Symbol.Render(ternary(leadingAfterEllipsis, "+…", "+ ")) +
501520 dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(afterContent),
502521 ))
503522 afterLine++
504523 }
505524
506− b.WriteRune('\n')
525+ write("\n")
507526
508527 printedLines++
509528 }
@@ -511,14 +530,14 @@ outer:
511530
512531 for printedLines < dv.height {
513532 if dv.lineNumbers {
514− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
533+ write(dv.style.MissingLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
515534 }
516− b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
535+ write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
517536 if dv.lineNumbers {
518− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
537+ write(dv.style.MissingLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
519538 }
520− b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
521− b.WriteRune('\n')
539+ write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
540+ write("\n")
522541 printedLines++
523542 }
524543
internal/exp/diffview/diffview_test.go+23
@@ -216,6 +216,29 @@ func TestDiffViewXOffset(t *testing.T) {
216216 }
217217}
218218
219+func TestDiffViewYOffset(t *testing.T) {
220+ for layoutName, layoutFunc := range LayoutFuncs {
221+ t.Run(layoutName, func(t *testing.T) {
222+ for yOffset := range 17 {
223+ t.Run(fmt.Sprintf("YOffsetOf%02d", yOffset), func(t *testing.T) {
224+ dv := diffview.New().
225+ Before("main.go", TestMultipleHunksBefore).
226+ After("main.go", TestMultipleHunksAfter).
227+ Style(diffview.DefaultLightStyle).
228+ Height(5).
229+ YOffset(yOffset)
230+ dv = layoutFunc(dv)
231+
232+ output := dv.String()
233+ golden.RequireEqual(t, []byte(output))
234+
235+ assertHeight(t, 5, output)
236+ })
237+ }
238+ })
239+ }
240+}
241+
219242func assertLineWidth(t *testing.T, expected int, output string) {
220243 var lineWidth int
221244 for line := range strings.SplitSeq(output, "\n") {