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