on main help sign in
feat(diffview): show line numbers on the left
commitab0c4ff7d016ffa1110a7ee13a4970d9a179dcd7 authorAndrey Nering <andreynering@users.noreply.github.com> dateWed 4 Jun 2025, 17:43 (1 year ago) parentf8fc11e
8 files changed+197−81
internal/exp/diffview/diffview.go+42−67 internal/exp/diffview/diffview_test.go+17 internal/exp/diffview/style.go+96 internal/exp/diffview/testdata/TestDefault/DarkMode.golden+7−7 internal/exp/diffview/testdata/TestDefault/LightMode.golden+7−7 internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.golden+7 internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden+7 internal/exp/diffview/util.go+14
internal/exp/diffview/diffview.go+42−67
@@ -2,13 +2,13 @@ package diffview
22
33import (
44 "os"
5+ "strconv"
56 "strings"
67
78 "github.com/aymanbagabas/go-udiff"
89 "github.com/aymanbagabas/go-udiff/myers"
910 "github.com/charmbracelet/lipgloss/v2"
1011 "github.com/charmbracelet/x/ansi"
11− "github.com/charmbracelet/x/exp/charmtone"
1212)
1313
1414const leadingSymbolsSize = 2
@@ -25,71 +25,13 @@ const (
2525 layoutSplit
2626)
2727
28−type LineStyle struct {
29− Symbol lipgloss.Style
30− Code lipgloss.Style
31−}
32−
33−type Style struct {
34− EqualLine LineStyle
35− InsertLine LineStyle
36− DeleteLine LineStyle
37−}
38−
39−var DefaultLightStyle = Style{
40− EqualLine: LineStyle{
41− Code: lipgloss.NewStyle().
42− Foreground(charmtone.Pepper).
43− Background(charmtone.Salt),
44− },
45− InsertLine: LineStyle{
46− Symbol: lipgloss.NewStyle().
47− Foreground(charmtone.Turtle).
48− Background(lipgloss.Color("#e8f5e9")),
49− Code: lipgloss.NewStyle().
50− Foreground(charmtone.Pepper).
51− Background(lipgloss.Color("#e8f5e9")),
52− },
53− DeleteLine: LineStyle{
54− Symbol: lipgloss.NewStyle().
55− Foreground(charmtone.Cherry).
56− Background(lipgloss.Color("#ffebee")),
57− Code: lipgloss.NewStyle().
58− Foreground(charmtone.Pepper).
59− Background(lipgloss.Color("#ffebee")),
60− },
61−}
62−
63−var DefaultDarkStyle = Style{
64− EqualLine: LineStyle{
65− Code: lipgloss.NewStyle().
66− Foreground(charmtone.Salt).
67− Background(charmtone.Pepper),
68− },
69− InsertLine: LineStyle{
70− Symbol: lipgloss.NewStyle().
71− Foreground(charmtone.Turtle).
72− Background(lipgloss.Color("#303a30")),
73− Code: lipgloss.NewStyle().
74− Foreground(charmtone.Salt).
75− Background(lipgloss.Color("#303a30")),
76− },
77− DeleteLine: LineStyle{
78− Symbol: lipgloss.NewStyle().
79− Foreground(charmtone.Cherry).
80− Background(lipgloss.Color("#3a3030")),
81− Code: lipgloss.NewStyle().
82− Foreground(charmtone.Salt).
83− Background(lipgloss.Color("#3a3030")),
84− },
85−}
86−
8728// DiffView represents a view for displaying differences between two files.
8829type DiffView struct {
8930 layout layout
9031 before file
9132 after file
9233 contextLines int
34+ lineNumbers bool
9335 highlight bool
9436 height int
9537 width int
@@ -106,6 +48,7 @@ func New() *DiffView {
10648 dv := &DiffView{
10749 layout: layoutUnified,
10850 contextLines: udiff.DefaultContextLines,
51+ lineNumbers: true,
10952 }
11053 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
11154 dv.style = DefaultDarkStyle
@@ -151,8 +94,14 @@ func (dv *DiffView) Style(style Style) *DiffView {
15194 return dv
15295}
15396
154−// SyntaxHighlight sets whether to enable syntax highlighting in the DiffView.
155−func (dv *DiffView) SyntaxHighlight(highlight bool) *DiffView {
97+// LineNumbers sets whether to display line numbers in the DiffView.
98+func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
99+ dv.lineNumbers = lineNumbers
100+ return dv
101+}
102+
103+// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
104+func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
156105 dv.highlight = highlight
157106 return dv
158107}
@@ -176,22 +125,48 @@ func (dv *DiffView) String() string {
176125 }
177126 dv.detectWidth()
178127
128+ lineNumberWidth := func(start, num int) int {
129+ return len(strconv.Itoa(start + num))
130+ }
131+
179132 var b strings.Builder
180133
181134 for _, h := range dv.unified.Hunks {
135+ beforeLine := h.FromLine
136+ afterLine := h.ToLine
137+
138+ beforeNumDigits := lineNumberWidth(h.FromLine, len(h.Lines))
139+ afterNumDigits := lineNumberWidth(h.ToLine, len(h.Lines))
140+
182141 for _, l := range h.Lines {
183142 content := strings.TrimSuffix(l.Content, "\n")
184− width := dv.width - leadingSymbolsSize
143+ codeWidth := dv.width - leadingSymbolsSize
185144
186145 switch l.Kind {
146+ case udiff.Equal:
147+ if dv.lineNumbers {
148+ b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
149+ b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
150+ }
151+ b.WriteString(dv.style.EqualLine.Code.Width(codeWidth + leadingSymbolsSize).Render(" " + content))
152+ beforeLine++
153+ afterLine++
187154 case udiff.Insert:
155+ if dv.lineNumbers {
156+ b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
157+ b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
158+ }
188159 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
189− b.WriteString(dv.style.InsertLine.Code.Width(width).Render(content))
160+ b.WriteString(dv.style.InsertLine.Code.Width(codeWidth).Render(content))
161+ afterLine++
190162 case udiff.Delete:
163+ if dv.lineNumbers {
164+ b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
165+ b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
166+ }
191167 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
192− b.WriteString(dv.style.DeleteLine.Code.Width(width).Render(content))
193− case udiff.Equal:
194− b.WriteString(dv.style.EqualLine.Code.Width(width + leadingSymbolsSize).Render(" " + content))
168+ b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
169+ beforeLine++
195170 }
196171 b.WriteRune('\n')
197172 }
internal/exp/diffview/diffview_test.go+17
@@ -29,3 +29,20 @@ func TestDefault(t *testing.T) {
2929 golden.RequireEqual(t, []byte(dv.String()))
3030 })
3131}
32+
33+func TestNoLineNumbers(t *testing.T) {
34+ dv := diffview.New().
35+ Before("main.go", TestDefaultBefore).
36+ After("main.go", TestDefaultAfter).
37+ LineNumbers(false)
38+
39+ t.Run("LightMode", func(t *testing.T) {
40+ dv = dv.Style(diffview.DefaultLightStyle)
41+ golden.RequireEqual(t, []byte(dv.String()))
42+ })
43+
44+ t.Run("DarkMode", func(t *testing.T) {
45+ dv = dv.Style(diffview.DefaultDarkStyle)
46+ golden.RequireEqual(t, []byte(dv.String()))
47+ })
48+}
internal/exp/diffview/style.gonew file+96
@@ -0,0 +1,96 @@
1+package diffview
2+
3+import (
4+ "github.com/charmbracelet/lipgloss/v2"
5+ "github.com/charmbracelet/x/exp/charmtone"
6+)
7+
8+type LineStyle struct {
9+ LineNumber lipgloss.Style
10+ Symbol lipgloss.Style
11+ Code lipgloss.Style
12+}
13+
14+type Style struct {
15+ EqualLine LineStyle
16+ InsertLine LineStyle
17+ DeleteLine LineStyle
18+}
19+
20+var DefaultLightStyle = Style{
21+ EqualLine: LineStyle{
22+ LineNumber: lipgloss.NewStyle().
23+ Foreground(charmtone.Charcoal).
24+ Background(charmtone.Ash).
25+ Align(lipgloss.Right).
26+ Padding(0, 1),
27+ Code: lipgloss.NewStyle().
28+ Foreground(charmtone.Pepper).
29+ Background(charmtone.Salt),
30+ },
31+ InsertLine: LineStyle{
32+ LineNumber: lipgloss.NewStyle().
33+ Foreground(charmtone.Turtle).
34+ Background(lipgloss.Color("#c8e6c9")).
35+ Align(lipgloss.Right).
36+ Padding(0, 1),
37+ Symbol: lipgloss.NewStyle().
38+ Foreground(charmtone.Turtle).
39+ Background(lipgloss.Color("#e8f5e9")),
40+ Code: lipgloss.NewStyle().
41+ Foreground(charmtone.Pepper).
42+ Background(lipgloss.Color("#e8f5e9")),
43+ },
44+ DeleteLine: LineStyle{
45+ LineNumber: lipgloss.NewStyle().
46+ Foreground(charmtone.Cherry).
47+ Background(lipgloss.Color("#ffcdd2")).
48+ Align(lipgloss.Left).
49+ Padding(0, 1),
50+ Symbol: lipgloss.NewStyle().
51+ Foreground(charmtone.Cherry).
52+ Background(lipgloss.Color("#ffebee")),
53+ Code: lipgloss.NewStyle().
54+ Foreground(charmtone.Pepper).
55+ Background(lipgloss.Color("#ffebee")),
56+ },
57+}
58+
59+var DefaultDarkStyle = Style{
60+ EqualLine: LineStyle{
61+ LineNumber: lipgloss.NewStyle().
62+ Foreground(charmtone.Ash).
63+ Background(charmtone.Charcoal).
64+ Align(lipgloss.Right).
65+ Padding(0, 1),
66+ Code: lipgloss.NewStyle().
67+ Foreground(charmtone.Salt).
68+ Background(charmtone.Pepper),
69+ },
70+ InsertLine: LineStyle{
71+ LineNumber: lipgloss.NewStyle().
72+ Foreground(charmtone.Turtle).
73+ Background(lipgloss.Color("#293229")).
74+ Align(lipgloss.Right).
75+ Padding(0, 1),
76+ Symbol: lipgloss.NewStyle().
77+ Foreground(charmtone.Turtle).
78+ Background(lipgloss.Color("#303a30")),
79+ Code: lipgloss.NewStyle().
80+ Foreground(charmtone.Salt).
81+ Background(lipgloss.Color("#303a30")),
82+ },
83+ DeleteLine: LineStyle{
84+ LineNumber: lipgloss.NewStyle().
85+ Foreground(charmtone.Cherry).
86+ Background(lipgloss.Color("#332929")).
87+ Align(lipgloss.Left).
88+ Padding(0, 1),
89+ Symbol: lipgloss.NewStyle().
90+ Foreground(charmtone.Cherry).
91+ Background(lipgloss.Color("#3a3030")),
92+ Code: lipgloss.NewStyle().
93+ Foreground(charmtone.Salt).
94+ Background(lipgloss.Color("#3a3030")),
95+ },
96+}
internal/exp/diffview/testdata/TestDefault/DarkMode.golden+7−7
@@ -1,7 +1,7 @@
1− ) 
2−  
3− func main() { 
4−-  fmt.Println("Hello, world!")
5−+  content := "Hello, world!" 
6−+  fmt.Println(content) 
7− } 
1+  5   5  ) 
2+  6   6   
3+  7   7  func main() { 
4+  8    -  fmt.Println("Hello, world!")
5+     8 +  content := "Hello, world!" 
6+     9 +  fmt.Println(content) 
7+  9  10  } 
internal/exp/diffview/testdata/TestDefault/LightMode.golden+7−7
@@ -1,7 +1,7 @@
1− ) 
2−  
3− func main() { 
4−-  fmt.Println("Hello, world!")
5−+  content := "Hello, world!" 
6−+  fmt.Println(content) 
7− } 
1+  5   5  ) 
2+  6   6   
3+  7   7  func main() { 
4+  8    -  fmt.Println("Hello, world!")
5+     8 +  content := "Hello, world!" 
6+     9 +  fmt.Println(content) 
7+  9  10  } 
internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ ) 
2+  
3+ func main() { 
4+-  fmt.Println("Hello, world!")
5++  content := "Hello, world!" 
6++  fmt.Println(content) 
7+ } 
internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ ) 
2+  
3+ func main() { 
4+-  fmt.Println("Hello, world!")
5++  content := "Hello, world!" 
6++  fmt.Println(content) 
7+ } 
internal/exp/diffview/util.gonew file+14
@@ -0,0 +1,14 @@
1+package diffview
2+
3+import (
4+ "fmt"
5+ "strings"
6+)
7+
8+func pad(v any, width int) string {
9+ s := fmt.Sprintf("%v", v)
10+ if len(s) >= width {
11+ return s
12+ }
13+ return strings.Repeat(" ", width-len(s)) + s
14+}