fix(tui): trim markdown wrap spaces (#22062)

* fix(tui): trim markdown wrap spaces

Use trim-aware wrapping for markdown prose so word-wrapped continuation lines do not keep boundary spaces.

* fix(tui): simplify markdown wrap nodes

Keep trim-aware wrapping on the rendered markdown text node while leaving nested inline segments as plain virtual text.

* fix(tui): trim definition row wrapping

Apply trim-aware wrapping to markdown definition rows so continuation lines match other prose rows.

* fix(tui): trim list and quote wrapping

Put trim-aware wrapping on the rendered list and quote rows that own markdown inline layout.

* fix(tui): preserve markdown nesting with trim wrap

Move list and quote indentation into layout padding so trim-aware wrapping does not erase nested markdown structure.

* fix(tui): trim only soft wrap spaces

Change trim-aware wrapping to remove whitespace only at soft-wrap boundaries so original leading inline spaces stay verbatim.

* fix(tui): preserve extra boundary whitespace

Trim only one soft-wrap boundary whitespace character so wrap-trim avoids leading continuations without collapsing intentional spacing.

* fix(tui): align styled wrap-trim mapping

Update styled text remapping to skip the single whitespace removed at soft-wrap boundaries without dropping preserved indentation.

* fix(tui): clean wrap trim test helpers

Clarify boundary-trim wording and strip OSC escapes from markdown render test output.

* fix(tui): strip osc before ansi in markdown tests

Remove OSC escapes from raw render output before SGR/CSI cleanup so markdown render assertions stay plain text.
This commit is contained in:
brooklyn! 2026-05-08 20:51:34 -07:00 committed by GitHub
parent 78b0008f44
commit a7e7921dbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 147 additions and 68 deletions

View file

@ -323,7 +323,7 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
parts.push(<Text key={parts.length}>{text.slice(last)}</Text>)
}
return <Text>{parts.length ? parts : <Text>{text}</Text>}</Text>
return <Text wrap="wrap-trim">{parts.length ? parts : text}</Text>
}
// Cross-instance parsed-children cache: useMemo's per-instance cache dies
@ -420,7 +420,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (media) {
start('paragraph')
nodes.push(
<Text color={t.color.muted} key={key}>
<Text color={t.color.muted} key={key} wrap="wrap-trim">
{'▸ '}
<Link url={/^(?:\/|[a-z]:[\\/])/i.test(media) ? `file://${media}` : media}>
@ -594,7 +594,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (heading) {
start('heading')
nodes.push(
<Text bold color={t.color.accent} key={key}>
<Text bold color={t.color.accent} key={key} wrap="wrap-trim">
<MdInline t={t} text={heading} />
</Text>
)
@ -606,7 +606,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (i + 1 < lines.length && SETEXT_RE.test(lines[i + 1]!)) {
start('heading')
nodes.push(
<Text bold color={t.color.accent} key={key}>
<Text bold color={t.color.accent} key={key} wrap="wrap-trim">
<MdInline t={t} text={line.trim()} />
</Text>
)
@ -632,7 +632,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (footnote) {
start('list')
nodes.push(
<Text color={t.color.muted} key={key}>
<Text color={t.color.muted} key={key} wrap="wrap-trim">
[{footnote[1]}] <MdInline t={t} text={footnote[2] ?? ''} />
</Text>
)
@ -641,7 +641,7 @@ function MdImpl({ compact, t, text }: MdProps) {
while (i < lines.length && /^\s{2,}\S/.test(lines[i]!)) {
nodes.push(
<Box key={`${key}-cont-${i}`} paddingLeft={2}>
<Text color={t.color.muted}>
<Text color={t.color.muted} wrap="wrap-trim">
<MdInline t={t} text={lines[i]!.trim()} />
</Text>
</Box>
@ -655,7 +655,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (i + 1 < lines.length && DEF_RE.test(lines[i + 1]!)) {
start('list')
nodes.push(
<Text bold key={key}>
<Text bold key={key} wrap="wrap-trim">
{line.trim()}
</Text>
)
@ -669,7 +669,7 @@ function MdImpl({ compact, t, text }: MdProps) {
}
nodes.push(
<Text key={`${key}-def-${i}`}>
<Text key={`${key}-def-${i}`} wrap="wrap-trim">
<Text color={t.color.muted}> · </Text>
<MdInline t={t} text={def} />
</Text>
@ -689,14 +689,12 @@ function MdImpl({ compact, t, text }: MdProps) {
const marker = task ? (task[1]!.toLowerCase() === 'x' ? '☑' : '☐') : '•'
nodes.push(
<Text key={key}>
<Text color={t.color.muted}>
{' '.repeat(indentDepth(bullet[1]!) * 2)}
{marker}{' '}
<Box key={key} paddingLeft={indentDepth(bullet[1]!) * 2}>
<Text wrap="wrap-trim">
<Text color={t.color.muted}>{marker} </Text>
<MdInline t={t} text={task ? task[2]! : bullet[2]!} />
</Text>
<MdInline t={t} text={task ? task[2]! : bullet[2]!} />
</Text>
</Box>
)
i++
@ -708,14 +706,12 @@ function MdImpl({ compact, t, text }: MdProps) {
if (numbered) {
start('list')
nodes.push(
<Text key={key}>
<Text color={t.color.muted}>
{' '.repeat(indentDepth(numbered[1]!) * 2)}
{numbered[2]}.{' '}
<Box key={key} paddingLeft={indentDepth(numbered[1]!) * 2}>
<Text wrap="wrap-trim">
<Text color={t.color.muted}>{numbered[2]}. </Text>
<MdInline t={t} text={numbered[3]!} />
</Text>
<MdInline t={t} text={numbered[3]!} />
</Text>
</Box>
)
i++
@ -737,11 +733,11 @@ function MdImpl({ compact, t, text }: MdProps) {
nodes.push(
<Box flexDirection="column" key={key}>
{quoteLines.map((ql, qi) => (
<Text color={t.color.muted} key={qi}>
{' '.repeat(Math.max(0, ql.depth - 1) * 2)}
{'│ '}
<MdInline t={t} text={ql.text} />
</Text>
<Box key={qi} paddingLeft={Math.max(0, ql.depth - 1) * 2}>
<Text color={t.color.muted} wrap="wrap-trim">
<MdInline t={t} text={ql.text} />
</Text>
</Box>
))}
</Box>
)
@ -774,7 +770,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (summary) {
start('paragraph')
nodes.push(
<Text color={t.color.muted} key={key}>
<Text color={t.color.muted} key={key} wrap="wrap-trim">
{summary}
</Text>
)
@ -786,7 +782,7 @@ function MdImpl({ compact, t, text }: MdProps) {
if (/^<\/?[^>]+>$/.test(line.trim())) {
start('paragraph')
nodes.push(
<Text color={t.color.muted} key={key}>
<Text color={t.color.muted} key={key} wrap="wrap-trim">
{line.trim()}
</Text>
)