Skip to content

Commit 38013db

Browse files
committed
Merge branch 'master' of github.com:aws-cloudformation/aws-cfn-lint-visual-studio-code
2 parents 1458f64 + 089b197 commit 38013db

File tree

11 files changed

+200
-25
lines changed

11 files changed

+200
-25
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ install:
2020
- nvm install 10.16.2;
2121
- npm install
2222
- npm run compile
23-
- pip install cfn-lint
23+
- pip install cfn-lint pydot
2424

2525
script:
2626
- npm run lint

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,24 @@ VS Code CloudFormation Linter uses cfn-lint to lint your CloudFormation template
88

99
## Features
1010

11-
Uses [cfn-lint](https://github.com/aws-cloudformation/cfn-python-lint) to parse and show issues with CloudFormation templates
11+
- Uses [cfn-lint](https://github.com/aws-cloudformation/cfn-python-lint) to parse the template and show problems with it.
1212

13-
For example if there is an image subfolder under your extension project workspace:
13+
- Uses [pydot](https://pypi.org/project/pydot/) to preview the template as a graph of resources.
1414

1515
![features](/images/features.png)
1616

1717
## Requirements
1818

19-
Requires cfn-lint to be installed: `pip install cfn-lint`
19+
Requires `cfn-lint` to be installed: `pip install cfn-lint`.
2020

21-
More information about cfn-lint can be found [here](https://github.com/aws-cloudformation/cfn-python-lint)
21+
If you want to be able to preview templates as graphs, you also need to install `pydot`: `pip install pydot`.
2222

2323
## Extension Settings
2424

25-
* `cfnLint.path`: path to the cfn-lint command
25+
* `cfnLint.path`: path to the `cfn-lint` command
2626
* `cfnLint.appendRules`: Array of paths containing additional Rules
2727
* `cfnLint.ignoreRules`: Array of Rule Ids to be ignored
28-
* `cfnLint.overrideSpecPath`: Path to an Specification overrule file
28+
* `cfnLint.overrideSpecPath`: Path to a specification override file
2929

3030
## Contribute
3131

client/src/extension.ts

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ permissions and limitations under the License.
1515
'use strict';
1616

1717
import * as path from 'path';
18-
19-
import { workspace, ExtensionContext, ConfigurationTarget, window } from 'vscode';
18+
import * as fs from 'fs';
19+
import { workspace, ExtensionContext, ConfigurationTarget, window, WebviewPanel, Uri, commands, ViewColumn, window as VsCodeWindow } from 'vscode';
2020
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient';
2121
import { registerYamlSchemaSupport } from './yaml-support/yaml-schema';
2222

23+
let previews: { [index: string]: WebviewPanel } = {};
24+
2325
export function activate(context: ExtensionContext) {
2426

2527
// The server is implemented in node
@@ -91,12 +93,96 @@ export function activate(context: ExtensionContext) {
9193
}
9294

9395
// Create the language client and start the client.
94-
let disposable = new LanguageClient('cfnLint', 'CloudFormation linter Language Server', serverOptions, clientOptions).start();
96+
let languageClient = new LanguageClient('cfnLint', 'CloudFormation linter Language Server', serverOptions, clientOptions);
97+
let clientDisposable = languageClient.start();
98+
99+
languageClient.onReady().then(() => {
100+
languageClient.onNotification('cfn/busy', () => {
101+
window.showInformationMessage("Linter is already running. Please try again.");
102+
});
103+
languageClient.onNotification('cfn/previewIsAvailable', (uri) => {
104+
reloadSidePreview(uri, languageClient);
105+
});
106+
languageClient.onNotification('cfn/isPreviewable', (value) => {
107+
commands.executeCommand('setContext', 'isPreviewable', value);
108+
});
109+
languageClient.onNotification('cfn/fileclosed', (uri) => {
110+
// if the user closed the template itself, we close the preview
111+
if (previews[uri]) {
112+
previews[uri].dispose();
113+
}
114+
});
115+
116+
let previewDisposable = commands.registerCommand('extension.sidePreview', () => {
117+
118+
if (window.activeTextEditor.document) {
119+
let uri = Uri.file(window.activeTextEditor.document.fileName).toString();
120+
121+
languageClient.sendNotification('cfn/requestPreview', uri);
122+
}
123+
124+
});
125+
126+
context.subscriptions.push(previewDisposable);
127+
});
95128

96129
// Push the disposable to the context's subscriptions so that the
97130
// client can be deactivated on extension deactivation
98-
context.subscriptions.push(disposable);
131+
context.subscriptions.push(clientDisposable);
132+
}
133+
134+
function reloadSidePreview(file:string, languageClient:LanguageClient) {
135+
let uri = Uri.parse(file);
136+
let stringifiedUri = uri.toString();
137+
let dotFile = uri.fsPath + ".dot";
138+
139+
if (!fs.existsSync(dotFile)) {
140+
window.showErrorMessage("Error previewing graph. Please run `pip3 install cfn-lint pydot --upgrade`");
141+
return;
142+
}
143+
let content = fs.readFileSync(dotFile, 'utf8');
144+
145+
if (!previews[stringifiedUri]) {
146+
previews[stringifiedUri] = VsCodeWindow.createWebviewPanel(
147+
'cfnLintPreview', // Identifies the type of the webview. Used internally
148+
'Template: ' + dotFile.slice(0,-4), // Title of the panel displayed to the user
149+
ViewColumn.Two, // Editor column to show the new webview panel in.
150+
{
151+
enableScripts: true,
152+
}
153+
);
154+
previews[stringifiedUri].onDidDispose(() => {
155+
// if the user closed the preview
156+
delete previews[stringifiedUri];
157+
fs.unlinkSync(dotFile);
158+
languageClient.sendNotification('cfn/previewClosed', stringifiedUri);
159+
});
160+
}
161+
162+
const panel = previews[stringifiedUri];
163+
panel.webview.html = getPreviewContent(content);
164+
}
99165

166+
function getPreviewContent(content: String) : string {
167+
168+
let multilineString = "`" + content + "`";
169+
// FIXME is there a better way of converting from dot to svg that is not using cdn urls?
170+
return `
171+
<!DOCTYPE html>
172+
<body>
173+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/d3.min.js" integrity="sha256-Xb6SSzhH3wEPC4Vy3W70Lqh9Y3Du/3KxPqI2JHQSpTw=" crossorigin="anonymous"></script>
174+
<script src="https://cdn.jsdelivr.net/npm/@hpcc-js/[email protected]/dist/index.min.js" integrity="sha256-GQQPKRntjhRqIwXvSCfytweTuDgJQ7hnK3RGsln9HWc=" crossorigin="anonymous"></script>
175+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/d3-graphviz.min.js" integrity="sha256-WRJh26uDo3BP+SjibjTwXI66JLkDFXmreIGpKm0xHV8=" crossorigin="anonymous"></script>
176+
<div id="graph" style="text-align: center;"></div>
177+
<script>
178+
d3.select("#graph")
179+
.graphviz()
180+
.width(screen.width)
181+
.height(screen.height)
182+
.renderDot(${multilineString});
183+
</script>
184+
</body>
185+
`;
100186
}
101187

102188
export async function yamlLangaugeServerValidation(): Promise<void> {

client/src/test/suite/cfnlint.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11

22
import * as vscode from 'vscode';
33
import * as assert from 'assert';
4-
import { getDocUri, activate } from './helper';
4+
import * as fs from 'fs';
5+
import { getDocUri, activate, activateAndPreview, getDocPath } from './helper';
56

67
suite('Should have failures with a bad template', () => {
78
const docUri = getDocUri('bad.yaml');
@@ -78,7 +79,7 @@ suite('Should have failures with a bad template', () => {
7879
});
7980
});
8081

81-
suite('Should not have failures on a goodtemplate', () => {
82+
suite('Should not have failures on a good template', () => {
8283
const docUri = getDocUri('good.yaml');
8384

8485
test('Diagnose good template', async () => {
@@ -136,6 +137,26 @@ suite('Should have failures even with a space in the filename', () => {
136137
});
137138
});
138139

140+
suite('Previews should work', () => {
141+
const docUri = 'preview.yaml';
142+
const dotUri = 'preview.yaml.dot';
143+
144+
test('Does NOT create .dot file if a preview was not requested', async () => {
145+
await activate(getDocUri(docUri));
146+
147+
assert.ok(! fs.existsSync(getDocPath(dotUri)));
148+
});
149+
150+
test('Does create .dot file if a preview was requested', async () => {
151+
await activateAndPreview(getDocUri(docUri));
152+
153+
assert.ok(fs.existsSync(getDocPath(dotUri)));
154+
155+
// cleanup
156+
fs.unlinkSync(getDocPath(dotUri));
157+
});
158+
});
159+
139160
function toRange(sLine: number, sChar: number, eLine: number, eChar: number) {
140161
const start = new vscode.Position(sLine, sChar);
141162
const end = new vscode.Position(eLine, eChar);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Resources:
2+
bucket:
3+
Type: AWS::S3::Bucket
4+
bucket2:
5+
Type: AWS::S3::Bucket
6+
DependsOn:
7+
- bucket

client/src/test/suite/helper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ export async function activate(docUri: vscode.Uri) {
2828
}
2929
}
3030

31+
export async function activateAndPreview(docUri: vscode.Uri) {
32+
await activate(docUri);
33+
34+
await vscode.commands.executeCommand('extension.sidePreview');
35+
36+
await sleep(4000); // Wait for preview to become available
37+
}
38+
3139
async function sleep(ms: number) {
3240
return new Promise(resolve => setTimeout(resolve, ms));
3341
}

images/features.png

23.1 KB
Loading

package.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"main": "./client/out/extension",
5050
"activationEvents": [
5151
"onLanguage:yaml",
52-
"onLanguage:json"
52+
"onLanguage:json",
53+
"onCommand:extension.sidePreview"
5354
],
5455
"extensionDependencies": [
5556
"redhat.vscode-yaml"
@@ -75,6 +76,25 @@
7576
"typescript": "^3.6.5"
7677
},
7778
"contributes": {
79+
"commands": [
80+
{
81+
"command": "extension.sidePreview",
82+
"title": "Preview CloudFormation template as graph",
83+
"icon": {
84+
"dark": "./resources/open-preview-dark.svg",
85+
"light": "./resources/open-preview-light.svg"
86+
}
87+
}
88+
],
89+
"menus": {
90+
"editor/title": [
91+
{
92+
"command": "extension.sidePreview",
93+
"group": "navigation",
94+
"when": "editorLangId =~ /(json|yaml)/ && isPreviewable"
95+
}
96+
]
97+
},
7898
"configuration": {
7999
"type": "object",
80100
"title": "CloudFormation Linter configuration",

resources/open-preview-dark.svg

Lines changed: 3 additions & 0 deletions
Loading

resources/open-preview-light.svg

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)