Detailed changes
@@ -2,13 +2,13 @@ package diffview
import (
"os"
+ "strconv"
"strings"
"github.com/aymanbagabas/go-udiff"
"github.com/aymanbagabas/go-udiff/myers"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
- "github.com/charmbracelet/x/exp/charmtone"
)
const leadingSymbolsSize = 2
@@ -25,71 +25,13 @@ const (
layoutSplit
)
-type LineStyle struct {
- Symbol lipgloss.Style
- Code lipgloss.Style
-}
-
-type Style struct {
- EqualLine LineStyle
- InsertLine LineStyle
- DeleteLine LineStyle
-}
-
-var DefaultLightStyle = Style{
- EqualLine: LineStyle{
- Code: lipgloss.NewStyle().
- Foreground(charmtone.Pepper).
- Background(charmtone.Salt),
- },
- InsertLine: LineStyle{
- Symbol: lipgloss.NewStyle().
- Foreground(charmtone.Turtle).
- Background(lipgloss.Color("#e8f5e9")),
- Code: lipgloss.NewStyle().
- Foreground(charmtone.Pepper).
- Background(lipgloss.Color("#e8f5e9")),
- },
- DeleteLine: LineStyle{
- Symbol: lipgloss.NewStyle().
- Foreground(charmtone.Cherry).
- Background(lipgloss.Color("#ffebee")),
- Code: lipgloss.NewStyle().
- Foreground(charmtone.Pepper).
- Background(lipgloss.Color("#ffebee")),
- },
-}
-
-var DefaultDarkStyle = Style{
- EqualLine: LineStyle{
- Code: lipgloss.NewStyle().
- Foreground(charmtone.Salt).
- Background(charmtone.Pepper),
- },
- InsertLine: LineStyle{
- Symbol: lipgloss.NewStyle().
- Foreground(charmtone.Turtle).
- Background(lipgloss.Color("#303a30")),
- Code: lipgloss.NewStyle().
- Foreground(charmtone.Salt).
- Background(lipgloss.Color("#303a30")),
- },
- DeleteLine: LineStyle{
- Symbol: lipgloss.NewStyle().
- Foreground(charmtone.Cherry).
- Background(lipgloss.Color("#3a3030")),
- Code: lipgloss.NewStyle().
- Foreground(charmtone.Salt).
- Background(lipgloss.Color("#3a3030")),
- },
-}
-
// DiffView represents a view for displaying differences between two files.
type DiffView struct {
layout layout
before file
after file
contextLines int
+ lineNumbers bool
highlight bool
height int
width int
@@ -106,6 +48,7 @@ func New() *DiffView {
dv := &DiffView{
layout: layoutUnified,
contextLines: udiff.DefaultContextLines,
+ lineNumbers: true,
}
if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
dv.style = DefaultDarkStyle
@@ -151,8 +94,14 @@ func (dv *DiffView) Style(style Style) *DiffView {
return dv
}
-// SyntaxHighlight sets whether to enable syntax highlighting in the DiffView.
-func (dv *DiffView) SyntaxHighlight(highlight bool) *DiffView {
+// LineNumbers sets whether to display line numbers in the DiffView.
+func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
+ dv.lineNumbers = lineNumbers
+ return dv
+}
+
+// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
+func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
dv.highlight = highlight
return dv
}
@@ -176,22 +125,48 @@ func (dv *DiffView) String() string {
}
dv.detectWidth()
+ lineNumberWidth := func(start, num int) int {
+ return len(strconv.Itoa(start + num))
+ }
+
var b strings.Builder
for _, h := range dv.unified.Hunks {
+ 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")
- width := dv.width - leadingSymbolsSize
+ codeWidth := dv.width - leadingSymbolsSize
switch l.Kind {
+ case udiff.Equal:
+ if dv.lineNumbers {
+ b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
+ b.WriteString(dv.style.EqualLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
+ }
+ b.WriteString(dv.style.EqualLine.Code.Width(codeWidth + leadingSymbolsSize).Render(" " + content))
+ beforeLine++
+ afterLine++
case udiff.Insert:
+ if dv.lineNumbers {
+ b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(" ", beforeNumDigits)))
+ b.WriteString(dv.style.InsertLine.LineNumber.Render(pad(afterLine, afterNumDigits)))
+ }
b.WriteString(dv.style.InsertLine.Symbol.Render("+ "))
- b.WriteString(dv.style.InsertLine.Code.Width(width).Render(content))
+ b.WriteString(dv.style.InsertLine.Code.Width(codeWidth).Render(content))
+ afterLine++
case udiff.Delete:
+ if dv.lineNumbers {
+ b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(beforeLine, beforeNumDigits)))
+ b.WriteString(dv.style.DeleteLine.LineNumber.Render(pad(" ", afterNumDigits)))
+ }
b.WriteString(dv.style.DeleteLine.Symbol.Render("- "))
- b.WriteString(dv.style.DeleteLine.Code.Width(width).Render(content))
- case udiff.Equal:
- b.WriteString(dv.style.EqualLine.Code.Width(width + leadingSymbolsSize).Render(" " + content))
+ b.WriteString(dv.style.DeleteLine.Code.Width(codeWidth).Render(content))
+ beforeLine++
}
b.WriteRune('\n')
}
@@ -29,3 +29,20 @@ func TestDefault(t *testing.T) {
golden.RequireEqual(t, []byte(dv.String()))
})
}
+
+func TestNoLineNumbers(t *testing.T) {
+ dv := diffview.New().
+ Before("main.go", TestDefaultBefore).
+ After("main.go", TestDefaultAfter).
+ LineNumbers(false)
+
+ 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()))
+ })
+}
@@ -0,0 +1,96 @@
+package diffview
+
+import (
+ "github.com/charmbracelet/lipgloss/v2"
+ "github.com/charmbracelet/x/exp/charmtone"
+)
+
+type LineStyle struct {
+ LineNumber lipgloss.Style
+ Symbol lipgloss.Style
+ Code lipgloss.Style
+}
+
+type Style struct {
+ EqualLine LineStyle
+ InsertLine LineStyle
+ DeleteLine LineStyle
+}
+
+var DefaultLightStyle = Style{
+ EqualLine: LineStyle{
+ LineNumber: lipgloss.NewStyle().
+ Foreground(charmtone.Charcoal).
+ Background(charmtone.Ash).
+ Align(lipgloss.Right).
+ Padding(0, 1),
+ Code: lipgloss.NewStyle().
+ Foreground(charmtone.Pepper).
+ Background(charmtone.Salt),
+ },
+ InsertLine: LineStyle{
+ LineNumber: lipgloss.NewStyle().
+ Foreground(charmtone.Turtle).
+ Background(lipgloss.Color("#c8e6c9")).
+ Align(lipgloss.Right).
+ Padding(0, 1),
+ Symbol: lipgloss.NewStyle().
+ Foreground(charmtone.Turtle).
+ Background(lipgloss.Color("#e8f5e9")),
+ Code: lipgloss.NewStyle().
+ Foreground(charmtone.Pepper).
+ Background(lipgloss.Color("#e8f5e9")),
+ },
+ DeleteLine: LineStyle{
+ LineNumber: lipgloss.NewStyle().
+ Foreground(charmtone.Cherry).
+ Background(lipgloss.Color("#ffcdd2")).
+ Align(lipgloss.Left).
+ Padding(0, 1),
+ Symbol: lipgloss.NewStyle().
+ Foreground(charmtone.Cherry).
+ Background(lipgloss.Color("#ffebee")),
+ Code: lipgloss.NewStyle().
+ Foreground(charmtone.Pepper).
+ Background(lipgloss.Color("#ffebee")),
+ },
+}
+
+var DefaultDarkStyle = Style{
+ EqualLine: LineStyle{
+ LineNumber: lipgloss.NewStyle().
+ Foreground(charmtone.Ash).
+ Background(charmtone.Charcoal).
+ Align(lipgloss.Right).
+ Padding(0, 1),
+ Code: lipgloss.NewStyle().
+ Foreground(charmtone.Salt).
+ Background(charmtone.Pepper),
+ },
+ InsertLine: LineStyle{
+ LineNumber: lipgloss.NewStyle().
+ Foreground(charmtone.Turtle).
+ Background(lipgloss.Color("#293229")).
+ Align(lipgloss.Right).
+ Padding(0, 1),
+ Symbol: lipgloss.NewStyle().
+ Foreground(charmtone.Turtle).
+ Background(lipgloss.Color("#303a30")),
+ Code: lipgloss.NewStyle().
+ Foreground(charmtone.Salt).
+ Background(lipgloss.Color("#303a30")),
+ },
+ DeleteLine: LineStyle{
+ LineNumber: lipgloss.NewStyle().
+ Foreground(charmtone.Cherry).
+ Background(lipgloss.Color("#332929")).
+ Align(lipgloss.Left).
+ Padding(0, 1),
+ Symbol: lipgloss.NewStyle().
+ Foreground(charmtone.Cherry).
+ Background(lipgloss.Color("#3a3030")),
+ Code: lipgloss.NewStyle().
+ Foreground(charmtone.Salt).
+ Background(lipgloss.Color("#3a3030")),
+ },
+}
@@ -1,7 +1,7 @@
-[38;2;241;239;239;48;2;32;31;38m )[m[48;2;32;31;38m [m
-[38;2;241;239;239;48;2;32;31;38m [m[48;2;32;31;38m [m
-[38;2;241;239;239;48;2;32;31;38m func main() {[m[48;2;32;31;38m [m
-[38;2;255;56;139;48;2;58;48;48m- [m[38;2;241;239;239;48;2;58;48;48m fmt.Println("Hello, world!")[m
-[38;2;10;220;217;48;2;48;58;48m+ [m[38;2;241;239;239;48;2;48;58;48m content := "Hello, world!"[m[48;2;48;58;48m [m
-[38;2;10;220;217;48;2;48;58;48m+ [m[38;2;241;239;239;48;2;48;58;48m fmt.Println(content)[m[48;2;48;58;48m [m
-[38;2;241;239;239;48;2;32;31;38m }[m[48;2;32;31;38m [m
+[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m 5[m[48;2;58;57;67m [m[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m 5[m[48;2;58;57;67m [m[38;2;241;239;239;48;2;32;31;38m )[m[48;2;32;31;38m [m
+[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m 6[m[48;2;58;57;67m [m[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m 6[m[48;2;58;57;67m [m[38;2;241;239;239;48;2;32;31;38m [m[48;2;32;31;38m [m
+[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m 7[m[48;2;58;57;67m [m[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m 7[m[48;2;58;57;67m [m[38;2;241;239;239;48;2;32;31;38m func main() {[m[48;2;32;31;38m [m
+[48;2;51;41;41m [m[38;2;255;56;139;48;2;51;41;41m 8[m[48;2;51;41;41m [m[48;2;51;41;41m [m[38;2;255;56;139;48;2;51;41;41m [m[48;2;51;41;41m [m[38;2;255;56;139;48;2;58;48;48m- [m[38;2;241;239;239;48;2;58;48;48m fmt.Println("Hello, world!")[m
+[48;2;41;50;41m [m[38;2;10;220;217;48;2;41;50;41m [m[48;2;41;50;41m [m[48;2;41;50;41m [m[38;2;10;220;217;48;2;41;50;41m 8[m[48;2;41;50;41m [m[38;2;10;220;217;48;2;48;58;48m+ [m[38;2;241;239;239;48;2;48;58;48m content := "Hello, world!"[m[48;2;48;58;48m [m
+[48;2;41;50;41m [m[38;2;10;220;217;48;2;41;50;41m [m[48;2;41;50;41m [m[48;2;41;50;41m [m[38;2;10;220;217;48;2;41;50;41m 9[m[48;2;41;50;41m [m[38;2;10;220;217;48;2;48;58;48m+ [m[38;2;241;239;239;48;2;48;58;48m fmt.Println(content)[m[48;2;48;58;48m [m
+[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m 9[m[48;2;58;57;67m [m[48;2;58;57;67m [m[38;2;223;219;221;48;2;58;57;67m10[m[48;2;58;57;67m [m[38;2;241;239;239;48;2;32;31;38m }[m[48;2;32;31;38m [m
@@ -1,7 +1,7 @@
-[38;2;32;31;38;48;2;241;239;239m )[m[48;2;241;239;239m [m
-[38;2;32;31;38;48;2;241;239;239m [m[48;2;241;239;239m [m
-[38;2;32;31;38;48;2;241;239;239m func main() {[m[48;2;241;239;239m [m
-[38;2;255;56;139;48;2;255;235;238m- [m[38;2;32;31;38;48;2;255;235;238m fmt.Println("Hello, world!")[m
-[38;2;10;220;217;48;2;232;245;233m+ [m[38;2;32;31;38;48;2;232;245;233m content := "Hello, world!"[m[48;2;232;245;233m [m
-[38;2;10;220;217;48;2;232;245;233m+ [m[38;2;32;31;38;48;2;232;245;233m fmt.Println(content)[m[48;2;232;245;233m [m
-[38;2;32;31;38;48;2;241;239;239m }[m[48;2;241;239;239m [m
+[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m 5[m[48;2;223;219;221m [m[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m 5[m[48;2;223;219;221m [m[38;2;32;31;38;48;2;241;239;239m )[m[48;2;241;239;239m [m
+[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m 6[m[48;2;223;219;221m [m[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m 6[m[48;2;223;219;221m [m[38;2;32;31;38;48;2;241;239;239m [m[48;2;241;239;239m [m
+[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m 7[m[48;2;223;219;221m [m[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m 7[m[48;2;223;219;221m [m[38;2;32;31;38;48;2;241;239;239m func main() {[m[48;2;241;239;239m [m
+[48;2;255;205;210m [m[38;2;255;56;139;48;2;255;205;210m 8[m[48;2;255;205;210m [m[48;2;255;205;210m [m[38;2;255;56;139;48;2;255;205;210m [m[48;2;255;205;210m [m[38;2;255;56;139;48;2;255;235;238m- [m[38;2;32;31;38;48;2;255;235;238m fmt.Println("Hello, world!")[m
+[48;2;200;230;201m [m[38;2;10;220;217;48;2;200;230;201m [m[48;2;200;230;201m [m[48;2;200;230;201m [m[38;2;10;220;217;48;2;200;230;201m 8[m[48;2;200;230;201m [m[38;2;10;220;217;48;2;232;245;233m+ [m[38;2;32;31;38;48;2;232;245;233m content := "Hello, world!"[m[48;2;232;245;233m [m
+[48;2;200;230;201m [m[38;2;10;220;217;48;2;200;230;201m [m[48;2;200;230;201m [m[48;2;200;230;201m [m[38;2;10;220;217;48;2;200;230;201m 9[m[48;2;200;230;201m [m[38;2;10;220;217;48;2;232;245;233m+ [m[38;2;32;31;38;48;2;232;245;233m fmt.Println(content)[m[48;2;232;245;233m [m
+[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m 9[m[48;2;223;219;221m [m[48;2;223;219;221m [m[38;2;58;57;67;48;2;223;219;221m10[m[48;2;223;219;221m [m[38;2;32;31;38;48;2;241;239;239m }[m[48;2;241;239;239m [m
@@ -0,0 +1,7 @@
+[38;2;241;239;239;48;2;32;31;38m )[m[48;2;32;31;38m [m
+[38;2;241;239;239;48;2;32;31;38m [m[48;2;32;31;38m [m
+[38;2;241;239;239;48;2;32;31;38m func main() {[m[48;2;32;31;38m [m
+[38;2;255;56;139;48;2;58;48;48m- [m[38;2;241;239;239;48;2;58;48;48m fmt.Println("Hello, world!")[m
+[38;2;10;220;217;48;2;48;58;48m+ [m[38;2;241;239;239;48;2;48;58;48m content := "Hello, world!"[m[48;2;48;58;48m [m
+[38;2;10;220;217;48;2;48;58;48m+ [m[38;2;241;239;239;48;2;48;58;48m fmt.Println(content)[m[48;2;48;58;48m [m
+[38;2;241;239;239;48;2;32;31;38m }[m[48;2;32;31;38m [m
@@ -0,0 +1,7 @@
+[38;2;32;31;38;48;2;241;239;239m )[m[48;2;241;239;239m [m
+[38;2;32;31;38;48;2;241;239;239m [m[48;2;241;239;239m [m
+[38;2;32;31;38;48;2;241;239;239m func main() {[m[48;2;241;239;239m [m
+[38;2;255;56;139;48;2;255;235;238m- [m[38;2;32;31;38;48;2;255;235;238m fmt.Println("Hello, world!")[m
+[38;2;10;220;217;48;2;232;245;233m+ [m[38;2;32;31;38;48;2;232;245;233m content := "Hello, world!"[m[48;2;232;245;233m [m
+[38;2;10;220;217;48;2;232;245;233m+ [m[38;2;32;31;38;48;2;232;245;233m fmt.Println(content)[m[48;2;232;245;233m [m
+[38;2;32;31;38;48;2;241;239;239m }[m[48;2;241;239;239m [m
@@ -0,0 +1,14 @@
+package diffview
+
+import (
+ "fmt"
+ "strings"
+)
+
+func pad(v any, width int) string {
+ s := fmt.Sprintf("%v", v)
+ if len(s) >= width {
+ return s
+ }
+ return strings.Repeat(" ", width-len(s)) + s
+}