Files
vscode/build/vite/fixtures/fixtureUtils.ts
Henning Dieterichs bce538863f adds component explorer (#294075)
* adds component explorer

* Removes log
2026-02-11 12:06:16 +01:00

513 lines
23 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { defineFixture, defineFixtureGroup, defineFixtureVariants } from '@vscode/component-explorer';
import { DisposableStore, toDisposable } from '../../../src/vs/base/common/lifecycle';
import { URI } from '../../../src/vs/base/common/uri';
import '../style.css';
// Theme
import { COLOR_THEME_DARK_INITIAL_COLORS, COLOR_THEME_LIGHT_INITIAL_COLORS } from '../../../src/vs/workbench/services/themes/common/workbenchThemeService';
import { ColorThemeData } from '../../../src/vs/workbench/services/themes/common/colorThemeData';
import { ColorScheme } from '../../../src/vs/platform/theme/common/theme';
import { generateColorThemeCSS } from '../../../src/vs/workbench/services/themes/browser/colorThemeCss';
import { Registry } from '../../../src/vs/platform/registry/common/platform';
import { Extensions as ThemingExtensions, IThemingRegistry } from '../../../src/vs/platform/theme/common/themeService';
import { IEnvironmentService } from '../../../src/vs/platform/environment/common/environment';
import { getIconsStyleSheet } from '../../../src/vs/platform/theme/browser/iconsStyleSheet';
// Instantiation
import { ServiceCollection } from '../../../src/vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from '../../../src/vs/platform/instantiation/common/descriptors';
import { ServiceIdentifier } from '../../../src/vs/platform/instantiation/common/instantiation';
import { TestInstantiationService } from '../../../src/vs/platform/instantiation/test/common/instantiationServiceMock';
// Test service implementations
import { TestAccessibilityService } from '../../../src/vs/platform/accessibility/test/common/testAccessibilityService';
import { MockKeybindingService, MockContextKeyService } from '../../../src/vs/platform/keybinding/test/common/mockKeybindingService';
import { TestClipboardService } from '../../../src/vs/platform/clipboard/test/common/testClipboardService';
import { TestEditorWorkerService } from '../../../src/vs/editor/test/common/services/testEditorWorkerService';
import { NullOpenerService } from '../../../src/vs/platform/opener/test/common/nullOpenerService';
import { TestNotificationService } from '../../../src/vs/platform/notification/test/common/testNotificationService';
import { TestDialogService } from '../../../src/vs/platform/dialogs/test/common/testDialogService';
import { TestConfigurationService } from '../../../src/vs/platform/configuration/test/common/testConfigurationService';
import { TestTextResourcePropertiesService } from '../../../src/vs/editor/test/common/services/testTextResourcePropertiesService';
import { TestThemeService } from '../../../src/vs/platform/theme/test/common/testThemeService';
import { TestLanguageConfigurationService } from '../../../src/vs/editor/test/common/modes/testLanguageConfigurationService';
import { TestCodeEditorService, TestCommandService } from '../../../src/vs/editor/test/browser/editorTestServices';
import { TestTreeSitterLibraryService } from '../../../src/vs/editor/test/common/services/testTreeSitterLibraryService';
import { TestMenuService } from '../../../src/vs/workbench/test/browser/workbenchTestServices';
// Service interfaces
import { IAccessibilityService } from '../../../src/vs/platform/accessibility/common/accessibility';
import { IKeybindingService } from '../../../src/vs/platform/keybinding/common/keybinding';
import { IClipboardService } from '../../../src/vs/platform/clipboard/common/clipboardService';
import { IEditorWorkerService } from '../../../src/vs/editor/common/services/editorWorker';
import { IOpenerService } from '../../../src/vs/platform/opener/common/opener';
import { INotificationService } from '../../../src/vs/platform/notification/common/notification';
import { IDialogService } from '../../../src/vs/platform/dialogs/common/dialogs';
import { IUndoRedoService } from '../../../src/vs/platform/undoRedo/common/undoRedo';
import { UndoRedoService } from '../../../src/vs/platform/undoRedo/common/undoRedoService';
import { ILanguageService } from '../../../src/vs/editor/common/languages/language';
import { LanguageService } from '../../../src/vs/editor/common/services/languageService';
import { ILanguageConfigurationService } from '../../../src/vs/editor/common/languages/languageConfigurationRegistry';
import { IConfigurationService } from '../../../src/vs/platform/configuration/common/configuration';
import { ITextResourcePropertiesService } from '../../../src/vs/editor/common/services/textResourceConfiguration';
import { IColorTheme, IThemeService } from '../../../src/vs/platform/theme/common/themeService';
import { ILogService, NullLogService, ILoggerService, NullLoggerService } from '../../../src/vs/platform/log/common/log';
import { IModelService } from '../../../src/vs/editor/common/services/model';
import { ModelService } from '../../../src/vs/editor/common/services/modelService';
import { ICodeEditorService } from '../../../src/vs/editor/browser/services/codeEditorService';
import { IContextKeyService } from '../../../src/vs/platform/contextkey/common/contextkey';
import { ICommandService } from '../../../src/vs/platform/commands/common/commands';
import { ITelemetryService } from '../../../src/vs/platform/telemetry/common/telemetry';
import { NullTelemetryServiceShape } from '../../../src/vs/platform/telemetry/common/telemetryUtils';
import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from '../../../src/vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from '../../../src/vs/editor/common/services/languageFeatures';
import { LanguageFeaturesService } from '../../../src/vs/editor/common/services/languageFeaturesService';
import { ITreeSitterLibraryService } from '../../../src/vs/editor/common/services/treeSitter/treeSitterLibraryService';
import { IInlineCompletionsService, InlineCompletionsService } from '../../../src/vs/editor/browser/services/inlineCompletionsService';
import { ICodeLensCache } from '../../../src/vs/editor/contrib/codelens/browser/codeLensCache';
import { IHoverService } from '../../../src/vs/platform/hover/browser/hover';
import { IDataChannelService, NullDataChannelService } from '../../../src/vs/platform/dataChannel/common/dataChannel';
import { IContextMenuService, IContextViewService } from '../../../src/vs/platform/contextview/browser/contextView';
import { ILabelService } from '../../../src/vs/platform/label/common/label';
import { IMenuService } from '../../../src/vs/platform/actions/common/actions';
import { IActionViewItemService, NullActionViewItemService } from '../../../src/vs/platform/actions/browser/actionViewItemService';
import { IDefaultAccountService } from '../../../src/vs/platform/defaultAccount/common/defaultAccount';
import { IStorageService, IStorageValueChangeEvent, IWillSaveStateEvent, StorageScope, StorageTarget, IStorageTargetChangeEvent, IStorageEntry, WillSaveStateReason, IWorkspaceStorageValueChangeEvent, IProfileStorageValueChangeEvent, IApplicationStorageValueChangeEvent } from '../../../src/vs/platform/storage/common/storage';
import { Emitter, Event } from '../../../src/vs/base/common/event';
import { mock } from '../../../src/vs/base/test/common/mock';
import { IAnyWorkspaceIdentifier } from '../../../src/vs/platform/workspace/common/workspace';
import { IUserDataProfile } from '../../../src/vs/platform/userDataProfile/common/userDataProfile';
import { IUserInteractionService, MockUserInteractionService } from '../../../src/vs/platform/userInteraction/browser/userInteractionService';
// Editor
import { ITextModel } from '../../../src/vs/editor/common/model';
// Import color registrations to ensure colors are available
import '../../../src/vs/platform/theme/common/colors/baseColors';
import '../../../src/vs/platform/theme/common/colors/editorColors';
import '../../../src/vs/platform/theme/common/colors/listColors';
import '../../../src/vs/platform/theme/common/colors/miscColors';
import '../../../src/vs/workbench/common/theme';
/**
* A storage service that never stores anything and always returns the default/fallback value.
* This is useful for fixtures where we want consistent behavior without persisted state.
*/
class NullStorageService implements IStorageService {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeValue = new Emitter<IStorageValueChangeEvent>();
onDidChangeValue(scope: StorageScope.WORKSPACE, key: string | undefined, disposable: DisposableStore): Event<IWorkspaceStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.PROFILE, key: string | undefined, disposable: DisposableStore): Event<IProfileStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope.APPLICATION, key: string | undefined, disposable: DisposableStore): Event<IApplicationStorageValueChangeEvent>;
onDidChangeValue(scope: StorageScope, key: string | undefined, disposable: DisposableStore): Event<IStorageValueChangeEvent> {
return Event.filter(this._onDidChangeValue.event, e => e.scope === scope && (key === undefined || e.key === key), disposable);
}
private readonly _onDidChangeTarget = new Emitter<IStorageTargetChangeEvent>();
readonly onDidChangeTarget: Event<IStorageTargetChangeEvent> = this._onDidChangeTarget.event;
private readonly _onWillSaveState = new Emitter<IWillSaveStateEvent>();
readonly onWillSaveState: Event<IWillSaveStateEvent> = this._onWillSaveState.event;
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
get(_key: string, _scope: StorageScope, fallbackValue?: string): string | undefined {
return fallbackValue;
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
getBoolean(_key: string, _scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
return fallbackValue;
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
getNumber(_key: string, _scope: StorageScope, fallbackValue?: number): number | undefined {
return fallbackValue;
}
getObject<T extends object>(key: string, scope: StorageScope, fallbackValue: T): T;
getObject<T extends object>(key: string, scope: StorageScope, fallbackValue?: T): T | undefined;
getObject<T extends object>(_key: string, _scope: StorageScope, fallbackValue?: T): T | undefined {
return fallbackValue;
}
store(_key: string, _value: string | boolean | number | undefined | null, _scope: StorageScope, _target: StorageTarget): void {
// no-op
}
storeAll(_entries: IStorageEntry[], _external: boolean): void {
// no-op
}
remove(_key: string, _scope: StorageScope): void {
// no-op
}
isNew(_scope: StorageScope): boolean {
return true;
}
flush(_reason?: WillSaveStateReason): Promise<void> {
return Promise.resolve();
}
optimize(_scope: StorageScope): Promise<void> {
return Promise.resolve();
}
log(): void {
// no-op
}
keys(_scope: StorageScope, _target: StorageTarget): string[] {
return [];
}
switch(): Promise<void> {
return Promise.resolve();
}
hasScope(_scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean {
return false;
}
}
// ============================================================================
// Themes
// ============================================================================
const themingRegistry = Registry.as<IThemingRegistry>(ThemingExtensions.ThemingContribution);
const mockEnvironmentService: IEnvironmentService = Object.create(null);
export const darkTheme = ColorThemeData.createUnloadedThemeForThemeType(
ColorScheme.DARK,
COLOR_THEME_DARK_INITIAL_COLORS
);
export const lightTheme = ColorThemeData.createUnloadedThemeForThemeType(
ColorScheme.LIGHT,
COLOR_THEME_LIGHT_INITIAL_COLORS
);
let globalStyleSheet: CSSStyleSheet | undefined;
let iconsStyleSheetCache: CSSStyleSheet | undefined;
let darkThemeStyleSheet: CSSStyleSheet | undefined;
let lightThemeStyleSheet: CSSStyleSheet | undefined;
function getGlobalStyleSheet(): CSSStyleSheet {
if (!globalStyleSheet) {
globalStyleSheet = new CSSStyleSheet();
const globalRules: string[] = [];
for (const sheet of Array.from(document.styleSheets)) {
try {
for (const rule of Array.from(sheet.cssRules)) {
globalRules.push(rule.cssText);
}
} catch {
// Cross-origin stylesheets can't be read
}
}
globalStyleSheet.replaceSync(globalRules.join('\n'));
}
return globalStyleSheet;
}
function getIconsStyleSheetCached(): CSSStyleSheet {
if (!iconsStyleSheetCache) {
iconsStyleSheetCache = new CSSStyleSheet();
const iconsSheet = getIconsStyleSheet(undefined);
iconsStyleSheetCache.replaceSync(iconsSheet.getCSS() as string);
iconsSheet.dispose();
}
return iconsStyleSheetCache;
}
function getThemeStyleSheet(theme: ColorThemeData): CSSStyleSheet {
const isDark = theme.type === ColorScheme.DARK;
if (isDark && darkThemeStyleSheet) {
return darkThemeStyleSheet;
}
if (!isDark && lightThemeStyleSheet) {
return lightThemeStyleSheet;
}
const sheet = new CSSStyleSheet();
const css = generateColorThemeCSS(
theme,
':host',
themingRegistry.getThemingParticipants(),
mockEnvironmentService
);
sheet.replaceSync(css.code);
if (isDark) {
darkThemeStyleSheet = sheet;
} else {
lightThemeStyleSheet = sheet;
}
return sheet;
}
/**
* Applies theme styling to a shadow DOM container.
* Adds theme class names and adopts shared stylesheets.
*/
export function setupTheme(container: HTMLElement, theme: ColorThemeData): void {
container.classList.add(...theme.classNames);
const shadowRoot = container.getRootNode() as ShadowRoot;
if (shadowRoot.adoptedStyleSheets !== undefined) {
shadowRoot.adoptedStyleSheets = [
getGlobalStyleSheet(),
getIconsStyleSheetCached(),
getThemeStyleSheet(theme),
];
}
}
// ============================================================================
// Services
// ============================================================================
export interface ServiceRegistration {
define<T>(id: ServiceIdentifier<T>, ctor: new (...args: never[]) => T): void;
defineInstance<T>(id: ServiceIdentifier<T>, instance: T): void;
}
export interface CreateServicesOptions {
/**
* The color theme to use for the theme service.
*/
colorTheme?: IColorTheme;
/**
* Additional services to register after the base editor services.
*/
additionalServices?: (registration: ServiceRegistration) => void;
}
/**
* Creates a TestInstantiationService with all services needed for CodeEditorWidget.
* Additional services can be registered via the options callback.
*/
export function createEditorServices(disposables: DisposableStore, options?: CreateServicesOptions): TestInstantiationService {
const services = new ServiceCollection();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serviceIdentifiers: ServiceIdentifier<any>[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const define = <T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T) => {
if (!services.has(id)) {
services.set(id, new SyncDescriptor(ctor));
}
serviceIdentifiers.push(id);
};
const defineInstance = <T>(id: ServiceIdentifier<T>, instance: T) => {
if (!services.has(id)) {
services.set(id, instance);
}
serviceIdentifiers.push(id);
};
// Base editor services
define(IAccessibilityService, TestAccessibilityService);
define(IKeybindingService, MockKeybindingService);
define(IClipboardService, TestClipboardService);
define(IEditorWorkerService, TestEditorWorkerService);
defineInstance(IOpenerService, NullOpenerService);
define(INotificationService, TestNotificationService);
define(IDialogService, TestDialogService);
define(IUndoRedoService, UndoRedoService);
define(ILanguageService, LanguageService);
define(ILanguageConfigurationService, TestLanguageConfigurationService);
define(IConfigurationService, TestConfigurationService);
define(ITextResourcePropertiesService, TestTextResourcePropertiesService);
defineInstance(IStorageService, new NullStorageService());
if (options?.colorTheme) {
defineInstance(IThemeService, new TestThemeService(options.colorTheme));
} else {
define(IThemeService, TestThemeService);
}
define(ILogService, NullLogService);
define(IModelService, ModelService);
define(ICodeEditorService, TestCodeEditorService);
define(IContextKeyService, MockContextKeyService);
define(ICommandService, TestCommandService);
define(ITelemetryService, NullTelemetryServiceShape);
define(ILoggerService, NullLoggerService);
define(IDataChannelService, NullDataChannelService);
define(IEnvironmentService, class extends mock<IEnvironmentService>() {
declare readonly _serviceBrand: undefined;
override isBuilt: boolean = true;
override isExtensionDevelopment: boolean = false;
});
define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService);
define(ILanguageFeaturesService, LanguageFeaturesService);
define(ITreeSitterLibraryService, TestTreeSitterLibraryService);
define(IInlineCompletionsService, InlineCompletionsService);
defineInstance(ICodeLensCache, {
_serviceBrand: undefined,
put: () => { },
get: () => undefined,
delete: () => { },
} as ICodeLensCache);
defineInstance(IHoverService, {
_serviceBrand: undefined,
showDelayedHover: () => undefined,
setupDelayedHover: () => ({ dispose: () => { } }),
setupDelayedHoverAtMouse: () => ({ dispose: () => { } }),
showInstantHover: () => undefined,
hideHover: () => { },
showAndFocusLastHover: () => { },
setupManagedHover: () => ({ dispose: () => { }, show: () => { }, hide: () => { }, update: () => { } }),
showManagedHover: () => { },
} as IHoverService);
defineInstance(IDefaultAccountService, {
_serviceBrand: undefined,
onDidChangeDefaultAccount: new Emitter<null>().event,
onDidChangePolicyData: new Emitter<null>().event,
policyData: null,
getDefaultAccount: async () => null,
getDefaultAccountAuthenticationProvider: () => ({ id: 'test', name: 'Test', scopes: [], enterprise: false }),
setDefaultAccountProvider: () => { },
refresh: async () => null,
signIn: async () => null,
} as IDefaultAccountService);
// User interaction service with focus simulation enabled (all elements appear focused in fixtures)
defineInstance(IUserInteractionService, new MockUserInteractionService(true, false));
// Allow additional services to be registered
options?.additionalServices?.({ define, defineInstance });
const instantiationService = disposables.add(new TestInstantiationService(services, true));
disposables.add(toDisposable(() => {
for (const id of serviceIdentifiers) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instanceOrDescriptor = services.get(id) as any;
if (typeof instanceOrDescriptor?.dispose === 'function') {
instanceOrDescriptor.dispose();
}
}
}));
return instantiationService;
}
/**
* Registers additional services needed by workbench components (merge editor, etc.).
* Use with createEditorServices additionalServices option.
*/
export function registerWorkbenchServices(registration: ServiceRegistration): void {
registration.defineInstance(IContextMenuService, {
showContextMenu: () => { },
onDidShowContextMenu: () => ({ dispose: () => { } }),
onDidHideContextMenu: () => ({ dispose: () => { } }),
} as unknown as IContextMenuService);
registration.defineInstance(IContextViewService, {
showContextView: () => ({ dispose: () => { } }),
hideContextView: () => { },
getContextViewElement: () => null,
layout: () => { },
} as unknown as IContextViewService);
registration.defineInstance(ILabelService, {
getUriLabel: (uri: URI) => uri.path,
getUriBasenameLabel: (uri: URI) => uri.path.split('/').pop() ?? '',
getWorkspaceLabel: () => '',
getHostLabel: () => '',
getSeparator: () => '/',
registerFormatter: () => ({ dispose: () => { } }),
onDidChangeFormatters: () => ({ dispose: () => { } }),
registerCachedFormatter: () => ({ dispose: () => { } }),
} as unknown as ILabelService);
registration.define(IMenuService, TestMenuService);
registration.define(IActionViewItemService, NullActionViewItemService);
}
// ============================================================================
// Text Models
// ============================================================================
/**
* Creates a text model using the ModelService.
*/
export function createTextModel(
instantiationService: TestInstantiationService,
text: string,
uri: URI,
languageId?: string
): ITextModel {
const modelService = instantiationService.get(IModelService);
const languageService = instantiationService.get(ILanguageService);
const languageSelection = languageId ? languageService.createById(languageId) : null;
return modelService.createModel(text, languageSelection, uri);
}
// ============================================================================
// Fixture Adapters
// ============================================================================
export interface ComponentFixtureContext {
container: HTMLElement;
disposableStore: DisposableStore;
theme: ColorThemeData;
}
export interface ComponentFixtureOptions {
render: (context: ComponentFixtureContext) => HTMLElement | Promise<HTMLElement>;
}
type ThemedFixtures = ReturnType<typeof defineFixtureVariants>;
/**
* Creates Dark and Light fixture variants from a single render function.
* The render function receives a context with container and disposableStore.
*/
export function defineComponentFixture(options: ComponentFixtureOptions): ThemedFixtures {
const createFixture = (theme: typeof darkTheme | typeof lightTheme) => defineFixture({
isolation: 'shadow-dom',
displayMode: { type: 'component' },
properties: [],
background: theme === darkTheme ? 'dark' : 'light',
render: async (container: HTMLElement) => {
const disposableStore = new DisposableStore();
setupTheme(container, theme);
return options.render({ container, disposableStore, theme });
},
});
return defineFixtureVariants({
Dark: createFixture(darkTheme),
Light: createFixture(lightTheme),
});
}
type ThemedFixtureGroupInput = Record<string, ThemedFixtures>;
/**
* Creates a nested fixture group from themed fixtures.
* E.g., { MergeEditor: { Dark: ..., Light: ... } } becomes a nested group: MergeEditor > Dark/Light
*/
export function defineThemedFixtureGroup(group: ThemedFixtureGroupInput): ReturnType<typeof defineFixtureGroup> {
return defineFixtureGroup(group);
}