├── .vscode ├── settings.json ├── extensions.json ├── tasks.json └── launch.json ├── icon.png ├── .gitignore ├── src ├── config.ts ├── utils.ts ├── dispose.ts ├── builder-jsx-lite-editor.ts └── extension.ts ├── README.md ├── tsconfig.json ├── media ├── reset.css ├── main.js └── vscode.css ├── .eslintrc.js ├── LICENSE └── package.json /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false 3 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuilderIO/vscode/main/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | Thumbs.db 4 | node_modules 5 | out 6 | */.vs/ -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const useDev = process.env.NODE_ENV === "development"; 2 | 3 | export const useBeta = true; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Builder.io for VSCode 2 | 3 | Visual coding in your IDE! 4 | 5 | [Grab the extension](https://marketplace.visualstudio.com/items?itemName=Builder.Builder) 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function getNonce() { 2 | let text = ""; 3 | const possible = 4 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 5 | for (let i = 0; i < 32; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | return text; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["ES2019"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": "src", 10 | "skipLibCheck": true 11 | }, 12 | "exclude": ["node_modules", ".vscode-test"] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "dbaeumer.vscode-eslint" 8 | ] 9 | } -------------------------------------------------------------------------------- /media/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-size: 13px; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | ol, 21 | ul { 22 | margin: 0; 23 | padding: 0; 24 | font-weight: normal; 25 | } 26 | 27 | img { 28 | max-width: 100%; 29 | height: auto; 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | } 20 | }; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "env": { 13 | "NODE_ENV": "development" 14 | }, 15 | "runtimeExecutable": "${execPath}", 16 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 17 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 18 | "preLaunchTask": "npm: watch" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/dispose.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export function disposeAll(disposables: vscode.Disposable[]): void { 4 | while (disposables.length) { 5 | const item = disposables.pop(); 6 | if (item) { 7 | item.dispose(); 8 | } 9 | } 10 | } 11 | 12 | export abstract class Disposable { 13 | private _isDisposed = false; 14 | 15 | protected _disposables: vscode.Disposable[] = []; 16 | 17 | public dispose(): any { 18 | if (this._isDisposed) { 19 | return; 20 | } 21 | this._isDisposed = true; 22 | disposeAll(this._disposables); 23 | } 24 | 25 | protected _register(value: T): T { 26 | if (this._isDisposed) { 27 | value.dispose(); 28 | } else { 29 | this._disposables.push(value); 30 | } 31 | return value; 32 | } 33 | 34 | protected get isDisposed(): boolean { 35 | return this._isDisposed; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Builder.io, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /media/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | alert("main.js -1"); 4 | 5 | // This script will be run within the webview itself 6 | // It cannot access the main VS Code APIs directly. 7 | (function () { 8 | console.log("main.js 0"); 9 | 10 | /** @type {HTMLIFrameElement} */ 11 | const frame = document.querySelector(".fiddle-frame"); 12 | 13 | console.log("main.js load", frame); 14 | 15 | frame.addEventListener("message", (e) => { 16 | const data = e.data; 17 | 18 | // eslint-disable-next-line no-undef 19 | console.log("message", data); 20 | 21 | if (data) { 22 | if (data.type === "builder.editorLoaded") { 23 | // Loaded - message down the data 24 | vscode.postMessage({ 25 | type: "builder.editorLoaded", 26 | }); 27 | } 28 | 29 | if (data.type === "builder.editorDataUpdated") { 30 | // Loaded - data updated 31 | vscode.postMessage({ 32 | type: "builder.editorDataUpdated", 33 | data: data.data, 34 | }); 35 | } 36 | } 37 | }); 38 | 39 | window.addEventListener("message", (e) => { 40 | const data = e.data; 41 | 42 | console.log("message", data, e); 43 | 44 | if (data) { 45 | if (data.type === "builder.textChanged") { 46 | frame.contentWindow.postMessage({ 47 | type: "builder.updateEditorData", 48 | data: { data: data.data.builderJson }, 49 | }); 50 | } 51 | } 52 | }); 53 | })(); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Builder", 3 | "description": "Builder.io integration for VSCode - turn designs into code!", 4 | "version": "0.0.8", 5 | "publisher": "builder", 6 | "engines": { 7 | "vscode": "^1.47.0" 8 | }, 9 | "categories": [ 10 | "Visualization", 11 | "Snippets" 12 | ], 13 | "icon": "icon.png", 14 | "galleryBanner": { 15 | "color": "207593" 16 | }, 17 | "activationEvents": [ 18 | "onCommand:builder.start", 19 | "onCommand:builder.doRefactor", 20 | "onWebviewPanel:builder", 21 | "onCommand:builder.openJsxLiteEditorToTheSide" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/BuilderIO/vscode.git" 26 | }, 27 | "main": "./out/extension.js", 28 | "contributes": { 29 | "commands": [ 30 | { 31 | "command": "builder.openJsxLiteEditorToTheSide", 32 | "title": "Open Builder.io visual editor to the side", 33 | "category": "Builder.io" 34 | }, 35 | { 36 | "command": "builder.start", 37 | "title": "Open Builder.io", 38 | "category": "Builder.io" 39 | } 40 | ], 41 | "customEditors": [ 42 | { 43 | "viewType": "builder.jsxLiteEditor", 44 | "displayName": "Builder.io Visual Editor", 45 | "selector": [ 46 | { 47 | "filenamePattern": "*.lite.*" 48 | } 49 | ], 50 | "priority": "option" 51 | } 52 | ] 53 | }, 54 | "scripts": { 55 | "vscode:prepublish": "npm run compile", 56 | "compile": "tsc -p ./", 57 | "lint": "eslint . --ext .ts,.tsx", 58 | "watch": "tsc -w -p ./" 59 | }, 60 | "dependencies": { 61 | "@jsx-lite/core": "0.0.9" 62 | }, 63 | "devDependencies": { 64 | "@typescript-eslint/eslint-plugin": "^3.0.2", 65 | "@typescript-eslint/parser": "^3.0.2", 66 | "eslint": "^7.1.0", 67 | "typescript": "^4.0.2", 68 | "@types/vscode": "^1.47.0", 69 | "@types/node": "^12.12.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /media/vscode.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --container-paddding: 20px; 3 | --input-padding-vertical: 6px; 4 | --input-padding-horizontal: 4px; 5 | --input-margin-vertical: 4px; 6 | --input-margin-horizontal: 0; 7 | } 8 | 9 | body { 10 | padding: 0 var(--container-paddding); 11 | color: var(--vscode-foreground); 12 | font-size: var(--vscode-font-size); 13 | font-weight: var(--vscode-font-weight); 14 | font-family: var(--vscode-font-family); 15 | background-color: var(--vscode-editor-background); 16 | } 17 | 18 | ol, 19 | ul { 20 | padding-left: var(--container-paddding); 21 | } 22 | 23 | body > *, 24 | form > * { 25 | margin-block-start: var(--input-margin-vertical); 26 | margin-block-end: var(--input-margin-vertical); 27 | } 28 | 29 | *:focus { 30 | outline-color: var(--vscode-focusBorder) !important; 31 | } 32 | 33 | a { 34 | color: var(--vscode-textLink-foreground); 35 | } 36 | 37 | a:hover, 38 | a:active { 39 | color: var(--vscode-textLink-activeForeground); 40 | } 41 | 42 | code { 43 | font-size: var(--vscode-editor-font-size); 44 | font-family: var(--vscode-editor-font-family); 45 | } 46 | 47 | button { 48 | border: none; 49 | padding: var(--input-padding-vertical) var(--input-padding-horizontal); 50 | width: 100%; 51 | text-align: center; 52 | outline: 1px solid transparent; 53 | outline-offset: 2px !important; 54 | color: var(--vscode-button-foreground); 55 | background: var(--vscode-button-background); 56 | } 57 | 58 | button:hover { 59 | cursor: pointer; 60 | background: var(--vscode-button-hoverBackground); 61 | } 62 | 63 | button:focus { 64 | outline-color: var(--vscode-focusBorder); 65 | } 66 | 67 | button.secondary { 68 | color: var(--vscode-button-secondaryForeground); 69 | background: var(--vscode-button-secondaryBackground); 70 | } 71 | 72 | button.secondary:hover { 73 | background: var(--vscode-button-secondaryHoverBackground); 74 | } 75 | 76 | input:not([type='checkbox']), 77 | textarea { 78 | display: block; 79 | width: 100%; 80 | border: none; 81 | font-family: var(--vscode-font-family); 82 | padding: var(--input-padding-vertical) var(--input-padding-horizontal); 83 | color: var(--vscode-input-foreground); 84 | outline-color: var(--vscode-input-border); 85 | background-color: var(--vscode-input-background); 86 | } 87 | 88 | input::placeholder, 89 | textarea::placeholder { 90 | color: var(--vscode-input-placeholderForeground); 91 | } 92 | 93 | body { 94 | overflow: hidden; 95 | } 96 | 97 | .fiddle-frame { 98 | border: none; 99 | position: absolute; 100 | top: -10%; 101 | left: -10%; 102 | right: -10%; 103 | bottom: -10%; 104 | width: 120%; 105 | height: 120%; 106 | transform: scale(0.8333); 107 | } -------------------------------------------------------------------------------- /src/builder-jsx-lite-editor.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as vscode from "vscode"; 3 | import { useDev } from "./config"; 4 | import { getNonce } from "./utils"; 5 | 6 | export class BuilderJSXLiteEditorProvider 7 | implements vscode.CustomTextEditorProvider { 8 | public static register(context: vscode.ExtensionContext): vscode.Disposable { 9 | const provider = new BuilderJSXLiteEditorProvider(context); 10 | const providerRegistration = vscode.window.registerCustomEditorProvider( 11 | BuilderJSXLiteEditorProvider.viewType, 12 | provider 13 | ); 14 | return providerRegistration; 15 | } 16 | 17 | private static readonly viewType = "builder.jsxLilteEditor"; 18 | 19 | constructor(private readonly context: vscode.ExtensionContext) {} 20 | 21 | /** 22 | * Called when our custom editor is opened. 23 | * 24 | * 25 | */ 26 | public async resolveCustomTextEditor( 27 | document: vscode.TextDocument, 28 | webviewPanel: vscode.WebviewPanel, 29 | _token: vscode.CancellationToken 30 | ): Promise { 31 | // Setup initial content for the webview 32 | webviewPanel.webview.options = { 33 | enableScripts: true, 34 | }; 35 | webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview); 36 | 37 | function updateWebview() { 38 | webviewPanel.webview.postMessage({ 39 | type: "update", 40 | text: document.getText(), 41 | }); 42 | } 43 | 44 | // Hook up event handlers so that we can synchronize the webview with the text document. 45 | // 46 | // The text document acts as our model, so we have to sync change in the document to our 47 | // editor and sync changes in the editor back to the document. 48 | // 49 | // Remember that a single text document can also be shared between multiple custom 50 | // editors (this happens for example when you split a custom editor) 51 | 52 | const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument( 53 | (e) => { 54 | if (e.document.uri.toString() === document.uri.toString()) { 55 | updateWebview(); 56 | } 57 | } 58 | ); 59 | 60 | // Make sure we get rid of the listener when our editor is closed. 61 | webviewPanel.onDidDispose(() => { 62 | changeDocumentSubscription.dispose(); 63 | }); 64 | 65 | // Receive message from the webview. 66 | webviewPanel.webview.onDidReceiveMessage((e) => { 67 | switch (e.type) { 68 | case "add": 69 | this.addNewScratch(document); 70 | return; 71 | 72 | case "delete": 73 | this.deleteScratch(document, e.id); 74 | return; 75 | } 76 | }); 77 | 78 | updateWebview(); 79 | } 80 | 81 | /** 82 | * Get the static html used for the editor webviews. 83 | */ 84 | private getHtmlForWebview(webview: vscode.Webview): string { 85 | // Local path to script and css for the webview 86 | const scriptUri = webview.asWebviewUri( 87 | vscode.Uri.file( 88 | path.join(this.context.extensionPath, "media", "catScratch.js") 89 | ) 90 | ); 91 | const styleResetUri = webview.asWebviewUri( 92 | vscode.Uri.file( 93 | path.join(this.context.extensionPath, "media", "reset.css") 94 | ) 95 | ); 96 | const styleVSCodeUri = webview.asWebviewUri( 97 | vscode.Uri.file( 98 | path.join(this.context.extensionPath, "media", "vscode.css") 99 | ) 100 | ); 101 | 102 | // Use a nonce to whitelist which scripts can be run 103 | const nonce = getNonce(); 104 | 105 | return /* html */ ` 106 | 107 | 108 | 109 | 110 | 111 | 115 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | Builder.io Editor 127 | 128 | 129 | 142 | 145 | 146 | 147 | 148 | `; 149 | } 150 | 151 | /** 152 | * Add a new scratch to the current document. 153 | */ 154 | private addNewScratch(document: vscode.TextDocument) { 155 | const json = this.getDocumentAsJson(document); 156 | const character = ""; 157 | json.scratches = [ 158 | ...(Array.isArray(json.scratches) ? json.scratches : []), 159 | { 160 | id: getNonce(), 161 | text: character, 162 | created: Date.now(), 163 | }, 164 | ]; 165 | 166 | return this.updateTextDocument(document, json); 167 | } 168 | 169 | /** 170 | * Delete an existing scratch from a document. 171 | */ 172 | private deleteScratch(document: vscode.TextDocument, id: string) { 173 | const json = this.getDocumentAsJson(document); 174 | if (!Array.isArray(json.scratches)) { 175 | return; 176 | } 177 | 178 | json.scratches = json.scratches.filter((note: any) => note.id !== id); 179 | 180 | return this.updateTextDocument(document, json); 181 | } 182 | 183 | /** 184 | * Try to get a current document as json text. 185 | */ 186 | private getDocumentAsJson(document: vscode.TextDocument): any { 187 | const text = document.getText(); 188 | if (text.trim().length === 0) { 189 | return {}; 190 | } 191 | 192 | try { 193 | return JSON.parse(text); 194 | } catch { 195 | throw new Error( 196 | "Could not get document as json. Content is not valid json" 197 | ); 198 | } 199 | } 200 | 201 | /** 202 | * Write out the json to a given document. 203 | */ 204 | private updateTextDocument(document: vscode.TextDocument, json: any) { 205 | const edit = new vscode.WorkspaceEdit(); 206 | 207 | // Just replace the entire document every time for this example extension. 208 | // A more complete extension should compute minimal edits instead. 209 | edit.replace( 210 | document.uri, 211 | new vscode.Range(0, 0, document.lineCount, 0), 212 | JSON.stringify(json, null, 2) 213 | ); 214 | 215 | return vscode.workspace.applyEdit(edit); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | builderContentToJsxLiteComponent, 3 | componentToBuilder, 4 | componentToJsxLite, 5 | parseJsx, 6 | } from "@jsx-lite/core"; 7 | import * as vscode from "vscode"; 8 | import { BuilderJSXLiteEditorProvider } from "./builder-jsx-lite-editor"; 9 | import { useBeta, useDev } from "./config"; 10 | 11 | export function activate(context: vscode.ExtensionContext) { 12 | context.subscriptions.push( 13 | vscode.commands.registerCommand("builder.start", () => { 14 | BuilderPanel.createOrShow(context.extensionUri); 15 | }) 16 | ); 17 | 18 | function openPreviewToTheSide(uri?: vscode.Uri) { 19 | let resource = uri; 20 | if (!(resource instanceof vscode.Uri)) { 21 | if (vscode.window.activeTextEditor) { 22 | // we are relaxed and don't check for markdown files 23 | resource = vscode.window.activeTextEditor.document.uri; 24 | } 25 | } 26 | BuilderPanel.openEditor(resource!, vscode.window.activeTextEditor!, { 27 | viewColumn: vscode.ViewColumn.Two, 28 | preserveFocus: true, 29 | }); 30 | } 31 | 32 | context.subscriptions.push( 33 | vscode.commands.registerCommand( 34 | "builder.openJsxLiteEditorToTheSide", 35 | openPreviewToTheSide 36 | ) 37 | ); 38 | 39 | context.subscriptions.push(BuilderJSXLiteEditorProvider.register(context)); 40 | 41 | if (vscode.window.registerWebviewPanelSerializer) { 42 | // Make sure we register a serializer in activation event 43 | vscode.window.registerWebviewPanelSerializer(BuilderPanel.viewType, { 44 | async deserializeWebviewPanel( 45 | webviewPanel: vscode.WebviewPanel, 46 | state: any 47 | ) { 48 | BuilderPanel.revive(webviewPanel, context.extensionUri); 49 | }, 50 | }); 51 | } 52 | } 53 | 54 | class BuilderPanel { 55 | /** 56 | * Track the currently panel. Only allow a single panel to exist at a time. 57 | */ 58 | public static currentPanel: BuilderPanel | undefined; 59 | 60 | public static readonly viewType = "catCoding"; 61 | 62 | private readonly _panel: vscode.WebviewPanel; 63 | private readonly _extensionUri: vscode.Uri; 64 | private _disposables: vscode.Disposable[] = []; 65 | 66 | public static openEditor( 67 | sourceUri: vscode.Uri, 68 | editor: vscode.TextEditor, 69 | viewOptions: { viewColumn: vscode.ViewColumn; preserveFocus?: boolean } 70 | ) { 71 | // Otherwise, create a new panel. 72 | const panel = vscode.window.createWebviewPanel( 73 | BuilderPanel.viewType, 74 | "Builder.io", 75 | viewOptions, 76 | { 77 | // Enable javascript in the webview 78 | enableScripts: true, 79 | 80 | // And restrict the webview to only loading content from our extension's `media` directory. 81 | localResourceRoots: [vscode.Uri.joinPath(sourceUri, "media")], 82 | } 83 | ); 84 | 85 | BuilderPanel.currentPanel = new BuilderPanel(panel, sourceUri, editor); 86 | } 87 | 88 | public static createOrShow(extensionUri: vscode.Uri) { 89 | const column = vscode.window.activeTextEditor 90 | ? vscode.window.activeTextEditor.viewColumn 91 | : undefined; 92 | 93 | // If we already have a panel, show it. 94 | if (BuilderPanel.currentPanel) { 95 | BuilderPanel.currentPanel._panel.reveal(column); 96 | return; 97 | } 98 | 99 | // Otherwise, create a new panel. 100 | const panel = vscode.window.createWebviewPanel( 101 | BuilderPanel.viewType, 102 | "Builder.io", 103 | column || vscode.ViewColumn.One, 104 | { 105 | // Enable javascript in the webview 106 | enableScripts: true, 107 | 108 | // And restrict the webview to only loading content from our extension's `media` directory. 109 | localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")], 110 | } 111 | ); 112 | 113 | BuilderPanel.currentPanel = new BuilderPanel(panel, extensionUri); 114 | } 115 | 116 | public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { 117 | BuilderPanel.currentPanel = new BuilderPanel(panel, extensionUri); 118 | } 119 | 120 | private constructor( 121 | panel: vscode.WebviewPanel, 122 | extensionUri: vscode.Uri, 123 | private editor?: vscode.TextEditor 124 | ) { 125 | this._panel = panel; 126 | this._extensionUri = extensionUri; 127 | 128 | // Set the webview's initial html content 129 | this._update(); 130 | 131 | // Listen for when the panel is disposed 132 | // This happens when the user closes the panel or when the panel is closed programatically 133 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables); 134 | 135 | // Handle messages from the webview 136 | this._panel.webview.onDidReceiveMessage( 137 | (message) => { 138 | switch (message.type) { 139 | case "builder.editorLoaded": { 140 | const text = this.editor!.document.getText(); 141 | 142 | const parsed = parseJsx(text); 143 | const builderContent = componentToBuilder(parsed); 144 | 145 | this._panel.webview.postMessage({ 146 | type: "builder.textChanged", 147 | data: { 148 | builderJson: builderContent, 149 | }, 150 | }); 151 | return; 152 | } 153 | case "builder.openWindow": { 154 | const url = message.data.url; 155 | 156 | vscode.env.openExternal(vscode.Uri.parse(url)); 157 | return; 158 | } 159 | 160 | case "builder.saveContent": { 161 | const content = message.data.content; 162 | if (typeof content?.data?.blocksString === "string") { 163 | content.data.blocks = JSON.parse(content.data.blocksString); 164 | delete content.data.blocksString; 165 | } 166 | 167 | const jsxLiteJson = builderContentToJsxLiteComponent(content); 168 | const jsxLite = componentToJsxLite(jsxLiteJson); 169 | 170 | const edit = new vscode.WorkspaceEdit(); 171 | const document = this.editor!.document; 172 | edit.replace( 173 | document.uri, 174 | new vscode.Range(0, 0, document.lineCount, 0), 175 | jsxLite 176 | ); 177 | 178 | vscode.workspace.applyEdit(edit); 179 | document.save(); 180 | return; 181 | } 182 | } 183 | }, 184 | null, 185 | this._disposables 186 | ); 187 | 188 | if (this.editor) { 189 | this._disposables.push( 190 | vscode.workspace.onDidChangeTextDocument((event) => { 191 | // const text = event.document.getText(); 192 | // const parsed = parseJsx(text); 193 | // const builderContent = componentToBuilder(parsed); 194 | // this._panel.webview.postMessage({ 195 | // type: "builder.textChanged", 196 | // data: { 197 | // builderJson: builderContent, 198 | // }, 199 | // }); 200 | }) 201 | ); 202 | this._disposables.push( 203 | vscode.workspace.onDidSaveTextDocument((document) => { 204 | const text = document.getText(); 205 | 206 | const parsed = parseJsx(text); 207 | const builderContent = componentToBuilder(parsed); 208 | 209 | this._panel.webview.postMessage({ 210 | type: "builder.textChanged", 211 | data: { 212 | builderJson: builderContent, 213 | }, 214 | }); 215 | console.info("save"); 216 | }) 217 | ); 218 | this._disposables.push( 219 | vscode.window.onDidChangeTextEditorSelection((event) => { 220 | // TODO: sync text cursor/selection with builder scroll/selection 221 | console.info("selection change", event); 222 | }) 223 | ); 224 | this._disposables.push( 225 | vscode.window.onDidChangeActiveTextEditor((event) => { 226 | console.info("active editor change", event); 227 | }) 228 | ); 229 | } 230 | } 231 | 232 | public doRefactor() { 233 | // Send a message to the webview webview. 234 | // You can send any JSON serializable data. 235 | this._panel.webview.postMessage({ command: "refactor" }); 236 | } 237 | 238 | public dispose() { 239 | BuilderPanel.currentPanel = undefined; 240 | 241 | // Clean up our resources 242 | this._panel.dispose(); 243 | 244 | while (this._disposables.length) { 245 | const x = this._disposables.pop(); 246 | if (x) { 247 | x.dispose(); 248 | } 249 | } 250 | } 251 | 252 | private _update() { 253 | const webview = this._panel.webview; 254 | 255 | this._updateWebview(webview); 256 | } 257 | 258 | private _updateWebview(webview: vscode.Webview) { 259 | this._panel.webview.html = this._getHtmlForWebview(webview); 260 | } 261 | 262 | private _getHtmlForWebview(webview: vscode.Webview) { 263 | // Local path to main script run in the webview 264 | const scriptPathOnDisk = vscode.Uri.joinPath( 265 | this._extensionUri, 266 | "media", 267 | "main.js" 268 | ); 269 | 270 | // And the uri we use to load this script in the webview 271 | const scriptUri = webview.asWebviewUri(scriptPathOnDisk); 272 | 273 | // Local path to css styles 274 | const styleResetPath = vscode.Uri.joinPath( 275 | this._extensionUri, 276 | "media", 277 | "reset.css" 278 | ); 279 | const stylesPathMainPath = vscode.Uri.joinPath( 280 | this._extensionUri, 281 | "media", 282 | "vscode.css" 283 | ); 284 | 285 | // Uri to load styles into webview 286 | const stylesResetUri = webview.asWebviewUri(styleResetPath); 287 | const stylesMainUri = webview.asWebviewUri(stylesPathMainPath); 288 | 289 | // Use a nonce to only allow specific scripts to be run 290 | const nonce = getNonce(); 291 | 292 | return ` 293 | 294 | 295 | 296 | 297 | 301 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | Builder.io 313 | 314 | 315 | 328 | 335 | 336 | 380 | 381 | `; 382 | } 383 | } 384 | 385 | function getNonce() { 386 | let text = ""; 387 | const possible = 388 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 389 | for (let i = 0; i < 32; i++) { 390 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 391 | } 392 | return text; 393 | } 394 | --------------------------------------------------------------------------------