Skip to content

Commit 947c471

Browse files
authored
feat(amazonq): support multiple uri in a single diff scheme (#7167)
## Problem The current viewDiff implementation is restricted to only allowing comparing the current file contents. This means it's not possible to view a diff for a change that was applied at a previous point in time. ## Solution - Create a new `DiffContentProvider` that can support showing multiple URIs in the same scheme. The current `ContentProvider` has a limitation that it can only show a single URI. - Refactor the code to use a defined type `ViewDiffMessage` instead of `any` - Change the logic so that diffs are stored in memory instead of a temp file on disk --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 3beb8b2 commit 947c471

File tree

10 files changed

+461
-30
lines changed

10 files changed

+461
-30
lines changed

package-lock.json

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

packages/amazonq/src/lsp/chat/messages.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ import * as jose from 'jose'
5959
import { AmazonQChatViewProvider } from './webviewProvider'
6060
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
6161
import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl } from 'aws-core-vscode/shared'
62-
import { DefaultAmazonQAppInitContext, messageDispatcher, EditorContentController } from 'aws-core-vscode/amazonq'
62+
import {
63+
DefaultAmazonQAppInitContext,
64+
messageDispatcher,
65+
EditorContentController,
66+
ViewDiffMessage,
67+
} from 'aws-core-vscode/amazonq'
6368
import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry'
6469
import { isValidResponseError } from './error'
6570

@@ -449,17 +454,24 @@ export function registerMessageListeners(
449454
new vscode.Position(0, 0),
450455
new vscode.Position(doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length)
451456
)
452-
await ecc.viewDiff(
453-
{
454-
context: {
455-
activeFileContext: { filePath: params.originalFileUri },
456-
focusAreaContext: { selectionInsideExtendedCodeBlock: entireDocumentSelection },
457+
const viewDiffMessage: ViewDiffMessage = {
458+
context: {
459+
activeFileContext: {
460+
filePath: params.originalFileUri,
461+
fileText: params.originalFileContent ?? '',
462+
fileLanguage: undefined,
463+
matchPolicy: undefined,
464+
},
465+
focusAreaContext: {
466+
selectionInsideExtendedCodeBlock: entireDocumentSelection,
467+
codeBlock: '',
468+
extendedCodeBlock: '',
469+
names: undefined,
457470
},
458-
code: params.fileContent ?? '',
459471
},
460-
amazonQDiffScheme,
461-
true
462-
)
472+
code: params.fileContent ?? '',
473+
}
474+
await ecc.viewDiff(viewDiffMessage, amazonQDiffScheme)
463475
})
464476

465477
languageClient.onNotification(chatUpdateNotificationType.method, (params: ChatUpdateParams) => {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@
444444
"@aws/chat-client": "^0.1.4",
445445
"@aws/chat-client-ui-types": "^0.1.24",
446446
"@aws/language-server-runtimes": "^0.2.70",
447-
"@aws/language-server-runtimes-types": "^0.1.21",
447+
"@aws/language-server-runtimes-types": "^0.1.26",
448448
"@cspotcode/source-map-support": "^0.8.1",
449449
"@sinonjs/fake-timers": "^10.0.2",
450450
"@types/adm-zip": "^0.4.34",

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@ import { amazonQDiffScheme, amazonQTabSuffix } from '../../../shared/constants'
1111
import { disposeOnEditorClose } from '../../../shared/utilities/editorUtilities'
1212
import {
1313
applyChanges,
14-
createTempFileForDiff,
14+
createTempUrisForDiff,
1515
getIndentedCode,
1616
getSelectionFromRange,
1717
} from '../../../shared/utilities/textDocumentUtilities'
1818
import { ToolkitError, getErrorMsg } from '../../../shared/errors'
1919
import fs from '../../../shared/fs/fs'
2020
import { extractFileAndCodeSelectionFromMessage } from '../../../shared/utilities/textUtilities'
2121
import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker'
22+
import type { ViewDiff } from '../../../codewhispererChat/controllers/chat/model'
23+
import type { TriggerEvent } from '../../../codewhispererChat/storages/triggerEvents'
24+
import { DiffContentProvider } from './diffContentProvider'
25+
26+
export type ViewDiffMessage = Pick<ViewDiff, 'code'> & Partial<Pick<TriggerEvent, 'context'>>
2227

2328
export class ContentProvider implements vscode.TextDocumentContentProvider {
2429
constructor(private uri: vscode.Uri) {}
@@ -155,26 +160,35 @@ export class EditorContentController {
155160
* isolating them from any other modifications in the original file.
156161
*
157162
* @param message the message from Amazon Q chat
163+
* @param scheme the URI scheme to use for the diff view
158164
*/
159-
public async viewDiff(message: any, scheme: string = amazonQDiffScheme, reverseOrder = false) {
165+
public async viewDiff(message: ViewDiffMessage, scheme: string = amazonQDiffScheme) {
160166
const errorNotification = 'Unable to Open Diff.'
161-
const { filePath, selection } = extractFileAndCodeSelectionFromMessage(message)
167+
const { filePath, fileText, selection } = extractFileAndCodeSelectionFromMessage(message)
162168

163169
try {
164170
if (filePath && message?.code !== undefined && selection) {
165-
const originalFileUri = vscode.Uri.file(filePath)
166-
const uri = await createTempFileForDiff(originalFileUri, message, selection, scheme)
167-
168171
// Register content provider and show diff
169-
const contentProvider = new ContentProvider(uri)
172+
const contentProvider = new DiffContentProvider()
170173
const disposable = vscode.workspace.registerTextDocumentContentProvider(scheme, contentProvider)
174+
175+
const [originalFileUri, modifiedFileUri] = await createTempUrisForDiff(
176+
filePath,
177+
fileText,
178+
message,
179+
selection,
180+
scheme,
181+
contentProvider
182+
)
183+
171184
await vscode.commands.executeCommand(
172185
'vscode.diff',
173-
...(reverseOrder ? [uri, originalFileUri] : [originalFileUri, uri]),
186+
originalFileUri,
187+
modifiedFileUri,
174188
`${path.basename(filePath)} ${amazonQTabSuffix}`
175189
)
176190

177-
disposeOnEditorClose(uri, disposable)
191+
disposeOnEditorClose(originalFileUri, disposable)
178192
}
179193
} catch (error) {
180194
void vscode.window.showInformationMessage(errorNotification)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import vscode from 'vscode'
6+
import { getLogger } from '../../../shared/logger/logger'
7+
8+
/**
9+
* A TextDocumentContentProvider that can handle multiple URIs with the same scheme.
10+
* This provider maintains a mapping of URIs to their content.
11+
*/
12+
export class DiffContentProvider implements vscode.TextDocumentContentProvider {
13+
private contentMap = new Map<string, string>()
14+
private _onDidChange = new vscode.EventEmitter<vscode.Uri>()
15+
16+
public readonly onDidChange = this._onDidChange.event
17+
18+
/**
19+
* Register content for a specific URI
20+
* @param uri The URI to register content for
21+
* @param content The content to serve for this URI
22+
*/
23+
public registerContent(uri: vscode.Uri, content: string): void {
24+
this.contentMap.set(uri.toString(), content)
25+
this._onDidChange.fire(uri)
26+
}
27+
28+
/**
29+
* Unregister a URI
30+
* @param uri The URI to unregister
31+
*/
32+
public unregisterUri(uri: vscode.Uri): void {
33+
this.contentMap.delete(uri.toString())
34+
}
35+
36+
/**
37+
* Provides the content for a given URI
38+
* @param uri The URI to provide content for
39+
* @returns The content as a string
40+
*/
41+
public provideTextDocumentContent(uri: vscode.Uri): string {
42+
const content = this.contentMap.get(uri.toString())
43+
44+
if (content === undefined) {
45+
getLogger().warn('No content registered for URI: %s', uri.toString())
46+
return ''
47+
}
48+
49+
return content
50+
}
51+
}

packages/core/src/amazonq/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export * as authConnection from '../auth/connection'
4747
export * as featureConfig from './webview/generators/featureConfig'
4848
export * as messageDispatcher from './webview/messages/messageDispatcher'
4949
import { FeatureContext } from '../shared/featureConfig'
50-
export { EditorContentController } from './commons/controllers/contentController'
50+
export { EditorContentController, ViewDiffMessage } from './commons/controllers/contentController'
5151

5252
/**
5353
* main from createMynahUI is a purely browser dependency. Due to this

packages/core/src/shared/utilities/textDocumentUtilities.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { getLogger } from '../logger/logger'
1111
import fs from '../fs/fs'
1212
import { ToolkitError } from '../errors'
1313
import { indent } from './textUtilities'
14+
import { ViewDiffMessage } from '../../amazonq/commons/controllers/contentController'
15+
import { DiffContentProvider } from '../../amazonq/commons/controllers/diffContentProvider'
1416

1517
/**
1618
* Finds occurences of text in a document. Currently only used for highlighting cloudwatchlogs data.
@@ -123,13 +125,13 @@ export async function applyChanges(doc: vscode.TextDocument, range: vscode.Range
123125
* and applying the proposed changes within the selected range.
124126
*
125127
* @param {vscode.Uri} originalFileUri - The URI of the original file.
126-
* @param {any} message - The message object containing the proposed code changes.
128+
* @param {ViewDiffMessage} message - The message object containing the proposed code changes.
127129
* @param {vscode.Selection} selection - The selection range in the document where the changes are applied.
128130
* @returns {Promise<vscode.Uri>} - A promise that resolves to the URI of the temporary file.
129131
*/
130132
export async function createTempFileForDiff(
131133
originalFileUri: vscode.Uri,
132-
message: any,
134+
message: ViewDiffMessage,
133135
selection: vscode.Selection,
134136
scheme: string
135137
): Promise<vscode.Uri> {
@@ -168,6 +170,53 @@ export async function createTempFileForDiff(
168170
return tempFileUri
169171
}
170172

173+
/**
174+
* Creates temporary URIs for diff comparison and registers their content with a DiffContentProvider.
175+
* This approach avoids writing to the file system by keeping content in memory.
176+
*
177+
* @param filePath The path of the original file (used for naming)
178+
* @param fileText Optional content of the original file (if not provided, will be read from filePath)
179+
* @param message The message object containing the proposed code changes
180+
* @param selection The selection range where changes should be applied
181+
* @param scheme The URI scheme to use
182+
* @param diffProvider The content provider to register URIs with
183+
* @returns A promise that resolves to a tuple of [originalUri, modifiedUri]
184+
*/
185+
export async function createTempUrisForDiff(
186+
filePath: string,
187+
fileText: string | undefined,
188+
message: ViewDiffMessage,
189+
selection: vscode.Selection,
190+
scheme: string,
191+
diffProvider: DiffContentProvider
192+
): Promise<[vscode.Uri, vscode.Uri]> {
193+
const originalFile = _path.parse(filePath)
194+
const id = Date.now()
195+
196+
// Create URIs with the custom scheme
197+
const originalFileUri = vscode.Uri.parse(`${scheme}:/${originalFile.name}_original-${id}${originalFile.ext}`)
198+
const modifiedFileUri = vscode.Uri.parse(`${scheme}:/${originalFile.name}_proposed-${id}${originalFile.ext}`)
199+
200+
// Get the original content
201+
const contentToUse = fileText ?? (await fs.readFileText(filePath))
202+
203+
// Register the original content
204+
diffProvider.registerContent(originalFileUri, contentToUse)
205+
206+
const indentedCode = getIndentedCodeFromOriginalContent(message, contentToUse, selection)
207+
const lines = contentToUse.split('\n')
208+
209+
// Create the modified content
210+
const beforeLines = lines.slice(0, selection.start.line)
211+
const afterLines = lines.slice(selection.end.line + 1)
212+
const modifiedContent = [...beforeLines, indentedCode, ...afterLines].join('\n')
213+
214+
// Register the modified content
215+
diffProvider.registerContent(modifiedFileUri, modifiedContent)
216+
217+
return [originalFileUri, modifiedFileUri]
218+
}
219+
171220
/**
172221
* Indents the given code based on the current document's indentation at the selection start.
173222
*
@@ -176,7 +225,7 @@ export async function createTempFileForDiff(
176225
* @param selection The selection range in the document.
177226
* @returns The processed code to be applied to the document.
178227
*/
179-
export function getIndentedCode(message: any, doc: vscode.TextDocument, selection: vscode.Selection) {
228+
export function getIndentedCode(message: ViewDiffMessage, doc: vscode.TextDocument, selection: vscode.Selection) {
180229
const indentRange = new vscode.Range(new vscode.Position(selection.start.line, 0), selection.active)
181230
let indentation = doc.getText(indentRange)
182231

@@ -187,6 +236,26 @@ export function getIndentedCode(message: any, doc: vscode.TextDocument, selectio
187236
return indent(message.code, indentation.length)
188237
}
189238

239+
/**
240+
* Indents the given code based on the indentation of the original content at the selection start.
241+
*
242+
* @param message The message object containing the code.
243+
* @param originalContent The original content of the document.
244+
* @param selection The selection range in the document.
245+
* @returns The processed code to be applied to the document.
246+
*/
247+
export function getIndentedCodeFromOriginalContent(
248+
message: ViewDiffMessage,
249+
originalContent: string,
250+
selection: vscode.Selection
251+
) {
252+
const lines = originalContent.split('\n')
253+
const selectionStartLine = lines[selection.start.line] || ''
254+
const indentMatch = selectionStartLine.match(/^(\s*)/)
255+
const indentation = indentMatch ? indentMatch[1] : ''
256+
return indent(message.code, indentation.length)
257+
}
258+
190259
export async function showFile(uri: vscode.Uri) {
191260
const doc = await vscode.workspace.openTextDocument(uri)
192261
await vscode.window.showTextDocument(doc, { preview: false })

packages/core/src/shared/utilities/textUtilities.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as crypto from 'crypto'
88
import * as fs from 'fs' // eslint-disable-line no-restricted-imports
99
import { default as stripAnsi } from 'strip-ansi'
1010
import { getLogger } from '../logger/logger'
11+
import { ViewDiffMessage } from '../../amazonq/commons/controllers/contentController'
1112

1213
/**
1314
* Truncates string `s` if it has or exceeds `n` chars.
@@ -268,10 +269,11 @@ export function decodeBase64(base64Str: string): string {
268269
* @param {any} message - The message object containing the file and selection context.
269270
* @returns {Object} - An object with `filePath` and `selection` properties.
270271
*/
271-
export function extractFileAndCodeSelectionFromMessage(message: any) {
272+
export function extractFileAndCodeSelectionFromMessage(message: ViewDiffMessage) {
272273
const filePath = message?.context?.activeFileContext?.filePath
274+
const fileText = message?.context?.activeFileContext?.fileText
273275
const selection = message?.context?.focusAreaContext?.selectionInsideExtendedCodeBlock as vscode.Selection
274-
return { filePath, selection }
276+
return { filePath, fileText, selection }
275277
}
276278

277279
export function matchesPattern(source: string, target: string | RegExp) {

0 commit comments

Comments
 (0)