Skip to content

Commit 051dd09

Browse files
committed
feat: add acceptDiff button to chat
1 parent 8633dfc commit 051dd09

File tree

16 files changed

+314
-75
lines changed

16 files changed

+314
-75
lines changed

packages/amazonq/src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode'
2121
import { CommonAuthWebview } from 'aws-core-vscode/login'
2222
import {
23-
amazonQDiffScheme,
23+
AMAZON_Q_DIFF_SCHEME,
2424
DefaultAWSClientBuilder,
2525
DefaultAwsContext,
2626
ExtContext,
@@ -140,7 +140,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
140140

141141
// Register an empty file that's used when a to open a diff
142142
vfs.registerProvider(
143-
vscode.Uri.from({ scheme: amazonQDiffScheme, path: 'empty' }),
143+
vscode.Uri.from({ scheme: AMAZON_Q_DIFF_SCHEME, path: 'empty' }),
144144
new VirtualMemoryFile(new Uint8Array())
145145
)
146146

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

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,24 @@
66
import * as vscode from 'vscode'
77
import fs from 'fs-extra'
88
import path from 'path'
9-
import { commands, Position, TextEditor, Uri, window, workspace } from 'vscode'
9+
import { Position, TextEditor, window } from 'vscode'
1010
import { getLogger } from '../../../shared/logger'
11-
import { amazonQDiffScheme, getNonexistentFilename, tempDirPath } from '../../../shared'
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'
1219

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+
}
1327
export class EditorContentController {
1428
/* *
1529
* Insert the Amazon Q chat written code to the cursor position
@@ -56,6 +70,31 @@ export class EditorContentController {
5670
}
5771
}
5872

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+
5998
/**
6099
* Displays a diff view comparing proposed changes with the existing file.
61100
*
@@ -70,63 +109,26 @@ export class EditorContentController {
70109
* @param message the message from Amazon Q chat
71110
*/
72111
public async viewDiff(message: any) {
73-
const { filePath } = message?.context?.activeFileContext || {}
74-
const selection = message?.context?.focusAreaContext?.selectionInsideExtendedCodeBlock as vscode.Selection
112+
const { filePath, selection } = extractFileAndCodeSelectionFromMessage(message)
75113

76114
if (filePath && message?.code?.trim().length > 0 && selection) {
77-
const id = Date.now()
78-
79-
const originalFileUri = Uri.file(filePath)
80-
const fileNameWithExtension = path.basename(originalFileUri.path)
81-
82-
const fileName = path.parse(fileNameWithExtension).name
83-
const fileExtension = path.extname(fileNameWithExtension)
84-
85-
// Create a new file in the temp directory
86-
const tempFile = await getNonexistentFilename(tempDirPath, `${fileName}_proposed-${id}`, fileExtension, 99)
87-
const tempFilePath = path.join(tempDirPath, tempFile)
88-
89-
// Create a new URI for the temp file
90-
const diffScheme = amazonQDiffScheme
91-
const tempFileUri = Uri.parse(`${diffScheme}:${tempFilePath}`)
92-
93-
// Write the initial code to the temp file
94-
fs.writeFileSync(tempFilePath, fs.readFileSync(originalFileUri.fsPath, 'utf-8'))
95-
const doc = await workspace.openTextDocument(tempFileUri.path)
96-
97-
// Apply the edit to the temp file
98-
const edit = new vscode.WorkspaceEdit()
99-
edit.replace(doc.uri, selection, message.code)
115+
const originalFileUri = vscode.Uri.file(filePath)
116+
const tempFileUri = await createTempFileForDiff(originalFileUri, message, selection)
100117

101-
const successfulEdit = await workspace.applyEdit(edit)
102-
if (successfulEdit) {
103-
await doc.save()
104-
fs.writeFileSync(tempFilePath, doc.getText())
105-
}
106-
class ContentProvider {
107-
provideTextDocumentContent(_uri: Uri) {
108-
return fs.readFileSync(tempFilePath, 'utf-8')
109-
}
110-
}
111-
const contentProvider = new ContentProvider()
112-
const disposable = workspace.registerTextDocumentContentProvider(diffScheme, contentProvider)
113-
114-
await commands.executeCommand(
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(
115125
'vscode.diff',
116126
originalFileUri,
117127
tempFileUri,
118-
`${fileName}${fileExtension} (Generated by Amazon Q)`
128+
`${path.basename(filePath)} ${amazonQTabSuffix}`
119129
)
120130

121-
vscode.window.onDidChangeVisibleTextEditors(() => {
122-
if (
123-
!vscode.window.visibleTextEditors.some(
124-
(editor) => editor.document.uri.toString() === tempFileUri.toString()
125-
)
126-
) {
127-
disposable.dispose()
128-
}
129-
})
131+
disposeOnEditorClose(tempFileUri, disposable)
130132
}
131133
}
132134
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type MessageCommand =
1616
| 'open-diff'
1717
| 'code_was_copied_to_clipboard'
1818
| 'insert_code_at_cursor_position'
19+
| 'accept_diff'
1920
| 'view_diff'
2021
| 'stop-response'
2122
| 'trigger-tabID-received'

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,35 @@ export class Connector {
250250
}
251251
}
252252

253+
onAcceptDiff = (
254+
tabId: string,
255+
messageId: string,
256+
actionId: string,
257+
data?: string,
258+
code?: string,
259+
type?: CodeSelectionType,
260+
referenceTrackerInformation?: ReferenceTrackerInformation[],
261+
eventId?: string,
262+
codeBlockIndex?: number,
263+
totalCodeBlocks?: number
264+
) => {
265+
const tabType = this.tabsStorage.getTab(tabId)?.type
266+
this.sendMessageToExtension({
267+
tabType,
268+
tabID: tabId,
269+
command: 'accept_diff',
270+
messageId,
271+
actionId,
272+
data,
273+
code,
274+
type,
275+
referenceTrackerInformation,
276+
eventId,
277+
codeBlockIndex,
278+
totalCodeBlocks,
279+
})
280+
}
281+
253282
onViewDiff = (
254283
tabId: string,
255284
messageId: string,

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

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
8080
// eslint-disable-next-line prefer-const
8181
let messageController: MessageController
8282

83-
function shouldDisplayViewDiff(messageData: any) {
83+
function shouldDisplayDiff(messageData: any) {
8484
const tab = tabsStorage.getTab(messageData?.tabID || '')
8585
const allowedCommands = [
8686
'aws.amazonq.refactorCode',
@@ -242,12 +242,19 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
242242
...item,
243243
messageId: item.messageId,
244244
codeBlockActions: {
245-
...(shouldDisplayViewDiff(messageData) // update the condition for fix, refactor etc.
245+
...(shouldDisplayDiff(messageData)
246246
? {
247+
'insert-to-cursor': undefined,
248+
'accept-diff': {
249+
id: 'accept-diff',
250+
label: 'Apply Diff',
251+
icon: MynahIcons.OK_CIRCLED,
252+
data: messageData,
253+
},
247254
'view-diff': {
248255
id: 'view-diff',
249256
label: 'View Diff',
250-
icon: MynahIcons.OK_CIRCLED,
257+
icon: MynahIcons.EYE,
251258
data: messageData,
252259
},
253260
}
@@ -483,19 +490,37 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
483490
codeBlockIndex?: number,
484491
totalCodeBlocks?: number
485492
) => {
486-
if (actionId === 'view-diff') {
487-
connector.onViewDiff(
488-
tabId,
489-
messageId,
490-
actionId,
491-
data,
492-
code,
493-
type,
494-
referenceTrackerInformation,
495-
eventId,
496-
codeBlockIndex,
497-
totalCodeBlocks
498-
)
493+
switch (actionId) {
494+
case 'accept-diff':
495+
connector.onAcceptDiff(
496+
tabId,
497+
messageId,
498+
actionId,
499+
data,
500+
code,
501+
type,
502+
referenceTrackerInformation,
503+
eventId,
504+
codeBlockIndex,
505+
totalCodeBlocks
506+
)
507+
break
508+
case 'view-diff':
509+
connector.onViewDiff(
510+
tabId,
511+
messageId,
512+
actionId,
513+
data,
514+
code,
515+
type,
516+
referenceTrackerInformation,
517+
eventId,
518+
codeBlockIndex,
519+
totalCodeBlocks
520+
)
521+
break
522+
default:
523+
break
499524
}
500525
},
501526
onCodeInsertToCursorPosition: connector.onCodeInsertToCursorPosition,

packages/core/src/codewhispererChat/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
TabCreatedMessage,
2626
TriggerTabIDReceived,
2727
UIFocusMessage,
28+
AcceptDiff,
2829
} from './controllers/chat/model'
2930
import { EditorContextCommand, registerCommands } from './commands/registerCommands'
3031

@@ -35,6 +36,7 @@ export function init(appContext: AmazonQAppInitContext) {
3536
processTabClosedMessage: new EventEmitter<TabClosedMessage>(),
3637
processTabChangedMessage: new EventEmitter<TabChangedMessage>(),
3738
processInsertCodeAtCursorPosition: new EventEmitter<InsertCodeAtCursorPosition>(),
39+
processAcceptDiff: new EventEmitter<AcceptDiff>(),
3840
processViewDiff: new EventEmitter<ViewDiff>(),
3941
processCopyCodeToClipboard: new EventEmitter<CopyCodeToClipboard>(),
4042
processContextMenuCommand: new EventEmitter<EditorContextCommand>(),
@@ -64,6 +66,7 @@ export function init(appContext: AmazonQAppInitContext) {
6466
processInsertCodeAtCursorPosition: new MessageListener<InsertCodeAtCursorPosition>(
6567
cwChatControllerEventEmitters.processInsertCodeAtCursorPosition
6668
),
69+
processAcceptDiff: new MessageListener<AcceptDiff>(cwChatControllerEventEmitters.processAcceptDiff),
6770
processViewDiff: new MessageListener<ViewDiff>(cwChatControllerEventEmitters.processViewDiff),
6871
processCopyCodeToClipboard: new MessageListener<CopyCodeToClipboard>(
6972
cwChatControllerEventEmitters.processCopyCodeToClipboard
@@ -111,6 +114,7 @@ export function init(appContext: AmazonQAppInitContext) {
111114
processInsertCodeAtCursorPosition: new MessagePublisher<InsertCodeAtCursorPosition>(
112115
cwChatControllerEventEmitters.processInsertCodeAtCursorPosition
113116
),
117+
processAcceptDiff: new MessagePublisher<AcceptDiff>(cwChatControllerEventEmitters.processAcceptDiff),
114118
processViewDiff: new MessagePublisher<ViewDiff>(cwChatControllerEventEmitters.processViewDiff),
115119
processCopyCodeToClipboard: new MessagePublisher<CopyCodeToClipboard>(
116120
cwChatControllerEventEmitters.processCopyCodeToClipboard

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
ChatPromptCommandType,
2626
FooterInfoLinkClick,
2727
ViewDiff,
28+
AcceptDiff,
2829
} from './model'
2930
import { AppToWebViewMessageDispatcher } from '../../view/connector/connector'
3031
import { MessagePublisher } from '../../../amazonq/messages/messagePublisher'
@@ -60,6 +61,7 @@ export interface ChatControllerMessagePublishers {
6061
readonly processTabClosedMessage: MessagePublisher<TabClosedMessage>
6162
readonly processTabChangedMessage: MessagePublisher<TabChangedMessage>
6263
readonly processInsertCodeAtCursorPosition: MessagePublisher<InsertCodeAtCursorPosition>
64+
readonly processAcceptDiff: MessagePublisher<AcceptDiff>
6365
readonly processViewDiff: MessagePublisher<ViewDiff>
6466
readonly processCopyCodeToClipboard: MessagePublisher<CopyCodeToClipboard>
6567
readonly processContextMenuCommand: MessagePublisher<EditorContextCommand>
@@ -79,6 +81,7 @@ export interface ChatControllerMessageListeners {
7981
readonly processTabClosedMessage: MessageListener<TabClosedMessage>
8082
readonly processTabChangedMessage: MessageListener<TabChangedMessage>
8183
readonly processInsertCodeAtCursorPosition: MessageListener<InsertCodeAtCursorPosition>
84+
readonly processAcceptDiff: MessageListener<AcceptDiff>
8285
readonly processViewDiff: MessageListener<ViewDiff>
8386
readonly processCopyCodeToClipboard: MessageListener<CopyCodeToClipboard>
8487
readonly processContextMenuCommand: MessageListener<EditorContextCommand>
@@ -161,6 +164,10 @@ export class ChatController {
161164
return this.processInsertCodeAtCursorPosition(data)
162165
})
163166

167+
this.chatControllerMessageListeners.processAcceptDiff.onMessage((data) => {
168+
return this.processAcceptDiff(data)
169+
})
170+
164171
this.chatControllerMessageListeners.processViewDiff.onMessage((data) => {
165172
return this.processViewDiff(data)
166173
})
@@ -284,6 +291,12 @@ export class ChatController {
284291
this.telemetryHelper.recordInteractWithMessage(message)
285292
}
286293

294+
private async processAcceptDiff(message: AcceptDiff) {
295+
const context = this.triggerEventsStorage.getTriggerEvent((message.data as any)?.triggerID) || ''
296+
void this.editorContentController.acceptDiff({ ...message, ...context })
297+
this.telemetryHelper.recordInteractWithMessage(message)
298+
}
299+
287300
private async processViewDiff(message: ViewDiff) {
288301
const context = this.triggerEventsStorage.getTriggerEvent((message.data as any)?.triggerID) || ''
289302
void this.editorContentController.viewDiff({ ...message, ...context })

packages/core/src/codewhispererChat/controllers/chat/model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ export interface CopyCodeToClipboard {
6161
totalCodeBlocks: number
6262
}
6363

64+
export interface AcceptDiff {
65+
command: string | undefined
66+
tabID: string // rename tabId
67+
messageId: string
68+
actionId: string
69+
data: string
70+
code: string
71+
referenceTrackerInformation?: CodeReference[]
72+
eventId: string
73+
codeBlockIndex?: number
74+
totalCodeBlocks?: number
75+
}
6476
export interface ViewDiff {
6577
command: string | undefined
6678
tabID: string // rename tabId

0 commit comments

Comments
 (0)