on main help sign in
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.

commitaae4c3082281f9233484609f73169e60257da73c authorAndrey Nering <andreynering@users.noreply.github.com> dateThu 29 Jan 2026, 10:29 (9 months ago) parent87fad18
3 files changed+19−8
internal/stringext/string.go+12 internal/ui/chat/tools.go+4−8 internal/ui/list/highlight.go+3
internal/stringext/string.go+12
@@ -1,6 +1,8 @@
11package stringext
22
33import (
4+ "strings"
5+
46 "golang.org/x/text/cases"
57 "golang.org/x/text/language"
68)
@@ -8,3 +10,13 @@ import (
810func Capitalize(text string) string {
911 return cases.Title(language.English, cases.Compact).String(text)
1012}
13+
14+// NormalizeSpace normalizes whitespace in the given content string.
15+// It replaces Windows-style line endings with Unix-style line endings,
16+// converts tabs to four spaces, and trims leading and trailing whitespace.
17+func NormalizeSpace(content string) string {
18+ content = strings.ReplaceAll(content, "\r\n", "\n")
19+ content = strings.ReplaceAll(content, "\t", " ")
20+ content = strings.TrimSpace(content)
21+ return content
22+}
internal/ui/chat/tools.go+4−8
@@ -15,6 +15,7 @@ import (
1515 "github.com/charmbracelet/crush/internal/diff"
1616 "github.com/charmbracelet/crush/internal/fsext"
1717 "github.com/charmbracelet/crush/internal/message"
18+ "github.com/charmbracelet/crush/internal/stringext"
1819 "github.com/charmbracelet/crush/internal/ui/anim"
1920 "github.com/charmbracelet/crush/internal/ui/common"
2021 "github.com/charmbracelet/crush/internal/ui/styles"
@@ -531,9 +532,7 @@ func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, n
531532
532533// toolOutputPlainContent renders plain text with optional expansion support.
533534func toolOutputPlainContent(sty *styles.Styles, content string, width int, expanded bool) string {
534− content = strings.ReplaceAll(content, "\r\n", "\n")
535− content = strings.ReplaceAll(content, "\t", " ")
536− content = strings.TrimSpace(content)
535+ content = stringext.NormalizeSpace(content)
537536 lines := strings.Split(content, "\n")
538537
539538 maxLines := responseContextHeight
@@ -566,8 +565,7 @@ func toolOutputPlainContent(sty *styles.Styles, content string, width int, expan
566565
567566// toolOutputCodeContent renders code with syntax highlighting and line numbers.
568567func toolOutputCodeContent(sty *styles.Styles, path, content string, offset, width int, expanded bool) string {
569− content = strings.ReplaceAll(content, "\r\n", "\n")
570− content = strings.ReplaceAll(content, "\t", " ")
568+ content = stringext.NormalizeSpace(content)
571569
572570 lines := strings.Split(content, "\n")
573571 maxLines := responseContextHeight
@@ -776,9 +774,7 @@ func roundedEnumerator(lPadding, width int) tree.Enumerator {
776774
777775// toolOutputMarkdownContent renders markdown content with optional truncation.
778776func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string {
779− content = strings.ReplaceAll(content, "\r\n", "\n")
780− content = strings.ReplaceAll(content, "\t", " ")
781− content = strings.TrimSpace(content)
777+ content = stringext.NormalizeSpace(content)
782778
783779 // Cap width for readability.
784780 if width > maxTextWidth {
internal/ui/list/highlight.go+3
@@ -5,6 +5,7 @@ import (
55 "strings"
66
77 "charm.land/lipgloss/v2"
8+ "github.com/charmbracelet/crush/internal/stringext"
89 uv "github.com/charmbracelet/ultraviolet"
910)
1011
@@ -53,6 +54,8 @@ func Highlight(content string, area image.Rectangle, startLine, startCol, endLin
5354// HighlightBuffer highlights a region of text within the given content and
5455// region, returning a [uv.ScreenBuffer].
5556func HighlightBuffer(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) *uv.ScreenBuffer {
57+ content = stringext.NormalizeSpace(content)
58+
5659 if startLine < 0 || startCol < 0 {
5760 return nil
5861 }