Skip to content

Commit

Permalink
feat: add content assistance (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangsa committed Oct 24, 2022
1 parent d3d386f commit 602f1ff
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 111 deletions.
21 changes: 21 additions & 0 deletions README.md
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:

- `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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 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
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
@@ -0,0 +1,82 @@
{
"Add AsyncAPI Skeleton": {
"prefix": "add asyncapi skeleton",
"body": [
"asyncapi: '2.5.0'",
"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
@@ -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;
}

0 comments on commit 602f1ff

Please sign in to comment.