Skip to content

Commit 80ac383

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

File tree

12 files changed

+310
-69
lines changed

12 files changed

+310
-69
lines changed

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

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,26 @@
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+
getIndentedCode,
17+
getSelectionFromRange,
18+
} from '../diff'
19+
import { amazonQDiffScheme } from '../../../shared'
20+
import { amazonQTabSuffix } from '../../../shared/constants'
1221

22+
class ContentProvider implements vscode.TextDocumentContentProvider {
23+
constructor(private tempFileUri: vscode.Uri) {}
24+
25+
provideTextDocumentContent(_uri: vscode.Uri) {
26+
return fs.readFileSync(this.tempFileUri.fsPath, 'utf-8')
27+
}
28+
}
1329
export class EditorContentController {
1430
/* *
1531
* Insert the Amazon Q chat written code to the cursor position
@@ -56,6 +72,31 @@ export class EditorContentController {
5672
}
5773
}
5874

75+
/**
76+
* Accept code changes received by Amazon Q
77+
*
78+
* @param message the message from Amazon Q chat
79+
*/
80+
public async acceptDiff(message: any) {
81+
const { filePath, selection } = extractFileAndCodeSelectionFromMessage(message)
82+
83+
if (filePath && message?.code?.trim().length > 0 && selection) {
84+
const doc = await vscode.workspace.openTextDocument(filePath)
85+
const codeToUpdate = getIndentedCode(message, doc, selection)
86+
const range = getSelectionFromRange(doc, selection)
87+
await applyChanges(doc, range, codeToUpdate)
88+
89+
// If vscode.diff is open for the filePath then close it.
90+
vscode.window.tabGroups.all.flatMap(({ tabs }) =>
91+
tabs.map((tab) => {
92+
if (tab.label === `${path.basename(filePath)} ${amazonQTabSuffix}`) {
93+
void vscode.window.tabGroups.close(tab)
94+
}
95+
})
96+
)
97+
}
98+
}
99+
59100
/**
60101
* Displays a diff view comparing proposed changes with the existing file.
61102
*
@@ -70,63 +111,23 @@ export class EditorContentController {
70111
* @param message the message from Amazon Q chat
71112
*/
72113
public async viewDiff(message: any) {
73-
const { filePath } = message?.context?.activeFileContext || {}
74-
const selection = message?.context?.focusAreaContext?.selectionInsideExtendedCodeBlock as vscode.Selection
114+
const { filePath, selection } = extractFileAndCodeSelectionFromMessage(message)
75115

76116
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)
117+
const originalFileUri = vscode.Uri.file(filePath)
118+
const tempFileUri = await createTempFileForDiff(originalFileUri, message, selection)
96119

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(
120+
// Register content provider and show diff
121+
const contentProvider = new ContentProvider(tempFileUri)
122+
const disposable = vscode.workspace.registerTextDocumentContentProvider(amazonQDiffScheme, contentProvider)
123+
await vscode.commands.executeCommand(
115124
'vscode.diff',
116125
originalFileUri,
117126
tempFileUri,
118-
`${fileName}${fileExtension} (Generated by Amazon Q)`
127+
`${path.basename(filePath)} ${amazonQTabSuffix}`
119128
)
120129

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-
})
130+
disposeOnEditorClose(tempFileUri, disposable)
130131
}
131132
}
132133
}

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

Lines changed: 127 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 { amazonQDiffScheme, 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,126 @@ 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+
* Indents the given code based on the current document's indentation at the selection start.
52+
*
53+
* @param {any} message - The message object containing the code.
54+
* @param {vscode.TextDocument} doc - The VSCode document where the code is applied.
55+
* @param {vscode.Selection} selection - The selection range in the document.
56+
* @returns {string} - The processed code to be applied to the document.
57+
*/
58+
export function getIndentedCode(message: any, doc: vscode.TextDocument, selection: vscode.Selection) {
59+
const code = message.code as string
60+
const indentRange = new vscode.Range(new vscode.Position(selection.start.line, 0), selection.active)
61+
let indent = doc.getText(indentRange)
62+
63+
if (indent.trim().length !== 0) {
64+
indent = ' '.repeat(indent.length - indent.trimStart().length)
65+
}
66+
67+
let trimmedCode = code
68+
if (code.endsWith('\n')) {
69+
const startLineText = doc.lineAt(selection.start.line).text
70+
if (!startLineText.endsWith('\n')) {
71+
trimmedCode = code.trimEnd()
72+
}
73+
}
74+
75+
const indentedCode = trimmedCode
76+
.split('\n')
77+
.map((line) => indent + line)
78+
.join('\n')
79+
80+
return indentedCode
81+
}
82+
83+
/**
84+
* Creates a selection range from the given document and selection.
85+
* If a user selects a partial code, this function generates the range from start line to end line.
86+
*
87+
* @param {vscode.TextDocument} doc - The VSCode document where the selection is applied.
88+
* @param {vscode.Selection} selection - The selection range in the document.
89+
* @returns {vscode.Range} - The VSCode range object representing the start and end of the selection.
90+
*/
91+
export function getSelectionFromRange(doc: vscode.TextDocument, selection: vscode.Selection) {
92+
return new vscode.Range(
93+
new vscode.Position(selection.start.line, 0),
94+
new vscode.Position(selection.end.line, doc.lineAt(selection.end.line).range.end.character)
95+
)
96+
}
97+
98+
/**
99+
* Applies the given code to the specified range in the document.
100+
* Saves the document after the edit is successfully applied.
101+
*
102+
* @param {vscode.TextDocument} doc - The VSCode document to which the changes are applied.
103+
* @param {vscode.Range} range - The range in the document where the code is replaced.
104+
* @param {string} code - The code to be applied to the document.
105+
* @returns {Promise<void>} - Resolves when the changes are successfully applied and the document is saved.
106+
*/
107+
export async function applyChanges(doc: vscode.TextDocument, range: vscode.Range, code: string) {
108+
const edit = new vscode.WorkspaceEdit()
109+
edit.replace(doc.uri, range, code)
110+
const successfulEdit = await vscode.workspace.applyEdit(edit)
111+
if (successfulEdit) {
112+
await doc.save()
113+
}
114+
}
115+
116+
/**
117+
* Creates a temporary file for diff comparison by cloning the original file
118+
* and applying the proposed changes within the selected range.
119+
*
120+
* @param {vscode.Uri} originalFileUri - The URI of the original file.
121+
* @param {any} message - The message object containing the proposed code changes.
122+
* @param {vscode.Selection} selection - The selection range in the document where the changes are applied.
123+
* @returns {Promise<vscode.Uri>} - A promise that resolves to the URI of the temporary file with the applied changes.
124+
*/
125+
export async function createTempFileForDiff(originalFileUri: vscode.Uri, message: any, selection: vscode.Selection) {
126+
const id = Date.now()
127+
const fileName = path.basename(originalFileUri.path)
128+
const tempFilePath = path.join(tempDirPath, `${fileName}_proposed-${id}`)
129+
const tempFileUri = vscode.Uri.parse(`${amazonQDiffScheme}:${tempFilePath}`)
130+
131+
// Write original content to temp file
132+
fs.writeFileSync(tempFilePath, fs.readFileSync(originalFileUri.fsPath, 'utf-8'))
133+
134+
// Apply the proposed changes to the temp file
135+
const doc = await vscode.workspace.openTextDocument(tempFileUri.path)
136+
const code = getIndentedCode(message, doc, selection)
137+
const range = getSelectionFromRange(doc, selection)
138+
139+
await applyChanges(doc, range, code)
140+
return tempFileUri
141+
}
142+
143+
/**
144+
* Disposes of resources (content provider) when the temporary diff editor is closed.
145+
*
146+
* @param {vscode.Uri} tempFileUri - The URI of the temporary file used for diff comparison.
147+
* @param {vscode.Disposable} disposable - The disposable resource to be cleaned up (e.g., content provider).
148+
*/
149+
export function disposeOnEditorClose(tempFileUri: vscode.Uri, disposable: vscode.Disposable) {
150+
vscode.window.onDidChangeVisibleTextEditors(() => {
151+
if (
152+
!vscode.window.visibleTextEditors.some(
153+
(editor) => editor.document.uri.toString() === tempFileUri.toString()
154+
)
155+
) {
156+
disposable.dispose()
157+
}
158+
})
159+
}

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,

0 commit comments

Comments
 (0)