Skip to content

Commit ae5a75d

Browse files
committed
feat: add acceptDiff button to chat
1 parent 34b59f4 commit ae5a75d

File tree

17 files changed

+317
-77
lines changed

17 files changed

+317
-77
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 '../diff'
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/commons/diff.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { existsSync } from 'fs'
76
import * as vscode from 'vscode'
7+
import fs from 'fs'
88
import { featureDevScheme } from '../../amazonqFeatureDev/constants'
9+
import { indent } from '../../shared'
910

1011
export async function openDiff(leftPath: string, rightPath: string, tabId: string) {
1112
const { left, right } = getFileDiffUris(leftPath, rightPath, tabId)
@@ -18,7 +19,7 @@ export async function openDeletedDiff(filePath: string, name: string, tabId: str
1819
}
1920

2021
export function getOriginalFileUri(fullPath: string, tabId: string) {
21-
return existsSync(fullPath) ? vscode.Uri.file(fullPath) : createAmazonQUri('empty', tabId)
22+
return fs.existsSync(fullPath) ? vscode.Uri.file(fullPath) : createAmazonQUri('empty', tabId)
2223
}
2324

2425
export function getFileDiffUris(leftPath: string, rightPath: string, tabId: string) {
@@ -32,3 +33,34 @@ export function createAmazonQUri(path: string, tabId: string) {
3233
// TODO change the featureDevScheme to a more general amazon q scheme
3334
return vscode.Uri.from({ scheme: featureDevScheme, path, query: `tabID=${tabId}` })
3435
}
36+
37+
/**
38+
* Extracts the file path and selection context from the message.
39+
*
40+
* @param {any} message - The message object containing the file and selection context.
41+
* @returns {Object} - An object with `filePath` and `selection` properties.
42+
*/
43+
export function extractFileAndCodeSelectionFromMessage(message: any) {
44+
const filePath = message?.context?.activeFileContext?.filePath
45+
const selection = message?.context?.focusAreaContext?.selectionInsideExtendedCodeBlock as vscode.Selection
46+
return { filePath, selection }
47+
}
48+
49+
/**
50+
* Indents the given code based on the current document's indentation at the selection start.
51+
*
52+
* @param {any} message - The message object containing the code.
53+
* @param {vscode.TextDocument} doc - The VSCode document where the code is applied.
54+
* @param {vscode.Selection} selection - The selection range in the document.
55+
* @returns {string} - The processed code to be applied to the document.
56+
*/
57+
export function getIndentedCode(message: any, doc: vscode.TextDocument, selection: vscode.Selection) {
58+
const indentRange = new vscode.Range(new vscode.Position(selection.start.line, 0), selection.active)
59+
let indentation = doc.getText(indentRange)
60+
61+
if (indentation.trim().length !== 0) {
62+
indentation = ' '.repeat(indentation.length - indentation.trimStart().length)
63+
}
64+
65+
return indent(message.code, indentation.length)
66+
}

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

0 commit comments

Comments
 (0)