Browse Source

wip: refactoring tui

adamdottv 8 months ago
parent
commit
38667682a7

+ 66 - 23
packages/tui/internal/components/chat/message.go

@@ -55,6 +55,10 @@ type blockRenderer struct {
 	fullWidth     bool
 	paddingTop    int
 	paddingBottom int
+	paddingLeft   int
+	paddingRight  int
+	marginTop     int
+	marginBottom  int
 }
 
 type renderingOption func(*blockRenderer)
@@ -77,6 +81,30 @@ func WithBorderColor(color compat.AdaptiveColor) renderingOption {
 	}
 }
 
+func WithMarginTop(padding int) renderingOption {
+	return func(c *blockRenderer) {
+		c.marginTop = padding
+	}
+}
+
+func WithMarginBottom(padding int) renderingOption {
+	return func(c *blockRenderer) {
+		c.marginBottom = padding
+	}
+}
+
+func WithPaddingLeft(padding int) renderingOption {
+	return func(c *blockRenderer) {
+		c.paddingLeft = padding
+	}
+}
+
+func WithPaddingRight(padding int) renderingOption {
+	return func(c *blockRenderer) {
+		c.paddingRight = padding
+	}
+}
+
 func WithPaddingTop(padding int) renderingOption {
 	return func(c *blockRenderer) {
 		c.paddingTop = padding
@@ -92,17 +120,23 @@ func WithPaddingBottom(padding int) renderingOption {
 func renderContentBlock(content string, options ...renderingOption) string {
 	t := theme.CurrentTheme()
 	renderer := &blockRenderer{
-		fullWidth: false,
+		fullWidth:     false,
+		paddingTop:    1,
+		paddingBottom: 1,
+		paddingLeft:   2,
+		paddingRight:  2,
 	}
 	for _, option := range options {
 		option(renderer)
 	}
 
 	style := styles.BaseStyle().
-		PaddingTop(1).
-		PaddingBottom(1).
-		PaddingLeft(2).
-		PaddingRight(2).
+		MarginTop(renderer.marginTop).
+		MarginBottom(renderer.marginBottom).
+		PaddingTop(renderer.paddingTop).
+		PaddingBottom(renderer.paddingBottom).
+		PaddingLeft(renderer.paddingLeft).
+		PaddingRight(renderer.paddingRight).
 		Background(t.BackgroundSubtle()).
 		Foreground(t.TextMuted()).
 		BorderStyle(lipgloss.ThickBorder())
@@ -142,12 +176,6 @@ func renderContentBlock(content string, options ...renderingOption) string {
 		style = style.Width(layout.Current.Container.Width)
 	}
 	content = style.Render(content)
-	if renderer.paddingTop > 0 {
-		content = strings.Repeat("\n", renderer.paddingTop) + content
-	}
-	if renderer.paddingBottom > 0 {
-		content = content + strings.Repeat("\n", renderer.paddingBottom)
-	}
 	content = lipgloss.PlaceHorizontal(
 		layout.Current.Container.Width,
 		align,
@@ -165,12 +193,11 @@ func renderText(message client.MessageInfo, text string, author string) string {
 	t := theme.CurrentTheme()
 	width := layout.Current.Container.Width
 	padding := 0
-	switch layout.Current.Size {
-	case layout.LayoutSizeSmall:
+	if layout.Current.Viewport.Width < 80 {
 		padding = 5
-	case layout.LayoutSizeNormal:
+	} else if layout.Current.Viewport.Width < 120 {
 		padding = 10
-	case layout.LayoutSizeLarge:
+	} else {
 		padding = 15
 	}
 
@@ -270,7 +297,7 @@ func renderToolInvocation(
 			error = styles.BaseStyle().
 				Foreground(t.Error()).
 				Render(m.(string))
-			error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+			error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
 		}
 	}
 
@@ -301,8 +328,24 @@ func renderToolInvocation(
 		title = fmt.Sprintf("Edit: %s   %s", relative(filename), elapsed)
 		if d, ok := metadata.Get("diff"); ok {
 			patch := d.(string)
-			diffWidth := min(layout.Current.Viewport.Width, 120)
-			formattedDiff, _ := diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
+			var formattedDiff string
+			if layout.Current.Viewport.Width < 80 {
+				formattedDiff, _ = diff.FormatUnifiedDiff(
+					filename,
+					patch,
+					diff.WithWidth(layout.Current.Container.Width-2),
+				)
+			} else {
+				diffWidth := min(layout.Current.Viewport.Width, 120)
+				formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
+			}
+			formattedDiff = strings.TrimSpace(formattedDiff)
+			formattedDiff = lipgloss.NewStyle().
+				BorderStyle(lipgloss.ThickBorder()).
+				BorderForeground(t.BackgroundSubtle()).
+				BorderLeft(true).
+				BorderRight(true).
+				Render(formattedDiff)
 			body = strings.TrimSpace(formattedDiff)
 			body = lipgloss.Place(
 				layout.Current.Viewport.Width,
@@ -326,7 +369,7 @@ func renderToolInvocation(
 			stdout := stdout.(string)
 			body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
 			body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
-			body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+			body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
 		}
 	case "opencode_webfetch":
 		title = fmt.Sprintf("Fetching: %s   %s", toolArgs, elapsed)
@@ -335,7 +378,7 @@ func renderToolInvocation(
 		if format == "html" || format == "markdown" {
 			body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
 		}
-		body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+		body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
 	case "opencode_todowrite":
 		title = fmt.Sprintf("Planning...   %s", elapsed)
 
@@ -355,13 +398,13 @@ func renderToolInvocation(
 				}
 			}
 			body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
-			body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+			body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
 		}
 	default:
 		toolName := renderToolName(toolCall.ToolName)
 		title = fmt.Sprintf("%s: %s   %s", toolName, toolArgs, elapsed)
 		body = truncateHeight(body, 10)
-		body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+		body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
 	}
 
 	content := style.Render(title)
@@ -435,7 +478,7 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
 	content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
 	content = toMarkdown(content, width, t.BackgroundSubtle())
 
-	return renderContentBlock(content, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+	return renderContentBlock(content, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
 }
 
 func renderToolAction(name string) string {

+ 1 - 1
packages/tui/internal/components/chat/messages.go

@@ -214,7 +214,7 @@ func (m *messagesComponent) renderView() {
 			case client.UnknownError:
 				clientError := errorValue.(client.UnknownError)
 				error = clientError.Data.Message
-				error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+				error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
 				blocks = append(blocks, error)
 				previousBlockType = errorBlock
 			}

+ 177 - 35
packages/tui/internal/components/diff/diff.go

@@ -104,6 +104,40 @@ func WithTotalWidth(width int) SideBySideOption {
 	}
 }
 
+// -------------------------------------------------------------------------
+// Unified Configuration
+// -------------------------------------------------------------------------
+
+// UnifiedConfig configures the rendering of unified diffs
+type UnifiedConfig struct {
+	Width int
+}
+
+// UnifiedOption modifies a UnifiedConfig
+type UnifiedOption func(*UnifiedConfig)
+
+// NewUnifiedConfig creates a UnifiedConfig with default values
+func NewUnifiedConfig(opts ...UnifiedOption) UnifiedConfig {
+	config := UnifiedConfig{
+		Width: 80, // Default width for unified view
+	}
+
+	for _, opt := range opts {
+		opt(&config)
+	}
+
+	return config
+}
+
+// WithWidth sets the width for unified view
+func WithWidth(width int) UnifiedOption {
+	return func(u *UnifiedConfig) {
+		if width > 0 {
+			u.Width = width
+		}
+	}
+}
+
 // -------------------------------------------------------------------------
 // Diff Parsing
 // -------------------------------------------------------------------------
@@ -642,6 +676,101 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
 	return sb.String()
 }
 
+// renderLinePrefix renders the line number and marker prefix for a diff line
+func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle lipgloss.Style, t theme.Theme) string {
+	// Style the marker based on line type
+	var styledMarker string
+	switch dl.Kind {
+	case LineRemoved:
+		styledMarker = lipgloss.NewStyle().Background(t.DiffRemovedBg()).Foreground(t.DiffRemoved()).Render(marker)
+	case LineAdded:
+		styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker)
+	case LineContext:
+		styledMarker = lipgloss.NewStyle().Background(t.DiffContextBg()).Foreground(t.TextMuted()).Render(marker)
+	default:
+		styledMarker = marker
+	}
+
+	return lineNumberStyle.Render(lineNum + " " + styledMarker)
+}
+
+// renderLineContent renders the content of a diff line with syntax and intra-line highlighting
+func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, highlightColor compat.AdaptiveColor, width int, t theme.Theme) string {
+	// Apply syntax highlighting
+	content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
+
+	// Apply intra-line highlighting if needed
+	if len(dl.Segments) > 0 && (dl.Kind == LineRemoved || dl.Kind == LineAdded) {
+		content = applyHighlighting(content, dl.Segments, dl.Kind, highlightColor)
+	}
+
+	// Add a padding space for added/removed lines
+	if dl.Kind == LineRemoved || dl.Kind == LineAdded {
+		content = bgStyle.Render(" ") + content
+	}
+
+	// Create the final line and truncate if needed
+	return bgStyle.MaxHeight(1).Width(width).Render(
+		ansi.Truncate(
+			content,
+			width,
+			lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
+		),
+	)
+}
+
+// renderUnifiedLine renders a single line in unified diff format
+func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) string {
+	removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
+
+	// Determine line style and marker based on line type
+	var marker string
+	var bgStyle lipgloss.Style
+	var lineNum string
+	var highlightColor compat.AdaptiveColor
+
+	switch dl.Kind {
+	case LineRemoved:
+		marker = "-"
+		bgStyle = removedLineStyle
+		lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
+		highlightColor = t.DiffHighlightRemoved()
+		if dl.OldLineNo > 0 {
+			lineNum = fmt.Sprintf("%6d       ", dl.OldLineNo)
+		} else {
+			lineNum = "            "
+		}
+	case LineAdded:
+		marker = "+"
+		bgStyle = addedLineStyle
+		lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
+		highlightColor = t.DiffHighlightAdded()
+		if dl.NewLineNo > 0 {
+			lineNum = fmt.Sprintf("      %7d", dl.NewLineNo)
+		} else {
+			lineNum = "            "
+		}
+	case LineContext:
+		marker = " "
+		bgStyle = contextLineStyle
+		if dl.OldLineNo > 0 && dl.NewLineNo > 0 {
+			lineNum = fmt.Sprintf("%6d %6d", dl.OldLineNo, dl.NewLineNo)
+		} else {
+			lineNum = "            "
+		}
+	}
+
+	// Create the line prefix
+	prefix := renderLinePrefix(dl, lineNum, marker, lineNumberStyle, t)
+
+	// Render the content
+	prefixWidth := ansi.StringWidth(prefix)
+	contentWidth := width - prefixWidth
+	content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t)
+
+	return prefix + content
+}
+
 // renderDiffColumnLine is a helper function that handles the common logic for rendering diff columns
 func renderDiffColumnLine(
 	fileName string,
@@ -661,7 +790,6 @@ func renderDiffColumnLine(
 	var marker string
 	var bgStyle lipgloss.Style
 	var lineNum string
-	var highlightType LineType
 	var highlightColor compat.AdaptiveColor
 
 	if isLeftColumn {
@@ -671,7 +799,6 @@ func renderDiffColumnLine(
 			marker = "-"
 			bgStyle = removedLineStyle
 			lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
-			highlightType = LineRemoved
 			highlightColor = t.DiffHighlightRemoved()
 		case LineAdded:
 			marker = "?"
@@ -692,7 +819,6 @@ func renderDiffColumnLine(
 			marker = "+"
 			bgStyle = addedLineStyle
 			lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
-			highlightType = LineAdded
 			highlightColor = t.DiffHighlightAdded()
 		case LineRemoved:
 			marker = "?"
@@ -708,44 +834,24 @@ func renderDiffColumnLine(
 		}
 	}
 
-	// Style the marker based on line type
-	var styledMarker string
-	switch dl.Kind {
-	case LineRemoved:
-		styledMarker = removedLineStyle.Foreground(t.DiffRemoved()).Render(marker)
-	case LineAdded:
-		styledMarker = addedLineStyle.Foreground(t.DiffAdded()).Render(marker)
-	case LineContext:
-		styledMarker = contextLineStyle.Foreground(t.TextMuted()).Render(marker)
-	default:
-		styledMarker = marker
-	}
-
 	// Create the line prefix
-	prefix := lineNumberStyle.Render(lineNum + " " + styledMarker)
+	prefix := renderLinePrefix(*dl, lineNum, marker, lineNumberStyle, t)
 
-	// Apply syntax highlighting
-	content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
+	// Determine if we should render content
+	shouldRenderContent := (dl.Kind == LineRemoved && isLeftColumn) ||
+		(dl.Kind == LineAdded && !isLeftColumn) ||
+		dl.Kind == LineContext
 
-	// Apply intra-line highlighting if needed
-	if (dl.Kind == LineRemoved && isLeftColumn || dl.Kind == LineAdded && !isLeftColumn) && len(dl.Segments) > 0 {
-		content = applyHighlighting(content, dl.Segments, highlightType, highlightColor)
+	if !shouldRenderContent {
+		return bgStyle.Width(colWidth).Render("")
 	}
 
-	// Add a padding space for added/removed lines
-	if (dl.Kind == LineRemoved && isLeftColumn) || (dl.Kind == LineAdded && !isLeftColumn) {
-		content = bgStyle.Render(" ") + content
-	}
+	// Render the content
+	prefixWidth := ansi.StringWidth(prefix)
+	contentWidth := colWidth - prefixWidth
+	content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t)
 
-	// Create the final line and truncate if needed
-	lineText := prefix + content
-	return bgStyle.MaxHeight(1).Width(colWidth).Render(
-		ansi.Truncate(
-			lineText,
-			colWidth,
-			lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
-		),
-	)
+	return prefix + content
 }
 
 // renderLeftColumn formats the left side of a side-by-side diff
@@ -762,6 +868,27 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
 // Public API
 // -------------------------------------------------------------------------
 
+// RenderUnifiedHunk formats a hunk for unified display
+func RenderUnifiedHunk(fileName string, h Hunk, opts ...UnifiedOption) string {
+	// Apply options to create the configuration
+	config := NewUnifiedConfig(opts...)
+
+	// Make a copy of the hunk so we don't modify the original
+	hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))}
+	copy(hunkCopy.Lines, h.Lines)
+
+	// Highlight changes within lines
+	HighlightIntralineChanges(&hunkCopy)
+
+	var sb strings.Builder
+	for _, line := range hunkCopy.Lines {
+		sb.WriteString(renderUnifiedLine(fileName, line, config.Width, theme.CurrentTheme()))
+		sb.WriteString("\n")
+	}
+
+	return sb.String()
+}
+
 // RenderSideBySideHunk formats a hunk for side-by-side display
 func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
 	// Apply options to create the configuration
@@ -792,6 +919,21 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
 	return sb.String()
 }
 
+// FormatUnifiedDiff creates a unified formatted view of a diff
+func FormatUnifiedDiff(filename string, diffText string, opts ...UnifiedOption) (string, error) {
+	diffResult, err := ParseUnifiedDiff(diffText)
+	if err != nil {
+		return "", err
+	}
+
+	var sb strings.Builder
+	for _, h := range diffResult.Hunks {
+		sb.WriteString(RenderUnifiedHunk(filename, h, opts...))
+	}
+
+	return sb.String(), nil
+}
+
 // FormatDiff creates a side-by-side formatted view of a diff
 func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (string, error) {
 	// t := theme.CurrentTheme()

+ 0 - 8
packages/tui/internal/layout/layout.go

@@ -11,7 +11,6 @@ var Current *LayoutInfo
 
 func init() {
 	Current = &LayoutInfo{
-		Size:      LayoutSizeNormal,
 		Viewport:  Dimensions{Width: 80, Height: 25},
 		Container: Dimensions{Width: 80, Height: 25},
 	}
@@ -19,19 +18,12 @@ func init() {
 
 type LayoutSize string
 
-const (
-	LayoutSizeSmall  LayoutSize = "small"
-	LayoutSizeNormal LayoutSize = "normal"
-	LayoutSizeLarge  LayoutSize = "large"
-)
-
 type Dimensions struct {
 	Width  int
 	Height int
 }
 
 type LayoutInfo struct {
-	Size      LayoutSize
 	Viewport  Dimensions
 	Container Dimensions
 }

+ 0 - 10
packages/tui/internal/tui/tui.go

@@ -181,18 +181,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		msg.Height -= 2 // Make space for the status bar
 		a.width, a.height = msg.Width, msg.Height
 
-		size := layout.LayoutSizeNormal
-		if a.width < 40 {
-			size = layout.LayoutSizeSmall
-		} else if a.width < 80 {
-			size = layout.LayoutSizeNormal
-		} else {
-			size = layout.LayoutSizeLarge
-		}
-
 		// TODO: move away from global state
 		layout.Current = &layout.LayoutInfo{
-			Size: size,
 			Viewport: layout.Dimensions{
 				Width:  a.width,
 				Height: a.height,