Path: blob/main/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { mainWindow } from '../../../../../base/browser/window.js';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { Event } from '../../../../../base/common/event.js';8import { ResourceSet } from '../../../../../base/common/map.js';9import { URI } from '../../../../../base/common/uri.js';10import { mock } from '../../../../../base/test/common/mock.js';11import { ICommandService } from '../../../../../platform/commands/common/commands.js';12import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js';13import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';14import { IFileService } from '../../../../../platform/files/common/files.js';15import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js';16import { ILabelService } from '../../../../../platform/label/common/label.js';17import { IListService, ListService } from '../../../../../platform/list/browser/listService.js';18import { IOpenerService } from '../../../../../platform/opener/common/opener.js';19import { IProductService } from '../../../../../platform/product/common/productService.js';20import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';21import { QuickInputService } from '../../../../../platform/quickinput/browser/quickInputService.js';22import { PromptFilePickers } from '../../../../contrib/chat/browser/promptSyntax/pickers/promptFilePickers.js';23import { PromptsType } from '../../../../contrib/chat/common/promptSyntax/promptTypes.js';24import { AgentInstructionFileType, IExtensionPromptPath, IPromptPath, IPromptsService, PromptsStorage, IAgentInstructionFile } from '../../../../contrib/chat/common/promptSyntax/service/promptsService.js';25import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js';26import { ParsedPromptFile } from '../../../../contrib/chat/common/promptSyntax/promptFileParser.js';2728interface IFixturePromptsState {29localPromptFiles: IPromptPath[];30userPromptFiles: IPromptPath[];31extensionPromptFiles: IExtensionPromptPath[];32agentInstructionFiles: IAgentInstructionFile[];33disabled: ResourceSet;34}3536interface RenderPromptPickerOptions extends ComponentFixtureContext {37type: PromptsType;38placeholder: string;39seedData: (state: IFixturePromptsState) => void;40}4142class FixtureQuickInputService extends QuickInputService {43override createQuickPick<T extends IQuickPickItem>(options: { useSeparators: true }): IQuickPick<T, { useSeparators: true }>;44override createQuickPick<T extends IQuickPickItem>(options?: { useSeparators: boolean }): IQuickPick<T, { useSeparators: false }>;45override createQuickPick<T extends IQuickPickItem>(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick<T, { useSeparators: boolean }> {46const quickPick = super.createQuickPick<T>(options) as IQuickPick<T, { useSeparators: boolean }>;47quickPick.ignoreFocusOut = true;48return quickPick;49}50}5152export default defineThemedFixtureGroup({ path: 'chat/' }, {53PromptFiles: defineComponentFixture({54labels: { kind: 'screenshot' },55render: context => renderPromptFilePickerFixture({56...context,57type: PromptsType.prompt,58placeholder: 'Select the prompt file to run',59seedData: promptsService => {60promptsService.localPromptFiles = [61{ uri: URI.file('/workspace/.github/prompts/refactor.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Refactor Prompt', description: 'Refactor selected code' },62{ uri: URI.file('/workspace/.github/prompts/docs.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Docs Prompt', description: 'Generate docs for symbols' },63];64promptsService.userPromptFiles = [65{ uri: URI.file('/home/dev/.copilot/prompts/review.prompt.md'), storage: PromptsStorage.user, type: PromptsType.prompt, name: 'Review Prompt', description: 'Review this change' },66];67},68}),69}),7071InstructionFilesWithAgentInstructions: defineComponentFixture({72labels: { kind: 'screenshot' },73render: context => renderPromptFilePickerFixture({74...context,75type: PromptsType.instructions,76placeholder: 'Select instruction files',77seedData: promptsService => {78promptsService.localPromptFiles = [79{ uri: URI.file('/workspace/.github/instructions/repo.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Repo Rules', description: 'Repository-wide coding rules' },80];81promptsService.agentInstructionFiles = [82{ uri: URI.file('/workspace/AGENTS.md'), realPath: undefined, type: AgentInstructionFileType.agentsMd },83{ uri: URI.file('/workspace/.github/copilot-instructions.md'), realPath: undefined, type: AgentInstructionFileType.copilotInstructionsMd },84];85},86}),87}),88});8990async function renderPromptFilePickerFixture({ container, disposableStore, theme, type, placeholder, seedData }: RenderPromptPickerOptions): Promise<void> {91const quickInputHost = document.createElement('div');92quickInputHost.style.position = 'relative';93const hostWidth = 800;94const hostHeight = 600;95quickInputHost.style.width = `${hostWidth}px`;96quickInputHost.style.height = `${hostHeight}px`;97quickInputHost.style.minHeight = `${hostHeight}px`;98quickInputHost.style.overflow = 'hidden';99container.appendChild(quickInputHost);100101const promptsState: IFixturePromptsState = {102localPromptFiles: [],103userPromptFiles: [],104extensionPromptFiles: [],105agentInstructionFiles: [],106disabled: new ResourceSet(),107};108seedData(promptsState);109110const promptsService = new class extends mock<IPromptsService>() {111override async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, _token: CancellationToken): Promise<readonly IPromptPath[]> {112switch (storage) {113case PromptsStorage.local:114return promptsState.localPromptFiles.filter(file => file.type === type);115case PromptsStorage.user:116return promptsState.userPromptFiles.filter(file => file.type === type);117case PromptsStorage.extension:118return promptsState.extensionPromptFiles.filter(file => file.type === type);119case PromptsStorage.plugin:120return [];121default:122return [];123}124}125126override async listAgentInstructions(_token: CancellationToken): Promise<IAgentInstructionFile[]> {127return promptsState.agentInstructionFiles;128}129130override async parseNew(_uri: URI, _token: CancellationToken): Promise<ParsedPromptFile> {131throw new Error('Not implemented');132}133134override getDisabledPromptFiles(_type: PromptsType): ResourceSet {135return promptsState.disabled;136}137138override setDisabledPromptFiles(_type: PromptsType, uris: ResourceSet): void {139promptsState.disabled = uris;140}141};142143const layoutService = new class extends mock<ILayoutService>() {144override activeContainer = quickInputHost;145override get activeContainerDimension() { return { width: hostWidth, height: hostHeight }; }146override activeContainerOffset = { top: 0, quickPickTop: 20 };147override mainContainer = quickInputHost;148override get mainContainerDimension() { return { width: hostWidth, height: hostHeight }; }149override mainContainerOffset = { top: 0, quickPickTop: 20 };150override containers = [quickInputHost];151override onDidLayoutMainContainer = Event.None;152override onDidLayoutContainer = Event.None;153override onDidLayoutActiveContainer = Event.None;154override onDidAddContainer = Event.None;155override onDidChangeActiveContainer = Event.None;156override getContainer(): HTMLElement {157return quickInputHost;158}159override whenContainerStylesLoaded(): Promise<void> | undefined {160return undefined;161}162override focus(): void { }163};164165const contextMenuService = new class extends mock<IContextMenuService>() {166override onDidShowContextMenu = Event.None;167override onDidHideContextMenu = Event.None;168override showContextMenu(): void { }169};170171const contextViewService = new class extends mock<IContextViewService>() {172override anchorAlignment = 0;173override showContextView() { return { close: () => { } }; }174override hideContextView(): void { }175override getContextViewElement(): HTMLElement { return quickInputHost; }176override layout(): void { }177};178179const instantiationService = createEditorServices(disposableStore, {180colorTheme: theme,181additionalServices: registration => {182registration.defineInstance(ILayoutService, layoutService);183registration.defineInstance(IContextMenuService, contextMenuService);184registration.defineInstance(IContextViewService, contextViewService);185registration.define(IListService, ListService);186registration.define(IQuickInputService, FixtureQuickInputService);187registration.defineInstance(IPromptsService, promptsService);188registration.defineInstance(IOpenerService, new class extends mock<IOpenerService>() { });189registration.defineInstance(IFileService, new class extends mock<IFileService>() { });190registration.defineInstance(IDialogService, new class extends mock<IDialogService>() { });191registration.defineInstance(ICommandService, new class extends mock<ICommandService>() { });192registration.defineInstance(ILabelService, new class extends mock<ILabelService>() {193override getUriLabel(uri: URI): string {194return uri.path;195}196});197registration.defineInstance(IProductService, new class extends mock<IProductService>() { });198}199});200201const pickers = instantiationService.createInstance(PromptFilePickers);202203void pickers.selectPromptFile({204placeholder,205type,206});207208// Wait for the quickpick widget to render and have dimensions209const quickInputWidget = await waitForElement<HTMLElement>(210quickInputHost,211'.quick-input-widget',212el => el.offsetWidth > 0 && el.offsetHeight > 0213);214215if (quickInputWidget) {216// Reset positioning217quickInputWidget.style.position = 'relative';218quickInputWidget.style.top = '0';219quickInputWidget.style.left = '0';220221// Move widget to container and remove host222container.appendChild(quickInputWidget);223quickInputHost.remove();224225// Set explicit dimensions on container to match widget226const rect = quickInputWidget.getBoundingClientRect();227container.style.width = `${rect.width}px`;228container.style.height = `${rect.height}px`;229}230}231232async function waitForElement<T extends HTMLElement>(233root: HTMLElement,234selector: string,235condition: (el: T) => boolean,236timeout = 2000237): Promise<T | null> {238const start = Date.now();239while (Date.now() - start < timeout) {240const el = root.querySelector<T>(selector);241if (el && condition(el)) {242// Wait one more frame to ensure layout is complete243await new Promise(resolve => mainWindow.requestAnimationFrame(resolve));244return el;245}246await new Promise(resolve => setTimeout(resolve, 10));247}248return root.querySelector<T>(selector);249}250251252