diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureResourceLoader.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureResourceLoader.ts new file mode 100644 index 00000000000000..bf7e7e947d5114 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureResourceLoader.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { mainWindow } from '../../../../base/browser/window.js'; +import { FileAccess } from '../../../../base/common/network.js'; +import { isNative } from '../../../../base/common/platform.js'; +import { URI } from '../../../../base/common/uri.js'; + +export function fixtureResourceUri(path: string): URI { + return isNative + ? URI.joinPath(FileAccess.asFileUri(''), '..', path) + : URI.parse(new URL(`/${path}`, mainWindow.document.baseURI).href); +} + +export function readFixtureTextResource(resource: URI): Promise { + return requestFixtureResource(resource, 'text').then(request => request.responseText); +} + +export function readFixtureBinaryResource(resource: URI): Promise { + return requestFixtureResource(resource, 'arraybuffer').then(request => request.response); +} + +function requestFixtureResource(resource: URI, responseType: XMLHttpRequestResponseType): Promise { + return new Promise((resolve, reject) => { + const request = new mainWindow.XMLHttpRequest(); + request.open('GET', resource.toString(true), true); + request.responseType = responseType; + request.onload = () => { + if (request.status === 0 || (request.status >= 200 && request.status < 300)) { + resolve(request); + } else { + reject(new Error(`Failed to load fixture resource ${resource.toString()}: ${request.status} ${request.statusText}`)); + } + }; + request.onerror = () => reject(new Error(`Failed to load fixture resource ${resource.toString()}: ${request.statusText}`)); + request.send(); + }); +} diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureSyntaxHighlighting.test.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureSyntaxHighlighting.test.ts new file mode 100644 index 00000000000000..85346eb57c7625 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureSyntaxHighlighting.test.ts @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { $ } from '../../../../base/browser/dom.js'; +import { mainWindow } from '../../../../base/browser/window.js'; +import { Color } from '../../../../base/common/color.js'; +import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { TokenMetadata } from '../../../../editor/common/encodedTokenAttributes.js'; +import { TokenizationRegistry } from '../../../../editor/common/languages.js'; +import { LanguageService } from '../../../../editor/common/services/languageService.js'; +import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js'; +import { ThemeTypeSelector } from '../../../../platform/theme/common/theme.js'; +import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; +import { editorForeground } from '../../../../platform/theme/common/colors/editorColors.js'; +import { ColorThemeData } from '../../../services/themes/common/colorThemeData.js'; +import { ExtensionData } from '../../../services/themes/common/workbenchThemeService.js'; +import { fixtureResourceUri, readFixtureTextResource } from './fixtureResourceLoader.js'; +import { registerFixtureLanguages, registerFixtureSyntaxHighlighting } from './fixtureSyntaxHighlighting.js'; + +suite('Component fixture TextMate syntax highlighting', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + const darkTheme = ColorThemeData.fromExtensionTheme( + { id: '2026-dark', path: '2026-dark.json', uiTheme: ThemeTypeSelector.VS_DARK, _watch: false }, + fixtureResourceUri('extensions/theme-defaults/themes/2026-dark.json'), + ExtensionData.fromName('vscode', 'theme-defaults', true), + ); + const themeLoader = new class implements IExtensionResourceLoaderService { + declare readonly _serviceBrand: undefined; + readExtensionResource(uri: URI): Promise { return readFixtureTextResource(uri); } + supportsExtensionGalleryResources(): Promise { return Promise.resolve(false); } + isExtensionGalleryResource(): Promise { return Promise.resolve(false); } + getExtensionGalleryResourceURL(): Promise { return Promise.resolve(undefined); } + }(); + let themeLoaded: Promise | undefined; + + async function createTokenizers(theme: IColorTheme = darkTheme): Promise<{ store: DisposableStore; languageService: LanguageService; host: HTMLElement }> { + const store = disposables.add(new DisposableStore()); + const languageService = store.add(new LanguageService()); + registerFixtureLanguages(store, languageService); + const host = mainWindow.document.body.appendChild($('div')); + store.add(toDisposable(() => host.remove())); + await (themeLoaded ??= darkTheme.ensureLoaded(themeLoader)); + await registerFixtureSyntaxHighlighting(store, host, darkTheme, theme); + return { store, languageService, host }; + } + + test('registers actual TextMate tokenizers for every fixture language', async () => { + const { languageService } = await createTokenizers(); + const samples = new Map([ + ['typescript', 'const value: number = 1;'], + ['typescriptreact', 'const view =
{value}
;'], + ['javascript', 'const value = /actual+/gi;'], + ['javascriptreact', 'const view =
{value}
;'], + ['json', '{ "enabled": true }'], + ['css', '.fixture { color: #ff7b72; }'], + ['html', '
text
'], + ]); + + const actual = [...samples].map(([languageId, line]) => { + const support = TokenizationRegistry.get(languageId); + assert.ok(support); + const result = support.tokenizeEncoded(line, true, support.getInitialState()); + return { + languageId, + registered: languageService.isRegisteredLanguageId(languageId), + tokenCount: result.tokens.length / 2, + encodedLanguageIds: distinctLanguageIds(result.tokens).map(id => languageService.languageIdCodec.decodeLanguageId(id)), + }; + }); + + assert.deepStrictEqual(actual, [ + { languageId: 'typescript', registered: true, tokenCount: 11, encodedLanguageIds: ['typescript'] }, + { languageId: 'typescriptreact', registered: true, tokenCount: 16, encodedLanguageIds: ['typescriptreact', 'jsx-tags'] }, + { languageId: 'javascript', registered: true, tokenCount: 10, encodedLanguageIds: ['javascript'] }, + { languageId: 'javascriptreact', registered: true, tokenCount: 16, encodedLanguageIds: ['javascriptreact', 'jsx-tags'] }, + { languageId: 'json', registered: true, tokenCount: 5, encodedLanguageIds: ['json'] }, + { languageId: 'css', registered: true, tokenCount: 6, encodedLanguageIds: ['css'] }, + { languageId: 'html', registered: true, tokenCount: 11, encodedLanguageIds: ['html'] }, + ]); + }); + + test('uses Dark 2026 TextMate scope distinctions and generated token CSS', async () => { + const { host } = await createTokenizers(); + const support = TokenizationRegistry.get('typescript')!; + const line = 'import value from "./value"; class Example { private readonly field = 1; } const result = value;'; + const result = support.tokenizeEncoded(line, true, support.getInitialState()); + + assert.deepStrictEqual({ + import: colorAt(result.tokens, line.indexOf('import')), + private: colorAt(result.tokens, line.indexOf('private')), + readonly: colorAt(result.tokens, line.indexOf('readonly')), + const: colorAt(result.tokens, line.indexOf('const')), + hasPurpleRule: host.querySelector('style')?.textContent?.includes('#c586c0'), + hasBlueRule: host.querySelector('style')?.textContent?.includes('#569cd6'), + hasCoralRule: host.querySelector('style')?.textContent?.includes('#ff7b72'), + }, { + import: '#c586c0', + private: '#569cd6', + readonly: '#569cd6', + const: '#ff7b72', + hasPurpleRule: true, + hasBlueRule: true, + hasCoralRule: true, + }); + }); + + test('preserves multiline TextMate state and embedded language IDs', async () => { + const { languageService } = await createTokenizers(); + const typescript = TokenizationRegistry.get('typescript')!; + const initialState = typescript.getInitialState(); + const commentStart = typescript.tokenizeEncoded('/* first line', true, initialState); + const commentEndLine = 'continued */ const value = 1;'; + const commentEnd = typescript.tokenizeEncoded(commentEndLine, true, commentStart.endState); + + const html = TokenizationRegistry.get('html')!; + const scriptStart = html.tokenizeEncoded('