Skip to content

Commit a512982

Browse files
viksheViktor Shesternyak
andauthored
feat(amazonq): /doc: add support for infrastructure diagrams (#6561)
Problem: - Amazon Q does not have support for infrastructure diagrams Solution: - Add support for them ![infra_diagram](https://github.com/user-attachments/assets/79693ab0-d95d-415e-8daf-04d59fed8573) --- - 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. Co-authored-by: Viktor Shesternyak <[email protected]>
1 parent b9af56c commit a512982

File tree

13 files changed

+245
-97
lines changed

13 files changed

+245
-97
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q /doc: Add support for infrastructure diagrams"
4+
}

packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as vscode from 'vscode'
66
import assert from 'assert'
77
import {
88
prepareRepoData,
9+
PrepareRepoDataOptions,
910
TelemetryHelper,
1011
ContentLengthError,
1112
maxRepoSizeBytes,
@@ -46,23 +47,23 @@ const testDevfilePrepareRepo = async (devfileEnabled: boolean) => {
4647
.stub(CodeWhispererSettings.instance, 'getAutoBuildSetting')
4748
.returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {})
4849

49-
await testPrepareRepoData([workspace], expectedFiles)
50+
await testPrepareRepoData([workspace], expectedFiles, { telemetry: new TelemetryHelper() })
5051
}
5152

5253
const testPrepareRepoData = async (
5354
workspaces: vscode.WorkspaceFolder[],
5455
expectedFiles: string[],
56+
prepareRepoDataOptions: PrepareRepoDataOptions,
5557
expectedTelemetryMetrics?: Array<{ metricName: MetricName; value: any }>
5658
) => {
5759
expectedFiles.sort((a, b) => a.localeCompare(b))
58-
const telemetry = new TelemetryHelper()
5960
const result = await prepareRepoData(
6061
workspaces.map((ws) => ws.uri.fsPath),
6162
workspaces as CurrentWsFolders,
62-
telemetry,
6363
{
6464
record: () => {},
65-
} as unknown as Span<AmazonqCreateUpload>
65+
} as unknown as Span<AmazonqCreateUpload>,
66+
prepareRepoDataOptions
6667
)
6768

6869
assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true)
@@ -84,6 +85,8 @@ const testPrepareRepoData = async (
8485

8586
describe('file utils', () => {
8687
describe('prepareRepoData', function () {
88+
const defaultPrepareRepoDataOptions: PrepareRepoDataOptions = { telemetry: new TelemetryHelper() }
89+
8790
afterEach(() => {
8891
sinon.restore()
8992
})
@@ -92,21 +95,33 @@ describe('file utils', () => {
9295
const folder = await TestFolder.create()
9396
await folder.write('file1.md', 'test content')
9497
await folder.write('file2.md', 'test content')
98+
await folder.write('docs/infra.svg', 'test content')
9599
const workspace = getWorkspaceFolder(folder.path)
96100

97-
await testPrepareRepoData([workspace], ['file1.md', 'file2.md'])
101+
await testPrepareRepoData([workspace], ['file1.md', 'file2.md'], defaultPrepareRepoDataOptions)
102+
})
103+
104+
it('infrastructure diagram is included', async function () {
105+
const folder = await TestFolder.create()
106+
await folder.write('file1.md', 'test content')
107+
await folder.write('file2.svg', 'test content')
108+
await folder.write('docs/infra.svg', 'test content')
109+
const workspace = getWorkspaceFolder(folder.path)
110+
111+
await testPrepareRepoData([workspace], ['file1.md', 'docs/infra.svg'], {
112+
telemetry: new TelemetryHelper(),
113+
isIncludeInfraDiagram: true,
114+
})
98115
})
99116

100117
it('prepareRepoData ignores denied file extensions', async function () {
101118
const folder = await TestFolder.create()
102119
await folder.write('file.mp4', 'test content')
103120
const workspace = getWorkspaceFolder(folder.path)
104121

105-
await testPrepareRepoData(
106-
[workspace],
107-
[],
108-
[{ metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }]
109-
)
122+
await testPrepareRepoData([workspace], [], defaultPrepareRepoDataOptions, [
123+
{ metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } },
124+
])
110125
})
111126

112127
it('should ignore devfile.yaml when setting is disabled', async function () {
@@ -122,14 +137,18 @@ describe('file utils', () => {
122137
const folder = await TestFolder.create()
123138
await folder.write('file.md', 'test content')
124139
const workspace = getWorkspaceFolder(folder.path)
125-
const telemetry = new TelemetryHelper()
126140

127141
sinon.stub(fs, 'stat').resolves({ size: 2 * maxRepoSizeBytes } as vscode.FileStat)
128142
await assert.rejects(
129143
() =>
130-
prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, {
131-
record: () => {},
132-
} as unknown as Span<AmazonqCreateUpload>),
144+
prepareRepoData(
145+
[workspace.uri.fsPath],
146+
[workspace],
147+
{
148+
record: () => {},
149+
} as unknown as Span<AmazonqCreateUpload>,
150+
defaultPrepareRepoDataOptions
151+
),
133152
ContentLengthError
134153
)
135154
})
@@ -144,7 +163,11 @@ describe('file utils', () => {
144163
const workspace2 = getWorkspaceFolder(folder.path + '/innerFolder')
145164
const folderName = path.basename(folder.path)
146165

147-
await testPrepareRepoData([workspace1, workspace2], [`${folderName}_${workspace1.name}/${testFilePath}`])
166+
await testPrepareRepoData(
167+
[workspace1, workspace2],
168+
[`${folderName}_${workspace1.name}/${testFilePath}`],
169+
defaultPrepareRepoDataOptions
170+
)
148171
})
149172
})
150173
})

packages/core/src/amazonq/session/sessionState.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode'
77
import { ToolkitError } from '../../shared/errors'
88
import globals from '../../shared/extensionGlobals'
99
import { getLogger } from '../../shared/logger/logger'
10-
import { telemetry } from '../../shared/telemetry/telemetry'
10+
import { AmazonqCreateUpload, Span, telemetry } from '../../shared/telemetry/telemetry'
1111
import { VirtualFileSystem } from '../../shared/virtualFilesystem'
1212
import { CodeReference, UploadHistory } from '../webview/ui/connector'
1313
import { AuthUtil } from '../../codewhisperer/util/authUtil'
@@ -25,7 +25,7 @@ import {
2525
SessionStateInteraction,
2626
SessionStatePhase,
2727
} from '../commons/types'
28-
import { prepareRepoData, getDeletedFileInfos, registerNewFiles } from '../util/files'
28+
import { prepareRepoData, getDeletedFileInfos, registerNewFiles, PrepareRepoDataOptions } from '../util/files'
2929
import { uploadCode } from '../util/upload'
3030

3131
export const EmptyCodeGenID = 'EMPTY_CURRENT_CODE_GENERATION_ID'
@@ -227,11 +227,11 @@ export abstract class BasePrepareCodeGenState implements SessionState {
227227
amazonqConversationId: this.config.conversationId,
228228
credentialStartUrl: AuthUtil.instance.startUrl,
229229
})
230-
const { zipFileBuffer, zipFileChecksum } = await prepareRepoData(
230+
const { zipFileBuffer, zipFileChecksum } = await this.prepareProjectZip(
231231
this.config.workspaceRoots,
232232
this.config.workspaceFolders,
233-
action.telemetry,
234-
span
233+
span,
234+
{ telemetry: action.telemetry }
235235
)
236236
const uploadId = randomUUID()
237237
const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
@@ -251,6 +251,15 @@ export abstract class BasePrepareCodeGenState implements SessionState {
251251
const nextState = this.createNextState({ ...this.config, uploadId })
252252
return nextState.interact(action)
253253
}
254+
255+
protected async prepareProjectZip(
256+
workspaceRoots: string[],
257+
workspaceFolders: CurrentWsFolders,
258+
span: Span<AmazonqCreateUpload>,
259+
options: PrepareRepoDataOptions
260+
) {
261+
return await prepareRepoData(workspaceRoots, workspaceFolders, span, options)
262+
}
254263
}
255264

256265
export interface CodeGenerationParams {

packages/core/src/amazonq/util/files.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
import * as vscode from 'vscode'
77
import * as path from 'path'
8-
import { collectFiles, getWorkspaceFoldersByPrefixes } from '../../shared/utilities/workspaceUtils'
8+
import {
9+
collectFiles,
10+
CollectFilesFilter,
11+
defaultExcludePatterns,
12+
getWorkspaceFoldersByPrefixes,
13+
} from '../../shared/utilities/workspaceUtils'
914

1015
import { ContentLengthError, PrepareRepoFailedError } from '../../amazonqFeatureDev/errors'
1116
import { getLogger } from '../../shared/logger/logger'
@@ -24,27 +29,69 @@ import { isPresent } from '../../shared/utilities/collectionUtils'
2429
import { AuthUtil } from '../../codewhisperer/util/authUtil'
2530
import { TelemetryHelper } from '../util/telemetryHelper'
2631

32+
export const SvgFileExtension = '.svg'
33+
2734
export async function checkForDevFile(root: string) {
2835
const devFilePath = root + '/devfile.yaml'
2936
const hasDevFile = await fs.existsFile(devFilePath)
3037
return hasDevFile
3138
}
3239

40+
function isInfraDiagramFile(relativePath: string) {
41+
return (
42+
relativePath.toLowerCase().endsWith(path.join('docs', 'infra.dot')) ||
43+
relativePath.toLowerCase().endsWith(path.join('docs', 'infra.svg'))
44+
)
45+
}
46+
47+
export type PrepareRepoDataOptions = {
48+
telemetry?: TelemetryHelper
49+
zip?: ZipStream
50+
isIncludeInfraDiagram?: boolean
51+
}
52+
3353
/**
3454
* given the root path of the repo it zips its files in memory and generates a checksum for it.
3555
*/
3656
export async function prepareRepoData(
3757
repoRootPaths: string[],
3858
workspaceFolders: CurrentWsFolders,
39-
telemetry: TelemetryHelper,
4059
span: Span<AmazonqCreateUpload>,
41-
zip: ZipStream = new ZipStream()
60+
options?: PrepareRepoDataOptions
4261
) {
4362
try {
63+
const telemetry = options?.telemetry
64+
const isIncludeInfraDiagram = options?.isIncludeInfraDiagram ?? false
65+
const zip = options?.zip ?? new ZipStream()
66+
4467
const autoBuildSetting = CodeWhispererSettings.instance.getAutoBuildSetting()
4568
const useAutoBuildFeature = autoBuildSetting[repoRootPaths[0]] ?? false
69+
const excludePatterns: string[] = []
70+
let filterFn: CollectFilesFilter | undefined = undefined
71+
4672
// We only respect gitignore file rules if useAutoBuildFeature is on, this is to avoid dropping necessary files for building the code (e.g. png files imported in js code)
47-
const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes, !useAutoBuildFeature)
73+
if (!useAutoBuildFeature) {
74+
if (isIncludeInfraDiagram) {
75+
// ensure svg is not filtered out by files search
76+
excludePatterns.push(...defaultExcludePatterns.filter((p) => !p.endsWith(SvgFileExtension)))
77+
// ensure only infra diagram is included from all svg files
78+
filterFn = (relativePath: string) => {
79+
if (!relativePath.toLowerCase().endsWith(SvgFileExtension)) {
80+
return false
81+
}
82+
return !isInfraDiagramFile(relativePath)
83+
}
84+
} else {
85+
excludePatterns.push(...defaultExcludePatterns)
86+
}
87+
}
88+
89+
const files = await collectFiles(repoRootPaths, workspaceFolders, {
90+
maxSizeBytes: maxRepoSizeBytes,
91+
excludeByGitIgnore: true,
92+
excludePatterns: excludePatterns,
93+
filterFn: filterFn,
94+
})
4895

4996
let totalBytes = 0
5097
const ignoredExtensionMap = new Map<string, number>()
@@ -68,9 +115,15 @@ export async function prepareRepoData(
68115
}
69116
const isCodeFile_ = isCodeFile(file.relativeFilePath)
70117
const isDevFile = file.relativeFilePath === 'devfile.yaml'
71-
// When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit, otherwise, exclude all non code files and gitignore files
72-
const isNonCodeFileAndIgnored = useAutoBuildFeature ? false : !isCodeFile_ || isDevFile
73-
if (fileSize >= maxFileSizeBytes || isNonCodeFileAndIgnored) {
118+
const isInfraDiagramFileExt = isInfraDiagramFile(file.relativeFilePath)
119+
120+
let isExcludeFile = fileSize >= maxFileSizeBytes
121+
// When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit
122+
if (!isExcludeFile && !useAutoBuildFeature) {
123+
isExcludeFile = isDevFile || (!isCodeFile_ && (!isIncludeInfraDiagram || !isInfraDiagramFileExt))
124+
}
125+
126+
if (isExcludeFile) {
74127
if (!isCodeFile_) {
75128
const re = /(?:\.([^.]+))?$/
76129
const extensionArray = re.exec(file.relativeFilePath)
@@ -83,6 +136,7 @@ export async function prepareRepoData(
83136
}
84137
continue
85138
}
139+
86140
totalBytes += fileSize
87141
// Paths in zip should be POSIX compliant regardless of OS
88142
// Reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
@@ -117,7 +171,10 @@ export async function prepareRepoData(
117171
}
118172
}
119173

120-
telemetry.setRepositorySize(totalBytes)
174+
if (telemetry) {
175+
telemetry.setRepositorySize(totalBytes)
176+
}
177+
121178
span.record({ amazonqRepositorySize: totalBytes })
122179
const zipResult = await zip.finalize()
123180

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ import { BaseChatSessionStorage } from '../../../amazonq/commons/baseChatStorage
3535
import { DocMessenger } from '../../messenger'
3636
import { AuthController } from '../../../amazonq/auth/controller'
3737
import { openUrl } from '../../../shared/utilities/vsCodeUtils'
38-
import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff'
38+
import { createAmazonQUri, openDeletedDiff, openDiff } from '../../../amazonq/commons/diff'
3939
import {
4040
getWorkspaceFoldersByPrefixes,
4141
getWorkspaceRelativePath,
4242
isMultiRootWorkspace,
4343
} from '../../../shared/utilities/workspaceUtils'
44-
import { getPathsFromZipFilePath } from '../../../amazonq/util/files'
44+
import { getPathsFromZipFilePath, SvgFileExtension } from '../../../amazonq/util/files'
4545
import { FollowUpTypes } from '../../../amazonq/commons/types'
4646
import { DocGenerationTask } from '../docGenerationTask'
4747
import { DevPhase } from '../../types'
@@ -204,7 +204,13 @@ export class DocController {
204204
uploadId = session?.state?.uploadHistory[codeGenerationId].uploadId
205205
}
206206
const rightPath = path.join(uploadId, zipFilePath)
207-
await openDiff(pathInfos.absolutePath, rightPath, tabId, this.scheme)
207+
if (rightPath.toLowerCase().endsWith(SvgFileExtension)) {
208+
const rightPathUri = createAmazonQUri(rightPath, tabId, this.scheme)
209+
const infraDiagramContent = await vscode.workspace.openTextDocument(rightPathUri)
210+
await vscode.window.showTextDocument(infraDiagramContent)
211+
} else {
212+
await openDiff(pathInfos.absolutePath, rightPath, tabId, this.scheme)
213+
}
208214
}
209215
}
210216
}

packages/core/src/amazonqDoc/session/sessionState.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { DocGenerationStep, docScheme, getFileSummaryPercentage, Mode } from '..
88

99
import { i18n } from '../../shared/i18n-helper'
1010

11-
import { NewFileInfo, SessionState, SessionStateAction, SessionStateConfig } from '../types'
11+
import { CurrentWsFolders, NewFileInfo, SessionState, SessionStateAction, SessionStateConfig } from '../types'
1212
import {
1313
ContentLengthError,
1414
DocServiceError,
@@ -23,6 +23,8 @@ import {
2323
import { DocMessenger } from '../messenger'
2424
import { BaseCodeGenState, BasePrepareCodeGenState, CreateNextStateParams } from '../../amazonq/session/sessionState'
2525
import { Intent } from '../../amazonq/commons/types'
26+
import { AmazonqCreateUpload, Span } from '../../shared/telemetry/telemetry'
27+
import { prepareRepoData, PrepareRepoDataOptions } from '../../amazonq/util/files'
2628

2729
export class DocCodeGenState extends BaseCodeGenState {
2830
protected handleProgress(messenger: DocMessenger, action: SessionStateAction, detail?: string): void {
@@ -132,4 +134,16 @@ export class DocPrepareCodeGenState extends BasePrepareCodeGenState {
132134
protected override createNextState(config: SessionStateConfig): SessionState {
133135
return super.createNextState(config, DocCodeGenState)
134136
}
137+
138+
protected override async prepareProjectZip(
139+
workspaceRoots: string[],
140+
workspaceFolders: CurrentWsFolders,
141+
span: Span<AmazonqCreateUpload>,
142+
options: PrepareRepoDataOptions
143+
) {
144+
return await prepareRepoData(workspaceRoots, workspaceFolders, span, {
145+
...options,
146+
isIncludeInfraDiagram: true,
147+
})
148+
}
135149
}

packages/core/src/amazonqFeatureDev/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ export { Session } from './session/session'
1111
export { FeatureDevClient } from './client/featureDev'
1212
export { FeatureDevChatSessionStorage } from './storages/chatSession'
1313
export { TelemetryHelper } from '../amazonq/util/telemetryHelper'
14-
export { prepareRepoData } from '../amazonq/util/files'
14+
export { prepareRepoData, PrepareRepoDataOptions } from '../amazonq/util/files'
1515
export { ChatControllerEventEmitters, FeatureDevController } from './controllers/chat/controller'

packages/core/src/amazonqFeatureDev/session/sessionState.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export class MockCodeGenState implements SessionState {
7777
const files = await collectFiles(
7878
this.config.workspaceFolders.map((f) => path.join(f.uri.fsPath, './mock-data')),
7979
this.config.workspaceFolders,
80-
false
80+
{
81+
excludeByGitIgnore: false,
82+
}
8183
)
8284
const newFileContents = files.map((f) => ({
8385
zipFilePath: f.zipFilePath,

0 commit comments

Comments
 (0)