From aae4c3082281f9233484609f73169e60257da73c Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 29 Jan 2026 10:29:46 -0300 Subject: [PATCH] fix(ui): fix selection of code blocks with tabs inside markdown (#2039) Yes, this is very specific. You need a code block, inside markdown, that uses tabs instead of spaces for indentation, like Go code. This affected both how the code is present on the TUI as well as the text copied to clipboard. We need to convert tabs into 4 spaces on the highlighter to match how it's shown in the TUI. Centralized this into a function to ensure we're doing the exact same thing everywhere. --- internal/stringext/string.go | 12 ++++++++++++ internal/ui/chat/tools.go | 12 ++++-------- internal/ui/list/highlight.go | 3 +++ 3 files changed, 19 insertions(+), 8 deletions(-) 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 }