diff --git a/docs/unistyles.md b/docs/unistyles.md index a17e22ff0..93677098d 100644 --- a/docs/unistyles.md +++ b/docs/unistyles.md @@ -301,6 +301,7 @@ Gotchas: - **Narrow the discriminated union before spreading.** `updateTheme`'s updater returns the theme union; spreading the union widens `colorScheme` to `"light" | "dark"`, which is assignable to neither concrete member. Branch on `t.colorScheme` so each branch spreads a single narrowed theme type (no `as`). - **`lineHeight.diff` is the code/diff line-height axis** — it is coupled to the code-font-size control (≈ `codeFontSize * 1.5`). Do NOT use it for prose. Markdown body line-height scales with the UI ramp (`Math.round(theme.fontSize.base * 1.4)`); routing prose through `lineHeight.diff` clips text at small code sizes. - **High-churn draft values** (live-while-typing in the appearance preview) bypass the theme: apply them as inline styles marked with `inlineUnistylesStyle` so per-keystroke values don't grow the `#unistyles-web` CSS registry. +- **Mounted parsed content uses `AppearanceStyleBoundary`.** Markdown, syntax-highlighted code, and tool-call detail bodies can contain memoized/custom renderer trees that do not naturally re-run when runtime-patched appearance tokens change. Wrap the parsed surface once with `packages/app/src/components/appearance-style-boundary.tsx`; do not add local "appearance key" props at each callsite. - **Dynamic font tokens stay widened.** `fontFamily`, `fontSize`, and `lineHeight` on `commonTheme` are annotated `string`/`number` (not narrowed by `as const`) so the updater's return assigns; the platform default stacks live in `DEFAULT_UI_FONT_STACK` / `DEFAULT_MONO_FONT_STACK`. ## Debugging diff --git a/package-lock.json b/package-lock.json index 79522d598..3d8c28892 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2813,6 +2813,50 @@ "integrity": "sha512-jFlTTD+0MK/01TdL5sHIsQ8RqzfmvBsGl4hSp87INv2+JIs/JF6EL9J8enuCz6z3fNdfOKISNbGCIrzZRXVrcw==", "license": "MIT OR Apache-2.0" }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz", + "integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", + "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -7874,6 +7918,12 @@ "node": ">=18" } }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -16979,6 +17029,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -34033,6 +34089,12 @@ "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", "license": "MIT" }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -36027,6 +36089,12 @@ "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "license": "MIT" }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -37674,6 +37742,8 @@ "name": "@getpaseo/highlight", "version": "0.1.87", "dependencies": { + "@codemirror/language": "^6.12.3", + "@codemirror/legacy-modes": "^6.5.3", "@lezer/common": "^1.5.0", "@lezer/cpp": "^1.1.5", "@lezer/css": "^1.3.0", diff --git a/packages/app/src/assistant-file-links/link.tsx b/packages/app/src/assistant-file-links/link.tsx index b4395c353..584e71a5c 100644 --- a/packages/app/src/assistant-file-links/link.tsx +++ b/packages/app/src/assistant-file-links/link.tsx @@ -12,6 +12,7 @@ import { isNative, isWeb } from "@/constants/platform"; import { Shortcut } from "@/components/ui/shortcut"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useStableEvent } from "@/hooks/use-stable-event"; +import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; import { useAssistantFileLinkResolverContext } from "./provider"; import type { AssistantFileLinkSource } from "./resolver"; import { useFileLink } from "./use-file-link"; @@ -19,10 +20,16 @@ import { useFileLink } from "./use-file-link"; interface AssistantMarkdownLinkProps { source: AssistantFileLinkSource; style: StyleProp; + monoSurface?: boolean; children: ReactNode; } -export function AssistantMarkdownLink({ source, style, children }: AssistantMarkdownLinkProps) { +export function AssistantMarkdownLink({ + source, + style, + monoSurface, + children, +}: AssistantMarkdownLinkProps) { const [hovered, setHovered] = useState(false); const { target, onHoverIn, onPress, onAuxPress } = useFileLink(source); const { configRef } = useAssistantFileLinkResolverContext(); @@ -52,7 +59,12 @@ export function AssistantMarkdownLink({ source, style, children }: AssistantMark if (isNative) { return ( - + {children} @@ -72,7 +84,9 @@ export function AssistantMarkdownLink({ source, style, children }: AssistantMark onHoverIn={handleHoverIn} onHoverOut={handleHoverOut} > - {children} + + {children} + ); @@ -100,7 +114,7 @@ export function AssistantMarkdownCodeLink({ [inheritedStyles, codeInlineStyle, linkStyle], ); return ( - + {children} ); diff --git a/packages/app/src/components/appearance-style-boundary.tsx b/packages/app/src/components/appearance-style-boundary.tsx new file mode 100644 index 000000000..5e6c31ed3 --- /dev/null +++ b/packages/app/src/components/appearance-style-boundary.tsx @@ -0,0 +1,66 @@ +import { Fragment, type ReactNode } from "react"; +import { withUnistyles } from "react-native-unistyles"; +import type { Theme } from "@/styles/theme"; + +interface AppearanceStyleBoundaryProps { + appearanceKey?: string; + children: ReactNode; +} + +function AppearanceStyleBoundaryBase({ appearanceKey, children }: AppearanceStyleBoundaryProps) { + return {children}; +} + +const appearanceStyleBoundaryMapping = (theme: Theme): Partial => ({ + appearanceKey: [ + theme.fontFamily.ui, + theme.fontFamily.mono, + theme.fontSize.xs, + theme.fontSize.sm, + theme.fontSize.base, + theme.fontSize.lg, + theme.fontSize.xl, + theme.fontSize["2xl"], + theme.fontSize["3xl"], + theme.fontSize["4xl"], + theme.fontSize.code, + theme.lineHeight.diff, + theme.colors.foreground, + theme.colors.foregroundMuted, + theme.colors.mutedForeground, + theme.colors.surface1, + theme.colors.surface2, + theme.colors.border, + theme.colors.accentBright, + theme.colors.syntax.keyword, + theme.colors.syntax.comment, + theme.colors.syntax.string, + theme.colors.syntax.number, + theme.colors.syntax.literal, + theme.colors.syntax.function, + theme.colors.syntax.definition, + theme.colors.syntax.class, + theme.colors.syntax.type, + theme.colors.syntax.tag, + theme.colors.syntax.attribute, + theme.colors.syntax.property, + theme.colors.syntax.variable, + theme.colors.syntax.operator, + theme.colors.syntax.punctuation, + theme.colors.syntax.regexp, + theme.colors.syntax.escape, + theme.colors.syntax.meta, + theme.colors.syntax.heading, + theme.colors.syntax.link, + ].join("\u0000"), +}); + +const ThemedAppearanceStyleBoundary = withUnistyles(AppearanceStyleBoundaryBase); + +export function AppearanceStyleBoundary({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/packages/app/src/components/markdown-text.android.tsx b/packages/app/src/components/markdown-text.android.tsx index d43d10ed8..f6f03c645 100644 --- a/packages/app/src/components/markdown-text.android.tsx +++ b/packages/app/src/components/markdown-text.android.tsx @@ -3,6 +3,7 @@ import { Text, View, type StyleProp, type TextStyle, type ViewStyle } from "reac interface MarkdownTextSpanProps { style?: StyleProp; + monoSurface?: boolean; children: ReactNode; } diff --git a/packages/app/src/components/markdown-text.ios.tsx b/packages/app/src/components/markdown-text.ios.tsx index 21b5c3c08..a66cc7708 100644 --- a/packages/app/src/components/markdown-text.ios.tsx +++ b/packages/app/src/components/markdown-text.ios.tsx @@ -4,6 +4,7 @@ import { UITextView } from "react-native-uitextview"; interface MarkdownTextSpanProps { style?: StyleProp; + monoSurface?: boolean; children: ReactNode; } diff --git a/packages/app/src/components/markdown-text.web.tsx b/packages/app/src/components/markdown-text.web.tsx index 146276e3d..f918b2868 100644 --- a/packages/app/src/components/markdown-text.web.tsx +++ b/packages/app/src/components/markdown-text.web.tsx @@ -1,8 +1,10 @@ import { useMemo, type ReactNode } from "react"; import { Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; +import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; interface MarkdownTextSpanProps { style?: StyleProp; + monoSurface?: boolean; children: ReactNode; } @@ -11,8 +13,12 @@ interface MarkdownTextSpanProps { // react-native-uitextview: its transitive import of codegenNativeComponent // pulls in setUpReactDevTools, which doesn't resolve under Metro's web // target in dev mode. -export function MarkdownTextSpan({ style, children }: MarkdownTextSpanProps) { - return {children}; +export function MarkdownTextSpan({ style, monoSurface, children }: MarkdownTextSpanProps) { + return ( + + {children} + + ); } interface MarkdownParagraphViewProps { diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 8269d2625..bf53770c7 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -11,6 +11,7 @@ import { type TextStyle, } from "react-native"; import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; +import { AppearanceStyleBoundary } from "@/components/appearance-style-boundary"; import * as React from "react"; import { useState, @@ -1496,16 +1497,18 @@ const MemoizedMarkdownBlock = React.memo(function MemoizedMarkdownBlock({ onLinkPress, }: MemoizedMarkdownBlockProps) { return ( - - {text} - + + + {text} + + ); }); @@ -1513,6 +1516,7 @@ interface MarkdownInheritedTextProps { inheritedStyles: TextStyle; textStyle: TextStyle; style?: StyleProp; + monoSurface?: boolean; children: ReactNode; } @@ -1520,13 +1524,18 @@ function MarkdownInheritedText({ inheritedStyles, textStyle, style: overrideStyle, + monoSurface, children, }: MarkdownInheritedTextProps) { const style = useMemo( () => [inheritedStyles, textStyle, overrideStyle], [inheritedStyles, textStyle, overrideStyle], ); - return {children}; + return ( + + {children} + + ); } interface MarkdownListItemContentProps { @@ -1696,6 +1705,7 @@ export const AssistantMessage = memo(function AssistantMessage({ key={node.key} inheritedStyles={inheritedStyles} textStyle={styles.code_inline} + monoSurface > {content} diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx index 54f752701..b83dcc56d 100644 --- a/packages/app/src/components/tool-call-details.tsx +++ b/packages/app/src/components/tool-call-details.tsx @@ -8,6 +8,7 @@ import { } from "react-native"; import { ScrollView as GHScrollView } from "react-native-gesture-handler"; import { StyleSheet } from "react-native-unistyles"; +import { AppearanceStyleBoundary } from "@/components/appearance-style-boundary"; import type { ToolCallDetail } from "@getpaseo/protocol/agent-types"; import { buildLineDiff, parseUnifiedDiff, type DiffLine } from "@/utils/tool-call-parsers"; import { highlightDiffLines } from "@/utils/diff-highlight"; @@ -756,7 +757,15 @@ function LoadingSkeleton({ containerStyle }: { containerStyle: StyleProp + + + ); +} + +function ToolCallDetailsContentInner({ detail, errorText, maxHeight, diff --git a/packages/app/src/hooks/use-settings/storage.test.ts b/packages/app/src/hooks/use-settings/storage.test.ts index d7b101c8a..e20d6e31b 100644 --- a/packages/app/src/hooks/use-settings/storage.test.ts +++ b/packages/app/src/hooks/use-settings/storage.test.ts @@ -240,7 +240,7 @@ describe("appearance settings", () => { expect(result.monoFontFamily).toBe(""); expect(result.uiFontSize).toBe(DEFAULT_UI_FONT_SIZE); expect(result.codeFontSize).toBe(DEFAULT_CODE_FONT_SIZE); - expect(result.syntaxTheme).toBe("github"); + expect(result.syntaxTheme).toBe("one"); }); it("clamps the UI font size into range and rejects non-numeric values", async () => { @@ -346,7 +346,7 @@ describe("appearance settings", () => { }), }); - expect((await loadAppSettingsFromStorage(deps)).syntaxTheme).toBe("github"); + expect((await loadAppSettingsFromStorage(deps)).syntaxTheme).toBe("one"); }); it("drops an unknown syntax theme id back to the default", async () => { @@ -356,7 +356,7 @@ describe("appearance settings", () => { }), }); - expect((await loadAppSettingsFromStorage(deps)).syntaxTheme).toBe("github"); + expect((await loadAppSettingsFromStorage(deps)).syntaxTheme).toBe("one"); }); }); diff --git a/packages/app/src/hooks/use-settings/storage.ts b/packages/app/src/hooks/use-settings/storage.ts index 65a8200b6..a06c73afe 100644 --- a/packages/app/src/hooks/use-settings/storage.ts +++ b/packages/app/src/hooks/use-settings/storage.ts @@ -33,7 +33,7 @@ export interface AppSettings { monoFontFamily: string; // "" = platform default mono stack uiFontSize: number; // clamped px, default 16 codeFontSize: number; // clamped px, default 12 - syntaxTheme: SyntaxThemeId; // default "auto" + syntaxTheme: SyntaxThemeId; // default "one" } export interface Settings extends AppSettings { @@ -50,7 +50,7 @@ export const DEFAULT_CLIENT_SETTINGS: AppSettings = { monoFontFamily: "", uiFontSize: DEFAULT_UI_FONT_SIZE, codeFontSize: DEFAULT_CODE_FONT_SIZE, - syntaxTheme: "github", + syntaxTheme: "one", }; export const DEFAULT_APP_SETTINGS: Settings = { diff --git a/packages/app/src/screens/settings/appearance/appearance-section.tsx b/packages/app/src/screens/settings/appearance/appearance-section.tsx index 59b1c4371..1e2f3e024 100644 --- a/packages/app/src/screens/settings/appearance/appearance-section.tsx +++ b/packages/app/src/screens/settings/appearance/appearance-section.tsx @@ -32,6 +32,7 @@ import { THEME_SWATCHES, type Theme, } from "@/styles/theme"; +import { isNative } from "@/constants/platform"; import { settingsStyles } from "@/styles/settings"; import { AppearancePreview } from "./appearance-preview"; @@ -247,6 +248,7 @@ interface FontSizeRowProps { title: string; accessibilityLabel: string; draft: string; + withBorder?: boolean; onChangeDraft: (value: string) => void; onCommit: () => void; } @@ -255,11 +257,12 @@ function FontSizeRow({ title, accessibilityLabel, draft, + withBorder = true, onChangeDraft, onCommit, }: FontSizeRowProps) { return ( - + {title} @@ -348,6 +351,7 @@ function SyntaxRow({ value, onChange }: SyntaxRowProps) { export function AppearanceSection() { const { settings, updateSettings } = useAppSettings(); + const showFontFamilyRows = !isNative; const [uiFontDraft, setUiFontDraft] = useState(settings.uiFontFamily); const [monoFontDraft, setMonoFontDraft] = useState(settings.monoFontFamily); @@ -458,35 +462,40 @@ export function AppearanceSection() { - + {showFontFamilyRows ? ( + + ) : null} - + {showFontFamilyRows ? ( + + ) : null} = {}): AppearanceInput { monoFontFamily: "", uiFontSize: 16, codeFontSize: 12, - syntaxTheme: "github", + syntaxTheme: "one", ...overrides, }; } diff --git a/packages/app/src/styles/markdown-styles.test.ts b/packages/app/src/styles/markdown-styles.test.ts index 09afb93b1..3b728ce6d 100644 --- a/packages/app/src/styles/markdown-styles.test.ts +++ b/packages/app/src/styles/markdown-styles.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createMarkdownStyles } from "./markdown-styles"; +import { createCompactMarkdownStyles, createMarkdownStyles } from "./markdown-styles"; import { darkTheme } from "./theme"; describe("createMarkdownStyles", () => { @@ -69,4 +69,28 @@ describe("createMarkdownStyles", () => { userSelect: "text", }); }); + + it("uses the mono font-size token directly for inline and block code", () => { + const styles = createMarkdownStyles(darkTheme); + const compactStyles = createCompactMarkdownStyles(darkTheme); + + expect(styles.code_inline).toMatchObject({ + fontFamily: darkTheme.fontFamily.mono, + fontSize: darkTheme.fontSize.code, + lineHeight: Math.round(darkTheme.fontSize.code * 1.45), + }); + expect(styles.code_block).toMatchObject({ + fontFamily: darkTheme.fontFamily.mono, + fontSize: darkTheme.fontSize.code, + }); + expect(styles.fence).toMatchObject({ + fontFamily: darkTheme.fontFamily.mono, + fontSize: darkTheme.fontSize.code, + }); + expect(compactStyles.code_inline).toMatchObject({ + fontFamily: darkTheme.fontFamily.mono, + fontSize: darkTheme.fontSize.code, + lineHeight: Math.round(darkTheme.fontSize.code * 1.45), + }); + }); }); diff --git a/packages/app/src/styles/markdown-styles.ts b/packages/app/src/styles/markdown-styles.ts index 8829af6bf..b7a185f1f 100644 --- a/packages/app/src/styles/markdown-styles.ts +++ b/packages/app/src/styles/markdown-styles.ts @@ -172,6 +172,7 @@ export function createMarkdownStyles(theme: Theme) { borderWidth: 0, fontFamily: theme.fontFamily.mono, fontSize: theme.fontSize.code, + lineHeight: Math.round(theme.fontSize.code * 1.45), }, code_block: { @@ -389,7 +390,7 @@ export function createCompactMarkdownStyles(theme: Theme) { code_inline: { ...baseStyles.code_inline, - fontSize: theme.fontSize.sm - 3, + fontSize: theme.fontSize.code, }, code_block: { diff --git a/packages/highlight/package.json b/packages/highlight/package.json index a913198c3..c137bafb6 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -25,6 +25,8 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { + "@codemirror/language": "^6.12.3", + "@codemirror/legacy-modes": "^6.5.3", "@lezer/common": "^1.5.0", "@lezer/cpp": "^1.1.5", "@lezer/css": "^1.3.0", diff --git a/packages/highlight/src/__tests__/highlighter.test.ts b/packages/highlight/src/__tests__/highlighter.test.ts index e74a8f0dd..af2d97615 100644 --- a/packages/highlight/src/__tests__/highlighter.test.ts +++ b/packages/highlight/src/__tests__/highlighter.test.ts @@ -30,6 +30,22 @@ describe("highlightCode", () => { expect(stringToken?.style).toBe("string"); }); + it("highlights Swift code", () => { + const code = 'struct Greeter {\n let message = "hello"\n}'; + const result = highlightCode(code, "test.swift"); + + expect(result).toHaveLength(3); + + const structToken = result[0].find((t) => t.text === "struct"); + expect(structToken?.style).toBe("keyword"); + + const typeToken = result[0].find((t) => t.text === "Greeter"); + expect(typeToken?.style).toBe("definition"); + + const stringToken = result[1].find((t) => t.text.includes("hello")); + expect(stringToken?.style).toBe("string"); + }); + it("highlights TSX code with correct dialect", () => { const code = 'const el =
hello
;'; const result = highlightCode(code, "test.tsx"); diff --git a/packages/highlight/src/__tests__/parsers.test.ts b/packages/highlight/src/__tests__/parsers.test.ts index 230ce3b6a..c0bf459bb 100644 --- a/packages/highlight/src/__tests__/parsers.test.ts +++ b/packages/highlight/src/__tests__/parsers.test.ts @@ -13,6 +13,7 @@ describe("isLanguageSupported", () => { expect(isLanguageSupported("test.css")).toBe(true); expect(isLanguageSupported("test.html")).toBe(true); expect(isLanguageSupported("test.java")).toBe(true); + expect(isLanguageSupported("test.swift")).toBe(true); expect(isLanguageSupported("test.ex")).toBe(true); }); @@ -49,6 +50,7 @@ describe("getSupportedExtensions", () => { expect(extensions).toContain("py"); expect(extensions).toContain("go"); expect(extensions).toContain("rs"); + expect(extensions).toContain("swift"); expect(extensions).toContain("json"); }); }); diff --git a/packages/highlight/src/__tests__/themes.test.ts b/packages/highlight/src/__tests__/themes.test.ts index 0b0d67e55..3cf5850d5 100644 --- a/packages/highlight/src/__tests__/themes.test.ts +++ b/packages/highlight/src/__tests__/themes.test.ts @@ -59,10 +59,19 @@ describe("resolveSyntaxColors", () => { }); it("dark-only themes ignore the color scheme", () => { - for (const id of ["dracula", "nord"] as const) { + for (const id of ["dracula"] as const) { expect(resolveSyntaxColors(id, "light")).toEqual(resolveSyntaxColors(id, "dark")); } }); + + it("nord uses a dark text palette in light mode", () => { + const light = resolveSyntaxColors("nord", "light"); + const dark = resolveSyntaxColors("nord", "dark"); + + expect(light).not.toEqual(dark); + expect(light.variable).toBe("#2e3440"); + expect(light.comment).toBe("#6b7280"); + }); }); describe("isSyntaxThemeId", () => { diff --git a/packages/highlight/src/parsers.ts b/packages/highlight/src/parsers.ts index c1c1c8cd4..32854914e 100644 --- a/packages/highlight/src/parsers.ts +++ b/packages/highlight/src/parsers.ts @@ -1,3 +1,5 @@ +import { StreamLanguage } from "@codemirror/language"; +import { swift } from "@codemirror/legacy-modes/mode/swift"; import { parser as jsParser } from "@lezer/javascript"; import { parser as jsonParser } from "@lezer/json"; import { parser as cssParser } from "@lezer/css"; @@ -55,6 +57,8 @@ const parsersByExtension: Record = { yml: yamlParser, // Rust rs: rustParser, + // Swift + swift: StreamLanguage.define(swift).parser, // Elixir ex: elixirParser, exs: elixirParser, diff --git a/packages/highlight/src/themes.ts b/packages/highlight/src/themes.ts index 51e12f2d2..8aa76d4a7 100644 --- a/packages/highlight/src/themes.ts +++ b/packages/highlight/src/themes.ts @@ -180,8 +180,20 @@ const oneDark: RolePalette = { operator: "#56b6c2", }; -// --- Nord (dark only) ---------------------------------------------------- -const nord: RolePalette = { +// --- Nord (Snow Storm light / Polar Night dark) --------------------------- +const nordLight: RolePalette = { + base: "#2e3440", + keyword: "#5e81ac", + comment: "#6b7280", + string: "#4f6f3a", + number: "#8f5e91", + function: "#2e6f8e", + type: "#3b7f87", + tag: "#5e81ac", + attribute: "#8f5e91", + operator: "#5e81ac", +}; +const nordDark: RolePalette = { base: "#d8dee9", keyword: "#81a1c1", comment: "#616e88", @@ -269,7 +281,7 @@ export function resolveSyntaxColors( case "one": return expandRolePalette(dark ? oneDark : oneLight); case "nord": - return expandRolePalette(nord); + return expandRolePalette(dark ? nordDark : nordLight); case "gruvbox": return expandRolePalette(dark ? gruvboxDark : gruvboxLight); case "solarized":