promptFilesLocator.ts ×31

Frontier kind: Code frontier

unlabeled · c_d48d2c80853b

323 tests · 24352 LOC · 129 files · introduces 0 tests · 261 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
31 ranges261 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2590 ranges24352 lines · 129 files · Browse complete extent
All tests (intent)
323 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 261 introduced LOC across 31 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts 261 introduced LOC · 31 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- promptFilesLocator.ts
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 { URI } from '../../../../../../base/common/uri.js';
7 > import { isAbsolute } from '../../../../../../base/common/path.js';
8 > import { ResourceSet } from '../../../../../../base/common/map.js';
9 > import * as nls from '../../../../../../nls.js';
10 > import { FileOperation, FileOperationError, FileOperationResult, IFileService } from '../../../../../../platform/files/common/files.js';
11 > import { getPromptFileLocationsConfigKey, isTildePath, PromptsConfig } from '../config/config.js';
12 > import { basename, dirname, isEqual, isEqualOrParent, joinPath } from '../../../../../../base/common/resources.js';
13 > import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js';
14 > import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
15 > import { AGENTS_SOURCE_FOLDER, CLAUDE_CONFIG_FOLDER, COPILOT_CONFIG_FOLDER, GITHUB_CONFIG_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, IResolvedPromptSourceFolder } from '../config/promptFileLocations.js';
16 > import { PromptFileSource, PromptsType } from '../promptTypes.js';
17 > import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js';
18 > import { Schemas } from '../../../../../../base/common/network.js';
19 > import { getExcludes, IFileQuery, ISearchConfiguration, ISearchService, QueryType } from '../../../../../services/search/common/search.js';
20 > import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js';
21 > import { isCancellationError } from '../../../../../../base/common/errors.js';
22 > import { AgentInstructionFileType, IPromptPath, IAgentInstructionFile, Logger, PromptsStorage } from '../service/promptsService.js';
23 > import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js';
24 > import { Emitter, Event } from '../../../../../../base/common/event.js';
25 > import { DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js';
26 > import { ILogService } from '../../../../../../platform/log/common/log.js';
27 > import { IPathService } from '../../../../../services/path/common/pathService.js';
28 > import { equalsIgnoreCase } from '../../../../../../base/common/strings.js';
29 > import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js';
30 > import { AGENT_HOST_SCHEME } from '../../../../../../platform/agentHost/common/agentHostUri.js';
31 >
32 > /**
33 > * Maximum recursion depth when traversing subdirectories for instruction files.
34 > */
35 > const MAX_INSTRUCTIONS_RECURSION_DEPTH = 5;
36 >
37 > export interface IWorkspaceInstructionFile {
38 > readonly fileName: string;
39 > readonly type: AgentInstructionFileType;
40 > }
41 >
42 > /**
43 > * Utility class to locate prompt files.
44 > */
45 > export class PromptFilesLocator {
46 >
47 > private readonly userDataFolder: IResolvedPromptSourceFolder;
48 >
49 > constructor(
50 @IFileService private readonly fileService: IFileService,
51 @IConfigurationService private readonly configService: IConfigurationService,
70 };
71 }
73 > protected getWorkspaceFolders(): readonly IWorkspaceFolder[] {
74 // Agent host workspace folders surface customizations through AHP
75 // (session state + findAgentSkills), not via filesystem scanning.
79 return this.workspaceService.getWorkspace().folders.filter(f => f.uri.scheme !== AGENT_HOST_SCHEME);
80 }
82 > protected getWorkspaceFolder(resource: URI): IWorkspaceFolder | undefined {
83 return this.workspaceService.getWorkspaceFolder(resource) ?? undefined;
84 }
86 > protected onDidChangeWorkspaceFolders(): Event<void> {
87 return Event.map(this.workspaceService.onDidChangeWorkspaceFolders, () => undefined);
88 }
90 > /**
91 > * Returns the configured prompt source folders for the given type.
92 > * Subclasses can override to filter out unsupported sources.
93 > */
94 > protected getPromptSourceFolders(type: PromptsType): IPromptSourceFolder[] {
95 return PromptsConfig.promptSourceFolders(this.configService, type);
96 }
98 > /**
99 > * Returns the default prompt source folders for the given type.
100 > * Subclasses can override to filter out unsupported sources.
101 > */
102 > protected getDefaultSourceFolders(type: PromptsType): readonly IPromptSourceFolder[] {
103 return getPromptFileDefaultLocations(type);
104 }
106 > public async getWorkspaceFolderRoots(includeParents: boolean, logger?: Logger): Promise<URI[]> {
107 const workspaceFolders = this.getWorkspaceFolders();
108 if (includeParents) {
123 return workspaceFolders.map(f => f.uri);
124 }
126 > /**
127 > * Walks up from {@link folderUri} collecting parent folders until a
128 > * repository root (a folder containing `.git`) is found. Returns the
129 > * intermediate parent folders only when a repo root is found; returns
130 > * an empty array when the walk reaches the filesystem root, the user
131 > * home directory, or a folder already present in {@link seen}.
132 > */
133 > private async findParentRepoFolders(folderUri: URI, userHome: URI, seen: ResourceSet, logger?: Logger): Promise<URI[]> {
134 const candidates: URI[] = [];
135 let current = folderUri;
164 return [];
165 }
167 > /**
168 > * List all prompt files from the filesystem.
169 > *
170 > * @returns List of prompt files found in the workspace.
171 > */
172 > public async listFiles(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise<readonly URI[]> {
173 if (storage !== PromptsStorage.user && storage !== PromptsStorage.local) {
174 throw new Error(`Unsupported prompt file storage: ${storage}`);
199 return [...paths];
200 }
202 > public createFilesUpdatedEvent(type: PromptsType): { readonly event: Event<void>; dispose: () => void } {
203 const disposables = new DisposableStore();
204 const eventEmitter = disposables.add(new Emitter<void>());
265 return { event: eventEmitter.event, dispose: () => disposables.dispose() };
266 }
268 > public createAgentInstructionsUpdatedEvent(): { readonly event: Event<void>; dispose: () => void } {
269 const disposables = new DisposableStore();
270 const eventEmitter = disposables.add(new Emitter<void>());
372 return { event: eventEmitter.event, dispose: () => disposables.dispose() };
373 }
375 > /**
376 > * Gets the hook source folders for creating new hooks.
377 > * Returns configured hook folders, excluding Claude paths (which are read-only).
378 > */
379 > public async getHookSourceFolders(): Promise<readonly IResolvedPromptSourceFolder[]> {
380 const configuredLocations = this.getPromptSourceFolders(PromptsType.hook);
381
404 return result;
405 }
407 > /**
408 > * Get all possible unambiguous prompt file source folders based on
409 > * the current workspace folder structure.
410 > *
411 > * This method is currently primarily used by the `> Create Prompt`
412 > * command that providers users with the list of destination folders
413 > * for a newly created prompt file. Because such a list cannot contain
414 > * paths that include `glob pattern` in them, we need to process config
415 > * values and try to create a list of clear and unambiguous locations.
416 > *
417 > * @returns List of possible unambiguous prompt file folders.
418 > */
419 > public async getConfigBasedSourceFolders(type: PromptsType): Promise<readonly URI[]> {
420 const configuredLocations = this.getPromptSourceFolders(type);
421 const absoluteLocations = await this.toAbsoluteLocations(type, configuredLocations);
461 return [...result];
462 }
464 > /**
465 > * Gets all resolved source folders for the given prompt type with metadata.
466 > * This method merges configured locations with default locations and resolves them
467 > * to absolute paths, including displayPath and isDefault information.
468 > *
469 > * The returned order prefers workspace (local) folders first, then user folders.
470 > * This is used for UX like the "Create Prompt" command where workspace is preferred.
471 > *
472 > * @param type The type of prompt files.
473 > * @returns List of resolved source folders with metadata.
474 > */
475 > public async getResolvedSourceFolders(type: PromptsType): Promise<readonly IResolvedPromptSourceFolder[]> {
476 const absoluteLocations = await this.getLocalStorageFolders(type);
477
480 return this.dedupeSourceFolders([...localFolders, ...userFolders]);
481 }
483 > /**
484 > * Gets all resolved source folders in the same order that file discovery
485 > * searches them (user folders first, then local/workspace folders).
486 > * This matches the order used by {@link listFiles} and should be used
487 > * for debug/diagnostic output so the displayed order is accurate.
488 > */
489 > public async getSourceFoldersInDiscoveryOrder(type: PromptsType): Promise<readonly IResolvedPromptSourceFolder[]> {
490 const absoluteLocations = await this.getLocalStorageFolders(type);
491 const userFolders = absoluteLocations.filter(loc => loc.storage === PromptsStorage.user);
493 return this.dedupeSourceFolders([...userFolders, ...localFolders]);
494 }
496 > /**
497 > * Gets all local (workspace) storage folders for the given prompt type.
498 > * This merges default folders with configured locations.
499 > */
500 > private async getLocalStorageFolders(type: PromptsType): Promise<readonly IResolvedPromptSourceFolder[]> {
501 const configuredLocations = this.getPromptSourceFolders(type);
502 const defaultFolders = this.getDefaultSourceFolders(type);
514 return absoluteLocations;
515 }
517 > /**
518 > * Deduplicates source folders by URI.
519 > */
520 > private dedupeSourceFolders(folders: readonly IResolvedPromptSourceFolder[]): IResolvedPromptSourceFolder[] {
521 const seen = new ResourceSet();
522 const result: IResolvedPromptSourceFolder[] = [];
529 return result;
530 }
532 > /**
533 > * Converts locations defined in `settings` to absolute filesystem path URIs with metadata.
534 > * This conversion is needed because locations in settings can be relative,
535 > * hence we need to resolve them based on the current workspace folders.
536 > * If userHome is provided, paths starting with `~` will be expanded. Otherwise these paths are ignored.
537 > * Preserves the type and location properties from the source folder definitions.
538 > */
539 > private async toAbsoluteLocations(type: PromptsType, configuredLocations: readonly IPromptSourceFolder[], defaultLocations?: readonly IPromptSourceFolder[]): Promise<IResolvedPromptSourceFolder[]> {
540 const result: IResolvedPromptSourceFolder[] = [];
541 const seen = new ResourceSet();
614 return result;
615 }
617 > /**
618 > * Uses the file service to resolve the provided location and return either the file at the location of files in the directory.
619 > * For instruction folders, this searches recursively (up to {@link MAX_INSTRUCTIONS_RECURSION_DEPTH} levels deep) provided
620 > * the location is not a workspace folder root and does not contain wildcards, to support subdirectories while avoiding
621 > * accidentally broad traversal.
622 > */
623 > private async resolveFilesAtLocation(location: URI, type: PromptsType, token: CancellationToken, depth: number = 0): Promise<URI[]> {
624 if (type === PromptsType.skill) {
625 return this.findAgentSkillsInFolder(location, token);
663 return [];
664 }
666 > /**
667 > * Uses the search service to find all files at the provided location.
668 > * Requires a FileSearchProvider to be available for the folder's scheme.
669 > */
670 > private async searchFilesInLocation(folder: URI, filePattern: string | undefined, token: CancellationToken): Promise<URI[]> {
671 // Check if a FileSearchProvider is available for this scheme
672 if (!this.searchService.schemeHasFileSearchProvider(folder.scheme)) {
703 return [];
704 }
706 > /**
707 > * Gets list of `AGENTS.md` files anywhere in the workspace.
708 > */
709 > public async findAgentMDsInWorkspace(token: CancellationToken): Promise<IAgentInstructionFile[]> {
710 const result = await Promise.all(this.getWorkspaceFolders().map(folder => this.findAgentMDsInFolder(folder.uri, token)));
711 return result.flat(1);
712 }
714 > private async findAgentMDsInFolder(folder: URI, token: CancellationToken): Promise<IAgentInstructionFile[]> {
715 // Check if a FileSearchProvider is available for this scheme
716 if (this.searchService.schemeHasFileSearchProvider(folder.scheme)) {
750 }
751 }
753 > /**
754 > * Recursively traverses a folder using the file service to find AGENTS.md files.
755 > * This is used as a fallback when no FileSearchProvider is available for the scheme.
756 > */
757 > private async findAgentMDsUsingFileService(folder: URI, token: CancellationToken): Promise<IAgentInstructionFile[]> {
758 const result: IAgentInstructionFile[] = [];
759 const agentsMdFileName = 'agents.md';
784 return result;
785 }
787 >
788 >
789 > public async findFilesInRoots(roots: URI[], folder: string | undefined, paths: IWorkspaceInstructionFile[], token: CancellationToken, result: IAgentInstructionFile[] = []): Promise<IAgentInstructionFile[]> {
790 const toResolve = roots.map(root => ({ resource: folder !== undefined ? joinPath(root, folder) : root }));
791 const resolvedRoots = await this.fileService.resolveAll(toResolve);
808 return result;
809 }
811 > public getAgentFileURIFromModeFile(oldURI: URI): URI | undefined {
812 if (oldURI.path.endsWith(LEGACY_MODE_FILE_EXTENSION)) {
813 let newLocation;
822 return undefined;
823 }
825 > private async findAgentSkillsInFolder(uri: URI, token: CancellationToken): Promise<URI[]> {
826 try {
827 const result: URI[] = [];
854 }
855 }
857 > /**
858 > * Searches for skills in all configured locations.
859 > */
860 > public async findAgentSkills(token: CancellationToken): Promise<IPromptPath[]> {
861 const configuredLocations = this.getPromptSourceFolders(PromptsType.skill);
862 const absoluteLocations = await this.toAbsoluteLocations(PromptsType.skill, configuredLocations);
875 return allResults;
876 }
878 >
879 >
880 > /**
881 > * Checks if the provided path contains a glob pattern (* or **).
882 > * Used to detect deprecated glob usage in prompt file locations.
883 > *
884 > * @param path - path to check
885 > * @returns `true` if the path contains `*` or `**`, `false` otherwise
886 > */
887 > export function hasGlobPattern(path: string): boolean {
888 return path.includes('*');
889 }
891 >
892 > /**
893 > * Checks if the provided `pattern` could be a valid glob pattern.
894 > */
895 > export function isValidGlob(pattern: string): boolean {
896 let squareBrackets = false;
897 let squareBracketsCount = 0;
959 return false;
960 }
962 > interface ISearchLocationResult {
963 > readonly searchRoot: URI;
964 > readonly filePattern?: string;
965 > }
966 >
967 > /**
968 > * Resolves the search root and optional file pattern for the provided location.
969 > * For paths with glob patterns, finds the deepest non-glob ancestor directory.
970 > *
971 > * Assumes that the location that is provided has a valid path (is abstract)
972 > *
973 > * ## Examples
974 > *
975 > * ```typescript
976 > * assert.strictDeepEqual(
977 > * resolveSearchLocation(PromptsType.prompt, URI.file('/home/user/{folder1,folder2}/file.md')),
978 > * { searchRoot: URI.file('/home/user'), filePattern: '{folder1,folder2}/file.md' },
979 > * 'Must find correct non-glob search root.',
980 > * );
981 > * ```
982 > */
983 function resolveSearchLocation(type: PromptsType, location: URI): ISearchLocationResult {
984 if (type !== PromptsType.instructions && type !== PromptsType.prompt) {
1008 };
1009 }
1011 >
1012 > /**
1013 > * Regex pattern string for validating paths for all prompt files.
1014 > * Paths only support:
1015 > * - Relative paths: someFolder, ./someFolder
1016 > * - User home paths: ~/folder (only forward slash, not backslash for cross-platform sharing)
1017 > * - Parent relative paths for monorepos: ../folder
1018 > *
1019 > * NOT supported:
1020 > * - Absolute paths (portability issue)
1021 > * - Glob patterns with * or ** (performance issue)
1022 > * - Backslashes (paths should be shareable in repos across platforms)
1023 > * - Tilde without forward slash (e.g., ~abc, ~\folder)
1024 > * - Empty or whitespace-only paths
1025 > *
1026 > * The regex validates:
1027 > * - Not a Windows absolute path (e.g., C:\, C:/)
1028 > * - Not starting with / (Unix absolute path)
1029 > * - No backslashes anywhere (use forward slashes only)
1030 > * - If starts with ~, must be followed by /
1031 > * - No glob pattern characters: * ? [ ] { }
1032 > * - At least one non-whitespace character
1033 > */
1034 > export const VALID_PROMPT_FOLDER_PATTERN = '^(?![A-Za-z]:[\\\\/])(?!/)(?!~(?!/))(?!.*\\\\)(?!.*[*?\\[\\]{}]).*\\S.*$';
1035 > const VALID_PROMPT_FOLDER_REGEX = new RegExp(VALID_PROMPT_FOLDER_PATTERN);
1036 >
1037 > /**
1038 > * Validates if a path is allowed for simplified path configurations.
1039 > * Only forward slashes are supported to ensure paths are shareable across platforms.
1040 > */
1041 > export function isValidPromptFolderPath(path: string): boolean {
1042 return VALID_PROMPT_FOLDER_REGEX.test(path);
1043 }