Skip to content

Commit 0cc8a89

Browse files
authored
refactor: Centralize diff logic for amazon q (#5356)
Problem: - A lot of the diffing functionality is hidden in feature dev Solution: - Pull it out into amazon q commons
1 parent cc8686f commit 0cc8a89

File tree

7 files changed

+167
-47
lines changed

7 files changed

+167
-47
lines changed

docs/TESTPLAN.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Checking the state works well if user interactions are not required by the code
104104
To handle this, test code can register event handler that listen for when a certain type of UI element is shown. For example, if we wanted to always accept the first item of a quick pick we can do this:
105105

106106
```ts
107-
getTestWindow().onDidShowQuickPick(async picker => {
107+
getTestWindow().onDidShowQuickPick(async (picker) => {
108108
// Some pickers load items asychronously
109109
// Wait until the picker is not busy before accepting an item
110110
await picker.untilReady()
@@ -121,3 +121,9 @@ const secondPicker = await pickers.next()
121121
```
122122

123123
Exceptions thrown within one of these handlers will cause the current test to fail. This allows you to make assertions within the callback without worrying about causing the test to hang.
124+
125+
## Common issues
126+
127+
### Stubbing VSCode outside of core
128+
129+
- Stubbing VSCode imports (like executeCommand) does not work outside of core. For now you will need to put any tests that require spying/stubbing VSCode imports in core until we move more source files into the amazon q package
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { existsSync } from 'fs'
7+
import * as vscode from 'vscode'
8+
import { featureDevScheme } from '../../amazonqFeatureDev/constants'
9+
10+
export async function openDiff(leftPath: string, rightPath: string, tabId: string) {
11+
const { left, right } = getFileDiffUris(leftPath, rightPath, tabId)
12+
await vscode.commands.executeCommand('vscode.diff', left, right)
13+
}
14+
15+
export async function openDeletedDiff(filePath: string, name: string, tabId: string) {
16+
const fileUri = getOriginalFileUri(filePath, tabId)
17+
await vscode.commands.executeCommand('vscode.open', fileUri, {}, `${name} (Deleted)`)
18+
}
19+
20+
export function getOriginalFileUri(fullPath: string, tabId: string) {
21+
return existsSync(fullPath) ? vscode.Uri.file(fullPath) : createAmazonQUri('empty', tabId)
22+
}
23+
24+
export function getFileDiffUris(leftPath: string, rightPath: string, tabId: string) {
25+
const left = getOriginalFileUri(leftPath, tabId)
26+
const right = createAmazonQUri(rightPath, tabId)
27+
28+
return { left, right }
29+
}
30+
31+
export function createAmazonQUri(path: string, tabId: string) {
32+
// TODO change the featureDevScheme to a more general amazon q scheme
33+
return vscode.Uri.from({ scheme: featureDevScheme, path, query: `tabID=${tabId}` })
34+
}

packages/core/src/amazonq/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export { amazonQHelpUrl } from '../shared/constants'
2727
export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/statusBarMenu'
2828
export { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands'
2929
export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens'
30+
export { createAmazonQUri, openDiff, openDeletedDiff, getOriginalFileUri, getFileDiffUris } from './commons/diff'
3031

3132
/**
3233
* main from createMynahUI is a purely browser dependency. Due to this

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

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
*/
55

66
import { ChatItemAction, MynahIcons } from '@aws/mynah-ui'
7-
import { existsSync } from 'fs'
87
import * as path from 'path'
98
import * as vscode from 'vscode'
109
import { EventEmitter } from 'vscode'
1110
import { telemetry } from '../../../shared/telemetry/telemetry'
1211
import { createSingleFileDialog } from '../../../shared/ui/common/openDialog'
13-
import { featureDevScheme } from '../../constants'
1412
import {
1513
CodeIterationLimitError,
1614
ContentLengthError,
@@ -53,6 +51,7 @@ import {
5351
} from '../../userFacingText'
5452
import { getWorkspaceFoldersByPrefixes } from '../../../shared/utilities/workspaceUtils'
5553
import { ErrorMessages } from './messenger/constants'
54+
import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff'
5655

5756
export interface ChatControllerEventEmitters {
5857
readonly processHumanChatMessage: EventEmitter<any>
@@ -744,24 +743,6 @@ export class FeatureDevController {
744743
})
745744
}
746745

747-
private getOriginalFileUri(fullPath: string, tabID: string) {
748-
const originalPath = fullPath
749-
return existsSync(originalPath)
750-
? vscode.Uri.file(originalPath)
751-
: vscode.Uri.from({ scheme: featureDevScheme, path: 'empty', query: `tabID=${tabID}` })
752-
}
753-
754-
private getFileDiffUris(zipFilePath: string, fullFilePath: string, tabId: string, session: Session) {
755-
const left = this.getOriginalFileUri(fullFilePath, tabId)
756-
const right = vscode.Uri.from({
757-
scheme: featureDevScheme,
758-
path: path.join(session.uploadId, zipFilePath),
759-
query: `tabID=${tabId}`,
760-
})
761-
762-
return { left, right }
763-
}
764-
765746
private async fileClicked(message: fileClickedMessage) {
766747
// TODO: add Telemetry here
767748
const tabId: string = message.tabID
@@ -804,12 +785,11 @@ export class FeatureDevController {
804785
const pathInfos = getPathsFromZipFilePath(zipFilePath, workspacePrefixMapping, session.config.workspaceFolders)
805786

806787
if (message.deleted) {
807-
const fileUri = this.getOriginalFileUri(pathInfos.absolutePath, tabId)
808-
const basename = path.basename(pathInfos.relativePath)
809-
await vscode.commands.executeCommand('vscode.open', fileUri, {}, `${basename} (Deleted)`)
788+
const name = path.basename(pathInfos.relativePath)
789+
await openDeletedDiff(pathInfos.absolutePath, name, tabId)
810790
} else {
811-
const { left, right } = this.getFileDiffUris(zipFilePath, pathInfos.absolutePath, tabId, session)
812-
await vscode.commands.executeCommand('vscode.diff', left, right)
791+
const rightPath = path.join(session.uploadId, zipFilePath)
792+
await openDiff(pathInfos.absolutePath, rightPath, tabId)
813793
}
814794
}
815795

packages/core/src/amazonqFeatureDev/types.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { VirtualFileSystem } from '../shared/virtualFilesystem'
88
import type { CancellationTokenSource } from 'vscode'
99
import { Messenger } from './controllers/chat/messenger/messenger'
1010
import { FeatureDevClient } from './client/featureDev'
11-
import { featureDevScheme } from './constants'
1211
import { TelemetryHelper } from './util/telemetryHelper'
1312
import { CodeReference } from '../amazonq/webview/ui/connector'
1413
import { DiffTreeFileInfo } from '../amazonq/webview/ui/diffTree/types'
@@ -107,12 +106,4 @@ export interface SessionStorage {
107106
[key: string]: SessionInfo
108107
}
109108

110-
export function createUri(filePath: string, tabID?: string) {
111-
return vscode.Uri.from({
112-
scheme: featureDevScheme,
113-
path: filePath,
114-
...(tabID ? { query: `tabID=${tabID}` } : {}),
115-
})
116-
}
117-
118109
export type LLMResponseType = 'EMPTY' | 'INVALID_STATE' | 'VALID'
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* TODO: Move this file to amazonq/test/unit when we can figure out how to spy on vscode imports from amazonq.
8+
*
9+
* See TESTPLAN.md#Stubbing VSCode outside of core
10+
*/
11+
import assert from 'assert'
12+
import * as path from 'path'
13+
import * as vscode from 'vscode'
14+
import fs from 'fs'
15+
import sinon from 'sinon'
16+
import { createAmazonQUri, getFileDiffUris, getOriginalFileUri, openDeletedDiff, openDiff } from '../../../amazonq'
17+
18+
describe('diff', () => {
19+
const filePath = path.join('/', 'foo', 'fi')
20+
const rightPath = path.join('foo', 'fee')
21+
const tabId = '0'
22+
23+
let sandbox: sinon.SinonSandbox
24+
let executeCommandSpy: sinon.SinonSpy
25+
26+
beforeEach(() => {
27+
sandbox = sinon.createSandbox()
28+
executeCommandSpy = sandbox.spy(vscode.commands, 'executeCommand')
29+
})
30+
31+
afterEach(() => {
32+
executeCommandSpy.restore()
33+
sandbox.restore()
34+
})
35+
36+
describe('openDiff', () => {
37+
it('file exists locally', async () => {
38+
sandbox.stub(fs, 'existsSync').returns(true)
39+
await openDiff(filePath, rightPath, tabId)
40+
41+
const leftExpected = vscode.Uri.file(filePath)
42+
const rightExpected = createAmazonQUri(rightPath, tabId)
43+
assert.ok(executeCommandSpy.calledWith('vscode.diff', leftExpected, rightExpected))
44+
})
45+
46+
it('file does not exists locally', async () => {
47+
sandbox.stub(fs, 'existsSync').returns(false)
48+
await openDiff(filePath, rightPath, tabId)
49+
50+
const leftExpected = getOriginalFileUri(filePath, tabId)
51+
const rightExpected = createAmazonQUri(rightPath, tabId)
52+
assert.ok(executeCommandSpy.calledWith('vscode.diff', leftExpected, rightExpected))
53+
})
54+
})
55+
56+
describe('openDeletedDiff', () => {
57+
const name = 'foo'
58+
59+
it('file exists locally', async () => {
60+
sandbox.stub(fs, 'existsSync').returns(true)
61+
await openDeletedDiff(filePath, name, tabId)
62+
63+
const expectedPath = vscode.Uri.file(filePath)
64+
assert.ok(executeCommandSpy.calledWith('vscode.open', expectedPath, {}, `${name} (Deleted)`))
65+
})
66+
67+
it('file does not exists locally', async () => {
68+
sandbox.stub(fs, 'existsSync').returns(false)
69+
await openDeletedDiff(filePath, name, tabId)
70+
71+
const expectedPath = createAmazonQUri('empty', tabId)
72+
assert.ok(executeCommandSpy.calledWith('vscode.open', expectedPath, {}, `${name} (Deleted)`))
73+
})
74+
})
75+
76+
describe('getOriginalFileUri', () => {
77+
it('file exists locally', () => {
78+
sandbox.stub(fs, 'existsSync').returns(true)
79+
assert.deepStrictEqual(getOriginalFileUri(filePath, tabId).fsPath, filePath)
80+
})
81+
82+
it('file does not exists locally', () => {
83+
sandbox.stub(fs, 'existsSync').returns(false)
84+
const expected = createAmazonQUri('empty', tabId)
85+
assert.deepStrictEqual(getOriginalFileUri(filePath, tabId), expected)
86+
})
87+
})
88+
89+
describe('getFileDiffUris', () => {
90+
it('file exists locally', () => {
91+
sandbox.stub(fs, 'existsSync').returns(true)
92+
93+
const { left, right } = getFileDiffUris(filePath, rightPath, tabId)
94+
95+
const leftExpected = vscode.Uri.file(filePath)
96+
assert.deepStrictEqual(left, leftExpected)
97+
98+
const rightExpected = createAmazonQUri(rightPath, tabId)
99+
assert.deepStrictEqual(right, rightExpected)
100+
})
101+
102+
it('file does not exists locally', () => {
103+
sandbox.stub(fs, 'existsSync').returns(false)
104+
const { left, right } = getFileDiffUris(filePath, rightPath, tabId)
105+
106+
const leftExpected = getOriginalFileUri(filePath, tabId)
107+
assert.deepStrictEqual(left, leftExpected)
108+
109+
const rightExpected = createAmazonQUri(rightPath, tabId)
110+
assert.deepStrictEqual(right, rightExpected)
111+
})
112+
})
113+
})

packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@ import * as path from 'path'
99
import sinon from 'sinon'
1010
import { waitUntil } from '../../../../shared/utilities/timeoutUtils'
1111
import { ControllerSetup, createController, createSession, generateVirtualMemoryUri } from '../../utils'
12-
import {
13-
CurrentWsFolders,
14-
FollowUpTypes,
15-
createUri,
16-
NewFileInfo,
17-
DeletedFileInfo,
18-
} from '../../../../amazonqFeatureDev/types'
12+
import { CurrentWsFolders, FollowUpTypes, NewFileInfo, DeletedFileInfo } from '../../../../amazonqFeatureDev/types'
1913
import { Session } from '../../../../amazonqFeatureDev/session/session'
2014
import { Prompter } from '../../../../shared/ui/prompter'
2115
import { assertTelemetry, toFile } from '../../../testUtil'
@@ -26,6 +20,7 @@ import {
2620
PrepareRefinementState,
2721
} from '../../../../amazonqFeatureDev/session/sessionState'
2822
import { FeatureDevClient } from '../../../../amazonqFeatureDev/client/featureDev'
23+
import { createAmazonQUri } from '../../../../amazonq/commons/diff'
2924

3025
let mockGetCodeGeneration: sinon.SinonStub
3126
describe('Controller', () => {
@@ -102,8 +97,8 @@ describe('Controller', () => {
10297
assert.strictEqual(
10398
executedDiff.calledWith(
10499
'vscode.diff',
105-
createUri('empty', tabID),
106-
createUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID)
100+
createAmazonQUri('empty', tabID),
101+
createAmazonQUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID)
107102
),
108103
true
109104
)
@@ -120,7 +115,7 @@ describe('Controller', () => {
120115
executedDiff.calledWith(
121116
'vscode.diff',
122117
vscode.Uri.file(newFileLocation),
123-
createUri(path.join(uploadID, 'mynewfile.js'), tabID)
118+
createAmazonQUri(path.join(uploadID, 'mynewfile.js'), tabID)
124119
),
125120
true
126121
)
@@ -137,7 +132,7 @@ describe('Controller', () => {
137132
executedDiff.calledWith(
138133
'vscode.diff',
139134
vscode.Uri.file(newFileLocation),
140-
createUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID)
135+
createAmazonQUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID)
141136
),
142137
true
143138
)
@@ -156,7 +151,7 @@ describe('Controller', () => {
156151
executedDiff.calledWith(
157152
'vscode.diff',
158153
vscode.Uri.file(newFileLocation),
159-
createUri(path.join(uploadID, 'foo', 'fi', 'mynewfile.js'), tabID)
154+
createAmazonQUri(path.join(uploadID, 'foo', 'fi', 'mynewfile.js'), tabID)
160155
),
161156
true
162157
)

0 commit comments

Comments
 (0)