Skip to content

Commit 42f5d38

Browse files
committed
feat(amazonq): add button to view and accept diff
1 parent 1205101 commit 42f5d38

File tree

25 files changed

+589
-50
lines changed

25 files changed

+589
-50
lines changed

package-lock.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"generateNonCodeFiles": "npm run generateNonCodeFiles -w packages/ --if-present"
4040
},
4141
"devDependencies": {
42-
"@aws-toolkits/telemetry": "^1.0.258",
42+
"@aws-toolkits/telemetry": "^1.0.259",
4343
"@playwright/browser-chromium": "^1.43.1",
4444
"@types/vscode": "^1.68.0",
4545
"@types/vscode-webview": "^1.57.1",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Add View Diff button to allow users to open native diff view in the editor."
4+
}

packages/amazonq/src/extension.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ import {
2020
import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode'
2121
import { CommonAuthWebview } from 'aws-core-vscode/login'
2222
import {
23+
AMAZON_Q_DIFF_SCHEME,
2324
DefaultAWSClientBuilder,
2425
DefaultAwsContext,
2526
ExtContext,
2627
RegionProvider,
2728
Settings,
29+
VirtualFileSystem,
30+
VirtualMemoryFile,
2831
activateLogger,
2932
activateTelemetry,
3033
env,
@@ -133,6 +136,14 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
133136
// Handle Amazon Q Extension un-installation.
134137
setupUninstallHandler(VSCODE_EXTENSION_ID.amazonq, context)
135138

139+
const vfs = new VirtualFileSystem()
140+
141+
// Register an empty file that's used when a to open a diff
142+
vfs.registerProvider(
143+
vscode.Uri.from({ scheme: AMAZON_Q_DIFF_SCHEME, path: 'empty' }),
144+
new VirtualMemoryFile(new Uint8Array())
145+
)
146+
136147
// Hide the Amazon Q tree in toolkit explorer
137148
await setContext('aws.toolkit.amazonq.dismissed', true)
138149

packages/core/src/amazonq/commons/controllers/contentController.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,26 @@
44
*/
55

66
import * as vscode from 'vscode'
7+
import fs from 'fs-extra'
8+
import path from 'path'
79
import { Position, TextEditor, window } from 'vscode'
810
import { getLogger } from '../../../shared/logger'
11+
import { AMAZON_Q_DIFF_SCHEME, amazonQTabSuffix } from '../../../shared/constants'
12+
import { disposeOnEditorClose } from '../../../shared/utilities/editorUtilities'
13+
import {
14+
applyChanges,
15+
createTempFileForDiff,
16+
getSelectionFromRange,
17+
} from '../../../shared/utilities/textDocumentUtilities'
18+
import { extractFileAndCodeSelectionFromMessage, getIndentedCode } from '../../../shared'
919

20+
class ContentProvider implements vscode.TextDocumentContentProvider {
21+
constructor(private tempFileUri: vscode.Uri) {}
22+
23+
provideTextDocumentContent(_uri: vscode.Uri) {
24+
return fs.readFileSync(this.tempFileUri.fsPath, 'utf-8')
25+
}
26+
}
1027
export class EditorContentController {
1128
/* *
1229
* Insert the Amazon Q chat written code to the cursor position
@@ -52,4 +69,66 @@ export class EditorContentController {
5269
)
5370
}
5471
}
72+
73+
/**
74+
* Accept code changes received by Amazon Q
75+
*
76+
* @param message the message from Amazon Q chat
77+
*/
78+
public async acceptDiff(message: any) {
79+
const { filePath, selection } = extractFileAndCodeSelectionFromMessage(message)
80+
81+
if (filePath && message?.code?.trim().length > 0 && selection) {
82+
const doc = await vscode.workspace.openTextDocument(filePath)
83+
const codeToUpdate = getIndentedCode(message, doc, selection)
84+
const range = getSelectionFromRange(doc, selection)
85+
await applyChanges(doc, range, codeToUpdate)
86+
87+
// If vscode.diff is open for the filePath then close it.
88+
vscode.window.tabGroups.all.flatMap(({ tabs }) =>
89+
tabs.map((tab) => {
90+
if (tab.label === `${path.basename(filePath)} ${amazonQTabSuffix}`) {
91+
void vscode.window.tabGroups.close(tab)
92+
}
93+
})
94+
)
95+
}
96+
}
97+
98+
/**
99+
* Displays a diff view comparing proposed changes with the existing file.
100+
*
101+
* How is diff generated:
102+
* 1. Creates a temporary file as a clone of the original file.
103+
* 2. Applies the proposed changes to the temporary file within the selected range.
104+
* 3. Opens a diff view comparing original file to the temporary file.
105+
*
106+
* This approach ensures that the diff view only shows the changes proposed by Amazon Q,
107+
* isolating them from any other modifications in the original file.
108+
*
109+
* @param message the message from Amazon Q chat
110+
*/
111+
public async viewDiff(message: any) {
112+
const { filePath, selection } = extractFileAndCodeSelectionFromMessage(message)
113+
114+
if (filePath && message?.code?.trim().length > 0 && selection) {
115+
const originalFileUri = vscode.Uri.file(filePath)
116+
const tempFileUri = await createTempFileForDiff(originalFileUri, message, selection)
117+
118+
// Register content provider and show diff
119+
const contentProvider = new ContentProvider(tempFileUri)
120+
const disposable = vscode.workspace.registerTextDocumentContentProvider(
121+
AMAZON_Q_DIFF_SCHEME,
122+
contentProvider
123+
)
124+
await vscode.commands.executeCommand(
125+
'vscode.diff',
126+
originalFileUri,
127+
tempFileUri,
128+
`${path.basename(filePath)} ${amazonQTabSuffix}`
129+
)
130+
131+
disposeOnEditorClose(tempFileUri, disposable)
132+
}
133+
}
55134
}

packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface ChatPayload {
1919
export interface ConnectorProps {
2020
sendMessageToExtension: (message: ExtensionMessage) => void
2121
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
22-
onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
22+
onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void
2323
onCWCContextCommandMessage: (message: ChatItem, command?: string) => string | undefined
2424
onError: (tabID: string, message: string, title: string) => void
2525
onWarning: (tabID: string, message: string, title: string) => void
@@ -282,7 +282,7 @@ export class Connector {
282282
content: messageData.relatedSuggestions,
283283
}
284284
}
285-
this.onChatAnswerReceived(messageData.tabID, answer)
285+
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
286286

287287
// Exit the function if we received an answer from AI
288288
if (
@@ -310,7 +310,7 @@ export class Connector {
310310
: undefined,
311311
traceId: messageData.traceId,
312312
}
313-
this.onChatAnswerReceived(messageData.tabID, answer)
313+
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
314314

315315
return
316316
}
@@ -321,13 +321,17 @@ export class Connector {
321321
return
322322
}
323323

324-
this.onChatAnswerReceived(messageData.tabID, {
325-
type: ChatItemType.ANSWER,
326-
messageId: messageData.triggerID,
327-
body: messageData.message,
328-
followUp: this.followUpGenerator.generateAuthFollowUps('cwc', messageData.authType),
329-
canBeVoted: false,
330-
})
324+
this.onChatAnswerReceived(
325+
messageData.tabID,
326+
{
327+
type: ChatItemType.ANSWER,
328+
messageId: messageData.triggerID,
329+
body: messageData.message,
330+
followUp: this.followUpGenerator.generateAuthFollowUps('cwc', messageData.authType),
331+
canBeVoted: false,
332+
},
333+
messageData
334+
)
331335

332336
return
333337
}

packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface ConnectorProps {
1919
sendMessageToExtension: (message: ExtensionMessage) => void
2020
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
2121
onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string) => void
22-
onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
22+
onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void
2323
sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined
2424
onError: (tabID: string, message: string, title: string) => void
2525
onWarning: (tabID: string, message: string, title: string) => void
@@ -153,7 +153,7 @@ export class Connector {
153153
}
154154
: undefined,
155155
}
156-
this.onChatAnswerReceived(messageData.tabID, answer)
156+
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
157157
}
158158
}
159159

@@ -176,7 +176,7 @@ export class Connector {
176176
},
177177
body: '',
178178
}
179-
this.onChatAnswerReceived(messageData.tabID, answer)
179+
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
180180
}
181181
}
182182

@@ -185,19 +185,27 @@ export class Connector {
185185
return
186186
}
187187

188-
this.onChatAnswerReceived(messageData.tabID, {
189-
type: ChatItemType.ANSWER,
190-
body: messageData.message,
191-
followUp: undefined,
192-
canBeVoted: false,
193-
})
194-
195-
this.onChatAnswerReceived(messageData.tabID, {
196-
type: ChatItemType.SYSTEM_PROMPT,
197-
body: undefined,
198-
followUp: this.followUpGenerator.generateAuthFollowUps('featuredev', messageData.authType),
199-
canBeVoted: false,
200-
})
188+
this.onChatAnswerReceived(
189+
messageData.tabID,
190+
{
191+
type: ChatItemType.ANSWER,
192+
body: messageData.message,
193+
followUp: undefined,
194+
canBeVoted: false,
195+
},
196+
messageData
197+
)
198+
199+
this.onChatAnswerReceived(
200+
messageData.tabID,
201+
{
202+
type: ChatItemType.SYSTEM_PROMPT,
203+
body: undefined,
204+
followUp: this.followUpGenerator.generateAuthFollowUps('featuredev', messageData.authType),
205+
canBeVoted: false,
206+
},
207+
messageData
208+
)
201209

202210
return
203211
}

packages/core/src/amazonq/webview/ui/apps/gumbyChatConnector.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface ConnectorProps {
1717
sendMessageToExtension: (message: ExtensionMessage) => void
1818
onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void
1919
onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string, messageId: string) => void
20-
onChatAnswerReceived?: (tabID: string, message: ChatItem) => void
20+
onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void
2121
onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void
2222
onQuickHandlerCommand: (tabID: string, command: string, eventId?: string) => void
2323
onError: (tabID: string, message: string, title: string) => void
@@ -90,7 +90,7 @@ export class Connector {
9090
canBeVoted: false,
9191
}
9292

93-
this.onChatAnswerReceived(tabID, answer)
93+
this.onChatAnswerReceived(tabID, answer, messageData)
9494

9595
return
9696
}
@@ -114,7 +114,7 @@ export class Connector {
114114
return
115115
}
116116

117-
this.onChatAnswerReceived(messageData.tabID, answer)
117+
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
118118
}
119119
}
120120

@@ -143,10 +143,14 @@ export class Connector {
143143
return
144144
}
145145

146-
this.onChatAnswerReceived(messageData.tabID, {
147-
type: ChatItemType.SYSTEM_PROMPT,
148-
body: messageData.message,
149-
})
146+
this.onChatAnswerReceived(
147+
messageData.tabID,
148+
{
149+
type: ChatItemType.SYSTEM_PROMPT,
150+
body: messageData.message,
151+
},
152+
messageData
153+
)
150154
}
151155

152156
onCustomFormAction(

packages/core/src/amazonq/webview/ui/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type MessageCommand =
1616
| 'open-diff'
1717
| 'code_was_copied_to_clipboard'
1818
| 'insert_code_at_cursor_position'
19+
| 'accept_diff'
20+
| 'view_diff'
1921
| 'stop-response'
2022
| 'trigger-tabID-received'
2123
| 'clear'

0 commit comments

Comments
 (0)