Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/linkify/test/node/modelFilePathLinkifier.spec.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
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 { expect, suite, test } from 'vitest';
7
import { NullEnvService } from '../../../../platform/env/common/nullEnvService';
8
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
9
import { FileType } from '../../../../platform/filesystem/common/fileTypes';
10
import { NullWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
11
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
12
import { URI } from '../../../../util/vs/base/common/uri';
13
import { Location, Position, Range } from '../../../../vscodeTypes';
14
import { LinkifyLocationAnchor } from '../../common/linkifiedText';
15
import { LinkifyService } from '../../common/linkifyService';
16
import { assertPartsEqual, createTestLinkifierService, linkify, workspaceFile } from './util';
17
18
suite('Model File Path Linkifier', () => {
19
test('Should linkify model generated file references with line range', async () => {
20
const service = createTestLinkifierService('src/file.ts');
21
const result = await linkify(service, '[src/file.ts](src/file.ts#L10-12)');
22
const anchor = result.parts[0] as LinkifyLocationAnchor;
23
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(9, 0), new Position(11, 0))));
24
expect(anchor.title).toBe('src/file.ts#L10-L12');
25
assertPartsEqual([anchor], [expected]);
26
});
27
28
test('Should linkify single line anchors', async () => {
29
const service = createTestLinkifierService('src/file.ts');
30
const result = await linkify(service, '[src/file.ts](src/file.ts#L5)');
31
const anchor = result.parts[0] as LinkifyLocationAnchor;
32
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(4, 0), new Position(4, 0))));
33
expect(anchor.title).toBe('src/file.ts#L5');
34
assertPartsEqual([anchor], [expected]);
35
});
36
37
test('Should linkify absolute file paths', async () => {
38
const absolutePath = workspaceFile('src/file.ts').fsPath;
39
const service = createTestLinkifierService('src/file.ts');
40
const result = await linkify(service, `[src/file.ts](${absolutePath}#L2)`);
41
const anchor = result.parts[0] as LinkifyLocationAnchor;
42
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(1, 0), new Position(1, 0))));
43
expect(anchor.title).toBe('src/file.ts#L2');
44
assertPartsEqual([anchor], [expected]);
45
});
46
47
test('Should decode percent-encoded targets', async () => {
48
const service = createTestLinkifierService('space file.ts');
49
const result = await linkify(service, '[space file.ts](space%20file.ts#L1)');
50
const anchor = result.parts[0] as LinkifyLocationAnchor;
51
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('space file.ts'), new Range(new Position(0, 0), new Position(0, 0))));
52
assertPartsEqual([anchor], [expected]);
53
});
54
55
test('Should fallback when text does not match base path and no anchor', async () => {
56
const service = createTestLinkifierService('src/file.ts');
57
const result = await linkify(service, '[other](src/file.ts)');
58
assertPartsEqual(result.parts, ['other']);
59
});
60
61
test('Should linkify descriptive text with anchor', async () => {
62
const service = createTestLinkifierService('src/file.ts');
63
const result = await linkify(service, '[Await chat view](src/file.ts#L54)');
64
const anchor = result.parts[0] as LinkifyLocationAnchor;
65
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(53, 0), new Position(53, 0))));
66
expect(anchor.title).toBe('src/file.ts#L54');
67
assertPartsEqual([anchor], [expected]);
68
});
69
70
test('Should fallback for invalid anchor syntax', async () => {
71
const service = createTestLinkifierService('src/file.ts');
72
const result = await linkify(service, '[src/file.ts](src/file.ts#Lines10-12)');
73
assertPartsEqual(result.parts, ['src/file.ts']);
74
});
75
76
test('Should handle backticks in link text', async () => {
77
const service = createTestLinkifierService('file.ts');
78
const result = await linkify(service, '[`file.ts`](file.ts)');
79
const anchor = result.parts[0] as LinkifyLocationAnchor;
80
const expected = new LinkifyLocationAnchor(workspaceFile('file.ts'));
81
assertPartsEqual([anchor], [expected]);
82
});
83
84
test('Should handle backticks in link text with line anchor', async () => {
85
const service = createTestLinkifierService('src/file.ts');
86
const result = await linkify(service, '[`src/file.ts`](src/file.ts#L42)');
87
const anchor = result.parts[0] as LinkifyLocationAnchor;
88
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(41, 0), new Position(41, 0))));
89
expect(anchor.title).toBe('src/file.ts#L42');
90
assertPartsEqual([anchor], [expected]);
91
});
92
93
test('Should handle L123-L456 anchor format with L prefix on end line', async () => {
94
const service = createTestLinkifierService('src/file.ts');
95
const result = await linkify(service, '[src/file.ts](src/file.ts#L10-L15)');
96
const anchor = result.parts[0] as LinkifyLocationAnchor;
97
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(9, 0), new Position(14, 0))));
98
expect(anchor.title).toBe('src/file.ts#L10-L15');
99
assertPartsEqual([anchor], [expected]);
100
});
101
102
test('Should handle descriptive text with L123-L456 anchor format', async () => {
103
const service = createTestLinkifierService('src/file.ts');
104
const result = await linkify(service, '[Some descriptive text](src/file.ts#L20-L25)');
105
const anchor = result.parts[0] as LinkifyLocationAnchor;
106
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(19, 0), new Position(24, 0))));
107
expect(anchor.title).toBe('src/file.ts#L20-L25');
108
assertPartsEqual([anchor], [expected]);
109
});
110
111
test('Should normalize non-standard L123-456 format to standard L123-L456', async () => {
112
const service = createTestLinkifierService('src/file.ts');
113
const result = await linkify(service, '[src/file.ts](src/file.ts#L20-25)');
114
const anchor = result.parts[0] as LinkifyLocationAnchor;
115
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(19, 0), new Position(24, 0))));
116
expect(anchor.title).toBe('src/file.ts#L20-L25');
117
assertPartsEqual([anchor], [expected]);
118
});
119
120
test('Should handle absolute paths with forward slashes on Windows', async () => {
121
const absolutePath = workspaceFile('src/file.ts').fsPath;
122
const service = createTestLinkifierService('src/file.ts');
123
// Simulate model-generated path with forward slashes (e.g., c:/Repos/...)
124
const pathWithForwardSlashes = absolutePath.replace(/\\/g, '/');
125
const result = await linkify(service, `[line 67](${pathWithForwardSlashes}#L67)`);
126
const anchor = result.parts[0] as LinkifyLocationAnchor;
127
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(66, 0), new Position(66, 0))));
128
expect(anchor.title).toBe('src/file.ts#L67');
129
assertPartsEqual([anchor], [expected]);
130
});
131
132
test('Should NOT linkify when display text looks like filename but does not match target filename', async () => {
133
// This tests the case where model hallucinates a link like [nonexistent.ts](existing.ts#L10)
134
// The display text "nonexistent.ts" looks like a filename but doesn't match "existing.ts"
135
const service = createTestLinkifierService('src/existing.ts');
136
const result = await linkify(service, '[nonexistent.ts](src/existing.ts#L10)');
137
// Should NOT create a link to the wrong file - just return the display text
138
assertPartsEqual(result.parts, ['nonexistent.ts']);
139
});
140
141
test('Should linkify when display text filename matches target filename with anchor', async () => {
142
// Display text is just the filename, target is full path - should work
143
const service = createTestLinkifierService('src/file.ts');
144
const result = await linkify(service, '[file.ts](src/file.ts#L10)');
145
const anchor = result.parts[0] as LinkifyLocationAnchor;
146
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(9, 0), new Position(9, 0))));
147
expect(anchor.title).toBe('src/file.ts#L10');
148
assertPartsEqual([anchor], [expected]);
149
});
150
151
test('Should linkify bare line number anchors without L prefix', async () => {
152
const service = createTestLinkifierService('src/file.ts');
153
const result = await linkify(service, '[src/file.ts](src/file.ts#10)');
154
const anchor = result.parts[0] as LinkifyLocationAnchor;
155
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(9, 0), new Position(9, 0))));
156
expect(anchor.title).toBe('src/file.ts#L10');
157
assertPartsEqual([anchor], [expected]);
158
});
159
160
test('Should linkify bare line range anchors without L prefix', async () => {
161
const service = createTestLinkifierService('src/file.ts');
162
const result = await linkify(service, '[src/file.ts](src/file.ts#10-20)');
163
const anchor = result.parts[0] as LinkifyLocationAnchor;
164
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(9, 0), new Position(19, 0))));
165
expect(anchor.title).toBe('src/file.ts#L10-L20');
166
assertPartsEqual([anchor], [expected]);
167
});
168
169
test('Should linkify descriptive text with bare line range anchor', async () => {
170
const service = createTestLinkifierService('src/file.ts');
171
const result = await linkify(service, '[existing pattern in chatListRenderer](src/file.ts#1287-1290)');
172
const anchor = result.parts[0] as LinkifyLocationAnchor;
173
const expected = new LinkifyLocationAnchor(new Location(workspaceFile('src/file.ts'), new Range(new Position(1286, 0), new Position(1289, 0))));
174
expect(anchor.title).toBe('src/file.ts#L1287-L1290');
175
assertPartsEqual([anchor], [expected]);
176
});
177
});
178
179
suite('Model File Path Linkifier Remote Workspace', () => {
180
function createRemoteService(root: URI, files: readonly URI[]): LinkifyService {
181
class MockFs implements IFileSystemService {
182
readonly _serviceBrand: undefined;
183
async stat(resource: URI) {
184
if (resource.toString() === root.toString()) {
185
return { ctime: 0, mtime: 0, size: 0, type: FileType.Directory };
186
}
187
const found = files.find(f => f.toString() === resource.toString());
188
if (!found) {
189
throw new Error('File not found: ' + resource.toString());
190
}
191
return { ctime: 0, mtime: 0, size: 0, type: found.path.endsWith('/') ? FileType.Directory : FileType.File };
192
}
193
readDirectory(): Promise<[string, FileType][]> { throw new Error('Not implemented'); }
194
createDirectory(): Promise<void> { throw new Error('Not implemented'); }
195
readFile(): Promise<Uint8Array> { throw new Error('Not implemented'); }
196
writeFile(): Promise<void> { throw new Error('Not implemented'); }
197
delete(): Promise<void> { throw new Error('Not implemented'); }
198
rename(): Promise<void> { throw new Error('Not implemented'); }
199
copy(): Promise<void> { throw new Error('Not implemented'); }
200
isWritableFileSystem(): boolean | undefined { return true; }
201
createFileSystemWatcher(): any { throw new Error('Not implemented'); }
202
}
203
const fs = new MockFs();
204
const workspaceService = new NullWorkspaceService([root]);
205
const service = new LinkifyService(fs, workspaceService, NullEnvService.Instance);
206
return service;
207
}
208
209
async function remoteLinkify(service: LinkifyService, text: string) {
210
const linkifier = service.createLinkifier({ requestId: undefined, references: [] }, []);
211
const initial = await linkifier.append(text, CancellationToken.None);
212
const flushed = await linkifier.flush(CancellationToken.None);
213
return flushed ? [...initial.parts, ...flushed.parts] : initial.parts;
214
}
215
216
const remoteRoot = URI.from({ scheme: 'test', authority: 'auth', path: '/home/user/project' });
217
const remoteFile = URI.from({ scheme: 'test', authority: 'auth', path: '/home/user/project/src/remote.ts' });
218
219
test('Should map absolute remote path preserving scheme', async () => {
220
const service = createRemoteService(remoteRoot, [remoteFile]);
221
const parts = await remoteLinkify(service, '[/home/user/project/src/remote.ts](/home/user/project/src/remote.ts)');
222
expect(parts.length).toBe(1);
223
const anchor = parts[0] as LinkifyLocationAnchor;
224
expect(anchor.value.toString()).toBe(remoteFile.toString());
225
expect(anchor.title).toBe('src/remote.ts');
226
});
227
228
test('Should parse line range anchor on remote absolute path', async () => {
229
const service = createRemoteService(remoteRoot, [remoteFile]);
230
const parts = await remoteLinkify(service, '[/home/user/project/src/remote.ts](/home/user/project/src/remote.ts#L3-5)');
231
expect(parts.length).toBe(1);
232
const anchor = parts[0] as LinkifyLocationAnchor;
233
// Anchor value is a Location when an anchor is present.
234
const location = anchor.value as Location;
235
expect(location.uri.toString()).toBe(remoteFile.toString());
236
const range = location.range;
237
expect(range.start.line).toBe(2);
238
expect(range.end.line).toBe(4);
239
expect(anchor.title).toBe('src/remote.ts#L3-L5');
240
});
241
});
242
243