diff --git a/internal/exp/diffview/diffview.go b/internal/exp/diffview/diffview.go index 40481539fdb0b233242dddd4990775abdacd982b..2f3b1e7927adbbfd489d2fa8bff8ada82d55a9bb 100644 --- a/internal/exp/diffview/diffview.go +++ b/internal/exp/diffview/diffview.go @@ -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') } diff --git a/internal/exp/diffview/diffview_test.go b/internal/exp/diffview/diffview_test.go index bd70048a4c81ce260d2d569a3ee1e8fe8aa10e19..982bad099ff29ca912a78fcb9068ada771f25e15 100644 --- a/internal/exp/diffview/diffview_test.go +++ b/internal/exp/diffview/diffview_test.go @@ -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())) + }) +} diff --git a/internal/exp/diffview/style.go b/internal/exp/diffview/style.go new file mode 100644 index 0000000000000000000000000000000000000000..13b025ea4b14b2d7bdee4f01870d18273afa5867 --- /dev/null +++ b/internal/exp/diffview/style.go @@ -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")), + }, +} diff --git a/internal/exp/diffview/testdata/TestDefault/DarkMode.golden b/internal/exp/diffview/testdata/TestDefault/DarkMode.golden index 1cefd40f62dbe898b5d215a7d9b3e88fea8cb477..c7449b30c8ab6d9c7b38c7c8babb8a551fbf159c 100644 --- a/internal/exp/diffview/testdata/TestDefault/DarkMode.golden +++ b/internal/exp/diffview/testdata/TestDefault/DarkMode.golden @@ -1,7 +1,7 @@ - )  -   - func main() {  --  fmt.Println("Hello, world!") -+  content := "Hello, world!"  -+  fmt.Println(content)  - }  +  5   5  )  +  6   6    +  7   7  func main() {  +  8    -  fmt.Println("Hello, world!") +     8 +  content := "Hello, world!"  +     9 +  fmt.Println(content)  +  9  10  }  diff --git a/internal/exp/diffview/testdata/TestDefault/LightMode.golden b/internal/exp/diffview/testdata/TestDefault/LightMode.golden index dffbc6071dea4f70e5534473aefdb02af7f0a389..31548d7db1806ccab600d85428259b0d4050d500 100644 --- a/internal/exp/diffview/testdata/TestDefault/LightMode.golden +++ b/internal/exp/diffview/testdata/TestDefault/LightMode.golden @@ -1,7 +1,7 @@ - )  -   - func main() {  --  fmt.Println("Hello, world!") -+  content := "Hello, world!"  -+  fmt.Println(content)  - }  +  5   5  )  +  6   6    +  7   7  func main() {  +  8    -  fmt.Println("Hello, world!") +     8 +  content := "Hello, world!"  +     9 +  fmt.Println(content)  +  9  10  }  diff --git a/internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.golden b/internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.golden new file mode 100644 index 0000000000000000000000000000000000000000..1cefd40f62dbe898b5d215a7d9b3e88fea8cb477 --- /dev/null +++ b/internal/exp/diffview/testdata/TestNoLineNumbers/DarkMode.golden @@ -0,0 +1,7 @@ + )  +   + func main() {  +-  fmt.Println("Hello, world!") ++  content := "Hello, world!"  ++  fmt.Println(content)  + }  diff --git a/internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden b/internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden new file mode 100644 index 0000000000000000000000000000000000000000..dffbc6071dea4f70e5534473aefdb02af7f0a389 --- /dev/null +++ b/internal/exp/diffview/testdata/TestNoLineNumbers/LightMode.golden @@ -0,0 +1,7 @@ + )  +   + func main() {  +-  fmt.Println("Hello, world!") ++  content := "Hello, world!"  ++  fmt.Println(content)  + }  diff --git a/internal/exp/diffview/util.go b/internal/exp/diffview/util.go new file mode 100644 index 0000000000000000000000000000000000000000..6cf260aa9c654a162fd1e41d64e832c2cacb3320 --- /dev/null +++ b/internal/exp/diffview/util.go @@ -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 +}