├── .eslintrc.json ├── .github ├── copilot-workspace │ └── CONTRIBUTING.md └── workflows │ └── release.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── global.d.ts ├── images └── icon.png ├── package-lock.json ├── package.json ├── src ├── abstractions │ ├── node │ │ └── sass.ts │ └── web │ │ └── sass.ts ├── ai │ ├── index.ts │ ├── preamble.txt │ └── storage.ts ├── api.ts ├── config.ts ├── constants.ts ├── creation │ ├── galleryProvider.ts │ ├── index.ts │ └── storage.ts ├── extension.ts ├── liveShare │ ├── guestService.ts │ ├── hostService.ts │ ├── index.ts │ └── service.ts ├── preview │ ├── codepen.ts │ ├── commands.ts │ ├── index.ts │ ├── languages │ │ ├── components │ │ │ ├── react.ts │ │ │ ├── svelte.ts │ │ │ └── vue.ts │ │ ├── go │ │ │ └── index.ts │ │ ├── languageProvider.ts │ │ ├── markup.ts │ │ ├── readme.ts │ │ ├── script.ts │ │ └── stylesheet.ts │ ├── layoutManager.ts │ ├── libraries │ │ ├── cdnjs.ts │ │ ├── index.ts │ │ └── skypack.ts │ ├── proxyFileSystemProvider.ts │ ├── stylesheets │ │ ├── stylesheets.d.ts │ │ └── themeStyles.css │ ├── tour.ts │ ├── tree │ │ ├── activeSwing.ts │ │ ├── commands.ts │ │ ├── index.ts │ │ └── nodes.ts │ ├── tutorials │ │ ├── index.ts │ │ ├── inputFileSystem.ts │ │ └── storage.ts │ └── webview.ts ├── store.ts └── utils.ts ├── templates ├── basic.json ├── basic.yml ├── components.json ├── components.yml ├── go.json ├── go.yml ├── languages.json └── languages.yml ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": "warn", 11 | "@typescript-eslint/semi": "warn", 12 | "curly": "warn", 13 | "eqeqeq": "warn", 14 | "no-throw-literal": "warn", 15 | "semi": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/copilot-workspace/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | * Anytime a user-facing change is made, make sure to update the CHANGELOG.md file with a note about it 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | Release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Install Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 14.x 18 | - run: npm install 19 | - name: Publish to VS Marketplace 20 | run: npx vsce publish 21 | env: 22 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 9 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 10 | "preLaunchTask": "${defaultBuildTask}" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": "explicit", 4 | "source.fixAll": "explicit" 5 | }, 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "files.exclude": { 9 | "out": false 10 | }, 11 | "search.exclude": { 12 | "out": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | .vscode/** 3 | src/** 4 | .gitignore 5 | **/tsconfig.json 6 | **/.eslintrc.json 7 | **/*.map 8 | **/*.ts 9 | node_modules 10 | webpack.config.js 11 | templates 12 | 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.0.25 (09/07/24) 2 | 3 | - Fixed some bugs with the latest version of VS Code 4 | 5 | # v0.0.24 (05/14/24) 6 | 7 | - Added support for reflecting changes to `window.title` in the title of the CodeSwing preview tab 8 | - Updated the default AI model to `gpt-4o` (which is much cheaper than Turbo) 9 | - Removed the logic that auto-adds `react` and `react-dom` to the `scripts` arrow in your `codeswing.json` file. 10 | 11 | # v0.0.23 (03/03/24) 12 | 13 | - Introduced the ability to generate and refine swings using an AI prompt (after configuring an OpenAI key) 14 | - Fixed a bug in searching/adding modules to a JavaScript file 15 | - Fixed a bug that prevented exporting swings to CodePen if they included an ` ` in a file 16 | - Fixed some bugs with importing CSS files, and using React 17 | 18 | # v0.0.22 (03/05/22) 19 | 20 | - Markup files can now be named `main.*` in addition to `App.*` and `index.*` 21 | - Added experimental support for Go swings, which include a `main.go` file 22 | - Added a new `Go` template gallery, that is disabled by default 23 | 24 | # v0.0.21 (09/19/21) 25 | 26 | - Introduced a new `CodeSwing: Launch Behavior` setting, that allows customizing how CodeSwing should behave when you open a swing workspace. 27 | - Introduced a new `CodeSwing: Root Directory` setting, that allows specifying the workspace directory that new swings should be created in 28 | - Introduced a new `CodeSwing: Initialize Workspace as Swing` command, that allows you to open a folder and turn it into a swing 29 | - Added support for calling `window.open()` on HTTP(S) URLs 30 | - The swing library selector now properly filters out CommonJS/ESM modules and source maps when adding a JavaScript library 31 | - The preview window is now automatically re-run when you upload a file to the active swing 32 | 33 | # v0.0.20 (07/08/21) 34 | 35 | - You can now use HTML in `index.md` files, when using Markdown as your markup format 36 | - Swings can now be open even when you don't have a workspace open 37 | - Fixed a bug with using CodeSwing in Safari 38 | 39 | # v0.0.19 (07/02/21) 40 | 41 | - Fixed formatting of the tutorial navigation header 42 | - Removed the timeout from tutorial step navigation 43 | - Removed the `CodeSwing: Temp Directory` setting 44 | 45 | # v0.0.18 (07/01/21) 46 | 47 | - You can now `import` `*.json` and `*.css` files from a JavaScript module 48 | 49 | # v0.0.17 (07/01/21) 50 | 51 | - You can now `import` `*.jsx` and `*.tsx` files from a JavaScript module 52 | - Enabling "run on edit" for all file types 53 | - Introduced the new `CodeSwing: Theme Preview` setting, which allows you to theme the swing preview window, to match your VS Code color theme 54 | 55 | # v0.0.16 (03/02/2021) 56 | 57 | - Added support for React Native web 🚀 58 | - Markup files/components can now be named `App.` in addition to `index.` 59 | 60 | # v0.0.15 (02/27/2021) 61 | 62 | - Introduced an MRU for templates, so that the last three templates you used show up at the top of the list 63 | - Introduced a new `CodeSwing: New Swing from Last Template` command, that creates a swing from your last used template 64 | - Renamed the `CodeSwing: New Scratch Swing...` command to `CodeSwing: New Swing...`, and `CodeSwing: New Swing..` to `CodeSwing: New Swing in Directory...` 65 | - Introduced a new `CodeSwing: Save Current Swing As...` command, that lets you save the current swing in a specific location 66 | - Added the `CodeSwing: Open Swing in New Window...` command 67 | 68 | # v0.0.14 (02/21/2021) 69 | 70 | - File extensions can now be renamed and immediately edited (e.g. `.js` -> `.ts`, `.css` -> `.scss`) 71 | - Fixed an issue with explicitly importing `react` from within a React component swing 72 | 73 | # v0.0.13 (02/19/2021) 74 | 75 | - Supporting NPM imports in import'd files 76 | - Being able to import Svelte/Vue components 77 | 78 | # v0.0.12 (02/19/2021) 79 | 80 | - You can now use the `@import` and `@use` statements in Sass files (file-based swings only) 81 | - Added the ability to upload local files to a swing 82 | - Your `style.css` and `script.js` files can now be explicitly linked from your `index.html` file, without breaking the run-on-type behavior. 83 | 84 | # v0.0.11 (02/16/2021) 85 | 86 | - You can now add/rename/delete files from the CodeSwing tree (including files within sub-directories) 87 | - NPM modules can now be `import`'d into React/Svelte/Vue components or script modules 88 | - Added support for using TypeScript and Scss/Sass within Svelte components 89 | 90 | # v0.0.10 (02/14/2021) 91 | 92 | - Introduced support for React/Svelte/Vue component-based swings 93 | 94 | # v0.0.9 (02/12/2021) 95 | 96 | - Added a keybinding for running a swing via `cmd+shift+b` (macOS/Linux) and `ctrl+shift+b` (Windows) 97 | - Fixed a bug with creating swings from a user-defined template 98 | 99 | # v0.0.8 (01/26/2021) 100 | 101 | - Fixed a bug with tutorial navigation 102 | - Optimized the extension to only activate when needed 103 | 104 | # v0.0.7 (01/02/2021) 105 | 106 | - Added support for the `fetch` API, in addition to the existing support for `XMLHttpRequest` 107 | - Introduced the `CodeSwing: Clear Console on Run` setting (defaults to `true`) 108 | 109 | # v0.0.6 (12/30/2020) 110 | 111 | - Fixed a couple of bugs that impacted the swing experience on Windows 112 | - The extension is now bundled with Webpack in order to improve peformance and reduce file size 113 | 114 | # v0.0.5 (12/28/2020) 115 | 116 | - Added initial Live Share support for workspace swings 117 | - Added support for exporting swings to CodePen via the new `CodeSwing: Export to CodePen` command 118 | - Added support for adding JavaScript module imports from Skypack 119 | - Temporary swings were renamed to "scratch swings", and are now stored in the temp directory instead of in-memory, and you can configure the location to write them to 120 | 121 | # v0.0.4 (12/19/2020) 122 | 123 | - Added the `CodeSwing: Open Workspace Swing` command, for re-opening the current workspace's swing after closing it. 124 | 125 | # v0.0.3 (12/18/2020) 126 | 127 | - Changed the default value of the `CodeSwing: Readme Behavior` setting to `none` 128 | - Added support for auto-closing the side-bar when the opened workspace is a swing 129 | 130 | # v0.0.2 (12/18/2020) 131 | 132 | - The panel area is now automatically closed when opening a new swing 133 | 134 | # v0.0.1 (12/18/2020) 135 | 136 | Initial release 🚀 137 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Jonathan Carter and Contributors. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.txt" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostintangent/codeswing/96060049ee9795cc6dc92fb60274072d22ed815e/images/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeswing", 3 | "displayName": "CodeSwing", 4 | "publisher": "codespaces-contrib", 5 | "description": "Interactive coding playground for building web applications (aka swings).", 6 | "version": "0.0.25", 7 | "engines": { 8 | "vscode": "^1.75.0" 9 | }, 10 | "categories": [ 11 | "Other" 12 | ], 13 | "icon": "images/icon.png", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/lostintangent/codeswing" 17 | }, 18 | "extensionKind": [ 19 | "ui", 20 | "workspace" 21 | ], 22 | "activationEvents": [ 23 | "workspaceContains:codeswing.json", 24 | "onFileSystem:vscode-vfs", 25 | "onStartupFinished" 26 | ], 27 | "main": "./dist/extension.js", 28 | "browser": "./dist/extension-web.js", 29 | "contributes": { 30 | "commands": [ 31 | { 32 | "command": "codeswing.addLibrary", 33 | "title": "Add Library", 34 | "category": "CodeSwing", 35 | "icon": "$(library)" 36 | }, 37 | { 38 | "command": "codeswing.addSwingFile", 39 | "title": "Add File", 40 | "icon": "$(add)" 41 | }, 42 | { 43 | "command": "codeswing.changeLayout", 44 | "title": "Change Layout", 45 | "category": "CodeSwing", 46 | "icon": "$(editor-layout)" 47 | }, 48 | { 49 | "command": "codeswing.clearOpenAiApiKey", 50 | "title": "Clear OpenAI API Key", 51 | "category": "CodeSwing" 52 | }, 53 | { 54 | "command": "codeswing.deleteSwingFile", 55 | "title": "Delete File" 56 | }, 57 | { 58 | "command": "codeswing.exportToCodePen", 59 | "title": "Export to CodePen", 60 | "category": "CodeSwing" 61 | }, 62 | { 63 | "command": "codeswing.initializeWorkspace", 64 | "title": "Initialize Workspace as Swing", 65 | "category": "CodeSwing" 66 | }, 67 | { 68 | "command": "codeswing.newSwingDirectory", 69 | "title": "New Swing in Directory...", 70 | "category": "CodeSwing" 71 | }, 72 | { 73 | "command": "codeswing.newSwing", 74 | "title": "New Swing...", 75 | "category": "CodeSwing" 76 | }, 77 | { 78 | "command": "codeswing.newSwingFromLastTemplate", 79 | "title": "New Swing from Last Template", 80 | "category": "CodeSwing" 81 | }, 82 | { 83 | "command": "codeswing.openConsole", 84 | "title": "Open Console", 85 | "category": "CodeSwing", 86 | "icon": "$(terminal)" 87 | }, 88 | { 89 | "command": "codeswing.openDeveloperTools", 90 | "title": "Open Dev Tools", 91 | "category": "CodeSwing", 92 | "icon": "$(tools)" 93 | }, 94 | { 95 | "command": "codeswing.openSwing", 96 | "title": "Open Swing...", 97 | "category": "CodeSwing" 98 | }, 99 | { 100 | "command": "codeswing.openSwingInNewWindow", 101 | "title": "Open Swing in New Window...", 102 | "category": "CodeSwing" 103 | }, 104 | { 105 | "command": "codeswing.openWorkspaceSwing", 106 | "title": "Re-Open Workspace Swing", 107 | "category": "CodeSwing" 108 | }, 109 | { 110 | "command": "codeswing.recordCodeTour", 111 | "title": "Record CodeTour" 112 | }, 113 | { 114 | "command": "codeswing.renameSwingFile", 115 | "title": "Rename File" 116 | }, 117 | { 118 | "command": "codeswing.run", 119 | "title": "Run Swing", 120 | "category": "CodeSwing", 121 | "icon": "$(play)" 122 | }, 123 | { 124 | "command": "codeswing.refineWithAI", 125 | "title": "Refine with AI", 126 | "category": "CodeSwing", 127 | "icon": "$(sparkle)" 128 | }, 129 | { 130 | "command": "codeswing.saveCurrentSwing", 131 | "title": "Save Current Swing As...", 132 | "category": "CodeSwing" 133 | }, 134 | { 135 | "command": "codeswing.setOpenAiApiKey", 136 | "title": "Set OpenAI API Key", 137 | "category": "CodeSwing" 138 | }, 139 | { 140 | "command": "codeswing.uploadSwingFile", 141 | "title": "Upload File(s)", 142 | "icon": "$(cloud-upload)" 143 | } 144 | ], 145 | "views": { 146 | "explorer": [ 147 | { 148 | "id": "codeswing.activeSwing", 149 | "name": "CodeSwing", 150 | "when": "codeswing:inSwing && !codeswing:inSwingWorkspace" 151 | } 152 | ] 153 | }, 154 | "menus": { 155 | "commandPalette": [ 156 | { 157 | "command": "codeswing.addLibrary", 158 | "when": "codeswing:inSwing" 159 | }, 160 | { 161 | "command": "codeswing.changeLayout", 162 | "when": "codeswing:inSwing" 163 | }, 164 | { 165 | "command": "codeswing.clearOpenAiApiKey", 166 | "when": "codeswing:hasOpenAiApiKey" 167 | }, 168 | { 169 | "command": "codeswing.exportToCodePen", 170 | "when": "codeswing:inSwing" 171 | }, 172 | { 173 | "command": "codeswing.initializeWorkspace", 174 | "when": "!codeswing:inSwingWorkspace && !codeswing:inSwing" 175 | }, 176 | { 177 | "command": "codeswing.openConsole", 178 | "when": "codeswing:inSwing" 179 | }, 180 | { 181 | "command": "codeswing.newSwingFromLastTemplate", 182 | "when": "codeswing:hasTemplateMRU" 183 | }, 184 | { 185 | "command": "codeswing.openDeveloperTools", 186 | "when": "codeswing:inSwing && !isWeb" 187 | }, 188 | { 189 | "command": "codeswing.openWorkspaceSwing", 190 | "when": "codeswing:inSwingWorkspace && !codeswing:inSwing" 191 | }, 192 | { 193 | "command": "codeswing.run", 194 | "when": "codeswing:inSwing" 195 | }, 196 | { 197 | "command": "codeswing.refineWithAI", 198 | "when": "codeswing:inSwing && codeswing:hasOpenAiApiKey" 199 | }, 200 | { 201 | "command": "codeswing.saveCurrentSwing", 202 | "when": "codeswing:inSwing" 203 | }, 204 | { 205 | "command": "codeswing.addSwingFile", 206 | "when": "false" 207 | }, 208 | { 209 | "command": "codeswing.deleteSwingFile", 210 | "when": "false" 211 | }, 212 | { 213 | "command": "codeswing.recordCodeTour", 214 | "when": "false" 215 | }, 216 | { 217 | "command": "codeswing.renameSwingFile", 218 | "when": "false" 219 | }, 220 | { 221 | "command": "codeswing.uploadSwingFile", 222 | "when": "false" 223 | } 224 | ], 225 | "editor/title": [ 226 | { 227 | "command": "codeswing.refineWithAI", 228 | "when": "codeswing:inSwing && codeswing:hasOpenAiApiKey", 229 | "group": "navigation@1" 230 | }, 231 | { 232 | "command": "codeswing.run", 233 | "when": "codeswing:inSwing", 234 | "group": "navigation@1" 235 | }, 236 | { 237 | "command": "codeswing.openConsole", 238 | "when": "codeswing:inSwing", 239 | "group": "navigation@2" 240 | }, 241 | { 242 | "command": "codeswing.changeLayout", 243 | "when": "codeswing:inSwing", 244 | "group": "navigation@3" 245 | }, 246 | { 247 | "command": "codeswing.addLibrary", 248 | "when": "codeswing:inSwing", 249 | "group": "navigation@4" 250 | }, 251 | { 252 | "command": "codeswing.openDeveloperTools", 253 | "when": "codeswing:inSwing && !isWeb", 254 | "group": "navigation@5" 255 | }, 256 | { 257 | "command": "codeswing.recordCodeTour", 258 | "when": "codeswing:inSwing && codeswing:codeTourEnabled", 259 | "group": "codetour@1" 260 | } 261 | ], 262 | "view/title": [ 263 | { 264 | "command": "codeswing.uploadSwingFile", 265 | "when": "view == codeswing.activeSwing", 266 | "group": "navigation@1" 267 | }, 268 | { 269 | "command": "codeswing.addSwingFile", 270 | "when": "view == codeswing.activeSwing", 271 | "group": "navigation@2" 272 | } 273 | ], 274 | "view/item/context": [ 275 | { 276 | "command": "codeswing.addSwingFile", 277 | "when": "viewItem == swing.directory", 278 | "group": "mutation@1" 279 | }, 280 | { 281 | "command": "codeswing.uploadSwingFile", 282 | "when": "viewItem == swing.directory", 283 | "group": "mutation@2" 284 | }, 285 | { 286 | "command": "codeswing.renameSwingFile", 287 | "when": "viewItem == swing.file", 288 | "group": "mutation@1" 289 | }, 290 | { 291 | "command": "codeswing.deleteSwingFile", 292 | "when": "viewItem == swing.file", 293 | "group": "mutation@2" 294 | } 295 | ] 296 | }, 297 | "jsonValidation": [ 298 | { 299 | "fileMatch": "codeswing.json", 300 | "url": "https://gist.githubusercontent.com/lostintangent/21727eab0d79c7b9fd0dde92df7b1f50/raw/schema.json" 301 | }, 302 | { 303 | "fileMatch": "gallery.json", 304 | "url": "https://gist.githubusercontent.com/lostintangent/091c0eec1f6443b526566d1cd3a85294/raw/schema.json" 305 | } 306 | ], 307 | "configuration": { 308 | "type": "object", 309 | "title": "CodeSwing", 310 | "properties": { 311 | "codeswing.autoRun": { 312 | "default": "onEdit", 313 | "enum": [ 314 | "onEdit", 315 | "onSave", 316 | "never" 317 | ], 318 | "description": "Specifies when to automatically run the code for a swing." 319 | }, 320 | "codeswing.autoSave": { 321 | "default": false, 322 | "type": "boolean", 323 | "description": "Specifies whether to automatically save your swing files (every 30s)." 324 | }, 325 | "codeswing.clearConsoleOnRun": { 326 | "default": true, 327 | "type": "boolean", 328 | "description": "Specifies whether to automatically clear the console when running a swing." 329 | }, 330 | "codeswing.launchBehavior": { 331 | "default": "openSwing", 332 | "enum": [ 333 | "newSwing", 334 | "none", 335 | "openSwing" 336 | ], 337 | "description": "Specifies how CodeSwing should behave when you open a swing workspace." 338 | }, 339 | "codeswing.layout": { 340 | "default": "splitLeft", 341 | "enum": [ 342 | "grid", 343 | "preview", 344 | "splitBottom", 345 | "splitLeft", 346 | "splitLeftTabbed", 347 | "splitRight", 348 | "splitRightTabbed", 349 | "splitTop" 350 | ], 351 | "description": "Specifies how to layout the editor windows when opening a swing." 352 | }, 353 | "codeswing.readmeBehavior": { 354 | "default": "none", 355 | "enum": [ 356 | "none", 357 | "previewFooter", 358 | "previewHeader" 359 | ], 360 | "description": "Specifies how the swing's readme content should be displayed." 361 | }, 362 | "codeswing.rootDirectory": { 363 | "default": null, 364 | "type": "string", 365 | "description": "Specifies the directory to create swings in within the open workspace." 366 | }, 367 | "codeswing.showConsole": { 368 | "default": false, 369 | "type": "boolean", 370 | "description": "Specifies whether to automatically show the console when opening a swing." 371 | }, 372 | "codeswing.templateGalleries": { 373 | "default": [ 374 | "web:basic", 375 | "web:components", 376 | "web:languages" 377 | ], 378 | "type": "array", 379 | "items": { 380 | "anyOf": [ 381 | { 382 | "type": "string", 383 | "enum": [ 384 | "web:basic", 385 | "web:components", 386 | "web:languages" 387 | ] 388 | }, 389 | { 390 | "type": "string", 391 | "format": "uri" 392 | } 393 | ] 394 | }, 395 | "description": "Specifies one or more URLs that point of template galleries for creating swings." 396 | }, 397 | "codeswing.themePreview": { 398 | "default": false, 399 | "type": "boolean", 400 | "description": "Specifies whether to apply Visual Studio Code theme to the preview window." 401 | }, 402 | "codeswing.ai.endpointUrl": { 403 | "default": null, 404 | "type": "string", 405 | "description": "Specifies the Azure OpenAI endpoint to use for AI generation." 406 | }, 407 | "codeswing.ai.model": { 408 | "default": "gpt-4o", 409 | "type": "string", 410 | "description": "Specifies the OpenAI model to use for AI generation." 411 | } 412 | } 413 | }, 414 | "languages": [ 415 | { 416 | "id": "typescriptreact", 417 | "filenames": [ 418 | "script.babel" 419 | ] 420 | }, 421 | { 422 | "id": "yaml", 423 | "filenames": [ 424 | ".block" 425 | ] 426 | } 427 | ], 428 | "keybindings": [ 429 | { 430 | "command": "codeswing.run", 431 | "when": "codeswing:inSwing", 432 | "key": "cmd+shift+b", 433 | "win": "ctrl+shift+b" 434 | } 435 | ], 436 | "codeswing.templateGalleries": [ 437 | { 438 | "id": "web:basic", 439 | "url": "https://cdn.jsdelivr.net/gh/lostintangent/codeswing@HEAD/templates/basic.json" 440 | }, 441 | { 442 | "id": "web:languages", 443 | "url": "https://cdn.jsdelivr.net/gh/lostintangent/codeswing@HEAD/templates/languages.json" 444 | }, 445 | { 446 | "id": "web:components", 447 | "url": "https://cdn.jsdelivr.net/gh/lostintangent/codeswing@main/templates/components.json" 448 | }, 449 | { 450 | "id": "go", 451 | "url": "https://cdn.jsdelivr.net/gh/lostintangent/codeswing@main/templates/go.json" 452 | } 453 | ] 454 | }, 455 | "scripts": { 456 | "vscode:prepublish": "node --max-old-space-size=8192 node_modules/webpack/bin/webpack.js", 457 | "compile": "tsc -p ./", 458 | "watch": "tsc -watch -p ./", 459 | "lint": "eslint src --ext ts", 460 | "package": "vsce package" 461 | }, 462 | "devDependencies": { 463 | "@types/debounce": "^1.2.0", 464 | "@types/glob": "^7.1.3", 465 | "@types/node": "^12.11.7", 466 | "@types/sass": "^1.16.1", 467 | "@types/vscode": "^1.75.0", 468 | "@typescript-eslint/eslint-plugin": "^4.1.1", 469 | "@typescript-eslint/parser": "^4.1.1", 470 | "eslint": "^7.9.0", 471 | "glob": "^10.3.10", 472 | "markdown-it": "^14.0.0", 473 | "raw-loader": "^4.0.2", 474 | "ts-loader": "^9.5.1", 475 | "typescript": "^5.3.3", 476 | "vsce": "^1.95.0", 477 | "webpack": "^5.90.3", 478 | "webpack-cli": "^5.1.4" 479 | }, 480 | "dependencies": { 481 | "@azure/openai": "^1.0.0-beta.11", 482 | "@vue/component-compiler": "^4.2.3", 483 | "axios": "^1.6.7", 484 | "case": "^1.6.3", 485 | "dayjs": "^1.11.10", 486 | "debounce": "^2.0.0", 487 | "form-data": "^3.0.0", 488 | "less": "^3.12.2", 489 | "mobx": "^6.13.1", 490 | "os-browserify": "^0.3.0", 491 | "path-browserify": "^1.0.1", 492 | "postcss": "^8.2.6", 493 | "pug": "^3.0.0", 494 | "sass": "^1.32.7", 495 | "sass.js": "^0.11.1", 496 | "stream-browserify": "^3.0.0", 497 | "svelte": "^4.1.2", 498 | "typescript": "^5.3.3", 499 | "vsls": "^1.0.3015", 500 | "vue-template-compiler": "^2.7.14" 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /src/abstractions/node/sass.ts: -------------------------------------------------------------------------------- 1 | import * as sass from "sass"; 2 | import { Uri } from "vscode"; 3 | 4 | export async function compile( 5 | content: string, 6 | indentedSyntax: boolean, 7 | importUri: Uri 8 | ) { 9 | const { css } = sass.renderSync({ 10 | data: content, 11 | indentedSyntax, 12 | includePaths: [importUri.path], 13 | }); 14 | 15 | return css; 16 | } 17 | -------------------------------------------------------------------------------- /src/abstractions/web/sass.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | const Sass = require("sass.js"); 3 | 4 | export async function compile( 5 | content: string, 6 | indentedSyntax: boolean, 7 | importUri: vscode.Uri 8 | ) { 9 | Sass.importer(async (request: any, done: any) => { 10 | if (request.path) { 11 | done(); 12 | } else if (request.current) { 13 | const fileExtension = indentedSyntax ? ".sass" : ".scss"; 14 | if (!request.current.endsWith(fileExtension)) { 15 | request.current += fileExtension; 16 | } 17 | 18 | const uri = vscode.Uri.joinPath(importUri, request.current); 19 | const content = await vscode.workspace.fs.readFile(uri); 20 | 21 | done({ 22 | content, 23 | }); 24 | } 25 | }); 26 | 27 | return new Promise((resolve) => { 28 | Sass.compile(content, { indentedSyntax }, (result: any) => 29 | resolve(result.text) 30 | ); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/ai/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AzureKeyCredential, 3 | OpenAIClient, 4 | OpenAIKeyCredential, 5 | } from "@azure/openai"; 6 | import { ExtensionContext, commands, window } from "vscode"; 7 | import * as config from "../config"; 8 | import { EXTENSION_NAME } from "../constants"; 9 | import { SwingFile, Version, store } from "../store"; 10 | import preamble from "./preamble.txt"; 11 | import { initializeStorage, storage } from "./storage"; 12 | 13 | const userPrompt = `REQUEST: 14 | {{REQUEST}} 15 | 16 | RESPONSE: 17 | `; 18 | 19 | export async function synthesizeTemplateFiles( 20 | prompt: string, 21 | options: { error?: string } = {} 22 | ): Promise { 23 | let openai: OpenAIClient; 24 | 25 | const apiKey = await storage.getOpenAiApiKey(); 26 | const endpointUrl = config.get("ai.endpointUrl"); 27 | if (endpointUrl) { 28 | const credential = new AzureKeyCredential(apiKey!); 29 | openai = new OpenAIClient(endpointUrl, credential); 30 | } else { 31 | const credential = new OpenAIKeyCredential(apiKey!); 32 | openai = new OpenAIClient(credential); 33 | } 34 | 35 | const messages = [{ role: "user", content: preamble }]; 36 | 37 | prompt = userPrompt.replace("{{REQUEST}}", prompt); 38 | 39 | let previousVersion: Version | undefined; 40 | if (store.history && store.history.length > 0) { 41 | previousVersion = store.history[store.history.length - 1]; 42 | const content = previousVersion.files 43 | .map((e) => `<<—[${e.filename}]=\n${e.content}\n—>>`) 44 | .join("\n\n"); 45 | 46 | messages.push( 47 | { role: "user", content: previousVersion.prompt }, 48 | { 49 | role: "assistant", 50 | content, 51 | } 52 | ); 53 | 54 | if (options.error) { 55 | const errorPrompt = `An error occured in the code you previously provided. Could you return an updated version of the code that fixes it? You don't need to apologize or return any prose. Simply look at the error message, and reply with the updated code that includes a fix. 56 | 57 | ERROR: 58 | ${options.error} 59 | 60 | RESPONSE: 61 | `; 62 | 63 | messages.push({ 64 | role: "user", 65 | content: errorPrompt, 66 | }); 67 | } else { 68 | const editPrompt = `Here's an updated version of my previous request. Detect the edits I made, modify your previous response with the neccessary code changes, and then provide the full code again, with those modifications made. You only need to reply with files that have changed. But when changing a file, you should return the entire contents of that new file. However, you can ignore any files that haven't changed, and you don't need to apologize or return any prose, or code comments indicating that no changes were made. 69 | 70 | ${prompt}`; 71 | 72 | messages.push({ 73 | role: "user", 74 | content: editPrompt, 75 | }); 76 | } 77 | } else { 78 | messages.push({ 79 | role: "user", 80 | content: prompt, 81 | }); 82 | } 83 | 84 | console.log("CS Request: %o", messages); 85 | 86 | const model = config.get("ai.model"); 87 | const chatCompletion = await openai.getChatCompletions( 88 | model, 89 | // @ts-ignore 90 | messages 91 | ); 92 | 93 | let response = chatCompletion.choices[0].message!.content!; 94 | 95 | // Despite asking it not to, the model will sometimes still add 96 | // prose to the beginning of the response. We need to remove it. 97 | const fileStart = response.indexOf("<<—["); 98 | if (fileStart !== 0) { 99 | response = response.slice(fileStart); 100 | } 101 | 102 | console.log("CS Response: %o", response); 103 | 104 | const files = response 105 | .split("—>>") 106 | .filter((e) => e !== "") 107 | .map((e) => { 108 | e = e.trim(); 109 | const p = e.split("]=\n"); 110 | return { filename: p[0].replace("<<—[", ""), content: p[1] }; 111 | })!; 112 | 113 | // Merge the contents of files that have the same name. 114 | const mergedFiles: SwingFile[] = []; 115 | files.forEach((e) => { 116 | const existing = mergedFiles.find((f) => f.filename === e.filename); 117 | if (existing) { 118 | existing.content += "\n\n" + e.content; 119 | } else { 120 | mergedFiles.push(e); 121 | } 122 | }); 123 | 124 | console.log("CS Files: %o", files); 125 | 126 | // If the model generated a component, then we need to remove any script 127 | // files that it might have also generated. Despite asking it not to! 128 | if (files.some((e) => e.filename.startsWith("App."))) { 129 | const scriptIndex = files.findIndex((e) => 130 | e.filename.startsWith("script.") 131 | ); 132 | if (scriptIndex !== -1) { 133 | files.splice(scriptIndex, 1); 134 | } 135 | } 136 | 137 | // Find any files in the previous files that aren't in the new files 138 | // and add them to the new files. 139 | if (previousVersion) { 140 | previousVersion.files.forEach((e) => { 141 | if (!files.some((f) => f.filename === e.filename)) { 142 | // @ts-ignore 143 | files.push(e); 144 | } 145 | }); 146 | } 147 | 148 | store.history!.push({ prompt, files }); 149 | 150 | return files; 151 | } 152 | 153 | export function registerAiModule(context: ExtensionContext) { 154 | context.subscriptions.push( 155 | commands.registerCommand(`${EXTENSION_NAME}.setOpenAiApiKey`, async () => { 156 | const key = await window.showInputBox({ 157 | prompt: "Enter your OpenAI API key", 158 | placeHolder: "", 159 | }); 160 | if (!key) return; 161 | await storage.setOpenAiApiKey(key); 162 | }) 163 | ); 164 | 165 | context.subscriptions.push( 166 | commands.registerCommand( 167 | `${EXTENSION_NAME}.clearOpenAiApiKey`, 168 | async () => { 169 | await storage.deleteOpenAiApiKey(); 170 | } 171 | ) 172 | ); 173 | 174 | initializeStorage(context); 175 | } 176 | -------------------------------------------------------------------------------- /src/ai/preamble.txt: -------------------------------------------------------------------------------- 1 | You are CodeSwing, a web coding playground that allows users to generate runnable code snippets/samples, using a combination of HTML/Pug, JavaScript/TypeScript, and CSS/Sass/Less, as well as the component styles of popular web frameworks (React, Vue, Svelte, etc.). 2 | You will be provided a description of a playground or sample that a developer wants to generate, and you should think about the steps needed to generate the associated code that demonstrates it. 3 | 4 | General rules: 5 | 6 | * When fulfilling a request for a playground, you should separate out the HTML/Pug, JavaScript, and CSS code into files called: index.html/index.pug, script.js/script.ts, and style.css/style.sass/style.less. 7 | * You only generate code, and offer no other description or hints to the user. 8 | * If the user’s request doesn’t require HTML, JavaScript, or CSS to fulfill, then omit the respective file for it. 9 | * If the only contents of a file are code comments, then omit that file from the response. 10 | * If the user asks about a CLI or Go, then generate a file called App.go, and populate it with the Go code needed to satisfy the request 11 | * If the user asks for a sample, then make sure to generate explainatory text in the actual code, so that it functions as a simple tutorial 12 | 13 | When generating HTML/Pug, follow these rules: 14 | 15 | * Don't include the , , or tags, as these will be automatically added by the playground's runtime environment. 16 | * Don't include 141 | 142 | 143 | 144 | {#if visible} 145 |
146 |

Hello

147 |
148 | {:else} 149 |
150 |

Goodbye

151 |
152 | {/if} 153 | —>> 154 | 155 | --- 156 | 157 | REQUEST: 158 | A hello world React Native app 159 | 160 | RESPONSE: 161 | <<—[script.js]= 162 | import * React from 'react'; 163 | import { View, Text } from 'react-native'; 164 | 165 | const App = () => { 166 | return ( 167 | 168 | Hello World!!!! 169 | 170 | ); 171 | }; 172 | 173 | export default App; 174 | —>> 175 | 176 | --- -------------------------------------------------------------------------------- /src/ai/storage.ts: -------------------------------------------------------------------------------- 1 | import { commands, ExtensionContext } from "vscode"; 2 | import { EXTENSION_NAME } from "../constants"; 3 | 4 | const OPENAI_CONTEXT_KEY = `${EXTENSION_NAME}:hasOpenAiApiKey`; 5 | const OPENAI_STORAGE_KEY = `${EXTENSION_NAME}:openAiApiKey`; 6 | 7 | export interface IAiStorage { 8 | deleteOpenAiApiKey(): Promise; 9 | getOpenAiApiKey(): Promise; 10 | setOpenAiApiKey(apiKey: string): Promise; 11 | } 12 | 13 | export let storage: IAiStorage; 14 | export async function initializeStorage(context: ExtensionContext) { 15 | storage = { 16 | async deleteOpenAiApiKey(): Promise { 17 | await context.secrets.delete(OPENAI_STORAGE_KEY); 18 | await commands.executeCommand("setContext", OPENAI_CONTEXT_KEY, false); 19 | }, 20 | async getOpenAiApiKey(): Promise { 21 | return context.secrets.get(OPENAI_STORAGE_KEY); 22 | }, 23 | async setOpenAiApiKey(key: string): Promise { 24 | await context.secrets.store(OPENAI_STORAGE_KEY, key); 25 | await commands.executeCommand("setContext", OPENAI_CONTEXT_KEY, true); 26 | }, 27 | }; 28 | 29 | if (storage.getOpenAiApiKey() !== undefined) { 30 | await commands.executeCommand("setContext", OPENAI_CONTEXT_KEY, true); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { SWING_FILE } from "./constants"; 3 | import { getCandidateMarkupFilenames } from "./preview/languages/markup"; 4 | 5 | export const api = { 6 | isSwing(files: string[]) { 7 | return ( 8 | files.includes(".block") || 9 | files.includes(SWING_FILE) || 10 | files.some((file) => getCandidateMarkupFilenames().includes(file)) || 11 | files.includes("scripts") || 12 | (files.includes("script.js") && 13 | files.some((file) => path.extname(file) === ".markdown")) 14 | ); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | import { EXTENSION_NAME } from "./constants"; 3 | 4 | export function get(key: "autoRun"): "onEdit" | "onSave" | "never"; 5 | export function get(key: "autoSave"): boolean; 6 | export function get(key: "clearConsoleOnRun"): boolean; 7 | export function get( 8 | key: "layout" 9 | ): "grid" | "splitLeft" | "splitRight" | "splitTop"; 10 | export function get(key: "launchBehavior"): "newSwing" | "none" | "openSwing"; 11 | export function get( 12 | key: "readmeBehavior" 13 | ): "none" | "previewFooter" | "previewHeader"; 14 | export function get(key: "rootDirectory"): string; 15 | export function get(key: "showConsole"): boolean; 16 | export function get(key: "templateGalleries"): string[]; 17 | export function get(key: "themePreview"): boolean; 18 | export function get(key: "ai.endpointUrl"): string; 19 | export function get(key: "ai.model"): string; 20 | export function get(key: any) { 21 | const extensionConfig = workspace.getConfiguration(EXTENSION_NAME); 22 | return extensionConfig.get(key); 23 | } 24 | 25 | export async function set(key: string, value: any) { 26 | const extensionConfig = workspace.getConfiguration(EXTENSION_NAME); 27 | return extensionConfig.update(key, value, true); 28 | } 29 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const EXTENSION_NAME = "codeswing"; 2 | export const EXTENSION_ID = `codespaces-contrib.${EXTENSION_NAME}`; 3 | 4 | export const INPUT_SCHEME = `${EXTENSION_NAME}-input`; 5 | 6 | export const SWING_FILE = `${EXTENSION_NAME}.json`; 7 | 8 | export const URI_PATTERN = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; 9 | -------------------------------------------------------------------------------- /src/creation/galleryProvider.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as vscode from "vscode"; 3 | import * as config from "../config"; 4 | import { EXTENSION_NAME } from "../constants"; 5 | import { SwingFile } from "../store"; 6 | 7 | interface Gallery { 8 | id: string; 9 | url: string; 10 | enabled: boolean; 11 | templates: GalleryTemplate[]; 12 | title: string; 13 | description?: string; 14 | } 15 | 16 | interface GalleryTemplate { 17 | title: string; 18 | description?: string; 19 | files: SwingFile[]; 20 | } 21 | 22 | const CONTRIBUTION_NAME = `${EXTENSION_NAME}.templateGalleries`; 23 | 24 | let loadGalleriesRunning = false; 25 | let loadGalleriesPromise: Promise = Promise.resolve([]); 26 | 27 | export async function loadGalleries() { 28 | if (loadGalleriesRunning) { 29 | return loadGalleriesPromise; 30 | } 31 | 32 | loadGalleriesPromise = new Promise(async (resolve) => { 33 | loadGalleriesRunning = true; 34 | 35 | const registrations = vscode.extensions.all 36 | .flatMap((e) => { 37 | return e.packageJSON.contributes && 38 | e.packageJSON.contributes[CONTRIBUTION_NAME] 39 | ? e.packageJSON.contributes[CONTRIBUTION_NAME] 40 | : []; 41 | }) 42 | .concat( 43 | Array.from(templateProviders.entries()).map( 44 | ([name, [provider, options]]) => { 45 | return { 46 | id: name, 47 | title: options.title || name, 48 | description: options.description, 49 | provider, 50 | enabled: true, 51 | }; 52 | } 53 | ) 54 | ); 55 | 56 | const settingContributions = await config.get("templateGalleries"); 57 | 58 | for (const gallery of settingContributions) { 59 | const registration = registrations.find( 60 | (registration) => registration.id === gallery 61 | ); 62 | if (registration) { 63 | registration.enabled = true; 64 | } else if (gallery.startsWith("https://")) { 65 | registrations.push({ 66 | id: gallery, 67 | url: gallery, 68 | enabled: true, 69 | description: "", 70 | }); 71 | } 72 | } 73 | 74 | for (const registration of registrations) { 75 | if (!settingContributions.includes(registration.id)) { 76 | registration.enabled = false; 77 | } 78 | } 79 | 80 | const galleries = await Promise.all( 81 | registrations.map(async (gallery) => { 82 | if (gallery.url) { 83 | const { data } = await axios.get(gallery.url); 84 | 85 | gallery.title = data.title; 86 | gallery.description = data.description; 87 | gallery.templates = data.templates.map( 88 | (template: GalleryTemplate) => ({ 89 | ...template, 90 | title: `${data.title}: ${template.title}`, 91 | }) 92 | ); 93 | } else if (gallery.provider) { 94 | const templates = await gallery.provider.provideTemplates(); 95 | gallery.templates = templates.map((template: GalleryTemplate) => ({ 96 | ...template, 97 | title: `${gallery.title}: ${template.title}`, 98 | })); 99 | } 100 | 101 | return gallery; 102 | }) 103 | ); 104 | 105 | loadGalleriesRunning = false; 106 | resolve(galleries); 107 | }); 108 | 109 | return loadGalleriesPromise; 110 | } 111 | 112 | export async function enableGalleries(galleryIds: string[]) { 113 | await config.set("templateGalleries", galleryIds); 114 | return loadGalleries(); 115 | } 116 | 117 | interface CodeSwingTemplateProvider { 118 | provideTemplates(): Promise; 119 | onDidChangeTemplates(listener: () => void): vscode.Disposable; 120 | } 121 | 122 | interface CodeSwingTemplateProviderOptions { 123 | title?: string; 124 | description?: string; 125 | } 126 | 127 | const templateProviders = new Map< 128 | string, 129 | [CodeSwingTemplateProvider, CodeSwingTemplateProviderOptions] 130 | >(); 131 | export function registerTemplateProvider( 132 | providerName: string, 133 | provider: CodeSwingTemplateProvider, 134 | options: CodeSwingTemplateProviderOptions 135 | ) { 136 | templateProviders.set(providerName, [provider, options]); 137 | provider.onDidChangeTemplates(loadGalleries); 138 | 139 | loadGalleries(); 140 | } 141 | 142 | vscode.extensions.onDidChange(loadGalleries); 143 | vscode.workspace.onDidChangeConfiguration((e) => { 144 | if (e.affectsConfiguration(CONTRIBUTION_NAME)) { 145 | loadGalleries(); 146 | } 147 | }); 148 | 149 | loadGalleries(); 150 | -------------------------------------------------------------------------------- /src/creation/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { synthesizeTemplateFiles } from "../ai"; 3 | import { storage as aiStorage } from "../ai/storage"; 4 | import * as config from "../config"; 5 | import { EXTENSION_NAME, SWING_FILE } from "../constants"; 6 | import { DEFAULT_MANIFEST, openSwing } from "../preview"; 7 | import { SwingFile, store } from "../store"; 8 | import { stringToByteArray, withProgress } from "../utils"; 9 | import { 10 | enableGalleries, 11 | loadGalleries, 12 | registerTemplateProvider, 13 | } from "./galleryProvider"; 14 | import { initializeStorage, storage } from "./storage"; 15 | 16 | interface CodeSwingTemplateItem extends vscode.QuickPickItem { 17 | files?: SwingFile[]; 18 | } 19 | 20 | async function createSwingDirectory() { 21 | const dayjs = require("dayjs"); 22 | const timestamp = dayjs().format("YYYY-MM-DD (hh-mm-ss A)"); 23 | 24 | const rootDirectory = config.get("rootDirectory"); 25 | const rootUri = rootDirectory 26 | ? vscode.Uri.joinPath( 27 | vscode.workspace.workspaceFolders![0].uri, 28 | rootDirectory 29 | ) 30 | : store.globalStorageUri!; 31 | 32 | const swingDirectory = vscode.Uri.joinPath(rootUri, timestamp); 33 | 34 | await vscode.workspace.fs.createDirectory(swingDirectory); 35 | return swingDirectory; 36 | } 37 | 38 | async function getTemplates(): Promise { 39 | const galleries = await loadGalleries(); 40 | 41 | const templates: CodeSwingTemplateItem[] = galleries 42 | .filter((gallery) => gallery.enabled) 43 | .flatMap((gallery) => gallery.templates) 44 | .sort((a, b) => a.title.localeCompare(b.title)) 45 | .map((t) => ({ ...t, label: t.title })); 46 | 47 | return templates; 48 | } 49 | 50 | export async function newSwing( 51 | uri: vscode.Uri | ((files: SwingFile[]) => Promise), 52 | title: string = "Create new swing" 53 | ) { 54 | const quickPick = vscode.window.createQuickPick(); 55 | quickPick.title = title; 56 | quickPick.placeholder = "Select the template to use"; 57 | quickPick.matchOnDescription = true; 58 | quickPick.ignoreFocusOut = true; 59 | 60 | const templates = await getTemplates(); 61 | if (templates.length === 0) { 62 | templates.push({ 63 | label: 64 | "No templates available. Configure your template galleries and try again.", 65 | }); 66 | } 67 | 68 | const mru = storage.getTemplateMRU(); 69 | if (mru && mru.length > 0) { 70 | for (let i = mru.length - 1; i >= 0; i--) { 71 | const itemIndex = templates.findIndex( 72 | (gallery) => gallery.label === mru[i] 73 | ); 74 | if (itemIndex !== -1) { 75 | const [item] = templates.splice(itemIndex, 1); 76 | item.alwaysShow = true; 77 | item.description = "Recently used"; 78 | templates.unshift(item); 79 | } 80 | } 81 | } 82 | 83 | const aiTooltip = "Generate swing with AI"; 84 | const quickPickButtons = [ 85 | { 86 | iconPath: new vscode.ThemeIcon("settings"), 87 | tooltip: "Configure Template Galleries", 88 | }, 89 | ]; 90 | 91 | if (await aiStorage.getOpenAiApiKey()) { 92 | quickPickButtons.unshift({ 93 | iconPath: new vscode.ThemeIcon("sparkle"), 94 | tooltip: aiTooltip, 95 | }); 96 | } 97 | 98 | quickPick.items = templates; 99 | quickPick.buttons = quickPickButtons; 100 | 101 | quickPick.onDidTriggerButton((e) => { 102 | if (e.tooltip === aiTooltip) { 103 | synthesizeTemplate(uri); 104 | } else { 105 | promptForGalleryConfiguration(uri, title); 106 | } 107 | }); 108 | 109 | quickPick.onDidAccept(async () => { 110 | quickPick.hide(); 111 | 112 | const template = quickPick.selectedItems[0] as CodeSwingTemplateItem; 113 | if (template.files) { 114 | store.history = [ 115 | { 116 | prompt: `Create a starter playground using the following template: ${template.detail}`, 117 | files: template.files, 118 | }, 119 | ]; 120 | await withProgress("Creating swing...", async () => 121 | newSwingFromTemplate(template.files!, uri) 122 | ); 123 | 124 | await storage.addTemplateToMRU(template.label); 125 | } 126 | }); 127 | 128 | quickPick.show(); 129 | } 130 | 131 | async function synthesizeTemplate( 132 | uri: vscode.Uri | ((files: SwingFile[]) => Promise) 133 | ) { 134 | const prompt = await vscode.window.showInputBox({ 135 | placeHolder: "Describe the swing you want to generate", 136 | }); 137 | if (!prompt) return; 138 | 139 | await withProgress("Creating swing...", async () => { 140 | store.history = []; 141 | const files = await synthesizeTemplateFiles(prompt); 142 | return newSwingFromTemplate(files!, uri); 143 | }); 144 | } 145 | 146 | async function newSwingFromTemplate( 147 | files: SwingFile[], 148 | uri: vscode.Uri | ((files: SwingFile[]) => Promise) 149 | ) { 150 | const manifest = files.find((file) => file.filename === SWING_FILE); 151 | if (!manifest) { 152 | const content = JSON.stringify(DEFAULT_MANIFEST, null, 2); 153 | files.push({ filename: SWING_FILE, content }); 154 | } else if (manifest.content) { 155 | try { 156 | const content = JSON.parse(manifest.content); 157 | delete content.template; 158 | manifest.content = JSON.stringify(content, null, 2); 159 | } catch { 160 | // If the template included an invalid 161 | // manifest file, then there's nothing 162 | // we can really do about it. 163 | } 164 | } 165 | 166 | let swingUri: vscode.Uri; 167 | if (uri instanceof Function) { 168 | swingUri = await uri( 169 | files.map(({ filename, content }) => ({ 170 | filename, 171 | content: content ? content : "", 172 | })) 173 | ); 174 | } else { 175 | await Promise.all( 176 | files.map(({ filename, content = "" }) => { 177 | const targetFileUri = vscode.Uri.joinPath(uri, filename); 178 | return vscode.workspace.fs.writeFile( 179 | targetFileUri, 180 | stringToByteArray(content) 181 | ); 182 | }) 183 | ); 184 | swingUri = uri; 185 | } 186 | 187 | openSwing(swingUri); 188 | } 189 | 190 | async function promptForGalleryConfiguration( 191 | uri: vscode.Uri | ((files: SwingFile[]) => Promise), 192 | title: string 193 | ) { 194 | const quickPick = vscode.window.createQuickPick(); 195 | quickPick.title = "Configure template galleries"; 196 | quickPick.placeholder = 197 | "Select the galleries you'd like to display templates from"; 198 | quickPick.canSelectMany = true; 199 | 200 | const galleries = (await loadGalleries()) 201 | .sort((a, b) => a.title.localeCompare(b.title)) 202 | .map((gallery) => ({ ...gallery, label: gallery.title })); 203 | 204 | quickPick.items = galleries; 205 | quickPick.selectedItems = galleries.filter((gallery) => gallery.enabled); 206 | 207 | quickPick.buttons = [vscode.QuickInputButtons.Back]; 208 | quickPick.onDidTriggerButton((e) => { 209 | if (e === vscode.QuickInputButtons.Back) { 210 | return newSwing(uri, title); 211 | } 212 | }); 213 | 214 | quickPick.onDidAccept(async () => { 215 | const galleries = quickPick.selectedItems.map((item) => (item as any).id); 216 | 217 | quickPick.busy = true; 218 | await enableGalleries(galleries); 219 | quickPick.busy = false; 220 | 221 | quickPick.hide(); 222 | 223 | return newSwing(uri, title); 224 | }); 225 | 226 | quickPick.show(); 227 | } 228 | 229 | export function registerCreationModule( 230 | context: vscode.ExtensionContext, 231 | api: any, 232 | syncKeys: string[] 233 | ) { 234 | context.subscriptions.push( 235 | vscode.commands.registerCommand(`${EXTENSION_NAME}.newSwing`, async () => { 236 | const uri = await createSwingDirectory(); 237 | newSwing(uri); 238 | }) 239 | ); 240 | 241 | context.subscriptions.push( 242 | vscode.commands.registerCommand( 243 | `${EXTENSION_NAME}.newSwingFromLastTemplate`, 244 | async () => { 245 | const [latestTemplate] = storage.getTemplateMRU(); 246 | const templates = await getTemplates(); 247 | const template = templates.find( 248 | (template) => template.label === latestTemplate 249 | ); 250 | if (template) { 251 | const uri = await createSwingDirectory(); 252 | newSwingFromTemplate(template.files!, uri); 253 | } 254 | } 255 | ) 256 | ); 257 | 258 | context.subscriptions.push( 259 | vscode.commands.registerCommand( 260 | `${EXTENSION_NAME}.newSwingDirectory`, 261 | async () => { 262 | const folder = await vscode.window.showOpenDialog({ 263 | canSelectFolders: true, 264 | canSelectFiles: false, 265 | canSelectMany: false, 266 | defaultUri: vscode.workspace.workspaceFolders?.[0].uri, 267 | }); 268 | 269 | if (folder) { 270 | newSwing(folder[0]); 271 | } 272 | } 273 | ) 274 | ); 275 | 276 | context.subscriptions.push( 277 | vscode.commands.registerCommand( 278 | `${EXTENSION_NAME}.initializeWorkspace`, 279 | async () => { 280 | const uri = vscode.workspace.workspaceFolders![0].uri; 281 | newSwing(uri); 282 | } 283 | ) 284 | ); 285 | 286 | context.subscriptions.push( 287 | vscode.commands.registerCommand( 288 | `${EXTENSION_NAME}.saveCurrentSwing`, 289 | async () => { 290 | const folder = await vscode.window.showOpenDialog({ 291 | canSelectFolders: true, 292 | canSelectFiles: false, 293 | canSelectMany: false, 294 | defaultUri: vscode.workspace.workspaceFolders?.[0].uri, 295 | }); 296 | 297 | if (!folder) { 298 | return; 299 | } 300 | 301 | await withProgress("Saving swing...", async () => { 302 | const files = await vscode.workspace.fs.readDirectory( 303 | store.activeSwing!.rootUri 304 | ); 305 | return Promise.all( 306 | files.map(async ([file]) => { 307 | const sourceUri = vscode.Uri.joinPath( 308 | store.activeSwing!.rootUri, 309 | file 310 | ); 311 | const contents = await vscode.workspace.fs.readFile(sourceUri); 312 | 313 | const uri = vscode.Uri.joinPath(folder[0], file); 314 | await vscode.workspace.fs.writeFile(uri, contents); 315 | }) 316 | ); 317 | }); 318 | } 319 | ) 320 | ); 321 | 322 | initializeStorage(context, syncKeys); 323 | 324 | api.newSwing = newSwing; 325 | api.registerTemplateProvider = registerTemplateProvider; 326 | } 327 | -------------------------------------------------------------------------------- /src/creation/storage.ts: -------------------------------------------------------------------------------- 1 | import { commands, ExtensionContext } from "vscode"; 2 | import { EXTENSION_NAME } from "../constants"; 3 | 4 | const MRU_SIZE = 3; 5 | 6 | const MRU_CONTEXT_KEY = `${EXTENSION_NAME}:hasTemplateMRU`; 7 | const MRU_STORAGE_KEY = `${EXTENSION_NAME}:templateMRU`; 8 | 9 | export interface IStorage { 10 | getTemplateMRU(): string[]; 11 | addTemplateToMRU(template: string): Promise; 12 | } 13 | 14 | export let storage: IStorage; 15 | export async function initializeStorage( 16 | context: ExtensionContext, 17 | syncKeys: string[] 18 | ) { 19 | storage = { 20 | getTemplateMRU(): string[] { 21 | const mru = context.globalState.get(MRU_STORAGE_KEY) || []; 22 | return mru.filter((template) => template !== null); 23 | }, 24 | async addTemplateToMRU(template: string) { 25 | const mru = this.getTemplateMRU(); 26 | if (mru.includes(template)) { 27 | const oldIndex = mru.findIndex((item) => item === template); 28 | mru.splice(oldIndex, 1); 29 | } 30 | 31 | mru.unshift(template); 32 | 33 | while (mru.length > MRU_SIZE) { 34 | mru.pop(); 35 | } 36 | 37 | await context.globalState.update(MRU_STORAGE_KEY, mru); 38 | await commands.executeCommand("setContext", MRU_CONTEXT_KEY, true); 39 | }, 40 | }; 41 | 42 | if (storage.getTemplateMRU().length > 0) { 43 | await commands.executeCommand("setContext", MRU_CONTEXT_KEY, true); 44 | } 45 | 46 | syncKeys.push(MRU_STORAGE_KEY); 47 | } 48 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { registerAiModule } from "./ai"; 3 | import { api } from "./api"; 4 | import { registerCreationModule } from "./creation"; 5 | import { registerLiveShareModule } from "./liveShare"; 6 | import { registerPreviewModule } from "./preview"; 7 | import { registerTreeViewModule } from "./preview/tree"; 8 | import { store } from "./store"; 9 | import { checkForSwingWorkspace } from "./utils"; 10 | 11 | export async function activate(context: vscode.ExtensionContext) { 12 | store.globalStorageUri = context.globalStorageUri; 13 | 14 | const syncKeys: string[] = []; 15 | 16 | registerAiModule(context); 17 | registerCreationModule(context, api, syncKeys); 18 | registerPreviewModule(context, api, syncKeys); 19 | 20 | context.globalState.setKeysForSync(syncKeys); 21 | 22 | registerTreeViewModule(context); 23 | registerLiveShareModule(); 24 | 25 | checkForSwingWorkspace(); 26 | 27 | return api; 28 | } 29 | -------------------------------------------------------------------------------- /src/liveShare/guestService.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import * as vsls from "vsls"; 3 | import { EXTENSION_NAME } from "../constants"; 4 | import { openSwing } from "../preview"; 5 | import initializeBaseService from "./service"; 6 | 7 | export async function initializeService(vslsApi: vsls.LiveShare) { 8 | const service = await vslsApi.getSharedService(EXTENSION_NAME); 9 | if (!service) return; 10 | 11 | const response = await service.request("getActiveSwing", []); 12 | if (response) { 13 | const uri = Uri.parse(response.uri); 14 | openSwing(uri); 15 | } 16 | 17 | initializeBaseService(vslsApi, vslsApi.session.peerNumber, service); 18 | } 19 | -------------------------------------------------------------------------------- /src/liveShare/hostService.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as vsls from "vsls"; 3 | import { EXTENSION_NAME } from "../constants"; 4 | import { store } from "../store"; 5 | import initializeBaseService from "./service"; 6 | 7 | // TODO: Replace this with a fixed version of the Live Share API 8 | function convertUri(uri: vscode.Uri): vscode.Uri { 9 | let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); 10 | const relativePath = 11 | workspaceFolder?.uri.toString() === uri.toString() 12 | ? "" 13 | : vscode.workspace.asRelativePath(uri, false); 14 | 15 | let rootPrefix = ""; 16 | if (workspaceFolder && workspaceFolder.index > 0) { 17 | rootPrefix = `~${workspaceFolder.index}/`; 18 | } 19 | 20 | return vscode.Uri.parse(`vsls:/${rootPrefix}${relativePath}`); 21 | } 22 | 23 | export async function initializeService(vslsApi: vsls.LiveShare) { 24 | const service = await vslsApi.shareService(EXTENSION_NAME); 25 | if (!service) return; 26 | 27 | service.onRequest("getActiveSwing", () => { 28 | if (!store.activeSwing) { 29 | return null; 30 | } 31 | 32 | const uri = convertUri(store.activeSwing.rootUri); 33 | return { 34 | uri: uri.toString(), 35 | }; 36 | }); 37 | 38 | initializeBaseService(vslsApi, vslsApi.session.peerNumber, service, true); 39 | } 40 | -------------------------------------------------------------------------------- /src/liveShare/index.ts: -------------------------------------------------------------------------------- 1 | import * as vsls from "vsls"; 2 | import { EXTENSION_NAME } from "../constants"; 3 | 4 | export async function registerLiveShareModule() { 5 | const vslsApi = await vsls.getApi(`codespaces-contrib.${EXTENSION_NAME}`); 6 | if (!vslsApi) return; 7 | 8 | vslsApi.onDidChangeSession((e) => { 9 | if (e.session.id) { 10 | initializeService(vslsApi); 11 | } 12 | }); 13 | 14 | if (vslsApi.session.id) { 15 | initializeService(vslsApi); 16 | } 17 | } 18 | 19 | async function initializeService(vslsApi: vsls.LiveShare) { 20 | let { initializeService } = 21 | vslsApi.session.role === vsls.Role.Host 22 | ? require("./hostService") 23 | : require("./guestService"); 24 | 25 | await initializeService(vslsApi); 26 | } 27 | -------------------------------------------------------------------------------- /src/liveShare/service.ts: -------------------------------------------------------------------------------- 1 | import { LiveShare, SharedService, SharedServiceProxy } from "vsls"; 2 | 3 | // TODO: Implement more features 4 | 5 | /* 6 | interface Message { 7 | data?: any; 8 | peer?: number; 9 | } 10 | 11 | const SWING_OPENED_NOTIFICATION = "swingOpened"; 12 | const SWING_CLOSED_NOTIFICATION = "swingClosed";*/ 13 | 14 | export default function ( 15 | api: LiveShare, 16 | peer: number, 17 | service: SharedService | SharedServiceProxy, 18 | broadcastNotifications: boolean = false 19 | ) { 20 | /* 21 | onDidCloseSwing(() => { 22 | service.notify(SWING_CLOSED_NOTIFICATION, { peer }); 23 | }); 24 | 25 | service.onNotify(SWING_CLOSED_NOTIFICATION, (message: Message) => { 26 | if (message.peer === peer) return; 27 | store.activeSwing?.webViewPanel.dispose(); 28 | 29 | if (broadcastNotifications) { 30 | service.notify(SWING_CLOSED_NOTIFICATION, message); 31 | } 32 | }); 33 | 34 | onDidOpenSwing((uri) => { 35 | let sharedUri; 36 | if (api.session.role === Role.Host) { 37 | sharedUri = api.convertLocalUriToShared(uri).toString(); 38 | } else { 39 | sharedUri = api.convertSharedUriToLocal(uri).toString(); 40 | } 41 | 42 | const message = { 43 | peer, 44 | data: { 45 | uri: sharedUri, 46 | }, 47 | }; 48 | 49 | service.notify(SWING_OPENED_NOTIFICATION, message); 50 | }); 51 | 52 | service.onNotify(SWING_OPENED_NOTIFICATION, (message: Message) => { 53 | if (message.peer === peer) return; 54 | openSwing(message.data.uri); 55 | 56 | if (broadcastNotifications) { 57 | service.notify(SWING_OPENED_NOTIFICATION, message); 58 | } 59 | });*/ 60 | } 61 | -------------------------------------------------------------------------------- /src/preview/codepen.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as path from "path"; 3 | import * as vscode from "vscode"; 4 | import { getFileOfType } from "."; 5 | import { EXTENSION_NAME, SWING_FILE, URI_PATTERN } from "../constants"; 6 | import { SwingFileType, store } from "../store"; 7 | import { getFileContents, getUriContents, stringToByteArray } from "../utils"; 8 | import { getCdnJsLibraries } from "./libraries/cdnjs"; 9 | 10 | function getExportMarkup(data: any) { 11 | const value = JSON.stringify(data) 12 | .replace(/"/g, """) 13 | .replace(/'/g, "'") 14 | .replace(/ /g, "&nbsp"); 15 | 16 | return `
17 | 18 |
19 | 20 | 27 | `; 28 | } 29 | 30 | const SCRIPT_PATTERN = /`; 89 | } catch (e) { 90 | console.log("Error: ", e); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/preview/languages/languageProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { EXTENSION_NAME } from "../../constants"; 3 | 4 | const CONTRIBUTION_NAME = `${EXTENSION_NAME}.languages`; 5 | 6 | type SwingLanguageType = "markup" | "stylesheet" | "script"; 7 | 8 | interface SwingLanguageDefinition { 9 | type: SwingLanguageType; 10 | extensions: string[]; 11 | source: string; 12 | } 13 | 14 | const languages = new Map>(); 15 | 16 | export function getExtensions(type: SwingLanguageType) { 17 | const languageDefinitions = languages.get(type); 18 | if (!languageDefinitions) { 19 | return []; 20 | } 21 | 22 | return Array.from(languageDefinitions.keys()); 23 | } 24 | 25 | export async function compileCode( 26 | type: SwingLanguageType, 27 | extension: string, 28 | code: string 29 | ): Promise { 30 | const extensionId = languages.get(type)?.get(extension); 31 | if (!extensionId) { 32 | return null; 33 | } 34 | const compiler = await getExtension(extensionId); 35 | return compiler ? compiler(extension, code) : null; 36 | } 37 | 38 | async function getExtension(id: string) { 39 | const extensionInstance = vscode.extensions.getExtension(id); 40 | if (!extensionInstance) { 41 | return; 42 | } 43 | 44 | if (!extensionInstance.isActive) { 45 | await extensionInstance.activate(); 46 | } 47 | 48 | return extensionInstance.exports?.codeSwingCompile; 49 | } 50 | 51 | export function discoverLanguageProviders() { 52 | const languageDefinitions: SwingLanguageDefinition[] = vscode.extensions.all.flatMap( 53 | (e) => 54 | e.packageJSON.contributes && e.packageJSON.contributes[CONTRIBUTION_NAME] 55 | ? e.packageJSON.contributes[CONTRIBUTION_NAME].map((language: any) => ({ 56 | source: e.id, 57 | ...language, 58 | })) 59 | : [] 60 | ); 61 | 62 | languageDefinitions.forEach((language) => { 63 | if (!languages.has(language.type)) { 64 | languages.set(language.type, new Map()); 65 | } 66 | 67 | language.extensions.forEach((extension) => { 68 | languages.get(language.type)!.set(extension, language.source); 69 | }); 70 | }); 71 | } 72 | 73 | vscode.extensions.onDidChange(discoverLanguageProviders); 74 | -------------------------------------------------------------------------------- /src/preview/languages/markup.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { TextDocument } from "vscode"; 3 | import { getModuleUrl, processImports } from "../libraries/skypack"; 4 | import { compileCode, getExtensions } from "./languageProvider"; 5 | import { REACT_EXTENSIONS } from "./script"; 6 | 7 | const MARKUP_BASE_NAMES = ["index", "App", "main"]; 8 | 9 | const MarkupLanguage = { 10 | html: ".html", 11 | markdown: ".md", 12 | pug: ".pug", 13 | vue: ".vue", 14 | svelte: ".svelte", 15 | go: ".go", 16 | }; 17 | 18 | const COMPONENT_EXTENSIONS = [ 19 | MarkupLanguage.vue, 20 | MarkupLanguage.svelte, 21 | ...REACT_EXTENSIONS, 22 | ]; 23 | 24 | const MARKUP_EXTENSIONS = [ 25 | MarkupLanguage.html, 26 | MarkupLanguage.markdown, 27 | MarkupLanguage.pug, 28 | MarkupLanguage.go, 29 | ...COMPONENT_EXTENSIONS, 30 | ]; 31 | 32 | function getMarkupExtensions() { 33 | const customExtensions = getExtensions("markup"); 34 | return [...MARKUP_EXTENSIONS, ...customExtensions]; 35 | } 36 | 37 | export function getCandidateMarkupFilenames() { 38 | return getMarkupExtensions().flatMap((extension) => 39 | MARKUP_BASE_NAMES.map((baseName) => `${baseName}${extension}`) 40 | ); 41 | } 42 | 43 | const COMPONENT_TYPE: { [extension: string]: string } = { 44 | ".jsx": "react", 45 | ".tsx": "react", 46 | ".vue": "vue", 47 | ".svelte": "svelte", 48 | }; 49 | 50 | export async function getMarkupContent( 51 | document: TextDocument 52 | ): Promise { 53 | const content = document.getText(); 54 | if (content.trim() === "") { 55 | return content; 56 | } 57 | 58 | const extension = path.extname(document.uri.path).toLocaleLowerCase(); 59 | try { 60 | if (COMPONENT_EXTENSIONS.includes(extension)) { 61 | const componentType = COMPONENT_TYPE[extension]; 62 | const { compileComponent } = require(`./components/${componentType}`); 63 | const [component, appInit, imports] = await compileComponent( 64 | content, 65 | document 66 | ); 67 | const code = processImports(component); 68 | return `
69 | `; 77 | } else if (extension === MarkupLanguage.go) { 78 | const { compileGo } = require("./go"); 79 | return await compileGo(content, document.uri); 80 | } 81 | 82 | switch (extension) { 83 | case MarkupLanguage.pug: 84 | const pug = require("pug"); 85 | return pug.render(content); 86 | case MarkupLanguage.markdown: 87 | const markdown = require("markdown-it")(); 88 | return markdown.render(content, { html: true }); 89 | case MarkupLanguage.html: 90 | return content; 91 | default: 92 | return compileCode("markup", extension, content); 93 | } 94 | } catch { 95 | return null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/preview/languages/readme.ts: -------------------------------------------------------------------------------- 1 | export const README_BASE_NAME = "README"; 2 | export const README_EXTENSIONS = [".md", ".markdown"]; 3 | 4 | export function getReadmeContent(readme: string): string | null { 5 | if (readme.trim() === "") { 6 | return readme; 7 | } 8 | 9 | try { 10 | const markdown = require("markdown-it")(); 11 | return markdown.render(readme); 12 | } catch { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/preview/languages/script.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { TextDocument } from "vscode"; 3 | import { SwingManifest } from "../../store"; 4 | import { processImports } from "../libraries/skypack"; 5 | 6 | export const SCRIPT_BASE_NAME = "script"; 7 | 8 | export const ScriptLanguage = { 9 | babel: ".babel", 10 | javascript: ".js", 11 | javascriptmodule: ".mjs", 12 | javascriptreact: ".jsx", 13 | typescript: ".ts", 14 | typescriptreact: ".tsx", 15 | }; 16 | 17 | export const REACT_EXTENSIONS = [ 18 | ScriptLanguage.babel, 19 | ScriptLanguage.javascriptreact, 20 | ScriptLanguage.typescriptreact, 21 | ]; 22 | 23 | const MODULE_EXTENSIONS = [ScriptLanguage.javascriptmodule]; 24 | 25 | const TYPESCRIPT_EXTENSIONS = [ScriptLanguage.typescript, ...REACT_EXTENSIONS]; 26 | 27 | export const SCRIPT_EXTENSIONS = [ 28 | ScriptLanguage.javascript, 29 | ...MODULE_EXTENSIONS, 30 | ...TYPESCRIPT_EXTENSIONS, 31 | ]; 32 | 33 | export function isReactFile(fileName: string) { 34 | return REACT_EXTENSIONS.includes(path.extname(fileName)); 35 | } 36 | 37 | export const REACT_SCRIPTS = ["react", "react-dom"]; 38 | 39 | export function includesReactFiles(files: string[]) { 40 | return files.some(isReactFile); 41 | } 42 | 43 | export function includesReactScripts(scripts: string[]) { 44 | return REACT_SCRIPTS.every((script) => scripts.includes(script)); 45 | } 46 | 47 | export function getScriptContent( 48 | document: TextDocument, 49 | manifest: SwingManifest | undefined 50 | ): [string, boolean] | null { 51 | const extension = path.extname(document.uri.path).toLocaleLowerCase(); 52 | let isModule = MODULE_EXTENSIONS.includes(extension); 53 | 54 | const content = document.getText(); 55 | if (content.trim() === "") { 56 | return [content, isModule]; 57 | } else if (manifest?.scriptType === "module") { 58 | isModule = true; 59 | } else { 60 | isModule = isModule || content.trim().startsWith("import "); 61 | } 62 | 63 | const includesJsx = 64 | manifest && manifest.scripts && manifest.scripts.includes("react"); 65 | 66 | const compileComponent = compileScriptContent( 67 | content, 68 | extension, 69 | isModule, 70 | includesJsx 71 | ); 72 | return compileComponent ? [compileComponent, isModule] : null; 73 | } 74 | 75 | export function compileScriptContent( 76 | content: string, 77 | extension: string, 78 | isModule: boolean = true, 79 | includesJsx: boolean = true 80 | ): string | null { 81 | if (isModule) { 82 | if (includesJsx) { 83 | // React can only be imported into the page once, and so if the user's 84 | // code is trying to import it, we need to replace that import statement. 85 | content = content 86 | .replace(/import (?:\* as )?React from (["'])react\1/, "") 87 | .replace(/import (?:\* as )?ReactDOM from (["'])react-dom\1/, "") 88 | .replace( 89 | /import React, {(.+)} from (["'])react\2/, 90 | "const {$1} = React" 91 | ) 92 | .replace(/import (.+) from (["'])react\2/, "const $1 = React") 93 | .replace( 94 | /from (["'])react-native\1/, 95 | "from $1https://gistcdn.githack.com/lostintangent/6de9be49a0f112dd36eff3b8bc771b9e/raw/ce12b9075322245be20a79eba4d89d4e5152a4aa/index.js$1" 96 | ); 97 | } 98 | 99 | content = processImports(content); 100 | } 101 | 102 | const containsJsx = 103 | includesJsx || content.match(/import .* from (["'])react\1/) !== null; 104 | 105 | if (TYPESCRIPT_EXTENSIONS.includes(extension) || containsJsx) { 106 | const typescript = require("typescript"); 107 | const compilerOptions: any = { 108 | experimentalDecorators: true, 109 | target: "ES2018", 110 | }; 111 | 112 | if (REACT_EXTENSIONS.includes(extension) || containsJsx) { 113 | compilerOptions.jsx = typescript.JsxEmit.React; 114 | } 115 | 116 | try { 117 | return typescript.transpile(content, compilerOptions); 118 | } catch (e) { 119 | // Something failed when trying to transpile Pug, 120 | // so don't attempt to return anything 121 | return null; 122 | } 123 | } else { 124 | return content; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/preview/languages/stylesheet.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { TextDocument } from "vscode"; 3 | import { store } from "../../store"; 4 | import { byteArrayToString } from "../../utils"; 5 | 6 | export const STYLESHEET_BASE_NAME = "style"; 7 | 8 | const StylesheetLanguage = { 9 | css: ".css", 10 | less: ".less", 11 | sass: ".sass", 12 | scss: ".scss", 13 | }; 14 | 15 | export const STYLESHEET_EXTENSIONS = [ 16 | StylesheetLanguage.css, 17 | StylesheetLanguage.less, 18 | StylesheetLanguage.sass, 19 | StylesheetLanguage.scss, 20 | ]; 21 | 22 | export async function getStylesheetContent( 23 | document: TextDocument 24 | ): Promise { 25 | const content = document.getText(); 26 | if (content.trim() === "") { 27 | return content; 28 | } 29 | 30 | const extension = path.extname(document.uri.path).toLocaleLowerCase(); 31 | 32 | try { 33 | switch (extension) { 34 | case StylesheetLanguage.scss: 35 | case StylesheetLanguage.sass: { 36 | const sass = require("@abstractions/sass"); 37 | const css = await sass.compile( 38 | content, 39 | extension === StylesheetLanguage.sass, 40 | store.activeSwing!.currentUri 41 | ); 42 | 43 | return byteArrayToString(css); 44 | } 45 | case StylesheetLanguage.less: { 46 | const less = require("less").default; 47 | const output = await less.render(content); 48 | return output.css; 49 | } 50 | default: 51 | return content; 52 | } 53 | } catch { 54 | return null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/preview/layoutManager.ts: -------------------------------------------------------------------------------- 1 | import { commands, TextDocument, ViewColumn, window } from "vscode"; 2 | import * as config from "../config"; 3 | 4 | enum EditorLayoutOrientation { 5 | horizontal = 0, 6 | vertical = 1, 7 | } 8 | 9 | const EditorLayouts = { 10 | splitOne: { 11 | orientation: EditorLayoutOrientation.horizontal, 12 | groups: [{}, {}], 13 | }, 14 | splitTwo: { 15 | orientation: EditorLayoutOrientation.horizontal, 16 | groups: [ 17 | { 18 | orientation: EditorLayoutOrientation.vertical, 19 | groups: [{}, {}], 20 | size: 0.5, 21 | }, 22 | { groups: [{}], size: 0.5 }, 23 | ], 24 | }, 25 | splitThree: { 26 | orientation: EditorLayoutOrientation.horizontal, 27 | groups: [ 28 | { 29 | orientation: EditorLayoutOrientation.vertical, 30 | groups: [{}, {}, {}], 31 | size: 0.5, 32 | }, 33 | { groups: [{}], size: 0.5 }, 34 | ], 35 | }, 36 | grid: { 37 | orientation: EditorLayoutOrientation.horizontal, 38 | groups: [ 39 | { 40 | orientation: EditorLayoutOrientation.vertical, 41 | groups: [{}, {}], 42 | size: 0.5, 43 | }, 44 | { 45 | orientation: EditorLayoutOrientation.vertical, 46 | groups: [{}, {}], 47 | size: 0.5, 48 | }, 49 | ], 50 | }, 51 | }; 52 | 53 | export enum SwingLayout { 54 | grid = "grid", 55 | preview = "preview", 56 | splitBottom = "splitBottom", 57 | splitLeft = "splitLeft", 58 | splitLeftTabbed = "splitLeftTabbed", 59 | splitRight = "splitRight", 60 | splitRightTabbed = "splitRightTabbed", 61 | splitTop = "splitTop", 62 | } 63 | 64 | export async function createLayoutManager( 65 | includedFiles: number, 66 | layout?: string 67 | ) { 68 | if (!layout) { 69 | layout = await config.get("layout"); 70 | } 71 | 72 | let currentViewColumn = ViewColumn.One; 73 | let previewViewColumn = includedFiles + 1; 74 | 75 | let editorLayout: any; 76 | if (includedFiles === 3) { 77 | editorLayout = 78 | layout === SwingLayout.grid 79 | ? EditorLayouts.grid 80 | : EditorLayouts.splitThree; 81 | } else if (includedFiles === 2) { 82 | editorLayout = EditorLayouts.splitTwo; 83 | } else { 84 | editorLayout = EditorLayouts.splitOne; 85 | } 86 | 87 | if (layout === SwingLayout.splitRight) { 88 | editorLayout = { 89 | ...editorLayout, 90 | groups: [...editorLayout.groups].reverse(), 91 | }; 92 | 93 | currentViewColumn = ViewColumn.Two; 94 | previewViewColumn = ViewColumn.One; 95 | } else if (layout === SwingLayout.splitTop) { 96 | editorLayout = { 97 | ...editorLayout, 98 | orientation: EditorLayoutOrientation.vertical, 99 | }; 100 | } else if (layout === SwingLayout.splitBottom) { 101 | editorLayout = { 102 | orientation: EditorLayoutOrientation.vertical, 103 | groups: [...editorLayout.groups].reverse(), 104 | }; 105 | 106 | currentViewColumn = ViewColumn.Two; 107 | previewViewColumn = ViewColumn.One; 108 | } else if (layout === SwingLayout.splitLeftTabbed) { 109 | editorLayout = EditorLayouts.splitOne; 110 | previewViewColumn = ViewColumn.Two; 111 | } else if (layout === SwingLayout.splitRightTabbed) { 112 | editorLayout = EditorLayouts.splitOne; 113 | 114 | currentViewColumn = ViewColumn.Two; 115 | previewViewColumn = ViewColumn.One; 116 | } 117 | 118 | await commands.executeCommand("workbench.action.closeAllEditors"); 119 | await commands.executeCommand("workbench.action.closePanel"); 120 | await commands.executeCommand("workbench.action.closeSidebar"); 121 | 122 | // The preview layout mode only shows a single file, 123 | // so there's no need to set a custom editor layout for it. 124 | if (includedFiles > 0 && layout !== SwingLayout.preview) { 125 | await commands.executeCommand("vscode.setEditorLayout", editorLayout); 126 | } 127 | 128 | return { 129 | previewViewColumn, 130 | showDocument: async function( 131 | document: TextDocument, 132 | preserveFocus: boolean = true 133 | ) { 134 | if (layout === SwingLayout.preview) { 135 | return; 136 | } 137 | 138 | const editor = window.showTextDocument(document, { 139 | preview: false, 140 | viewColumn: currentViewColumn, 141 | preserveFocus, 142 | }); 143 | 144 | if ( 145 | layout !== SwingLayout.splitLeftTabbed && 146 | layout !== SwingLayout.splitRightTabbed 147 | ) { 148 | currentViewColumn++; 149 | } 150 | 151 | return editor; 152 | }, 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /src/preview/libraries/cdnjs.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const LIBRARIES_URL = "https://api.cdnjs.com/libraries"; 4 | 5 | export interface CdnJsLibrary { 6 | name: string; 7 | description: string; 8 | latest: string; 9 | } 10 | 11 | export interface CdnJsLibraryVersion { 12 | version: string; 13 | files: string[]; 14 | } 15 | 16 | let libraries: CdnJsLibrary[] | undefined; 17 | async function getLibrariesInternal(): Promise { 18 | try { 19 | const response = await axios.get<{ results: CdnJsLibrary[] }>( 20 | `${LIBRARIES_URL}?fields=description` 21 | ); 22 | 23 | libraries = response.data.results; 24 | return libraries; 25 | } catch { 26 | throw new Error("Cannot get the libraries."); 27 | } 28 | } 29 | 30 | let currentGetLibrariesPromise: Promise | undefined; 31 | export async function getCdnJsLibraries() { 32 | if (libraries) { 33 | return libraries; 34 | } 35 | 36 | if (currentGetLibrariesPromise) { 37 | return await currentGetLibrariesPromise; 38 | } 39 | 40 | currentGetLibrariesPromise = getLibrariesInternal(); 41 | return await currentGetLibrariesPromise; 42 | } 43 | 44 | export async function getLibraryVersions( 45 | libraryName: string 46 | ): Promise { 47 | try { 48 | const { 49 | data: { assets }, 50 | } = await axios.get(`${LIBRARIES_URL}/${libraryName}?fields=assets`); 51 | 52 | // The CDNJS API returns the versions 53 | // in ascending order, so we want to reverse it. 54 | return assets.reverse(); 55 | } catch { 56 | return []; 57 | } 58 | } 59 | 60 | export async function searchPackages(searchString: string) { 61 | const librariesResponse = await axios.get<{ results: CdnJsLibrary[] }>( 62 | `${LIBRARIES_URL}?search=${searchString}` 63 | ); 64 | 65 | return librariesResponse.data.results; 66 | } 67 | -------------------------------------------------------------------------------- /src/preview/libraries/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Uri } from "vscode"; 3 | import { DEFAULT_MANIFEST } from ".."; 4 | import { SWING_FILE, URI_PATTERN } from "../../constants"; 5 | import { SwingLibraryType, SwingManifest, store } from "../../store"; 6 | import { byteArrayToString, stringToByteArray } from "../../utils"; 7 | import { 8 | CdnJsLibraryVersion, 9 | getCdnJsLibraries, 10 | getLibraryVersions, 11 | searchPackages, 12 | } from "./cdnjs"; 13 | import { hasDefaultExport } from "./skypack"; 14 | import debounce = require("debounce"); 15 | 16 | async function libraryToVersionsPickList(libraryName: string) { 17 | const versions = await getLibraryVersions(libraryName); 18 | return versions.map((version) => ({ 19 | label: version.version, 20 | version, 21 | })); 22 | } 23 | 24 | function libraryFilesToPickList(files: string[]) { 25 | return files.map((file) => ({ 26 | label: file, 27 | })); 28 | } 29 | 30 | function createLibraryUrl( 31 | libraryName: string, 32 | libraryVersion: string, 33 | libraryFile: string 34 | ) { 35 | return `https://cdnjs.cloudflare.com/ajax/libs/${libraryName}/${libraryVersion}/${libraryFile}`; 36 | } 37 | 38 | function getSwingManifest(text: string): SwingManifest { 39 | try { 40 | const json = JSON.parse(text) as SwingManifest; 41 | 42 | return { 43 | ...DEFAULT_MANIFEST, 44 | ...json, 45 | }; 46 | } catch { 47 | return DEFAULT_MANIFEST; 48 | } 49 | } 50 | 51 | async function addDependencyLink( 52 | libraryType: SwingLibraryType, 53 | libraryUrl: string 54 | ) { 55 | const uri = Uri.joinPath(store.activeSwing!.rootUri, SWING_FILE); 56 | 57 | let manifest; 58 | try { 59 | const content = byteArrayToString(await vscode.workspace.fs.readFile(uri)); 60 | manifest = getSwingManifest(content); 61 | } catch (e) { 62 | manifest = DEFAULT_MANIFEST; 63 | } 64 | 65 | manifest[libraryType]!.push(libraryUrl); 66 | manifest[libraryType] = [...new Set(manifest[libraryType])]; 67 | 68 | const updatedContent = JSON.stringify(manifest, null, 2); 69 | vscode.workspace.fs.writeFile(uri, stringToByteArray(updatedContent)); 70 | 71 | store.activeSwing!.webView.updateManifest(updatedContent, true); 72 | } 73 | 74 | const EXCLUDED_LABELS = ["cjs", "esm", ".min.", ".prod."]; 75 | const EXCLUDED_FILES = [".mjs", ".map"]; 76 | 77 | function filterVersionFiles({ files }: CdnJsLibraryVersion) { 78 | return files.filter( 79 | (file) => 80 | EXCLUDED_LABELS.every((label) => !file.includes(label)) && 81 | EXCLUDED_FILES.every((excludedFile) => !file.endsWith(excludedFile)) 82 | ); 83 | } 84 | 85 | export async function addSwingLibrary(libraryType: SwingLibraryType) { 86 | const libraries = await getCdnJsLibraries(); 87 | const libraryPickListItems = libraries.map((library) => { 88 | return { 89 | label: library.name, 90 | description: library.description, 91 | library, 92 | }; 93 | }); 94 | 95 | const list = vscode.window.createQuickPick(); 96 | list.placeholder = "Select the library you'd like to add to the swing"; 97 | list.items = libraryPickListItems; 98 | 99 | list.onDidChangeValue((value) => { 100 | list.items = 101 | value && value.match(URI_PATTERN) 102 | ? [{ label: value }, ...libraryPickListItems] 103 | : libraryPickListItems; 104 | }); 105 | 106 | const clipboardValue = await vscode.env.clipboard.readText(); 107 | if (clipboardValue && clipboardValue.match(URI_PATTERN)) { 108 | list.value = clipboardValue; 109 | list.items = [{ label: clipboardValue }, ...libraryPickListItems]; 110 | } 111 | 112 | list.onDidAccept(async () => { 113 | const libraryAnswer = list.selectedItems[0] || list.value; 114 | 115 | list.hide(); 116 | 117 | if (libraryAnswer.label.match(URI_PATTERN)) { 118 | return await addDependencyLink(libraryType, libraryAnswer.label); 119 | } 120 | 121 | const libraryVersionAnswer = await vscode.window.showQuickPick( 122 | await libraryToVersionsPickList(libraryAnswer.label), 123 | { 124 | placeHolder: "Select the library version you'd like to use", 125 | } 126 | ); 127 | 128 | if (!libraryVersionAnswer) { 129 | return; 130 | } 131 | 132 | const libraryFiles = filterVersionFiles(libraryVersionAnswer.version); 133 | 134 | const fileAnswer = 135 | libraryFiles.length > 1 136 | ? await vscode.window.showQuickPick( 137 | await libraryFilesToPickList(libraryFiles), 138 | { 139 | placeHolder: "Select file version", 140 | } 141 | ) 142 | : { label: libraryFiles[0] }; 143 | 144 | if (!fileAnswer) { 145 | return; 146 | } 147 | 148 | const libraryUrl = createLibraryUrl( 149 | (libraryAnswer).library.name, 150 | libraryVersionAnswer.label, 151 | fileAnswer.label 152 | ); 153 | 154 | await addDependencyLink(libraryType, libraryUrl); 155 | }); 156 | 157 | list.show(); 158 | } 159 | 160 | // JavaScript module functions 161 | 162 | const DEFAULT_MODULES = [ 163 | ["angular", "HTML enhanced for web apps"], 164 | ["react", "React is a JavaScript library for building user interfaces."], 165 | ["react-dom", "React package for working with the DOM."], 166 | ["vue", "Reactive, component-oriented view layer for modern web interfaces."], 167 | ]; 168 | 169 | async function addModuleImport(moduleName: string) { 170 | const { camel } = require("case"); 171 | const importName = camel(moduleName); 172 | 173 | const prefix = (await hasDefaultExport(moduleName)) ? "" : "* as "; 174 | const importContent = `import ${prefix}${importName} from "${moduleName}";\n\n`; 175 | store.activeSwing!.scriptEditor?.edit((edit) => { 176 | edit.insert(new vscode.Position(0, 0), importContent); 177 | }); 178 | } 179 | 180 | export async function addScriptModule() { 181 | const list = vscode.window.createQuickPick(); 182 | list.placeholder = "Specify the module name you'd like to add"; 183 | list.items = DEFAULT_MODULES.map(([label, description]) => ({ 184 | label, 185 | description, 186 | })); 187 | 188 | list.onDidChangeValue( 189 | debounce( 190 | async (value) => { 191 | if (value === "") { 192 | list.items = DEFAULT_MODULES.map(([label, description]) => ({ 193 | label, 194 | description, 195 | })); 196 | } else { 197 | list.busy = true; 198 | list.items = [{ label: `Searching modules for "${value}"...` }]; 199 | const modules = await searchPackages(value); 200 | list.items = modules.map((module) => ({ 201 | label: module.name, 202 | description: module.latest, 203 | })); 204 | 205 | list.busy = false; 206 | } 207 | }, 208 | 100, 209 | { immediate: true } 210 | ) 211 | ); 212 | 213 | list.onDidAccept(async () => { 214 | list.hide(); 215 | 216 | const moduleAnswer = list.selectedItems[0]; 217 | if (!moduleAnswer) { 218 | return; 219 | } 220 | 221 | await addModuleImport(moduleAnswer.label); 222 | }); 223 | 224 | list.show(); 225 | } 226 | -------------------------------------------------------------------------------- /src/preview/libraries/skypack.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export function getModuleUrl(moduleName: string) { 4 | return `https://cdn.skypack.dev/${moduleName}`; 5 | } 6 | 7 | export async function hasDefaultExport(moduleName: string) { 8 | const moduleUrl = getModuleUrl(moduleName); 9 | const { data } = await axios.get(`${moduleUrl}?meta`); 10 | return data.packageExports?.["."]?.hasDefaultExport || false; 11 | } 12 | 13 | const IMPORT_PATTERN = /(import\s.+\sfrom\s)(["'])(?!\.\/|http)(.+)\2/gi; 14 | const IMPORT_SUBSTITION = `$1$2https://esm.sh/$3$2`; 15 | export function processImports(code: string) { 16 | return code 17 | .replace(IMPORT_PATTERN, IMPORT_SUBSTITION) 18 | .replace(/\.\/(\S+)\.(svelte|vue|jsx|tsx|json|css)/g, "./$1.js?type=$2"); 19 | } 20 | -------------------------------------------------------------------------------- /src/preview/proxyFileSystemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as vscode from "vscode"; 3 | import { byteArrayToString, stringToByteArray } from "../utils"; 4 | import { compileComponent } from "./languages/components/svelte"; 5 | import { compileScriptContent } from "./languages/script"; 6 | import { processImports } from "./libraries/skypack"; 7 | 8 | export class ProxyFileSystemProvider implements vscode.FileSystemProvider { 9 | private _emitter = new vscode.EventEmitter(); 10 | readonly onDidChangeFile: vscode.Event = this 11 | ._emitter.event; 12 | 13 | static SCHEME = "codeswing-proxy"; 14 | 15 | stat(uri: vscode.Uri): vscode.FileStat { 16 | return { 17 | type: vscode.FileType.File, 18 | ctime: Date.now(), 19 | mtime: Date.now(), 20 | size: 0, 21 | }; 22 | } 23 | 24 | async readFile(uri: vscode.Uri): Promise { 25 | let proxyUri = vscode.Uri.parse(decodeURIComponent(uri.path.substr(1))); 26 | 27 | const extension = path.extname(uri.path); 28 | if (extension === ".js") { 29 | let type; 30 | if (uri.query) { 31 | const query = new URLSearchParams(uri.query); 32 | type = query.get("type"); 33 | proxyUri = proxyUri.with({ 34 | path: proxyUri.path.replace(".js", `.${type}`), 35 | query: "", 36 | }); 37 | } 38 | 39 | let contents = byteArrayToString( 40 | await vscode.workspace.fs.readFile(proxyUri) 41 | ); 42 | if (type === "svelte") { 43 | [contents] = await compileComponent(contents); 44 | } else if (type === "jsx" || type === "tsx") { 45 | const compiledContent = await compileScriptContent( 46 | contents, 47 | `.${type}` 48 | ); 49 | if (compiledContent) { 50 | contents = compiledContent; 51 | } 52 | } else if (type === "json") { 53 | contents = `export default ${contents}`; 54 | } else if (type === "css") { 55 | contents = `const styleElement = document.createElement("style"); 56 | styleElement.textContent = \`${contents}\`; 57 | document.head.appendChild(styleElement);`; 58 | } 59 | 60 | return stringToByteArray(processImports(contents)); 61 | } else { 62 | return vscode.workspace.fs.readFile(proxyUri); 63 | } 64 | } 65 | 66 | public static getProxyUri(uri: vscode.Uri) { 67 | return vscode.Uri.parse( 68 | `${ProxyFileSystemProvider.SCHEME}:/${encodeURIComponent(uri.toString())}` 69 | ); 70 | } 71 | 72 | writeFile( 73 | uri: vscode.Uri, 74 | content: Uint8Array, 75 | options: { create: boolean; overwrite: boolean } 76 | ): void { 77 | throw vscode.FileSystemError.NoPermissions("Not supported"); 78 | } 79 | 80 | delete(uri: vscode.Uri): void { 81 | throw vscode.FileSystemError.NoPermissions("Not supported"); 82 | } 83 | 84 | readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { 85 | throw vscode.FileSystemError.NoPermissions("Not supported"); 86 | } 87 | 88 | rename( 89 | oldUri: vscode.Uri, 90 | newUri: vscode.Uri, 91 | options: { overwrite: boolean } 92 | ): void { 93 | throw vscode.FileSystemError.NoPermissions("Not supported"); 94 | } 95 | 96 | createDirectory(uri: vscode.Uri): void { 97 | throw vscode.FileSystemError.NoPermissions("Not supported"); 98 | } 99 | 100 | watch(_resource: vscode.Uri): vscode.Disposable { 101 | throw vscode.FileSystemError.NoPermissions("Not supported"); 102 | } 103 | } 104 | 105 | export function registerProxyFileSystemProvider() { 106 | vscode.workspace.registerFileSystemProvider( 107 | ProxyFileSystemProvider.SCHEME, 108 | new ProxyFileSystemProvider(), 109 | { 110 | isReadonly: true, 111 | } 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/preview/stylesheets/stylesheets.d.ts: -------------------------------------------------------------------------------- 1 | declare module "raw-loader!*" { 2 | const value: string; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /src/preview/stylesheets/themeStyles.css: -------------------------------------------------------------------------------- 1 | *:focus { 2 | outline: var(--vscode-focusBorder) solid 1px; 3 | } 4 | 5 | ::selection { 6 | background-color: var(--vscode-editor-selectionBackground); 7 | } 8 | 9 | body { 10 | width: auto; 11 | height: auto; 12 | background-color: var(--vscode-editor-background, white); 13 | color: var(--vscode-editor-foreground, revert); 14 | caret-color: var(--vscode-editorCursor-foreground); 15 | font-family: var(--vscode-font-family, sans-serif); 16 | font-weight: var(--vscode-font-weight); 17 | font-size: var(--vscode-font-size); 18 | } 19 | 20 | button { 21 | background-color: var(--vscode-button-background); 22 | color: var(--vscode-button-foreground); 23 | border: none; 24 | outline-offset: 2px; 25 | padding: 4px 8px; 26 | } 27 | 28 | button:hover { 29 | background-color: var(--vscode-button-hoverBackground); 30 | } 31 | 32 | a:link { 33 | color: var(--vscode-textLink-foreground); 34 | } 35 | 36 | a:link:active { 37 | color: var(--vscode-textLink-activeForeground); 38 | } 39 | 40 | blockquote { 41 | background-color: var(--vscode-textBlockQuote-background); 42 | border-left: solid 2px var(--vscode-textBlockQuote-border); 43 | padding: 0.1px 1em; 44 | } 45 | 46 | hr { 47 | border-style: solid; 48 | border-width: 1px 0 0 0; 49 | border-color: var(--vscode-textSeparator-foreground); 50 | } 51 | 52 | pre, code, samp, var { 53 | font-family: var(--vscode-editor-font-family, monospace); 54 | font-weight: var(--vscode-editor-font-weight); 55 | font-size: var(--vscode-editor-font-size); 56 | } 57 | 58 | pre, code { 59 | color: var(--vscode-textPreformat-foreground); 60 | } 61 | 62 | pre { 63 | background-color: var(--vscode-textCodeBlock-background); 64 | padding: 4px; 65 | overflow: hidden; 66 | } 67 | 68 | pre ins, pre del { 69 | text-decoration: none; 70 | position: relative; 71 | display: inline-block; 72 | z-index: 0; 73 | } 74 | 75 | pre ins::before, pre del::before { 76 | content: ''; 77 | position: absolute; 78 | left: -100vw; 79 | width: 200vw; 80 | top: 0; 81 | bottom: 0; 82 | z-index: -1; 83 | border-top: 1px solid transparent; 84 | border-bottom: 1px solid transparent; 85 | } 86 | 87 | pre ins::before { 88 | background-color: var(--vscode-diffEditor-insertedTextBackground); 89 | border-color: var(--vscode-diffEditor-insertedTextBorder, transparent); 90 | } 91 | 92 | pre del::before { 93 | background-color: var(--vscode-diffEditor-removedTextBackground); 94 | border-color: var(--vscode-diffEditor-removedTextBorder, transparent); 95 | } 96 | 97 | details summary { 98 | outline-offset: 2px; 99 | } 100 | 101 | details[open] summary { 102 | margin-bottom: 2px; 103 | } 104 | 105 | fieldset { 106 | border: solid 1px var(--vscode-textSeparator-foreground); 107 | } 108 | 109 | textarea, input, select { 110 | border: none; 111 | background-color: var(--vscode-input-background); 112 | color: var(--vscode-input-foreground); 113 | padding: 4px; 114 | } 115 | 116 | textarea::placeholder, input::placeholder { 117 | color: var(--vscode-input-placeholderForeground); 118 | } 119 | 120 | select { 121 | padding: 2px; 122 | } 123 | 124 | textarea:focus, input:focus, select:focus { 125 | color: var(--vscode-inputOption-activeForeground); 126 | outline: var(--vscode-focusBorder) solid 1px; 127 | outline-offset: -1px; 128 | } 129 | 130 | textarea:invalid, input:invalid, select:invalid { 131 | outline: var(--vscode-inputValidation-errorBorder) solid 1px; 132 | outline-offset: -1px; 133 | } 134 | 135 | input[type="checkbox"]:focus, input[type="radio"]:focus, 136 | input[type="checkbox"]:invalid, input[type="radio"]:invalid { 137 | outline-offset: revert; 138 | } 139 | 140 | kbd { 141 | font-family: var(--vscode-font-family, sans-serif); 142 | font-size: calc(var(--vscode-font-size) * .85); 143 | color: var(--vscode-keybindingLabel--foreground); 144 | border-radius: 3px; 145 | padding: 0 4px; 146 | background-color: var(--vscode-keybindingLabel-background); 147 | border: 1px solid var(--vscode-keybindingLabel-border); 148 | border-bottom-color: var(--vscode-editor-background); 149 | box-shadow: 0 1px 0 var(--vscode-keybindingLabel-bottomBorder); 150 | } 151 | 152 | kbd + kbd { 153 | margin-left: 4px; 154 | } 155 | -------------------------------------------------------------------------------- /src/preview/tour.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Event, Uri } from "vscode"; 3 | import { EXTENSION_NAME } from "../constants"; 4 | import { store } from "../store"; 5 | import { byteArrayToString, stringToByteArray, withProgress } from "../utils"; 6 | 7 | export const TOUR_FILE = "main.tour"; 8 | 9 | interface CodeTourApi { 10 | startTour( 11 | tour: any, 12 | stepNumber: number, 13 | workspaceRoot: Uri, 14 | startInEditMode: boolean 15 | ): void; 16 | 17 | endCurrentTour(): void; 18 | exportTour(tour: any): string; 19 | recordTour(workspaceRoot: Uri): void; 20 | 21 | promptForTour(workspaceRoot: Uri, tours: any[]): Promise; 22 | selectTour(tours: any[], workspaceRoot: Uri): Promise; 23 | 24 | onDidEndTour: Event; 25 | } 26 | 27 | let codeTourApi: CodeTourApi; 28 | async function ensureApi() { 29 | if (!codeTourApi) { 30 | const codeTour = vscode.extensions.getExtension("vsls-contrib.codetour"); 31 | if (!codeTour) { 32 | return; 33 | } 34 | if (!codeTour.isActive) { 35 | await codeTour.activate(); 36 | } 37 | 38 | codeTourApi = codeTour.exports; 39 | } 40 | } 41 | 42 | export async function isCodeTourInstalled() { 43 | await ensureApi(); 44 | return !!codeTourApi; 45 | } 46 | 47 | export async function startTour( 48 | tour: any, 49 | workspaceRoot: Uri, 50 | startInEditMode: boolean = false 51 | ) { 52 | tour.id = Uri.joinPath(workspaceRoot, TOUR_FILE).toString(); 53 | codeTourApi.startTour(tour, 0, workspaceRoot, startInEditMode); 54 | } 55 | 56 | export async function startTourFromUri(tourUri: Uri, workspaceRoot: Uri) { 57 | try { 58 | const contents = await vscode.workspace.fs.readFile(tourUri); 59 | const tour = JSON.parse(byteArrayToString(contents)); 60 | startTour(tour, workspaceRoot); 61 | } catch {} 62 | } 63 | 64 | export async function endCurrentTour() { 65 | codeTourApi.endCurrentTour(); 66 | } 67 | 68 | export async function registerTourCommands(context: vscode.ExtensionContext) { 69 | context.subscriptions.push( 70 | vscode.commands.registerCommand( 71 | `${EXTENSION_NAME}.recordCodeTour`, 72 | async () => 73 | withProgress("Starting tour recorder...", async () => { 74 | const { rootUri: uri } = store.activeSwing!; 75 | 76 | const tour = { 77 | title: "CodeSwing", 78 | steps: [], 79 | }; 80 | 81 | const tourUri = vscode.Uri.joinPath(uri, TOUR_FILE); 82 | const tourContent = JSON.stringify(tour, null, 2); 83 | await vscode.workspace.fs.writeFile( 84 | tourUri, 85 | stringToByteArray(tourContent) 86 | ); 87 | 88 | startTour(tour, uri, true); 89 | store.activeSwing!.hasTour = true; 90 | }) 91 | ) 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/preview/tree/activeSwing.ts: -------------------------------------------------------------------------------- 1 | import { reaction } from "mobx"; 2 | import { 3 | Disposable, 4 | Event, 5 | EventEmitter, 6 | FileType, 7 | ProviderResult, 8 | TreeDataProvider, 9 | TreeItem, 10 | Uri, 11 | window, 12 | workspace, 13 | } from "vscode"; 14 | import { EXTENSION_NAME } from "../../constants"; 15 | import { store } from "../../store"; 16 | import { CodeSwingDirectoryNode, CodeSwingFileNode } from "./nodes"; 17 | 18 | async function getSwingFiles(subDirectory?: string) { 19 | const swingUri = store.activeSwing!.rootUri; 20 | const directory = subDirectory ? `${subDirectory}/` : ""; 21 | const files = await workspace.fs.readDirectory( 22 | Uri.joinPath(swingUri, directory) 23 | ); 24 | 25 | return files 26 | .sort(([_, typeA], [__, typeB]) => typeB - typeA) 27 | .map(([file, fileType]) => { 28 | const filePath = `${directory}${file}`; 29 | return fileType === FileType.Directory 30 | ? new CodeSwingDirectoryNode(swingUri, filePath) 31 | : new CodeSwingFileNode(swingUri, filePath); 32 | }); 33 | } 34 | 35 | class ActiveSwingTreeProvider 36 | implements TreeDataProvider, Disposable { 37 | private _disposables: Disposable[] = []; 38 | 39 | private _onDidChangeTreeData = new EventEmitter(); 40 | public readonly onDidChangeTreeData: Event = this._onDidChangeTreeData 41 | .event; 42 | 43 | constructor() { 44 | reaction(() => [store.activeSwing], this.refreshTree.bind(this)); 45 | } 46 | 47 | getTreeItem = (node: TreeItem) => node; 48 | 49 | getChildren(element?: TreeItem): ProviderResult { 50 | if (!element) { 51 | if (!store.activeSwing) { 52 | return undefined; 53 | } 54 | 55 | return getSwingFiles(); 56 | } else if (element instanceof CodeSwingDirectoryNode) { 57 | return getSwingFiles(element.directory); 58 | } 59 | } 60 | 61 | dispose() { 62 | this._disposables.forEach((disposable) => disposable.dispose()); 63 | } 64 | 65 | refreshTree() { 66 | this._onDidChangeTreeData.fire(); 67 | } 68 | } 69 | 70 | let treeDataProvider: ActiveSwingTreeProvider; 71 | export function refreshTreeView() { 72 | treeDataProvider.refreshTree(); 73 | } 74 | 75 | export function registerTreeProvider() { 76 | treeDataProvider = new ActiveSwingTreeProvider(); 77 | window.createTreeView(`${EXTENSION_NAME}.activeSwing`, { 78 | showCollapseAll: true, 79 | treeDataProvider, 80 | canSelectMany: true, 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/preview/tree/commands.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { commands, ExtensionContext, Uri, window, workspace } from "vscode"; 3 | import { EXTENSION_NAME } from "../../constants"; 4 | import { store } from "../../store"; 5 | import { withProgress } from "../../utils"; 6 | import { refreshTreeView } from "./activeSwing"; 7 | import { CodeSwingDirectoryNode, CodeSwingFileNode } from "./nodes"; 8 | 9 | export function registerTreeViewCommands(context: ExtensionContext) { 10 | context.subscriptions.push( 11 | commands.registerCommand( 12 | `${EXTENSION_NAME}.addSwingFile`, 13 | async (node?: CodeSwingDirectoryNode) => { 14 | const file = await window.showInputBox({ 15 | placeHolder: "Enter the name of the file you'd like to add", 16 | }); 17 | 18 | if (!file) { 19 | return; 20 | } 21 | 22 | const filePath = 23 | node && node.directory ? `${node.directory}/${file}` : file; 24 | const uri = Uri.joinPath(store.activeSwing!.rootUri, filePath); 25 | 26 | await workspace.fs.writeFile(uri, new Uint8Array()); 27 | window.showTextDocument(uri); 28 | refreshTreeView(); 29 | } 30 | ) 31 | ); 32 | 33 | context.subscriptions.push( 34 | commands.registerCommand( 35 | `${EXTENSION_NAME}.uploadSwingFile`, 36 | async (node?: CodeSwingDirectoryNode) => { 37 | const files = await window.showOpenDialog({ 38 | canSelectFiles: true, 39 | canSelectFolders: false, 40 | canSelectMany: true, 41 | openLabel: "Upload", 42 | }); 43 | 44 | if (!files) { 45 | return; 46 | } 47 | 48 | await Promise.all( 49 | files.map(async (file) => { 50 | const contents = await workspace.fs.readFile(file); 51 | 52 | const fileName = path.basename(file.path); 53 | const filePath = node ? `${node.directory}/${fileName}` : fileName; 54 | const uri = Uri.joinPath(store.activeSwing!.rootUri, filePath); 55 | 56 | await workspace.fs.writeFile(uri, contents); 57 | }) 58 | ); 59 | 60 | refreshTreeView(); 61 | 62 | // We're assuming the uploaded file impacts 63 | // the rendering of the swing. 64 | store.activeSwing!.webView.rebuildWebview(); 65 | } 66 | ) 67 | ); 68 | 69 | context.subscriptions.push( 70 | commands.registerCommand( 71 | `${EXTENSION_NAME}.renameSwingFile`, 72 | async (node: CodeSwingFileNode) => { 73 | const file = await window.showInputBox({ 74 | placeHolder: "Enter the name you'd like to rename this file to", 75 | value: node.file, 76 | }); 77 | 78 | if (!file) { 79 | return; 80 | } 81 | 82 | const newUri = Uri.joinPath(store.activeSwing!.rootUri, file); 83 | 84 | await withProgress("Renaming file...", async () => { 85 | // If the file being renamed is dirty, then we 86 | // need to save it before renaming it. Otherwise, 87 | // VS Code will retain the old file and show it as 88 | // deleted, since they don't want to lose the changing. 89 | const visibleDocument = window.visibleTextEditors.find( 90 | (editor) => 91 | editor.document.uri.toString() === node.resourceUri!.toString() 92 | ); 93 | if (visibleDocument) { 94 | await visibleDocument.document.save(); 95 | } 96 | 97 | await workspace.fs.rename(node.resourceUri!, newUri); 98 | }); 99 | 100 | refreshTreeView(); 101 | } 102 | ) 103 | ); 104 | 105 | context.subscriptions.push( 106 | commands.registerCommand( 107 | `${EXTENSION_NAME}.deleteSwingFile`, 108 | async (node: CodeSwingFileNode) => { 109 | const message = `Are you sure you want to delete the "${node.file}" file?`; 110 | if (await window.showInformationMessage(message, "Delete")) { 111 | await workspace.fs.delete(node.resourceUri!); 112 | refreshTreeView(); 113 | } 114 | } 115 | ) 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/preview/tree/index.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from "vscode"; 2 | import { registerTreeProvider } from "./activeSwing"; 3 | import { registerTreeViewCommands } from "./commands"; 4 | 5 | export function registerTreeViewModule(context: ExtensionContext) { 6 | registerTreeViewCommands(context); 7 | registerTreeProvider(); 8 | } 9 | -------------------------------------------------------------------------------- /src/preview/tree/nodes.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; 3 | 4 | export class CodeSwingFileNode extends TreeItem { 5 | constructor(public swingUri: Uri, public file: string) { 6 | super(path.basename(file), TreeItemCollapsibleState.None); 7 | 8 | this.iconPath = ThemeIcon.File; 9 | this.resourceUri = Uri.joinPath(swingUri, file); 10 | this.contextValue = "swing.file"; 11 | 12 | this.command = { 13 | command: "vscode.open", 14 | title: "Open File", 15 | arguments: [this.resourceUri], 16 | }; 17 | } 18 | } 19 | 20 | export class CodeSwingDirectoryNode extends TreeItem { 21 | constructor(public swingUri: Uri, public directory: string) { 22 | super(path.basename(directory), TreeItemCollapsibleState.Collapsed); 23 | 24 | this.iconPath = ThemeIcon.Folder; 25 | this.resourceUri = Uri.joinPath(swingUri, directory); 26 | this.contextValue = "swing.directory"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/preview/tutorials/index.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from "vscode"; 2 | import { registerInputFileSystemProvider } from "./inputFileSystem"; 3 | import { initializeStorage } from "./storage"; 4 | 5 | export async function registerTutorialModule(context: ExtensionContext, syncKeys: string[]) { 6 | registerInputFileSystemProvider(); 7 | initializeStorage(context, syncKeys); 8 | } 9 | -------------------------------------------------------------------------------- /src/preview/tutorials/inputFileSystem.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as vscode from "vscode"; 3 | import { INPUT_SCHEME } from "../../constants"; 4 | import { byteArrayToString, stringToByteArray } from "../../utils"; 5 | 6 | export class InputFileSystemProvider implements vscode.FileSystemProvider { 7 | private _emitter = new vscode.EventEmitter(); 8 | readonly onDidChangeFile: vscode.Event = this 9 | ._emitter.event; 10 | 11 | private files = new Map(); 12 | 13 | stat(uri: vscode.Uri): vscode.FileStat { 14 | return { 15 | type: vscode.FileType.File, 16 | ctime: Date.now(), 17 | mtime: Date.now(), 18 | size: 100, 19 | }; 20 | } 21 | 22 | readFile(uri: vscode.Uri): Uint8Array { 23 | const inputName = path.basename(uri.path); 24 | const input = this.files.get(inputName); 25 | return stringToByteArray(input); 26 | } 27 | 28 | writeFile( 29 | uri: vscode.Uri, 30 | content: Uint8Array, 31 | options: { create: boolean; overwrite: boolean } 32 | ): void { 33 | const inputName = path.basename(uri.path); 34 | this.files.set(inputName, byteArrayToString(content)); 35 | } 36 | 37 | delete(uri: vscode.Uri): void { 38 | const inputName = path.basename(uri.path); 39 | this.files.delete(inputName); 40 | } 41 | 42 | readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { 43 | throw vscode.FileSystemError.NoPermissions("Not supported"); 44 | } 45 | 46 | rename( 47 | oldUri: vscode.Uri, 48 | newUri: vscode.Uri, 49 | options: { overwrite: boolean } 50 | ): void { 51 | throw vscode.FileSystemError.NoPermissions("Not supported"); 52 | } 53 | 54 | createDirectory(uri: vscode.Uri): void { 55 | throw vscode.FileSystemError.NoPermissions("Not supported"); 56 | } 57 | 58 | watch(_resource: vscode.Uri): vscode.Disposable { 59 | return { dispose: () => {} }; 60 | } 61 | } 62 | 63 | export function registerInputFileSystemProvider() { 64 | vscode.workspace.registerFileSystemProvider( 65 | INPUT_SCHEME, 66 | new InputFileSystemProvider() 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/preview/tutorials/storage.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, Uri } from "vscode"; 2 | import { EXTENSION_NAME } from "../../constants"; 3 | import { store } from "../../store"; 4 | 5 | const TUTORIAL_KEY = `${EXTENSION_NAME}:tutorials`; 6 | 7 | export interface IStorage { 8 | currentTutorialStep(uri?: Uri): number; 9 | setCurrentTutorialStep(tutorialStep: number): Promise; 10 | } 11 | 12 | type TutorialStatus = [string, number]; 13 | 14 | export let storage: IStorage; 15 | export async function initializeStorage(context: ExtensionContext, syncKeys: string[]) { 16 | storage = { 17 | currentTutorialStep(uri: Uri = store.activeSwing!.rootUri): number { 18 | const tutorials = context.globalState.get( 19 | TUTORIAL_KEY, 20 | [] 21 | ); 22 | 23 | const tutorial = tutorials.find(([id, _]) => id === uri.toString()); 24 | return tutorial ? tutorial[1] : 1; 25 | }, 26 | async setCurrentTutorialStep(tutorialStep: number) { 27 | const tutorialId = store.activeSwing!.rootUri.toString(); 28 | const tutorials = context.globalState.get( 29 | TUTORIAL_KEY, 30 | [] 31 | ); 32 | 33 | const tutorial = tutorials.find(([id, _]) => id === tutorialId); 34 | if (tutorial) { 35 | tutorial[1] = tutorialStep; 36 | } else { 37 | tutorials.push([tutorialId, tutorialStep]); 38 | } 39 | 40 | return context.globalState.update(TUTORIAL_KEY, tutorials); 41 | }, 42 | }; 43 | 44 | syncKeys.push(TUTORIAL_KEY); 45 | } 46 | -------------------------------------------------------------------------------- /src/preview/webview.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import themeStyles from "raw-loader!./stylesheets/themeStyles.css"; 3 | import * as vscode from "vscode"; 4 | import { openSwing } from "."; 5 | import * as config from "../config"; 6 | import { URI_PATTERN } from "../constants"; 7 | import { store, SwingLibraryType, SwingManifest } from "../store"; 8 | import { byteArrayToString } from "../utils"; 9 | import { getScriptContent } from "./languages/script"; 10 | import { getCdnJsLibraries } from "./libraries/cdnjs"; 11 | import { ProxyFileSystemProvider } from "./proxyFileSystemProvider"; 12 | import { storage } from "./tutorials/storage"; 13 | 14 | const EXIT_RESPONSE = "Exit Swing"; 15 | 16 | export class SwingWebView { 17 | private css: string = ""; 18 | private html: string = ""; 19 | private javascript: string = ""; 20 | private isJavaScriptModule: boolean = false; 21 | private manifest: SwingManifest | undefined; 22 | private readme: string = ""; 23 | private config: string = ""; 24 | private input: string = ""; 25 | 26 | constructor( 27 | private webview: vscode.Webview, 28 | private output: vscode.OutputChannel, 29 | private swing: vscode.Uri, 30 | private codePenScripts: string = "", 31 | private codePenStyles: string = "", 32 | private totalTutorialSteps?: number, 33 | private tutorialTitle?: string 34 | ) { 35 | webview.onDidReceiveMessage(async ({ command, value }) => { 36 | switch (command) { 37 | case "alert": 38 | if (value) { 39 | vscode.window.showInformationMessage(value); 40 | } 41 | break; 42 | 43 | case "clear": 44 | output.clear(); 45 | break; 46 | 47 | case "log": 48 | output.appendLine(value); 49 | break; 50 | 51 | case "httpRequest": { 52 | let response; 53 | if (value.url.startsWith("http")) { 54 | response = await axios.request({ 55 | url: value.url, 56 | method: value.method, 57 | data: value.body, 58 | headers: JSON.parse(value.headers || "{}"), 59 | responseType: value.responseType || "text", 60 | transformResponse: (data) => data, 61 | }); 62 | } else { 63 | const uri = vscode.Uri.joinPath(this.swing, value.url); 64 | const contents = await vscode.workspace.fs.readFile(uri); 65 | response = { 66 | data: byteArrayToString(contents), 67 | status: 200, 68 | statusText: "OK", 69 | }; 70 | } 71 | 72 | const body = 73 | value.responseType === "arraybuffer" 74 | ? byteArrayToString(response.data) 75 | : response.data; 76 | 77 | webview.postMessage({ 78 | command: "httpResponse", 79 | value: { 80 | id: value.id, 81 | body, 82 | responseType: value.responseType, 83 | status: response.status, 84 | statusText: response.statusText, 85 | source: value.source, 86 | headers: JSON.stringify(response.headers || {}), 87 | }, 88 | }); 89 | break; 90 | } 91 | 92 | case "navigateCode": { 93 | const file = vscode.Uri.joinPath(swing, value.file); 94 | let editor = vscode.window.visibleTextEditors.find( 95 | (editor) => editor.document.uri.toString() === file.toString() 96 | ); 97 | 98 | const line = value.line - 1; 99 | const column = value.column - 1; 100 | const range = new vscode.Range(line, column, line, 1000); 101 | 102 | if (editor) { 103 | editor.selection = new vscode.Selection(range.start, range.end); 104 | } else { 105 | editor = await vscode.window.showTextDocument(file, { 106 | selection: range, 107 | preserveFocus: false, 108 | }); 109 | } 110 | 111 | editor.revealRange(range); 112 | break; 113 | } 114 | 115 | case "navigateTutorial": { 116 | const currentStep = storage.currentTutorialStep(); 117 | const nextStep = currentStep + value; 118 | 119 | // Save all open files, to prevent the user 120 | // from getting a save dialog upon navigation. 121 | await vscode.workspace.saveAll(); 122 | 123 | if (nextStep <= this.totalTutorialSteps!) { 124 | await storage.setCurrentTutorialStep(nextStep); 125 | openSwing(store.activeSwing!.rootUri); 126 | } else { 127 | const completionMessage = 128 | this.manifest!.input && this.manifest!.input!.completionMessage 129 | ? this.manifest!.input!.completionMessage 130 | : "Congratulations! You're completed this tutorial"; 131 | 132 | const response = await vscode.window.showInformationMessage( 133 | completionMessage, 134 | { modal: true }, 135 | EXIT_RESPONSE 136 | ); 137 | 138 | if (response === EXIT_RESPONSE) { 139 | return store.activeSwing?.webViewPanel.dispose(); 140 | } 141 | } 142 | break; 143 | } 144 | 145 | case "openUrl": { 146 | if ((value as string).startsWith("http")) { 147 | vscode.env.openExternal(vscode.Uri.parse(value)); 148 | } else { 149 | const uri = vscode.Uri.joinPath(store.activeSwing!.rootUri, value); 150 | await vscode.commands.executeCommand( 151 | "simpleBrowser.api.open", 152 | uri, 153 | { 154 | viewColumn: vscode.ViewColumn.Beside, 155 | } 156 | ); 157 | } 158 | break; 159 | } 160 | 161 | case "updateTitle": { 162 | const title = value; 163 | store.activeSwing!.webViewPanel.title = `CodeSwing (${title})`; 164 | break; 165 | } 166 | } 167 | }); 168 | } 169 | 170 | public updateCSS(css: string, rebuild = false) { 171 | this.css = css; 172 | 173 | if (rebuild) { 174 | this.webview.postMessage({ command: "updateCSS", value: css }); 175 | } 176 | } 177 | 178 | public updateInput(input: string, rebuild = false) { 179 | this.input = input; 180 | 181 | if (rebuild) { 182 | this.webview.postMessage({ command: "updateInput", value: input }); 183 | } 184 | } 185 | 186 | public async updateReadme(readme: string, rebuild = false) { 187 | this.readme = readme; 188 | 189 | if (rebuild) { 190 | await this.rebuildWebview(); 191 | } 192 | } 193 | 194 | public async updateConfig(config: string, rebuild = false) { 195 | this.config = config; 196 | 197 | if (rebuild) { 198 | await this.rebuildWebview(); 199 | } 200 | } 201 | 202 | public async updateHTML(html: string, rebuild = false) { 203 | this.html = html; 204 | 205 | if (rebuild) { 206 | await this.rebuildWebview(); 207 | } 208 | } 209 | 210 | public async updateJavaScript( 211 | textDocument: vscode.TextDocument, 212 | rebuild = false 213 | ) { 214 | const data = getScriptContent(textDocument, this.manifest); 215 | if (data === null) { 216 | return; 217 | } 218 | 219 | this.javascript = data[0]; 220 | this.isJavaScriptModule = data[1]; 221 | 222 | if (rebuild) { 223 | await this.rebuildWebview(); 224 | } 225 | } 226 | 227 | public async updateManifest(manifest: string, rebuild = false) { 228 | if (!manifest) { 229 | return; 230 | } 231 | 232 | try { 233 | this.manifest = JSON.parse(manifest); 234 | 235 | if (rebuild) { 236 | await this.rebuildWebview(); 237 | } 238 | } catch (e) { 239 | // The user might have typed invalid JSON 240 | } 241 | } 242 | 243 | private async resolveLibraries(libraryType: SwingLibraryType) { 244 | let libraries = 245 | libraryType === SwingLibraryType.script 246 | ? this.codePenScripts 247 | : this.codePenStyles; 248 | 249 | if ( 250 | !this.manifest || 251 | !this.manifest[libraryType] || 252 | this.manifest[libraryType]!.length === 0 253 | ) { 254 | return libraries; 255 | } 256 | 257 | await Promise.all( 258 | this.manifest![libraryType]!.map(async (library) => { 259 | if (!library || (library && !library.trim())) { 260 | return; 261 | } 262 | 263 | const appendLibrary = (url: string) => { 264 | if (libraryType === SwingLibraryType.style) { 265 | libraries += ``; 266 | } else { 267 | libraries += ``; 268 | } 269 | }; 270 | 271 | const isUrl = library.match(URI_PATTERN); 272 | if (isUrl) { 273 | appendLibrary(library); 274 | } else { 275 | const libraries = await getCdnJsLibraries(); 276 | const libraryEntry = libraries.find((lib) => lib.name === library); 277 | 278 | if (!libraryEntry) { 279 | return; 280 | } 281 | 282 | appendLibrary(libraryEntry.latest); 283 | } 284 | }) 285 | ); 286 | 287 | return libraries; 288 | } 289 | 290 | public async rebuildWebview() { 291 | if (config.get("clearConsoleOnRun")) { 292 | this.output.clear(); 293 | } 294 | 295 | // The URL needs to have a trailing slash, or end the URLs could get messed up. 296 | const baseUrl = this.webview 297 | .asWebviewUri( 298 | ProxyFileSystemProvider.getProxyUri( 299 | vscode.Uri.joinPath(this.swing, "/") 300 | ) 301 | ) 302 | .toString(); 303 | const styleId = `swing-style-${Math.random()}`; 304 | 305 | const scripts = await this.resolveLibraries(SwingLibraryType.script); 306 | const styles = await this.resolveLibraries(SwingLibraryType.style); 307 | 308 | const scriptType = this.isJavaScriptModule 309 | ? "module" 310 | : (this.manifest?.scriptType || "text/javascript"); 311 | 312 | const readmeBehavior = 313 | (this.manifest && this.manifest.readmeBehavior) || 314 | (await config.get("readmeBehavior")); 315 | 316 | const header = readmeBehavior === "previewHeader" ? this.readme : ""; 317 | const footer = readmeBehavior === "previewFooter" ? this.readme : ""; 318 | 319 | const shouldUseThemeStyles = 320 | this.manifest?.themePreview ?? config.get("themePreview"); 321 | 322 | // TODO: Refactor this out to a "tutorial renderer" that 323 | // can handle all of the tutorial-specific UI and behavior 324 | let title = ""; 325 | let tutorialNavigation = ""; 326 | if (this.totalTutorialSteps) { 327 | const currentTutorialStep = storage.currentTutorialStep(); 328 | if (this.tutorialTitle) { 329 | title = `${this.tutorialTitle}`; 330 | } 331 | const frame = ` 332 | 333 | 352 | 362 | 363 | 364 | 365 | ${title} 366 |
367 | 370 | Step ${currentTutorialStep} of ${this.totalTutorialSteps} 371 | 374 |
375 |
376 | 377 | 378 | `; 379 | 380 | tutorialNavigation = ``; 381 | } 382 | 383 | this.webview.html = ` 384 | 385 | 386 | 387 | CodeSwing 388 | 410 | ${styles} 411 | 414 | 415 | 416 | 611 | 612 | 613 | ${scripts} 614 | ${tutorialNavigation} 615 | ${header} 616 | ${this.html} 617 | ${footer} 618 | 621 | 622 | `; 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { observable } from "mobx"; 2 | import * as vscode from "vscode"; 3 | import { SwingWebView } from "./preview/webview"; 4 | 5 | export type ScriptType = "text/javascript" | "module"; 6 | export type ReadmeBehavior = 7 | | "none" 8 | | "inputComment" 9 | | "previewHeader" 10 | | "previewFooter"; 11 | 12 | export interface SwingInput { 13 | fileName?: string; 14 | prompt?: string; 15 | completionMessage?: string; 16 | } 17 | 18 | export interface SwingManifest { 19 | scripts?: string[]; 20 | styles?: string[]; 21 | layout?: string; 22 | showConsole?: boolean; 23 | template?: boolean; 24 | scriptType?: ScriptType; 25 | readmeBehavior?: ReadmeBehavior; 26 | tutorial?: string; 27 | input?: SwingInput; 28 | themePreview?: boolean; 29 | } 30 | 31 | export enum SwingLibraryType { 32 | script = "scripts", 33 | style = "styles", 34 | } 35 | 36 | export enum SwingFileType { 37 | config, 38 | markup, 39 | script, 40 | stylesheet, 41 | manifest, 42 | readme, 43 | tour, 44 | } 45 | 46 | export interface SwingFile { 47 | filename: string; 48 | content?: string; 49 | } 50 | 51 | export interface Version { 52 | prompt: string; 53 | files: SwingFile[]; 54 | } 55 | 56 | interface ActiveSwing { 57 | rootUri: vscode.Uri; 58 | currentUri: vscode.Uri; 59 | 60 | hasTour: boolean; 61 | 62 | webView: SwingWebView; 63 | webViewPanel: vscode.WebviewPanel; 64 | console: vscode.OutputChannel; 65 | commentController?: vscode.CommentController; 66 | 67 | scriptEditor?: vscode.TextEditor; 68 | } 69 | 70 | export interface Store { 71 | globalStorageUri?: vscode.Uri; 72 | activeSwing?: ActiveSwing; 73 | history?: Version[]; 74 | } 75 | 76 | export const store: Store = observable({}); 77 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { commands, ProgressLocation, Uri, window, workspace } from "vscode"; 2 | import * as config from "./config"; 3 | import { EXTENSION_NAME, SWING_FILE } from "./constants"; 4 | import { openSwing } from "./preview"; 5 | 6 | export function byteArrayToString(value: Uint8Array) { 7 | return new TextDecoder().decode(value); 8 | } 9 | 10 | export function stringToByteArray(value: string) { 11 | return new TextEncoder().encode(value); 12 | } 13 | 14 | export async function checkForSwingWorkspace() { 15 | switch (config.get("launchBehavior")) { 16 | case "openSwing": { 17 | if (workspace.workspaceFolders) { 18 | const files = await workspace.findFiles(SWING_FILE); 19 | if (files.length > 0) { 20 | openSwing(workspace.workspaceFolders[0].uri); 21 | } else if (config.get("rootDirectory")) { 22 | commands.executeCommand(`${EXTENSION_NAME}.initializeWorkspace`); 23 | } 24 | } 25 | break; 26 | } 27 | case "newSwing": { 28 | commands.executeCommand(`${EXTENSION_NAME}.newSwing`); 29 | break; 30 | } 31 | } 32 | } 33 | 34 | export async function getFileContents(swingUri: Uri, file: string) { 35 | const uri = Uri.joinPath(swingUri, file); 36 | return getUriContents(uri); 37 | } 38 | 39 | export async function getUriContents(uri: Uri) { 40 | const contents = await workspace.fs.readFile(uri); 41 | return byteArrayToString(contents); 42 | } 43 | 44 | export function withProgress(title: string, action: () => Promise) { 45 | return window.withProgress( 46 | { 47 | location: ProgressLocation.Notification, 48 | title, 49 | }, 50 | action 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /templates/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "web:basic", 3 | "title": "Basic", 4 | "description": "Get started with the basic web development configurations.", 5 | "templates": [ 6 | { 7 | "title": "HTML/CSS/JavaScript", 8 | "files": [ 9 | { 10 | "filename": "index.html" 11 | }, 12 | { 13 | "filename": "script.js" 14 | }, 15 | { 16 | "filename": "style.css" 17 | } 18 | ] 19 | }, 20 | { 21 | "title": "HTML/CSS/TypeScript", 22 | "files": [ 23 | { 24 | "filename": "index.html" 25 | }, 26 | { 27 | "filename": "script.ts" 28 | }, 29 | { 30 | "filename": "style.css" 31 | } 32 | ] 33 | }, 34 | { 35 | "title": "HTML-Only", 36 | "files": [ 37 | { 38 | "filename": "index.html" 39 | } 40 | ] 41 | }, 42 | { 43 | "title": "JavaScript-Only", 44 | "files": [ 45 | { 46 | "filename": "script.js" 47 | }, 48 | { 49 | "filename": "codeswing.json", 50 | "content": "{\"scripts\": [],\"showConsole\": true}" 51 | } 52 | ] 53 | }, 54 | { 55 | "title": "JavaScript-Only (Module)", 56 | "files": [ 57 | { 58 | "filename": "script.mjs" 59 | }, 60 | { 61 | "filename": "codeswing.json", 62 | "content": "{\"scripts\": [],\"showConsole\": true}" 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /templates/basic.yml: -------------------------------------------------------------------------------- 1 | id: web:basic 2 | title: Basic 3 | description: Get started with the basic web development configurations. 4 | templates: 5 | - title: HTML/CSS/JavaScript 6 | files: 7 | - filename: index.html 8 | - filename: script.js 9 | - filename: style.css 10 | 11 | - title: HTML/CSS/TypeScript 12 | files: 13 | - filename: index.html 14 | - filename: script.ts 15 | - filename: style.css 16 | 17 | - title: HTML-Only 18 | files: 19 | - filename: index.html 20 | 21 | - title: JavaScript-Only 22 | files: 23 | - filename: script.js 24 | - filename: codeswing.json 25 | content: | 26 | { 27 | "scripts": [], 28 | "showConsole": true 29 | } 30 | 31 | - title: JavaScript-Only (Module) 32 | files: 33 | - filename: script.mjs 34 | - filename: codeswing.json 35 | content: | 36 | { 37 | "scripts": [], 38 | "showConsole": true 39 | } -------------------------------------------------------------------------------- /templates/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "web:components", 3 | "title": "Components", 4 | "description": "Create components using popular libraries and frameworks.", 5 | "templates": [ 6 | { 7 | "title": "React.js", 8 | "files": [ 9 | { 10 | "filename": "App.jsx", 11 | "content": "export default function App() {\n return
Hello world
;\n}" 12 | } 13 | ] 14 | }, 15 | { 16 | "title": "React.js w/TypeScript", 17 | "files": [ 18 | { 19 | "filename": "App.tsx", 20 | "content": "export default function App() {\n return
Hello world
;\n}" 21 | } 22 | ] 23 | }, 24 | { 25 | "title": "React Native", 26 | "files": [ 27 | { 28 | "filename": "App.jsx", 29 | "content": "import { StyleSheet, Text, View } from \"react-native\";\n\nexport default function App() {\n return (\n \n Hello world 🎉\n \n );\n}\n\nconst styles = StyleSheet.create({\n text: {\n fontWeight: \"bold\",\n fontSize: \"1.5rem\",\n marginVertical: \"1em\",\n textAlign: \"center\"\n }\n});" 30 | } 31 | ] 32 | }, 33 | { 34 | "title": "Vue.js", 35 | "files": [ 36 | { 37 | "filename": "App.vue", 38 | "content": "\n\n\n\n\n\n" 39 | } 40 | ] 41 | }, 42 | { 43 | "title": "Svelte", 44 | "files": [ 45 | { 46 | "filename": "App.svelte", 47 | "content": "\n\n

Hello {name}!

" 48 | } 49 | ] 50 | }, 51 | { 52 | "title": "Svelte w/TypeScript", 53 | "files": [ 54 | { 55 | "filename": "App.svelte", 56 | "content": "\n\n

Hello {name}!

" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /templates/components.yml: -------------------------------------------------------------------------------- 1 | id: web:components 2 | title: Components 3 | description: Create components using popular libraries and frameworks. 4 | templates: 5 | - title: React.js 6 | files: 7 | - filename: App.jsx 8 | content: | 9 | export default function App() { 10 | return
Hello world
; 11 | } 12 | 13 | - title: React.js w/TypeScript 14 | files: 15 | - filename: App.tsx 16 | content: | 17 | export default function App() { 18 | return
Hello world
; 19 | } 20 | 21 | - title: React Native 22 | files: 23 | - filename: App.jsx 24 | content: | 25 | import { StyleSheet, Text, View } from "react-native"; 26 | 27 | export default function App() { 28 | return ( 29 | 30 | Hello world 🎉 31 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | text: { 37 | fontWeight: "bold", 38 | fontSize: "1.5rem", 39 | marginVertical: "1em", 40 | textAlign: "center" 41 | } 42 | }); 43 | 44 | - title: Vue.js 45 | files: 46 | - filename: App.vue 47 | content: | 48 | 51 | 52 | 61 | 62 | 67 | 68 | - title: Svelte 69 | files: 70 | - filename: App.svelte 71 | content: | 72 | 75 | 76 |

Hello {name}!

77 | 78 | - title: Svelte w/TypeScript 79 | files: 80 | - filename: App.svelte 81 | content: | 82 | 85 | 86 |

Hello {name}!

-------------------------------------------------------------------------------- /templates/go.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "go", 3 | "title": "Go", 4 | "description": "Go development, powered by the Go playground (https://go.dev/play)", 5 | "templates": [ 6 | { 7 | "title": "Hello World", 8 | "files": [ 9 | { 10 | "filename": "main.go", 11 | "content": "package main\n\nimport (\n \"fmt\"\n)\n\nfunc main() {\n fmt.Println(\"Hello, World!\")\n}\n" 12 | } 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /templates/go.yml: -------------------------------------------------------------------------------- 1 | id: go 2 | title: Go 3 | description: Go development, powered by the Go playground (https://go.dev/play) 4 | templates: 5 | - title: Hello World 6 | files: 7 | - filename: main.go 8 | content: | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | ) 14 | 15 | func main() { 16 | fmt.Println("Hello, World!") 17 | } -------------------------------------------------------------------------------- /templates/languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "web:languages", 3 | "title": "Languages", 4 | "description": "Get started with commonly-used language configurations: Pug/HTML, SCSS/Less/Saas and JS/TS.", 5 | "templates": [ 6 | { 7 | "title": "HTML w/Less + JavaScript", 8 | "files": [ 9 | { 10 | "filename": "index.html" 11 | }, 12 | { 13 | "filename": "script.js" 14 | }, 15 | { 16 | "filename": "style.less" 17 | } 18 | ] 19 | }, 20 | { 21 | "title": "HTML w/Less + TypeScript", 22 | "files": [ 23 | { 24 | "filename": "index.html" 25 | }, 26 | { 27 | "filename": "script.ts" 28 | }, 29 | { 30 | "filename": "style.less" 31 | } 32 | ] 33 | }, 34 | { 35 | "title": "HTML w/Sass + JavaScript", 36 | "files": [ 37 | { 38 | "filename": "index.html" 39 | }, 40 | { 41 | "filename": "script.js" 42 | }, 43 | { 44 | "filename": "style.sass" 45 | } 46 | ] 47 | }, 48 | { 49 | "title": "HTML w/Sass + TypeScript", 50 | "files": [ 51 | { 52 | "filename": "index.html" 53 | }, 54 | { 55 | "filename": "script.ts" 56 | }, 57 | { 58 | "filename": "style.sass" 59 | } 60 | ] 61 | }, 62 | { 63 | "title": "HTML w/SCSS + JavaScript", 64 | "files": [ 65 | { 66 | "filename": "index.html" 67 | }, 68 | { 69 | "filename": "script.js" 70 | }, 71 | { 72 | "filename": "style.scss" 73 | } 74 | ] 75 | }, 76 | { 77 | "title": "HTML w/SCSS + TypeScript", 78 | "files": [ 79 | { 80 | "filename": "index.html" 81 | }, 82 | { 83 | "filename": "script.ts" 84 | }, 85 | { 86 | "filename": "style.scss" 87 | } 88 | ] 89 | }, 90 | { 91 | "title": "Pug w/Less + TypeScript", 92 | "files": [ 93 | { 94 | "filename": "index.pug" 95 | }, 96 | { 97 | "filename": "script.ts" 98 | }, 99 | { 100 | "filename": "style.less" 101 | } 102 | ] 103 | }, 104 | { 105 | "title": "Pug w/Less + JavaScript", 106 | "files": [ 107 | { 108 | "filename": "index.pug" 109 | }, 110 | { 111 | "filename": "script.js" 112 | }, 113 | { 114 | "filename": "style.less" 115 | } 116 | ] 117 | }, 118 | { 119 | "title": "Pug w/Sass + JavaScript", 120 | "files": [ 121 | { 122 | "filename": "index.pug" 123 | }, 124 | { 125 | "filename": "script.js" 126 | }, 127 | { 128 | "filename": "style.sass" 129 | } 130 | ] 131 | }, 132 | { 133 | "title": "Pug w/Sass + TypeScript", 134 | "files": [ 135 | { 136 | "filename": "index.pug" 137 | }, 138 | { 139 | "filename": "script.ts" 140 | }, 141 | { 142 | "filename": "style.sass" 143 | } 144 | ] 145 | }, 146 | { 147 | "title": "Pug w/SCSS + JavaScript", 148 | "files": [ 149 | { 150 | "filename": "index.pug" 151 | }, 152 | { 153 | "filename": "script.js" 154 | }, 155 | { 156 | "filename": "style.scss" 157 | } 158 | ] 159 | }, 160 | { 161 | "title": "Pug w/SCSS + TypeScript", 162 | "files": [ 163 | { 164 | "filename": "index.pug" 165 | }, 166 | { 167 | "filename": "script.ts" 168 | }, 169 | { 170 | "filename": "style.scss" 171 | } 172 | ] 173 | } 174 | ] 175 | } 176 | -------------------------------------------------------------------------------- /templates/languages.yml: -------------------------------------------------------------------------------- 1 | id: "web:languages" 2 | title: "Languages" 3 | description: "Get started with commonly-used language configurations: Pug/HTML, SCSS/Less/Saas and JS/TS." 4 | templates: 5 | - title: "HTML w/Less + JavaScript" 6 | files: 7 | - filename: "index.html" 8 | - filename: "script.js" 9 | - filename: "style.less" 10 | 11 | - title: "HTML w/Less + TypeScript" 12 | files: 13 | - filename: "index.html" 14 | - filename: "script.ts" 15 | - filename: "style.less" 16 | 17 | - title: "HTML w/Sass + JavaScript" 18 | files: 19 | - filename: "index.html" 20 | - filename: "script.js" 21 | - filename: "style.sass" 22 | 23 | - title: "HTML w/Sass + TypeScript" 24 | files: 25 | - filename: "index.html" 26 | - filename: "script.ts" 27 | - filename: "style.sass" 28 | 29 | - title: "HTML w/SCSS + JavaScript" 30 | files: 31 | - filename: "index.html" 32 | - filename: "script.js" 33 | - filename: "style.scss" 34 | 35 | - title: "HTML w/SCSS + TypeScript" 36 | files: 37 | - filename: "index.html" 38 | - filename: "script.ts" 39 | - filename: "style.scss" 40 | 41 | - title: "Pug w/Less + TypeScript" 42 | files: 43 | - filename: "index.pug" 44 | - filename: "script.ts" 45 | - filename: "style.less" 46 | 47 | - title: "Pug w/Less + JavaScript" 48 | files: 49 | - filename: "index.pug" 50 | - filename: "script.js" 51 | - filename: "style.less" 52 | 53 | - title: "Pug w/Sass + JavaScript" 54 | files: 55 | - filename: "index.pug" 56 | - filename: "script.js" 57 | - filename: "style.sass" 58 | 59 | - title: "Pug w/Sass + TypeScript" 60 | files: 61 | - filename: "index.pug" 62 | - filename: "script.ts" 63 | - filename: "style.sass" 64 | 65 | - title: "Pug w/SCSS + JavaScript" 66 | files: 67 | - filename: "index.pug" 68 | - filename: "script.js" 69 | - filename: "style.scss" 70 | 71 | - title: "Pug w/SCSS + TypeScript" 72 | files: 73 | - filename: "index.pug" 74 | - filename: "script.ts" 75 | - filename: "style.scss" 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "ES2019", 6 | "outDir": "out", 7 | "lib": ["ES2019", "DOM"], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "baseUrl": ".", 13 | "experimentalDecorators": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | 4 | const config = { 5 | mode: "development", 6 | entry: "./src/extension.ts", 7 | externals: { 8 | vscode: "commonjs vscode", 9 | "uglify-js": "commonjs uglify-js", // Pug relies on uglify-js, which doesn't play nice with Webpack. Fortunately we don't need it, so we exclude it from the bundle 10 | "aws-sdk": "commonjs aws-sdk", // This comes from the Sass dependency, and is an optional dependency that we don't need 11 | fsevents: "commonjs fsevents", // This comes from the SaaS dependency, but is a native module and therefore can't be webpacked 12 | "@microsoft/typescript-etw": "commonjs @microsoft/typescript-etw", 13 | velocityjs: "commonjs velocityjs", // The following come from @vue/component-compiler-utils 14 | "dustjs-linkedin": "commonjs dustjs-linkedin", 15 | atpl: "commonjs atpl", 16 | liquor: "commonjs liquor", 17 | twig: "commonjs twig", 18 | ejs: "commonjs ejs", 19 | eco: "commonjs eco", 20 | jazz: "commonjs jazz", 21 | jqtpl: "commonjs jqtpl", 22 | hamljs: "commonjs hamljs", 23 | hamlet: "commonjs hamlet", 24 | whiskers: "commonjs whiskers", 25 | "haml-coffee": "commonjs haml-coffee", 26 | "hogan.js": "commonjs hogan.js", 27 | templayed: "commonjs templayed", 28 | handlebars: "commonjs handlebars", 29 | walrus: "commonjs walrus", 30 | mustache: "commonjs mustache", 31 | just: "commonjs just", 32 | ect: "commonjs ect", 33 | mote: "commonjs mote", 34 | toffee: "commonjs toffee", 35 | dot: "commonjs dot", 36 | "bracket-template": "commonjs bracket-template", 37 | ractive: "commonjs ractive", 38 | htmling: "commonjs htmling", 39 | "babel-core": "commonjs babel-core", 40 | plates: "commonjs plates", 41 | "react-dom/server": "commonjs react-dom", 42 | react: "commonjs react", 43 | vash: "commonjs vash", 44 | slm: "commonjs slm", 45 | marko: "commonjs marko", 46 | "teacup/lib/express": "commonjs teacup", 47 | "coffee-script": "commonjs coffee-script", 48 | "./lib-cov/stylus": "commonjs stylus", 49 | vue: "commonjs vue", 50 | }, 51 | resolve: { 52 | extensions: [".ts", ".js", ".json", ".txt"], 53 | }, 54 | node: { 55 | __filename: false, 56 | __dirname: false, 57 | }, 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.ts$/, 62 | exclude: /node_modules/, 63 | use: [ 64 | { 65 | loader: "ts-loader", 66 | }, 67 | ], 68 | }, 69 | { 70 | test: /\.txt$/i, 71 | loader: "raw-loader", 72 | }, 73 | ], 74 | }, 75 | }; 76 | 77 | const nodeConfig = { 78 | ...config, 79 | target: "node", 80 | output: { 81 | path: path.resolve(__dirname, "dist"), 82 | filename: "extension.js", 83 | libraryTarget: "commonjs2", 84 | devtoolModuleFilenameTemplate: "../[resource-path]", 85 | }, 86 | resolve: { 87 | ...config.resolve, 88 | alias: { 89 | "@abstractions": path.join(__dirname, "./src/abstractions/node"), 90 | }, 91 | }, 92 | }; 93 | 94 | const webConfig = { 95 | ...config, 96 | target: "webworker", 97 | output: { 98 | path: path.resolve(__dirname, "dist"), 99 | filename: "extension-web.js", 100 | libraryTarget: "commonjs2", 101 | devtoolModuleFilenameTemplate: "../[resource-path]", 102 | }, 103 | resolve: { 104 | ...config.resolve, 105 | fallback: { 106 | assert: false, 107 | buffer: false, 108 | crypto: false, 109 | fs: false, 110 | http: false, 111 | https: false, 112 | module: false, 113 | os: require.resolve("os-browserify/browser"), 114 | path: require.resolve("path-browserify"), 115 | readline: false, 116 | stream: require.resolve("stream-browserify"), 117 | url: false, 118 | util: false, 119 | vm: false, 120 | }, 121 | alias: { 122 | "@abstractions": path.join(__dirname, "./src/abstractions/web"), 123 | }, 124 | }, 125 | }; 126 | 127 | module.exports = [nodeConfig, webConfig]; 128 | --------------------------------------------------------------------------------