diff --git a/CHANGELOG.md b/CHANGELOG.md index 349c9ff..4b7b4f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,12 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Any row that names a symbol can start a **flow**: press `Flow ›`, then type a second symbol or press `→ here` on another row, and you get the path between them — so "how does `POST /v1/payroll/cycles/{cycleID}/run` reach the database" is two clicks. Typing into the search box now finds entry points too, under their own heading below the symbol matches, so a URL comes back with its handler attached instead of on its own. +- **Syntax colouring in `codegraph ui` now comes from CodeGraph's own reading of your code.** The viewer used to run a second syntax highlighter over source CodeGraph had already parsed, with its own separate set of grammars. It doesn't any more: the colouring is taken straight from the parse that built your graph, so a file is coloured by exactly the grammar that decided what its symbols are. Three things you will notice — the name a definition declares now stands out on the line that declares it, wherever it appears; calls written inside a string (`${user.name()}`, `#{...}`, `$"{...}"`) are read as code and are now clickable links like every other call site; and built-in type words such as `string`, `int` and `void` look the same in every language instead of one way in Go and another in TypeScript. A big file paints far faster, most visibly in TypeScript, which was by a wide margin the slowest before. + + Two formats change for the worse and it is worth saying so: Liquid, Razor, YAML, Twig, XML and `.properties` files are shown without colouring now, and in `.svelte`, `.vue` and `.astro` files the `\n', 'svelte'); + expect(regions).toHaveLength(1); + expect(regions?.[0]?.language).toBe('typescript'); }); - withGrammars('loads a grammar chain dependencies-first, so embedded blocks highlight', () => { - const found = loadManifest(); - const vue = (found as NonNullable).manifest.languages['vue'] ?? []; - // The single-file component's own grammar is last; everything it embeds - // has to be registered before Shiki resolves `embeddedLangs`. - expect(vue[vue.length - 1]).toBe('vue'); - expect(vue).toContain('typescript'); - expect(vue.indexOf('typescript')).toBeLessThan(vue.length - 1); + it('has no grammar for the formats that only have file-level extraction', () => { + for (const language of ['yaml', 'xml', 'properties', 'twig', 'unknown']) { + expect(grammarFor(language)).toBeNull(); + } }); }); describe('classification', () => { beforeAll(() => clearHighlightCache()); - withGrammars('reads TypeScript with the four classes the theme paints', async () => { + it('reads TypeScript with the classes the theme paints', async () => { const result = await highlightLines(['const answer = 42; // note'], { language: 'typescript', }); - expect(result.engine).toBe('shiki'); + expect(result.engine).toBe('tree-sitter'); expect(result.grammar).toBe('typescript'); expect(result.classes).toEqual([...TOKEN_CLASSES]); const rendered = shape(result, 0); @@ -123,7 +119,7 @@ describe('classification', () => { expect(rendered).toContain('comment:// note'); }); - withGrammars('reads a # comment as a comment in Python and as code in TypeScript', async () => { + it('reads a # comment as a comment in Python and as code in TypeScript', async () => { const python = await highlightLines(['x = 1 # note'], { language: 'python' }); expect(shape(python, 0).at(-1)).toBe('comment:# note'); @@ -131,7 +127,7 @@ describe('classification', () => { expect(shape(ts, 0).at(-1)).not.toBe('comment:# note'); }); - withGrammars('carries a block comment across lines within one slice', async () => { + it('carries a block comment across lines within one slice', async () => { const result = await highlightLines(['/* open', 'still comment', 'done */ const x = 1;'], { language: 'typescript', }); @@ -140,21 +136,73 @@ describe('classification', () => { expect(shape(result, 2)).toContain('keyword:const'); }); - withGrammars('reads Go, which has its own idea of what a keyword is', async () => { + it('reads Go, which has its own idea of what a keyword is', async () => { const result = await highlightLines(['func Greet(name string) string {'], { language: 'go' }); expect(shape(result, 0)).toContain('keyword:func'); - expect(shape(result, 0)).toContain('ident:Greet'); + expect(shape(result, 0)).toContain('def:Greet'); }); - withGrammars('reads ArkTS with the TypeScript grammar', async () => { + it('reads ArkTS with its own grammar, not TypeScript’s', async () => { const result = await highlightLines(['@Entry struct Index { build() {} }'], { language: 'arkts', }); - expect(result.engine).toBe('shiki'); - expect(result.grammar).toBe('typescript'); + expect(result.engine).toBe('tree-sitter'); + expect(result.grammar).toBe('arkts'); }); - withGrammars('emits one entry per source line, always', async () => { + it('does not read a type annotation’s `string` as a string literal', async () => { + // An anonymous tree-sitter node's type IS its text, so `string` in a + // signature arrives as a node literally typed `string`. Reading that as a + // string literal greys out half of every signature in TypeScript and PHP. + for (const [language, line] of [ + ['typescript', 'function put(key: string): void {}'], + ['php', ' { + // The grammars disagree: `string` is a `type_identifier` in Go and an + // anonymous token inside a `predefined_type` in TypeScript. Left alone that + // is one word painting two ways on the same screen. + for (const [language, line] of [ + ['typescript', 'let a: string;'], + ['go', 'var a string'], + ['csharp', 'string a;'], + ['rust', 'let a: u32 = 1;'], + ] as const) { + const rendered = shape(await highlightLines([line], { language }), 0); + expect(rendered.some((t) => t.startsWith('type:'))).toBe(true); + expect(rendered.some((t) => t === 'keyword:string' || t === 'keyword:u32')).toBe(false); + } + }); + + it('keeps a template literal’s interpolated call as code, so it can link', async () => { + const line = 'const s = `n=${store.size()} done`;'; + const result = await highlightLines([line], { language: 'typescript' }); + expect(shape(result, 0)).toContain('ident:size'); + expect(claimedText(result, 0, lineRef({ ident: 'size' }))).toBe('size'); + }); + + it('marks a definition’s own name, from the extractor’s tables', async () => { + const cases: [string, string, string][] = [ + ['typescript', 'export class Store {}', 'Store'], + ['python', 'def put(self):', 'put'], + ['rust', 'pub fn put(&self) {}', 'put'], + ['ruby', 'class Store', 'Store'], + ['csharp', 'public class Store {}', 'Store'], + ['swift', 'final class Store {}', 'Store'], + ]; + for (const [language, line, name] of cases) { + const result = await highlightLines([line], { language }); + expect(shape(result, 0)).toContain(`def:${name}`); + } + }); + + it('emits one entry per source line, always', async () => { const lines = ['a();', '', 'b();', '']; const result = await highlightLines(lines, { language: 'typescript' }); // The code block indexes rows positionally: one short answer and every @@ -162,6 +210,34 @@ describe('classification', () => { expect(result.lines).toHaveLength(lines.length); expect(result.lines[1]).toEqual([]); }); + + it('reproduces every line of a real file exactly', async () => { + // The code block renders these tokens and nothing else, so a dropped or + // duplicated character is a corrupted file on screen — silently. + const file = path.join(__dirname, '..', 'src', 'ui-server', 'api', 'source.ts'); + const lines = fs.readFileSync(file, 'utf-8').split('\n'); + const result = await highlightLines(lines, { language: 'typescript' }); + expect(result.engine).toBe('tree-sitter'); + result.lines.forEach((row, i) => { + expect(row.map(([, text]) => text).join('')).toBe(lines[i]); + }); + }); + + it('classifies a component’s script and leaves its markup plain', async () => { + const lines = [ + '', + '', + '', + ]; + const result = await highlightLines(lines, { language: 'svelte' }); + expect(result.engine).toBe('tree-sitter'); + expect(shape(result, 1)).toContain('keyword:let'); + // The markup still splits into identifiers, so a call site in it links. + expect(claimedText(result, 4, lineRef({ ident: 'bump' }))).toBe('bump'); + expect(result.lines.map((row) => row.map(([, t]) => t).join(''))).toEqual(lines); + }); }); describe('the plain fallback', () => { @@ -182,7 +258,7 @@ describe('the plain fallback', () => { expect(claimedText(result, 0, lineRef({ ident: 'withLock', col: 9 }))).toBe('withLock'); }); - it('refuses to tokenise a minified line rather than wedging on it', async () => { + it('refuses to classify a minified line rather than wedging on it', async () => { const enormous = 'a'.repeat(MAX_HIGHLIGHT_CHARS + 1); const result = await highlightLines([enormous], { language: 'javascript' }); expect(result.engine).toBe('plain'); @@ -191,41 +267,17 @@ describe('the plain fallback', () => { expect(result.lines[0]?.map(([, text]) => text).join('')).toHaveLength(enormous.length); }); - it('answers plain when a shipped grammar file is missing or unreadable', async () => { - // The install is half there: a manifest that names a grammar whose file - // never made it. The viewer must still get its source. - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-textmate-')); - fs.writeFileSync( - path.join(dir, 'manifest.json'), - JSON.stringify({ shikiVersion: 'test', languages: { typescript: ['typescript'] } }) - ); - - const previous = process.env.CODEGRAPH_TEXTMATE_PATH; - process.env.CODEGRAPH_TEXTMATE_PATH = dir; - // A fresh module registry: the highlighter and its grammar bookkeeping are - // created once per process, and this case is about that first attempt. - vi.resetModules(); - try { - const mod = await import('../src/ui-server/highlight'); - const result: HighlightResult = await mod.highlightLines(['const x = 1;'], { - language: 'typescript', - }); - expect(result.engine).toBe('plain'); - expect(result.reason).toBeTruthy(); - expect(result.lines[0]?.map(([, text]) => text).join('')).toBe('const x = 1;'); - } finally { - if (previous === undefined) delete process.env.CODEGRAPH_TEXTMATE_PATH; - else process.env.CODEGRAPH_TEXTMATE_PATH = previous; - fs.rmSync(dir, { recursive: true, force: true }); - vi.resetModules(); - } + it('answers plain for a component whose script block is empty', async () => { + const result = await highlightLines(['

hello

'], { language: 'svelte' }); + expect(result.engine).toBe('plain'); + expect(result.lines[0]?.map(([, text]) => text).join('')).toBe('

hello

'); }); }); describe('graph links land on the right token', () => { beforeAll(() => clearHighlightCache()); - withGrammars('marks the callee, not the receiver the recorded column points at', async () => { + it('marks the callee, not the receiver the recorded column points at', async () => { // The recorded column is the start of the calling EXPRESSION — `this` — // and the underline has to end up on `withLock`. const line = ' return this.indexMutex.withLock(async () => {'; @@ -235,7 +287,7 @@ describe('graph links land on the right token', () => { ); }); - withGrammars('lands on a real call site in the engine’s own src/index.ts', async () => { + it('lands on a real call site in the engine’s own src/index.ts', async () => { const file = path.join(__dirname, '..', 'src', 'index.ts'); const source = fs.readFileSync(file, 'utf-8').split('\n'); // A line the engine actually contains, found rather than hard-coded, so a @@ -252,7 +304,7 @@ describe('graph links land on the right token', () => { ); }); - withGrammars('lands on a Go method call', async () => { + it('lands on a Go method call', async () => { const line = '\tresult := s.repo.FindByID(ctx, id)'; const result = await highlightLines([line], { language: 'go' }); expect(claimedText(result, 0, lineRef({ ident: 'FindByID', col: line.indexOf('s.repo') }))).toBe( @@ -260,7 +312,7 @@ describe('graph links land on the right token', () => { ); }); - withGrammars('lands on a Python method call, not on the receiver of the same name', async () => { + it('lands on a Python method call, not on the receiver of the same name', async () => { const line = ' return self.store.join(self.store.path)'; const result = await highlightLines([line], { language: 'python' }); expect(claimedText(result, 0, lineRef({ ident: 'join', col: line.indexOf('self') }))).toBe( @@ -268,7 +320,7 @@ describe('graph links land on the right token', () => { ); }); - withGrammars('leaves a word inside a comment or a string alone', async () => { + it('leaves a word inside a comment or a string alone', async () => { const result = await highlightLines( [' // call render here', ' const s = "render";'], { language: 'typescript' } @@ -277,7 +329,7 @@ describe('graph links land on the right token', () => { expect(claimedText(result, 1, lineRef({ ident: 'render' }))).toBeUndefined(); }); - withGrammars('keeps every identifier separately claimable', async () => { + it('keeps every identifier separately claimable', async () => { const result = await highlightLines(['render(); render();'], { language: 'typescript' }); const tokens = tokensOf(result, 0); const claimed = assignRefs(tokens, [ @@ -287,7 +339,13 @@ describe('graph links land on the right token', () => { expect(claimed.size).toBe(2); }); - withGrammars('reproduces the line exactly — the code block renders these tokens', async () => { + it('keeps a type name claimable — it is a distinct class, not an excluded one', async () => { + const result = await highlightLines(['let store: Store = make();'], { language: 'typescript' }); + expect(shape(result, 0)).toContain('type:Store'); + expect(claimedText(result, 0, lineRef({ ident: 'Store' }))).toBe('Store'); + }); + + it('reproduces the line exactly — the code block renders these tokens', async () => { const line = ' const s = `a ${b.c()} d`; // 1 + 2'; const result = await highlightLines([line], { language: 'typescript' }); expect( @@ -299,7 +357,27 @@ describe('graph links land on the right token', () => { }); describe('cost', () => { - withGrammars('answers a cached slice without re-tokenising it', async () => { + it('classifies three thousand lines of TypeScript well inside the budget', async () => { + clearHighlightCache(); + const lines = fs + .readFileSync(path.join(__dirname, '..', 'src', 'extraction', 'tree-sitter.ts'), 'utf-8') + .split('\n') + .slice(0, 3000); + // Warm the grammar load, which is a one-off per language per process. + await highlightLines(lines.slice(0, 5), { language: 'typescript' }); + clearHighlightCache(); + + const started = Date.now(); + const result = await highlightLines(lines, { language: 'typescript' }); + const elapsed = Date.now() - started; + + expect(result.engine).toBe('tree-sitter'); + // The whole point of CG-57's swap: the TextMate grammar took ~700 ms here. + // Generous against a loaded CI box; the dev Mac measures 24–41 ms. + expect(elapsed).toBeLessThan(400); + }); + + it('answers a cached slice without re-classifying it', async () => { clearHighlightCache(); const lines = fs .readFileSync(path.join(__dirname, '..', 'src', 'ui-server', 'api', 'source.ts'), 'utf-8') @@ -313,7 +391,7 @@ describe('cost', () => { const second = await highlightLines(lines, { language: 'typescript', cacheKey: 'a:1:9999' }); const warmMs = Date.now() - warm; - expect(second.engine).toBe('shiki'); + expect(second.engine).toBe('tree-sitter'); // The cache is what makes a re-render free: every resize, theme flip and // step back through the trail re-asks for the same slice. expect(warmMs).toBeLessThan(Math.max(20, coldMs / 4)); @@ -333,7 +411,7 @@ describe('cost', () => { expect(stats.lines).toBeLessThanOrEqual(SLICE_CACHE_LINES); }); - withGrammars('keys the cache on the content, so an edited file re-highlights', async () => { + it('keys the cache on the content, so an edited file re-classifies', async () => { clearHighlightCache(); const first = await highlightLines(['const a = 1;'], { language: 'typescript', @@ -347,3 +425,34 @@ describe('cost', () => { expect(second.lines[0]?.map(([, t]) => t).join('')).toBe('const bbb = 2;'); }); }); + +describe('the classifier itself', () => { + it('covers the source with ordered, non-overlapping spans', async () => { + const source = fs + .readFileSync(path.join(__dirname, '..', 'src', 'ui-server', 'api', 'flow.ts'), 'utf-8') + .slice(0, 40_000); + await initGrammars(); + await loadGrammarsForLanguages(['typescript']); + const parser = getParser('typescript'); + expect(parser).not.toBeNull(); + const tree = (parser as NonNullable).parse(source); + const spans = classifyTree((tree as NonNullable).rootNode, source, 'typescript'); + + expect(spans.length).toBeGreaterThan(1000); + let previous = 0; + for (const span of spans) { + expect(span.start).toBeGreaterThanOrEqual(previous); + expect(span.end).toBeGreaterThan(span.start); + previous = span.end; + } + expect(previous).toBeLessThanOrEqual(source.length); + // Everything the walk did not claim is whitespace the caller fills in. + const uncovered: string[] = []; + let at = 0; + for (const span of spans) { + if (span.start > at) uncovered.push(source.slice(at, span.start)); + at = span.end; + } + expect(uncovered.every((gap) => gap.trim() === '')).toBe(true); + }); +}); diff --git a/__tests__/ui-server-api.test.ts b/__tests__/ui-server-api.test.ts index 182742c..0d73906 100644 --- a/__tests__/ui-server-api.test.ts +++ b/__tests__/ui-server-api.test.ts @@ -671,6 +671,8 @@ describe('GET /api/source', () => { 'string', 'keyword', 'number', + 'type', + 'def', ]); expect(body.highlight.lines).toHaveLength(body.lines.length); // Every line's tokens reproduce that line exactly — the code block renders diff --git a/__tests__/ui-symbol-model.test.ts b/__tests__/ui-symbol-model.test.ts index d96e7c8..b97d111 100644 --- a/__tests__/ui-symbol-model.test.ts +++ b/__tests__/ui-symbol-model.test.ts @@ -484,7 +484,7 @@ describe('showsBody', () => { describe('client-side token decoding', () => { // The classification itself is the server's job (`src/ui-server/highlight/`, - // real TextMate grammars); what is worth pinning here is the decoding — the + // the engine's own tree-sitter parse); what is worth pinning here is the decoding — the // columns the call-site overlay matches against, and the plain fallback that // has to keep links working when no grammar covers a file. const CLASSES = ['other', 'ident', 'comment', 'string', 'keyword', 'number']; @@ -552,7 +552,7 @@ describe('client-side token decoding', () => { it('keys a slice by real file line, not by offset into the slice', () => { const byLine = tokensByLine(['a();', 'b();'], 120, { - engine: 'shiki', + engine: 'tree-sitter', grammar: 'typescript', classes: CLASSES, lines: [ diff --git a/docs/design/cg57-highlighting-parity.md b/docs/design/cg57-highlighting-parity.md new file mode 100644 index 0000000..a8f4174 --- /dev/null +++ b/docs/design/cg57-highlighting-parity.md @@ -0,0 +1,95 @@ +# Highlighting parity: Shiki → the engine's own tree-sitter parse (CG-57) + +The viewer's code block used to be classified by a second highlighter — Shiki with 56 pruned +TextMate grammars shipped in `dist/textmate/` — over source the engine had already parsed with a +real grammar. CG-57 takes the classification off that tree instead. This file records what the swap +changed, measured rather than asserted, so nobody has to re-derive it from a diff. + +Screenshots, one per language, before on the left and after on the right, same stylesheet: +[`cg57-highlighting-parity/`](./cg57-highlighting-parity/) — `typescript.png`, `go.png`, +`python.png`, `rust.png`, `swift.png`, `csharp.png`, `ruby.png`, `php.png`. + +## What it costs + +3 000 lines, cold, dev Mac (M-series), parse + classify + wire: + +| | TypeScript | Go | Python | Rust | Swift | C# | Ruby | PHP | +|---|---|---|---|---|---|---|---|---| +| Shiki + TextMate | ~700 ms | 43–57 ms | 35–47 ms | — | — | — | — | — | +| Engine tree-sitter | 24–41 ms | ~30 ms | 25–29 ms | 18–19 ms | 25–27 ms | 20–25 ms | 14–16 ms | 20–22 ms | + +The task's budget was **< 100 ms per 3 000-line file warm**; every language clears it *cold*. +TypeScript is the number that mattered: its TextMate grammar was 5–7× every other one and the cost +was regex *execution*, not compilation, so nothing about the old module could have fixed it. The +slice cache still exists — a re-render (resize, theme flip, stepping back through the trail) should +cost nothing at all, and the whole-file view pages the same file repeatedly. + +## What it changes on screen + +Per-character comparison over ~40 lines of realistic source per language, counting only +non-whitespace characters, and treating `ident` / `other` / `type` as one bucket because all three +paint at plain ink: + +| language | painted identically | what moved | +|---|---|---| +| TypeScript | 91.3% | 33 `def`, 40 interpolation chars now code, 2 punctuation | +| Go | 91.5% | 37 built-in type words, 13 `def` | +| Python | 93.2% | 26 `def`, 15 keyword (`is not`, `__future__`) | +| Rust | 96.3% | 21 `def`, 3 keyword | +| Swift | 93.3% | 18 `def`, 14 keyword (`throws`/`rethrows`) | +| C# | 88.3% | 30 built-in type words, 23 `def`, 31 interpolation chars now code | +| Ruby | 83.8% | 23 `def`, 35 interpolation chars now code, 14 symbol literals, 3 keyword | +| PHP | 85.9% | 33 built-in type words, 29 `def`, 15 phpdoc tag chars, 12 keyword | + +Every remaining difference is one of five deliberate categories: + +1. **`ident` → `def`.** The definition's own name now carries weight 600, everywhere rather than + only on the line the Symbol view opened at. It comes from the extractors' own definition tables + (`functionTypes`, `classTypes`, `methodTypes`, …) plus each language's `nameField`, so it cannot + drift from what indexing considers a definition. +2. **`string` → code, inside an interpolation.** A template literal's `${…}`, an f-string's `{…}`, + Ruby's `#{…}` and C#'s `$"{…}"` are classified as code. This is the one difference that is not + cosmetic: the call-site overlay deliberately refuses to claim a token classed `string`, so + **calls inside interpolated strings now link and did not before.** +3. **`keyword` → `type`, on built-in type words.** `string`, `int`, `u32`, `void`. The grammars + disagree with each other about what a built-in type is — tree-sitter-go calls `string` a + `type_identifier`, tree-sitter-typescript wraps it in a `predefined_type` whose child is an + anonymous token spelled `string` — and TextMate scoped them inconsistently too (plain in + TypeScript, `storage.type` in Go). They now all paint at plain ink, like a user-defined type + name, in every language. +4. **Keyword-set corrections.** Python's `is not`, Rust's and Swift's modifiers, and Ruby's `new` + (which is a method, not a keyword — TextMate's `keyword.operator.new` matched it anyway). +5. **`keyword` → `comment`, on phpdoc tags.** `@var` and friends recede with the comment they are + in, which is what the near-monochrome ramp asks for. + +## What is no longer highlighted + +Nine formats have extraction but no tree-sitter grammar. Three of them — `.svelte`, `.vue`, +`.astro` — are classified through their `'.length; + regions.push({ + start, + end: start + body.length, + language: TS_LANG_ATTR.test(match[1] ?? '') ? 'typescript' : 'javascript', + }); + } + return regions; +} + +/* --------------------------------------------------------------- the API -- */ + +export interface TokenizeResult { + spans: SyntaxSpan[]; + /** The grammar(s) that produced them, for the payload's `grammar` field. */ + grammars: string[]; +} + +/** + * Parse `source` and classify it. + * + * Returns null when nothing in the file has a grammar — a plain answer, which + * every caller here already knows how to serve. Never throws: a grammar that + * fails to load or a parse that comes back empty is the same outcome as not + * having one. + */ +export async function tokenizeSource( + source: string, + language: Language +): Promise { + const regions = syntaxRegionsFor(source, language); + if (regions === null) { + const spans = await tokenizeRegion(source, language, 0); + return spans ? { spans, grammars: [language] } : null; + } + if (regions.length === 0) return null; + + const spans: SyntaxSpan[] = []; + const grammars = new Set(); + for (const region of regions) { + const part = await tokenizeRegion( + source.slice(region.start, region.end), + region.language, + region.start + ); + if (!part) continue; + grammars.add(region.language); + spans.push(...part); + } + if (spans.length === 0) return null; + spans.sort((a, b) => a.start - b.start); + return { spans, grammars: [...grammars] }; +} + +async function tokenizeRegion( + source: string, + language: Language, + offset: number +): Promise { + try { + await loadGrammarsForLanguages([language]); + const parser = getParser(language); + if (!parser) return null; + const tree = parser.parse(source); + if (!tree?.rootNode) return null; + try { + return classifyTree(tree.rootNode, source, language, offset); + } finally { + tree.delete(); + } + } catch { + // A grammar that will not load, or a parse that threw: the caller serves + // the source unclassified, which is the whole point of the plain path. + return null; + } +} diff --git a/src/ui-server/api/filecode.ts b/src/ui-server/api/filecode.ts index 8c51551..efb29e3 100644 --- a/src/ui-server/api/filecode.ts +++ b/src/ui-server/api/filecode.ts @@ -18,7 +18,7 @@ * before a single page of source has arrived. * * The source itself does NOT ride along. A 6 800-line TypeScript file is ~1.5 s - * of TextMate tokenising and megabytes of JSON; the viewer pages it through + * of parsing and megabytes of JSON; the viewer pages it through * `/api/source` as the reader scrolls, which is also what lets the graph * facts — ports, arcs, rail rows — be complete from the first frame while the * text fills in behind them. diff --git a/src/ui-server/highlight/grammars.ts b/src/ui-server/highlight/grammars.ts deleted file mode 100644 index a8d2d08..0000000 --- a/src/ui-server/highlight/grammars.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Finding and reading the pruned TextMate grammars on disk. - * - * Shiki ships 700-odd grammars; the engine indexes 40-odd languages. The build - * writes only the closure those 40 need — including the grammars they embed, so - * a `.vue` file still gets its `