Skip to content

Commit 5faec4a

Browse files
JLargent42Aiden CarpenterJacob Largent
authored
feat(appcomposer): show CodeLens link to Application Composer #4436
Problem: - Users have reported difficulty finding Application Composer in the Toolkit. Solution: - Add an "Open with Application Composer" link using CodeLens to the start of detected CloudFormation / SAM templates. Co-authored-by: Aiden Carpenter <[email protected]> Co-authored-by: Jacob Largent <[email protected]>
1 parent 613bedd commit 5faec4a

File tree

8 files changed

+126
-54
lines changed

8 files changed

+126
-54
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": "CloudFormation and SAM templates now have a CodeLens link to Application Composer"
4+
}

packages/toolkit/src/applicationcomposer/activation.ts

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,9 @@
55

66
import * as vscode from 'vscode'
77
import { ApplicationComposerManager } from './webviewManager'
8-
import { Commands } from '../shared/vscode/commands2'
9-
import { ToolkitError } from '../shared/errors'
10-
import { AuthUtil, getChatAuthState } from '../codewhisperer/util/authUtil'
11-
import { telemetry } from '../shared/telemetry/telemetry'
12-
13-
export const openTemplateInComposerCommand = Commands.declare(
14-
'aws.openInApplicationComposer',
15-
(manager: ApplicationComposerManager) => async (arg?: vscode.TextEditor | vscode.Uri) => {
16-
const authState = await getChatAuthState(AuthUtil.instance)
17-
18-
let result: vscode.WebviewPanel | undefined
19-
await telemetry.appcomposer_openTemplate.run(async span => {
20-
span.record({
21-
hasChatAuth: authState.codewhispererChat === 'connected' || authState.codewhispererChat === 'expired',
22-
})
23-
arg ??= vscode.window.activeTextEditor
24-
const input = arg instanceof vscode.Uri ? arg : arg?.document
25-
26-
if (!input) {
27-
throw new ToolkitError('No active text editor or document found')
28-
}
29-
30-
result = await manager.visualizeTemplate(input)
31-
})
32-
return result
33-
}
34-
)
35-
36-
export const openInComposerDialogCommand = Commands.declare(
37-
'aws.openInApplicationComposerDialog',
38-
(manager: ApplicationComposerManager) => async () => {
39-
const fileUri = await vscode.window.showOpenDialog({
40-
filters: {
41-
Templates: ['yml', 'yaml', 'json', 'template'],
42-
},
43-
canSelectFiles: true,
44-
canSelectFolders: false,
45-
canSelectMany: false,
46-
})
47-
if (fileUri && fileUri[0]) {
48-
return await manager.visualizeTemplate(fileUri[0])
49-
}
50-
}
51-
)
8+
import { ApplicationComposerCodeLensProvider } from './codeLensProvider'
9+
import { openTemplateInComposerCommand } from './commands/openTemplateInComposer'
10+
import { openInComposerDialogCommand } from './commands/openInComposerDialog'
5211

5312
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
5413
const visualizationManager = new ApplicationComposerManager(extensionContext)
@@ -57,4 +16,11 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
5716
openTemplateInComposerCommand.register(visualizationManager),
5817
openInComposerDialogCommand.register(visualizationManager)
5918
)
19+
20+
extensionContext.subscriptions.push(
21+
vscode.languages.registerCodeLensProvider(
22+
['yaml', 'json', { scheme: 'file' }],
23+
new ApplicationComposerCodeLensProvider()
24+
)
25+
)
6026
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import vscode from 'vscode'
7+
import * as CloudFormation from '../shared/cloudformation/cloudformation'
8+
import * as nls from 'vscode-nls'
9+
import { openTemplateInComposerCommand } from './commands/openTemplateInComposer'
10+
11+
const localize = nls.loadMessageBundle()
12+
13+
export class ApplicationComposerCodeLensProvider implements vscode.CodeLensProvider {
14+
public async provideCodeLenses(
15+
document: vscode.TextDocument,
16+
_token: vscode.CancellationToken
17+
): Promise<vscode.CodeLens[]> {
18+
const cfnTemplate = CloudFormation.isValidFilename(document.uri)
19+
? await CloudFormation.tryLoad(document.uri)
20+
: undefined
21+
if (!cfnTemplate?.template) {
22+
return []
23+
}
24+
25+
let codeLensLine = 0
26+
for (let i = 0; i < document.getText().length; i++) {
27+
const line = document.lineAt(i)
28+
const lineContents = line.text.substring(line.firstNonWhitespaceCharacterIndex)
29+
if (lineContents.length > 0 && !lineContents.startsWith('#')) {
30+
codeLensLine = i
31+
break
32+
}
33+
}
34+
const resourcesLoc = new vscode.Range(codeLensLine, 0, codeLensLine, 0)
35+
const codeLens = openTemplateInComposerCommand.build().asCodeLens(resourcesLoc, {
36+
title: localize('AWS.applicationComposer.codeLens.title', 'Visualize with Application Composer'),
37+
tooltip: localize(
38+
'AWS.applicationComposer.codeLens.tooltip',
39+
'Visually design and build modern applications quickly'
40+
),
41+
})
42+
return [codeLens]
43+
}
44+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Commands } from '../../shared/vscode/commands2'
7+
import { ApplicationComposerManager } from '../webviewManager'
8+
import vscode from 'vscode'
9+
10+
export const openInComposerDialogCommand = Commands.declare(
11+
'aws.openInApplicationComposerDialog',
12+
(manager: ApplicationComposerManager) => async () => {
13+
const fileUri = await vscode.window.showOpenDialog({
14+
filters: {
15+
Templates: ['yml', 'yaml', 'json', 'template'],
16+
},
17+
canSelectFiles: true,
18+
canSelectFolders: false,
19+
canSelectMany: false,
20+
})
21+
if (fileUri && fileUri[0]) {
22+
return await manager.visualizeTemplate(fileUri[0])
23+
}
24+
}
25+
)
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 { Commands } from '../../shared/vscode/commands2'
7+
import { ApplicationComposerManager } from '../webviewManager'
8+
import vscode from 'vscode'
9+
import { AuthUtil, getChatAuthState } from '../../codewhisperer/util/authUtil'
10+
import { telemetry } from '../../shared/telemetry/telemetry'
11+
import { ToolkitError } from '../../shared/errors'
12+
13+
export const openTemplateInComposerCommand = Commands.declare(
14+
'aws.openInApplicationComposer',
15+
(manager: ApplicationComposerManager) => async (arg?: vscode.TextEditor | vscode.Uri) => {
16+
const authState = await getChatAuthState(AuthUtil.instance)
17+
18+
let result: vscode.WebviewPanel | undefined
19+
await telemetry.appcomposer_openTemplate.run(async span => {
20+
span.record({
21+
hasChatAuth: authState.codewhispererChat === 'connected' || authState.codewhispererChat === 'expired',
22+
})
23+
arg ??= vscode.window.activeTextEditor
24+
const input = arg instanceof vscode.Uri ? arg : arg?.document
25+
26+
if (!input) {
27+
throw new ToolkitError('No active text editor or document found')
28+
}
29+
30+
result = await manager.visualizeTemplate(input)
31+
})
32+
return result
33+
}
34+
)

packages/toolkit/src/shared/awsFiletypes.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,9 @@ export function activate(): void {
6767
vscode.workspace.onDidOpenTextDocument(async (doc: vscode.TextDocument) => {
6868
const isAwsFileExt = isAwsFiletype(doc)
6969
const isSchemaHandled = globals.schemaService.isMapped(doc.uri)
70-
const cfnTemplate =
71-
CloudFormation.isValidFilename(doc.uri) && doc.languageId === 'yaml'
72-
? await CloudFormation.tryLoad(doc.uri)
73-
: undefined
70+
const cfnTemplate = CloudFormation.isValidFilename(doc.uri)
71+
? await CloudFormation.tryLoad(doc.uri)
72+
: undefined
7473
const isCfnTemplate = cfnTemplate?.template !== undefined
7574

7675
if (!isAwsFileExt && !isSchemaHandled && !isCfnTemplate) {

packages/toolkit/src/shared/cloudformation/cloudformation.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const SERVERLESS_API_TYPE = 'AWS::Serverless::Api' // eslint-disable-line
1919
export const SERVERLESS_FUNCTION_TYPE = 'AWS::Serverless::Function' // eslint-disable-line @typescript-eslint/naming-convention
2020
export const LAMBDA_FUNCTION_TYPE = 'AWS::Lambda::Function' // eslint-disable-line @typescript-eslint/naming-convention
2121

22-
export const templateFileGlobPattern = '**/*.{yaml,yml}'
22+
export const templateFileGlobPattern = '**/*.{yaml,yml,json,template}'
23+
export const templateFileRegexPattern = /.*\.(yaml|yml|json|template)$/i
2324
export const devfileExcludePattern = /.*devfile\.(yaml|yml)/i
2425
/**
2526
* Match any file path that contains a .aws-sam folder. The way this works is:
@@ -378,16 +379,13 @@ export interface TemplateResources {
378379
export function isValidFilename(filename: string | vscode.Uri): boolean {
379380
filename = typeof filename === 'string' ? filename : filename.fsPath
380381
filename = filename.trim()
381-
if (!filename.endsWith('.yml') && !filename.endsWith('.yaml')) {
382+
if (!filename.match(templateFileRegexPattern)) {
382383
return false
383384
}
384385
// Note: intentionally _not_ checking `templateFileExcludePattern` here, because while excluding
385386
// template files in .aws-sam/ is relevant for the workspace scan, it's irrelevant if such
386387
// a file was opened explicitly by the user.
387-
if (filename.match(devfileExcludePattern)) {
388-
return false
389-
}
390-
return true
388+
return !filename.match(devfileExcludePattern)
391389
}
392390

393391
export async function load(filename: string, validate: boolean = true): Promise<Template> {

packages/toolkit/src/test/shared/cloudformation/cloudformation.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ describe('CloudFormation', function () {
3535
})
3636

3737
it('isValidFilename()', async function () {
38+
assert.deepStrictEqual(CloudFormation.isValidFilename('/foo/bar.json'), true)
39+
assert.deepStrictEqual(CloudFormation.isValidFilename('/foo/bar.template'), true)
3840
assert.deepStrictEqual(CloudFormation.isValidFilename('/foo/bar.yaml'), true)
3941
assert.deepStrictEqual(CloudFormation.isValidFilename('/foo/template.yaml'), true)
4042
assert.deepStrictEqual(CloudFormation.isValidFilename('template.yaml'), true)

0 commit comments

Comments
 (0)