Skip to content

Commit 6824497

Browse files
fix: convert walkDir to an async generator (#1783)
* walkdir to async generator * add yields * handle ignore files * Update CodebaseIndexer.ts * Update walkDir.ts
1 parent ff8be81 commit 6824497

File tree

2 files changed

+115
-112
lines changed

2 files changed

+115
-112
lines changed

core/indexing/walkDir.ts

+114-111
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Walker extends EventEmitter {
6262

6363
emit(ev: string, data: any): boolean {
6464
let ret = false;
65+
6566
if (!(this.sawError && ev === "error")) {
6667
if (ev === "error") {
6768
this.sawError = true;
@@ -81,73 +82,67 @@ class Walker extends EventEmitter {
8182
return ret;
8283
}
8384

84-
start(): this {
85-
this.ide
86-
.listDir(this.path)
87-
.then((entries) => {
88-
this.onReaddir(entries);
89-
})
90-
.catch((err) => {
91-
this.emit("error", err);
92-
});
93-
return this;
85+
async *start() {
86+
try {
87+
const entries = await this.ide.listDir(this.path);
88+
89+
for await (const result of this.onReadDir(entries)) {
90+
yield result;
91+
}
92+
} catch (err) {
93+
this.emit("error", err);
94+
}
9495
}
9596

9697
isIgnoreFile(e: Entry): boolean {
9798
const p = e[0];
9899
return p !== "." && p !== ".." && this.ignoreFiles.indexOf(p) !== -1;
99100
}
100101

101-
onReaddir(entries: Entry[]): void {
102+
async *onReadDir(entries: Entry[]) {
102103
this.entries = entries;
104+
103105
if (entries.length === 0) {
104106
if (this.includeEmpty) {
105107
this.result.add(this.path.slice(this.root.length + 1));
106108
}
107109
this.emit("done", this.result);
110+
yield this.result;
108111
} else {
109112
const hasIg = this.entries.some((e) => this.isIgnoreFile(e));
110113

111114
if (hasIg) {
112-
this.addIgnoreFiles();
113-
} else {
114-
this.filterEntries();
115+
await this.addIgnoreFiles();
115116
}
117+
118+
yield* this.filterEntries();
116119
}
117120
}
118121

119-
addIgnoreFiles(): void {
122+
async addIgnoreFiles() {
120123
const newIg = this.entries!.filter((e) => this.isIgnoreFile(e));
121-
122-
let igCount = newIg.length;
123-
const then = () => {
124-
if (--igCount === 0) {
125-
this.filterEntries();
126-
}
127-
};
128-
129-
newIg.forEach((e) => this.addIgnoreFile(e, then));
124+
await Promise.all(newIg.map((e) => this.addIgnoreFile(e)));
130125
}
131126

132-
addIgnoreFile(file: Entry, then: () => void): void {
133-
const ig = path.resolve(this.path, file[0]);
134-
this.ide
135-
.readFile(ig)
136-
.then((data) => {
137-
this.onReadIgnoreFile(file, data, then);
138-
})
139-
.catch((err) => {
140-
this.emit("error", err);
141-
});
127+
async addIgnoreFile(fileEntry: Entry) {
128+
const ig = path.resolve(this.path, fileEntry[0]);
129+
130+
try {
131+
const file = await this.ide.readFile(ig);
132+
this.onReadIgnoreFile(fileEntry, file);
133+
} catch (err) {
134+
this.emit("error", err);
135+
}
142136
}
143137

144-
onReadIgnoreFile(file: Entry, data: string, then: () => void): void {
138+
onReadIgnoreFile(file: Entry, data: string): void {
145139
const mmopt = {
146140
matchBase: true,
147141
dot: true,
148142
flipNegate: true,
149143
nocase: true,
150144
};
145+
151146
const rules = data
152147
.split(/\r?\n/)
153148
.filter((line) => !/^#|^$/.test(line.trim()))
@@ -156,8 +151,6 @@ class Walker extends EventEmitter {
156151
});
157152

158153
this.ignoreRules[file[0]] = rules;
159-
160-
then();
161154
}
162155

163156
addIgnoreRules(rules: string[]) {
@@ -167,6 +160,7 @@ class Walker extends EventEmitter {
167160
flipNegate: true,
168161
nocase: true,
169162
};
163+
170164
const minimatchRules = rules
171165
.filter((line) => !/^#|^$/.test(line.trim()))
172166
.map((rule) => {
@@ -176,15 +170,22 @@ class Walker extends EventEmitter {
176170
this.ignoreRules[".defaultignore"] = minimatchRules;
177171
}
178172

179-
filterEntries(): void {
180-
const filtered = this.entries!.map((entry) => {
181-
const passFile = this.filterEntry(entry[0]);
182-
const passDir = this.filterEntry(entry[0], true);
183-
return passFile || passDir ? [entry, passFile, passDir] : false;
184-
}).filter((e) => e) as [Entry, boolean, boolean][];
173+
async *filterEntries() {
174+
const filtered = (await Promise.all(
175+
this.entries!.map(async (entry) => {
176+
const passFile = await this.filterEntry(entry[0]);
177+
const passDir = await this.filterEntry(entry[0], true);
178+
return passFile || passDir ? [entry, passFile, passDir] : false;
179+
}),
180+
).then((entries) => entries.filter((e) => e))) as [
181+
Entry,
182+
boolean,
183+
boolean,
184+
][];
185185
let entryCount = filtered.length;
186186
if (entryCount === 0) {
187187
this.emit("done", this.result);
188+
yield this.result;
188189
} else {
189190
const then = () => {
190191
if (--entryCount === 0) {
@@ -195,10 +196,12 @@ class Walker extends EventEmitter {
195196
this.emit("done", this.result);
196197
}
197198
};
198-
filtered.forEach((filt) => {
199-
const [entry, file, dir] = filt;
200-
this.stat(entry, file, dir, then);
201-
});
199+
200+
for (const [entry, file, dir] of filtered) {
201+
for await (const statResult of this.stat(entry, file, dir, then)) {
202+
yield statResult;
203+
}
204+
}
202205
}
203206
}
204207

@@ -212,29 +215,36 @@ class Walker extends EventEmitter {
212215
return entry[1] === Directory;
213216
}
214217

215-
onstat(entry: Entry, file: boolean, dir: boolean, then: () => void): void {
218+
async *onstat(entry: Entry, file: boolean, dir: boolean, then: () => void) {
216219
const abs = this.path + "/" + entry[0];
217220
const isSymbolicLink = this.entryIsSymlink(entry);
218221
if (!this.entryIsDirectory(entry)) {
219222
if (file && !this.onlyDirs) {
220223
this.result.add(abs.slice(this.root.length + 1));
221224
}
222225
then();
226+
yield this.result;
223227
} else {
224228
if (dir) {
225-
this.walker(
229+
yield* this.walker(
226230
entry[0],
227-
{ isSymbolicLink, exact: this.filterEntry(entry[0] + "/") },
231+
{ isSymbolicLink, exact: await this.filterEntry(entry[0] + "/") },
228232
then,
229233
);
230234
} else {
231235
then();
236+
yield this.result;
232237
}
233238
}
234239
}
235240

236-
stat(entry: Entry, file: boolean, dir: boolean, then: () => void): void {
237-
this.onstat(entry, file, dir, then);
241+
async *stat(
242+
entry: Entry,
243+
file: boolean,
244+
dir: boolean,
245+
then: () => void,
246+
): any {
247+
yield* this.onstat(entry, file, dir, then);
238248
}
239249

240250
walkerOpt(entry: string, opts: Partial<WalkerOptions>): WalkerOptions {
@@ -249,29 +259,36 @@ class Walker extends EventEmitter {
249259
};
250260
}
251261

252-
walker(entry: string, opts: Partial<WalkerOptions>, then: () => void): void {
253-
new Walker(this.walkerOpt(entry, opts), this.ide).on("done", then).start();
262+
async *walker(entry: string, opts: Partial<WalkerOptions>, then: () => void) {
263+
const walker = new Walker(this.walkerOpt(entry, opts), this.ide);
264+
265+
walker.on("done", then);
266+
yield* walker.start();
254267
}
255268

256-
filterEntry(
269+
async filterEntry(
257270
entry: string,
258271
partial?: boolean,
259272
entryBasename?: string,
260-
): boolean {
273+
): Promise<boolean> {
261274
let included = true;
262275

263276
if (this.parent && this.parent.filterEntry) {
264277
const parentEntry = this.basename + "/" + entry;
265278
const parentBasename = entryBasename || entry;
266-
included = this.parent.filterEntry(parentEntry, partial, parentBasename);
279+
included = await this.parent.filterEntry(
280+
parentEntry,
281+
partial,
282+
parentBasename,
283+
);
267284
if (!included && !this.exact) {
268285
return false;
269286
}
270287
}
271288

272-
this.ignoreFiles.forEach((f) => {
289+
for (const f of this.ignoreFiles) {
273290
if (this.ignoreRules[f]) {
274-
this.ignoreRules[f].forEach((rule) => {
291+
for (const rule of this.ignoreRules[f]) {
275292
if (rule.negate !== included) {
276293
const isRelativeRule =
277294
entryBasename &&
@@ -299,29 +316,14 @@ class Walker extends EventEmitter {
299316
included = rule.negate;
300317
}
301318
}
302-
});
319+
}
303320
}
304-
});
321+
}
305322

306323
return included;
307324
}
308325
}
309326

310-
interface WalkCallback {
311-
(err: Error | null, result?: string[]): void;
312-
}
313-
314-
async function walkDirWithCallback(
315-
opts: WalkerOptions,
316-
ide: IDE,
317-
callback?: WalkCallback,
318-
): Promise<string[] | void> {
319-
const p = new Promise<string[]>((resolve, reject) => {
320-
new Walker(opts, ide).on("done", resolve).on("error", reject).start();
321-
});
322-
return callback ? p.then((res) => callback(null, res), callback) : p;
323-
}
324-
325327
const defaultOptions: WalkerOptions = {
326328
ignoreFiles: [".gitignore", ".continueignore"],
327329
onlyDirs: false,
@@ -333,40 +335,41 @@ export async function walkDir(
333335
ide: IDE,
334336
_options?: WalkerOptions,
335337
): Promise<string[]> {
338+
let entries: string[] = [];
336339
const options = { ...defaultOptions, ..._options };
337-
return new Promise((resolve, reject) => {
338-
walkDirWithCallback(
339-
{
340-
path,
341-
ignoreFiles: options.ignoreFiles,
342-
onlyDirs: options.onlyDirs,
343-
follow: true,
344-
includeEmpty: false,
345-
additionalIgnoreRules: options.additionalIgnoreRules,
346-
},
347-
ide,
348-
async (err, result) => {
349-
if (err) {
350-
reject(err);
351-
} else {
352-
const relativePaths = result || [];
353-
if (options?.returnRelativePaths) {
354-
resolve(relativePaths);
355-
} else {
356-
const pathSep = await ide.pathSep();
357-
if (pathSep === "/") {
358-
resolve(relativePaths.map((p) => path + pathSep + p));
359-
} else {
360-
// Need to replace with windows path sep
361-
resolve(
362-
relativePaths.map(
363-
(p) => path + pathSep + p.split("/").join(pathSep),
364-
),
365-
);
366-
}
367-
}
368-
}
369-
},
370-
);
371-
});
340+
341+
const walker = new Walker(
342+
{
343+
path,
344+
ignoreFiles: options.ignoreFiles,
345+
onlyDirs: options.onlyDirs,
346+
follow: true,
347+
includeEmpty: false,
348+
additionalIgnoreRules: options.additionalIgnoreRules,
349+
},
350+
ide,
351+
);
352+
353+
try {
354+
for await (const walkedEntries of walker.start()) {
355+
entries = [...walkedEntries];
356+
}
357+
} catch (err) {
358+
console.error(`Error walking directories: ${err}`);
359+
throw err;
360+
}
361+
362+
const relativePaths = entries || [];
363+
364+
if (options?.returnRelativePaths) {
365+
return relativePaths;
366+
}
367+
368+
const pathSep = await ide.pathSep();
369+
370+
if (pathSep === "/") {
371+
return relativePaths.map((p) => path + pathSep + p);
372+
}
373+
374+
return relativePaths.map((p) => path + pathSep + p.split("/").join(pathSep));
372375
}

core/test/walkDir.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ describe("walkDir", () => {
264264
});
265265

266266
test("should walk continue/extensions/vscode without getting any files in the .continueignore", async () => {
267-
const vscodePath = path.join(__dirname, "..", "extensions", "vscode");
267+
const vscodePath = path.join(__dirname, "../..", "extensions", "vscode");
268268
const results = await walkDir(vscodePath, ide, {
269269
ignoreFiles: [".gitignore", ".continueignore"],
270270
});

0 commit comments

Comments
 (0)