// The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import * as path from 'path'; import * as http from 'http'; import * as fs from 'fs'; import * as serverStatic from 'serve-static'; import * as finalhandler from "finalhandler"; import * as vscode from 'vscode'; import { AddressInfo } from 'net'; import { ExcalidrawInstance } from './ExcalidrawInstance'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed function startServer(): http.Server { const root = path.resolve(__dirname, '../build'); const serve = serverStatic(root); return http.createServer(function (req, res) { serve(req as any, res as any, finalhandler(req, res)); }); } function setupWebview(webview: vscode.Webview, port: number) { webview.options = { enableScripts: true, }; webview.html = ` `; return new ExcalidrawInstance({ sendMessage: (msg) => webview.postMessage(msg), registerMessageHandler: (handler) => webview.onDidReceiveMessage(handler), }); } class ExcalidrawEditorProvider implements vscode.CustomTextEditorProvider, vscode.Disposable { server: http.Server; serverReady: Promise; instances: { excalidrawInstance: ExcalidrawInstance, panel: vscode.WebviewPanel }[] = []; constructor(private context: vscode.ExtensionContext) { this.server = startServer(); this.serverReady = new Promise(resolve => { this.server.listen(undefined, 'localhost', () => { resolve(); }); }); } async resolveCustomTextEditor(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken) { let localDisposables: vscode.Disposable[] = []; let initialized = false; let firstChange = false; await this.serverReady; const excalidrawInstance = setupWebview(webviewPanel.webview, (this.server.address() as AddressInfo).port); let isEditorSaving = false; localDisposables.push(vscode.workspace.onDidChangeTextDocument((event) => { if (event.document !== document || isEditorSaving || event.contentChanges.length === 0) { return; } excalidrawInstance.loadData(event.document.getText()); })); localDisposables.push(excalidrawInstance.onChange(async (data) => { if (!firstChange) { firstChange = true; return; } if (!data || data.newData === document.getText()) { return; } const edit = new vscode.WorkspaceEdit(); edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), data.newData); isEditorSaving = true; try { await vscode.workspace.applyEdit(edit); } finally { isEditorSaving = false; } })); localDisposables.push( excalidrawInstance.onSave(async () => { await document.save(); }) ); webviewPanel.onDidDispose(() => { localDisposables.forEach(d => d.dispose()); localDisposables = []; this.instances = this.instances.filter(i => i.panel !== webviewPanel); }); excalidrawInstance.onInit(() => { if (initialized) { return; } initialized = true; excalidrawInstance.loadData(document.getText()); }); this.instances.push({ excalidrawInstance, panel: webviewPanel, }); } broadcastDelete() { this.instances.filter(i => i.panel.active).forEach(({ excalidrawInstance }) => { excalidrawInstance.deleteShape(); }); } public async exportTo(format: 'png' | 'svg' = 'png', scale = 1.0): Promise { const instance = this.instances.find(i => i.panel.active); if (!instance) { return null; } const { excalidrawInstance } = instance; return await excalidrawInstance.exportTo(format, scale); } dispose() { if (this.server) { this.server.close(); } this.instances = []; } } export async function activate(context: vscode.ExtensionContext) { // Use the console to output diagnostic information (console.log) and errors (console.error) // This line of code will only be executed once when your extension is activated const provider = new ExcalidrawEditorProvider(context); context.subscriptions.push(vscode.window.registerCustomEditorProvider( 'brijeshb42-excalidraw.texteditor', provider, { webviewOptions: { retainContextWhenHidden: true } }, )); vscode.commands.registerCommand('brijeshb42-excalidraw.deleteshape', async () => { provider.broadcastDelete(); }); vscode.commands.registerCommand('brijeshb42-excalidraw.export', async () => { const result = await vscode.window.showQuickPick(['png', 'svg'], { placeHolder: 'Select the export format', ignoreFocusOut: false, }); if (!result) { return; } let scale: string | number | undefined = 1; if (result === 'png') { scale = await vscode.window.showInputBox({ value: '1', prompt: 'Input the scale of image (any number)', placeHolder: '1', ignoreFocusOut: false, validateInput(value) { if (!Number.isNaN(parseFloat(value))) { return null; } return 'error'; }, }); } if (!scale) { return; } const data = await provider.exportTo(result as 'svg' | 'png', parseFloat(scale as string)); if (!data) { vscode.window.showErrorMessage('Could not export.'); } else { vscode.window.showSaveDialog({ saveLabel: `Export as ${result}`, }).then(uri => { if (!uri) { return; } let modUri = uri.path; if (!modUri.endsWith(`.${result}`)) { modUri += `.${result}`; } if (result === 'png') { fs.writeFileSync(modUri, data.replace(/^data:image\/\w+;base64,/, ''), { encoding: 'base64' }); } else { fs.writeFileSync(modUri, data); } }); } }); context.subscriptions.push(provider); }