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