feat(app): syntax highlighting and copy button for chat code blocks (#1062)

Adds Lezer-based highlighting to fenced code blocks in assistant messages,
with a hover/tap copy button. Colors move into the theme as
`theme.colors.syntax`, so the chat blocks, file preview, and diff viewer
share one StyleSheet path — token color updates flow through the Unistyles
native ShadowRegistry with no React re-renders on the chat hot path.
This commit is contained in:
Mohamed Boudra
2026-05-17 16:01:08 +08:00
committed by GitHub
parent 952b58c0eb
commit f6afe0d864
7 changed files with 347 additions and 82 deletions
@@ -0,0 +1,40 @@
import { StyleSheet } from "react-native-unistyles";
// Per-token color styles for syntax-highlighted code. Each value reads a
// theme path so the Unistyles Babel plugin tracks the dependency and updates
// native style values on theme changes — no React re-render of consumers.
// Used by message code blocks (hot path), file preview, and the diff viewer.
export const syntaxTokenStyles = StyleSheet.create((theme) => ({
base: { color: theme.colors.foreground },
keyword: { color: theme.colors.syntax.keyword },
comment: { color: theme.colors.syntax.comment },
string: { color: theme.colors.syntax.string },
number: { color: theme.colors.syntax.number },
literal: { color: theme.colors.syntax.literal },
function: { color: theme.colors.syntax.function },
definition: { color: theme.colors.syntax.definition },
class: { color: theme.colors.syntax.class },
type: { color: theme.colors.syntax.type },
tag: { color: theme.colors.syntax.tag },
attribute: { color: theme.colors.syntax.attribute },
property: { color: theme.colors.syntax.property },
variable: { color: theme.colors.syntax.variable },
operator: { color: theme.colors.syntax.operator },
punctuation: { color: theme.colors.syntax.punctuation },
regexp: { color: theme.colors.syntax.regexp },
escape: { color: theme.colors.syntax.escape },
meta: { color: theme.colors.syntax.meta },
heading: { color: theme.colors.syntax.heading },
link: { color: theme.colors.syntax.link },
}));
type SyntaxTokenStyleValue = (typeof syntaxTokenStyles)["base"];
// Accepts a plain string so diff tokens (server-typed `string | null`) and
// @getpaseo/highlight tokens (typed `HighlightStyle | null`) share one path.
// Unknown styles fall back to the base color.
export function syntaxTokenStyleFor(style: string | null | undefined): SyntaxTokenStyleValue {
if (!style) return syntaxTokenStyles.base;
const indexed = syntaxTokenStyles as unknown as Record<string, SyntaxTokenStyleValue>;
return indexed[style] ?? syntaxTokenStyles.base;
}
+4
View File
@@ -1,3 +1,5 @@
import { darkHighlightColors, lightHighlightColors } from "@getpaseo/highlight";
export const baseColors = {
// Base colors
white: "#ffffff",
@@ -518,6 +520,7 @@ function buildDarkTheme(semanticColors: ReturnType<typeof buildDarkSemanticColor
colors: {
...semanticColors,
palette: baseColors,
syntax: darkHighlightColors,
},
shadow: darkShadow,
...commonTheme,
@@ -535,6 +538,7 @@ export const lightTheme = {
colors: {
...lightSemanticColors,
palette: baseColors,
syntax: lightHighlightColors,
},
shadow: {
sm: {