Skip to content

Commit 75323b3

Browse files
authored
Improve resolveFilePath function to correctly handle URIs and file paths (#25632)
Fixes #25382
1 parent 2cb58d5 commit 75323b3

File tree

2 files changed

+262
-6
lines changed

2 files changed

+262
-6
lines changed

src/client/chat/utils.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,29 @@ import { JUPYTER_EXTENSION_ID, NotebookCellScheme } from '../common/constants';
1919
import { dirname, join } from 'path';
2020
import { resolveEnvironment, useEnvExtension } from '../envExt/api.internal';
2121
import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils';
22+
import { getWorkspaceFolders } from '../common/vscodeApis/workspaceApis';
2223

2324
export interface IResourceReference {
2425
resourcePath?: string;
2526
}
2627

2728
export function resolveFilePath(filepath?: string): Uri | undefined {
2829
if (!filepath) {
29-
return workspace.workspaceFolders ? workspace.workspaceFolders[0].uri : undefined;
30+
const folders = getWorkspaceFolders() ?? [];
31+
return folders.length > 0 ? folders[0].uri : undefined;
3032
}
31-
// starts with a scheme
32-
try {
33-
return Uri.parse(filepath);
34-
} catch (e) {
35-
return Uri.file(filepath);
33+
// Check if it's a URI with a scheme (contains "://")
34+
// This handles schemes like "file://", "vscode-notebook://", etc.
35+
// But avoids treating Windows drive letters like "C:" as schemes
36+
if (filepath.includes('://')) {
37+
try {
38+
return Uri.parse(filepath);
39+
} catch {
40+
return Uri.file(filepath);
41+
}
3642
}
43+
// For file paths (Windows with drive letters, Unix absolute/relative paths)
44+
return Uri.file(filepath);
3745
}
3846

3947
/**

src/test/chat/utils.unit.test.ts

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { expect } from 'chai';
7+
import * as sinon from 'sinon';
8+
import { Uri, WorkspaceFolder } from 'vscode';
9+
import { resolveFilePath } from '../../client/chat/utils';
10+
import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis';
11+
12+
suite('Chat Utils - resolveFilePath()', () => {
13+
let getWorkspaceFoldersStub: sinon.SinonStub;
14+
15+
setup(() => {
16+
getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders');
17+
getWorkspaceFoldersStub.returns([]);
18+
});
19+
20+
teardown(() => {
21+
sinon.restore();
22+
});
23+
24+
suite('When filepath is undefined or empty', () => {
25+
test('Should return first workspace folder URI when workspace folders exist', () => {
26+
const expectedUri = Uri.file('/test/workspace');
27+
const mockFolder: WorkspaceFolder = {
28+
uri: expectedUri,
29+
name: 'test',
30+
index: 0,
31+
};
32+
getWorkspaceFoldersStub.returns([mockFolder]);
33+
34+
const result = resolveFilePath(undefined);
35+
36+
expect(result?.toString()).to.equal(expectedUri.toString());
37+
});
38+
39+
test('Should return first folder when multiple workspace folders exist', () => {
40+
const firstUri = Uri.file('/first/workspace');
41+
const secondUri = Uri.file('/second/workspace');
42+
const mockFolders: WorkspaceFolder[] = [
43+
{ uri: firstUri, name: 'first', index: 0 },
44+
{ uri: secondUri, name: 'second', index: 1 },
45+
];
46+
getWorkspaceFoldersStub.returns(mockFolders);
47+
48+
const result = resolveFilePath(undefined);
49+
50+
expect(result?.toString()).to.equal(firstUri.toString());
51+
});
52+
53+
test('Should return undefined when no workspace folders exist', () => {
54+
getWorkspaceFoldersStub.returns(undefined);
55+
56+
const result = resolveFilePath(undefined);
57+
58+
expect(result).to.be.undefined;
59+
});
60+
61+
test('Should return undefined when workspace folders is empty array', () => {
62+
getWorkspaceFoldersStub.returns([]);
63+
64+
const result = resolveFilePath(undefined);
65+
66+
expect(result).to.be.undefined;
67+
});
68+
69+
test('Should return undefined for empty string when no workspace folders', () => {
70+
getWorkspaceFoldersStub.returns(undefined);
71+
72+
const result = resolveFilePath('');
73+
74+
expect(result).to.be.undefined;
75+
});
76+
});
77+
78+
suite('Windows file paths', () => {
79+
test('Should handle Windows path with lowercase drive letter', () => {
80+
const filepath = 'c:\\GIT\\tests\\simple-python-app';
81+
82+
const result = resolveFilePath(filepath);
83+
84+
expect(result).to.not.be.undefined;
85+
expect(result?.scheme).to.equal('file');
86+
// Uri.file normalizes drive letters to lowercase
87+
expect(result?.fsPath.toLowerCase()).to.include('git');
88+
});
89+
90+
test('Should handle Windows path with uppercase drive letter', () => {
91+
const filepath = 'C:\\Users\\test\\project';
92+
93+
const result = resolveFilePath(filepath);
94+
95+
expect(result).to.not.be.undefined;
96+
expect(result?.scheme).to.equal('file');
97+
expect(result?.fsPath.toLowerCase()).to.include('users');
98+
});
99+
100+
test('Should handle Windows path with forward slashes', () => {
101+
const filepath = 'C:/Users/test/project';
102+
103+
const result = resolveFilePath(filepath);
104+
105+
expect(result).to.not.be.undefined;
106+
expect(result?.scheme).to.equal('file');
107+
});
108+
});
109+
110+
suite('Unix file paths', () => {
111+
test('Should handle Unix absolute path', () => {
112+
const filepath = '/home/user/projects/myapp';
113+
114+
const result = resolveFilePath(filepath);
115+
116+
expect(result).to.not.be.undefined;
117+
expect(result?.scheme).to.equal('file');
118+
expect(result?.path).to.include('/home/user/projects/myapp');
119+
});
120+
121+
test('Should handle Unix root path', () => {
122+
const filepath = '/';
123+
124+
const result = resolveFilePath(filepath);
125+
126+
expect(result).to.not.be.undefined;
127+
expect(result?.scheme).to.equal('file');
128+
});
129+
});
130+
131+
suite('Relative paths', () => {
132+
test('Should handle relative path with dot prefix', () => {
133+
const filepath = './src/main.py';
134+
135+
const result = resolveFilePath(filepath);
136+
137+
expect(result).to.not.be.undefined;
138+
expect(result?.scheme).to.equal('file');
139+
});
140+
141+
test('Should handle relative path without prefix', () => {
142+
const filepath = 'src/main.py';
143+
144+
const result = resolveFilePath(filepath);
145+
146+
expect(result).to.not.be.undefined;
147+
expect(result?.scheme).to.equal('file');
148+
});
149+
150+
test('Should handle parent directory reference', () => {
151+
const filepath = '../other-project/file.py';
152+
153+
const result = resolveFilePath(filepath);
154+
155+
expect(result).to.not.be.undefined;
156+
expect(result?.scheme).to.equal('file');
157+
});
158+
});
159+
160+
suite('URI schemes', () => {
161+
test('Should handle file:// URI scheme', () => {
162+
const filepath = 'file:///home/user/test.py';
163+
164+
const result = resolveFilePath(filepath);
165+
166+
expect(result).to.not.be.undefined;
167+
expect(result?.scheme).to.equal('file');
168+
expect(result?.path).to.include('/home/user/test.py');
169+
});
170+
171+
test('Should handle vscode-notebook:// URI scheme', () => {
172+
const filepath = 'vscode-notebook://jupyter/notebook.ipynb';
173+
174+
const result = resolveFilePath(filepath);
175+
176+
expect(result).to.not.be.undefined;
177+
expect(result?.scheme).to.equal('vscode-notebook');
178+
});
179+
180+
test('Should handle untitled: URI scheme without double slash as file path', () => {
181+
const filepath = 'untitled:Untitled-1';
182+
183+
const result = resolveFilePath(filepath);
184+
185+
expect(result).to.not.be.undefined;
186+
// untitled: doesn't have ://, so it will be treated as a file path
187+
expect(result?.scheme).to.equal('file');
188+
});
189+
190+
test('Should handle https:// URI scheme', () => {
191+
const filepath = 'https://example.com/path';
192+
193+
const result = resolveFilePath(filepath);
194+
195+
expect(result).to.not.be.undefined;
196+
expect(result?.scheme).to.equal('https');
197+
});
198+
199+
test('Should handle vscode-vfs:// URI scheme', () => {
200+
const filepath = 'vscode-vfs://github/microsoft/vscode/file.ts';
201+
202+
const result = resolveFilePath(filepath);
203+
204+
expect(result).to.not.be.undefined;
205+
expect(result?.scheme).to.equal('vscode-vfs');
206+
});
207+
});
208+
209+
suite('Edge cases', () => {
210+
test('Should handle path with spaces', () => {
211+
const filepath = '/home/user/my project/file.py';
212+
213+
const result = resolveFilePath(filepath);
214+
215+
expect(result).to.not.be.undefined;
216+
expect(result?.scheme).to.equal('file');
217+
});
218+
219+
test('Should handle path with special characters', () => {
220+
const filepath = '/home/user/project-name_v2/file.py';
221+
222+
const result = resolveFilePath(filepath);
223+
224+
expect(result).to.not.be.undefined;
225+
expect(result?.scheme).to.equal('file');
226+
});
227+
228+
test('Should not treat Windows drive letter colon as URI scheme', () => {
229+
// Windows path should not be confused with a URI scheme
230+
const filepath = 'd:\\projects\\test';
231+
232+
const result = resolveFilePath(filepath);
233+
234+
expect(result).to.not.be.undefined;
235+
expect(result?.scheme).to.equal('file');
236+
});
237+
238+
test('Should not treat single colon as URI scheme', () => {
239+
// A path with a colon but not :// should be treated as a file
240+
const filepath = 'c:somepath';
241+
242+
const result = resolveFilePath(filepath);
243+
244+
expect(result).to.not.be.undefined;
245+
expect(result?.scheme).to.equal('file');
246+
});
247+
});
248+
});

0 commit comments

Comments
 (0)