Fix mobile code block rendering

This commit is contained in:
Mohamed Boudra
2026-05-28 15:40:36 +07:00
parent 3baba543a4
commit 93189148f5
12 changed files with 68 additions and 52 deletions
+1 -1
View File
@@ -189,7 +189,7 @@ const styles = StyleSheet.create((theme) => {
}, },
lineText: { lineText: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foreground, color: theme.colors.foreground,
...(isWeb ...(isWeb
? { ? {
@@ -1158,7 +1158,7 @@ const styles = StyleSheet.create((theme) => ({
codeText: { codeText: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.code,
flexShrink: 0, flexShrink: 0,
}, },
previewImageScrollContent: { previewImageScrollContent: {
+7 -7
View File
@@ -172,15 +172,15 @@ const codeLineStyles = StyleSheet.create((theme) => ({
gutterText: { gutterText: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.code,
lineHeight: theme.fontSize.sm * 1.45, lineHeight: theme.fontSize.code * 1.45,
opacity: 0.4, opacity: 0.4,
userSelect: "none", userSelect: "none",
}, },
lineText: { lineText: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.code,
lineHeight: theme.fontSize.sm * 1.45, lineHeight: theme.fontSize.code * 1.45,
flex: 1, flex: 1,
}, },
})); }));
@@ -216,9 +216,9 @@ function FilePreviewBody({
const gutterWidth = useMemo(() => { const gutterWidth = useMemo(() => {
if (!highlightedLines) return 0; if (!highlightedLines) return 0;
return lineNumberGutterWidth(highlightedLines.length, theme.fontSize.sm); return lineNumberGutterWidth(highlightedLines.length, theme.fontSize.code);
}, [highlightedLines, theme.fontSize.sm]); }, [highlightedLines, theme.fontSize.code]);
const lineHeight = theme.fontSize.sm * 1.45; const lineHeight = theme.fontSize.code * 1.45;
const lineSelection = useMemo(() => { const lineSelection = useMemo(() => {
if (!highlightedLines) { if (!highlightedLines) {
return null; return null;
@@ -1,12 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import { Pressable, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
Pressable,
Text,
View,
type StyleProp,
type TextStyle,
type ViewStyle,
} from "react-native";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { MarkdownTextSpan } from "@/components/markdown-text"; import { MarkdownTextSpan } from "@/components/markdown-text";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
@@ -47,6 +40,10 @@ function fenceLanguageToExtension(info: string | null | undefined): string | nul
return LANGUAGE_ALIASES[normalized] ?? normalized; return LANGUAGE_ALIASES[normalized] ?? normalized;
} }
function stripTerminalFenceNewline(code: string): string {
return code.endsWith("\n") ? code.slice(0, -1) : code;
}
// Cross-instance cache for tokenized code blocks. Tokenization is // Cross-instance cache for tokenized code blocks. Tokenization is
// theme-independent (colors are applied at render time), so the key is just // theme-independent (colors are applied at render time), so the key is just
// (language, code). Bounded by entry count — 200 is generous for a chat // (language, code). Bounded by entry count — 200 is generous for a chat
@@ -88,23 +85,24 @@ export const HighlightedCodeBlock = React.memo(function HighlightedCodeBlock({
() => splitFenceStyle(inheritedStyles, textStyle), () => splitFenceStyle(inheritedStyles, textStyle),
[inheritedStyles, textStyle], [inheritedStyles, textStyle],
); );
const renderedCode = useMemo(() => stripTerminalFenceNewline(code), [code]);
const keyedLines = useMemo<KeyedLine[] | null>(() => { const keyedLines = useMemo<KeyedLine[] | null>(() => {
const ext = fenceLanguageToExtension(language); const ext = fenceLanguageToExtension(language);
if (!ext) return null; if (!ext) return null;
const cacheKey = `${ext}:${code}`; const cacheKey = `${ext}:${renderedCode}`;
const cached = tokenizationCache.get(cacheKey); const cached = tokenizationCache.get(cacheKey);
if (cached) return cached; if (cached) return cached;
let tokenizedLines: HighlightToken[][]; let tokenizedLines: HighlightToken[][];
try { try {
tokenizedLines = highlightCode(code, `x.${ext}`); tokenizedLines = highlightCode(renderedCode, `x.${ext}`);
} catch { } catch {
return null; return null;
} }
const result = tokenizedLines.map(toKeyedLine); const result = tokenizedLines.map(toKeyedLine);
tokenizationCache.set(cacheKey, result); tokenizationCache.set(cacheKey, result);
return result; return result;
}, [code, language]); }, [renderedCode, language]);
const isCompact = useIsCompactFormFactor(); const isCompact = useIsCompactFormFactor();
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@@ -120,18 +118,9 @@ export const HighlightedCodeBlock = React.memo(function HighlightedCodeBlock({
onPointerLeave={handlePointerLeave} onPointerLeave={handlePointerLeave}
> >
{keyedLines ? ( {keyedLines ? (
<MarkdownTextSpan style={innerTextStyle}> <MarkdownTextSpan style={innerTextStyle}>{renderCodeSegments(keyedLines)}</MarkdownTextSpan>
{keyedLines.map((line, lineIndex) => (
<React.Fragment key={line.key}>
{lineIndex > 0 ? "\n" : null}
{line.tokens.map(({ key, token }) => (
<TokenSpan key={key} token={token} />
))}
</React.Fragment>
))}
</MarkdownTextSpan>
) : ( ) : (
<MarkdownTextSpan style={innerTextStyle}>{code}</MarkdownTextSpan> <MarkdownTextSpan style={innerTextStyle}>{renderedCode}</MarkdownTextSpan>
)} )}
<CopyButton getCode={getCode} visible={controlsVisible} /> <CopyButton getCode={getCode} visible={controlsVisible} />
</View> </View>
@@ -158,13 +147,38 @@ function toKeyedLine(tokens: HighlightToken[], lineIndex: number): KeyedLine {
}; };
} }
function renderCodeSegments(keyedLines: KeyedLine[]): React.ReactNode[] {
const segments: React.ReactNode[] = [];
for (let lineIndex = 0; lineIndex < keyedLines.length; lineIndex += 1) {
const line = keyedLines[lineIndex];
if (lineIndex > 0) {
segments.push(<CodeTextSpan key={`${line.key}-newline`} text={"\n"} />);
}
for (const { key, token } of line.tokens) {
segments.push(<TokenSpan key={`${line.key}-${key}`} token={token} />);
}
}
return segments;
}
interface TokenSpanProps { interface TokenSpanProps {
token: HighlightToken; token: HighlightToken;
} }
const TokenSpan = React.memo(function TokenSpan({ token }: TokenSpanProps) { const TokenSpan = React.memo(function TokenSpan({ token }: TokenSpanProps) {
if (!token.style) return token.text; return (
return <Text style={syntaxTokenStyleFor(token.style)}>{token.text}</Text>; <MarkdownTextSpan style={token.style ? syntaxTokenStyleFor(token.style) : undefined}>
{token.text}
</MarkdownTextSpan>
);
});
interface CodeTextSpanProps {
text: string;
}
const CodeTextSpan = React.memo(function CodeTextSpan({ text }: CodeTextSpanProps) {
return <MarkdownTextSpan>{text}</MarkdownTextSpan>;
}); });
interface SplitStyles { interface SplitStyles {
@@ -180,6 +194,7 @@ function splitFenceStyle(inheritedStyles: TextStyle, textStyle: TextStyle): Spli
const textOnly: TextStyle = { ...WEB_SELECTABLE }; const textOnly: TextStyle = { ...WEB_SELECTABLE };
if (fontFamily !== undefined) textOnly.fontFamily = fontFamily; if (fontFamily !== undefined) textOnly.fontFamily = fontFamily;
if (fontSize !== undefined) textOnly.fontSize = fontSize; if (fontSize !== undefined) textOnly.fontSize = fontSize;
if (fontSize !== undefined) textOnly.lineHeight = Math.round(fontSize * 1.45);
if (color !== undefined) textOnly.color = color; if (color !== undefined) textOnly.color = color;
return { return {
containerStyle: [box as ViewStyle, CONTAINER_BASE], containerStyle: [box as ViewStyle, CONTAINER_BASE],
+1 -1
View File
@@ -1975,7 +1975,7 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({
}, },
metadataText: { metadataText: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
lineHeight: 16, lineHeight: 16,
}, },
@@ -608,7 +608,7 @@ const sheetStyles = StyleSheet.create((theme) => ({
}, },
monoHint: { monoHint: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
flexShrink: 0, flexShrink: 0,
}, },
@@ -717,7 +717,7 @@ const sheetStyles = StyleSheet.create((theme) => ({
}, },
codeText: { codeText: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foreground, color: theme.colors.foreground,
lineHeight: 18, lineHeight: 18,
}, },
@@ -844,7 +844,7 @@ const styles = StyleSheet.create((theme) => {
}, },
scrollText: { scrollText: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foreground, color: theme.colors.foreground,
lineHeight: 18, lineHeight: 18,
...(isWeb ...(isWeb
@@ -859,7 +859,7 @@ const styles = StyleSheet.create((theme) => {
}, },
subAgentSessionText: { subAgentSessionText: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
lineHeight: 18, lineHeight: 18,
marginBottom: theme.spacing[2], marginBottom: theme.spacing[2],
@@ -875,13 +875,13 @@ const styles = StyleSheet.create((theme) => {
}, },
subAgentActionTool: { subAgentActionTool: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
lineHeight: 18, lineHeight: 18,
}, },
subAgentActionSummary: { subAgentActionSummary: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foreground, color: theme.colors.foreground,
lineHeight: 18, lineHeight: 18,
}, },
+3 -3
View File
@@ -944,7 +944,7 @@ function DiffFileBody({
hunk.newStart + hunk.newCount, hunk.newStart + hunk.newCount,
); );
} }
const gutterWidth = lineNumberGutterWidth(maxLineNo, theme.fontSize.xs); const gutterWidth = lineNumberGutterWidth(maxLineNo, theme.fontSize.code);
if (layout === "split") { if (layout === "split") {
const rows = buildSplitDiffRows(file); const rows = buildSplitDiffRows(file);
@@ -2475,7 +2475,7 @@ const styles = StyleSheet.create((theme) => ({
width: "100%", width: "100%",
textAlign: "right", textAlign: "right",
paddingRight: theme.spacing[2], paddingRight: theme.spacing[2],
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
lineHeight: theme.lineHeight.diff, lineHeight: theme.lineHeight.diff,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
@@ -2490,7 +2490,7 @@ const styles = StyleSheet.create((theme) => ({
diffLineText: { diffLineText: {
flex: 1, flex: 1,
paddingRight: theme.spacing[3], paddingRight: theme.spacing[3],
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
lineHeight: theme.lineHeight.diff, lineHeight: theme.lineHeight.diff,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
color: theme.colors.foreground, color: theme.colors.foreground,
+1 -1
View File
@@ -541,7 +541,7 @@ const styles = StyleSheet.create((theme) => ({
}, },
logText: { logText: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.code,
lineHeight: 20, lineHeight: 20,
color: theme.colors.foreground, color: theme.colors.foreground,
}, },
@@ -234,7 +234,7 @@ const styles = StyleSheet.create((theme) => ({
}, },
errorMessage: { errorMessage: {
color: theme.colors.destructive, color: theme.colors.destructive,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.code,
lineHeight: 20, lineHeight: 20,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
}, },
@@ -258,7 +258,7 @@ const styles = StyleSheet.create((theme) => ({
}, },
logsText: { logsText: {
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
color: theme.colors.foreground, color: theme.colors.foreground,
lineHeight: 18, lineHeight: 18,
...(isWeb ...(isWeb
+6 -6
View File
@@ -170,7 +170,7 @@ export function createMarkdownStyles(theme: Theme) {
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
borderWidth: 0, borderWidth: 0,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.base - 3,
}, },
code_block: { code_block: {
@@ -180,7 +180,7 @@ export function createMarkdownStyles(theme: Theme) {
padding: theme.spacing[3], padding: theme.spacing[3],
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.code,
marginVertical: theme.spacing[2], marginVertical: theme.spacing[2],
}, },
@@ -193,7 +193,7 @@ export function createMarkdownStyles(theme: Theme) {
borderWidth: 1, borderWidth: 1,
borderColor: theme.colors.border, borderColor: theme.colors.border,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.code,
marginVertical: theme.spacing[3], marginVertical: theme.spacing[3],
}, },
@@ -388,18 +388,18 @@ export function createCompactMarkdownStyles(theme: Theme) {
code_inline: { code_inline: {
...baseStyles.code_inline, ...baseStyles.code_inline,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.sm - 3,
}, },
code_block: { code_block: {
...baseStyles.code_block, ...baseStyles.code_block,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
padding: theme.spacing[2], padding: theme.spacing[2],
}, },
fence: { fence: {
...baseStyles.fence, ...baseStyles.fence,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.code,
padding: theme.spacing[2], padding: theme.spacing[2],
}, },
}; };
+1
View File
@@ -434,6 +434,7 @@ export const SPACING = {
export const FONT_SIZE = { export const FONT_SIZE = {
xs: 12, xs: 12,
code: 12,
sm: 14, sm: 14,
base: 16, base: 16,
lg: 18, lg: 18,