fix(tui): better message rendering performance
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run

This commit is contained in:
adamdottv 2025-07-01 07:57:31 -05:00
parent d56991006c
commit 33b5fe236a
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
5 changed files with 63 additions and 83 deletions

View file

@ -104,14 +104,15 @@ func (m *messagesComponent) renderView() {
defer measure("messageCount", len(m.app.Messages)) defer measure("messageCount", len(m.app.Messages))
t := theme.CurrentTheme() t := theme.CurrentTheme()
blocks := make([]string, 0)
align := lipgloss.Center align := lipgloss.Center
width := layout.Current.Container.Width width := layout.Current.Container.Width
for _, message := range m.app.Messages { sb := strings.Builder{}
util.WriteStringsPar(&sb, m.app.Messages, func(message opencode.Message) string {
var content string var content string
var cached bool var cached bool
blocks := make([]string, 0)
switch message.Role { switch message.Role {
case opencode.MessageRoleUser: case opencode.MessageRoleUser:
@ -224,7 +225,6 @@ func (m *messagesComponent) renderView() {
} }
} }
} }
} }
error := "" error := ""
@ -247,20 +247,14 @@ func (m *messagesComponent) renderView() {
) )
blocks = append(blocks, error) blocks = append(blocks, error)
} }
}
centered := []string{} return strings.Join(blocks, "\n\n")
for _, block := range blocks { })
centered = append(centered, lipgloss.PlaceHorizontal(
m.width, content := sb.String()
lipgloss.Center,
block+"\n",
styles.WhitespaceStyle(t.Background()),
))
}
m.viewport.SetHeight(m.height - lipgloss.Height(m.header()) + 1) m.viewport.SetHeight(m.height - lipgloss.Height(m.header()) + 1)
m.viewport.SetContent("\n" + strings.Join(centered, "\n")) m.viewport.SetContent("\n" + content)
} }
func (m *messagesComponent) header() string { func (m *messagesComponent) header() string {

View file

@ -1,6 +1,7 @@
package diff package diff
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"image/color" "image/color"
@ -148,101 +149,87 @@ func WithWidth(width int) UnifiedOption {
func ParseUnifiedDiff(diff string) (DiffResult, error) { func ParseUnifiedDiff(diff string) (DiffResult, error) {
var result DiffResult var result DiffResult
var currentHunk *Hunk var currentHunk *Hunk
result.Hunks = make([]Hunk, 0, 10) // Pre-allocate with a reasonable capacity
hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`) scanner := bufio.NewScanner(strings.NewReader(diff))
lines := strings.Split(diff, "\n")
var oldLine, newLine int var oldLine, newLine int
inFileHeader := true inFileHeader := true
for _, line := range lines { for scanner.Scan() {
// Parse file headers line := scanner.Text()
if inFileHeader { if inFileHeader {
if strings.HasPrefix(line, "--- a/") { if strings.HasPrefix(line, "--- a/") {
result.OldFile = strings.TrimPrefix(line, "--- a/") result.OldFile = line[6:]
continue continue
} }
if strings.HasPrefix(line, "+++ b/") { if strings.HasPrefix(line, "+++ b/") {
result.NewFile = strings.TrimPrefix(line, "+++ b/") result.NewFile = line[6:]
inFileHeader = false inFileHeader = false
continue continue
} }
} }
// Parse hunk headers if strings.HasPrefix(line, "@@") {
if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil {
if currentHunk != nil { if currentHunk != nil {
result.Hunks = append(result.Hunks, *currentHunk) result.Hunks = append(result.Hunks, *currentHunk)
} }
currentHunk = &Hunk{ currentHunk = &Hunk{
Header: line, Header: line,
Lines: []DiffLine{}, Lines: make([]DiffLine, 0, 10), // Pre-allocate
} }
oldStart, _ := strconv.Atoi(matches[1]) // Manual parsing of hunk header is faster than regex
newStart, _ := strconv.Atoi(matches[3]) parts := strings.Split(line, " ")
oldLine = oldStart if len(parts) > 2 {
newLine = newStart oldRange := strings.Split(parts[1][1:], ",")
newRange := strings.Split(parts[2][1:], ",")
oldLine, _ = strconv.Atoi(oldRange[0])
newLine, _ = strconv.Atoi(newRange[0])
}
continue continue
} }
// Ignore "No newline at end of file" markers if strings.HasPrefix(line, "\\ No newline at end of file") || currentHunk == nil {
if strings.HasPrefix(line, "\\ No newline at end of file") {
continue continue
} }
if currentHunk == nil { var dl DiffLine
continue dl.Content = line
}
// Process the line based on its prefix
if len(line) > 0 { if len(line) > 0 {
switch line[0] { switch line[0] {
case '+': case '+':
currentHunk.Lines = append(currentHunk.Lines, DiffLine{ dl.Kind = LineAdded
OldLineNo: 0, dl.NewLineNo = newLine
NewLineNo: newLine, dl.Content = line[1:]
Kind: LineAdded,
Content: line[1:],
})
newLine++ newLine++
case '-': case '-':
currentHunk.Lines = append(currentHunk.Lines, DiffLine{ dl.Kind = LineRemoved
OldLineNo: oldLine, dl.OldLineNo = oldLine
NewLineNo: 0, dl.Content = line[1:]
Kind: LineRemoved,
Content: line[1:],
})
oldLine++ oldLine++
default: default: // context line
currentHunk.Lines = append(currentHunk.Lines, DiffLine{ dl.Kind = LineContext
OldLineNo: oldLine, dl.OldLineNo = oldLine
NewLineNo: newLine, dl.NewLineNo = newLine
Kind: LineContext,
Content: line,
})
oldLine++ oldLine++
newLine++ newLine++
} }
} else { } else { // empty context line
// Handle empty lines dl.Kind = LineContext
currentHunk.Lines = append(currentHunk.Lines, DiffLine{ dl.OldLineNo = oldLine
OldLineNo: oldLine, dl.NewLineNo = newLine
NewLineNo: newLine,
Kind: LineContext,
Content: "",
})
oldLine++ oldLine++
newLine++ newLine++
} }
currentHunk.Lines = append(currentHunk.Lines, dl)
} }
// Add the last hunk if there is one
if currentHunk != nil { if currentHunk != nil {
result.Hunks = append(result.Hunks, *currentHunk) result.Hunks = append(result.Hunks, *currentHunk)
} }
return result, nil return result, scanner.Err()
} }
// HighlightIntralineChanges updates lines in a hunk to show character-level differences // HighlightIntralineChanges updates lines in a hunk to show character-level differences
@ -744,8 +731,6 @@ func renderLineContent(fileName string, dl DiffLine, bgStyle stylesi.Style, high
content, content,
width, width,
"...", "...",
// stylesi.NewStyleWithColors(t.TextMuted(), bgStyle.GetBackground()).Render("..."),
// stylesi.WithForeground(stylesi.NewStyle().Background(bgStyle.GetBackground()), t.TextMuted()).Render("..."),
), ),
) )
} }
@ -912,10 +897,11 @@ func RenderUnifiedHunk(fileName string, h Hunk, opts ...UnifiedOption) string {
HighlightIntralineChanges(&hunkCopy) HighlightIntralineChanges(&hunkCopy)
var sb strings.Builder var sb strings.Builder
for _, line := range hunkCopy.Lines { sb.Grow(len(hunkCopy.Lines) * config.Width)
sb.WriteString(renderUnifiedLine(fileName, line, config.Width, theme.CurrentTheme()))
sb.WriteString("\n") util.WriteStringsPar(&sb, hunkCopy.Lines, func(line DiffLine) string {
} return renderUnifiedLine(fileName, line, config.Width, theme.CurrentTheme()) + "\n"
})
return sb.String() return sb.String()
} }
@ -969,32 +955,22 @@ func FormatUnifiedDiff(filename string, diffText string, opts ...UnifiedOption)
} }
var sb strings.Builder var sb strings.Builder
for _, h := range diffResult.Hunks { util.WriteStringsPar(&sb, diffResult.Hunks, func(h Hunk) string {
unifiedDiff := RenderUnifiedHunk(filename, h, opts...) return RenderUnifiedHunk(filename, h, opts...)
sb.WriteString(unifiedDiff) })
}
return sb.String(), nil return sb.String(), nil
} }
// FormatDiff creates a side-by-side formatted view of a diff // FormatDiff creates a side-by-side formatted view of a diff
func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (string, error) { func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (string, error) {
// t := theme.CurrentTheme()
diffResult, err := ParseUnifiedDiff(diffText) diffResult, err := ParseUnifiedDiff(diffText)
if err != nil { if err != nil {
return "", err return "", err
} }
var sb strings.Builder var sb strings.Builder
// config := NewSideBySideConfig(opts...)
util.WriteStringsPar(&sb, diffResult.Hunks, func(h Hunk) string { util.WriteStringsPar(&sb, diffResult.Hunks, func(h Hunk) string {
// sb.WriteString(
// lipgloss.NewStyle().
// Background(t.DiffHunkHeader()).
// Foreground(t.Background()).
// Width(config.TotalWidth).
// Render(h.Header) + "\n",
// )
return RenderSideBySideHunk(filename, h, opts...) return RenderSideBySideHunk(filename, h, opts...)
}) })

View file

@ -27,6 +27,10 @@ type LoadedTheme struct {
name string name string
} }
func (t *LoadedTheme) Name() string {
return t.name
}
type colorRef struct { type colorRef struct {
value any value any
resolved bool resolved bool

View file

@ -27,6 +27,10 @@ func NewSystemTheme(terminalBg color.Color, isDark bool) *SystemTheme {
return theme return theme
} }
func (t *SystemTheme) Name() string {
return "system"
}
// initializeColors sets up all theme colors // initializeColors sets up all theme colors
func (t *SystemTheme) initializeColors() { func (t *SystemTheme) initializeColors() {
// Generate gray scale based on terminal background // Generate gray scale based on terminal background

View file

@ -8,6 +8,8 @@ import (
// All colors must be defined as compat.AdaptiveColor to support // All colors must be defined as compat.AdaptiveColor to support
// both light and dark terminal backgrounds. // both light and dark terminal backgrounds.
type Theme interface { type Theme interface {
Name() string
// Background colors // Background colors
Background() compat.AdaptiveColor // Radix 1 Background() compat.AdaptiveColor // Radix 1
BackgroundPanel() compat.AdaptiveColor // Radix 2 BackgroundPanel() compat.AdaptiveColor // Radix 2