-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathworkspace-find-files.ts
More file actions
177 lines (165 loc) · 5.48 KB
/
Copy pathworkspace-find-files.ts
File metadata and controls
177 lines (165 loc) · 5.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the
* repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as vscode from 'vscode';
/**
* Schemes with a registered search provider that work with findFiles.
* vscode-test-web registers a FileSearchProvider but it uses a lazy
* in-memory cache that is empty until files are accessed — findFiles
* always returns 0 for it in practice. Use walkDirectory instead.
* memfs has no provider at all; anchoring RelativePattern to it stalls
* the search service indefinitely.
*/
const SEARCH_PROVIDER_SCHEMES = new Set(['file', 'vscode-vfs']);
/**
* Schemes whose filesystem provider supports workspace.fs APIs.
* Used for the directory-traversal fallback when findFiles won't work.
*/
const FS_PROVIDER_SCHEMES = new Set(['vscode-test-web']);
/** Directories to skip during filesystem traversal. */
const SKIP_DIRS = new Set([
'.git',
'node_modules',
'.sfdx',
'.sf',
'.vscode',
'.github',
'.husky',
]);
/**
* Convert the last segment of a glob pattern to a RegExp that matches
* filenames. Handles literal names, * wildcards, and {a,b} alternatives.
* Example: **\/GeocodingService*.cls -> /^GeocodingService[^/]*\.cls$/i
*/
function globSegmentToRegex(pattern: string): RegExp {
const segment = pattern.split('/').pop() ?? pattern;
const expanded = segment.replace(
/\{([^}]+)\}/g,
(_match: string, opts: string) => {
const alternatives = opts
.split(',')
.map((o: string) => o.trim())
.join('|');
return '(' + alternatives + ')';
},
);
const escaped = expanded.replace(/\./g, '\\.').replace(/\*/g, '[^/]*');
return new RegExp('^' + escaped + '$', 'i');
}
/**
* Recursively walk a directory using workspace.fs and collect files whose
* names match the regex. Skips known non-source directories and stops once
* maxResults is reached.
*/
async function walkDirectory(
dirUri: vscode.Uri,
matcher: RegExp,
results: vscode.Uri[],
maxResults: number,
depth: number,
): Promise<void> {
if (results.length >= maxResults || depth > 12) {
return;
}
let entries: [string, vscode.FileType][];
try {
entries = await vscode.workspace.fs.readDirectory(dirUri);
} catch {
return; // directory not accessible — skip silently
}
const subdirs: vscode.Uri[] = [];
for (const [name, type] of entries) {
if (results.length >= maxResults) break;
if (type === vscode.FileType.File) {
if (matcher.test(name)) {
results.push(vscode.Uri.joinPath(dirUri, name));
}
} else if (type === vscode.FileType.Directory && !SKIP_DIRS.has(name)) {
subdirs.push(vscode.Uri.joinPath(dirUri, name));
}
}
for (const sub of subdirs) {
await walkDirectory(sub, matcher, results, maxResults, depth + 1);
}
}
/**
* Find files matching a glob pattern across all workspace folders, using
* the appropriate strategy per scheme:
*
* - `file` / `vscode-vfs`: use vscode.workspace.findFiles with
* RelativePattern (these schemes have search providers).
* - `vscode-test-web`: use workspace.fs.readDirectory traversal because
* the search provider's in-memory cache is lazy and returns 0 until
* files are accessed; direct fs traversal reads from the HTTP server.
* - `memfs` and unknown schemes: skipped (no FS or search provider, or
* anchoring RelativePattern to memfs stalls the search service).
*/
export async function findFilesAcrossWorkspaceFolders(
pattern: string,
exclude?: string | null,
maxResults: number = Infinity,
): Promise<vscode.Uri[]> {
const allFolders = vscode.workspace.workspaceFolders ?? [];
if (allFolders.length === 0) {
return vscode.workspace.findFiles(
pattern,
exclude ?? null,
Number.isFinite(maxResults) ? maxResults : undefined,
);
}
const seen = new Set<string>();
const results: vscode.Uri[] = [];
// Pass 1: findFiles for schemes with a reliable search provider
const searchFolders = allFolders.filter((f) =>
SEARCH_PROVIDER_SCHEMES.has(f.uri.scheme),
);
for (const folder of searchFolders) {
if (results.length >= maxResults) break;
const remaining = maxResults - results.length;
const relPattern = new vscode.RelativePattern(folder, pattern);
try {
const files = await vscode.workspace.findFiles(
relPattern,
exclude ?? null,
Number.isFinite(remaining) ? remaining : undefined,
);
for (const f of files) {
const key = f.toString();
if (!seen.has(key)) {
seen.add(key);
results.push(f);
}
}
} catch {
// search failed for this folder — skip
}
}
// Pass 2: fs.readDirectory traversal for schemes without a reliable search provider
const fsFolders = allFolders.filter((f) =>
FS_PROVIDER_SCHEMES.has(f.uri.scheme),
);
if (fsFolders.length > 0 && results.length < maxResults) {
const matcher = globSegmentToRegex(pattern);
for (const folder of fsFolders) {
if (results.length >= maxResults) break;
await walkDirectory(folder.uri, matcher, results, maxResults, 0);
}
}
// Pass 3: fallback to bare findFiles if no known-scheme folders matched
if (
results.length === 0 &&
searchFolders.length === 0 &&
fsFolders.length === 0
) {
return vscode.workspace.findFiles(
pattern,
exclude ?? null,
Number.isFinite(maxResults) ? maxResults : undefined,
);
}
return results;
}