feat(diffview): show hunk lines and test for multiple hunks

Andrey Nering created

Change summary

internal/exp/diffview/diffview.go                                 | 56 
internal/exp/diffview/diffview_test.go                            | 22 
internal/exp/diffview/style.go                                    | 27 
internal/exp/diffview/testdata/TestDefault/DarkMode.golden        |  1 
internal/exp/diffview/testdata/TestDefault/LightMode.golden       |  1 
internal/exp/diffview/testdata/TestMultipleHunks.after            | 15 
internal/exp/diffview/testdata/TestMultipleHunks.before           | 13 
internal/exp/diffview/testdata/TestMultipleHunks/DarkMode.golden  | 16 
internal/exp/diffview/testdata/TestMultipleHunks/LightMode.golden | 16 
internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.golden  |  1 
internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden |  1 
11 files changed, 158 insertions(+), 11 deletions(-)

Detailed changes

internal/exp/diffview/diffview.go šŸ”—

@@ -1,6 +1,7 @@
 package diffview
 
 import (
+	"fmt"
 	"os"
 	"strconv"
 	"strings"
@@ -125,22 +126,34 @@ func (dv *DiffView) String() string {
 	}
 	dv.detectWidth()
 
-	lineNumberWidth := func(start, num int) int {
-		return len(strconv.Itoa(start + num))
-	}
+	codeWidth := dv.width - leadingSymbolsSize
+	beforeNumDigits, afterNumDigits := dv.lineNumberDigits()
 
 	var b strings.Builder
 
-	for _, h := range dv.unified.Hunks {
+	for i, h := range dv.unified.Hunks {
+		beforeShownLines, afterShownLines := dv.hunkShownLines(i)
+
+		if dv.lineNumbers {
+			b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", beforeNumDigits)))
+			b.WriteString(dv.style.DividerLine.LineNumber.Render(pad("…", afterNumDigits)))
+		}
+		b.WriteString(dv.style.DividerLine.Code.Width(codeWidth + leadingSymbolsSize).Render(
+			fmt.Sprintf(
+				"  @@ -%d,%d +%d,%d @@",
+				h.FromLine,
+				beforeShownLines,
+				h.ToLine,
+				afterShownLines,
+			),
+		))
+		b.WriteRune('\n')
+
 		beforeLine := h.FromLine
 		afterLine := h.ToLine
 
-		beforeNumDigits := lineNumberWidth(h.FromLine, len(h.Lines))
-		afterNumDigits := lineNumberWidth(h.ToLine, len(h.Lines))
-
 		for _, l := range h.Lines {
 			content := strings.TrimSuffix(l.Content, "\n")
-			codeWidth := dv.width - leadingSymbolsSize
 
 			switch l.Kind {
 			case udiff.Equal:
@@ -194,6 +207,33 @@ func (dv *DiffView) computeDiff() error {
 	return dv.err
 }
 
+// lineNumberDigits calculates the maximum number of digits needed for before and
+// after line numbers.
+func (dv *DiffView) lineNumberDigits() (maxBefore, maxAfter int) {
+	for _, h := range dv.unified.Hunks {
+		maxBefore = max(maxBefore, len(strconv.Itoa(h.FromLine+len(h.Lines))))
+		maxAfter = max(maxAfter, len(strconv.Itoa(h.ToLine+len(h.Lines))))
+	}
+	return
+}
+
+// hunkShownLines calculates the number of lines shown in a hunk for both before
+// and after versions.
+func (dv *DiffView) hunkShownLines(i int) (before, after int) {
+	for _, l := range dv.unified.Hunks[i].Lines {
+		switch l.Kind {
+		case udiff.Equal:
+			before++
+			after++
+		case udiff.Insert:
+			after++
+		case udiff.Delete:
+			before++
+		}
+	}
+	return
+}
+
 func (dv *DiffView) detectWidth() {
 	if dv.width > 0 {
 		return

internal/exp/diffview/diffview_test.go šŸ”—

@@ -14,6 +14,12 @@ var TestDefaultBefore string
 //go:embed testdata/TestDefault.after
 var TestDefaultAfter string
 
+//go:embed testdata/TestMultipleHunks.before
+var TestMultipleHunksBefore string
+
+//go:embed testdata/TestMultipleHunks.after
+var TestMultipleHunksAfter string
+
 func TestDefault(t *testing.T) {
 	dv := diffview.New().
 		Before("main.go", TestDefaultBefore).
@@ -46,3 +52,19 @@ func TestNoLineNumbers(t *testing.T) {
 		golden.RequireEqual(t, []byte(dv.String()))
 	})
 }
+
+func TestMultipleHunks(t *testing.T) {
+	dv := diffview.New().
+		Before("main.go", TestMultipleHunksBefore).
+		After("main.go", TestMultipleHunksAfter)
+
+	t.Run("LightMode", func(t *testing.T) {
+		dv = dv.Style(diffview.DefaultLightStyle)
+		golden.RequireEqual(t, []byte(dv.String()))
+	})
+
+	t.Run("DarkMode", func(t *testing.T) {
+		dv = dv.Style(diffview.DefaultDarkStyle)
+		golden.RequireEqual(t, []byte(dv.String()))
+	})
+}

internal/exp/diffview/style.go šŸ”—

@@ -12,12 +12,23 @@ type LineStyle struct {
 }
 
 type Style struct {
-	EqualLine  LineStyle
-	InsertLine LineStyle
-	DeleteLine LineStyle
+	DividerLine LineStyle
+	EqualLine   LineStyle
+	InsertLine  LineStyle
+	DeleteLine  LineStyle
 }
 
 var DefaultLightStyle = Style{
+	DividerLine: LineStyle{
+		LineNumber: lipgloss.NewStyle().
+			Foreground(charmtone.Iron).
+			Background(charmtone.Thunder).
+			Align(lipgloss.Right).
+			Padding(0, 1),
+		Code: lipgloss.NewStyle().
+			Foreground(charmtone.Oyster).
+			Background(charmtone.Anchovy),
+	},
 	EqualLine: LineStyle{
 		LineNumber: lipgloss.NewStyle().
 			Foreground(charmtone.Charcoal).
@@ -57,6 +68,16 @@ var DefaultLightStyle = Style{
 }
 
 var DefaultDarkStyle = Style{
+	DividerLine: LineStyle{
+		LineNumber: lipgloss.NewStyle().
+			Foreground(charmtone.Smoke).
+			Background(charmtone.Sapphire).
+			Align(lipgloss.Right).
+			Padding(0, 1),
+		Code: lipgloss.NewStyle().
+			Foreground(charmtone.Smoke).
+			Background(charmtone.Ox),
+	},
 	EqualLine: LineStyle{
 		LineNumber: lipgloss.NewStyle().
 			Foreground(charmtone.Ash).

internal/exp/diffview/testdata/TestDefault/DarkMode.golden šŸ”—

@@ -1,3 +1,4 @@
+Ā  …Ā Ā  …Ā   @@ -5,5 +5,6 @@                 
 Ā  5Ā Ā  5Ā   )                               
 Ā  6Ā Ā  6Ā                                   
 Ā  7Ā Ā  7Ā   func main() {                   

internal/exp/diffview/testdata/TestDefault/LightMode.golden šŸ”—

@@ -1,3 +1,4 @@
+Ā  …Ā Ā  …Ā   @@ -5,5 +5,6 @@                 
 Ā  5Ā Ā  5Ā   )                               
 Ā  6Ā Ā  6Ā                                   
 Ā  7Ā Ā  7Ā   func main() {                   

internal/exp/diffview/testdata/TestMultipleHunks/DarkMode.golden šŸ”—

@@ -0,0 +1,16 @@
+Ā  …Ā Ā  …Ā   @@ -2,6 +2,7 @@                                
+Ā  2Ā Ā  2Ā                                                  
+Ā  3Ā Ā  3Ā   import (                                       
+Ā  4Ā Ā  4Ā       "fmt"                                      
+Ā   Ā Ā  5Ā +     "strings"                                  
+Ā  5Ā Ā  6Ā   )                                              
+Ā  6Ā Ā  7Ā                                                  
+Ā  7Ā Ā  8Ā   func main() {                                  
+Ā  …Ā Ā  …Ā   @@ -9,5 +10,6 @@                               
+Ā  9Ā Ā 10Ā   }                                              
+Ā 10Ā Ā 11Ā                                                  
+Ā 11Ā Ā 12Ā   func getContent() string {                     
+Ā 12Ā Ā   Ā -     return "Hello, world!"                     
+Ā   Ā Ā 13Ā +     content := strings.ToUpper("Hello, World!")
+Ā   Ā Ā 14Ā +     return content                             
+Ā 13Ā Ā 15Ā   }                                              

internal/exp/diffview/testdata/TestMultipleHunks/LightMode.golden šŸ”—

@@ -0,0 +1,16 @@
+Ā  …Ā Ā  …Ā   @@ -2,6 +2,7 @@                                
+Ā  2Ā Ā  2Ā                                                  
+Ā  3Ā Ā  3Ā   import (                                       
+Ā  4Ā Ā  4Ā       "fmt"                                      
+Ā   Ā Ā  5Ā +     "strings"                                  
+Ā  5Ā Ā  6Ā   )                                              
+Ā  6Ā Ā  7Ā                                                  
+Ā  7Ā Ā  8Ā   func main() {                                  
+Ā  …Ā Ā  …Ā   @@ -9,5 +10,6 @@                               
+Ā  9Ā Ā 10Ā   }                                              
+Ā 10Ā Ā 11Ā                                                  
+Ā 11Ā Ā 12Ā   func getContent() string {                     
+Ā 12Ā Ā   Ā -     return "Hello, world!"                     
+Ā   Ā Ā 13Ā +     content := strings.ToUpper("Hello, World!")
+Ā   Ā Ā 14Ā +     return content                             
+Ā 13Ā Ā 15Ā   }                                              

internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden šŸ”—

@@ -1,3 +1,4 @@
+  @@ -5,5 +5,6 @@                 
   )                               
                                   
   func main() {