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/.gitignore b/packages/vscode-ext/.gitignore index c712b96a..e1e96097 100644 --- a/packages/vscode-ext/.gitignore +++ b/packages/vscode-ext/.gitignore @@ -1,3 +1,4 @@ /media/vendor /out /*.vsix +/syntaxes/*.json diff --git a/packages/vscode-ext/README.md b/packages/vscode-ext/README.md index afedd282..9d23c1e2 100644 --- a/packages/vscode-ext/README.md +++ b/packages/vscode-ext/README.md @@ -4,6 +4,17 @@ _[marketplace](https://marketplace.visualstudio.com/items?itemName=QURI.vscode-s This extension provides support for [Squiggle](https://www.squiggle-language.com/) in VS Code. +Features: + +- Preview `.squiggle` files in a preview pane +- Syntax highlighting for `.squiggle` and `.squiggleU` files + +# Configuration + +Some preview settings, e.g. whether to show the summary table or types of outputs, can be configurable on in the VS Code settings and persist between different preview sessions. + +Check out the full list of Squiggle settings in the main VS Code settings. + # Build locally We assume you ran `yarn` at the monorepo level for all dependencies. diff --git a/packages/vscode-ext/language-configuration.json b/packages/vscode-ext/language-configuration.json new file mode 100644 index 00000000..727d0444 --- /dev/null +++ b/packages/vscode-ext/language-configuration.json @@ -0,0 +1,18 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "\"", "close": "\"", "notIn": ["string", "comment"] } + ] +} diff --git a/packages/vscode-ext/media/previewWebview.js b/packages/vscode-ext/media/previewWebview.js new file mode 100644 index 00000000..556a893b --- /dev/null +++ b/packages/vscode-ext/media/previewWebview.js @@ -0,0 +1,41 @@ +(function () { + const vscode = acquireVsCodeApi(); + + const container = document.getElementById("root"); + + const root = ReactDOM.createRoot(container); + function updateContent(text, showSettings) { + root.render( + React.createElement(squiggle_components.SquigglePlayground, { + code: text, + showEditor: false, + showTypes: Boolean(showSettings.showTypes), + showControls: Boolean(showSettings.showControls), + showSummary: Boolean(showSettings.showSummary), + }) + ); + } + + // 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, showSettings } = message; + + // Update our webview's content + updateContent(text, showSettings); + + // Then persist state information. + // This state is returned in the call to `vscode.getState` below when a webview is reloaded. + vscode.setState({ text, showSettings }); + + return; + } + }); + + const state = vscode.getState(); + if (state) { + updateContent(state.text, state.showSettings); + } +})(); 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..af70abda 100644 --- a/packages/vscode-ext/package.json +++ b/packages/vscode-ext/package.json @@ -3,7 +3,7 @@ "displayName": "Squiggle", "description": "Squiggle language support", "license": "MIT", - "version": "0.0.4", + "version": "0.1.2", "publisher": "QURI", "repository": { "type": "git", @@ -18,10 +18,45 @@ "Visualization" ], "activationEvents": [ - "onCustomEditor:squiggle.wysiwyg" + "onCustomEditor:squiggle.wysiwyg", + "onCommand:squiggle.preview" ], "main": "./out/extension.js", "contributes": { + "languages": [ + { + "id": "squiggle", + "extensions": [ + ".squiggle" + ], + "aliases": [ + "Squiggle" + ], + "configuration": "./language-configuration.json" + }, + { + "id": "squiggleU", + "extensions": [ + ".squiggleU" + ], + "aliases": [ + "SquiggleU" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "squiggle", + "scopeName": "source.squiggle", + "path": "./syntaxes/squiggle.tmLanguage.json" + }, + { + "language": "squiggleU", + "scopeName": "source.squiggle", + "path": "./syntaxes/squiggle.tmLanguage.json" + } + ], "customEditors": [ { "viewType": "squiggle.wysiwyg", @@ -31,29 +66,82 @@ "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" + } + ], + "configuration": { + "title": "Squiggle", + "properties": { + "squiggle.playground.showTypes": { + "type": "boolean", + "default": false, + "description": "Whether to show the types of outputs in the playground" + }, + "squiggle.playground.showControls": { + "type": "boolean", + "default": false, + "description": "Whether to show the log scale controls in the playground" + }, + "squiggle.playground.showSummary": { + "type": "boolean", + "default": false, + "description": "Whether to show the summary table in the playground" + } + } + } }, "scripts": { "vscode:prepublish": "yarn run compile", "compile:tsc": "tsc -p ./", + "compile:grammar": "js-yaml syntaxes/squiggle.tmLanguage.yaml >syntaxes/squiggle.tmLanguage.json", "compile:vendor": "(cd ../squiggle-lang && yarn run build) && (cd ../components && yarn run bundle && yarn run build:css) && mkdir -p media/vendor && cp ../components/dist/bundle.js media/vendor/components.js && cp ../components/dist/main.css media/vendor/components.css && cp ../../node_modules/react/umd/react.production.min.js media/vendor/react.js && cp ../../node_modules/react-dom/umd/react-dom.production.min.js media/vendor/react-dom.js && cp ../website/static/img/quri-logo.png media/vendor/icon.png", - "compile": "yarn run compile:tsc && yarn run compile:vendor", + "compile": "yarn run compile:tsc && yarn run compile:grammar && yarn run compile:vendor", "watch": "tsc -watch -p ./", "pretest": "yarn run compile && yarn run lint", "lint": "eslint src --ext ts", "format": "eslint src --ext ts --fix" }, "devDependencies": { - "@types/vscode": "^1.68.0", "@types/glob": "^7.2.0", "@types/node": "18.x", + "@types/vscode": "^1.68.0", "@typescript-eslint/eslint-plugin": "^5.27.0", "@typescript-eslint/parser": "^5.27.0", "eslint": "^8.18.0", "glob": "^8.0.3", + "js-yaml": "^4.1.0", "typescript": "^4.7.4" } } 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 */ ` - - -
- - - - - -