Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/test/node/editCodeIntent.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
7
import { describe, expect, it } from 'vitest';
8
import { PromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService';
9
import { TestWorkspaceService } from '../../../../platform/test/node/testWorkspaceService';
10
import { URI } from '../../../../util/vs/base/common/uri';
11
import { CodeBlock } from '../../../prompt/common/conversation';
12
import { MockChatResponseStream } from '../../../test/node/testHelpers';
13
import { getCodeBlocksFromResponse } from '../../node/editCodeIntent';
14
15
async function getCodeblocks(input: string[], outputStream: MockChatResponseStream, remoteName?: string) {
16
const textStream = async function* () {
17
for (const item of input) {
18
yield item;
19
}
20
}();
21
22
function createUriFromResponsePath(path: string): URI | undefined {
23
return new PromptPathRepresentationService(new TestWorkspaceService()).resolveFilePath(path);
24
}
25
26
const result: CodeBlock[] = [];
27
const codeBlocks = getCodeBlocksFromResponse(textStream, outputStream, createUriFromResponsePath, remoteName);
28
for await (const codeBlock of codeBlocks) {
29
result.push(codeBlock);
30
}
31
return result;
32
}
33
34
describe('getCodeBlocksFromResponse', () => {
35
it('should process code blocks with valid URIs', async () => {
36
const input = [
37
'### /valid/path\n',
38
'\n',
39
'lets do the following change\n',
40
'```typescript\n',
41
'// filepath: /valid/path\n',
42
'const x = 1;\n',
43
'```\n',
44
];
45
const outputStream = new MockChatResponseStream();
46
const result = await getCodeblocks(input, outputStream);
47
48
expect(result.length).toBe(1);
49
expect(result[0].resource?.toString()).toBe('file:///valid/path');
50
expect(result[0].language).toBe('typescript');
51
expect(result[0].code).toBe([
52
'const x = 1;\n'
53
].join(''));
54
expect(outputStream.output.join('')).toBe([
55
'### [path](file:///valid/path)\n',
56
'\n',
57
'lets do the following change\n',
58
'```typescript\n',
59
'const x = 1;\n',
60
'```\n',
61
].join(''));
62
expect(outputStream.uris).toEqual([
63
'file:///valid/path'
64
]);
65
});
66
67
it('should process code blocks without URIs', async () => {
68
const input = [
69
'### /valid/path\n',
70
'\n',
71
'run a command\n',
72
'```sh\n',
73
'npm install minify\n',
74
'```\n',
75
];
76
const outputStream = new MockChatResponseStream();
77
const result = await getCodeblocks(input, outputStream);
78
79
expect(result.length).toBe(1);
80
expect(result[0].resource).toBe(undefined);
81
expect(result[0].language).toBe('sh');
82
expect(result[0].code).toBe([
83
'npm install minify\n'
84
].join(''));
85
expect(outputStream.output.join('')).toBe([
86
'### [path](file:///valid/path)\n',
87
'\n',
88
'run a command\n',
89
'```sh\n',
90
'npm install minify\n',
91
'```\n',
92
].join(''));
93
expect(outputStream.uris).toEqual([
94
]);
95
});
96
97
it('should not linkify non-filepath headers (#11079)', async () => {
98
const input = [
99
'### change summary\n',
100
'\n',
101
'1. Create a new file.\n',
102
'\n',
103
'### untitled:Untitled-1\n',
104
'\n',
105
'```python\n',
106
'# filepath: untitled:Untitled-1\n',
107
`print('Hello, World!')\n`,
108
'```\n',
109
];
110
const outputStream = new MockChatResponseStream();
111
const result = await getCodeblocks(input, outputStream);
112
113
expect(result.length).toBe(1);
114
expect(result[0].resource?.toString()).toBe('untitled:Untitled-1');
115
expect(result[0].code).toBe([
116
`print('Hello, World!')\n`,
117
].join(''));
118
expect(outputStream.output.join('')).toBe([
119
'### change summary\n',
120
'\n',
121
'1. Create a new file.\n',
122
'\n',
123
'### [Untitled-1](untitled:Untitled-1)\n',
124
'\n',
125
'```python\n',
126
`print('Hello, World!')\n`,
127
'```\n',
128
].join(''));
129
expect(outputStream.uris).toEqual([
130
'untitled:Untitled-1'
131
]);
132
});
133
134
it('should not linkify other headers', async () => {
135
const input = [
136
'# /a/b\n',
137
'\n',
138
'### /a/b\n',
139
'\n',
140
'#### /a/b\n',
141
'\n',
142
'```python\n',
143
'# filepath: /a/b\n',
144
`print('Hello, World!')\n`,
145
'```\n',
146
];
147
const outputStream = new MockChatResponseStream();
148
const result = await getCodeblocks(input, outputStream);
149
150
expect(result.length).toBe(1);
151
expect(result[0].resource?.toString()).toBe('file:///a/b');
152
expect(result[0].code).toBe([
153
`print('Hello, World!')\n`,
154
].join(''));
155
expect(outputStream.output.join('')).toBe([
156
'# /a/b\n',
157
'\n',
158
'### [b](file:///a/b)\n',
159
'\n',
160
'#### /a/b\n',
161
'\n',
162
'```python\n',
163
`print('Hello, World!')\n`,
164
'```\n',
165
].join(''));
166
expect(outputStream.uris).toEqual([
167
'file:///a/b'
168
]);
169
});
170
171
it('using 4 backticks (#11112)', async () => {
172
const input = [
173
'Sure, let\'s create a new file named `dog.txt` in the specified folder.\n',
174
'\n',
175
'### /Users/jospicer/dev/BunnyBunch/dog.txt\n',
176
'\n',
177
'Create a new file with some placeholder content.\n',
178
'\n',
179
'````plaintext\n',
180
'// filepath: /Users/jospicer/dev/BunnyBunch/dog.txt\n',
181
'This is a placeholder text for the dog.txt file.\n',
182
'````\n',
183
];
184
const outputStream = new MockChatResponseStream();
185
const result = await getCodeblocks(input, outputStream);
186
187
expect(result.length).toBe(1);
188
expect(result[0].resource?.toString()).toBe('file:///Users/jospicer/dev/BunnyBunch/dog.txt');
189
expect(result[0].code).toBe([
190
'This is a placeholder text for the dog.txt file.\n'
191
].join(''));
192
expect(outputStream.output.join('')).toBe([
193
'Sure, let\'s create a new file named `dog.txt` in the specified folder.\n',
194
'\n',
195
'### [dog.txt](file:///Users/jospicer/dev/BunnyBunch/dog.txt)\n',
196
'\n',
197
'Create a new file with some placeholder content.\n',
198
'\n',
199
'````plaintext\n',
200
'This is a placeholder text for the dog.txt file.\n',
201
'````\n',
202
].join(''));
203
expect(outputStream.uris).toEqual([
204
'file:///Users/jospicer/dev/BunnyBunch/dog.txt'
205
]);
206
});
207
208
it('multi code block', async () => {
209
const input = [
210
'Sure, let\'s create a new file named `dog.txt` in the specified folder.\n',
211
'\n',
212
'### /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift\n',
213
'\n',
214
'Add unit selection support and conversion methods to WaterStore.\n',
215
'\n',
216
'```swift\n',
217
'// filepath: /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift\n',
218
'import Foundation\n',
219
'import SwiftUI\n',
220
'```\n',
221
'### /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift\n',
222
'\n',
223
'Create a new settings view for unit selection.\n',
224
'\n',
225
'```swift\n',
226
'// filepath: /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift\n',
227
'import SwiftUI\n',
228
'```\n',
229
'\n'
230
];
231
const outputStream = new MockChatResponseStream();
232
const result = await getCodeblocks(input, outputStream);
233
234
expect(result.length).toBe(2);
235
expect(result[0].resource?.toString()).toBe('file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift');
236
expect(result[0].code).toBe([
237
'import Foundation\n',
238
'import SwiftUI\n',
239
].join(''));
240
expect(result[0].language).toBe('swift');
241
expect(result[1].resource?.toString()).toBe('file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift');
242
expect(result[1].code).toBe([
243
'import SwiftUI\n',
244
].join(''));
245
expect(result[1].language).toBe('swift');
246
247
expect(outputStream.output.join('')).toBe([
248
'Sure, let\'s create a new file named `dog.txt` in the specified folder.\n',
249
'\n',
250
'### [WaterStore.swift](file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift)\n',
251
'\n',
252
'Add unit selection support and conversion methods to WaterStore.\n',
253
'\n',
254
'```swift\n',
255
'import Foundation\n',
256
'import SwiftUI\n',
257
'```\n',
258
259
'### [SettingsView.swift](file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift)\n',
260
'\n',
261
'Create a new settings view for unit selection.\n',
262
'\n',
263
'```swift\n',
264
'import SwiftUI\n',
265
'```\n',
266
'\n'
267
].join(''));
268
expect(outputStream.uris).toEqual([
269
'file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift',
270
'file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift'
271
]);
272
});
273
274
275
it('multi code block (#11034)', async () => {
276
const input = [
277
'Sure, let\'s create a new file named `dog.txt` in the specified folder.\n',
278
'\n',
279
'### /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift\n',
280
'\n',
281
'Add unit selection support and conversion methods to WaterStore.\n',
282
'\n',
283
'```swift\n',
284
'// filepath: /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift\n',
285
'import Foundation\n',
286
'import SwiftUI\n',
287
'```\n',
288
'### /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift\n',
289
'\n',
290
'Create a new settings view for unit selection.\n',
291
'\n',
292
'```swift\n',
293
'// filepath: /home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift\n',
294
'import SwiftUI\n',
295
'```\n',
296
'\n'
297
];
298
const outputStream = new MockChatResponseStream();
299
const result = await getCodeblocks(input, outputStream);
300
301
expect(result.length).toBe(2);
302
expect(result[0].resource?.toString()).toBe('file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift');
303
expect(result[0].code).toBe([
304
'import Foundation\n',
305
'import SwiftUI\n',
306
].join(''));
307
expect(result[0].language).toBe('swift');
308
expect(result[1].resource?.toString()).toBe('file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift');
309
expect(result[1].code).toBe([
310
'import SwiftUI\n',
311
].join(''));
312
expect(result[1].language).toBe('swift');
313
expect(outputStream.output.join('')).toBe([
314
'Sure, let\'s create a new file named `dog.txt` in the specified folder.\n',
315
'\n',
316
'### [WaterStore.swift](file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift)\n',
317
'\n',
318
'Add unit selection support and conversion methods to WaterStore.\n',
319
'\n',
320
'```swift\n',
321
'import Foundation\n',
322
'import SwiftUI\n',
323
'```\n',
324
325
'### [SettingsView.swift](file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift)\n',
326
'\n',
327
'Create a new settings view for unit selection.\n',
328
'\n',
329
'```swift\n',
330
'import SwiftUI\n',
331
'```\n',
332
'\n'
333
].join(''));
334
expect(outputStream.uris).toEqual([
335
'file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/WaterStore.swift',
336
'file:///home/martin/workspaces/testing/water-tracker-ios/WaterTracker/SettingsView.swift'
337
]);
338
});
339
340
it('should process code blocks from vscode-copilot-release#3983', async () => {
341
const input = [
342
`I'll help you split the Counter component and create a new CounterButton component. Here's the solution:\n`,
343
'\n',
344
'1. Create a new CounterButton component\n',
345
'2. Move the button markup and click handler to the new component\n',
346
'3. Modify the Counter component to use CounterButton\n',
347
'\n',
348
'### /workspaces/vscode-remote-try-dotnet/Components/Pages/Counter.razor\n',
349
'\n',
350
'Simplify the Counter component by removing the button markup and using the new CounterButton component.\n',
351
'\n',
352
'```razor\n',
353
'// filepath: /workspaces/vscode-remote-try-dotnet/Components/Pages/Counter.razor\n',
354
'@page "/counter"\n',
355
'@rendermode InteractiveServer\n',
356
'\n',
357
'<PageTitle>Counter</PageTitle>\n',
358
'\n',
359
'<h1>Counter</h1>\n',
360
'\n',
361
'<p role="status">Current count: @currentCount</p>\n',
362
'\n',
363
'<CounterButton OnClick="IncrementCount" />\n',
364
'\n',
365
'@code {\n',
366
' private int currentCount = 0;\n',
367
'\n',
368
' private void IncrementCount()\n',
369
' {\n',
370
' currentCount++;\n',
371
' }\n',
372
'}\n',
373
'```\n',
374
'\n',
375
'### /workspaces/vscode-remote-try-dotnet/Components/CounterButton.razor\n',
376
'\n',
377
'Create a new component for the button with an event callback.\n',
378
'\n',
379
'```razor\n',
380
'// filepath: /workspaces/vscode-remote-try-dotnet/Components/CounterButton.razor\n',
381
'@rendermode InteractiveServer\n',
382
'\n',
383
'<button class="btn btn-primary" @onclick="OnClick">Click me</button>\n',
384
'\n',
385
'@code {\n',
386
' [Parameter]\n',
387
' public EventCallback OnClick { get; set; }\n',
388
'}\n',
389
'```\n',
390
];
391
const outputStream = new MockChatResponseStream();
392
const result = await getCodeblocks(input, outputStream);
393
394
expect(result.length).toBe(2);
395
expect(result[0].resource?.toString()).toBe('file:///workspaces/vscode-remote-try-dotnet/Components/Pages/Counter.razor');
396
expect(result[0].language).toBe('razor');
397
expect(result[0].code).toBe([
398
'@page "/counter"\n',
399
'@rendermode InteractiveServer\n',
400
'\n',
401
'<PageTitle>Counter</PageTitle>\n',
402
'\n',
403
'<h1>Counter</h1>\n',
404
'\n',
405
'<p role="status">Current count: @currentCount</p>\n',
406
'\n',
407
'<CounterButton OnClick="IncrementCount" />\n',
408
'\n',
409
'@code {\n',
410
' private int currentCount = 0;\n',
411
'\n',
412
' private void IncrementCount()\n',
413
' {\n',
414
' currentCount++;\n',
415
' }\n',
416
'}\n'
417
].join(''));
418
expect(result[1].resource?.toString()).toBe('file:///workspaces/vscode-remote-try-dotnet/Components/CounterButton.razor');
419
expect(result[1].language).toBe('razor');
420
expect(result[1].code).toBe([
421
'@rendermode InteractiveServer\n',
422
'\n',
423
'<button class="btn btn-primary" @onclick="OnClick">Click me</button>\n',
424
'\n',
425
'@code {\n',
426
' [Parameter]\n',
427
' public EventCallback OnClick { get; set; }\n',
428
'}\n'
429
].join(''));
430
expect(outputStream.output.join('')).toBe([
431
`I'll help you split the Counter component and create a new CounterButton component. Here's the solution:\n`,
432
'\n',
433
'1. Create a new CounterButton component\n',
434
'2. Move the button markup and click handler to the new component\n',
435
'3. Modify the Counter component to use CounterButton\n',
436
'\n',
437
'### [Counter.razor](file:///workspaces/vscode-remote-try-dotnet/Components/Pages/Counter.razor)\n',
438
'\n',
439
'Simplify the Counter component by removing the button markup and using the new CounterButton component.\n',
440
'\n',
441
'```razor\n',
442
'@page "/counter"\n',
443
'@rendermode InteractiveServer\n',
444
'\n',
445
'<PageTitle>Counter</PageTitle>\n',
446
'\n',
447
'<h1>Counter</h1>\n',
448
'\n',
449
'<p role="status">Current count: @currentCount</p>\n',
450
'\n',
451
'<CounterButton OnClick="IncrementCount" />\n',
452
'\n',
453
'@code {\n',
454
' private int currentCount = 0;\n',
455
'\n',
456
' private void IncrementCount()\n',
457
' {\n',
458
' currentCount++;\n',
459
' }\n',
460
'}\n',
461
'```\n',
462
'\n',
463
'### [CounterButton.razor](file:///workspaces/vscode-remote-try-dotnet/Components/CounterButton.razor)\n',
464
'\n',
465
'Create a new component for the button with an event callback.\n',
466
'\n',
467
'```razor\n',
468
'@rendermode InteractiveServer\n',
469
'\n',
470
'<button class="btn btn-primary" @onclick="OnClick">Click me</button>\n',
471
'\n',
472
'@code {\n',
473
' [Parameter]\n',
474
' public EventCallback OnClick { get; set; }\n',
475
'}\n',
476
'```\n'
477
].join(''));
478
expect(outputStream.uris).toEqual([
479
'file:///workspaces/vscode-remote-try-dotnet/Components/Pages/Counter.razor',
480
'file:///workspaces/vscode-remote-try-dotnet/Components/CounterButton.razor'
481
]);
482
});
483
});
484
485