src/vs/workbench/api/common/extHostWorkspace.ts

1218 LOC · 322 covered · 896 uncovered · 67 ranges · 107 concepts · 1 introducers · 71 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 > /*--------------------------------------------------------------------------------------------- extHostWorkspace.ts ×67
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 { delta as arrayDelta, mapArrayOrNot } from '../../../base/common/arrays.js';
7 > import { AsyncIterableProducer, Barrier } from '../../../base/common/async.js';
8 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
9 > import { AsyncEmitter, Emitter, Event } from '../../../base/common/event.js';
10 > import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
11 > import { StopWatch } from '../../../base/common/stopwatch.js';
12 > import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js';
13 > import { Schemas } from '../../../base/common/network.js';
14 > import { Counter } from '../../../base/common/numbers.js';
15 > import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from '../../../base/common/resources.js';
16 > import { compare } from '../../../base/common/strings.js';
17 > import { isUriComponents, URI, UriComponents } from '../../../base/common/uri.js';
18 > import { localize } from '../../../nls.js';
19 > import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
20 > import { FileSystemProviderCapabilities } from '../../../platform/files/common/files.js';
21 > import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
22 > import { ILogService } from '../../../platform/log/common/log.js';
23 > import { Severity } from '../../../platform/notification/common/notification.js';
24 > import { EditSessionIdentityMatch } from '../../../platform/workspace/common/editSessions.js';
25 > import { Workspace, WorkspaceFolder } from '../../../platform/workspace/common/workspace.js';
26 > import { IExtHostFileSystemInfo } from './extHostFileSystemInfo.js';
27 > import { IExtHostInitDataService } from './extHostInitDataService.js';
28 > import { IExtHostRpcService } from './extHostRpcService.js';
29 > import { GlobPattern } from './extHostTypeConverters.js';
30 > import { Range } from './extHostTypes.js';
31 > import { IURITransformerService } from './extHostUriTransformerService.js';
32 > import { IFileQueryBuilderOptions, ISearchPatternBuilder, ITextQueryBuilderOptions } from '../../services/search/common/queryBuilder.js';
33 > import { IRawFileMatch2, ITextSearchResult, resultIsMatch } from '../../services/search/common/search.js';
34 > import type * as vscode from 'vscode';
35 > import { ExtHostWorkspaceShape, IRelativePatternDto, IWorkspaceData, MainContext, MainThreadMessageOptions, MainThreadMessageServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape } from './extHost.protocol.js';
36 > import { revive } from '../../../base/common/marshalling.js';
37 > import { AuthInfo, Credentials } from '../../../platform/request/common/request.js';
38 > import { ExcludeSettingOptions, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js';
39 > import { bufferToStream, readableToBuffer, VSBuffer } from '../../../base/common/buffer.js';
40 > import { toDecodeStream, toEncodeReadable, UTF8 } from '../../services/textfile/common/encoding.js';
41 > import { consumeStream } from '../../../base/common/stream.js';
42 > import { stringToSnapshot } from '../../services/textfile/common/textfiles.js';
43 > // Type-only import to avoid a runtime cycle with extHostConfiguration.ts.
44 > import type { ExtHostConfigProvider } from './extHostConfiguration.js';
45 >
46 > export interface IExtHostWorkspaceProvider {
47 > getWorkspaceFolder2(uri: vscode.Uri, resolveParent?: boolean): Promise<vscode.WorkspaceFolder | undefined>;
48 > resolveWorkspaceFolder(uri: vscode.Uri): Promise<vscode.WorkspaceFolder | undefined>;
49 > getWorkspaceFolders2(): Promise<vscode.WorkspaceFolder[] | undefined>;
50 > resolveProxy(url: string): Promise<string | undefined>;
51 > lookupAuthorization(authInfo: AuthInfo): Promise<Credentials | undefined>;
52 > lookupKerberosAuthorization(url: string): Promise<string | undefined>;
53 > loadCertificates(): Promise<string[]>;
54 > }
55 >
56 function isFolderEqual(folderA: URI, folderB: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean {
57 return new ExtUri(uri => ignorePathCasing(uri, extHostFileSystemInfo)).isEqual(folderA, folderB);
58 }
60 function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number {
61 return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? 0 : compare(a.uri.toString(), b.uri.toString());
62 }
64 function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number {
65 if (a.index !== b.index) {
66 return a.index < b.index ? -1 : 1;
67 }
68
69 return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString());
70 }
72 function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo) => number, extHostFileSystemInfo: IExtHostFileSystemInfo): { removed: vscode.WorkspaceFolder[]; added: vscode.WorkspaceFolder[] } {
73 const oldSortedFolders = oldFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo));
74 const newSortedFolders = newFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo));
75
76 return arrayDelta(oldSortedFolders, newSortedFolders, (a, b) => compare(a, b, extHostFileSystemInfo));
77 }
79 function ignorePathCasing(uri: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean {
80 const capabilities = extHostFileSystemInfo.getCapabilities(uri.scheme);
81 return !(capabilities && (capabilities & FileSystemProviderCapabilities.PathCaseSensitive));
82 }
84 > interface MutableWorkspaceFolder extends vscode.WorkspaceFolder {
85 > name: string;
86 > index: number;
87 > }
88 >
89 > interface QueryOptions<T> {
90 > options: T;
91 > folder: URI | undefined;
92 > }
93 >
94 > type FindFilesApiKind = 'findFiles' | 'findFiles2';
95 >
96 > interface FindFilesCallIntent {
97 > /** Value the extension explicitly passed for `useIgnoreFiles.local` (findFiles2); `undefined` if not specified or N/A for legacy `findFiles`. */
98 > readonly useIgnoreFilesLocal: boolean | undefined;
99 > /** Whether the extension passed `null` as the `exclude` argument to legacy `findFiles` (the documented escape hatch). Always `false` for findFiles2. */
100 > readonly excludeWasNull: boolean;
101 > }
102 >
103 > class ExtHostWorkspaceImpl extends Workspace {
104 >
105 > static toExtHostWorkspace(data: IWorkspaceData | null, previousConfirmedWorkspace: ExtHostWorkspaceImpl | undefined, previousUnconfirmedWorkspace: ExtHostWorkspaceImpl | undefined, extHostFileSystemInfo: IExtHostFileSystemInfo): { workspace: ExtHostWorkspaceImpl | null; added: vscode.WorkspaceFolder[]; removed: vscode.WorkspaceFolder[] } {
106 > if (!data) {
107 > return { workspace: null, added: [], removed: [] };
108 > }
109 >
110 > const { id, name, folders, configuration, transient, isUntitled } = data;
111 > const newWorkspaceFolders: vscode.WorkspaceFolder[] = [];
112 >
113 > // If we have an existing workspace, we try to find the folders that match our
114 > // data and update their properties. It could be that an extension stored them
115 > // for later use and we want to keep them "live" if they are still present.
116 > const oldWorkspace = previousConfirmedWorkspace;
117 > if (previousConfirmedWorkspace) {
118 > folders.forEach((folderData, index) => {
119 > const folderUri = URI.revive(folderData.uri);
120 > const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri, extHostFileSystemInfo);
121 >
122 > if (existingFolder) {
123 > existingFolder.name = folderData.name;
124 > existingFolder.index = folderData.index;
125 >
126 > newWorkspaceFolders.push(existingFolder);
127 > } else {
128 > newWorkspaceFolders.push({ uri: folderUri, name: folderData.name, index });
129 > }
130 > });
131 > } else {
132 > newWorkspaceFolders.push(...folders.map(({ uri, name, index }) => ({ uri: URI.revive(uri), name, index })));
133 > }
134 >
135 > // make sure to restore sort order based on index
136 > newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1);
137 >
138 > const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders, !!transient, configuration ? URI.revive(configuration) : null, !!isUntitled, uri => ignorePathCasing(uri, extHostFileSystemInfo));
139 > const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri, extHostFileSystemInfo);
140 >
141 > return { workspace, added, removed };
142 > }
143 >
144 > private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): MutableWorkspaceFolder | undefined {
145 for (let i = 0; i < workspace.folders.length; i++) {
146 const folder = workspace.workspaceFolders[i];
147 if (isFolderEqual(folder.uri, folderUriToFind, extHostFileSystemInfo)) {
148 return folder;
149 }
150 }
151
152 return undefined;
153 }
155 > private readonly _workspaceFolders: vscode.WorkspaceFolder[] = [];
156 > private readonly _structure: TernarySearchTree<URI, vscode.WorkspaceFolder>;
157 >
158 > constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[], transient: boolean, configuration: URI | null, private _isUntitled: boolean, ignorePathCasing: (key: URI) => boolean) {
159 super(id, folders.map(f => new WorkspaceFolder(f)), transient, configuration, ignorePathCasing);
160 this._structure = TernarySearchTree.forUris<vscode.WorkspaceFolder>(ignorePathCasing, () => true);
161
162 // setup the workspace folder data structure
163 folders.forEach(folder => {
164 this._workspaceFolders.push(folder);
165 this._structure.set(folder.uri, folder);
166 });
167 }
169 > override get name(): string {
170 return this._name;
171 }
173 > get isUntitled(): boolean {
174 return this._isUntitled;
175 }
177 > get workspaceFolders(): vscode.WorkspaceFolder[] {
178 return this._workspaceFolders.slice(0);
179 }
181 > getWorkspaceFolder(uri: URI, resolveParent?: boolean): vscode.WorkspaceFolder | undefined {
182 if (resolveParent && this._structure.get(uri)) {
183 // `uri` is a workspace folder so we check for its parent
184 uri = dirname(uri);
185 }
186 return this._structure.findSubstr(uri);
187 }
189 > resolveWorkspaceFolder(uri: URI): vscode.WorkspaceFolder | undefined {
190 return this._structure.get(uri);
191 }
193 >
194 > export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspaceProvider {
195 >
196 > readonly _serviceBrand: undefined;
197 >
198 > private readonly _onDidChangeWorkspace = new Emitter<vscode.WorkspaceFoldersChangeEvent>();
199 > readonly onDidChangeWorkspace: Event<vscode.WorkspaceFoldersChangeEvent> = this._onDidChangeWorkspace.event;
200 >
201 > private readonly _onDidGrantWorkspaceTrust = new Emitter<void>();
202 > readonly onDidGrantWorkspaceTrust: Event<void> = this._onDidGrantWorkspaceTrust.event;
203 >
204 > private readonly _onDidChangeWorkspaceTrustedFolders = new Emitter<void>();
205 > readonly onDidChangeWorkspaceTrustedFolders: Event<void> = this._onDidChangeWorkspaceTrustedFolders.event;
206 >
207 > private readonly _logService: ILogService;
208 > private readonly _requestIdProvider: Counter;
209 > private readonly _barrier: Barrier;
210 >
211 > private _confirmedWorkspace?: ExtHostWorkspaceImpl;
212 > private _unconfirmedWorkspace?: ExtHostWorkspaceImpl;
213 >
214 > private readonly _proxy: MainThreadWorkspaceShape;
215 > private readonly _messageService: MainThreadMessageServiceShape;
216 > private readonly _telemetryProxy: MainThreadTelemetryShape;
217 > private readonly _extHostFileSystemInfo: IExtHostFileSystemInfo;
218 > private readonly _uriTransformerService: IURITransformerService;
219 >
220 > private readonly _activeSearchCallbacks: ((match: IRawFileMatch2) => any)[] = [];
221 >
222 > private _trusted: boolean = false;
223 >
224 > private readonly _editSessionIdentityProviders = new Map<string, vscode.EditSessionIdentityProvider>();
225 >
226 > // Pushed in by ExtHostConfiguration after init (see `$setConfigProvider`).
227 > private _configProvider?: ExtHostConfigProvider;
228 >
229 > constructor(
230 @IExtHostRpcService extHostRpc: IExtHostRpcService,
231 @IExtHostInitDataService initData: IExtHostInitDataService,
232 @IExtHostFileSystemInfo extHostFileSystemInfo: IExtHostFileSystemInfo,
233 @ILogService logService: ILogService,
234 @IURITransformerService uriTransformerService: IURITransformerService,
235 ) {
236 this._logService = logService;
237 this._extHostFileSystemInfo = extHostFileSystemInfo;
238 this._uriTransformerService = uriTransformerService;
239 this._requestIdProvider = new Counter();
240 this._barrier = new Barrier();
241
242 this._proxy = extHostRpc.getProxy(MainContext.MainThreadWorkspace);
243 this._messageService = extHostRpc.getProxy(MainContext.MainThreadMessageService);
244 this._telemetryProxy = extHostRpc.getProxy(MainContext.MainThreadTelemetry);
245 const data = initData.workspace;
246 this._confirmedWorkspace = data ? new ExtHostWorkspaceImpl(data.id, data.name, [], !!data.transient, data.configuration ? URI.revive(data.configuration) : null, !!data.isUntitled, uri => ignorePathCasing(uri, extHostFileSystemInfo)) : undefined;
247 }
249 > /**
250 > * Receives the configuration provider from ExtHostConfiguration after init. We cannot inject
251 > * IExtHostConfiguration directly because it creates a DI cycle (ExtHostConfiguration already
252 > * depends on IExtHostWorkspace). Once set, settings reads in findFiles become synchronous.
253 > */
254 > $setConfigProvider(provider: ExtHostConfigProvider): void {
255 this._configProvider = provider;
256 }
258 > private _useIgnoreFilesInFindFiles(): boolean {
259 return this._configProvider?.getConfiguration('search').get<boolean>('experimental.useIgnoreFilesInFindFiles') ?? false;
260 }
262 > private _userIgnoreFilesSetting(): boolean {
263 // Default in `search.useIgnoreFiles` is `true`; mirror that here so telemetry computed against
264 // an unset config still reflects the fallback the query builder will apply.
265 return this._configProvider?.getConfiguration('search').get<boolean>('useIgnoreFiles') ?? true;
266 }
268 > $initializeWorkspace(data: IWorkspaceData | null, trusted: boolean): void {
269 this._trusted = trusted;
270 this.$acceptWorkspaceData(data);
271 this._barrier.open();
272 }
274 > waitForInitializeCall(): Promise<boolean> {
275 return this._barrier.wait();
276 }
278 > // --- workspace ---
279 >
280 > get workspace(): Workspace | undefined {
281 return this._actualWorkspace;
282 }
284 > get name(): string | undefined {
285 return this._actualWorkspace ? this._actualWorkspace.name : undefined;
286 }
288 > get workspaceFile(): vscode.Uri | undefined {
289 if (this._actualWorkspace) {
290 if (this._actualWorkspace.configuration) {
291 if (this._actualWorkspace.isUntitled) {
292 return URI.from({ scheme: Schemas.untitled, path: basename(dirname(this._actualWorkspace.configuration)) }); // Untitled Workspace: return untitled URI
293 }
294
295 return this._actualWorkspace.configuration; // Workspace: return the configuration location
296 }
297 }
298
299 return undefined;
300 }
302 > private get _actualWorkspace(): ExtHostWorkspaceImpl | undefined {
303 return this._unconfirmedWorkspace || this._confirmedWorkspace;
304 }
306 > getWorkspaceFolders(): vscode.WorkspaceFolder[] | undefined {
307 if (!this._actualWorkspace) {
308 return undefined;
309 }
310 return this._actualWorkspace.workspaceFolders.slice(0);
311 }
313 > async getWorkspaceFolders2(): Promise<vscode.WorkspaceFolder[] | undefined> {
314 await this._barrier.wait();
315 if (!this._actualWorkspace) {
316 return undefined;
317 }
318 return this._actualWorkspace.workspaceFolders.slice(0);
319 }
321 > updateWorkspaceFolders(extension: IExtensionDescription, index: number, deleteCount: number, ...workspaceFoldersToAdd: { uri: vscode.Uri; name?: string }[]): boolean {
322 const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri; name?: string }[] = [];
323 if (Array.isArray(workspaceFoldersToAdd)) {
324 workspaceFoldersToAdd.forEach(folderToAdd => {
325 if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri, this._extHostFileSystemInfo))) {
326 validatedDistinctWorkspaceFoldersToAdd.push({ uri: folderToAdd.uri, name: folderToAdd.name || basenameOrAuthority(folderToAdd.uri) });
327 }
328 });
329 }
330
331 if (!!this._unconfirmedWorkspace) {
332 return false; // prevent accumulated calls without a confirmed workspace
333 }
334
335 if ([index, deleteCount].some(i => typeof i !== 'number' || i < 0)) {
336 return false; // validate numbers
337 }
338
339 if (deleteCount === 0 && validatedDistinctWorkspaceFoldersToAdd.length === 0) {
340 return false; // nothing to delete or add
341 }
342
343 const currentWorkspaceFolders: MutableWorkspaceFolder[] = this._actualWorkspace ? this._actualWorkspace.workspaceFolders : [];
344 if (index + deleteCount > currentWorkspaceFolders.length) {
345 return false; // cannot delete more than we have
346 }
347
348 // Simulate the updateWorkspaceFolders method on our data to do more validation
349 const newWorkspaceFolders = currentWorkspaceFolders.slice(0);
350 newWorkspaceFolders.splice(index, deleteCount, ...validatedDistinctWorkspaceFoldersToAdd.map(f => ({ uri: f.uri, name: f.name || basenameOrAuthority(f.uri), index: undefined! /* fixed later */ })));
351
352 for (let i = 0; i < newWorkspaceFolders.length; i++) {
353 const folder = newWorkspaceFolders[i];
354 if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri, this._extHostFileSystemInfo))) {
355 return false; // cannot add the same folder multiple times
356 }
357 }
358
359 newWorkspaceFolders.forEach((f, index) => f.index = index); // fix index
360 const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex, this._extHostFileSystemInfo);
361 if (added.length === 0 && removed.length === 0) {
362 return false; // nothing actually changed
363 }
364
365 // Trigger on main side
366 if (this._proxy) {
367 const extName = extension.displayName || extension.name;
368 this._proxy.$updateWorkspaceFolders(extName, index, deleteCount, validatedDistinctWorkspaceFoldersToAdd).then(undefined, error => {
369
370 // in case of an error, make sure to clear out the unconfirmed workspace
371 // because we cannot expect the acknowledgement from the main side for this
372 this._unconfirmedWorkspace = undefined;
373
374 // show error to user
375 const options: MainThreadMessageOptions = { source: { identifier: extension.identifier, label: extension.displayName || extension.name } };
376 this._messageService.$showMessage(Severity.Error, localize('updateerror', "Extension '{0}' failed to update workspace folders: {1}", extName, error.toString()), options, []);
377 });
378 }
379
380 // Try to accept directly
381 this.trySetWorkspaceFolders(newWorkspaceFolders);
382
383 return true;
384 }
386 > getWorkspaceFolder(uri: vscode.Uri, resolveParent?: boolean): vscode.WorkspaceFolder | undefined {
387 if (!this._actualWorkspace) {
388 return undefined;
389 }
390 return this._actualWorkspace.getWorkspaceFolder(uri, resolveParent);
391 }
393 > async getWorkspaceFolder2(uri: vscode.Uri, resolveParent?: boolean): Promise<vscode.WorkspaceFolder | undefined> {
394 await this._barrier.wait();
395 if (!this._actualWorkspace) {
396 return undefined;
397 }
398 return this._actualWorkspace.getWorkspaceFolder(uri, resolveParent);
399 }
401 > async resolveWorkspaceFolder(uri: vscode.Uri): Promise<vscode.WorkspaceFolder | undefined> {
402 await this._barrier.wait();
403 if (!this._actualWorkspace) {
404 return undefined;
405 }
406 return this._actualWorkspace.resolveWorkspaceFolder(uri);
407 }
409 > getPath(): string | undefined {
410
411 // this is legacy from the days before having
412 // multi-root and we keep it only alive if there
413 // is just one workspace folder.
414 if (!this._actualWorkspace) {
415 return undefined;
416 }
417
418 const { folders } = this._actualWorkspace;
419 if (folders.length === 0) {
420 return undefined;
421 }
422 // #54483 @Joh Why are we still using fsPath?
423 return folders[0].uri.fsPath;
424 }
426 > getRelativePath(pathOrUri: string | vscode.Uri, includeWorkspace?: boolean): string {
427
428 let resource: URI | undefined;
429 let path: string = '';
430 if (typeof pathOrUri === 'string') {
431 resource = URI.file(pathOrUri);
432 path = pathOrUri;
433 } else if (typeof pathOrUri !== 'undefined') {
434 resource = pathOrUri;
435 path = pathOrUri.fsPath;
436 }
437
438 if (!resource) {
439 return path;
440 }
441
442 const folder = this.getWorkspaceFolder(
443 resource,
444 true
445 );
446
447 if (!folder) {
448 return path;
449 }
450
451 if (typeof includeWorkspace === 'undefined' && this._actualWorkspace) {
452 includeWorkspace = this._actualWorkspace.folders.length > 1;
453 }
454
455 let result = relativePath(folder.uri, resource);
456 if (includeWorkspace && folder.name) {
457 result = `${folder.name}/${result}`;
458 }
459 return result!;
460 }
462 > private trySetWorkspaceFolders(folders: vscode.WorkspaceFolder[]): void {
463
464 // Update directly here. The workspace is unconfirmed as long as we did not get an
465 // acknowledgement from the main side (via $acceptWorkspaceData)
466 if (this._actualWorkspace) {
467 this._unconfirmedWorkspace = ExtHostWorkspaceImpl.toExtHostWorkspace({
468 id: this._actualWorkspace.id,
469 name: this._actualWorkspace.name,
470 configuration: this._actualWorkspace.configuration,
471 folders,
472 isUntitled: this._actualWorkspace.isUntitled
473 }, this._actualWorkspace, undefined, this._extHostFileSystemInfo).workspace || undefined;
474 }
475 }
477 > $acceptWorkspaceData(data: IWorkspaceData | null): void {
478
479 const { workspace, added, removed } = ExtHostWorkspaceImpl.toExtHostWorkspace(data, this._confirmedWorkspace, this._unconfirmedWorkspace, this._extHostFileSystemInfo);
480
481 // Update our workspace object. We have a confirmed workspace, so we drop our
482 // unconfirmed workspace.
483 this._confirmedWorkspace = workspace || undefined;
484 this._unconfirmedWorkspace = undefined;
485
486 // Events
487 this._onDidChangeWorkspace.fire(Object.freeze({
488 added,
489 removed,
490 }));
491 }
493 > // --- search ---
494 >
495 > /**
496 > * Note, null/undefined have different and important meanings for "exclude"
497 > */
498 > findFiles(include: vscode.GlobPattern | undefined, exclude: vscode.GlobPattern | null | undefined, maxResults: number | undefined, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise<vscode.Uri[]> {
499 this._logService.trace(`extHostWorkspace#findFiles: fileSearch, extension: ${extensionId.value}, entryPoint: findFiles`);
500
501 let excludeString: string = '';
502 let useFileExcludes = true;
503 if (exclude === null) {
504 useFileExcludes = false;
505 } else if (exclude !== undefined) {
506 if (typeof exclude === 'string') {
507 excludeString = exclude;
508 } else {
509 excludeString = exclude.pattern;
510 }
511 }
512
513 const useIgnoreFilesOptIn = this._useIgnoreFilesInFindFiles();
514 // `useIgnoreFiles.local` semantics: `false` means "do not respect local .gitignore" (--no-ignore to rg).
515 // Default (PR #204845): hardcoded `false` for every legacy findFiles caller, regardless of `search.useIgnoreFiles`.
516 // Opt-in (`search.experimental.useIgnoreFilesInFindFiles: true`): honor the user's `search.useIgnoreFiles`,
517 // while keeping `exclude === null` as the documented escape hatch (no excludes => bypass .gitignore).
518 const localIgnoreFiles = useIgnoreFilesOptIn && exclude !== null ? undefined : false;
519
520 // todo: consider exclude baseURI if available
521 return this._findFilesImpl({ type: 'include', value: include }, {
522 exclude: [excludeString],
523 maxResults,
524 useExcludeSettings: useFileExcludes ? ExcludeSettingOptions.FilesExclude : ExcludeSettingOptions.None,
525 useIgnoreFiles: {
526 local: localIgnoreFiles
527 }
528 }, extensionId, 'findFiles', { useIgnoreFilesLocal: undefined, excludeWasNull: exclude === null }, token);
529 }
531 >
532 > findFiles2(filePatterns: readonly vscode.GlobPattern[],
533 options: vscode.FindFiles2Options = {},
534 extensionId: ExtensionIdentifier,
535 token: vscode.CancellationToken = CancellationToken.None): Promise<vscode.Uri[]> {
536 this._logService.trace(`extHostWorkspace#findFiles2New: fileSearch, extension: ${extensionId.value}, entryPoint: findFiles2New`);
537 return this._findFilesImpl({ type: 'filePatterns', value: filePatterns }, options, extensionId, 'findFiles2', { useIgnoreFilesLocal: options.useIgnoreFiles?.local, excludeWasNull: false }, token);
538 }
540 > private async _findFilesImpl(
541 // the old `findFiles` used `include` to query, but the new `findFiles2` uses `filePattern` to query.
542 // `filePattern` is the proper way to handle this, since it takes less precedence than the ignore files.
543 query: { readonly type: 'include'; readonly value: vscode.GlobPattern | undefined } | { readonly type: 'filePatterns'; readonly value: readonly vscode.GlobPattern[] },
544 options: vscode.FindFiles2Options,
545 extensionId: ExtensionIdentifier,
546 apiKind: FindFilesApiKind,
547 intent: FindFilesCallIntent,
548 token: vscode.CancellationToken
549 ): Promise<vscode.Uri[]> {
550 const useIgnoreFilesLocalRequested: 'unspecified' | 'true' | 'false' =
551 intent.useIgnoreFilesLocal === true ? 'true'
552 : intent.useIgnoreFilesLocal === false ? 'false'
553 : 'unspecified';
554 const sw = new StopWatch(true);
555 let queryCount = 0;
556 let respectedIgnoreFiles = this._userIgnoreFilesSetting();
557 let resultCount = 0;
558 let cancelled = false;
559 let errored = false;
560 try {
561 if (token.isCancellationRequested) {
562 cancelled = true;
563 return [];
564 }
565
566 const filePatternsToUse = query.type === 'include' ? [query.value] : query.value ?? [];
567 if (!Array.isArray(filePatternsToUse)) {
568 console.error('Invalid file pattern provided', filePatternsToUse);
569 throw new Error(`Invalid file pattern provided ${JSON.stringify(filePatternsToUse)}`);
570 }
571
572 const queryOptions: QueryOptions<IFileQueryBuilderOptions>[] = filePatternsToUse.map(filePattern => {
573
574 const excludePatterns = globsToISearchPatternBuilder(options.exclude);
575
576 const fileQueries: IFileQueryBuilderOptions = {
577 ignoreSymlinks: typeof options.followSymlinks === 'boolean' ? !options.followSymlinks : undefined,
578 disregardIgnoreFiles: typeof options.useIgnoreFiles?.local === 'boolean' ? !options.useIgnoreFiles.local : undefined,
579 disregardGlobalIgnoreFiles: typeof options.useIgnoreFiles?.global === 'boolean' ? !options.useIgnoreFiles.global : undefined,
580 disregardParentIgnoreFiles: typeof options.useIgnoreFiles?.parent === 'boolean' ? !options.useIgnoreFiles.parent : undefined,
581 disregardExcludeSettings: options.useExcludeSettings !== undefined && options.useExcludeSettings === ExcludeSettingOptions.None,
582 disregardSearchExcludeSettings: options.useExcludeSettings !== undefined && (options.useExcludeSettings !== ExcludeSettingOptions.SearchAndFilesExclude),
583 maxResults: options.maxResults,
584 excludePattern: excludePatterns.length > 0 ? excludePatterns : undefined,
585 ignoreGlobCase: options.caseInsensitive,
586 _reason: 'startFileSearch',
587 shouldGlobSearch: query.type === 'include' ? undefined : true,
588 };
589
590 const parseInclude = parseSearchExcludeInclude(GlobPattern.from(filePattern));
591 const folderToUse = parseInclude?.folder;
592 if (query.type === 'include') {
593 fileQueries.includePattern = parseInclude?.pattern;
594 } else {
595 fileQueries.filePattern = parseInclude?.pattern;
596 }
597
598 return {
599 folder: folderToUse,
600 options: fileQueries
601 };
602 });
603
604 queryCount = queryOptions.length;
605 // Effective ignore-file behavior across all sub-queries: a call respected `.gitignore` only when every
606 // sub-query is either explicitly honoring it or falls back to a user setting that honors it. When
607 // `disregardIgnoreFiles` is `undefined` the query builder uses `search.useIgnoreFiles`, which we mirror here.
608 const userHonorsIgnore = this._userIgnoreFilesSetting();
609 respectedIgnoreFiles = queryOptions.every(q =>
610 q.options.disregardIgnoreFiles === true ? false
611 : q.options.disregardIgnoreFiles === false ? true
612 : userHonorsIgnore);
613
614 const result = await this._findFilesBase(queryOptions, token);
615 resultCount = result.length;
616 cancelled = token.isCancellationRequested;
617 return result;
618 } catch (err) {
619 errored = true;
620 cancelled = token.isCancellationRequested;
621 throw err;
622 } finally {
623 this._reportFindFilesTelemetry({
624 extensionId: extensionId.value,
625 apiKind,
626 respectedIgnoreFiles,
627 useIgnoreFilesLocalRequested,
628 excludeWasNull: intent.excludeWasNull,
629 resultCount,
630 durationMs: sw.elapsed(),
631 queryCount,
632 cancelled,
633 errored,
634 });
635 }
636 }
638 > private async _findFilesBase(
639 queryOptions: QueryOptions<IFileQueryBuilderOptions>[] | undefined,
640 token: CancellationToken
641 ): Promise<vscode.Uri[]> {
642 // Ensure the token is recognized by the RPC protocol. Tokens from extension
643 // bundles may use a different CancellationToken module and fail the instanceof
644 // check in isCancellationToken(), causing them to be serialized (without
645 // functions) rather than handled as cancellation signals.
646 let tokenToUse = token;
647 let linkedSource: CancellationTokenSource | undefined;
648 if (!CancellationToken.isCancellationToken(token)) {
649 linkedSource = new CancellationTokenSource();
650 const foreignToken = token as unknown as Partial<CancellationToken>;
651 if (typeof foreignToken.onCancellationRequested === 'function') {
652 foreignToken.onCancellationRequested(() => linkedSource!.cancel());
653 }
654 tokenToUse = linkedSource.token;
655 }
656
657 const result = await Promise.all(queryOptions?.map(option => this._proxy.$startFileSearch(
658 option.folder ?? null,
659 option.options,
660 tokenToUse).then(data => Array.isArray(data) ? data.map(d => URI.revive(d)) : [])
661 ) ?? []);
662
663 const flatResult = result.flat();
664 linkedSource?.dispose();
665
666 // Dedupe entries in a flat array
667 const extUri = new ExtUri(uri => ignorePathCasing(uri, this._extHostFileSystemInfo));
668 const uriMap = new Map<string, vscode.Uri>();
669
670 for (const uri of flatResult) {
671 const key = extUri.getComparisonKey(uri);
672 if (!uriMap.has(key)) {
673 uriMap.set(key, uri);
674 }
675 }
676
677 return Array.from(uriMap.values());
678 }
680 > private _reportFindFilesTelemetry(event: {
681 extensionId: string;
682 apiKind: FindFilesApiKind;
683 respectedIgnoreFiles: boolean;
684 useIgnoreFilesLocalRequested: 'unspecified' | 'true' | 'false';
685 excludeWasNull: boolean;
686 resultCount: number;
687 durationMs: number;
688 queryCount: number;
689 cancelled: boolean;
690 errored: boolean;
691 }): void {
692 type FindFilesEvent = {
693 extensionId: string;
694 apiKind: string;
695 respectedIgnoreFiles: boolean;
696 useIgnoreFilesLocalRequested: string;
697 excludeWasNull: boolean;
698 resultCount: number;
699 durationMs: number;
700 queryCount: number;
701 cancelled: boolean;
702 errored: boolean;
703 };
704 type FindFilesEventClassification = {
705 owner: 'osortega';
706 comment: 'Telemetry for the extension API workspace.findFiles / findFiles2 calls. Used to assess the impact of flipping the default for search.experimental.useIgnoreFilesInFindFiles by comparing result counts and durations between calls that respected .gitignore and those that did not.';
707 extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Id of the extension that issued the findFiles call.' };
708 apiKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Which API entry point: findFiles (legacy) or findFiles2.' };
709 respectedIgnoreFiles: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the underlying search respected local .gitignore for this call (effective value after applying the experimental setting and any escape hatches).' };
710 useIgnoreFilesLocalRequested: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'What the extension explicitly passed for useIgnoreFiles.local (findFiles2 only): "true", "false", or "unspecified" (always "unspecified" for legacy findFiles since that API does not expose the option).' };
711 excludeWasNull: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the extension passed null as the exclude argument to legacy findFiles (the documented escape hatch for unfiltered results). Always false for findFiles2.' };
712 resultCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of unique results returned to the extension.' };
713 durationMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Total wall-clock duration of the findFiles call in milliseconds.' };
714 queryCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of underlying file-search queries dispatched (one per workspace folder/file pattern).' };
715 cancelled: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the call was cancelled before completion.' };
716 errored: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the call threw an error.' };
717 };
718 this._telemetryProxy.$publicLog2<FindFilesEvent, FindFilesEventClassification>('extHostFindFiles', event);
719 }
721 > findTextInFiles2(query: vscode.TextSearchQuery2, options: vscode.FindTextInFilesOptions2 | undefined, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): vscode.FindTextInFilesResponse {
722 this._logService.trace(`extHostWorkspace#findTextInFiles2: textSearch, extension: ${extensionId.value}, entryPoint: findTextInFiles2`);
723
724
725 const getOptions = (include: vscode.GlobPattern | undefined): QueryOptions<ITextQueryBuilderOptions> => {
726 if (!options) {
727 return {
728 folder: undefined,
729 options: {}
730 };
731 }
732 const parsedInclude = include ? parseSearchExcludeInclude(GlobPattern.from(include)) : undefined;
733
734 const excludePatterns = options.exclude ? globsToISearchPatternBuilder(options.exclude) : undefined;
735
736 return {
737 options: {
738
739 ignoreSymlinks: typeof options.followSymlinks === 'boolean' ? !options.followSymlinks : undefined,
740 disregardIgnoreFiles: typeof options.useIgnoreFiles?.local === 'boolean' ? !options.useIgnoreFiles?.local : undefined,
741 disregardGlobalIgnoreFiles: typeof options.useIgnoreFiles?.global === 'boolean' ? !options.useIgnoreFiles?.global : undefined,
742 disregardParentIgnoreFiles: typeof options.useIgnoreFiles?.parent === 'boolean' ? !options.useIgnoreFiles?.parent : undefined,
743 disregardExcludeSettings: options.useExcludeSettings !== undefined && options.useExcludeSettings === ExcludeSettingOptions.None,
744 disregardSearchExcludeSettings: options.useExcludeSettings !== undefined && (options.useExcludeSettings !== ExcludeSettingOptions.SearchAndFilesExclude),
745 fileEncoding: options.encoding,
746 maxResults: options.maxResults,
747 ignoreGlobCase: options.caseInsensitive,
748 previewOptions: options.previewOptions ? {
749 matchLines: options.previewOptions?.numMatchLines ?? 100,
750 charsPerLine: options.previewOptions?.charsPerLine ?? 10000,
751 } : undefined,
752 surroundingContext: options.surroundingContext,
753
754 includePattern: parsedInclude?.pattern,
755 excludePattern: excludePatterns
756 } satisfies ITextQueryBuilderOptions,
757 folder: parsedInclude?.folder
758 } satisfies QueryOptions<ITextQueryBuilderOptions>;
759 };
760
761 const queryOptionsRaw: (QueryOptions<ITextQueryBuilderOptions> | undefined)[] = ((options?.include?.map((include) =>
762 getOptions(include)))) ?? [getOptions(undefined)];
763
764 const queryOptions = queryOptionsRaw.filter((queryOps): queryOps is QueryOptions<ITextQueryBuilderOptions> => !!queryOps);
765
766 const disposables = new DisposableStore();
767 const progressEmitter = disposables.add(new Emitter<{ result: ITextSearchResult<URI>; uri: URI }>());
768 const complete = this.findTextInFilesBase(
769 query,
770 queryOptions,
771 (result, uri) => progressEmitter.fire({ result, uri }),
772 token
773 );
774 const asyncIterable = new AsyncIterableProducer<vscode.TextSearchResult2>(async emitter => {
775 disposables.add(progressEmitter.event(e => {
776 const result = e.result;
777 const uri = e.uri;
778 if (resultIsMatch(result)) {
779 emitter.emitOne(new TextSearchMatch2(
780 uri,
781 result.rangeLocations.map((range) => ({
782 previewRange: new Range(range.preview.startLineNumber, range.preview.startColumn, range.preview.endLineNumber, range.preview.endColumn),
783 sourceRange: new Range(range.source.startLineNumber, range.source.startColumn, range.source.endLineNumber, range.source.endColumn)
784 })),
785 result.previewText
786
787 ));
788 } else {
789 emitter.emitOne(new TextSearchContext2(
790 uri,
791 result.text,
792 result.lineNumber
793 ));
794
795 }
796 }));
797 await complete;
798 });
799
800 return {
801 results: asyncIterable,
802 complete: complete.then((e) => {
803 disposables.dispose();
804 return {
805 limitHit: e?.limitHit ?? false
806 };
807 }),
808 };
809 }
811 >
812 > async findTextInFilesBase(query: vscode.TextSearchQuery, queryOptions: QueryOptions<ITextQueryBuilderOptions>[] | undefined, callback: (result: ITextSearchResult<URI>, uri: URI) => void, token: vscode.CancellationToken = CancellationToken.None): Promise<vscode.TextSearchComplete> {
813 const requestId = this._requestIdProvider.getNext();
814
815 let isCanceled = false;
816 token.onCancellationRequested(_ => {
817 isCanceled = true;
818 });
819
820 this._activeSearchCallbacks[requestId] = p => {
821 if (isCanceled) {
822 return;
823 }
824
825 const uri = URI.revive(p.resource);
826 p.results!.forEach(rawResult => {
827 const result: ITextSearchResult<URI> = revive(rawResult);
828 callback(result, uri);
829 });
830 };
831
832 if (token.isCancellationRequested) {
833 return {};
834 }
835
836 try {
837 const result = await Promise.all(queryOptions?.map(option => this._proxy.$startTextSearch(
838 query,
839 option.folder ?? null,
840 option.options,
841 requestId,
842 token) || {}
843 ) ?? []);
844 delete this._activeSearchCallbacks[requestId];
845 return result.reduce((acc, val) => {
846 return {
847 limitHit: acc?.limitHit || (val?.limitHit ?? false),
848 message: [acc?.message ?? [], val?.message ?? []].flat(),
849 };
850 }, {}) ?? { limitHit: false };
851
852 } catch (err) {
853 delete this._activeSearchCallbacks[requestId];
854 throw err;
855 }
856 }
858 > async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions & { useSearchExclude?: boolean }, callback: (result: vscode.TextSearchResult) => void, extensionId: ExtensionIdentifier, token: vscode.CancellationToken = CancellationToken.None): Promise<vscode.TextSearchComplete> {
859 this._logService.trace(`extHostWorkspace#findTextInFiles: textSearch, extension: ${extensionId.value}, entryPoint: findTextInFiles`);
860
861 const previewOptions: vscode.TextSearchPreviewOptions = typeof options.previewOptions === 'undefined' ?
862 {
863 matchLines: 100,
864 charsPerLine: 10000
865 } :
866 options.previewOptions;
867
868 const parsedInclude = parseSearchExcludeInclude(GlobPattern.from(options.include));
869
870 const excludePattern = (typeof options.exclude === 'string') ? options.exclude :
871 options.exclude ? options.exclude.pattern : undefined;
872 const queryOptions: ITextQueryBuilderOptions = {
873 ignoreSymlinks: typeof options.followSymlinks === 'boolean' ? !options.followSymlinks : undefined,
874 disregardIgnoreFiles: typeof options.useIgnoreFiles === 'boolean' ? !options.useIgnoreFiles : undefined,
875 disregardGlobalIgnoreFiles: typeof options.useGlobalIgnoreFiles === 'boolean' ? !options.useGlobalIgnoreFiles : undefined,
876 disregardParentIgnoreFiles: typeof options.useParentIgnoreFiles === 'boolean' ? !options.useParentIgnoreFiles : undefined,
877 disregardExcludeSettings: typeof options.useDefaultExcludes === 'boolean' ? !options.useDefaultExcludes : true,
878 disregardSearchExcludeSettings: typeof options.useSearchExclude === 'boolean' ? !options.useSearchExclude : true,
879 fileEncoding: options.encoding,
880 maxResults: options.maxResults,
881 previewOptions,
882 surroundingContext: options.afterContext, // TODO: remove ability to have before/after context separately
883
884 includePattern: parsedInclude?.pattern,
885 excludePattern: excludePattern ? [{ pattern: excludePattern }] : undefined,
886 };
887
888 const progress = (result: ITextSearchResult<URI>, uri: URI) => {
889 if (resultIsMatch(result)) {
890 callback({
891 uri,
892 preview: {
893 text: result.previewText,
894 matches: mapArrayOrNot(
895 result.rangeLocations,
896 m => new Range(m.preview.startLineNumber, m.preview.startColumn, m.preview.endLineNumber, m.preview.endColumn))
897 },
898 ranges: mapArrayOrNot(
899 result.rangeLocations,
900 r => new Range(r.source.startLineNumber, r.source.startColumn, r.source.endLineNumber, r.source.endColumn))
901 } satisfies vscode.TextSearchMatch);
902 } else {
903 callback({
904 uri,
905 text: result.text,
906 lineNumber: result.lineNumber
907 } satisfies vscode.TextSearchContext);
908 }
909 };
910
911 return this.findTextInFilesBase(query, [{ options: queryOptions, folder: parsedInclude?.folder }], progress, token);
912 }
914 > $handleTextSearchResult(result: IRawFileMatch2, requestId: number): void {
915 this._activeSearchCallbacks[requestId]?.(result);
916 }
918 > async save(uri: URI): Promise<URI | undefined> {
919 const result = await this._proxy.$save(uri, { saveAs: false });
920
921 return URI.revive(result);
922 }
924 > async saveAs(uri: URI): Promise<URI | undefined> {
925 const result = await this._proxy.$save(uri, { saveAs: true });
926
927 return URI.revive(result);
928 }
930 > saveAll(includeUntitled?: boolean): Promise<boolean> {
931 return this._proxy.$saveAll(includeUntitled);
932 }
934 > resolveProxy(url: string): Promise<string | undefined> {
935 return this._proxy.$resolveProxy(url);
936 }
938 > lookupAuthorization(authInfo: AuthInfo): Promise<Credentials | undefined> {
939 return this._proxy.$lookupAuthorization(authInfo);
940 }
942 > lookupKerberosAuthorization(url: string): Promise<string | undefined> {
943 return this._proxy.$lookupKerberosAuthorization(url);
944 }
946 > loadCertificates(): Promise<string[]> {
947 return this._proxy.$loadCertificates();
948 }
950 > // --- trust ---
951 >
952 > get trusted(): boolean {
953 return this._trusted;
954 }
956 > requestResourceTrust(options: vscode.ResourceTrustRequestOptions): Promise<boolean | undefined> {
957 return this._proxy.$requestResourceTrust(options);
958 }
960 > requestWorkspaceTrust(options?: vscode.WorkspaceTrustRequestOptions): Promise<boolean | undefined> {
961 return this._proxy.$requestWorkspaceTrust(options);
962 }
964 > $onDidGrantWorkspaceTrust(): void {
965 if (!this._trusted) {
966 this._trusted = true;
967 this._onDidGrantWorkspaceTrust.fire();
968 }
969 }
971 > $onDidChangeWorkspaceTrustedFolders(): void {
972 this._onDidChangeWorkspaceTrustedFolders.fire();
973 }
975 > isResourceTrusted(resource: vscode.Uri): Promise<boolean> {
976 return this._proxy.$isResourceTrusted(resource);
977 }
979 > // --- edit sessions ---
980 >
981 > private _providerHandlePool = 0;
982 >
983 > // called by ext host
984 > registerEditSessionIdentityProvider(scheme: string, provider: vscode.EditSessionIdentityProvider) {
985 if (this._editSessionIdentityProviders.has(scheme)) {
986 throw new Error(`A provider has already been registered for scheme ${scheme}`);
987 }
988
989 this._editSessionIdentityProviders.set(scheme, provider);
990 const outgoingScheme = this._uriTransformerService.transformOutgoingScheme(scheme);
991 const handle = this._providerHandlePool++;
992 this._proxy.$registerEditSessionIdentityProvider(handle, outgoingScheme);
993
994 return toDisposable(() => {
995 this._editSessionIdentityProviders.delete(scheme);
996 this._proxy.$unregisterEditSessionIdentityProvider(handle);
997 });
998 }
1000 > // called by main thread
1001 > async $getEditSessionIdentifier(workspaceFolder: UriComponents, cancellationToken: CancellationToken): Promise<string | undefined> {
1002 this._logService.info('Getting edit session identifier for workspaceFolder', workspaceFolder);
1003 const folder = await this.resolveWorkspaceFolder(URI.revive(workspaceFolder));
1004 if (!folder) {
1005 this._logService.warn('Unable to resolve workspace folder');
1006 return undefined;
1007 }
1008
1009 this._logService.info('Invoking #provideEditSessionIdentity for workspaceFolder', folder);
1010
1011 const provider = this._editSessionIdentityProviders.get(folder.uri.scheme);
1012 this._logService.info(`Provider for scheme ${folder.uri.scheme} is defined: `, !!provider);
1013 if (!provider) {
1014 return undefined;
1015 }
1016
1017 const result = await provider.provideEditSessionIdentity(folder, cancellationToken);
1018 this._logService.info('Provider returned edit session identifier: ', result);
1019 if (!result) {
1020 return undefined;
1021 }
1022
1023 return result;
1024 }
1026 > async $provideEditSessionIdentityMatch(workspaceFolder: UriComponents, identity1: string, identity2: string, cancellationToken: CancellationToken): Promise<EditSessionIdentityMatch | undefined> {
1027 this._logService.info('Getting edit session identifier for workspaceFolder', workspaceFolder);
1028 const folder = await this.resolveWorkspaceFolder(URI.revive(workspaceFolder));
1029 if (!folder) {
1030 this._logService.warn('Unable to resolve workspace folder');
1031 return undefined;
1032 }
1033
1034 this._logService.info('Invoking #provideEditSessionIdentity for workspaceFolder', folder);
1035
1036 const provider = this._editSessionIdentityProviders.get(folder.uri.scheme);
1037 this._logService.info(`Provider for scheme ${folder.uri.scheme} is defined: `, !!provider);
1038 if (!provider) {
1039 return undefined;
1040 }
1041
1042 const result = await provider.provideEditSessionIdentityMatch?.(identity1, identity2, cancellationToken);
1043 this._logService.info('Provider returned edit session identifier match result: ', result);
1044 if (!result) {
1045 return undefined;
1046 }
1047
1048 return result;
1049 }
1051 > private readonly _onWillCreateEditSessionIdentityEvent = new AsyncEmitter<vscode.EditSessionIdentityWillCreateEvent>();
1052 >
1053 > getOnWillCreateEditSessionIdentityEvent(extension: IExtensionDescription): Event<vscode.EditSessionIdentityWillCreateEvent> {
1054 return (listener, thisArg, disposables) => {
1055 const wrappedListener: IExtensionListener<vscode.EditSessionIdentityWillCreateEvent> = function wrapped(e) { listener.call(thisArg, e); };
1056 wrappedListener.extension = extension;
1057 return this._onWillCreateEditSessionIdentityEvent.event(wrappedListener, undefined, disposables);
1058 };
1059 }
1061 > // main thread calls this to trigger participants
1062 > async $onWillCreateEditSessionIdentity(workspaceFolder: UriComponents, token: CancellationToken, timeout: number): Promise<void> {
1063 const folder = await this.resolveWorkspaceFolder(URI.revive(workspaceFolder));
1064
1065 if (folder === undefined) {
1066 throw new Error('Unable to resolve workspace folder');
1067 }
1068
1069 await this._onWillCreateEditSessionIdentityEvent.fireAsync({ workspaceFolder: folder }, token, async (thenable: Promise<unknown>, listener) => {
1070 const now = Date.now();
1071 await Promise.resolve(thenable);
1072 if (Date.now() - now > timeout) {
1073 this._logService.warn('SLOW edit session create-participant', (<IExtensionListener<vscode.EditSessionIdentityWillCreateEvent>>listener).extension.identifier);
1074 }
1075 });
1076
1077 if (token.isCancellationRequested) {
1078 return undefined;
1079 }
1080 }
1082 > // --- canonical uri identity ---
1083 >
1084 > private readonly _canonicalUriProviders = new Map<string, vscode.CanonicalUriProvider>();
1085 >
1086 > // called by ext host
1087 > registerCanonicalUriProvider(scheme: string, provider: vscode.CanonicalUriProvider) {
1088 if (this._canonicalUriProviders.has(scheme)) {
1089 throw new Error(`A provider has already been registered for scheme ${scheme}`);
1090 }
1091
1092 this._canonicalUriProviders.set(scheme, provider);
1093 const outgoingScheme = this._uriTransformerService.transformOutgoingScheme(scheme);
1094 const handle = this._providerHandlePool++;
1095 this._proxy.$registerCanonicalUriProvider(handle, outgoingScheme);
1096
1097 return toDisposable(() => {
1098 this._canonicalUriProviders.delete(scheme);
1099 this._proxy.$unregisterCanonicalUriProvider(handle);
1100 });
1101 }
1103 > async provideCanonicalUri(uri: URI, options: vscode.CanonicalUriRequestOptions, cancellationToken: CancellationToken): Promise<URI | undefined> {
1104 const provider = this._canonicalUriProviders.get(uri.scheme);
1105 if (!provider) {
1106 return undefined;
1107 }
1108
1109 const result = await provider.provideCanonicalUri?.(URI.revive(uri), options, cancellationToken);
1110 if (!result) {
1111 return undefined;
1112 }
1113
1114 return result;
1115 }
1117 > // called by main thread
1118 > async $provideCanonicalUri(uri: UriComponents, targetScheme: string, cancellationToken: CancellationToken): Promise<UriComponents | undefined> {
1119 return this.provideCanonicalUri(URI.revive(uri), { targetScheme }, cancellationToken);
1120 }
1122 > // --- encodings ---
1123 >
1124 > async decode(content: Uint8Array, args?: { uri?: vscode.Uri; encoding?: string }): Promise<string> {
1125 const [uri, opts] = this.toEncodeDecodeParameters(args);
1126 const options = await this._proxy.$resolveDecoding(uri, opts);
1127
1128 const stream = (await toDecodeStream(bufferToStream(VSBuffer.wrap(content)), {
1129 ...options,
1130 acceptTextOnly: true,
1131 overwriteEncoding: detectedEncoding => {
1132 if (detectedEncoding === null || detectedEncoding === options.preferredEncoding) {
1133 // Prevent another roundtrip to the main thread
1134 // if the detected encoding is null or the same
1135 // as the preferred encoding
1136 return Promise.resolve(options.preferredEncoding);
1137 }
1138
1139 return this._proxy.$validateDetectedEncoding(uri, detectedEncoding, opts);
1140 },
1141 })).stream;
1142
1143 return consumeStream(stream, chunks => chunks.join(''));
1144 }
1146 > async encode(content: string, args?: { uri?: vscode.Uri; encoding?: string }): Promise<Uint8Array> {
1147 const [uri, options] = this.toEncodeDecodeParameters(args);
1148 const { encoding, addBOM } = await this._proxy.$resolveEncoding(uri, options);
1149
1150 // when encoding is standard skip encoding step
1151 if (encoding === UTF8 && !addBOM) {
1152 return VSBuffer.fromString(content).buffer;
1153 }
1154
1155 // otherwise create encoded readable
1156 const res = await toEncodeReadable(stringToSnapshot(content), encoding, { addBOM });
1157 return readableToBuffer(res).buffer;
1158 }
1160 > private toEncodeDecodeParameters(opts?: { uri?: vscode.Uri; encoding?: string }): [UriComponents | undefined, { encoding: string } | undefined] {
1161 const uri = isUriComponents(opts?.uri) ? opts.uri : undefined;
1162 const encoding = typeof opts?.encoding === 'string' ? opts.encoding : undefined;
1163
1164 return [uri, encoding ? { encoding } : undefined];
1165 }
1167 >
1168 > export const IExtHostWorkspace = createDecorator<IExtHostWorkspace>('IExtHostWorkspace');
1169 > export interface IExtHostWorkspace extends ExtHostWorkspace, ExtHostWorkspaceShape, IExtHostWorkspaceProvider { }
1170 >
1171 function parseSearchExcludeInclude(include: string | IRelativePatternDto | undefined | null): { pattern: string; folder?: URI } | undefined {
1172 let pattern: string | undefined;
1173 let includeFolder: URI | undefined;
1174 if (include) {
1175 if (typeof include === 'string') {
1176 pattern = include;
1177 } else {
1178 pattern = include.pattern;
1179 includeFolder = URI.revive(include.baseUri);
1180 }
1181
1182 return {
1183 pattern,
1184 folder: includeFolder
1185 };
1186 }
1187 return undefined;
1188 }
1190 > interface IExtensionListener<E> {
1191 > extension: IExtensionDescription;
1192 > (e: E): any;
1193 > }
1194 >
1195 function globsToISearchPatternBuilder(excludes: vscode.GlobPattern[] | undefined): ISearchPatternBuilder<URI>[] {
1196 return (
1197 excludes?.map((exclude): ISearchPatternBuilder<URI> | undefined => {
1198 if (typeof exclude === 'string') {
1199 if (exclude === '') {
1200 return undefined;
1201 }
1202 return {
1203 pattern: exclude,
1204 uri: undefined
1205 } satisfies ISearchPatternBuilder<URI>;
1206 } else {
1207 const parsedExclude = parseSearchExcludeInclude(exclude);
1208 if (!parsedExclude) {
1209 return undefined;
1210 }
1211 return {
1212 pattern: parsedExclude.pattern,
1213 uri: parsedExclude.folder
1214 } satisfies ISearchPatternBuilder<URI>;
1215 }
1216 }) ?? []
1217 ).filter((e): e is ISearchPatternBuilder<URI> => !!e);
1218 }