adds component explorer (#294075)

* adds component explorer

* Removes log
This commit is contained in:
Henning Dieterichs
2026-02-11 12:06:16 +01:00
committed by GitHub
parent bde73caaee
commit bce538863f
11 changed files with 1993 additions and 651 deletions

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { observableValue } from '../../../src/vs/base/common/observable';
import { createAiStatsHover, IAiStatsHoverData } from '../../../src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar';
import { ISessionData } from '../../../src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart';
import { Random } from '../../../src/vs/editor/test/common/core/random';
import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils';
export default defineThemedFixtureGroup({
AiStatsHover: defineComponentFixture({
render: (context) => renderAiStatsHover({ ...context, data: createSampleDataWithSessions() }),
}),
AiStatsHoverNoData: defineComponentFixture({
render: (context) => renderAiStatsHover({ ...context, data: createEmptyData() }),
}),
});
function createSampleDataWithSessions(): IAiStatsHoverData {
const random = Random.create(42);
// Use a fixed base time for determinism (Jan 1, 2025, 12:00:00 UTC)
const baseTime = 1735732800000;
const dayMs = 24 * 60 * 60 * 1000;
const sessionLengthMs = 5 * 60 * 1000;
// Generate fake session data for the last 7 days
const fakeSessions: ISessionData[] = [];
for (let day = 6; day >= 0; day--) {
const dayStart = baseTime - day * dayMs;
const sessionsPerDay = random.nextIntRange(3, 9);
for (let s = 0; s < sessionsPerDay; s++) {
const sessionTime = dayStart + s * sessionLengthMs * 2;
fakeSessions.push({
startTime: sessionTime,
typedCharacters: random.nextIntRange(100, 600),
aiCharacters: random.nextIntRange(200, 1000),
acceptedInlineSuggestions: random.nextIntRange(1, 16),
chatEditCount: random.nextIntRange(0, 5),
});
}
}
const totalAi = fakeSessions.reduce((sum, s) => sum + s.aiCharacters, 0);
const totalTyped = fakeSessions.reduce((sum, s) => sum + s.typedCharacters, 0);
const aiRate = totalAi / (totalAi + totalTyped);
// "Today" for the fixture is the baseTime day
const startOfToday = baseTime - (baseTime % dayMs);
const todaySessions = fakeSessions.filter(s => s.startTime >= startOfToday);
const acceptedToday = todaySessions.reduce((sum, s) => sum + (s.acceptedInlineSuggestions ?? 0), 0);
return {
aiRate: observableValue('aiRate', aiRate),
acceptedInlineSuggestionsToday: observableValue('acceptedToday', acceptedToday),
sessions: observableValue('sessions', fakeSessions),
};
}
function createEmptyData(): IAiStatsHoverData {
return {
aiRate: observableValue('aiRate', 0),
acceptedInlineSuggestionsToday: observableValue('acceptedToday', 0),
sessions: observableValue('sessions', []),
};
}
interface RenderAiStatsOptions extends ComponentFixtureContext {
data: IAiStatsHoverData;
}
function renderAiStatsHover({ container, disposableStore, data }: RenderAiStatsOptions): HTMLElement {
container.style.width = '320px';
container.style.padding = '8px';
container.style.backgroundColor = 'var(--vscode-editorHoverWidget-background)';
container.style.border = '1px solid var(--vscode-editorHoverWidget-border)';
container.style.borderRadius = '4px';
container.style.color = 'var(--vscode-editorHoverWidget-foreground)';
const hover = createAiStatsHover({
data,
onOpenSettings: () => console.log('Open settings clicked'),
});
const elem = hover.keepUpdated(disposableStore).element;
container.appendChild(elem);
return container;
}

View File

@@ -0,0 +1,531 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $ } from '../../../src/vs/base/browser/dom';
import { Codicon } from '../../../src/vs/base/common/codicons';
import { ThemeIcon } from '../../../src/vs/base/common/themables';
import { Action, Separator } from '../../../src/vs/base/common/actions';
// UI Components
import { Button, ButtonBar, ButtonWithDescription, unthemedButtonStyles } from '../../../src/vs/base/browser/ui/button/button';
import { Toggle, Checkbox, unthemedToggleStyles } from '../../../src/vs/base/browser/ui/toggle/toggle';
import { InputBox, MessageType, unthemedInboxStyles } from '../../../src/vs/base/browser/ui/inputbox/inputBox';
import { CountBadge } from '../../../src/vs/base/browser/ui/countBadge/countBadge';
import { ActionBar } from '../../../src/vs/base/browser/ui/actionbar/actionbar';
import { ProgressBar } from '../../../src/vs/base/browser/ui/progressbar/progressbar';
import { HighlightedLabel } from '../../../src/vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils';
// ============================================================================
// Styles (themed versions for fixture display)
// ============================================================================
const themedButtonStyles = {
...unthemedButtonStyles,
buttonBackground: 'var(--vscode-button-background)',
buttonHoverBackground: 'var(--vscode-button-hoverBackground)',
buttonForeground: 'var(--vscode-button-foreground)',
buttonSecondaryBackground: 'var(--vscode-button-secondaryBackground)',
buttonSecondaryHoverBackground: 'var(--vscode-button-secondaryHoverBackground)',
buttonSecondaryForeground: 'var(--vscode-button-secondaryForeground)',
buttonBorder: 'var(--vscode-button-border)',
};
const themedToggleStyles = {
...unthemedToggleStyles,
inputActiveOptionBorder: 'var(--vscode-inputOption-activeBorder)',
inputActiveOptionForeground: 'var(--vscode-inputOption-activeForeground)',
inputActiveOptionBackground: 'var(--vscode-inputOption-activeBackground)',
};
const themedCheckboxStyles = {
checkboxBackground: 'var(--vscode-checkbox-background)',
checkboxBorder: 'var(--vscode-checkbox-border)',
checkboxForeground: 'var(--vscode-checkbox-foreground)',
checkboxDisabledBackground: undefined,
checkboxDisabledForeground: undefined,
};
const themedInputBoxStyles = {
...unthemedInboxStyles,
inputBackground: 'var(--vscode-input-background)',
inputForeground: 'var(--vscode-input-foreground)',
inputBorder: 'var(--vscode-input-border)',
inputValidationInfoBackground: 'var(--vscode-inputValidation-infoBackground)',
inputValidationInfoBorder: 'var(--vscode-inputValidation-infoBorder)',
inputValidationWarningBackground: 'var(--vscode-inputValidation-warningBackground)',
inputValidationWarningBorder: 'var(--vscode-inputValidation-warningBorder)',
inputValidationErrorBackground: 'var(--vscode-inputValidation-errorBackground)',
inputValidationErrorBorder: 'var(--vscode-inputValidation-errorBorder)',
};
const themedBadgeStyles = {
badgeBackground: 'var(--vscode-badge-background)',
badgeForeground: 'var(--vscode-badge-foreground)',
badgeBorder: undefined,
};
const themedProgressBarOptions = {
progressBarBackground: 'var(--vscode-progressBar-background)',
};
// ============================================================================
// Buttons
// ============================================================================
function renderButtons({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
// Section: Primary Buttons
const primarySection = $('div');
primarySection.style.display = 'flex';
primarySection.style.gap = '8px';
primarySection.style.alignItems = 'center';
container.appendChild(primarySection);
const primaryButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Primary button' }));
primaryButton.label = 'Primary Button';
const primaryIconButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'With Icon', supportIcons: true }));
primaryIconButton.label = '$(add) Add Item';
const smallButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Small button', small: true }));
smallButton.label = 'Small';
// Section: Secondary Buttons
const secondarySection = $('div');
secondarySection.style.display = 'flex';
secondarySection.style.gap = '8px';
secondarySection.style.alignItems = 'center';
container.appendChild(secondarySection);
const secondaryButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Secondary button' }));
secondaryButton.label = 'Secondary Button';
const secondaryIconButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Cancel', supportIcons: true }));
secondaryIconButton.label = '$(close) Cancel';
// Section: Disabled Buttons
const disabledSection = $('div');
disabledSection.style.display = 'flex';
disabledSection.style.gap = '8px';
disabledSection.style.alignItems = 'center';
container.appendChild(disabledSection);
const disabledButton = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, title: 'Disabled', disabled: true }));
disabledButton.label = 'Disabled';
disabledButton.enabled = false;
const disabledSecondary = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, secondary: true, title: 'Disabled Secondary', disabled: true }));
disabledSecondary.label = 'Disabled Secondary';
disabledSecondary.enabled = false;
return container;
}
function renderButtonBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '16px';
// Button Bar
const barContainer = $('div');
container.appendChild(barContainer);
const buttonBar = new ButtonBar(barContainer);
disposableStore.add(buttonBar);
const okButton = buttonBar.addButton({ ...themedButtonStyles, title: 'OK' });
okButton.label = 'OK';
const cancelButton = buttonBar.addButton({ ...themedButtonStyles, secondary: true, title: 'Cancel' });
cancelButton.label = 'Cancel';
// Button with Description
const descContainer = $('div');
descContainer.style.width = '300px';
container.appendChild(descContainer);
const buttonWithDesc = disposableStore.add(new ButtonWithDescription(descContainer, { ...themedButtonStyles, title: 'Install Extension', supportIcons: true }));
buttonWithDesc.label = '$(extensions) Install Extension';
buttonWithDesc.description = 'This will install the extension and enable it globally';
return container;
}
// ============================================================================
// Toggles and Checkboxes
// ============================================================================
function renderToggles({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
// Toggles
const toggleSection = $('div');
toggleSection.style.display = 'flex';
toggleSection.style.gap = '16px';
toggleSection.style.alignItems = 'center';
container.appendChild(toggleSection);
const toggle1 = disposableStore.add(new Toggle({
...themedToggleStyles,
title: 'Case Sensitive',
isChecked: false,
icon: Codicon.caseSensitive,
}));
toggleSection.appendChild(toggle1.domNode);
const toggle2 = disposableStore.add(new Toggle({
...themedToggleStyles,
title: 'Whole Word',
isChecked: true,
icon: Codicon.wholeWord,
}));
toggleSection.appendChild(toggle2.domNode);
const toggle3 = disposableStore.add(new Toggle({
...themedToggleStyles,
title: 'Use Regular Expression',
isChecked: false,
icon: Codicon.regex,
}));
toggleSection.appendChild(toggle3.domNode);
// Checkboxes
const checkboxSection = $('div');
checkboxSection.style.display = 'flex';
checkboxSection.style.flexDirection = 'column';
checkboxSection.style.gap = '8px';
container.appendChild(checkboxSection);
const createCheckboxRow = (label: string, checked: boolean) => {
const row = $('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '8px';
const checkbox = disposableStore.add(new Checkbox(label, checked, themedCheckboxStyles));
row.appendChild(checkbox.domNode);
const labelEl = $('span');
labelEl.textContent = label;
labelEl.style.color = 'var(--vscode-foreground)';
row.appendChild(labelEl);
return row;
};
checkboxSection.appendChild(createCheckboxRow('Enable auto-save', true));
checkboxSection.appendChild(createCheckboxRow('Show line numbers', true));
checkboxSection.appendChild(createCheckboxRow('Word wrap', false));
return container;
}
// ============================================================================
// Input Boxes
// ============================================================================
function renderInputBoxes({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '16px';
container.style.width = '350px';
// Normal input
const normalInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'Enter search query...',
inputBoxStyles: themedInputBoxStyles,
}));
// Input with value
const filledInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'File path',
inputBoxStyles: themedInputBoxStyles,
}));
filledInput.value = '/src/vs/editor/browser';
// Input with info validation
const infoInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'Username',
inputBoxStyles: themedInputBoxStyles,
validationOptions: {
validation: (value) => value.length < 3 ? { content: 'Username must be at least 3 characters', type: MessageType.INFO } : null
}
}));
infoInput.value = 'ab';
infoInput.validate();
// Input with warning validation
const warningInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'Password',
inputBoxStyles: themedInputBoxStyles,
validationOptions: {
validation: (value) => value.length < 8 ? { content: 'Password should be at least 8 characters for security', type: MessageType.WARNING } : null
}
}));
warningInput.value = 'pass';
warningInput.validate();
// Input with error validation
const errorInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'Email address',
inputBoxStyles: themedInputBoxStyles,
validationOptions: {
validation: (value) => !value.includes('@') ? { content: 'Please enter a valid email address', type: MessageType.ERROR } : null
}
}));
errorInput.value = 'invalid-email';
errorInput.validate();
return container;
}
// ============================================================================
// Count Badges
// ============================================================================
function renderCountBadges({ container }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.gap = '12px';
container.style.alignItems = 'center';
// Various badge counts
const counts = [1, 5, 12, 99, 999];
for (const count of counts) {
const badgeContainer = $('div');
badgeContainer.style.display = 'flex';
badgeContainer.style.alignItems = 'center';
badgeContainer.style.gap = '8px';
const label = $('span');
label.textContent = 'Issues';
label.style.color = 'var(--vscode-foreground)';
badgeContainer.appendChild(label);
new CountBadge(badgeContainer, { count }, themedBadgeStyles);
container.appendChild(badgeContainer);
}
return container;
}
// ============================================================================
// Action Bar
// ============================================================================
function renderActionBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '16px';
// Horizontal action bar
const horizontalLabel = $('div');
horizontalLabel.textContent = 'Horizontal Actions:';
horizontalLabel.style.color = 'var(--vscode-foreground)';
horizontalLabel.style.marginBottom = '4px';
container.appendChild(horizontalLabel);
const horizontalContainer = $('div');
container.appendChild(horizontalContainer);
const horizontalBar = disposableStore.add(new ActionBar(horizontalContainer, {
ariaLabel: 'Editor Actions',
}));
horizontalBar.push([
new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save')),
new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo')),
new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo')),
new Separator(),
new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find')),
new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace')),
]);
// Action bar with disabled items
const mixedLabel = $('div');
mixedLabel.textContent = 'Mixed States:';
mixedLabel.style.color = 'var(--vscode-foreground)';
mixedLabel.style.marginBottom = '4px';
container.appendChild(mixedLabel);
const mixedContainer = $('div');
container.appendChild(mixedContainer);
const mixedBar = disposableStore.add(new ActionBar(mixedContainer, {
ariaLabel: 'Mixed Actions',
}));
mixedBar.push([
new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { }),
new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { }),
new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { }),
]);
return container;
}
// ============================================================================
// Progress Bar
// ============================================================================
function renderProgressBars({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '24px';
container.style.width = '400px';
const createSection = (label: string) => {
const section = $('div');
const labelEl = $('div');
labelEl.textContent = label;
labelEl.style.color = 'var(--vscode-foreground)';
labelEl.style.marginBottom = '8px';
labelEl.style.fontSize = '12px';
section.appendChild(labelEl);
// Progress bar container with proper constraints
const barContainer = $('div');
barContainer.style.position = 'relative';
barContainer.style.width = '100%';
barContainer.style.height = '4px';
barContainer.style.overflow = 'hidden';
section.appendChild(barContainer);
container.appendChild(section);
return barContainer;
};
// Infinite progress
const infiniteSection = createSection('Infinite Progress (loading...)');
const infiniteBar = disposableStore.add(new ProgressBar(infiniteSection, themedProgressBarOptions));
infiniteBar.infinite();
// Discrete progress - 30%
const progress30Section = createSection('Discrete Progress - 30%');
const progress30Bar = disposableStore.add(new ProgressBar(progress30Section, themedProgressBarOptions));
progress30Bar.total(100);
progress30Bar.worked(30);
// Discrete progress - 60%
const progress60Section = createSection('Discrete Progress - 60%');
const progress60Bar = disposableStore.add(new ProgressBar(progress60Section, themedProgressBarOptions));
progress60Bar.total(100);
progress60Bar.worked(60);
// Discrete progress - 90%
const progress90Section = createSection('Discrete Progress - 90%');
const progress90Bar = disposableStore.add(new ProgressBar(progress90Section, themedProgressBarOptions));
progress90Bar.total(100);
progress90Bar.worked(90);
// Completed progress
const doneSection = createSection('Completed (100%)');
const doneBar = disposableStore.add(new ProgressBar(doneSection, themedProgressBarOptions));
doneBar.total(100);
doneBar.worked(100);
return container;
}
// ============================================================================
// Highlighted Label
// ============================================================================
function renderHighlightedLabels({ container }: ComponentFixtureContext): HTMLElement {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '8px';
container.style.color = 'var(--vscode-foreground)';
const createHighlightedLabel = (text: string, highlights: { start: number; end: number }[]) => {
const row = $('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '8px';
const labelContainer = $('div');
const label = new HighlightedLabel(labelContainer);
label.set(text, highlights);
row.appendChild(labelContainer);
const queryLabel = $('span');
queryLabel.style.color = 'var(--vscode-descriptionForeground)';
queryLabel.style.fontSize = '12px';
queryLabel.textContent = `(matches highlighted)`;
row.appendChild(queryLabel);
return row;
};
// File search examples
container.appendChild(createHighlightedLabel('codeEditorWidget.ts', [{ start: 0, end: 4 }])); // "code"
container.appendChild(createHighlightedLabel('inlineCompletionsController.ts', [{ start: 6, end: 10 }])); // "Comp"
container.appendChild(createHighlightedLabel('diffEditorViewModel.ts', [{ start: 0, end: 4 }, { start: 10, end: 14 }])); // "diff" and "View"
container.appendChild(createHighlightedLabel('workbenchTestServices.ts', [{ start: 9, end: 13 }])); // "Test"
return container;
}
// ============================================================================
// Export Fixtures
// ============================================================================
export default defineThemedFixtureGroup({
Buttons: defineComponentFixture({
render: renderButtons,
}),
ButtonBar: defineComponentFixture({
render: renderButtonBar,
}),
Toggles: defineComponentFixture({
render: renderToggles,
}),
InputBoxes: defineComponentFixture({
render: renderInputBoxes,
}),
CountBadges: defineComponentFixture({
render: renderCountBadges,
}),
ActionBar: defineComponentFixture({
render: renderActionBar,
}),
ProgressBars: defineComponentFixture({
render: renderProgressBars,
}),
HighlightedLabels: defineComponentFixture({
render: renderHighlightedLabels,
}),
});

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from '../../../../src/vs/base/common/uri';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../src/vs/editor/browser/widget/codeEditor/codeEditorWidget';
import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils';
const SAMPLE_CODE = `// Welcome to VS Code
function greet(name: string): string {
return \`Hello, \${name}!\`;
}
class Counter {
private _count = 0;
increment(): void {
this._count++;
}
get count(): number {
return this._count;
}
}
const counter = new Counter();
counter.increment();
console.log(greet('World'));
console.log(\`Count: \${counter.count}\`);
`;
function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtureContext): HTMLElement {
container.style.width = '600px';
container.style.height = '400px';
container.style.border = '1px solid var(--vscode-editorWidget-border)';
const instantiationService = createEditorServices(disposableStore, { colorTheme: theme });
const model = disposableStore.add(createTextModel(
instantiationService,
SAMPLE_CODE,
URI.parse('inmemory://sample.ts'),
'typescript'
));
const editorOptions: ICodeEditorWidgetOptions = {
contributions: []
};
const editor = disposableStore.add(instantiationService.createInstance(
CodeEditorWidget,
container,
{
automaticLayout: true,
minimap: { enabled: true },
lineNumbers: 'on',
scrollBeyondLastLine: false,
fontSize: 14,
fontFamily: 'Consolas, "Courier New", monospace',
renderWhitespace: 'selection',
bracketPairColorization: { enabled: true },
},
editorOptions
));
editor.setModel(model);
return container;
}
export default defineThemedFixtureGroup({
CodeEditor: defineComponentFixture({
render: (context) => renderCodeEditor(context),
}),
});

View File

@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { constObservable } from '../../../../src/vs/base/common/observable';
import { URI } from '../../../../src/vs/base/common/uri';
import { Range } from '../../../../src/vs/editor/common/core/range';
import { IEditorOptions } from '../../../../src/vs/editor/common/config/editorOptions';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../src/vs/editor/browser/widget/codeEditor/codeEditorWidget';
import { EditorExtensionsRegistry } from '../../../../src/vs/editor/browser/editorExtensions';
import { InlineCompletionsController } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController';
import { InlineCompletionsSource, InlineCompletionsState } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource';
import { InlineEditItem } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem';
import { TextModelValueReference } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/textModelValueReference';
import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils';
// Import to register the inline completions contribution
import '../../../../src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution';
// ============================================================================
// Inline Edit Fixture
// ============================================================================
interface InlineEditOptions extends ComponentFixtureContext {
code: string;
cursorLine: number;
range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number };
newText: string;
width?: string;
height?: string;
editorOptions?: IEditorOptions;
}
function renderInlineEdit(options: InlineEditOptions): HTMLElement {
const { container, disposableStore, theme } = options;
container.style.width = options.width ?? '500px';
container.style.height = options.height ?? '170px';
container.style.border = '1px solid var(--vscode-editorWidget-border)';
const instantiationService = createEditorServices(disposableStore, { colorTheme: theme });
const textModel = disposableStore.add(createTextModel(
instantiationService,
options.code,
URI.parse('inmemory://inline-edit.ts'),
'typescript'
));
// Mock the InlineCompletionsSource to provide our test completion
instantiationService.stubInstance(InlineCompletionsSource, {
cancelUpdate: () => { },
clear: () => { },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
clearOperationOnTextModelChange: constObservable(undefined) as any,
clearSuggestWidgetInlineCompletions: () => { },
dispose: () => { },
fetch: async () => true,
inlineCompletions: constObservable(new InlineCompletionsState([
InlineEditItem.createForTest(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TextModelValueReference.snapshot(textModel as any),
new Range(
options.range.startLineNumber,
options.range.startColumn,
options.range.endLineNumber,
options.range.endColumn
),
options.newText
)
], undefined)),
loading: constObservable(false),
seedInlineCompletionsWithSuggestWidget: () => { },
seedWithCompletion: () => { },
suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()),
});
const editorWidgetOptions: ICodeEditorWidgetOptions = {
contributions: EditorExtensionsRegistry.getEditorContributions()
};
const editor = disposableStore.add(instantiationService.createInstance(
CodeEditorWidget,
container,
{
automaticLayout: true,
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
fontSize: 14,
cursorBlinking: 'solid',
...options.editorOptions,
},
editorWidgetOptions
));
editor.setModel(textModel);
editor.setPosition({ lineNumber: options.cursorLine, column: 1 });
editor.focus();
// Trigger inline completions
const controller = InlineCompletionsController.get(editor);
controller?.model?.get();
return container;
}
// ============================================================================
// Fixtures
// ============================================================================
export default defineThemedFixtureGroup({
// Side-by-side view: Multi-line replacement
SideBySideView: defineComponentFixture({
render: (context) => renderInlineEdit({
...context,
code: `function greet(name) {
console.log("Hello, " + name);
}`,
cursorLine: 2,
range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 100 },
newText: ' console.log(`Hello, ${name}!`);',
}),
}),
// Word replacement view: Single word change
WordReplacementView: defineComponentFixture({
render: (context) => renderInlineEdit({
...context,
code: `class BufferData {
append(data: number[]) {
this.data.push(data);
}
}`,
cursorLine: 2,
range: { startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 8 },
newText: 'push',
height: '200px',
}),
}),
// Insertion view: Insert new content
InsertionView: defineComponentFixture({
render: (context) => renderInlineEdit({
...context,
code: `class BufferData {
append(data: number[]) {} // appends data
}`,
cursorLine: 2,
range: { startLineNumber: 2, startColumn: 26, endLineNumber: 2, endColumn: 26 },
newText: `
console.log(data);
`,
height: '200px',
editorOptions: {
inlineSuggest: {
edits: { allowCodeShifting: 'always' }
}
}
}),
}),
});

View File

@@ -0,0 +1,512 @@
/*---------------------------------------------------------------------------------------------
* 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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,14 @@
"preview": "vite preview"
},
"devDependencies": {
"@vscode/rollup-plugin-esm-url": "^1.0.1-0",
"vite": "^7.1.11"
"@vscode/rollup-plugin-esm-url": "^1.0.1-1",
"vite": "npm:rolldown-vite@latest",
"@vscode/component-explorer": "0.1.1-1",
"@vscode/component-explorer-vite-plugin": "0.1.1-1"
},
"overrides": {
"@vscode/component-explorer-vite-plugin": {
"vite": "$vite"
}
}
}

View File

@@ -10,6 +10,7 @@ import { getSingletonServiceDescriptors, InstantiationType, registerSingleton }
import { IWebWorkerService } from '../../src/vs/platform/webWorker/browser/webWorkerService.ts';
// eslint-disable-next-line local/code-no-standalone-editor
import { StandaloneWebWorkerService } from '../../src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts';
import './style.css';
enableHotReload();

View File

@@ -7,3 +7,9 @@
height: 400px;
border: 1px solid black;
}
@font-face {
font-family: "codicon";
font-display: block;
src: url("~@vscode/codicons/dist/codicon.ttf") format("truetype");
}

View File

@@ -5,9 +5,11 @@
import { createLogger, defineConfig, Plugin } from 'vite';
import path, { join } from 'path';
import { rollupEsmUrlPlugin } from '@vscode/rollup-plugin-esm-url';
// import { componentExplorer } from 'D:/dev/hediet/js-component-explorer/packages/vite-plugin';
import { componentExplorer } from '@vscode/component-explorer-vite-plugin';
import { statSync } from 'fs';
import { pathToFileURL } from 'url';
import { rollupEsmUrlPlugin } from '@vscode/rollup-plugin-esm-url';
function injectBuiltinExtensionsPlugin(): Plugin {
let builtinExtensionsCache: unknown[] | null = null;
@@ -166,9 +168,20 @@ export default defineConfig({
plugins: [
rollupEsmUrlPlugin({}),
injectBuiltinExtensionsPlugin(),
createHotClassSupport()
createHotClassSupport(),
componentExplorer({
logLevel: 'verbose',
include: 'build/vite/**/*.fixture.ts',
}),
],
customLogger: logger,
resolve: {
alias: {
// '@vscode/component-explorer': 'D:/dev/hediet/js-component-explorer/packages/explorer/src',
'@vscode/component-explorer': path.resolve(__dirname, 'node_modules/@vscode/component-explorer'),
'~@vscode/codicons': '/node_modules/@vscode/codicons',
}
},
esbuild: {
tsconfigRaw: {
compilerOptions: {