diff --git a/.gitignore b/.gitignore index 24d132ebd7..c136a37d85 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules *.vsix extensions/vscode/out +extensions/vscode/tests/embeddedGrammars/*.tmLanguage.json packages/*/*.d.ts packages/*/*.js packages/*/*.map diff --git a/extensions/vscode/scripts/grammars-sync.ts b/extensions/vscode/scripts/grammars-sync.ts new file mode 100644 index 0000000000..41fed8c49a --- /dev/null +++ b/extensions/vscode/scripts/grammars-sync.ts @@ -0,0 +1,119 @@ +import { createHash } from 'node:crypto'; +import * as fs from 'node:fs'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +interface LockItem { + file: string; + url: string; + checksum?: string; +} + +const CHECKSUM_PREFIX = 'sha256-'; + +function computeChecksum(content: string | Buffer): string { + const buffer = typeof content === 'string' ? Buffer.from(content) : content; + return CHECKSUM_PREFIX + createHash('sha256').update(buffer).digest('hex'); +} + +async function fetchWithChecksum(url: string) { + console.log(`Downloading ${url}...`); + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`); + } + const content = await res.text(); + return { content, checksum: computeChecksum(content) }; +} + +async function fileChecksum(filePath: string): Promise { + if (!fs.existsSync(filePath)) { + return null; + } + const content = await readFile(filePath); + return computeChecksum(content); +} + +async function safeWriteFile(filePath: string, content: string | Buffer) { + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content); +} + +async function processItem(dirPath: string, item: LockItem, update: boolean): Promise { + if (!item.file || !item.url) { + throw new Error('Lock item must include "file" and "url".'); + } + + const filePath = path.resolve(dirPath, item.file); + + if (update || !item.checksum) { + const { content, checksum } = await fetchWithChecksum(item.url); + await safeWriteFile(filePath, content); + const changed = item.checksum !== checksum; + item.checksum = checksum; + return changed || update; + } + + const expected = item.checksum; + const localChecksum = await fileChecksum(filePath); + if (localChecksum === expected) { + return false; + } + + const { content, checksum } = await fetchWithChecksum(item.url); + if (checksum !== expected) { + throw new Error( + `Checksum mismatch for ${item.file}. Expected ${expected}, got ${checksum}. +Please run "pnpm test:prepare -u" to update the lock hash.`, + ); + } + + if (checksum !== localChecksum) { + await safeWriteFile(filePath, content); + } + + return false; +} + +const dir = path.dirname(fileURLToPath(import.meta.url)); +const dirPath = path.resolve(dir, '../tests/embeddedGrammars'); +const lockPath = path.resolve(dirPath, '_lock.json'); +const update = !fs.existsSync(lockPath); +const lock: LockItem[] = update + ? [ + { + 'file': 'css.tmLanguage.json', + 'url': 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/css/syntaxes/css.tmLanguage.json', + 'checksum': 'sha256-bcc97d1a3a6bf112f72d8bdb58bc438eb68aa0e070b94d00c6064b75f5cab69b', + }, + { + 'file': 'html.tmLanguage.json', + 'url': 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/html/syntaxes/html.tmLanguage.json', + 'checksum': 'sha256-80dedf4fb27e88889ac8fb72763954a6d2660502c686f4415208d8c8d00352cd', + }, + { + 'file': 'javascript.tmLanguage.json', + 'url': + 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/javascript/syntaxes/JavaScript.tmLanguage.json', + 'checksum': 'sha256-db6f17f15bc4f5e860a3b8fa6055a69720a53df845c8d5121cdc4f128c16291f', + }, + { + 'file': 'scss.tmLanguage.json', + 'url': 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/scss/syntaxes/scss.tmLanguage.json', + 'checksum': 'sha256-8f2824a80a7c6fd558fc538ec52d0a7a42a4d7ecb7ddf20d79f0d1f00fa6602b', + }, + { + 'file': 'typescript.tmLanguage.json', + 'url': + 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json', + 'checksum': 'sha256-4e92e0d7de560217d6c8d3236d85e6e17a5d77825b15729a230c761743122661', + }, + ] + : JSON.parse(await readFile(lockPath, 'utf8')); +if (!Array.isArray(lock)) { + throw new Error('_lock.json must contain an array of lock items.'); +} +await Promise.all(lock.map(item => processItem(dirPath, item, update))); +await mkdir(dirPath, { recursive: true }); +await writeFile(lockPath, JSON.stringify(lock, null, '\t') + '\n', 'utf8'); diff --git a/extensions/vscode/tests/__snapshots__/grammar.spec.ts.snap b/extensions/vscode/tests/__snapshots__/grammar.spec.ts.snap index 2d1c54552b..d1c35cff9f 100644 --- a/extensions/vscode/tests/__snapshots__/grammar.spec.ts.snap +++ b/extensions/vscode/tests/__snapshots__/grammar.spec.ts.snap @@ -1,5 +1,807 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`embedded grammar > #2060-ts-type-semicolon.vue 1`] = ` +"> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + +exports[`embedded grammar > #2060-type-without-semicolon.vue 1`] = ` +"> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + +exports[`embedded grammar > #3999-multi-line-setup.vue 1`] = ` +"> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^ text.html.vue entity.name.tag.style.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^ text.html.vue entity.name.tag.style.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + +exports[`embedded grammar > #4741-leading-operator.vue 1`] = ` +"> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + +exports[`embedded grammar > basic.vue 1`] = ` +"> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^^^ text.html.vue entity.name.tag.template.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^ text.html.vue entity.name.tag.style.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^ text.html.vue entity.name.tag.style.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + +exports[`embedded grammar > leading-operator.vue 1`] = ` +"> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + +exports[`embedded grammar > multi-line-setup.vue 1`] = ` +"> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^ text.html.vue entity.name.tag.style.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue +> +#^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^ text.html.vue entity.name.tag.style.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + +exports[`embedded grammar > single-line-setup.vue 1`] = ` +"> +#^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue meta.tag-stuff +# ^^^^^ text.html.vue meta.tag-stuff meta.attribute.unrecognized.setup.html entity.other.attribute-name.html +# ^ text.html.vue meta.tag-stuff +# ^^^^ text.html.vue meta.tag-stuff meta.attribute.lang.html entity.other.attribute-name.html +# ^ text.html.vue meta.tag-stuff meta.attribute.lang.html punctuation.separator.key-value.html +# ^ text.html.vue meta.tag-stuff meta.attribute.lang.html string.quoted.double.html punctuation.definition.string.begin.html +# ^^ text.html.vue meta.tag-stuff meta.attribute.lang.html string.quoted.double.html +# ^ text.html.vue meta.tag-stuff meta.attribute.lang.html string.quoted.double.html punctuation.definition.string.end.html +# ^ text.html.vue meta.tag-stuff punctuation.definition.tag.end.html.vue +# ^^^^^ text.html.vue source.ts meta.var.expr.ts storage.type.ts +# ^ text.html.vue source.ts meta.var.expr.ts +# ^ text.html.vue source.ts meta.var.expr.ts meta.var-single-variable.expr.ts meta.definition.variable.ts variable.other.constant.ts +# ^ text.html.vue source.ts meta.var.expr.ts meta.var-single-variable.expr.ts +# ^ text.html.vue source.ts meta.var.expr.ts keyword.operator.assignment.ts +# ^ text.html.vue source.ts meta.var.expr.ts +# ^ text.html.vue source.ts meta.var.expr.ts constant.numeric.decimal.ts +# ^ text.html.vue source.ts punctuation.terminator.statement.ts +# ^^ text.html.vue punctuation.definition.tag.begin.html.vue +# ^^^^^^ text.html.vue entity.name.tag.script.html.vue +# ^ text.html.vue punctuation.definition.tag.end.html.vue +> +#^ text.html.vue" +`; + exports[`grammar > basic.vue 1`] = ` ">
#^ text.html.vue punctuation.definition.tag.begin.html.vue diff --git a/extensions/vscode/tests/embeddedGrammarFixtures/#2060-ts-type-semicolon.vue b/extensions/vscode/tests/embeddedGrammarFixtures/#2060-ts-type-semicolon.vue new file mode 100644 index 0000000000..bcff9e0040 --- /dev/null +++ b/extensions/vscode/tests/embeddedGrammarFixtures/#2060-ts-type-semicolon.vue @@ -0,0 +1,7 @@ + + + diff --git a/extensions/vscode/tests/embeddedGrammarFixtures/#3999-multi-line-setup.vue b/extensions/vscode/tests/embeddedGrammarFixtures/#3999-multi-line-setup.vue new file mode 100644 index 0000000000..0bbc503677 --- /dev/null +++ b/extensions/vscode/tests/embeddedGrammarFixtures/#3999-multi-line-setup.vue @@ -0,0 +1,27 @@ + + + + + + + diff --git a/extensions/vscode/tests/grammarFixtures/leading-operator.vue b/extensions/vscode/tests/embeddedGrammarFixtures/#4741-leading-operator.vue similarity index 93% rename from extensions/vscode/tests/grammarFixtures/leading-operator.vue rename to extensions/vscode/tests/embeddedGrammarFixtures/#4741-leading-operator.vue index af5c2c1bb9..c03a5c37bc 100644 --- a/extensions/vscode/tests/grammarFixtures/leading-operator.vue +++ b/extensions/vscode/tests/embeddedGrammarFixtures/#4741-leading-operator.vue @@ -2,18 +2,12 @@ if (false < true ) { } -( -
-); + + + + + + + + diff --git a/extensions/vscode/tests/embeddedGrammarFixtures/single-line-setup.vue b/extensions/vscode/tests/embeddedGrammarFixtures/single-line-setup.vue new file mode 100644 index 0000000000..a6165d686e --- /dev/null +++ b/extensions/vscode/tests/embeddedGrammarFixtures/single-line-setup.vue @@ -0,0 +1 @@ + diff --git a/extensions/vscode/tests/embeddedGrammars/_lock.json b/extensions/vscode/tests/embeddedGrammars/_lock.json new file mode 100644 index 0000000000..31e5855253 --- /dev/null +++ b/extensions/vscode/tests/embeddedGrammars/_lock.json @@ -0,0 +1,27 @@ +[ + { + "file": "css.tmLanguage.json", + "url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/css/syntaxes/css.tmLanguage.json", + "checksum": "sha256-bcc97d1a3a6bf112f72d8bdb58bc438eb68aa0e070b94d00c6064b75f5cab69b" + }, + { + "file": "html.tmLanguage.json", + "url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/html/syntaxes/html.tmLanguage.json", + "checksum": "sha256-80dedf4fb27e88889ac8fb72763954a6d2660502c686f4415208d8c8d00352cd" + }, + { + "file": "javascript.tmLanguage.json", + "url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/javascript/syntaxes/JavaScript.tmLanguage.json", + "checksum": "sha256-db6f17f15bc4f5e860a3b8fa6055a69720a53df845c8d5121cdc4f128c16291f" + }, + { + "file": "scss.tmLanguage.json", + "url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/scss/syntaxes/scss.tmLanguage.json", + "checksum": "sha256-8f2824a80a7c6fd558fc538ec52d0a7a42a4d7ecb7ddf20d79f0d1f00fa6602b" + }, + { + "file": "typescript.tmLanguage.json", + "url": "https://raw.githubusercontent.com/microsoft/vscode/main/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json", + "checksum": "sha256-4e92e0d7de560217d6c8d3236d85e6e17a5d77825b15729a230c761743122661" + } +] diff --git a/extensions/vscode/tests/grammar.spec.ts b/extensions/vscode/tests/grammar.spec.ts index e88e4a2659..70608334ba 100644 --- a/extensions/vscode/tests/grammar.spec.ts +++ b/extensions/vscode/tests/grammar.spec.ts @@ -3,10 +3,13 @@ import * as path from 'node:path'; import { describe, expect, it } from 'vitest'; import { createGrammarSnapshot } from 'vscode-tmlanguage-snapshot'; +const grammarsSync = import('../scripts/grammars-sync'); const fixturesDir = path.resolve(__dirname, './grammarFixtures'); +const embeddedFixturesDir = path.resolve(__dirname, './embeddedGrammarFixtures'); const packageJsonPath = path.resolve(__dirname, '../package.json'); describe('grammar', async () => { + await grammarsSync; const snapshot = await createGrammarSnapshot(packageJsonPath); const fixtures = fs.readdirSync(fixturesDir); @@ -18,3 +21,26 @@ describe('grammar', async () => { }); } }); + +describe('embedded grammar', async () => { + await grammarsSync; + const embeddedGrammarsDir = path.resolve(__dirname, './embeddedGrammars'); + const snapshot = await createGrammarSnapshot(packageJsonPath, { + extraGrammarPaths: [ + path.resolve(embeddedGrammarsDir, './typescript.tmLanguage.json'), + path.resolve(embeddedGrammarsDir, './javascript.tmLanguage.json'), + path.resolve(embeddedGrammarsDir, './css.tmLanguage.json'), + path.resolve(embeddedGrammarsDir, './scss.tmLanguage.json'), + path.resolve(embeddedGrammarsDir, './html.tmLanguage.json'), + ], + }); + const fixtures = fs.readdirSync(embeddedFixturesDir); + + for (const fixture of fixtures) { + it(fixture, async () => { + const result = await snapshot(`tests/embeddedGrammarFixtures/${fixture}`); + + expect(result).toMatchSnapshot(); + }); + } +}); diff --git a/package.json b/package.json index 81714620b3..277136436e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build": "tsgo -b", "watch": "tsgo -b -w", "test": "npm run build && vitest run", - "test:update": "npm run build && vitest run --update", + "test:grammar": "vitest run extensions/vscode/tests/grammar.spec.ts", "format": "dprint fmt", "lint": "tsslint --project {tsconfig.json,packages/*/tsconfig.json,extensions/*/tsconfig.json}", "lint:fix": "npm run lint -- --fix" diff --git a/vitest.config.ts b/vitest.config.ts index 54b31ab673..34162392ad 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,12 +2,8 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - testTimeout: 60_000, - poolOptions: { - forks: { - singleFork: true, - isolate: false, - }, - }, + testTimeout: 20_000, + fileParallelism: false, + isolate: false, }, });