@theia/ai-ide 1.66.2 → 1.67.0-next.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/lib/browser/context-file-validation-service-impl.d.ts +13 -0
  2. package/lib/browser/context-file-validation-service-impl.d.ts.map +1 -0
  3. package/lib/browser/context-file-validation-service-impl.js +123 -0
  4. package/lib/browser/context-file-validation-service-impl.js.map +1 -0
  5. package/lib/browser/context-file-validation-service-impl.spec.d.ts +2 -0
  6. package/lib/browser/context-file-validation-service-impl.spec.d.ts.map +1 -0
  7. package/lib/browser/context-file-validation-service-impl.spec.js +340 -0
  8. package/lib/browser/context-file-validation-service-impl.spec.js.map +1 -0
  9. package/lib/browser/context-functions.d.ts +2 -0
  10. package/lib/browser/context-functions.d.ts.map +1 -1
  11. package/lib/browser/context-functions.js +41 -5
  12. package/lib/browser/context-functions.js.map +1 -1
  13. package/lib/browser/context-functions.spec.js +120 -0
  14. package/lib/browser/context-functions.spec.js.map +1 -1
  15. package/lib/browser/frontend-module.d.ts.map +1 -1
  16. package/lib/browser/frontend-module.js +4 -0
  17. package/lib/browser/frontend-module.js.map +1 -1
  18. package/lib/browser/workspace-functions.d.ts +3 -0
  19. package/lib/browser/workspace-functions.d.ts.map +1 -1
  20. package/lib/browser/workspace-functions.js +56 -0
  21. package/lib/browser/workspace-functions.js.map +1 -1
  22. package/package.json +21 -21
  23. package/src/browser/context-file-validation-service-impl.spec.ts +405 -0
  24. package/src/browser/context-file-validation-service-impl.ts +120 -0
  25. package/src/browser/context-functions.spec.ts +155 -1
  26. package/src/browser/context-functions.ts +40 -6
  27. package/src/browser/frontend-module.ts +5 -0
  28. package/src/browser/workspace-functions.ts +68 -1
@@ -0,0 +1,405 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+ let disableJSDOM = enableJSDOM();
19
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
20
+ FrontendApplicationConfigProvider.set({});
21
+
22
+ import { expect } from 'chai';
23
+ import { Container } from '@theia/core/shared/inversify';
24
+ import { URI, PreferenceService } from '@theia/core';
25
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
26
+ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
27
+ import { FileStat } from '@theia/filesystem/lib/common/files';
28
+ import { ContextFileValidationService, FileValidationState } from '@theia/ai-chat/lib/browser/context-file-validation-service';
29
+ import { ContextFileValidationServiceImpl } from './context-file-validation-service-impl';
30
+ import { WorkspaceFunctionScope } from './workspace-functions';
31
+
32
+ disableJSDOM();
33
+
34
+ describe('ContextFileValidationService', () => {
35
+ let container: Container;
36
+ let validationService: ContextFileValidationService;
37
+ let mockFileService: FileService;
38
+ let mockWorkspaceService: WorkspaceService;
39
+ let mockPreferenceService: PreferenceService;
40
+
41
+ const workspaceRoot = new URI('file:///home/user/workspace');
42
+
43
+ // Store URIs as actual URI strings, exactly as URI.toString() would produce them
44
+ const existingFiles = new Map<string, boolean>([
45
+ // Files inside workspace
46
+ ['file:///home/user/workspace/src/index.tsx', true],
47
+ ['file:///home/user/workspace/package.json', true],
48
+ ['file:///home/user/workspace/README.md', true],
49
+ ['file:///home/user/workspace/src/components/Button.tsx', true],
50
+ ['file:///home/user/workspace/config.json', true],
51
+ ['file:///home/user/workspace/src/file%20with%20spaces.tsx', true],
52
+ // Files outside workspace (these exist but should be rejected)
53
+ ['file:///etc/passwd', true],
54
+ ['file:///etc/hosts', true],
55
+ ['file:///home/other-user/secret.txt', true],
56
+ ['file:///tmp/temporary-file.log', true]
57
+ ]);
58
+
59
+ before(() => {
60
+ disableJSDOM = enableJSDOM();
61
+ });
62
+
63
+ after(() => {
64
+ disableJSDOM();
65
+ });
66
+
67
+ beforeEach(async () => {
68
+ container = new Container();
69
+
70
+ // Mock WorkspaceService
71
+ mockWorkspaceService = {
72
+ tryGetRoots: () => [{
73
+ resource: workspaceRoot,
74
+ isDirectory: true
75
+ } as FileStat],
76
+ roots: Promise.resolve([{
77
+ resource: workspaceRoot,
78
+ isDirectory: true
79
+ } as FileStat])
80
+ } as unknown as WorkspaceService;
81
+
82
+ // Mock FileService
83
+ mockFileService = {
84
+ exists: async (uri: URI) => {
85
+ const normalizedUri = uri.path.normalize();
86
+ const normalizedUriString = uri.withPath(normalizedUri).toString();
87
+ const uriString = uri.toString();
88
+
89
+ const exists = (existingFiles.has(uriString) && existingFiles.get(uriString) === true) ||
90
+ (existingFiles.has(normalizedUriString) && existingFiles.get(normalizedUriString) === true);
91
+ return exists;
92
+ },
93
+ resolve: async (uri: URI) => {
94
+ const uriString = uri.toString();
95
+ if (existingFiles.has(uriString) && existingFiles.get(uriString) === true) {
96
+ return {
97
+ resource: uri,
98
+ isDirectory: false
99
+ } as FileStat;
100
+ }
101
+ throw new Error('File not found');
102
+ }
103
+ } as unknown as FileService;
104
+
105
+ // Mock PreferenceService
106
+ mockPreferenceService = {
107
+ get: () => false
108
+ } as unknown as PreferenceService;
109
+
110
+ container.bind(FileService).toConstantValue(mockFileService);
111
+ container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
112
+ container.bind(PreferenceService).toConstantValue(mockPreferenceService);
113
+ container.bind(WorkspaceFunctionScope).toSelf();
114
+ container.bind(ContextFileValidationServiceImpl).toSelf();
115
+ container.bind(ContextFileValidationService).toService(ContextFileValidationServiceImpl);
116
+
117
+ validationService = await container.getAsync(ContextFileValidationService);
118
+ });
119
+
120
+ describe('validateFile with relative paths', () => {
121
+ it('should validate existing file with relative path', async () => {
122
+ const result = await validationService.validateFile('src/index.tsx');
123
+ expect(result.state).to.equal(FileValidationState.VALID);
124
+ });
125
+
126
+ it('should reject non-existing file with relative path', async () => {
127
+ const result = await validationService.validateFile('src/missing.tsx');
128
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
129
+ });
130
+
131
+ it('should validate nested file with relative path', async () => {
132
+ const result = await validationService.validateFile('src/components/Button.tsx');
133
+ expect(result.state).to.equal(FileValidationState.VALID);
134
+ });
135
+
136
+ it('should validate file in root with relative path', async () => {
137
+ const result = await validationService.validateFile('package.json');
138
+ expect(result.state).to.equal(FileValidationState.VALID);
139
+ });
140
+ });
141
+
142
+ describe('validateFile with absolute file paths', () => {
143
+ it('should validate existing file with absolute path within workspace', async () => {
144
+ const result = await validationService.validateFile('/home/user/workspace/src/index.tsx');
145
+ expect(result.state).to.equal(FileValidationState.VALID);
146
+ });
147
+
148
+ it('should reject non-existing file with absolute path within workspace', async () => {
149
+ const result = await validationService.validateFile('/home/user/workspace/src/missing.tsx');
150
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
151
+ });
152
+
153
+ it('should reject existing file with absolute path outside workspace (/etc/passwd)', async () => {
154
+ const result = await validationService.validateFile('/etc/passwd');
155
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
156
+ });
157
+
158
+ it('should reject existing file with absolute path outside workspace (/etc/hosts)', async () => {
159
+ const result = await validationService.validateFile('/etc/hosts');
160
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
161
+ });
162
+
163
+ it('should reject existing file with absolute path in other user directory', async () => {
164
+ const result = await validationService.validateFile('/home/other-user/secret.txt');
165
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
166
+ });
167
+
168
+ it('should reject existing file with absolute path in /tmp', async () => {
169
+ const result = await validationService.validateFile('/tmp/temporary-file.log');
170
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
171
+ });
172
+
173
+ it('should reject non-existing file with absolute path outside workspace', async () => {
174
+ const result = await validationService.validateFile('/var/log/nonexistent.log');
175
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
176
+ });
177
+
178
+ it('should validate nested file with absolute path within workspace', async () => {
179
+ const result = await validationService.validateFile('/home/user/workspace/src/components/Button.tsx');
180
+ expect(result.state).to.equal(FileValidationState.VALID);
181
+ });
182
+ });
183
+
184
+ describe('validateFile with file:// URIs', () => {
185
+ it('should validate existing file with file:// URI within workspace', async () => {
186
+ const result = await validationService.validateFile('file:///home/user/workspace/src/index.tsx');
187
+ expect(result.state).to.equal(FileValidationState.VALID);
188
+ });
189
+
190
+ it('should reject non-existing file with file:// URI within workspace', async () => {
191
+ const result = await validationService.validateFile('file:///home/user/workspace/src/missing.tsx');
192
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
193
+ });
194
+
195
+ it('should reject existing file with file:// URI outside workspace (/etc/passwd)', async () => {
196
+ const result = await validationService.validateFile('file:///etc/passwd');
197
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
198
+ });
199
+
200
+ it('should reject existing file with file:// URI outside workspace (/etc/hosts)', async () => {
201
+ const result = await validationService.validateFile('file:///etc/hosts');
202
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
203
+ });
204
+
205
+ it('should reject existing file with file:// URI in other user directory', async () => {
206
+ const result = await validationService.validateFile('file:///home/other-user/secret.txt');
207
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
208
+ });
209
+
210
+ it('should reject existing file with file:// URI in /tmp', async () => {
211
+ const result = await validationService.validateFile('file:///tmp/temporary-file.log');
212
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
213
+ });
214
+
215
+ it('should reject non-existing file with file:// URI outside workspace', async () => {
216
+ const result = await validationService.validateFile('file:///var/log/nonexistent.log');
217
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
218
+ });
219
+
220
+ it('should validate file at workspace root with file:// URI', async () => {
221
+ const result = await validationService.validateFile('file:///home/user/workspace/package.json');
222
+ expect(result.state).to.equal(FileValidationState.VALID);
223
+ });
224
+ });
225
+
226
+ describe('validateFile with URI objects', () => {
227
+ it('should validate existing file with URI object within workspace', async () => {
228
+ const uri = new URI('file:///home/user/workspace/src/index.tsx');
229
+ const result = await validationService.validateFile(uri);
230
+ expect(result.state).to.equal(FileValidationState.VALID);
231
+ });
232
+
233
+ it('should reject non-existing file with URI object within workspace', async () => {
234
+ const uri = new URI('file:///home/user/workspace/src/missing.tsx');
235
+ const result = await validationService.validateFile(uri);
236
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
237
+ });
238
+
239
+ it('should reject existing file with URI object outside workspace', async () => {
240
+ const uri = new URI('file:///etc/passwd');
241
+ const result = await validationService.validateFile(uri);
242
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
243
+ });
244
+
245
+ it('should reject another existing file with URI object outside workspace', async () => {
246
+ const uri = new URI('file:///home/other-user/secret.txt');
247
+ const result = await validationService.validateFile(uri);
248
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
249
+ });
250
+ });
251
+
252
+ describe('validateFile with no workspace', () => {
253
+ beforeEach(async () => {
254
+ // Override mock to return no workspace roots
255
+ mockWorkspaceService.tryGetRoots = () => [];
256
+ });
257
+
258
+ it('should reject any file when no workspace is open', async () => {
259
+ const result = await validationService.validateFile('src/index.tsx');
260
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
261
+ });
262
+
263
+ it('should reject absolute path when no workspace is open', async () => {
264
+ const result = await validationService.validateFile('/home/user/file.txt');
265
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
266
+ });
267
+
268
+ it('should reject file:// URI when no workspace is open', async () => {
269
+ const result = await validationService.validateFile('file:///home/user/file.txt');
270
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
271
+ });
272
+ });
273
+
274
+ describe('validateFile with multiple workspace roots', () => {
275
+ const workspaceRoot2 = new URI('file:///home/user/other-project');
276
+
277
+ beforeEach(async () => {
278
+ // Override mock to return multiple workspace roots
279
+ mockWorkspaceService.tryGetRoots = () => [
280
+ {
281
+ resource: workspaceRoot,
282
+ isDirectory: true
283
+ } as FileStat,
284
+ {
285
+ resource: workspaceRoot2,
286
+ isDirectory: true
287
+ } as FileStat
288
+ ];
289
+
290
+ // Add files in the second workspace
291
+ existingFiles.set('file:///home/user/other-project/index.js', true);
292
+ existingFiles.set('file:///home/user/other-project/lib/utils.js', true);
293
+ });
294
+
295
+ afterEach(() => {
296
+ // Clean up files added for this test
297
+ existingFiles.delete('file:///home/user/other-project/index.js');
298
+ existingFiles.delete('file:///home/user/other-project/lib/utils.js');
299
+ });
300
+
301
+ it('should validate file in first workspace root', async () => {
302
+ const result = await validationService.validateFile('src/index.tsx');
303
+ expect(result.state).to.equal(FileValidationState.VALID);
304
+ });
305
+
306
+ it('should validate file in second workspace root with relative path', async () => {
307
+ const result = await validationService.validateFile('index.js');
308
+ expect(result.state).to.equal(FileValidationState.INVALID_SECONDARY);
309
+ });
310
+
311
+ it('should validate file in second workspace root with absolute path', async () => {
312
+ const result = await validationService.validateFile('/home/user/other-project/index.js');
313
+ expect(result.state).to.equal(FileValidationState.INVALID_SECONDARY);
314
+ });
315
+
316
+ it('should validate file in second workspace root with file:// URI', async () => {
317
+ const result = await validationService.validateFile('file:///home/user/other-project/lib/utils.js');
318
+ expect(result.state).to.equal(FileValidationState.INVALID_SECONDARY);
319
+ });
320
+
321
+ it('should still reject files outside both workspace roots', async () => {
322
+ const result = await validationService.validateFile('/etc/passwd');
323
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
324
+ });
325
+ });
326
+
327
+ describe('validateFile error handling', () => {
328
+ it('should return false when FileService.exists throws error', async () => {
329
+ mockFileService.exists = async () => {
330
+ throw new Error('Permission denied');
331
+ };
332
+
333
+ const result = await validationService.validateFile('src/index.tsx');
334
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
335
+ });
336
+
337
+ it('should handle Windows-style paths', async () => {
338
+ // Add a Windows path to existing files
339
+ // Note: URI encoding will convert 'c:' to 'c%3A'
340
+ const windowsRoot = new URI('file:///c:/Users/user/project');
341
+ const windowsFile = new URI('file:///c:/Users/user/project/file.txt');
342
+ existingFiles.set(windowsFile.toString(), true);
343
+
344
+ // Override workspace to use Windows path
345
+ mockWorkspaceService.tryGetRoots = () => [{
346
+ resource: windowsRoot,
347
+ isDirectory: true
348
+ } as FileStat];
349
+
350
+ const result = await validationService.validateFile('file:///c:/Users/user/project/file.txt');
351
+ expect(result.state).to.equal(FileValidationState.VALID);
352
+
353
+ // Clean up
354
+ existingFiles.delete(windowsFile.toString());
355
+ });
356
+
357
+ it('should reject Windows system files outside workspace', async () => {
358
+ // Add Windows system file
359
+ const windowsSystemFile = 'file:///c:/Windows/System32/config/sam';
360
+ existingFiles.set(windowsSystemFile, true);
361
+
362
+ // Keep workspace as Linux for this test
363
+ const result = await validationService.validateFile('file:///c:/Windows/System32/config/sam');
364
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
365
+
366
+ // Clean up
367
+ existingFiles.delete(windowsSystemFile);
368
+ });
369
+ });
370
+
371
+ describe('edge cases', () => {
372
+ it('should handle paths with special characters', async () => {
373
+ const result = await validationService.validateFile('file:///home/user/workspace/src/file%20with%20spaces.tsx');
374
+ expect(result.state).to.equal(FileValidationState.VALID);
375
+ });
376
+
377
+ it('should handle paths with normalized separators', async () => {
378
+ const result = await validationService.validateFile('src\\components\\Button.tsx');
379
+ expect(result.state).to.equal(FileValidationState.VALID);
380
+ });
381
+
382
+ it('should reject empty path', async () => {
383
+ const result = await validationService.validateFile('');
384
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
385
+ });
386
+
387
+ it('should reject parent directory references in relative paths', async () => {
388
+ // Parent directory references are not allowed for security and clarity
389
+ const result = await validationService.validateFile('src/../config.json');
390
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
391
+ });
392
+
393
+ it('should reject path traversal attempts with parent directory references', async () => {
394
+ // Path traversal attempts should be rejected
395
+ const result = await validationService.validateFile('../../../../../../etc/passwd');
396
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
397
+ });
398
+
399
+ it('should reject absolute paths with parent directory references', async () => {
400
+ // Even absolute paths with .. should be rejected for consistency
401
+ const result = await validationService.validateFile('/home/user/workspace/src/../config.json');
402
+ expect(result.state).to.equal(FileValidationState.INVALID_NOT_FOUND);
403
+ });
404
+ });
405
+ });
@@ -0,0 +1,120 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { injectable, inject } from '@theia/core/shared/inversify';
18
+ import { URI } from '@theia/core';
19
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
20
+ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
21
+ import { ContextFileValidationService, FileValidationResult, FileValidationState } from '@theia/ai-chat/lib/browser/context-file-validation-service';
22
+ import { WorkspaceFunctionScope } from './workspace-functions';
23
+
24
+ @injectable()
25
+ export class ContextFileValidationServiceImpl implements ContextFileValidationService {
26
+
27
+ @inject(FileService)
28
+ protected readonly fileService: FileService;
29
+
30
+ @inject(WorkspaceFunctionScope)
31
+ protected readonly workspaceScope: WorkspaceFunctionScope;
32
+
33
+ @inject(WorkspaceService)
34
+ protected readonly workspaceService: WorkspaceService;
35
+
36
+ async validateFile(pathOrUri: string | URI): Promise<FileValidationResult> {
37
+ try {
38
+ const resolvedUri = await this.workspaceScope.resolveToUri(pathOrUri);
39
+
40
+ if (!resolvedUri) {
41
+ return {
42
+ state: FileValidationState.INVALID_NOT_FOUND,
43
+ message: 'File does not exist'
44
+ };
45
+ }
46
+
47
+ const exists = await this.fileService.exists(resolvedUri);
48
+ if (!exists) {
49
+ const secondaryRootUri = await this.findInSecondaryWorkspaceRoots(pathOrUri);
50
+ if (secondaryRootUri) {
51
+ return {
52
+ state: FileValidationState.INVALID_SECONDARY,
53
+ message: 'File is in a secondary workspace root. AI agents can only access files in the first workspace root.'
54
+ };
55
+ }
56
+ return {
57
+ state: FileValidationState.INVALID_NOT_FOUND,
58
+ message: 'File does not exist'
59
+ };
60
+ }
61
+
62
+ if (this.workspaceScope.isInPrimaryWorkspace(resolvedUri)) {
63
+ return {
64
+ state: FileValidationState.VALID
65
+ };
66
+ }
67
+
68
+ if (this.workspaceScope.isInWorkspace(resolvedUri)) {
69
+ return {
70
+ state: FileValidationState.INVALID_SECONDARY,
71
+ message: 'File is in a secondary workspace root. AI agents can only access files in the first workspace root.'
72
+ };
73
+ }
74
+
75
+ return {
76
+ state: FileValidationState.INVALID_NOT_FOUND,
77
+ message: 'File does not exist in the workspace'
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ state: FileValidationState.INVALID_NOT_FOUND,
82
+ message: 'File does not exist'
83
+ };
84
+ }
85
+ }
86
+
87
+ protected async findInSecondaryWorkspaceRoots(pathOrUri: string | URI): Promise<URI | undefined> {
88
+ const roots = this.workspaceService.tryGetRoots();
89
+ if (roots.length <= 1) {
90
+ return undefined;
91
+ }
92
+
93
+ for (let i = 1; i < roots.length; i++) {
94
+ const root = roots[i];
95
+ let candidateUri: URI;
96
+
97
+ if (pathOrUri instanceof URI) {
98
+ candidateUri = pathOrUri;
99
+ } else if (pathOrUri.includes('://')) {
100
+ try {
101
+ candidateUri = new URI(pathOrUri);
102
+ } catch {
103
+ continue;
104
+ }
105
+ } else {
106
+ candidateUri = root.resource.resolve(pathOrUri);
107
+ }
108
+
109
+ try {
110
+ if (await this.fileService.exists(candidateUri)) {
111
+ return candidateUri;
112
+ }
113
+ } catch {
114
+ continue;
115
+ }
116
+ }
117
+
118
+ return undefined;
119
+ }
120
+ }