Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add content assistance #135

Merged
merged 9 commits into from
Oct 24, 2022
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ You can open AsyncAPI Preview from the editor title/context menu.

Automatic hot-reloading on editor save, but currently, it doesn't reload when saving referenced external files.

## Content Assistance

### Available snippets:

Open an empty or otherwise yaml file and start typing one of the following prefixes, you may need to press `Ctrl+space` to trigger autocompletion in some cases:
ivangsa marked this conversation as resolved.
Show resolved Hide resolved

- `add asyncapi skeleton`: Adds an asyncapi skeleton for jump starting your API editing.
- `add asyncapi subscribe to async request`: Inserts a new subscribe operation, for listening to incoming async requests/commands.
- `add asyncapi publish event operation`: Inserts a new publish operation, for producing domain events.
- `add asyncapi message`: Inserts a new message, you can choose it to be either a **Request** or an **Event**.

Once snippets are inserted use the `<TAB>` key to travel between snippet placeholders.

## Paste as Schema

You can also autogenerate an Schema object from a JSON example.

Right-click inside `#/components/schemas` section and choose `AsyncAPI: Paste as Schema` from the context menu.

![VSCode AsyncapiPreview - Content Assistance](docs/VSCode%20AsyncAPI%20Content%20Assistance-X4.gif)

### Credits

AsyncAPI Viewer utilizes the following open source projects:
Expand Down
Binary file added docs/VSCode AsyncAPI Content Assistance-X4.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 26 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
"light": "resources/icons/open-preview_black.svg",
"dark": "resources/icons/open-preview_white.svg"
}
},
{
"command": "asyncapi.paste",
"title": "AsyncAPI: Paste as Schema"
}
],
"snippets": [
{
"language": "yaml",
"path": "./snippets/snippets.json"
}
],
"menus": {
Expand All @@ -63,6 +73,22 @@
{
"command": "asyncapi.preview"
}
],
"editor/context": [
{
"when": "resourceLangId == json || resourceLangId == yaml && asyncapi.isAsyncAPI",
"command": "asyncapi.paste",
"group": "9_cutcopypaste@5"
}
],
"keybindings": [
{
"command": "asyncapi.paste",
"key": "Ctrl+V",
"linux": "Ctrl+Shift+V",
"mac": "Cmd+V",
"when": "editorTextFocus"
}
]
}
},
Expand All @@ -83,6 +109,7 @@
"@semantic-release/github": "8.0.6",
"@semantic-release/release-notes-generator": "^10.0.3",
"@types/glob": "^7.2.0",
"@types/js-yaml": "^4.0.5",
"@types/mocha": "^9.1.1",
"@types/node": "14.x",
"@types/vscode": "^1.66.0",
Expand All @@ -93,6 +120,7 @@
"conventional-changelog-conventionalcommits": "^4.2.3",
"copy-webpack-plugin": "^10.2.4",
"eslint": "^8.14.0",
"genson-js": "0.0.8",
"glob": "^8.0.1",
"mocha": "^9.2.2",
"semantic-release": "19.0.5",
Expand Down
82 changes: 82 additions & 0 deletions snippets/snippets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"Add AsyncAPI Skeleton": {
"prefix": "add asyncapi skeleton",
"body": [
"asyncapi: '2.4.0'",
ivangsa marked this conversation as resolved.
Show resolved Hide resolved
"info:",
" title: ${1:Title}",
" version: '1.0.0'",
" description: |",
" Add your multiline description here.",
" contact:",
" name: ${2:Contact Name}",
" email: ${3:email@company.com}",
"",
"servers:",
" production:",
" url: server.url",
" protocol: mqtt",
" description: server description",
"",
"defaultContentType: application/json",
"",
"channels:",
" this.is.one.channel.please.rename:",
" description: A sample topic on which messages may be produced and consumed.",
"${4:# type 'add publish' or 'add subscribe' to get snippets autocomplete}",
"",
"",
"",
"components:",
" messages:",
" description: add one message and remove this line",
" # type here 'add message' to get snippets autocomplete",
"",
"",
" schemas:",
" OneEntity:",
" type: object",
"",
""
]
},
"Add Subscribe to Async Request": {
"prefix": "add asyncapi subscribe to async request",
"body": [
" subscribe:",
" summary: ${1:Entity} Async Requests",
" operationId: do${1:Entity}Request",
" tags:",
" - name: ${1:Entity}",
" message:",
" \\$ref: '#/components/messages/${1:Entity}RequestMessage'",
""
]
},
"Add Publish Event Operation": {
"prefix": "add asyncapi publish event operation",
"body": [
" publish:",
" summary: ${1:Entity} Domain Events",
" operationId: on${1:Entity}Event",
" tags:",
" - name: ${1:Entity}",
" message:",
" \\$ref: '#/components/messages/${1:Entity}EventMessage'",
""
]
},
"Add AsyncAPI Message": {
"prefix": "add asyncapi message",
"body": [
" ${1:Entity}${2|Request,Event|}Message:",
" name: ${1}${2}Message",
" title: Async ${2} for a ${1}",
" summary: Async ${2} for a ${1}",
" schemaFormat: application/vnd.aai.asyncapi;version=2.4.0",
" payload:",
" \\$ref: '#/components/schemas/${1}'",
""
]
}
}
116 changes: 116 additions & 0 deletions src/PreviewWebPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as vscode from 'vscode';
import * as path from 'path';

export function previewAsyncAPI(context: vscode.ExtensionContext) {
return async (uri: vscode.Uri) => {
uri = uri || (await promptForAsyncapiFile()) as vscode.Uri;
if (uri) {
console.log('Opening asyncapi file', uri.fsPath);
openAsyncAPI(context, uri);
}
};
}

export const openAsyncapiFiles: { [id: string]: vscode.WebviewPanel } = {}; // vscode.Uri.fsPath => vscode.WebviewPanel

export function isAsyncAPIFile(document?: vscode.TextDocument) {
if (!document) {
return false;
}
if (document.languageId === 'json') {
try {
const json = JSON.parse(document.getText());
return json.asyncapi;
} catch (e) {
return false;
}
}
if (document.languageId === 'yml' || document.languageId === 'yaml') {
return document.getText().match('^asyncapi:') !== null;
}
return false;
}

export function openAsyncAPI(context: vscode.ExtensionContext, uri: vscode.Uri) {
const localResourceRoots = [
vscode.Uri.file(path.dirname(uri.fsPath)),
vscode.Uri.joinPath(context.extensionUri, 'dist/node_modules/@asyncapi/react-component/browser/standalone'),
vscode.Uri.joinPath(context.extensionUri, 'dist/node_modules/@asyncapi/react-component/styles'),
];
if (vscode.workspace.workspaceFolders) {
vscode.workspace.workspaceFolders.forEach(folder => {
localResourceRoots.push(folder.uri);
});
}
const panel: vscode.WebviewPanel =
openAsyncapiFiles[uri.fsPath] ||
vscode.window.createWebviewPanel('asyncapi-preview', '', vscode.ViewColumn.Two, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots,
});
panel.title = path.basename(uri.fsPath);
panel.webview.html = getWebviewContent(context, panel.webview, uri);

panel.onDidDispose(() => {
delete openAsyncapiFiles[uri.fsPath];
});
openAsyncapiFiles[uri.fsPath] = panel;
}

async function promptForAsyncapiFile() {
if (isAsyncAPIFile(vscode.window.activeTextEditor?.document)) {
return vscode.window.activeTextEditor?.document.uri;
}
return await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: 'Open AsyncAPI file',
filters: {
AsyncAPI: ['yml', 'yaml', 'json'],
},
});
}

function getWebviewContent(context: vscode.ExtensionContext, webview: vscode.Webview, asyncapiFile: vscode.Uri) {
const asyncapiComponentJs = webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'dist/node_modules/@asyncapi/react-component/browser/standalone/index.js')
);
const asyncapiComponentCss = webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'dist/node_modules/@asyncapi/react-component/styles/default.min.css')
);
const asyncapiWebviewUri = webview.asWebviewUri(asyncapiFile);
const asyncapiBasePath = asyncapiWebviewUri.toString().replace('%2B', '+'); // this is loaded by a different library so it requires unescaping the + character
const html = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="${asyncapiComponentCss}">
</head>
<body x-timestamp="${Date.now()}">

<div id="asyncapi"></div>

<script src="${asyncapiComponentJs}"></script>
<script>
AsyncApiStandalone.render({
schema: {
url: '${asyncapiWebviewUri}',
options: { method: "GET", mode: "cors" },
},
config: {
show: {
sidebar: true,
errors: true,
},
parserOptions: { path: '${asyncapiBasePath}' }
},
}, document.getElementById('asyncapi'));
</script>

</body>
</html>
`;
return html;
}
Loading