Skip to content

Commit d1e632d

Browse files
authored
event handling for custom chat modes (microsoft#250012)
* event handling for custom chat modes * fix tests
1 parent 0011ac8 commit d1e632d

File tree

4 files changed

+90
-33
lines changed

4 files changed

+90
-33
lines changed

src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { basename } from '../../../../../../base/common/path.js';
1616
import { ResourceSet } from '../../../../../../base/common/map.js';
1717
import { PromptFilesLocator } from '../utils/promptFilesLocator.js';
1818
import { Disposable } from '../../../../../../base/common/lifecycle.js';
19-
import { Emitter, Event } from '../../../../../../base/common/event.js';
19+
import { Event } from '../../../../../../base/common/event.js';
2020
import { type ITextModel } from '../../../../../../editor/common/model.js';
2121
import { ObjectCache } from '../../../../../../base/common/objectCache.js';
2222
import { ILogService } from '../../../../../../platform/log/common/log.js';
@@ -52,6 +52,11 @@ export class PromptsService extends Disposable implements IPromptsService {
5252
*/
5353
public logTime: TLogFunction;
5454

55+
/**
56+
* Lazily created event that is fired when the custom chat modes change.
57+
*/
58+
private onDidChangeCustomChatModesEvent: Event<void> | undefined;
59+
5560
constructor(
5661
@ILogService public readonly logger: ILogService,
5762
@ILabelService private readonly labelService: ILabelService,
@@ -61,7 +66,8 @@ export class PromptsService extends Disposable implements IPromptsService {
6166
) {
6267
super();
6368

64-
this.fileLocator = this.instantiationService.createInstance(PromptFilesLocator);
69+
this.fileLocator = this._register(this.instantiationService.createInstance(PromptFilesLocator));
70+
6571
this.logTime = this.logger.trace.bind(this.logger);
6672

6773
// the factory function below creates a new prompt parser object
@@ -95,6 +101,17 @@ export class PromptsService extends Disposable implements IPromptsService {
95101
);
96102
}
97103

104+
/**
105+
* Emitter for the custom chat modes change event.
106+
*/
107+
public get onDidChangeCustomChatModes(): Event<void> {
108+
if (!this.onDidChangeCustomChatModesEvent) {
109+
this.onDidChangeCustomChatModesEvent = this.fileLocator.getFilesUpdatedEvent(PromptsType.mode);
110+
}
111+
return this.onDidChangeCustomChatModesEvent;
112+
}
113+
114+
98115
/**
99116
* @throws {Error} if:
100117
* - the provided model is disposed
@@ -184,10 +201,6 @@ export class PromptsService extends Disposable implements IPromptsService {
184201
});
185202
}
186203

187-
private readonly _onDidChangeCustomChatModesEmitter: Emitter<void> = new Emitter<void>();
188-
// todo: firing events not yet implemented
189-
public readonly onDidChangeCustomChatModes: Event<void> = this._onDidChangeCustomChatModesEmitter.event;
190-
191204
@logTime()
192205
public async getCustomChatModes(): Promise<readonly ICustomChatMode[]> {
193206
const modeFiles = (await this.listPromptFiles(PromptsType.mode, CancellationToken.None))

src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@ import { PromptsConfig } from '../../../../../../platform/prompts/common/config.
1212
import { basename, dirname, joinPath } from '../../../../../../base/common/resources.js';
1313
import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js';
1414
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
15-
import { getPromptFileExtension, getPromptFileType, PromptsType } from '../../../../../../platform/prompts/common/prompts.js';
15+
import { getPromptFileExtension, getPromptFileLocationsConfigKey, getPromptFileType, PromptsType } from '../../../../../../platform/prompts/common/prompts.js';
1616
import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js';
1717
import { Schemas } from '../../../../../../base/common/network.js';
1818
import { getExcludes, IFileQuery, ISearchConfiguration, ISearchService, QueryType } from '../../../../../services/search/common/search.js';
1919
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
2020
import { isCancellationError } from '../../../../../../base/common/errors.js';
2121
import { TPromptsStorage } from '../service/types.js';
2222
import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js';
23+
import { Emitter, Event } from '../../../../../../base/common/event.js';
24+
import { Disposable } from '../../../../../../base/common/lifecycle.js';
2325

2426
/**
2527
* Utility class to locate prompt files.
2628
*/
27-
export class PromptFilesLocator {
29+
export class PromptFilesLocator extends Disposable {
2830

2931
constructor(
3032
@IFileService private readonly fileService: IFileService,
@@ -33,7 +35,9 @@ export class PromptFilesLocator {
3335
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
3436
@ISearchService private readonly searchService: ISearchService,
3537
@IUserDataProfileService private readonly userDataService: IUserDataProfileService,
36-
) { }
38+
) {
39+
super();
40+
}
3741

3842
/**
3943
* List all prompt files from the filesystem.
@@ -42,9 +46,7 @@ export class PromptFilesLocator {
4246
*/
4347
public async listFiles(type: PromptsType, storage: TPromptsStorage, token: CancellationToken): Promise<readonly URI[]> {
4448
if (storage === 'local') {
45-
const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type);
46-
const absoluteLocations = this.toAbsoluteLocations(configuredLocations);
47-
return await this.findFilesInLocations(absoluteLocations, type, token);
49+
return await this.listFilesInLocal(type, token);
4850
} else {
4951
return await this.listFilesInUserData(type, token);
5052
}
@@ -55,6 +57,29 @@ export class PromptFilesLocator {
5557
return files.filter(file => getPromptFileType(file) === type);
5658
}
5759

60+
public getFilesUpdatedEvent(type: PromptsType): Event<void> {
61+
const eventEmitter = this._register(new Emitter<void>());
62+
const key = getPromptFileLocationsConfigKey(type);
63+
let parentFolders = this.getLocalParentFolders(type).map(folder => folder.parent);
64+
this._register(this.configService.onDidChangeConfiguration(e => {
65+
if (e.affectsConfiguration(key)) {
66+
parentFolders = this.getLocalParentFolders(type).map(folder => folder.parent);
67+
eventEmitter.fire();
68+
}
69+
}));
70+
this._register(this.fileService.onDidFilesChange(e => {
71+
if (e.affects(this.userDataService.currentProfile.promptsHome)) {
72+
eventEmitter.fire();
73+
return;
74+
}
75+
if (parentFolders.some(folder => e.affects(folder))) {
76+
eventEmitter.fire();
77+
return;
78+
}
79+
}));
80+
return eventEmitter.event;
81+
}
82+
5883
/**
5984
* Get all possible unambiguous prompt file source folders based on
6085
* the current workspace folder structure.
@@ -107,29 +132,19 @@ export class PromptFilesLocator {
107132
}
108133

109134
/**
110-
* Finds all existent prompt files in the provided source folders.
111-
*
112-
* @throws if any of the provided folder paths is not an `absolute path`.
135+
* Finds all existent prompt files in the configured local source folders.
113136
*
114-
* @param absoluteLocations List of prompt file source folders to search for prompt files in. Must be absolute paths.
115-
* @returns List of prompt files found in the provided source folders.
137+
* @returns List of prompt files found in the local source folders.
116138
*/
117-
private async findFilesInLocations(
118-
absoluteLocations: readonly URI[],
139+
private async listFilesInLocal(
119140
type: PromptsType,
120141
token: CancellationToken
121142
): Promise<readonly URI[]> {
122143
// find all prompt files in the provided locations, then match
123144
// the found file paths against (possible) glob patterns
124145
const paths = new ResourceSet();
125-
for (const absoluteLocation of absoluteLocations) {
126-
assert(
127-
isAbsolute(absoluteLocation.path),
128-
`Provided location must be an absolute path, got '${absoluteLocation.path}'.`,
129-
);
130-
131-
const { parent, filePattern } = firstNonGlobParentAndPattern(absoluteLocation);
132146

147+
for (const { parent, filePattern } of this.getLocalParentFolders(type)) {
133148
const files = (filePattern === undefined)
134149
? await this.resolveFilesAtLocation(parent, token) // if the location does not contain a glob pattern, resolve the location directly
135150
: await this.searchFilesInLocation(parent, filePattern, token);
@@ -146,6 +161,12 @@ export class PromptFilesLocator {
146161
return [...paths];
147162
}
148163

164+
private getLocalParentFolders(type: PromptsType): readonly { parent: URI; filePattern?: string }[] {
165+
const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type);
166+
const absoluteLocations = this.toAbsoluteLocations(configuredLocations);
167+
return absoluteLocations.map(firstNonGlobParentAndPattern);
168+
}
169+
149170
/**
150171
* Converts locations defined in `settings` to absolute filesystem path URIs.
151172
* This conversion is needed because locations in settings can be relative,
@@ -330,7 +351,7 @@ export const isValidGlob = (pattern: string): boolean => {
330351
*/
331352
const firstNonGlobParentAndPattern = (
332353
location: URI
333-
): { parent: URI; filePattern: string | undefined } => {
354+
): { parent: URI; filePattern?: string } => {
334355
const segments = location.path.split('/');
335356
let i = 0;
336357
while (i < segments.length && isValidGlob(segments[i]) === false) {
@@ -339,18 +360,16 @@ const firstNonGlobParentAndPattern = (
339360
if (i === segments.length) {
340361
// the path does not contain a glob pattern, so we can
341362
// just find all prompt files in the provided location
342-
return { parent: location, filePattern: undefined };
363+
return { parent: location };
343364
}
365+
const parent = location.with({ path: segments.slice(0, i).join('/') });
344366
if (i === segments.length - 1 && segments[i] === '*' || segments[i] === ``) {
345-
return {
346-
parent: location.with({ path: segments.slice(0, i).join('/') }),
347-
filePattern: undefined
348-
};
367+
return { parent };
349368
}
350369

351370
// the path contains a glob pattern, so we search in last folder that does not contain a glob pattern
352371
return {
353-
parent: location.with({ path: segments.slice(0, i).join('/') }),
372+
parent,
354373
filePattern: segments.slice(i).join('/')
355374
};
356375
};

src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/
2929
import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
3030
import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js';
3131
import { INSTRUCTION_FILE_EXTENSION, PROMPT_FILE_EXTENSION, PromptsType } from '../../../../../../../platform/prompts/common/prompts.js';
32+
import { IWorkspacesService } from '../../../../../../../platform/workspaces/common/workspaces.js';
3233

3334
/**
3435
* Helper class to assert the properties of a link.
@@ -106,6 +107,7 @@ suite('PromptsService', () => {
106107
setup(async () => {
107108
instaService = disposables.add(new TestInstantiationService());
108109
instaService.stub(ILogService, new NullLogService());
110+
instaService.stub(IWorkspacesService, {});
109111
instaService.stub(IConfigurationService, new TestConfigurationService());
110112

111113
const fileService = disposables.add(instaService.createInstance(FileService));

0 commit comments

Comments
 (0)