diff --git a/packages/components/package.json b/packages/components/package.json index 3983954a..7c64463c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -18,6 +18,7 @@ "vega": "^5.22.1", "vega-embed": "^6.21.0", "vega-lite": "^5.2.0", + "vscode-uri": "^3.0.3", "yup": "^0.32.11" }, "devDependencies": { diff --git a/packages/vscode-ext/media/previewWebview.js b/packages/vscode-ext/media/previewWebview.js new file mode 100644 index 00000000..6d91b1b9 --- /dev/null +++ b/packages/vscode-ext/media/previewWebview.js @@ -0,0 +1,43 @@ +// based on https://github.com/microsoft/vscode-extension-samples/blob/main/custom-editor-sample/media/catScratch.js +(function () { + console.log("hello world"); + const vscode = acquireVsCodeApi(); + + const container = document.getElementById("root"); + + const root = ReactDOM.createRoot(container); + function updateContent(text) { + root.render( + React.createElement(squiggle_components.SquigglePlayground, { + code: text, + onCodeChange: (code) => { + vscode.postMessage({ type: "edit", text: code }); + }, + showEditor: false, + }) + ); + } + + // Handle messages sent from the extension to the webview + window.addEventListener("message", (event) => { + const message = event.data; // The json data that the extension sent + switch (message.type) { + case "update": + const text = message.text; + + // Update our webview's content + updateContent(text); + + // Then persist state information. + // This state is returned in the call to `vscode.getState` below when a webview is reloaded. + vscode.setState({ text }); + + return; + } + }); + + const state = vscode.getState(); + if (state) { + updateContent(state.text); + } +})(); diff --git a/packages/vscode-ext/media/wysiwyg.js b/packages/vscode-ext/media/wysiwygWebview.js similarity index 100% rename from packages/vscode-ext/media/wysiwyg.js rename to packages/vscode-ext/media/wysiwygWebview.js diff --git a/packages/vscode-ext/package.json b/packages/vscode-ext/package.json index bcd18474..47ac5c1f 100644 --- a/packages/vscode-ext/package.json +++ b/packages/vscode-ext/package.json @@ -18,10 +18,22 @@ "Visualization" ], "activationEvents": [ - "onCustomEditor:squiggle.wysiwyg" + "onCustomEditor:squiggle.wysiwyg", + "onCommand:squiggle.preview" ], "main": "./out/extension.js", "contributes": { + "languages": [ + { + "id": "squiggle", + "extensions": [ + ".squiggle" + ], + "aliases": [ + "Squiggle" + ] + } + ], "customEditors": [ { "viewType": "squiggle.wysiwyg", @@ -31,10 +43,41 @@ "filenamePattern": "*.squiggle" } ], - "priority": "default" + "priority": "option" } ], - "commands": [] + "commands": [ + { + "command": "squiggle.preview", + "title": "Open Preview", + "category": "Squiggle", + "when": "editorLangId == squiggle", + "icon": "$(open-preview)" + } + ], + "menus": { + "editor/title": [ + { + "command": "squiggle.preview", + "when": "editorLangId == squiggle", + "group": "navigation" + } + ], + "commandPalette": [ + { + "command": "squiggle.preview", + "when": "editorLangId == squiggle" + } + ] + }, + "keybindings": [ + { + "command": "squiggle.preview", + "key": "ctrl+k v", + "mac": "cmd+k v", + "when": "editorLangId == squiggle" + } + ] }, "scripts": { "vscode:prepublish": "yarn run compile", diff --git a/packages/vscode-ext/src/squiggleEditor.ts b/packages/vscode-ext/src/editor.ts similarity index 55% rename from packages/vscode-ext/src/squiggleEditor.ts rename to packages/vscode-ext/src/editor.ts index 3e3d9f2c..4410ba26 100644 --- a/packages/vscode-ext/src/squiggleEditor.ts +++ b/packages/vscode-ext/src/editor.ts @@ -1,14 +1,5 @@ import * as vscode from "vscode"; - -function getNonce() { - let text = ""; - const possible = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} +import { getWebviewContent } from "./utils"; export class SquiggleEditorProvider implements vscode.CustomTextEditorProvider { public static register(context: vscode.ExtensionContext): vscode.Disposable { @@ -26,8 +17,6 @@ export class SquiggleEditorProvider implements vscode.CustomTextEditorProvider { /** * Called when our custom editor is opened. - * - * */ public async resolveCustomTextEditor( document: vscode.TextDocument, @@ -37,7 +26,12 @@ export class SquiggleEditorProvider implements vscode.CustomTextEditorProvider { webviewPanel.webview.options = { enableScripts: true, }; - webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview); + webviewPanel.webview.html = getWebviewContent({ + webview: webviewPanel.webview, + script: "media/wysiwygWebview.js", + title: "Squiggle Editor", + context: this.context, + }); function updateWebview() { webviewPanel.webview.postMessage({ @@ -79,57 +73,6 @@ export class SquiggleEditorProvider implements vscode.CustomTextEditorProvider { updateWebview(); } - /** - * Get the static html used for the editor webviews. - */ - private getHtmlForWebview(webview: vscode.Webview): string { - // Local path to main script run in the webview - - const styleUri = webview.asWebviewUri( - vscode.Uri.joinPath( - this.context.extensionUri, - "media/vendor/components.css" - ) - ); - - const scriptUris = [ - // vendor files are copied over by `yarn run compile` - "media/vendor/react.js", - "media/vendor/react-dom.js", - "media/vendor/components.js", - "media/wysiwyg.js", - ].map((script) => - webview.asWebviewUri( - vscode.Uri.joinPath(this.context.extensionUri, script) - ) - ); - - // Use a nonce to whitelist which scripts can be run - const nonce = getNonce(); - - return /* html */ ` - - - - - - - - - Squiggle Editor - - -
- ${scriptUris - .map((uri) => ``) - .join("")} - - `; - } - private updateTextDocument(document: vscode.TextDocument, text: string) { const edit = new vscode.WorkspaceEdit(); diff --git a/packages/vscode-ext/src/extension.ts b/packages/vscode-ext/src/extension.ts index 9759472b..3b07fcfb 100644 --- a/packages/vscode-ext/src/extension.ts +++ b/packages/vscode-ext/src/extension.ts @@ -2,12 +2,15 @@ // Import the module and reference it with the alias vscode in your code below import * as vscode from "vscode"; -import { SquiggleEditorProvider } from "./squiggleEditor"; +import { SquiggleEditorProvider } from "./editor"; +import { registerPreviewCommand } from "./preview"; // this method is called when your extension is activated // your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(SquiggleEditorProvider.register(context)); + + registerPreviewCommand(context); } // this method is called when your extension is deactivated diff --git a/packages/vscode-ext/src/preview.ts b/packages/vscode-ext/src/preview.ts new file mode 100644 index 00000000..c6d3e212 --- /dev/null +++ b/packages/vscode-ext/src/preview.ts @@ -0,0 +1,51 @@ +import * as vscode from "vscode"; +import * as uri from "vscode-uri"; +import { getWebviewContent } from "./utils"; + +export const registerPreviewCommand = (context: vscode.ExtensionContext) => { + context.subscriptions.push( + vscode.commands.registerTextEditorCommand("squiggle.preview", (editor) => { + // Create and show a new webview + const title = `Preview ${uri.Utils.basename(editor.document.uri)}`; + + const panel = vscode.window.createWebviewPanel( + "squigglePreview", + title, + vscode.ViewColumn.Beside, + {} // Webview options. More on these later. + ); + + panel.webview.options = { + enableScripts: true, + }; + + panel.webview.html = getWebviewContent({ + context, + webview: panel.webview, + title, + script: "media/previewWebview.js", + }); + + const updateWebview = () => { + panel.webview.postMessage({ + type: "update", + text: editor.document.getText(), + }); + }; + + updateWebview(); + + const changeDocumentSubscription = + vscode.workspace.onDidChangeTextDocument((e) => { + if (e.document.uri.toString() === editor.document.uri.toString()) { + updateWebview(); + } + }); + + // Make sure we get rid of the listener when our editor is closed. + panel.onDidDispose(() => { + changeDocumentSubscription.dispose(); + }); + }) + ); +}; diff --git a/packages/vscode-ext/src/utils.ts b/packages/vscode-ext/src/utils.ts new file mode 100644 index 00000000..7c3f28f4 --- /dev/null +++ b/packages/vscode-ext/src/utils.ts @@ -0,0 +1,59 @@ +import * as vscode from "vscode"; + +const getNonce = () => { + let text = ""; + const possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +}; + +export const getWebviewContent = ({ + webview, + title, + script, + context, +}: { + webview: vscode.Webview; + title: string; + script: string; + context: vscode.ExtensionContext; +}) => { + const nonce = getNonce(); + + const styleUri = webview.asWebviewUri( + vscode.Uri.joinPath(context.extensionUri, "media/vendor/components.css") + ); + + const scriptUris = [ + // vendor files are copied over by `yarn run compile` + "media/vendor/react.js", + "media/vendor/react-dom.js", + "media/vendor/components.js", + script, + ].map((script) => + webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, script)) + ); + + return ` + + + + + + + + + ${title} + + +
+ ${scriptUris + .map((uri) => ``) + .join("")} + + + `; +}; diff --git a/yarn.lock b/yarn.lock index 158b9dfc..ff155b64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14791,10 +14791,10 @@ react-vega@^7.5.1: prop-types "^15.8.1" vega-embed "^6.5.1" -react@^18.0.0, react@^18.1.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== +react@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" + integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ== dependencies: loose-envify "^1.1.0" @@ -17894,6 +17894,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-uri@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" + integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA== + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"