src/vs/platform/agentHost/node/agentHostWorkspaceFiles.ts

204 LOC · 166 covered · 38 uncovered · 34 ranges · 951 concepts · 9 introducers · 510 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

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 related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentHostWorkspaceFiles.ts ×5
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 * as cp from 'child_process';
7 > import { CancellationToken } from '../../../base/common/cancellation.js';
8 > import { CancellationError } from '../../../base/common/errors.js';
9 > import { Disposable } from '../../../base/common/lifecycle.js';
10 > import { Schemas } from '../../../base/common/network.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { ILogService } from '../../log/common/log.js';
13 > import { rgDiskPath } from '../../../base/node/ripgrep.js';
14 >
15 > /** Maximum number of files cached per working directory. */
16 > const MAX_FILES = 50_000;
17 >
18 > /** TTL for a cached file list before we re-enumerate. */
19 > const CACHE_TTL_MS = 30_000;
20 >
21 > interface ICacheEntry {
22 > readonly promise: Promise<readonly URI[]>;
23 > expiresAt: number;
24 > }
25 >
26 > /**
27 > * Enumerates files under a working directory using ripgrep, with results
28 > * cached per working directory for a short TTL.
29 > *
30 > * Mirrors the workbench's file-search invocation pattern (see
31 > * `ripgrepFileSearch.ts` in `vs/workbench/services/search/node/`) but does
32 > * not depend on the workbench layer — the agent host runs in a separate
33 > * node process that may not import from `vs/workbench/`.
34 > *
35 > * Files are returned as absolute {@link URI}s relative to the working
36 > * directory. `.gitignore` and other `.ignore` files are honoured by
37 > * ripgrep. Symlinks are followed.
38 > */
39 > export class AgentHostWorkspaceFiles extends Disposable {
40 >
41 > private readonly _cache = new Map<string, ICacheEntry>();
42 > /** Active ripgrep child processes, killed on dispose. */
43 > private readonly _activeChildren = new Set<cp.ChildProcessWithoutNullStreams>();
44 >
45 > constructor(
46 > @ILogService private readonly _logService: ILogService, agentHostWorkspaceFiles.ts ×3
47 > ) {
48 > super();
49 > }
51 > override dispose(): void {
52 > for (const child of this._activeChildren) { agentHostWorkspaceFiles.ts ×3
54 > child.kill();
55 > } catch {
56 // ignore
57 }
59 > this._activeChildren.clear(); agentHostWorkspaceFiles.ts ×3
60 > this._cache.clear();
61 > super.dispose();
62 > }
64 > /**
65 > * Return the list of files under `workingDirectory`. Concurrent calls
66 > * with the same working directory share an in-flight enumeration.
67 > *
68 > * Only `file://` URIs are supported. Other schemes return an empty list.
69 > */
70 > async getFiles(workingDirectory: URI, token: CancellationToken): Promise<readonly URI[]> {
71 > if (workingDirectory.scheme !== Schemas.file) { agentHostWorkspaceFiles.ts ×3
73 > }
75 > const key = workingDirectory.toString();
76 > const now = Date.now();
77 > const existing = this._cache.get(key);
78 > let shared: Promise<readonly URI[]>;
79 > if (existing && existing.expiresAt > now) { agentHostWorkspaceFiles.ts ×3
80 > shared = existing.promise; agentHostWorkspaceFiles.ts ×1
82 > shared = this._enumerate(workingDirectory);
83 > const entry: ICacheEntry = { promise: shared, expiresAt: now + CACHE_TTL_MS };
84 > this._cache.set(key, entry);
85 > // If enumeration fails, drop the cache entry so the next caller retries.
86 > shared.catch(() => {
87 if (this._cache.get(key) === entry) {
88 this._cache.delete(key);
89 }
91 > }
92 >
93 > // Race the shared enumeration against the caller's cancellation
94 > // token. Only the caller's promise rejects on cancellation; the
95 > // shared enumeration runs to completion so concurrent callers (and
96 > // future cache hits within the TTL) still see the result.
97 > if (token.isCancellationRequested) {
98 throw new CancellationError();
99 }
100 > if (token === CancellationToken.None) { agentHostWorkspaceFiles.ts ×12
101 > return shared; agentHostWorkspaceFiles.ts ×5
102 > }
103 > return new Promise<readonly URI[]>((resolve, reject) => { agentHostWorkspaceFiles.ts ×2
104 > const cancelListener = token.onCancellationRequested(() => {
105 > cancelListener.dispose();
106 > reject(new CancellationError());
107 > });
108 > shared.then(value => {
109 > cancelListener.dispose();
110 > resolve(value);
111 > }, err => {
112 cancelListener.dispose();
113 reject(err);
115 > });
118 > private async _enumerate(workingDirectory: URI): Promise<readonly URI[]> {
119 > const resolvedRgDiskPath = await rgDiskPath(); agentHostWorkspaceFiles.ts ×12
120 > return new Promise<readonly URI[]>(resolve => {
121 > const cwd = workingDirectory.fsPath;
122 > // Mirror the workbench's `ripgrepFileSearch.ts` invocation: pass
123 > // `--no-config` so a user's global `~/.ripgreprc` cannot change
124 > // enumeration results (or enable preprocessors etc.).
125 > const args = ['--files', '--hidden', '--no-require-git', '--follow', '--no-config', '--glob', '!.git'];
126 >
127 > let child: cp.ChildProcessWithoutNullStreams;
128 > try {
129 > child = cp.spawn(resolvedRgDiskPath, args, { cwd });
130 > } catch (err) {
131 this._logService.warn(`[AgentHostWorkspaceFiles] Failed to spawn ripgrep: ${err}`);
132 resolve([]);
133 return;
134 }
135 > this._activeChildren.add(child); agentHostWorkspaceFiles.ts ×12
136 >
137 > const results: URI[] = [];
138 > let buffer = '';
139 > let limitHit = false;
140 > let settled = false;
141 >
142 > const finish = (value: readonly URI[]) => {
143 > if (settled) {
144 return;
145 }
146 > settled = true; agentHostWorkspaceFiles.ts ×12
147 > this._activeChildren.delete(child);
148 > resolve(value);
149 > };
150 >
151 > child.stdout.setEncoding('utf8');
152 > child.stdout.on('data', (chunk: string) => {
153 > if (limitHit) { agentHostWorkspaceFiles.ts ×5
154 return;
155 }
156 > buffer += chunk; agentHostWorkspaceFiles.ts ×5
157 > let newlineIndex: number;
158 > while ((newlineIndex = buffer.indexOf('\n')) >= 0) {
159 > const line = buffer.slice(0, newlineIndex).replace(/\r$/, '');
160 > buffer = buffer.slice(newlineIndex + 1);
161 > if (!line) {
162 continue;
163 }
164 > results.push(URI.joinPath(workingDirectory, line)); agentHostWorkspaceFiles.ts ×5
165 > if (results.length >= MAX_FILES) {
166 limitHit = true;
167 try {
168 child.kill();
169 } catch {
170 // ignore
171 }
172 break;
173 }
176 >
177 > child.stderr.setEncoding('utf8');
178 > let stderr = '';
179 > child.stderr.on('data', (chunk: string) => {
180 stderr += chunk;
182 >
183 > child.on('error', err => {
184 this._logService.warn(`[AgentHostWorkspaceFiles] ripgrep error: ${err}`);
185 finish([]);
187 >
188 > child.on('close', () => {
189 > // Flush any trailing line still in the buffer.
190 > if (!limitHit && buffer.length > 0) {
191 const line = buffer.replace(/\r$/, '');
192 if (line) {
193 results.push(URI.joinPath(workingDirectory, line));
194 }
195 buffer = '';
196 }
197 > if (stderr) { agentHostWorkspaceFiles.ts ×12
198 this._logService.trace(`[AgentHostWorkspaceFiles] ripgrep stderr: ${stderr}`);
199 }
200 > finish(results); agentHostWorkspaceFiles.ts ×12
201 > });
202 > });
203 > }