on main help sign in
feat(diffview): add support for setting vertical offset
45ddc66Andrey Nering
internal/exp/diffview/Taskfile.yaml+20
@@ -80,3 +80,23 @@ tasks:
80 - for: sources
80 - for: sources
81 cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
81 cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
82 silent: true
82 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 {
40 height int
40 height int
41 width int
41 width int
42 xOffset int
42 xOffset int
43+ yOffset int
43 style Style
44 style Style
44
45
45 isComputed bool
46 isComputed bool
@@ -137,6 +138,12 @@ func (dv *DiffView) XOffset(xOffset int) *DiffView {
137 return dv
138 return dv
138}
139}
139
140
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+
140// String returns the string representation of the DiffView.
147// String returns the string representation of the DiffView.
141func (dv *DiffView) String() string {
148func (dv *DiffView) String() string {
142 if err := dv.computeDiff(); err != nil {
149 if err := dv.computeDiff(); err != nil {
@@ -296,17 +303,23 @@ func (dv *DiffView) renderUnified() string {
296 var b strings.Builder
303 var b strings.Builder
297
304
298 fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
305 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+ }
300
313
301outer:
314outer:
302 for i, h := range dv.unified.Hunks {
315 for i, h := range dv.unified.Hunks {
303 if dv.lineNumbers {
316 if dv.lineNumbers {
304− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
317+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
305− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
318+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
306 }
319 }
307 content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
320 content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
308− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
321+ write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth).Render(content))
309− b.WriteRune('\n')
322+ write("\n")
310 printedLines++
323 printedLines++
311
324
312 beforeLine := h.FromLine
325 beforeLine := h.FromLine
@@ -319,13 +332,13 @@ outer:
319 isLastLine := j+1 == len(h.Lines)
332 isLastLine := j+1 == len(h.Lines)
320 if hasReachedHeight && (!isLastHunk || !isLastLine) {
333 if hasReachedHeight && (!isLastHunk || !isLastLine) {
321 if dv.lineNumbers {
334 if dv.lineNumbers {
322− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
335+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
323− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
336+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
324 }
337 }
325− b.WriteString(fullContentStyle.Render(
338+ write(fullContentStyle.Render(
326 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
339 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
327 ))
340 ))
328− b.WriteRune('\n')
341+ write("\n")
329 break outer
342 break outer
330 }
343 }
331
344
@@ -338,36 +351,36 @@ outer:
338 switch l.Kind {
351 switch l.Kind {
339 case udiff.Equal:
352 case udiff.Equal:
340 if dv.lineNumbers {
353 if dv.lineNumbers {
341− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
354+ write(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
342− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
355+ write(dv.style.EqualLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
343 }
356 }
344− b.WriteString(fullContentStyle.Render(
357+ write(fullContentStyle.Render(
345 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
358 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
346 ))
359 ))
347 beforeLine++
360 beforeLine++
348 afterLine++
361 afterLine++
349 case udiff.Insert:
362 case udiff.Insert:
350 if dv.lineNumbers {
363 if dv.lineNumbers {
351− b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
364+ write(dv.style.InsertLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
352− b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
365+ write(dv.style.InsertLine.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
353 }
366 }
354− b.WriteString(fullContentStyle.Render(
367+ write(fullContentStyle.Render(
355 dv.style.InsertLine.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
368 dv.style.InsertLine.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
356 dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content),
369 dv.style.InsertLine.Code.Width(dv.codeWidth).Render(content),
357 ))
370 ))
358 afterLine++
371 afterLine++
359 case udiff.Delete:
372 case udiff.Delete:
360 if dv.lineNumbers {
373 if dv.lineNumbers {
361− b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
374+ write(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
362− b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
375+ write(dv.style.DeleteLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
363 }
376 }
364− b.WriteString(fullContentStyle.Render(
377+ write(fullContentStyle.Render(
365 dv.style.DeleteLine.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
378 dv.style.DeleteLine.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
366 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
379 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(content),
367 ))
380 ))
368 beforeLine++
381 beforeLine++
369 }
382 }
370− b.WriteRune('\n')
383+ write("\n")
371
384
372 printedLines++
385 printedLines++
373 }
386 }
@@ -375,11 +388,11 @@ outer:
375
388
376 for printedLines < dv.height {
389 for printedLines < dv.height {
377 if dv.lineNumbers {
390 if dv.lineNumbers {
378− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
391+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
379− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
392+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
380 }
393 }
381− b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
394+ write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
382− b.WriteRune('\n')
395+ write("\n")
383 printedLines++
396 printedLines++
384 }
397 }
385
398
@@ -392,20 +405,26 @@ func (dv *DiffView) renderSplit() string {
392
405
393 beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
406 beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
394 afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
407 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+ }
396
415
397outer:
416outer:
398 for i, h := range dv.splitHunks {
417 for i, h := range dv.splitHunks {
399 if dv.lineNumbers {
418 if dv.lineNumbers {
400− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
419+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
401 }
420 }
402 content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
421 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))
404 if dv.lineNumbers {
423 if dv.lineNumbers {
405− b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
424+ write(dv.style.DividerLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
406 }
425 }
407− b.WriteString(dv.style.DividerLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
426+ write(dv.style.DividerLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
408− b.WriteRune('\n')
427+ write("\n")
409 printedLines++
428 printedLines++
410
429
411 beforeLine := h.fromLine
430 beforeLine := h.fromLine
@@ -418,18 +437,18 @@ outer:
418 isLastLine := j+1 == len(h.lines)
437 isLastLine := j+1 == len(h.lines)
419 if hasReachedHeight && (!isLastHunk || !isLastLine) {
438 if hasReachedHeight && (!isLastHunk || !isLastLine) {
420 if dv.lineNumbers {
439 if dv.lineNumbers {
421− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
440+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
422 }
441 }
423− b.WriteString(beforeFullContentStyle.Render(
442+ write(beforeFullContentStyle.Render(
424 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
443 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
425 ))
444 ))
426 if dv.lineNumbers {
445 if dv.lineNumbers {
427− b.WriteString(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
446+ write(dv.style.EqualLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
428 }
447 }
429− b.WriteString(afterFullContentStyle.Render(
448+ write(afterFullContentStyle.Render(
430 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
449 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(" …"),
431 ))
450 ))
432− b.WriteRune('\n')
451+ write("\n")
433 break outer
452 break outer
434 }
453 }
435
454
@@ -452,24 +471,24 @@ outer:
452 switch {
471 switch {
453 case l.before == nil:
472 case l.before == nil:
454 if dv.lineNumbers {
473 if dv.lineNumbers {
455− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
474+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
456 }
475 }
457− b.WriteString(beforeFullContentStyle.Render(
476+ write(beforeFullContentStyle.Render(
458 dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "),
477 dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "),
459 ))
478 ))
460 case l.before.Kind == udiff.Equal:
479 case l.before.Kind == udiff.Equal:
461 if dv.lineNumbers {
480 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)))
463 }
482 }
464− b.WriteString(beforeFullContentStyle.Render(
483+ write(beforeFullContentStyle.Render(
465 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingBeforeEllipsis, " …", " ") + beforeContent),
484 dv.style.EqualLine.Code.Width(dv.fullCodeWidth).Render(ternary(leadingBeforeEllipsis, " …", " ") + beforeContent),
466 ))
485 ))
467 beforeLine++
486 beforeLine++
468 case l.before.Kind == udiff.Delete:
487 case l.before.Kind == udiff.Delete:
469 if dv.lineNumbers {
488 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)))
471 }
490 }
472− b.WriteString(beforeFullContentStyle.Render(
491+ write(beforeFullContentStyle.Render(
473 dv.style.DeleteLine.Symbol.Render(ternary(leadingBeforeEllipsis, "-…", "- ")) +
492 dv.style.DeleteLine.Symbol.Render(ternary(leadingBeforeEllipsis, "-…", "- ")) +
474 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent),
493 dv.style.DeleteLine.Code.Width(dv.codeWidth).Render(beforeContent),
475 ))
494 ))
@@ -479,31 +498,31 @@ outer:
479 switch {
498 switch {
480 case l.after == nil:
499 case l.after == nil:
481 if dv.lineNumbers {
500 if dv.lineNumbers {
482− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
501+ write(dv.style.MissingLine.LineNumber.Render(pad(" ", dv.afterNumDigits)))
483 }
502 }
484− b.WriteString(afterFullContentStyle.Render(
503+ write(afterFullContentStyle.Render(
485 dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
504 dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
486 ))
505 ))
487 case l.after.Kind == udiff.Equal:
506 case l.after.Kind == udiff.Equal:
488 if dv.lineNumbers {
507 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)))
490 }
509 }
491− b.WriteString(afterFullContentStyle.Render(
510+ write(afterFullContentStyle.Render(
492 dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingAfterEllipsis, " …", " ") + afterContent),
511 dv.style.EqualLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingAfterEllipsis, " …", " ") + afterContent),
493 ))
512 ))
494 afterLine++
513 afterLine++
495 case l.after.Kind == udiff.Insert:
514 case l.after.Kind == udiff.Insert:
496 if dv.lineNumbers {
515 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)))
498 }
517 }
499− b.WriteString(afterFullContentStyle.Render(
518+ write(afterFullContentStyle.Render(
500 dv.style.InsertLine.Symbol.Render(ternary(leadingAfterEllipsis, "+…", "+ ")) +
519 dv.style.InsertLine.Symbol.Render(ternary(leadingAfterEllipsis, "+…", "+ ")) +
501 dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(afterContent),
520 dv.style.InsertLine.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(afterContent),
502 ))
521 ))
503 afterLine++
522 afterLine++
504 }
523 }
505
524
506− b.WriteRune('\n')
525+ write("\n")
507
526
508 printedLines++
527 printedLines++
509 }
528 }
@@ -511,14 +530,14 @@ outer:
511
530
512 for printedLines < dv.height {
531 for printedLines < dv.height {
513 if dv.lineNumbers {
532 if dv.lineNumbers {
514− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
533+ write(dv.style.MissingLine.LineNumber.Render(pad("…", dv.beforeNumDigits)))
515 }
534 }
516− b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
535+ write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth).Render(" "))
517 if dv.lineNumbers {
536 if dv.lineNumbers {
518− b.WriteString(dv.style.MissingLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
537+ write(dv.style.MissingLine.LineNumber.Render(pad("…", dv.afterNumDigits)))
519 }
538 }
520− b.WriteString(dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
539+ write(dv.style.MissingLine.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
521− b.WriteRune('\n')
540+ write("\n")
522 printedLines++
541 printedLines++
523 }
542 }
524
543
internal/exp/diffview/diffview_test.go+23
@@ -216,6 +216,29 @@ func TestDiffViewXOffset(t *testing.T) {
216 }
216 }
217}
217}
218
218
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+
219func assertLineWidth(t *testing.T, expected int, output string) {
242func assertLineWidth(t *testing.T, expected int, output string) {
220 var lineWidth int
243 var lineWidth int
221 for line := range strings.SplitSeq(output, "\n") {
244 for line := range strings.SplitSeq(output, "\n") {