Skip to content

Commit 6671b6a

Browse files
author
Viktor Shesternyak
committed
feat(amazonq): /doc: add support for infrastructure diagrams
1 parent 0fa1b5b commit 6671b6a

File tree

13 files changed

+242
-91
lines changed

13 files changed

+242
-91
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: 39 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,
@@ -44,19 +45,24 @@ const testDevfilePrepareRepo = async (devfileEnabled: boolean) => {
4445
.stub(CodeWhispererSettings.instance, 'getAutoBuildSetting')
4546
.returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {})
4647

47-
await testPrepareRepoData(workspace, expectedFiles)
48+
await testPrepareRepoData(workspace, expectedFiles, { telemetry: new TelemetryHelper() })
4849
}
4950

5051
const testPrepareRepoData = async (
5152
workspace: vscode.WorkspaceFolder,
5253
expectedFiles: string[],
54+
prepareRepoDataOptions: PrepareRepoDataOptions,
5355
expectedTelemetryMetrics?: Array<{ metricName: MetricName; value: any }>
5456
) => {
5557
expectedFiles.sort((a, b) => a.localeCompare(b))
56-
const telemetry = new TelemetryHelper()
57-
const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, {
58-
record: () => {},
59-
} as unknown as Span<AmazonqCreateUpload>)
58+
const result = await prepareRepoData(
59+
[workspace.uri.fsPath],
60+
[workspace],
61+
{
62+
record: () => {},
63+
} as unknown as Span<AmazonqCreateUpload>,
64+
prepareRepoDataOptions
65+
)
6066

6167
assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true)
6268
// checksum is not the same across different test executions because some unique random folder names are generated
@@ -77,6 +83,8 @@ const testPrepareRepoData = async (
7783

7884
describe('file utils', () => {
7985
describe('prepareRepoData', function () {
86+
const defaultPrepareRepoDataOptions: PrepareRepoDataOptions = { telemetry: new TelemetryHelper() }
87+
8088
afterEach(() => {
8189
sinon.restore()
8290
})
@@ -85,21 +93,33 @@ describe('file utils', () => {
8593
const folder = await TestFolder.create()
8694
await folder.write('file1.md', 'test content')
8795
await folder.write('file2.md', 'test content')
96+
await folder.write('docs/infra.svg', 'test content')
97+
const workspace = getWorkspaceFolder(folder.path)
98+
99+
await testPrepareRepoData(workspace, ['file1.md', 'file2.md'], defaultPrepareRepoDataOptions)
100+
})
101+
102+
it('infrastructure diagram is included', async function () {
103+
const folder = await TestFolder.create()
104+
await folder.write('file1.md', 'test content')
105+
await folder.write('file2.svg', 'test content')
106+
await folder.write('docs/infra.svg', 'test content')
88107
const workspace = getWorkspaceFolder(folder.path)
89108

90-
await testPrepareRepoData(workspace, ['file1.md', 'file2.md'])
109+
await testPrepareRepoData(workspace, ['file1.md', 'docs/infra.svg'], {
110+
telemetry: new TelemetryHelper(),
111+
isIncludeInfraDiagram: true,
112+
})
91113
})
92114

93115
it('prepareRepoData ignores denied file extensions', async function () {
94116
const folder = await TestFolder.create()
95117
await folder.write('file.mp4', 'test content')
96118
const workspace = getWorkspaceFolder(folder.path)
97119

98-
await testPrepareRepoData(
99-
workspace,
100-
[],
101-
[{ metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }]
102-
)
120+
await testPrepareRepoData(workspace, [], defaultPrepareRepoDataOptions, [
121+
{ metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } },
122+
])
103123
})
104124

105125
it('should ignore devfile.yaml when setting is disabled', async function () {
@@ -115,14 +135,18 @@ describe('file utils', () => {
115135
const folder = await TestFolder.create()
116136
await folder.write('file.md', 'test content')
117137
const workspace = getWorkspaceFolder(folder.path)
118-
const telemetry = new TelemetryHelper()
119138

120139
sinon.stub(fs, 'stat').resolves({ size: 2 * maxRepoSizeBytes } as vscode.FileStat)
121140
await assert.rejects(
122141
() =>
123-
prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, {
124-
record: () => {},
125-
} as unknown as Span<AmazonqCreateUpload>),
142+
prepareRepoData(
143+
[workspace.uri.fsPath],
144+
[workspace],
145+
{
146+
record: () => {},
147+
} as unknown as Span<AmazonqCreateUpload>,
148+
defaultPrepareRepoDataOptions
149+
),
126150
ContentLengthError
127151
)
128152
})

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: 67 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+
CollectFilesFileFilter,
11+
DefaultExtraExcludePatterns,
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,71 @@ 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 extraExcludeFilePatterns: string[] = []
70+
let extraFileFilterFn: CollectFilesFileFilter | 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+
extraExcludeFilePatterns.push(
77+
...DefaultExtraExcludePatterns.filter((p) => !p.endsWith(SvgFileExtension))
78+
)
79+
// ensure only infra diagram is included from all svg files
80+
extraFileFilterFn = (relativePath: string) => {
81+
if (!relativePath.toLowerCase().endsWith(SvgFileExtension)) {
82+
return false
83+
}
84+
return !isInfraDiagramFile(relativePath)
85+
}
86+
} else {
87+
extraExcludeFilePatterns.push(...DefaultExtraExcludePatterns)
88+
}
89+
}
90+
91+
const files = await collectFiles(repoRootPaths, workspaceFolders, {
92+
maxSizeLimitInBytes: maxRepoSizeBytes,
93+
useGitIgnoreFileAsFilter: true,
94+
extraExcludeFilePatterns,
95+
extraFileFilterFn,
96+
})
4897

4998
let totalBytes = 0
5099
const ignoredExtensionMap = new Map<string, number>()
@@ -62,9 +111,15 @@ export async function prepareRepoData(
62111
}
63112
const isCodeFile_ = isCodeFile(file.relativeFilePath)
64113
const isDevFile = file.relativeFilePath === 'devfile.yaml'
65-
// 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
66-
const isNonCodeFileAndIgnored = useAutoBuildFeature ? false : !isCodeFile_ || isDevFile
67-
if (fileSize >= maxFileSizeBytes || isNonCodeFileAndIgnored) {
114+
const isInfraDiagramFileExt = isInfraDiagramFile(file.relativeFilePath)
115+
116+
let isExcludeFile = fileSize >= maxFileSizeBytes
117+
// When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit
118+
if (!isExcludeFile && !useAutoBuildFeature) {
119+
isExcludeFile = isDevFile || (!isCodeFile_ && (!isIncludeInfraDiagram || !isInfraDiagramFileExt))
120+
}
121+
122+
if (isExcludeFile) {
68123
if (!isCodeFile_) {
69124
const re = /(?:\.([^.]+))?$/
70125
const extensionArray = re.exec(file.relativeFilePath)
@@ -77,6 +132,7 @@ export async function prepareRepoData(
77132
}
78133
continue
79134
}
135+
80136
totalBytes += fileSize
81137
// Paths in zip should be POSIX compliant regardless of OS
82138
// Reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
@@ -111,7 +167,10 @@ export async function prepareRepoData(
111167
}
112168
}
113169

114-
telemetry.setRepositorySize(totalBytes)
170+
if (telemetry) {
171+
telemetry.setRepositorySize(totalBytes)
172+
}
173+
115174
span.record({ amazonqRepositorySize: totalBytes })
116175
const zipResult = await zip.finalize()
117176

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+
// use open instead of diff for svg
209+
const rightPathUri = createAmazonQUri(rightPath, tabId, this.scheme)
210+
await vscode.commands.executeCommand('vscode.open', rightPathUri)
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+
useGitIgnoreFileAsFilter: false,
82+
}
8183
)
8284
const newFileContents = files.map((f) => ({
8385
zipFilePath: f.zipFilePath,

packages/core/src/codewhisperer/util/zipUtil.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,9 @@ export class ZipUtil {
410410
return
411411
}
412412

413-
const sourceFiles = await collectFiles(
414-
projectPaths,
415-
vscode.workspace.workspaceFolders as CurrentWsFolders,
416-
true,
417-
this.getProjectScanPayloadSizeLimitInBytes()
418-
)
413+
const sourceFiles = await collectFiles(projectPaths, vscode.workspace.workspaceFolders as CurrentWsFolders, {
414+
maxSizeLimitInBytes: this.getProjectScanPayloadSizeLimitInBytes(),
415+
})
419416
for (const file of sourceFiles) {
420417
const projectName = path.basename(file.workspaceFolder.uri.fsPath)
421418
const zipEntryPath = this.getZipEntryPath(projectName, file.relativeFilePath)

0 commit comments

Comments
 (0)