on main help sign in
feat(diffview): show line numbers on the left
ab0c4ffAndrey Nering
internal/exp/diffview/diffview.go+42−67
@@ -2,13 +2,13 @@ package diffview
2
2
3import (
3import (
4 "os"
4 "os"
5+ "strconv"
5 "strings"
6 "strings"
6
7
7 "github.com/aymanbagabas/go-udiff"
8 "github.com/aymanbagabas/go-udiff"
8 "github.com/aymanbagabas/go-udiff/myers"
9 "github.com/aymanbagabas/go-udiff/myers"
9 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/charmbracelet/x/ansi"
11 "github.com/charmbracelet/x/ansi"
11− "github.com/charmbracelet/x/exp/charmtone"
12)
12)
13
13
14const leadingSymbolsSize = 2
14const leadingSymbolsSize = 2
@@ -25,71 +25,13 @@ const (
25 layoutSplit
25 layoutSplit
26)
26)
27
27
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−
87// DiffView represents a view for displaying differences between two files.
28// DiffView represents a view for displaying differences between two files.
88type DiffView struct {
29type DiffView struct {
89 layout layout
30 layout layout
90 before file
31 before file
91 after file
32 after file
92 contextLines int
33 contextLines int
34+ lineNumbers bool
93 highlight bool
35 highlight bool
94 height int
36 height int
95 width int
37 width int
@@ -106,6 +48,7 @@ func New() *DiffView {
106 dv := &DiffView{
48 dv := &DiffView{
107 layout: layoutUnified,
49 layout: layoutUnified,
108 contextLines: udiff.DefaultContextLines,
50 contextLines: udiff.DefaultContextLines,
51+ lineNumbers: true,
109 }
52 }
110 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
53 if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
111 dv.style = DefaultDarkStyle
54 dv.style = DefaultDarkStyle
@@ -151,8 +94,14 @@ func (dv *DiffView) Style(style Style) *DiffView {
151 return dv
94 return dv
152}
95}
153
96
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+
154−// SyntaxHighlight sets whether to enable syntax highlighting in the DiffView.
103+// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
155−func (dv *DiffView) SyntaxHighlight(highlight bool) *DiffView {
104+func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
156 dv.highlight = highlight
105 dv.highlight = highlight
157 return dv
106 return dv
158}
107}
@@ -176,22 +125,48 @@ func (dv *DiffView) String() string {
176 }
125 }
177 dv.detectWidth()
126 dv.detectWidth()
178
127
128+ lineNumberWidth := func(start, num int) int {
129+ return len(strconv.Itoa(start + num))
130+ }
131+
179 var b strings.Builder
132 var b strings.Builder
180
133
181 for _, h := range dv.unified.Hunks {
134 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+
182 for _, l := range h.Lines {
141 for _, l := range h.Lines {
183 content := strings.TrimSuffix(l.Content, "\n")
142 content := strings.TrimSuffix(l.Content, "\n")
184− width := dv.width - leadingSymbolsSize
143+ codeWidth := dv.width - leadingSymbolsSize
185
144
186 switch l.Kind {
145 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++
187 case udiff.Insert:
154 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+ }
188 b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
159 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++
190 case udiff.Delete:
162 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+ }
191 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
167 b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
192− b.WriteString(dv.style.DeleteLine.Code.Width(width).Render(content))
168+ b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
193− case udiff.Equal:
169+ beforeLine++
194− b.WriteString(dv.style.EqualLine.Code.Width(width + leadingSymbolsSize).Render(" " + content))
195 }
170 }
196 b.WriteRune('\n')
171 b.WriteRune('\n')
197 }
172 }
internal/exp/diffview/diffview_test.go+17
@@ -29,3 +29,20 @@ func TestDefault(t *testing.T) {
29 golden.RequireEqual(t, []byte(dv.String()))
29 golden.RequireEqual(t, []byte(dv.String()))
30 })
30 })
31}
31}
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− ) 
1+  5   5  ) 
2−  
2+  6   6   
3− func main() { 
3+  7   7  func main() { 
4−-  fmt.Println("Hello, world!")
4+  8    -  fmt.Println("Hello, world!")
5−+  content := "Hello, world!" 
5+     8 +  content := "Hello, world!" 
6−+  fmt.Println(content) 
6+     9 +  fmt.Println(content) 
7− } 
7+  9  10  } 
internal/exp/diffview/testdata/TestDefault/LightMode.golden+7−7
@@ -1,7 +1,7 @@
1− ) 
1+  5   5  ) 
2−  
2+  6   6   
3− func main() { 
3+  7   7  func main() { 
4−-  fmt.Println("Hello, world!")
4+  8    -  fmt.Println("Hello, world!")
5−+  content := "Hello, world!" 
5+     8 +  content := "Hello, world!" 
6−+  fmt.Println(content) 
6+     9 +  fmt.Println(content) 
7− } 
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+}