diff --git a/internal/stringext/string.go b/internal/stringext/string.go index 03456db93bc148f7c77e52da3c493c94fa79624f..8be28ccc2096c3d54b9f3106ed30d584503acdf4 100644 --- a/internal/stringext/string.go +++ b/internal/stringext/string.go @@ -1,6 +1,8 @@ package stringext import ( + "strings" + "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -8,3 +10,13 @@ import ( func Capitalize(text string) string { return cases.Title(language.English, cases.Compact).String(text) } + +// NormalizeSpace normalizes whitespace in the given content string. +// It replaces Windows-style line endings with Unix-style line endings, +// converts tabs to four spaces, and trims leading and trailing whitespace. +func NormalizeSpace(content string) string { + content = strings.ReplaceAll(content, "\r\n", "\n") + content = strings.ReplaceAll(content, "\t", " ") + content = strings.TrimSpace(content) + return content +} diff --git a/internal/ui/chat/tools.go b/internal/ui/chat/tools.go index 8aac1c1401fe299b24bd2cda81e18113bfd6176d..3ae403160b241eca6f5d74fb9841c2b10a7735b9 100644 --- a/internal/ui/chat/tools.go +++ b/internal/ui/chat/tools.go @@ -15,6 +15,7 @@ import ( "github.com/charmbracelet/crush/internal/diff" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/message" + "github.com/charmbracelet/crush/internal/stringext" "github.com/charmbracelet/crush/internal/ui/anim" "github.com/charmbracelet/crush/internal/ui/common" "github.com/charmbracelet/crush/internal/ui/styles" @@ -531,9 +532,7 @@ func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, n // toolOutputPlainContent renders plain text with optional expansion support. func toolOutputPlainContent(sty *styles.Styles, content string, width int, expanded bool) string { - content = strings.ReplaceAll(content, "\r\n", "\n") - content = strings.ReplaceAll(content, "\t", " ") - content = strings.TrimSpace(content) + content = stringext.NormalizeSpace(content) lines := strings.Split(content, "\n") maxLines := responseContextHeight @@ -566,8 +565,7 @@ func toolOutputPlainContent(sty *styles.Styles, content string, width int, expan // toolOutputCodeContent renders code with syntax highlighting and line numbers. func toolOutputCodeContent(sty *styles.Styles, path, content string, offset, width int, expanded bool) string { - content = strings.ReplaceAll(content, "\r\n", "\n") - content = strings.ReplaceAll(content, "\t", " ") + content = stringext.NormalizeSpace(content) lines := strings.Split(content, "\n") maxLines := responseContextHeight @@ -776,9 +774,7 @@ func roundedEnumerator(lPadding, width int) tree.Enumerator { // toolOutputMarkdownContent renders markdown content with optional truncation. func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string { - content = strings.ReplaceAll(content, "\r\n", "\n") - content = strings.ReplaceAll(content, "\t", " ") - content = strings.TrimSpace(content) + content = stringext.NormalizeSpace(content) // Cap width for readability. if width > maxTextWidth { diff --git a/internal/ui/list/highlight.go b/internal/ui/list/highlight.go index fefe836d110b52496028d21071fffc5262189d92..631181db29ce5bc3a2087de30341342f0374b229 100644 --- a/internal/ui/list/highlight.go +++ b/internal/ui/list/highlight.go @@ -5,6 +5,7 @@ import ( "strings" "charm.land/lipgloss/v2" + "github.com/charmbracelet/crush/internal/stringext" uv "github.com/charmbracelet/ultraviolet" ) @@ -53,6 +54,8 @@ func Highlight(content string, area image.Rectangle, startLine, startCol, endLin // HighlightBuffer highlights a region of text within the given content and // region, returning a [uv.ScreenBuffer]. func HighlightBuffer(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) *uv.ScreenBuffer { + content = stringext.NormalizeSpace(content) + if startLine < 0 || startCol < 0 { return nil }