Skip to content

Commit 8e95d4c

Browse files
author
Viktor Shesternyak
committed
feat(amazonq): /doc: add support for infrastructure diagrams
1 parent f738715 commit 8e95d4c

File tree

10 files changed

+173
-66
lines changed

10 files changed

+173
-66
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/core/src/amazonq/session/sessionState.ts

Lines changed: 12 additions & 2 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'
@@ -27,6 +27,7 @@ import {
2727
} from '../commons/types'
2828
import { prepareRepoData, getDeletedFileInfos, registerNewFiles } from '../util/files'
2929
import { uploadCode } from '../util/upload'
30+
import { TelemetryHelper } from '../util/telemetryHelper'
3031

3132
export const EmptyCodeGenID = 'EMPTY_CURRENT_CODE_GENERATION_ID'
3233

@@ -227,7 +228,7 @@ export abstract class BasePrepareCodeGenState implements SessionState {
227228
amazonqConversationId: this.config.conversationId,
228229
credentialStartUrl: AuthUtil.instance.startUrl,
229230
})
230-
const { zipFileBuffer, zipFileChecksum } = await prepareRepoData(
231+
const { zipFileBuffer, zipFileChecksum } = await this.prepareProjectZip(
231232
this.config.workspaceRoots,
232233
this.config.workspaceFolders,
233234
action.telemetry,
@@ -251,6 +252,15 @@ export abstract class BasePrepareCodeGenState implements SessionState {
251252
const nextState = this.createNextState({ ...this.config, uploadId })
252253
return nextState.interact(action)
253254
}
255+
256+
protected async prepareProjectZip(
257+
workspaceRoots: string[],
258+
workspaceFolders: CurrentWsFolders,
259+
telemetry: TelemetryHelper,
260+
span: Span<AmazonqCreateUpload>
261+
) {
262+
return await prepareRepoData(workspaceRoots, workspaceFolders, telemetry, span)
263+
}
254264
}
255265

256266
export interface CodeGenerationParams {

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

Lines changed: 50 additions & 6 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'
@@ -30,6 +35,13 @@ export async function checkForDevFile(root: string) {
3035
return hasDevFile
3136
}
3237

38+
function isInfraDiagramFile(relativePath: string) {
39+
return (
40+
relativePath.toLowerCase().endsWith(path.join('docs', 'infra.dot')) ||
41+
relativePath.toLowerCase().endsWith(path.join('docs', 'infra.svg'))
42+
)
43+
}
44+
3345
/**
3446
* given the root path of the repo it zips its files in memory and generates a checksum for it.
3547
*/
@@ -38,13 +50,38 @@ export async function prepareRepoData(
3850
workspaceFolders: CurrentWsFolders,
3951
telemetry: TelemetryHelper,
4052
span: Span<AmazonqCreateUpload>,
41-
zip: ZipStream = new ZipStream()
53+
zip: ZipStream = new ZipStream(),
54+
isIncludeInfraDiagram: boolean = false
4255
) {
4356
try {
4457
const autoBuildSetting = CodeWhispererSettings.instance.getAutoBuildSetting()
4558
const useAutoBuildFeature = autoBuildSetting[repoRootPaths[0]] ?? false
59+
const extraExcludeFilePatterns: string[] = []
60+
let extraFileFilterFn: CollectFilesFileFilter | undefined = undefined
61+
4662
// 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)
63+
if (!useAutoBuildFeature) {
64+
if (isIncludeInfraDiagram) {
65+
// ensure svg is not filtered out by files search
66+
extraExcludeFilePatterns.push(...DefaultExtraExcludePatterns.filter((p) => !p.endsWith('.svg')))
67+
// ensure only infra diagram is included from all svg files
68+
extraFileFilterFn = (relativePath: string) => {
69+
if (!relativePath.toLowerCase().endsWith('.svg')) {
70+
return false
71+
}
72+
return !isInfraDiagramFile(relativePath)
73+
}
74+
} else {
75+
extraExcludeFilePatterns.push(...DefaultExtraExcludePatterns)
76+
}
77+
}
78+
79+
const files = await collectFiles(repoRootPaths, workspaceFolders, {
80+
maxSizeLimitInBytes: maxRepoSizeBytes,
81+
useGitIgnoreFileAsFilter: true,
82+
extraExcludeFilePatterns,
83+
extraFileFilterFn,
84+
})
4885

4986
let totalBytes = 0
5087
const ignoredExtensionMap = new Map<string, number>()
@@ -62,9 +99,15 @@ export async function prepareRepoData(
6299
}
63100
const isCodeFile_ = isCodeFile(file.relativeFilePath)
64101
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) {
102+
const isInfraDiagramFileExt = isInfraDiagramFile(file.relativeFilePath)
103+
104+
let isExcludeFile = fileSize >= maxFileSizeBytes
105+
// When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit
106+
if (!isExcludeFile && !useAutoBuildFeature) {
107+
isExcludeFile = isDevFile || (!isCodeFile_ && (!isIncludeInfraDiagram || !isInfraDiagramFileExt))
108+
}
109+
110+
if (isExcludeFile) {
68111
if (!isCodeFile_) {
69112
const re = /(?:\.([^.]+))?$/
70113
const extensionArray = re.exec(file.relativeFilePath)
@@ -77,6 +120,7 @@ export async function prepareRepoData(
77120
}
78121
continue
79122
}
123+
80124
totalBytes += fileSize
81125
// Paths in zip should be POSIX compliant regardless of OS
82126
// Reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ 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,
@@ -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('.svg')) {
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: 14 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,10 @@ import {
2323
import { DocMessenger } from '../messenger'
2424
import { BaseCodeGenState, BasePrepareCodeGenState, CreateNextStateParams } from '../../amazonq/session/sessionState'
2525
import { Intent } from '../../amazonq/commons/types'
26+
import { TelemetryHelper } from '../../amazonq/util/telemetryHelper'
27+
import { AmazonqCreateUpload, Span } from '../../shared/telemetry/telemetry'
28+
import { prepareRepoData } from '../../amazonq/util/files'
29+
import { ZipStream } from '../../shared/utilities/zipStream'
2630

2731
export class DocCodeGenState extends BaseCodeGenState {
2832
protected handleProgress(messenger: DocMessenger, action: SessionStateAction, detail?: string): void {
@@ -132,4 +136,13 @@ export class DocPrepareCodeGenState extends BasePrepareCodeGenState {
132136
protected override createNextState(config: SessionStateConfig): SessionState {
133137
return super.createNextState(config, DocCodeGenState)
134138
}
139+
140+
protected override async prepareProjectZip(
141+
workspaceRoots: string[],
142+
workspaceFolders: CurrentWsFolders,
143+
telemetry: TelemetryHelper,
144+
span: Span<AmazonqCreateUpload>
145+
) {
146+
return await prepareRepoData(workspaceRoots, workspaceFolders, telemetry, span, new ZipStream(), true)
147+
}
135148
}

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)

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

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as parser from '@gerhobbelt/gitignore-parser'
1919
import fs from '../fs/fs'
2020
import { ChildProcess } from './processUtils'
2121
import { isWin } from '../vscode/env'
22+
import { maxRepoSizeBytes } from '../../amazonqFeatureDev/constants'
2223

2324
type GitIgnoreRelativeAcceptor = {
2425
folderPath: string
@@ -268,30 +269,44 @@ export function checkUnsavedChanges(): boolean {
268269
return vscode.workspace.textDocuments.some((doc) => doc.isDirty)
269270
}
270271

272+
export const DefaultExtraExcludePatterns = [
273+
'**/package-lock.json',
274+
'**/yarn.lock',
275+
'**/*.zip',
276+
'**/*.tar.gz',
277+
'**/*.bin',
278+
'**/*.png',
279+
'**/*.jpg',
280+
'**/*.svg',
281+
'**/*.pyc',
282+
'**/*.pdf',
283+
'**/*.ttf',
284+
'**/*.ico',
285+
'**/license.txt',
286+
'**/License.txt',
287+
'**/LICENSE.txt',
288+
'**/license.md',
289+
'**/License.md',
290+
'**/LICENSE.md',
291+
]
292+
271293
export function getExcludePattern(defaultExcludePatterns: boolean = true) {
272-
const globAlwaysExcludedDirs = getGlobDirExcludedPatterns().map((pattern) => `**/${pattern}/*`)
273-
const extraPatterns = [
274-
'**/package-lock.json',
275-
'**/yarn.lock',
276-
'**/*.zip',
277-
'**/*.tar.gz',
278-
'**/*.bin',
279-
'**/*.png',
280-
'**/*.jpg',
281-
'**/*.svg',
282-
'**/*.pyc',
283-
'**/*.pdf',
284-
'**/*.ttf',
285-
'**/*.ico',
286-
'**/license.txt',
287-
'**/License.txt',
288-
'**/LICENSE.txt',
289-
'**/license.md',
290-
'**/License.md',
291-
'**/LICENSE.md',
292-
]
293-
const allPatterns = [...globAlwaysExcludedDirs, ...(defaultExcludePatterns ? extraPatterns : [])]
294-
return `{${allPatterns.join(',')}}`
294+
const globAlwaysExcludedDirs = getGlobalExcludePatterns()
295+
const allPatterns = [...globAlwaysExcludedDirs]
296+
297+
if (defaultExcludePatterns) {
298+
allPatterns.push(...DefaultExtraExcludePatterns)
299+
}
300+
301+
return excludePatternsAsString(allPatterns)
302+
}
303+
304+
export function getGlobalExcludePatterns() {
305+
return getGlobDirExcludedPatterns().map((pattern) => `**/${pattern}/*`)
306+
}
307+
308+
export function excludePatternsAsString(patterns: string[]): string {
309+
return `{${patterns.join(',')}}`
295310
}
296311

297312
/**
@@ -313,30 +328,33 @@ async function filterOutGitignoredFiles(
313328
return gitIgnoreFilter.filterFiles(files)
314329
}
315330

331+
export type CollectFilesResultItem = {
332+
workspaceFolder: vscode.WorkspaceFolder
333+
relativeFilePath: string
334+
fileUri: vscode.Uri
335+
fileContent: string
336+
zipFilePath: string
337+
}
338+
export type CollectFilesFileFilter = (relativePath: string) => boolean // returns true if file should be filtered out
339+
316340
/**
317-
* collects all files that are marked as source
341+
* search and collect source files
318342
* @param sourcePaths the paths where collection starts
319343
* @param workspaceFolders the current workspace folders opened
320-
* @param respectGitIgnore whether to respect gitignore file
321-
* @param addExtraIgnorePatterns whether to add extra exclude patterns even if not in gitignore
344+
* @param options - filtering options
322345
* @returns all matched files
323346
*/
324347
export async function collectFiles(
325348
sourcePaths: string[],
326349
workspaceFolders: CurrentWsFolders,
327-
respectGitIgnore: boolean = true,
328-
maxSize = 200 * 1024 * 1024, // 200 MB
329-
defaultExcludePatterns: boolean = true
330-
): Promise<
331-
{
332-
workspaceFolder: vscode.WorkspaceFolder
333-
relativeFilePath: string
334-
fileUri: vscode.Uri
335-
fileContent: string
336-
zipFilePath: string
337-
}[]
338-
> {
339-
const storage: Awaited<ReturnType<typeof collectFiles>> = []
350+
options?: {
351+
maxSizeLimitInBytes?: number // 200 MB default
352+
useGitIgnoreFileAsFilter?: boolean // default true
353+
extraExcludeFilePatterns?: string[] // default DefaultExtraExcludePatterns
354+
extraFileFilterFn?: CollectFilesFileFilter
355+
}
356+
): Promise<CollectFilesResultItem[]> {
357+
const storage: Awaited<CollectFilesResultItem[]> = []
340358

341359
const workspaceFoldersMapping = getWorkspaceFoldersByPrefixes(workspaceFolders)
342360
const workspaceToPrefix = new Map<vscode.WorkspaceFolder, string>(
@@ -360,24 +378,37 @@ export async function collectFiles(
360378
}
361379

362380
let totalSizeBytes = 0
381+
382+
const useGitIgnoreFileAsFilter = options?.useGitIgnoreFileAsFilter ?? true
383+
const extraExcludeFilePatterns = options?.extraExcludeFilePatterns ?? DefaultExtraExcludePatterns
384+
const maxSizeLimitInBytes = options?.maxSizeLimitInBytes ?? maxRepoSizeBytes
385+
386+
const excludePatterns = [...getGlobalExcludePatterns()]
387+
if (extraExcludeFilePatterns.length) {
388+
excludePatterns.push(...extraExcludeFilePatterns)
389+
}
390+
const excludePatternFilter = excludePatternsAsString(excludePatterns)
391+
363392
for (const rootPath of sourcePaths) {
364393
const allFiles = await vscode.workspace.findFiles(
365394
new vscode.RelativePattern(rootPath, '**'),
366-
getExcludePattern(defaultExcludePatterns)
395+
excludePatternFilter
367396
)
368397

369-
const files = respectGitIgnore
370-
? await filterOutGitignoredFiles(rootPath, allFiles, defaultExcludePatterns)
371-
: allFiles
398+
const files = useGitIgnoreFileAsFilter ? await filterOutGitignoredFiles(rootPath, allFiles, false) : allFiles
372399

373400
for (const file of files) {
374401
const relativePath = getWorkspaceRelativePath(file.fsPath, { workspaceFolders })
375402
if (!relativePath) {
376403
continue
377404
}
378405

406+
if (options?.extraFileFilterFn && options.extraFileFilterFn(relativePath.relativePath)) {
407+
continue
408+
}
409+
379410
const fileStat = await fs.stat(file)
380-
if (totalSizeBytes + fileStat.size > maxSize) {
411+
if (totalSizeBytes + fileStat.size > maxSizeLimitInBytes) {
381412
throw new ToolkitError(
382413
'The project you have selected for source code is too large to use as context. Please select a different folder to use',
383414
{ code: 'ContentLengthError' }

0 commit comments

Comments
 (0)