Skip to content

Commit 9fdce73

Browse files
committed
feat: add acceptDiff button to chat
1 parent a5b7654 commit 9fdce73

File tree

11 files changed

+267
-56
lines changed

11 files changed

+267
-56
lines changed

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

Lines changed: 42 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 {
12+
applyChanges,
13+
createTempFileForDiff,
14+
disposeOnEditorClose,
15+
extractFileAndCodeSelectionFromMessage,
16+
getTrimmedCodeFromMessage,
17+
getSelectionFromRange,
18+
} 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,23 @@ 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 = getTrimmedCodeFromMessage(message, doc, selection)
84+
const range = getSelectionFromRange(doc, selection)
85+
86+
await applyChanges(doc, range, codeToUpdate)
87+
}
88+
}
89+
5990
/**
6091
* Displays a diff view comparing proposed changes with the existing file.
6192
*
@@ -70,63 +101,23 @@ export class EditorContentController {
70101
* @param message the message from Amazon Q chat
71102
*/
72103
public async viewDiff(message: any) {
73-
const { filePath } = message?.context?.activeFileContext || {}
74-
const selection = message?.context?.focusAreaContext?.selectionInsideExtendedCodeBlock as vscode.Selection
104+
const { filePath, selection } = extractFileAndCodeSelectionFromMessage(message)
75105

76106
if (filePath && message?.code?.trim().length > 0 && selection) {
77-
const id = Date.now()
107+
const originalFileUri = vscode.Uri.file(filePath)
108+
const tempFileUri = await createTempFileForDiff(originalFileUri, message, selection)
78109

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)
100-
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(
110+
// Register content provider and show diff
111+
const contentProvider = new ContentProvider(tempFileUri)
112+
const disposable = vscode.workspace.registerTextDocumentContentProvider('amazonQDiff', contentProvider)
113+
await vscode.commands.executeCommand(
115114
'vscode.diff',
116115
originalFileUri,
117116
tempFileUri,
118-
`${fileName}${fileExtension} (Generated by Amazon Q)`
117+
`${path.basename(filePath)} (Generated by Amazon Q)`
119118
)
120119

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-
})
120+
disposeOnEditorClose(tempFileUri, disposable)
130121
}
131122
}
132123
}

packages/core/src/amazonq/commons/diff.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
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'
8+
import path from 'path'
89
import { featureDevScheme } from '../../amazonqFeatureDev/constants'
10+
import { tempDirPath } from '../../shared'
911

1012
export async function openDiff(leftPath: string, rightPath: string, tabId: string) {
1113
const { left, right } = getFileDiffUris(leftPath, rightPath, tabId)
@@ -18,7 +20,7 @@ export async function openDeletedDiff(filePath: string, name: string, tabId: str
1820
}
1921

2022
export function getOriginalFileUri(fullPath: string, tabId: string) {
21-
return existsSync(fullPath) ? vscode.Uri.file(fullPath) : createAmazonQUri('empty', tabId)
23+
return fs.existsSync(fullPath) ? vscode.Uri.file(fullPath) : createAmazonQUri('empty', tabId)
2224
}
2325

2426
export function getFileDiffUris(leftPath: string, rightPath: string, tabId: string) {
@@ -32,3 +34,113 @@ export function createAmazonQUri(path: string, tabId: string) {
3234
// TODO change the featureDevScheme to a more general amazon q scheme
3335
return vscode.Uri.from({ scheme: featureDevScheme, path, query: `tabID=${tabId}` })
3436
}
37+
38+
/**
39+
* Extracts the file path and selection context from the message.
40+
*
41+
* @param {any} message - The message object containing the file and selection context.
42+
* @returns {Object} - An object with `filePath` and `selection` properties.
43+
*/
44+
export function extractFileAndCodeSelectionFromMessage(message: any) {
45+
const filePath = message?.context?.activeFileContext?.filePath
46+
const selection = message?.context?.focusAreaContext?.selectionInsideExtendedCodeBlock as vscode.Selection
47+
return { filePath, selection }
48+
}
49+
50+
/**
51+
* Processes the code to ensure proper newline handling based on the original document.
52+
* If the original line does not end with a newline, trims the ending newline from the message code.
53+
*
54+
* @param {any} message - The message object containing the code.
55+
* @param {vscode.TextDocument} doc - The VSCode document where the code is applied.
56+
* @param {vscode.Selection} selection - The selection range in the document.
57+
* @returns {string} - The processed code to be applied to the document.
58+
*/
59+
export function getTrimmedCodeFromMessage(message: any, doc: vscode.TextDocument, selection: vscode.Selection) {
60+
let codeToUpdate = message.code
61+
if (message.code.endsWith('\n')) {
62+
const startLineText = doc.lineAt(selection.start.line).text
63+
if (!startLineText.endsWith('\n')) {
64+
codeToUpdate = message.code.trimEnd()
65+
}
66+
}
67+
return codeToUpdate
68+
}
69+
70+
/**
71+
* Creates a selection range from the given document and selection.
72+
* If a user selects a partial code, this function generates the range from start line to end line.
73+
*
74+
* @param {vscode.TextDocument} doc - The VSCode document where the selection is applied.
75+
* @param {vscode.Selection} selection - The selection range in the document.
76+
* @returns {vscode.Range} - The VSCode range object representing the start and end of the selection.
77+
*/
78+
export function getSelectionFromRange(doc: vscode.TextDocument, selection: vscode.Selection) {
79+
return new vscode.Range(
80+
new vscode.Position(selection.start.line, 0),
81+
new vscode.Position(selection.end.line, doc.lineAt(selection.end.line).range.end.character)
82+
)
83+
}
84+
85+
/**
86+
* Applies the given code to the specified range in the document.
87+
* Saves the document after the edit is successfully applied.
88+
*
89+
* @param {vscode.TextDocument} doc - The VSCode document to which the changes are applied.
90+
* @param {vscode.Range} range - The range in the document where the code is replaced.
91+
* @param {string} code - The code to be applied to the document.
92+
* @returns {Promise<void>} - Resolves when the changes are successfully applied and the document is saved.
93+
*/
94+
export async function applyChanges(doc: vscode.TextDocument, range: vscode.Range, code: string) {
95+
const edit = new vscode.WorkspaceEdit()
96+
edit.replace(doc.uri, range, code)
97+
const successfulEdit = await vscode.workspace.applyEdit(edit)
98+
if (successfulEdit) {
99+
await doc.save()
100+
}
101+
}
102+
103+
/**
104+
* Creates a temporary file for diff comparison by cloning the original file
105+
* and applying the proposed changes within the selected range.
106+
*
107+
* @param {vscode.Uri} originalFileUri - The URI of the original file.
108+
* @param {any} message - The message object containing the proposed code changes.
109+
* @param {vscode.Selection} selection - The selection range in the document where the changes are applied.
110+
* @returns {Promise<vscode.Uri>} - A promise that resolves to the URI of the temporary file with the applied changes.
111+
*/
112+
export async function createTempFileForDiff(originalFileUri: vscode.Uri, message: any, selection: vscode.Selection) {
113+
const id = Date.now()
114+
const fileName = path.basename(originalFileUri.path)
115+
const tempFilePath = path.join(tempDirPath, `${fileName}_proposed-${id}`)
116+
117+
// Write original content to temp file
118+
fs.writeFileSync(tempFilePath, fs.readFileSync(originalFileUri.fsPath, 'utf-8'))
119+
120+
// Apply the proposed changes to the temp file
121+
const tempFileUri = vscode.Uri.file(tempFilePath)
122+
const doc = await vscode.workspace.openTextDocument(tempFileUri.path)
123+
const code = getTrimmedCodeFromMessage(message, doc, selection)
124+
const range = getSelectionFromRange(doc, selection)
125+
126+
await applyChanges(doc, range, code)
127+
return tempFileUri
128+
}
129+
130+
/**
131+
* Disposes of resources (content provider) when the temporary diff editor is closed.
132+
*
133+
* @param {vscode.Uri} tempFileUri - The URI of the temporary file used for diff comparison.
134+
* @param {vscode.Disposable} disposable - The disposable resource to be cleaned up (e.g., content provider).
135+
*/
136+
export function disposeOnEditorClose(tempFileUri: vscode.Uri, disposable: vscode.Disposable) {
137+
vscode.window.onDidChangeVisibleTextEditors(() => {
138+
if (
139+
!vscode.window.visibleTextEditors.some(
140+
(editor) => editor.document.uri.toString() === tempFileUri.toString()
141+
)
142+
) {
143+
disposable.dispose()
144+
}
145+
})
146+
}

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: 22 additions & 3 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,11 +242,17 @@ 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
? {
247247
'view-diff': {
248248
id: 'view-diff',
249249
label: 'View Diff',
250+
icon: MynahIcons.EYE,
251+
data: messageData,
252+
},
253+
'accept-diff': {
254+
id: 'accept-diff',
255+
label: 'Accept',
250256
icon: MynahIcons.OK_CIRCLED,
251257
data: messageData,
252258
},
@@ -483,7 +489,20 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
483489
codeBlockIndex?: number,
484490
totalCodeBlocks?: number
485491
) => {
486-
if (actionId === 'view-diff') {
492+
if (actionId === 'accept-diff') {
493+
connector.onAcceptDiff(
494+
tabId,
495+
messageId,
496+
actionId,
497+
data,
498+
code,
499+
type,
500+
referenceTrackerInformation,
501+
eventId,
502+
codeBlockIndex,
503+
totalCodeBlocks
504+
)
505+
} else if (actionId === 'view-diff') {
487506
connector.onViewDiff(
488507
tabId,
489508
messageId,

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)