diff --git a/packages/client/ui-primitives/src/markdown/highlight.ts b/packages/client/ui-primitives/src/markdown/highlight.ts index 9cd05d7f5d..f80ef74687 100644 --- a/packages/client/ui-primitives/src/markdown/highlight.ts +++ b/packages/client/ui-primitives/src/markdown/highlight.ts @@ -11,10 +11,11 @@ * (`@deepseek-ai/dsh-util-code-language`: python, rust, yaml, markup, …) is * imported lazily and registered the first time such a language is requested, * so a session that never opens a code surface in one of those languages pays - * neither the grammar modules nor their synchronous init. The first render of a lazy language falls back to plain text while its - * grammar loads, then {@link onGrammarLoaded} notifies subscribers to re-render - * with highlighting. An unknown or absent language falls back to plain text (no - * highlighting, still monospace) — never an error. + * neither the grammar modules nor their synchronous init. The first render of a + * lazy language falls back to plain text while its grammar loads, then + * {@link onGrammarLoaded} notifies subscribers to re-render with highlighting. + * An unknown or absent language falls back to plain text (no highlighting, still + * monospace) — never an error. */ import { createHighlighterCoreSync, createCssVariablesTheme } from 'shiki/core' @@ -233,7 +234,7 @@ const LANG_ALIASES = new Map([ /** * Resolve a language hint to the grammar id {@link LANG_ALIASES} selects. - * @param lang - Language hint from a code surface: a shared canonical id or a legacy read-card hint. + * @param lang - Language hint from a code surface: a canonical grammar id or the read card's persisted short id. * @returns The resolved grammar id, or `undefined` when the table aliases no grammar. */ export function grammarForHint(lang: string | undefined): string | undefined { diff --git a/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx b/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx index eb8c398992..1c2b829a5d 100644 --- a/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx +++ b/packages/client/ui-primitives/tests/code-highlighting.client.spec.tsx @@ -51,12 +51,12 @@ describe('code highlighting', () => { expect(languageForPath).toBe(sharedLanguageForPath) }) - it('reaches one grammar from the shared canonical id and the read card legacy hint', () => { - // The read card persists the old short ids while this surface reads the - // canonical table directly; both must select the same grammar, so a - // persisted hint can never render differently from this surface's own - // selection. This is the alias-normalized check the identity assertion - // cannot make now that the read projection intentionally differs. + it("reaches one grammar from the shared canonical id and the read card's persisted hint", () => { + // The read card persists short ids while this surface reads the canonical + // table directly; both must select the same grammar, so a persisted hint can + // never render differently from this surface's own selection. The identity + // assertion above cannot cover that, because the two projections + // intentionally differ. for (const extension of CODE_HIGHLIGHT_EXTENSIONS) { const path = `file.${extension}` const readHint = readLangHintForPath(path) diff --git a/packages/fs/tool-fs/src/read-render.ts b/packages/fs/tool-fs/src/read-render.ts index 23a27a210f..13a849f011 100644 --- a/packages/fs/tool-fs/src/read-render.ts +++ b/packages/fs/tool-fs/src/read-render.ts @@ -173,10 +173,9 @@ ${body} * Derive the persisted `lang` hint from a read path's file extension. The shared * table in `@deepseek-ai/dsh-util-code-language` owns the recognized suffixes and * the path rules (both separators, a leading dot as the extension separator, and - * prototype-key safety); `readLangHintForPath` projects the read card's - * historical short ids over it, so a suffix the pre-unification read table - * recognized keeps its byte-identical old value while a newly added suffix uses - * its language's short name. + * prototype-key safety); `readLangHintForPath` projects the read card's short ids + * over it, so a suffix whose value a recorded session already holds keeps it + * byte-identical while every other suffix uses its language's short name. * @param path - the model-facing path the read reported. * @returns the persisted language hint, or `undefined` when the extension maps to none. */ diff --git a/packages/fs/tool-fs/tests/read-render.spec.ts b/packages/fs/tool-fs/tests/read-render.spec.ts index 5f679caff9..5a2d32f693 100644 --- a/packages/fs/tool-fs/tests/read-render.spec.ts +++ b/packages/fs/tool-fs/tests/read-render.spec.ts @@ -119,11 +119,12 @@ describe('buildWindow', () => { }) /** - * The pre-unification read table's extension-to-hint mapping, transcribed - * verbatim. A read card persisted one of these strings, so changing any value - * changes replay-visible output for an already-recorded session. + * The short `lang` values a recorded read persisted, transcribed verbatim. + * Changing any value changes replay-visible output for an already-recorded + * session, so this table is the test's independent expectation rather than a + * second read of the implementation's own data. */ -const MASTER_READ_LANG_BY_EXTENSION: Readonly> = { +const PERSISTED_READ_LANG_BY_EXTENSION: Readonly> = { ts: 'ts', tsx: 'tsx', mts: 'ts', cts: 'ts', js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js', json: 'json', jsonc: 'json', @@ -138,16 +139,16 @@ const MASTER_READ_LANG_BY_EXTENSION: Readonly> = { } describe('langFromPath', () => { - it('keeps every pre-unification suffix byte-identical to the old persisted hint', () => { - expect(Object.keys(MASTER_READ_LANG_BY_EXTENSION)).toHaveLength(43) - for (const [extension, hint] of Object.entries(MASTER_READ_LANG_BY_EXTENSION)) { + it('keeps every already-persisted suffix byte-identical', () => { + expect(Object.keys(PERSISTED_READ_LANG_BY_EXTENSION)).toHaveLength(43) + for (const [extension, hint] of Object.entries(PERSISTED_READ_LANG_BY_EXTENSION)) { expect(langFromPath(`file.${extension}`), extension).toBe(hint) } }) - it('gives a suffix added after the old read table the language short id', () => { - // A suffix the old read table did not know used to persist the canonical - // grammar id; it now persists the language's short name. + it('gives every other suffix the language short id', () => { + // A suffix with no persisted value has none to preserve, so the projection + // applies the language's short name instead of the canonical grammar id. expect(langFromPath('build.ps1')).toBe('ps1') expect(langFromPath('deploy.bat')).toBe('bat') expect(langFromPath('.env')).toBe('env') @@ -167,7 +168,7 @@ describe('langFromPath', () => { expect(langFromPath('notebook.ipynb')).toBe('json') }) - it('maps a known extension to its old short hint, case-insensitively', () => { + it('maps a known extension to its persisted short hint, case-insensitively', () => { expect(langFromPath('src/a.ts')).toBe('ts') expect(langFromPath('src/a.TSX')).toBe('tsx') expect(langFromPath('/abs/module.mjs')).toBe('js') @@ -177,7 +178,8 @@ describe('langFromPath', () => { it('keeps the canonical ids the Client code surfaces use out of the persisted hint', () => { // The Client reads the shared table directly; only this projection owns the - // persisted value, so the two intentionally differ for the old suffixes. + // persisted value, so the two intentionally differ for the suffixes a + // recorded session already holds. expect(languageForPath('src/a.ts')).toBe('typescript') expect(languageForPath('README.md')).toBe('markdown') expect(langFromPath('src/a.ts')).not.toBe(languageForPath('src/a.ts')) diff --git a/packages/util/code-language/src/index.ts b/packages/util/code-language/src/index.ts index dd4a37115a..6b16b232f8 100644 --- a/packages/util/code-language/src/index.ts +++ b/packages/util/code-language/src/index.ts @@ -5,9 +5,9 @@ * highlighter's alias table resolves, so a language returned here reaches * `highlightLines`/`highlightToHtml` unchanged; a filename outside the table, or * one naming a language the highlighter does not register, renders as plain text. - * The read card predates the shared table, so {@link readLangHintForPath} projects - * short ids over this table: the suffixes the old read table already recognized - * keep their historical values byte-identical, and every later suffix persists the + * The read tool persists a short `lang` id, so {@link readLangHintForPath} + * projects short ids over this table: a suffix whose value a recorded session + * already holds keeps it byte-identical, and every other suffix persists the * canonical language's short name rather than its grammar id. * @module @deepseek-ai/dsh-util-code-language */ @@ -101,10 +101,10 @@ const LANGUAGES = new Map(Object.entries(LANGUAGE_EXTENSIONS) export const CODE_HIGHLIGHT_EXTENSIONS: readonly string[] = [...LANGUAGES.keys()] /** - * The read card's historical short language hints for the suffixes its - * pre-unification table recognized. Keys are shared-table extensions, so the - * recognized set still derives from {@link LANGUAGE_EXTENSIONS}; values preserve - * the old persisted `lang` strings byte for byte. An extension absent here but + * The short `lang` values recorded sessions already hold, keyed by the suffix + * that produced them. Keys are shared-table extensions, so the recognized set + * derives from {@link LANGUAGE_EXTENSIONS}; each value is the exact string a + * recorded session persists and must not change. An extension absent here but * present in the shared table falls back to {@link SHORT_BY_LANGUAGE}, so every * persisted hint is a short id. */ @@ -123,13 +123,13 @@ const READ_LANG_BY_EXTENSION = new Map(Object.entries({ })) /** - * The short `lang` id the read card persists for each canonical language the - * pre-unification read table did not recognize. Keys are exactly the languages - * in {@link LANGUAGE_EXTENSIONS}: the 27 languages the old read table covered - * repeat their historical short id, and the remaining 33 shorten the grammar id - * (`powershell` to `ps1`, `hcl` to `tf`, `system-verilog` to `sv`). - * Keys are canonical ids, not untrusted extensions, so an object lookup cannot - * reach an `Object.prototype` member the way the extension tables can. + * The short `lang` id the read card persists, keyed by canonical language id. + * Keys are exactly the languages in {@link LANGUAGE_EXTENSIONS}: where + * {@link READ_LANG_BY_EXTENSION} does not already fix a suffix's value, this + * table supplies the language's short name (`powershell` to `ps1`, `hcl` to + * `tf`, `system-verilog` to `sv`) rather than the grammar id. Keys are canonical + * ids, not untrusted extensions, so an object lookup cannot reach an + * `Object.prototype` member the way the extension tables can. */ const SHORT_BY_LANGUAGE: Readonly> = { typescript: 'ts', @@ -218,11 +218,11 @@ export function languageForPath(path: string): string | undefined { /** * Derive the Host read card's persisted `lang` hint from a read path's - * extension. The hint is always a short id: a suffix the pre-unification read - * table recognized keeps its old value (`ts`, `md`, `cpp`, …), while a suffix - * only the shared table knows (`.ps1`, `.env`, `.tf`, …) uses its language's - * short name (`ps1`, `env`, `tf`, …) rather than the canonical grammar id. An - * unrecognized suffix stays `undefined`, so the card renders as plain text. + * extension. The hint is always a short id: a suffix a recorded session already + * holds keeps its persisted value (`ts`, `md`, `cpp`, …), while a suffix with no + * recorded value (`.ps1`, `.env`, `.tf`, …) uses its language's short name + * (`ps1`, `env`, `tf`, …) rather than the canonical grammar id. An unrecognized + * suffix stays `undefined`, so the card renders as plain text. * @param path - the model-facing path the read reported. * @returns the persisted language hint, or `undefined` when the extension maps to none. */ diff --git a/packages/util/code-language/tests/code-language.spec.ts b/packages/util/code-language/tests/code-language.spec.ts index d03b02d719..0c798db20d 100644 --- a/packages/util/code-language/tests/code-language.spec.ts +++ b/packages/util/code-language/tests/code-language.spec.ts @@ -32,7 +32,7 @@ describe('languageForPath', () => { expect(languageForPath(path)).toBe(language) }) - it('keeps the extensions the two former tables already agreed on', () => { + it('resolves the extensions the preview and read tables already agreed on', () => { expect(languageForPath('src/a.ts')).toBe('typescript') expect(languageForPath('src/a.TSX')).toBe('typescript') expect(languageForPath('/abs/module.mjs')).toBe('javascript') @@ -41,7 +41,7 @@ describe('languageForPath', () => { expect(languageForPath('C:\\src\\main.rs')).toBe('rust') }) - it('resolves every extension that only the preview table knew, including the new ones', () => { + it('resolves every suffix the preview registry declares beyond the persisted read hints', () => { for (const extension of ['jsonl', 'ndjson', 'pyw', 'pyi', 'rake', 'gemspec', 'hh', 'hxx', 'kts', 'xhtml', 'xsd', 'xsl', 'xslt']) { expect(languageForPath(`file.${extension}`), extension).toBeDefined() } @@ -91,7 +91,7 @@ describe('languageForPath', () => { }) describe('readLangHintForPath', () => { - it('keeps the read card pre-unification short hints for the old suffixes', () => { + it('keeps the read card short hints recorded sessions already hold', () => { expect(readLangHintForPath('src/a.ts')).toBe('ts') expect(readLangHintForPath('src/a.tsx')).toBe('tsx') expect(readLangHintForPath('src/a.mts')).toBe('ts') @@ -110,7 +110,7 @@ describe('readLangHintForPath', () => { expect(readLangHintForPath('page.htm')).toBe('html') }) - it('uses the language short id for a suffix added after the old read table', () => { + it('uses the language short id for a suffix with no persisted value', () => { expect(readLangHintForPath('build.ps1')).toBe('ps1') expect(readLangHintForPath('.env')).toBe('env') expect(readLangHintForPath('infra.tf')).toBe('tf') @@ -137,11 +137,12 @@ describe('readLangHintForPath', () => { } it('persists a short id for every suffix, never a canonical grammar id', () => { - // The persisted `lang` has one style: the historical short id for the - // suffixes the old read table knew, the language's short name otherwise. A - // result outside both sets is a canonical grammar id leaking back in - // (`powershell`, `dotenv`, `latex`, `hcl`). The old table's `tsx`/`jsx` - // hints are the only legacy values absent from the short-name table. + // The persisted `lang` has one style: a short id. Every suffix must resolve + // to one of these short names, except the persisted `tsx`/`jsx` values, + // which the language table cannot express because typescript and javascript + // already shorten to `ts` and `js`. A result outside both sets is a + // canonical grammar id leaking back in (`powershell`, `dotenv`, `latex`, + // `hcl`). const allowed = new Set([...Object.values(SHORT_BY_LANGUAGE), 'tsx', 'jsx']) for (const extension of CODE_HIGHLIGHT_EXTENSIONS) { const hint = readLangHintForPath(`file.${extension}`)