├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── deploy.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── language-configuration.json ├── media ├── cheatsheet │ ├── cheatsheet-auto.css │ ├── cheatsheet-original.css │ └── cheatsheet.html ├── icons │ ├── export-dark.svg │ ├── export-light.svg │ ├── kill-dark.svg │ ├── kill-light.svg │ ├── preview-dark.svg │ ├── preview-light.svg │ └── source │ │ ├── export-icon.afdesign │ │ └── preview+kill.afdesign ├── logo.png ├── openscad.svg └── screenshots │ ├── comparison-os.png │ ├── comparison-vsc.png │ ├── comparison.png │ ├── customizer-highlights.png │ ├── open-cheatsheet.gif │ ├── openscad-export.gif │ └── openscad-preview.gif ├── package.json ├── pnpm-lock.yaml ├── snippets └── snippets.json ├── src ├── cheatsheet │ ├── cheatsheet-content.ts │ ├── cheatsheet-panel.ts │ └── styles.ts ├── config.ts ├── export │ ├── export-file-extensions.ts │ └── variable-resolver.ts ├── extension.node.ts ├── extension.web.ts ├── logging-service.ts ├── preview │ ├── openscad-exe.ts │ ├── preview-manager.ts │ ├── preview-store.ts │ └── preview.ts └── test │ ├── run-test.ts │ └── suite │ ├── extension.test.ts │ └── index.ts ├── syntaxes ├── scad.tmLanguage.json └── scad.yaml-tmLanguage ├── test ├── customizer-sample.scad └── highlight-sample.scad ├── tsconfig.json └── webpack.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | // 10 | "@typescript-eslint", 11 | "prettier", 12 | "simple-import-sort", 13 | "import", 14 | "unicorn" 15 | ], 16 | "extends": [ 17 | // 18 | "vscode-ext", 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/eslint-recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:unicorn/recommended", 23 | "prettier" 24 | ], 25 | "rules": { 26 | "prettier/prettier": [ 27 | "warn", 28 | { 29 | "endOfLine": "auto", 30 | "singleQuote": true 31 | } 32 | ], 33 | "simple-import-sort/imports": [ 34 | "warn", 35 | { 36 | // Default groups, but group 'src' with relative imports 37 | "groups": [["^\\u0000"], ["^@?\\w"], ["^"], ["^src", "^\\."]] 38 | } 39 | ], 40 | "simple-import-sort/exports": "warn", 41 | "import/first": "error", 42 | "import/newline-after-import": "error", 43 | "import/no-duplicates": "error", 44 | "no-trailing-spaces": "warn", 45 | "eol-last": ["warn", "always"], 46 | // node:protocol imports are supported in Node v16.0.0, v14.18.0. 47 | // VS Code 1.61.0 runs on Node v14.16.0, so this must be disabled for now 48 | "unicorn/prefer-node-protocol": "off", 49 | "unicorn/import-style": "off" 50 | }, 51 | "ignorePatterns": [ 52 | // 53 | "**/*.d.ts", 54 | "dist/*" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: Antyos 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows, Linux] 28 | - VS Code version: [e.g. chrome, safari] 29 | - Extension version: [e.g. v1.3.0] 30 | - OpenSCAD version: [e.g. 2021.01] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | 35 | **Logs** 36 | Enable debug logging by adding: `"openscad.logLevel": "DEBUG"` to your `settings.json` file. Then, paste the contents of the OpenSCAD output channel here. (The command to reveal the output channel is **openscad.showOutput** if you have trouble finding it). You may revert the settings change afterwards. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Deploy VSX Extension 2 | # Based on: https://github.com/HaaLeo/publish-vscode-extension 3 | name: Build and deploy Extension 4 | 5 | on: [push, pull_request] 6 | 7 | env: 8 | NODE_VERSION: '20.x' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | vsixName: ${{ steps.packageExtension.outputs.vsixPath }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ env.NODE_VERSION }} 23 | 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: 9 28 | run_install: false 29 | 30 | - name: Get pnpm store directory 31 | id: pnpm-cache 32 | shell: bash 33 | run: | 34 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 35 | 36 | - name: Setup pnpm cache 37 | uses: actions/cache@v4 38 | with: 39 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm-store- 43 | 44 | - name: Install dependencies 45 | run: pnpm install 46 | 47 | - name: Run update-grammar script 48 | run: pnpm run update-grammar 49 | 50 | - name: Commit changes to tmLanauge.json files 51 | run: | 52 | git config --local user.email "action@github.com" 53 | git config --local user.name "GitHub Action" 54 | git add *.tmLanguage.json 55 | FILE_DIFF=$(git diff --cached --name-only --diff-filter=d) 56 | if [[ $FILE_DIFF == *".tmLanguage.json"* ]]; then 57 | git commit -m "Update grammar [skip ci]" 58 | git push 59 | fi 60 | 61 | # Check (and update if necessary) package.json version 62 | - name: Check package version 63 | uses: technote-space/package-version-check-action@v1.9.2 64 | 65 | - name: Package extension .vsix 66 | id: packageExtension 67 | uses: HaaLeo/publish-vscode-extension@v1 68 | with: 69 | pat: stub 70 | dryRun: true 71 | dependencies: false 72 | 73 | - name: Upload extension artifact 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: openscad-vsix 77 | path: ${{ steps.packageExtension.outputs.vsixPath }} 78 | 79 | release: 80 | runs-on: ubuntu-latest 81 | needs: [build] 82 | if: startsWith(github.ref, 'refs/tags/v') 83 | steps: 84 | - uses: actions/checkout@v4 85 | 86 | - name: Download artifact 87 | id: download 88 | uses: actions/download-artifact@v4.1.7 89 | with: 90 | name: openscad-vsix 91 | 92 | - name: Create release 93 | uses: ncipollo/release-action@v1 94 | with: 95 | artifacts: ${{ steps.download.outputs.download-path }}/${{ needs.build.outputs.vsixName }} 96 | 97 | # Pulbish to Open VSX Registry 98 | - name: Publish to Open VSX Registry 99 | uses: HaaLeo/publish-vscode-extension@v1 100 | id: publishToOpenVSX 101 | with: 102 | pat: ${{ secrets.OPEN_VSX_TOKEN }} 103 | extensionFile: ${{ steps.download.outputs.download-path }}/${{ needs.build.outputs.vsixName }} 104 | registryUrl: https://open-vsx.org 105 | 106 | # Publish to VS Marketplace 107 | - name: Publish to Visual Studio Marketplace 108 | uses: HaaLeo/publish-vscode-extension@v1 109 | with: 110 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }} 111 | extensionFile: ${{ steps.download.outputs.download-path }}/${{ needs.build.outputs.vsixName }} 112 | registryUrl: https://marketplace.visualstudio.com 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated code 2 | node_modules/ 3 | out/ 4 | dist/ 5 | tsconfig.tsbuildinfo 6 | 7 | # Workspace settings 8 | .vscode/settings.json 9 | 10 | # Secret files 11 | **/secret 12 | *secret* 13 | 14 | # Log files 15 | *.log 16 | 17 | # Extension releases 18 | *.vsix 19 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "endOfLine": "auto", 8 | "overrides": [ 9 | { 10 | "files": [ 11 | "**/.vscode/*.json", 12 | "**/tsconfig.json", 13 | "**/tsconfig.*.json" 14 | ], 15 | "options": { 16 | "tabWidth": 4, 17 | "parser": "json5", 18 | "quoteProps": "preserve", 19 | "singleQuote": false, 20 | "trailingComma": "all" 21 | } 22 | }, 23 | { 24 | "files": ["*.yml", "*.yaml", "*.yaml-tmLanguage"], 25 | "options": { 26 | "tabWidth": 2 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Node Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--disable-extensions", 15 | "--extensionDevelopmentPath=${workspaceFolder}", 16 | ], 17 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 18 | "skipFiles": [ 19 | "${workspaceFolder}/node_modules/*", 20 | "/**", 21 | ], 22 | "preLaunchTask": "npm: watch", 23 | }, 24 | { 25 | "name": "Run Web Extension", 26 | "type": "extensionHost", 27 | "debugWebWorkerHost": true, 28 | "request": "launch", 29 | "args": [ 30 | "--disable-extensions", 31 | "--extensionDevelopmentPath=${workspaceFolder}", 32 | "--extensionDevelopmentKind=web", 33 | ], 34 | "skipFiles": [ 35 | "${workspaceFolder}/node_modules/*", 36 | "/**", 37 | ], 38 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 39 | "preLaunchTask": "npm: watch", 40 | "sourceMaps": true, 41 | }, 42 | { 43 | "name": "Extension Tests", 44 | "type": "extensionHost", 45 | "debugWebWorkerHost": true, 46 | "request": "launch", 47 | "args": [ 48 | "--disable-extensions", 49 | "--extensionDevelopmentPath=${workspaceFolder}", 50 | "--extensionDevelopmentKind=web", 51 | "--extensionTestsPath=${workspaceFolder}/node/test/index", 52 | ], 53 | "outFiles": ["${workspaceFolder}/dist/node/test/**/*.js"], 54 | "preLaunchTask": "npm: watch", 55 | }, 56 | ], 57 | } 58 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "lint", 9 | "problemMatcher": ["$eslint-stylish"], 10 | "label": "npm: lint", 11 | }, 12 | { 13 | "type": "npm", 14 | "script": "compile-clean", 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true, 18 | }, 19 | "problemMatcher": ["$ts-webpack", "$tslint-webpack"], 20 | }, 21 | { 22 | "type": "npm", 23 | "script": "watch", 24 | "group": "build", 25 | "isBackground": true, 26 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 27 | }, 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # VSCode workspace files 2 | .vscode/** 3 | .vscode-test/** 4 | .vscode-test-web/** 5 | 6 | # Gitignore 7 | .gitignore 8 | 9 | # GitHub 10 | .github/ 11 | 12 | # Config / ESLint / Prettier 13 | tsconfig.json 14 | .eslintrc.json 15 | .eslintignore 16 | .prettierrc.json 17 | .prettierignore 18 | 19 | # Webpack 20 | webpack.config.js 21 | 22 | # Typescript files 23 | src/ 24 | out/*.js.map 25 | 26 | # Tests 27 | test/ 28 | 29 | # Ignore source files for images 30 | media/icons/source/** 31 | 32 | # Ignore yaml-tmlanguage files (only used for dev) 33 | syntaxes/*.yaml-tmLanguage 34 | 35 | # Secret files 36 | **/secret 37 | *secret* 38 | 39 | # Log files 40 | *.log 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [[1.3.2](https://github.com/Antyos/vscode-openscad/releases/tag/v1.3.2)] - (2025-01-30) 4 | 5 | ### Added 6 | 7 | - `openscad.experimental.skipLaunchPathValidation` configuration. (Not an ideal 8 | approach, but it should work.) 9 | - Better logging. 10 | 11 | ### Fixed 12 | 13 | - Errors when validating OpenSCAD executable. (See PR 14 | [#69](https://github.com/Antyos/vscode-openscad/pull/69)). 15 | - Executable path not considered valid if `openscad --version` does not 16 | output to `stdout` (See 17 | [#62](https://github.com/Antyos/vscode-openscad/issues/62)). 18 | - Enable `openscad.experimental.skipLaunchPathValidation` to bypass the 19 | check `openscad --version` check. 20 | - Simlink to OpenSCAD executable or VS Code itself not resolving correctly. 21 | (See [#68](https://github.com/Antyos/vscode-openscad/issues/68)). 22 | 23 | ## [[1.3.1](https://github.com/Antyos/vscode-openscad/releases/tag/v1.3.1)] - (2024-02-01) 24 | 25 | ### Fixed 26 | 27 | - Fixed incorrect display of deprecation warnings related to 28 | [#58](https://github.com/Antyos/vscode-openscad/pull/58). 29 | (See PR[#61](https://github.com/Antyos/vscode-openscad/pull/35)). 30 | Thanks [bluekeyes](https://github.com/bluekeyes). 31 | 32 | ## [[1.3.0](htts://github.com/Antyos/vscode-openscad/releases/tag/v1.3.0)] - (2023-01-16) 33 | 34 | ### Changed 35 | 36 | - Configurations (See PR [#58](https://github.com/Antyos/vscode-openscad/pull/58)) 37 | 38 | | Old | New | 39 | | --- | --- | 40 | | `openscad.export.autoNamingFormat` | `openscad.export.exportNameFormat` | 41 | | `openscad.export.useAutoNamingExport` | `openscad.export.skipSaveDialog` | 42 | | `openscad.export.useAutoNamingInSaveDialogues` | `openscad.export.saveDialogExportNameFormat` | 43 | 44 | ### Added 45 | 46 | - Override `openscad.export.exportNameFormat` on a per-file basis. (See PR 47 | [#58](https://github.com/Antyos/vscode-openscad/pull/58)). 48 | - `openscad.export.exportNameFormat` now supports date time variables. Use `${date}` 49 | for an ISO 8601 date string or use a custom format with: `${date:TEMPLATE}` 50 | according to [Luxon tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens). 51 | (See PR [#57](https://github.com/Antyos/vscode-openscad/issues/57)) 52 | Fixes: [#55](https://github.com/Antyos/vscode-openscad/issues/55). 53 | 54 | ### Fixed 55 | 56 | - Auto versioning started at "-Infinity" instead of "1" for a folder without 57 | similarly named files. 58 | 59 | ### Deprecated 60 | 61 | - Configurations (See PR [#58](https://github.com/Antyos/vscode-openscad/pull/58)) 62 | - `openscad.export.autoNamingFormat` 63 | - `openscad.export.useAutoNamingExport` 64 | - `openscad.export.useAutoNamingInSaveDialogues` 65 | 66 | ## [[1.2.2](htts://github.com/Antyos/vscode-openscad/releases/tag/v1.2.2)] - (2023-10-09) 67 | 68 | ### Added 69 | 70 | - Implemented logging-service.ts based on the one found in the prettier/prettier-vscode. This should make it easier for troubleshooting issues in the future. 71 | - `openscad.showOutput` command to display the output channel. 72 | - `openscad.logLevel` configuration to set the output channel log level. 73 | 74 | ### Fixed 75 | 76 | - Bug with `openscad.launchPath` using an empty value. Should help with 77 | [#49](https://github.com/Antyos/vscode-openscad/issues/49) and 78 | [#50](https://github.com/Antyos/vscode-openscad/issues/50) 79 | 80 | ## [[1.2.1](https://github.com/Antyos/vscode-openscad/releases/tag/v1.2.1)] - (2023-07-26) 81 | 82 | ### Added 83 | 84 | - Find widget in Cheatsheet. Fixes [#42](https://github.com/Antyos/vscode-openscad/issues/42) (See 85 | PR[#47](https://github.com/Antyos/vscode-openscad/pull/47)). Thanks [Duckapple](https://github.com/Duckapple) 86 | 87 | ## [[1.2.0](https://github.com/Antyos/vscode-openscad/releases/tag/v1.2.0)] - (2023-07-22) 88 | 89 | ### Added 90 | 91 | - `openscad.launchArgs` configuration. Fixes [#36](https://github.com/Antyos/vscode-openscad/issues/36). 92 | 93 | #### Web extension 94 | 95 | VSCode OpenSCAD can now run as a web extension! 96 | 97 | - Syntax highlighting and OpenSCAD cheatsheet are available when using VS Code 98 | for the web 99 | - Preview- and export- related commands are not available when running as a web 100 | extension. Attempting to run these commands will display a popup notification 101 | that the commands are disabled when running as a web extension. 102 | 103 | ### Fixed 104 | 105 | - Syntax highlighting for `$vpf` (See PR[#35](https://github.com/Antyos/vscode-openscad/pull/35)). Thanks [atnbueno](https://github.com/atnbueno) 106 | 107 | ### Development 108 | 109 | - Migrated to PNPM for package management. (See PR[#46](https://github.com/Antyos/vscode-openscad/pull/46)). 110 | 111 | ## [[1.1.1](https://github.com/Antyos/vscode-openscad/releases/tag/v1.1.1)] - (2021-06-07) 112 | 113 | ### Changed 114 | 115 | - Cheatsheet version to v2021.01 (See PR[#23](https://github.com/Antyos/vscode-openscad/pull/23)) 116 | - `poly` snippet (See PR[#22](https://github.com/Antyos/vscode-openscad/pull/22)). Thanks [mathiasvr](https://github.com/mathiasvr) 117 | - License from LGPL-3 to GPL-3 to be consistent with [openscad/openscad](https://github.com/openscad/openscad) 118 | 119 | ### Fixed 120 | 121 | - Various vulnerabilities related to outdated dependencies. All dependencies have been updated. 122 | 123 | ## [[1.1.0](https://github.com/Antyos/vscode-openscad/releases/tag/v1.1.0)] - (2021-01-18) 124 | 125 | ### Added 126 | 127 | - `difference()` to snippets (See PR [#11](https://github.com/Antyos/vscode-openscad/pull/11)). Thanks [williambuttenham](https://github.com/williambuttenham) 128 | - Keybinding `F5` for `openscad.preview` (See PR [#12](https://github.com/Antyos/vscode-openscad/pull/12)). Closes [#7](https://github.com/Antyos/vscode-openscad/issues/7). Thanks [williambuttenham](https://github.com/williambuttenham) 129 | - Keybinding `F6` for `openscad.render` 130 | 131 | ### Development 132 | 133 | - Updated `@types/node` from v9.4.6 to v14.14.20 (*WHY* did I leave this outdated for so long???) 134 | - Upgraded from TSLint to ESLint 135 | - Added Prettier and formatting styles 136 | - Reformatred all code according to styles set by ESLint and Prettier 137 | 138 | 139 | *See PR [#14](https://github.com/Antyos/vscode-openscad/pull/14) and PR [#15](https://github.com/Antyos/vscode-openscad/pull/15) for details on the above* 140 | 141 | ## [[1.0.2](https://github.com/Antyos/vscode-openscad/releases/tag/v1.0.2)] - (2020-12-09) 142 | 143 | ### Fixed 144 | 145 | - Updated cheatsheet (See PR [#8](https://github.com/Antyos/vscode-openscad/pull/8)). Thanks [ckittle](https://github.com/ckittel) 146 | - Included path to openscad command in error message for invalid openscad command 147 | - Configurations with markdownDescription were showing a less descriptive, plaintext description now shows the full description 148 | - Syntax highlighting (See issue [#5](https://github.com/Antyos/vscode-openscad/issues/5)) 149 | - Improved highlighting of `include` and `use` statements 150 | - Highlighting of non-alpha characters used within a customizer section header 151 | or any character outside the `[]` does not prevent the `[]` from being highlighted 152 | - Inline customizer syntax for defining possible values do not highlight when preceded by only spaces 153 | 154 | ### Development 155 | 156 | - Added files to test syntax highlighting 157 | 158 | ## [1.0.1] - (2020-07-19) 159 | 160 | ### Fixed 161 | 162 | - Fixed vulnerability with Lodash 163 | 164 | ## [[1.0.0](https://github.com/Antyos/vscode-openscad/releases/tag/v1.0.0)] - (2020-06-19) 165 | 166 | ### Added 167 | 168 | - Commands 169 | - **Preview in OpenSCAD** (`openscad.preview`) launches an instance of OpenSCAD to preview a `.scad` file 170 | - Only available in context menu and command palette for `.scad` files 171 | - Preview button in editor/title is shown for all `.scad` files 172 | - **Kill OpenSCAD Previews** (`openscad.kill`) kills a single open instance of OpenSCAD 173 | - Only available when there are open previews 174 | - Opens a Quick-Pick box to select one of the open previews to kill (or choose **Kill All** to kill them all) 175 | - **Kill All OpenSCAD Previews** (`openscad.killAll`) kills all open previews 176 | - Only available when there are open previews 177 | - (Hidden) `openscad.autoKill` functions as **Kill All** if one preview is open, otherwise functions as **Kill** 178 | - Only accesible through button on editor/title bar 179 | - **Export Model** (`openscad.exportByConfig`) exports model based on config: `openscad.export.preferredFileExtension` 180 | - Only available in context menu and command palette for `.scad` files 181 | - **Export Model (Select File Type)** (`openscad.exportByType`) exports model to a selected file type 182 | - Only available in command palette for `.scad` files 183 | - Opens quick-pick box to select file type 184 | - **Export Model with Save Dialogue** (`openscad.exportWithSaveDialogue`) exports model using a save dialogue 185 | - Only available in context menu and command palette for `.scad` files 186 | - Replaces `openscad.exportByConfig` in context menus when holding alt 187 | - Menu buttons (in editor/title for `scad` files) 188 | - **Preview** - Runs `openscad.preview` 189 | - **Kill** - Runs `openscad.autoKill`. If `alt` is held, runs `openscad.kill` 190 | - **Export** - Runs `openscad.exportByConfig`. If `alt` is held, runs `openscad.exportByType` 191 | - Configurations 192 | - `openscad.launchPath` - Overrides default path to `openscad` executable. 193 | - `openscad.maxInstances` - Limits the max number of preview windows open at one time. Set 0 for no limit. 194 | - `openscad.showKillMessage` - Show message when a preview is killed. 195 | - `openscad.export.preferredExportFileExtension` - Preferred file extension to use when exporting using the 'Export' button in the editor title bar. Set to `none` to select the file extension each time. 196 | - `openscad.export.autoNamingFormat` - A configurable string that dynamically names exported files. 197 | - `openscad.export.useAutoNamingExport` - Setting to true will replace the standard behavior of **Export Model** to automatically export files according to the name specified in openscad.export.autoNamingFormat` instead of opening a save dialogue. 198 | - `openscad.export.useAutoNamingInSaveDialogues` - The default name of to-be exported files in save dialouges will be generated according to the config of `openscad.export.autoNamingFormat` instead of using the original filename. 199 | - `openscad.interface.showPreviewIconInEditorTitleMenu` - Shows **Preview in OpenSCAD** button in editor title menu (right side of tabs). 200 | - `openscad.interface.showKillIconInEditorTitleMenu` - Shows **Kill OpenSCAD Previews** button in editor title menu (right side of tabs). 201 | - `openscad.interface.showExportIconInEditorTitleMenu` - Shows **Export Model** button in editor title menu (right side of tabs). 202 | - `openscad.interface.showCommandsInEditorTitleContextMenu` - Shows preview and export commands in editor title (tab) context menu. 203 | - `openscad.interface.showCommandsInExplorerContextMenu` - Shows preview and export commands in explorer context menu. 204 | - `openscad.interface.showPreviewInContextMenus` - Shows **Preview in OpenSCAD** command in context menus. 205 | - `openscad.interface.showExportInContextMenus` - Shows **Export Model** command in context menus. 206 | - Grammar 207 | - Added unicode/hex escape codes in strings. See: , for details on escape codes in strings. 208 | 209 | ### Changed 210 | 211 | - `scad.cheatsheet` command is now `openscad.cheatsheet` for consistancy with configurations 212 | - `openscad.cheatsheet.openToSide` configuration is an enumerated string instead of a boolean for improved clarity. Options now include `beside` (was `true`) and `currentGroup` (was `false`) 213 | 214 | ## [[0.1.2](https://github.com/Antyos/vscode-openscad/releases/tag/v0.1.2)] - (2020-03-23) 215 | 216 | ### Fixed 217 | 218 | - Fixed syntax highlighting not working on case sensitive operating systems (i.e. Linux) 219 | 220 | ## [[0.1.0](https://github.com/Antyos/vscode-openscad/releases/tag/v0.1.0)] - (2020-03-17) 221 | 222 | ### Added 223 | 224 | - Syntax highlighting for OpenSCAD Customizer widgets. Highlighting support includes: 225 | - Drop down boxes 226 | - Slider 227 | - Tabs 228 | - `Open OpenSCAD Cheatsheet` command to natively launch the OpenSCAD cheatsheet in VSCode 229 | - Included a status bar icon for easy access to the command 230 | - By default, it is visible whenever a `.scad` file is in an open tab 231 | - Extension Configurations: 232 | - `openscad.cheatsheet.displayInStatusBar`: When the "Open Cheatsheet" button should be displaying in the status bar 233 | - Known bug: When set to `openDoc`, the status bar icon won't *initially* show up until viewing a `.scad` file, even if one is open in another tab. 234 | - `openscad.cheatsheet.colorScheme`: The color scheme used for the cheatsheet. Default uses VSCode's current theme for colors, but the original color scheme is available if desired. 235 | - `openscad.cheatsheet.openToSide`: Open the cheatsheet in the current column or beside the current column 236 | 237 | ## [[0.0.1](https://github.com/Antyos/vscode-openscad/releases/tag/v0.0.1)] - (2020-02-23) 238 | 239 | ### Initial release 240 | 241 | Includes syntax highlighting and snippets. 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenSCAD for VS Code 2 | 3 | Edit OpenSCAD files with all the luxuries of VSCode! Works with OpenSCAD v2019.05. 4 | 5 | Project is available at: 6 | 7 | This extension builds upon the "scad" extension by `Erik Benson` and later expanded upon by GitHub user `atnbueno` 8 | 9 | ## Features 10 | 11 | This extension features: 12 | 13 | - Syntax highlighting for: 14 | - Built-in OpenSCAD and user modules/functions 15 | - Includes customizer syntax support for Drop down boxes, Sliders, and Tabs 16 | - Preview and Export buttons to Preview/Export files with a single click 17 | - Snippets 18 | - Built-in access to the OpenSCAD cheatsheet 19 | 20 | ## Highlighting comparison 21 | 22 | VSCode with OpenSCAD Plugin| OpenSCAD Editor 23 | :-------------------------:|:-------------------------: 24 | ![comparison-vsc](media/screenshots/comparison-vsc.png) | ![comparison-os](media/screenshots/comparison-os.png) 25 | 26 | Code: 27 | 28 | ## Customizer Syntax 29 | 30 | Highlights customizer keywords in comments! As of OpenSCAD v2019.5, OpenSCAD itself does not currently do this. 31 | 32 | ![customizer-highlights](media/screenshots/customizer-highlights.png) 33 | 34 | ## Preview Files 35 | 36 | Click the **Preview in OpenSCAD** button to instantly launch and preview your file in OpenSCAD! 37 | 38 | Check [**usage**](#usage) section for more information. 39 | 40 | ![openscad-preview](media/screenshots/openscad-preview.gif) 41 | 42 | ## Export Files 43 | 44 | Click the **Export Model** button to export your model to any of the supported file types! (`stl`, `3mf`, `dxf`, etc.) 45 | 46 | You can also set a naming scheme to automatically name exported files. See [Auto-Exporting](https://github.com/Antyos/vscode-openscad/wiki/Auto-Exporting). 47 | 48 | ![openscad-export](media/screenshots/openscad-export.gif) 49 | 50 | ## Built-in Cheatsheet 51 | 52 | Launch the built-in OpenSCAD cheatsheet with the command `Open OpenSCAD Cheatsheet` or with the button in the status bar! 53 | 54 | ![open-cheatsheet](media/screenshots/open-cheatsheet.gif) 55 | 56 | ## Usage 57 | 58 | **Make sure you have installed OpenSCAD here:** 59 | 60 | If you install OpenSCAD to the default location for you system, the extension should automatically detect it. If not, you will need to set the `openscad.launchPath` configuration option in **Settings > Extensions > OpenSCAD > Launch Path** or by adding the following line to your `settings.json` file: 61 | 62 | ``` json 63 | { 64 | "someSetting": "", 65 | "openscad.launchPath": "path/to/openscad" 66 | } 67 | ``` 68 | 69 | > _Note: If you are using Windows, make sure to specify the path to `openscad.exe` so the extension can properly manage open previews and exports._ 70 | 71 | To preview a `.scad` file, you can use the **Preview in OpenSCAD** command which can be accessed via the Command Pallette, the button in the editor title bar, or by right clicking the file in the title bar or explorer and choosing it from the context menu. 72 | 73 | In OpenSCAD, make sure to have **Automatic Reload and Preview** checked under **Design > Automatic Reload and Preview**. You may also want to hide the editor and customizer panels in OpenSCAD by checking **View > Hide Editor** and **View > Hide Customizer**. 74 | 75 | When you save your file in VSCode, OpenSCAD will automatically reload and preview the updated file. 76 | 77 | For more information, see: [Using an external Editor with OpenSCAD](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Using_an_external_Editor_with_OpenSCAD) 78 | 79 | ## To-Do 80 | 81 | - Add to syntax highlighting 82 | - Auto-naming export 83 | - Include ${var:x} in export format 84 | - Add configurable export map for each file format 85 | - Launch file with args (get user input for args) 86 | - Add snippets for common things in the OpenSCAD cheat sheet 87 | - Create language server 88 | - _Parameters in functions should be highlighted in contents as well_ 89 | - _Block comment new lines keep '*' at the beginning of the line (comment continuation patterns)_ 90 | - _Recognize modifier characters (* ! # %)_ 91 | - _Create shortcut to open `.scad` files with VSCode by default but has OpenSCAD icon_ 92 | - _Extension auto-installs its own copy of OpenSCAD_ 93 | 94 | > _Note: Italicized items on the to-do list are more challenging and I do not know when (if ever) I will get to them._ 95 | 96 | ## Changelog 97 | 98 | See the changelog [here](https://github.com/Antyos/vscode-openscad/blob/master/CHANGELOG.md). 99 | 100 | ## Contributing 101 | 102 | I made this extension because I like OpenSCAD and there wasn't any language support in VS Code I liked. I currently maintain this extension as a side project, so I can't promise when I will get to adding new features. With that being said, please feel free to submit a pull request! 103 | 104 | If you would like to contribute, here's what you can do: 105 | 106 | 1. Fork the repository: 107 | 2. Install [pnpm](https://pnpm.io/installation) 108 | 3. Run `pnpm install` to download node modules 109 | 4. Make changes 110 | 5. Submit a pull request! 111 | 112 | ### Contributing to the Grammar 113 | 114 | VSCode can only read tmLanguage files in `xml` or `json` format. If you want to make changes to the grammar in the `.yaml-tmlanguage` file, you will need to convert it to `.json` before VSCode can use it. 115 | 116 | Assuming you have followed the steps above to obtain a local copy of the extension, run the `update-grammar` script to convert `syntaxes/scad.yaml-tmlanguage` to `syntaxes/scad.tmlanguage.json` 117 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "//", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | // symbols that are auto closed when typing 15 | "autoClosingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"] 21 | ], 22 | // symbols that that can be used to surround a selection 23 | "surroundingPairs": [ 24 | ["{", "}"], 25 | ["[", "]"], 26 | ["(", ")"], 27 | ["\"", "\""], 28 | ["'", "'"] 29 | ] 30 | } -------------------------------------------------------------------------------- /media/cheatsheet/cheatsheet-auto.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/openscad/openscad.github.com/blob/master/cheatsheet/css/main.css (0354879) */ 2 | body { 3 | /* font-family: "UbuntuMonoRegular", "Ubuntu Mono", 'courier new', monospace, sans-serif; */ 4 | font-size: 100%; 5 | line-height: 1em; 6 | color: var(--vscode-foreground); 7 | padding: 0.6em; 8 | } 9 | 10 | a { 11 | color: var(--vscode-editorLink-activeForeground); 12 | } 13 | 14 | a:hover { 15 | color: var(--vscode-editorLink-activeForeground); 16 | } 17 | 18 | h1 { 19 | /* font-family: "UbuntuMonoBold", "Ubuntu Mono", 'courier new', monospace, sans-serif; */ 20 | font-weight: normal; 21 | font-style: normal; 22 | font-size: 1.8em; 23 | color: var(--vscode-foreground); 24 | margin: 0 0 0.6em 0; 25 | } 26 | 27 | header h1 { 28 | display: inline-block; 29 | margin-right: 1ex; 30 | } 31 | 32 | /* colour the header 'Open' & 'SCAD' and make bolder and chunky 33 | like the cornfield default scheme from colormap.cc 34 | Open - #9dcb51;} OPENCSG_FACE_BACK_COLOR - green 35 | SCAD - #f9d72c;} OPENCSG_FACE_FRONT_COLOR - yellow 36 | - for reference #FFFFE5;} BACKGROUND_COLOR - pale yellow/cream, used above 37 | */ 38 | header h1.title {color:var(--vscode-foreground); letter-spacing:-1px; font-weight:700;} 39 | header h1 span.green {color:var(--vscode-foreground); letter-spacing:0px;} 40 | 41 | /* header h2.subtitle {color:black; letter-spacing:-1px;} */ 42 | 43 | /* colour OPEN & SCAD in the home/section heading the same*/ 44 | section h1 strong {color:var(--vscode-foreground); font-weight:700; letter-spacing:-1px;} 45 | section h1 strong span.green {color:var(--vscode-foreground); letter-spacing:0px;} 46 | 47 | h2 { 48 | /* font-family: "UbuntuMonoBold", "Ubuntu Mono", 'courier new', monospace, sans-serif; */ 49 | font-weight: normal; 50 | font-style: normal; 51 | font-size: 1.4em; 52 | color: var(--vscode-tab-activeForeground); 53 | margin: 0 0 10px 0; 54 | } 55 | 56 | header h2 { 57 | display: inline; 58 | color: var(--vscode-foreground);; 59 | } 60 | 61 | header h2 a { 62 | color: var(--vscode-foreground);; 63 | } 64 | 65 | section { 66 | float: left; 67 | } 68 | 69 | section section article { 70 | float: none; 71 | } 72 | 73 | article { 74 | /* background: var(--vscode-editor-background); */ 75 | -webkit-border-radius: 0.6em; 76 | -moz-border-radius: 0.6em; 77 | border-radius: 0.6em; 78 | padding: 0.6em 0.6em 0.6em 0.6em; 79 | float: left; 80 | margin: 0 0.6em 0.6em 0; 81 | border: 1px solid var(--vscode-tab-inactiveForeground); 82 | } 83 | 84 | article.info { 85 | /* background: #fff; */ 86 | border: 1px solid var(--vscode-tab-inactiveForeground); 87 | } 88 | 89 | article.info h2 { 90 | color: var(--vscode-tab-activeForeground); 91 | } 92 | 93 | code { 94 | /* font-family: "UbuntuMonoRegular", "Ubuntu Mono", 'courier new', monospace, sans-serif; */ 95 | color: var(--vscode-tab-activeForeground); 96 | display: block; 97 | font-size: larger; 98 | margin: 0 0 0.5em 0; 99 | } 100 | 101 | /* Used for `for()` and other function argument descriptions */ 102 | code span { 103 | color: var(--vscode-tab-inactiveForeground); 104 | } 105 | 106 | dt code { 107 | display: inline; 108 | } 109 | 110 | dt { 111 | float: left; 112 | clear: left; 113 | } 114 | 115 | dd { 116 | margin: 0 0 .5em 3em; 117 | color: var(--vscode-tab-activeForeground) 118 | } 119 | 120 | ul { 121 | margin: 0 0 0.5em; 122 | padding: 0 1em; 123 | } 124 | 125 | ul li { 126 | /*margin: 0 0 0.2em 0;*/ 127 | line-height: 1.2em; 128 | } 129 | 130 | footer { 131 | clear: both; 132 | float: right; 133 | font-size: 0.7em; 134 | margin: 0 0.6em 0.6em 0; 135 | text-align: right; 136 | } 137 | 138 | .clear { 139 | clear: both; 140 | } 141 | 142 | .fork { 143 | position: absolute; 144 | top: 0; 145 | right:0; 146 | } 147 | 148 | .fork img{ 149 | border: none; 150 | } 151 | 152 | /* Tooltip stuff */ 153 | .tooltip { 154 | position: relative; 155 | display: inline-block; 156 | border-bottom: 1px dotted; /*var(--vscode-symbolIcon-keywordForeground); /* If you want dots under the hoverable text */ 157 | } 158 | 159 | .tooltip .tooltiptext { 160 | visibility: hidden; 161 | background-color: var(--vscode-editorGroupHeader-tabsBackground); 162 | color: var(--vscode-editor-selectionForeground); 163 | text-align: center; 164 | white-space: nowrap; 165 | padding: 8px; 166 | border-radius: 6px; 167 | bottom: 100%; 168 | left: 50%; 169 | margin-left: -60px; 170 | position: absolute; 171 | z-index: 1; 172 | } 173 | 174 | .tooltip:hover .tooltiptext { 175 | visibility: visible; 176 | } 177 | 178 | .tooltip .tooltiptext::after { 179 | content: " "; 180 | position: absolute; 181 | top: 100%; /* At the bottom of the tooltip */ 182 | left: 3em; 183 | margin-left: -5px; 184 | border-width: 5px; 185 | border-style: solid; 186 | border-color: var(--vscode-editorGroupHeader-tabsBackground) transparent transparent transparent; 187 | } -------------------------------------------------------------------------------- /media/cheatsheet/cheatsheet-original.css: -------------------------------------------------------------------------------- 1 | /* https://github.com/openscad/openscad.github.com/blob/master/cheatsheet/css/main.css (0354879) */ 2 | body { 3 | font-family: "UbuntuMonoRegular", "Ubuntu Mono", 'courier new', monospace, sans-serif; 4 | font-size: 100%; 5 | line-height: 1em; 6 | color: #999; 7 | padding: 0.6em; 8 | } 9 | 10 | a { 11 | color: #000099; 12 | } 13 | 14 | h1 { 15 | font-family: "UbuntuMonoBold", "Ubuntu Mono", 'courier new', monospace, sans-serif; 16 | font-weight: normal; 17 | font-style: normal; 18 | font-size: 1.8em; 19 | color: #DEBA00; 20 | margin: 0 0 0.6em 0; 21 | } 22 | 23 | header h1 { 24 | display: inline-block; 25 | margin-right: 1ex; 26 | } 27 | 28 | /* colour the header 'Open' & 'SCAD' and make bolder and chunky 29 | like the cornfield default scheme from colormap.cc 30 | Open - #9dcb51;} OPENCSG_FACE_BACK_COLOR - green 31 | SCAD - #f9d72c;} OPENCSG_FACE_FRONT_COLOR - yellow 32 | - for reference #FFFFE5;} BACKGROUND_COLOR - pale yellow/cream, used above 33 | */ 34 | header h1.title {color:#f9d72c; letter-spacing:-1px; font-weight:700;} 35 | header h1 span.green {color:#9dcb51; letter-spacing:0px;} 36 | 37 | header h2.subtitle {color:black; letter-spacing:-1px;} 38 | 39 | /* colour OPEN & SCAD in the home/section heading the same*/ 40 | section h1 strong {color:#f9d72c; font-weight:700; letter-spacing:-1px;} 41 | section h1 strong span.green {color:#9dcb51; letter-spacing:0px;} 42 | 43 | h2 { 44 | font-family: "UbuntuMonoBold", "Ubuntu Mono", 'courier new', monospace, sans-serif; 45 | font-weight: normal; 46 | font-style: normal; 47 | font-size: 1.4em; 48 | color: #79A22E; 49 | margin: 0 0 10px 0; 50 | } 51 | 52 | header h2 { 53 | display: inline; 54 | color: #bbb; 55 | } 56 | 57 | header h2 a { 58 | color: #bbb; 59 | } 60 | 61 | section { 62 | float: left; 63 | } 64 | 65 | section section article { 66 | float: none; 67 | } 68 | 69 | article { 70 | background: #FFFFDD; 71 | -webkit-border-radius: 0.6em; 72 | -moz-border-radius: 0.6em; 73 | border-radius: 0.6em; 74 | padding: 0.6em 0.6em 0.6em 0.6em; 75 | float: left; 76 | margin: 0 0.6em 0.6em 0; 77 | border: 1px solid #DEBA00; 78 | } 79 | 80 | article.info { 81 | background: #fff; 82 | border: 1px solid #ddd; 83 | } 84 | 85 | article.info h2 { 86 | color: #bbb; 87 | } 88 | 89 | code { 90 | font-family: "UbuntuMonoRegular", "Ubuntu Mono", 'courier new', monospace, sans-serif; 91 | color: #000; 92 | display: block; 93 | margin: 0 0 0.5em 0; 94 | } 95 | 96 | code span { 97 | color: #999; 98 | } 99 | 100 | dt code { 101 | display: inline; 102 | } 103 | 104 | dt { 105 | float: left; 106 | clear: left; 107 | } 108 | 109 | dd { 110 | margin: 0 0 .5em 3em; 111 | color: #000 112 | } 113 | 114 | ul { 115 | margin: 0 0 0.5em; 116 | padding: 0 1em; 117 | } 118 | 119 | ul li { 120 | /*margin: 0 0 0.2em 0;*/ 121 | line-height: 1.2em; 122 | } 123 | 124 | footer { 125 | clear: both; 126 | float: right; 127 | font-size: 0.7em; 128 | margin: 0 0.6em 0.6em 0; 129 | text-align: right; 130 | } 131 | 132 | .clear { 133 | clear: both; 134 | } 135 | 136 | .fork { 137 | position: absolute; 138 | top: 0; 139 | right:0; 140 | } 141 | 142 | .fork img{ 143 | border: none; 144 | } 145 | 146 | .tooltip { 147 | position: relative; 148 | display: inline-block; 149 | border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ 150 | } 151 | 152 | .tooltip .tooltiptext { 153 | visibility: hidden; 154 | background-color: #79A22E; 155 | color: #fff; 156 | text-align: center; 157 | white-space: nowrap; 158 | padding: 8px; 159 | border-radius: 6px; 160 | bottom: 100%; 161 | left: 50%; 162 | margin-left: -60px; 163 | position: absolute; 164 | z-index: 1; 165 | } 166 | 167 | .tooltip:hover .tooltiptext { 168 | visibility: visible; 169 | } 170 | 171 | .tooltip .tooltiptext::after { 172 | content: " "; 173 | position: absolute; 174 | top: 100%; /* At the bottom of the tooltip */ 175 | left: 3em; 176 | margin-left: -5px; 177 | border-width: 5px; 178 | border-style: solid; 179 | border-color: #79A22E transparent transparent transparent; 180 | } -------------------------------------------------------------------------------- /media/cheatsheet/cheatsheet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | OpenSCAD CheatSheet 13 | 27 | 28 | 29 |
30 |

OpenSCAD

31 |

v2021.01

32 |
33 |
34 |
35 |
36 |

Syntax

37 | var = value; 38 | var = cond ? value_if_true : value_if_false; 39 | var = function (x) x + x; 40 | module name(…) { … }
41 | name();
42 | function name(…) = …
43 | name();
44 | include <….scad> 45 | use <….scad> 46 |
47 |
48 |

Constants

49 |
50 |
undef
51 |
undefined value
52 |
PI
53 |
mathematical constant π (~3.14159)
54 |
55 |
56 | 76 |
77 |

Special variables

78 |
79 |
$fa
80 |
minimum angle
81 |
$fs
82 |
minimum size
83 |
$fn
84 |
number of fragments
85 |
$t
86 |
animation step
87 |
$vpr
88 |
viewport rotation angles in degrees
89 |
$vpt
90 |
viewport translation
91 |
$vpd
92 |
viewport camera distance
93 |
$vpf
94 |
viewport camera field of view
95 |
$children
96 |
 number of module children
97 |
$preview
98 |
 true in F5 preview, false for F6
99 |
100 |
101 |
102 |
103 |
104 |

Modifier Characters

105 |
106 |
*
107 |
disable
108 |
!
109 |
show only
110 |
#
111 |
highlight / debug
112 |
%
113 |
transparent / background
114 |
115 |
116 |
117 |

2D

118 | circle(radius | d=diameter) 119 | square(size,center) 120 | square([width,height],center) 121 | polygon([points]) 122 | polygon([points],[paths]) 123 | text(t, size, font,
     halign, valign, spacing,
     direction, language, script)
124 | import("….extformats: DXF|SVG") 125 | projection(cut) 126 |
127 |
128 |

3D

129 | sphere(radius | d=diameter) 130 | cube(size, center) 131 | cube([width,depth,height], center) 132 | cylinder(h,r|d,center) 133 | cylinder(h,r1|d1,r2|d2,center) 134 | polyhedron(points, faces, convexity) 135 | import("….extformats: STL|OFF|AMF|3MF") 136 | linear_extrude(height,center,convexity,twist,slices) 137 | rotate_extrude(angle,convexity) 138 | surface(file = "….extformats: DAT|PNG",center,convexity) 139 |
140 | 156 |
157 |
158 | 169 | 175 |
176 |

List Comprehensions

177 | Generate [ for (i = range|list) i ] 178 | Generate [ for (init;condition;next) i ] 179 | Flatten [ each i ] 180 | Conditions [ for (i = …) if (condition(i)) i ] 181 | Conditions [ for (i = …) if (condition(i)) x else y ] 182 | Assignments [ for (i = …) let (assignments) a ] 183 |
184 |
185 |

Flow Control

186 | for (i = [start:end]) { … } 187 | for (i = [start:step:end]) { … } 188 | for (i = […,…,…]) { … } 189 | for (i = …, j = …, …) { … } 190 | intersection_for(i = [start:end]) { … } 191 | intersection_for(i = [start:step:end]) { … } 192 | intersection_for(i = […,…,…]) { … } 193 | if (…) { … } 194 | let (…) { … } 195 |
196 | 205 | 213 |
214 | 215 |
216 | 228 | 255 |
256 | 257 |
258 | 259 |
260 | 270 |
271 |
272 | 273 | 277 | 278 | 279 | -------------------------------------------------------------------------------- /media/icons/export-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /media/icons/export-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /media/icons/kill-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /media/icons/kill-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /media/icons/preview-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /media/icons/preview-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /media/icons/source/export-icon.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/icons/source/export-icon.afdesign -------------------------------------------------------------------------------- /media/icons/source/preview+kill.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/icons/source/preview+kill.afdesign -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/logo.png -------------------------------------------------------------------------------- /media/screenshots/comparison-os.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/screenshots/comparison-os.png -------------------------------------------------------------------------------- /media/screenshots/comparison-vsc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/screenshots/comparison-vsc.png -------------------------------------------------------------------------------- /media/screenshots/comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/screenshots/comparison.png -------------------------------------------------------------------------------- /media/screenshots/customizer-highlights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/screenshots/customizer-highlights.png -------------------------------------------------------------------------------- /media/screenshots/open-cheatsheet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/screenshots/open-cheatsheet.gif -------------------------------------------------------------------------------- /media/screenshots/openscad-export.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/screenshots/openscad-export.gif -------------------------------------------------------------------------------- /media/screenshots/openscad-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antyos/vscode-openscad/a5390eda6833b0b74618261844913214093d6e41/media/screenshots/openscad-preview.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openscad", 3 | "displayName": "OpenSCAD", 4 | "description": "OpenSCAD highlighting, snippets, and more for VSCode!", 5 | "version": "1.3.2", 6 | "publisher": "Antyos", 7 | "icon": "media/logo.png", 8 | "license": "SEE LICENSE IN LICENSE.txt", 9 | "engines": { 10 | "vscode": "^1.75.0" 11 | }, 12 | "categories": [ 13 | "Programming Languages", 14 | "Snippets" 15 | ], 16 | "keywords": [ 17 | "openscad", 18 | "scad" 19 | ], 20 | "homepage": "https://github.com/Antyos/vscode-openscad/blob/master/README.md", 21 | "bugs": { 22 | "url": "https://github.com/Antyos/vscode-openscad/issues" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/Antyos/vscode-openscad" 27 | }, 28 | "activationEvents": [ 29 | "workspaceContains:**/*.scad", 30 | "onWebviewPanel:cheatsheet" 31 | ], 32 | "main": "./dist/extension.node.js", 33 | "browser": "./dist/extension.web.js", 34 | "contributes": { 35 | "commands": [ 36 | { 37 | "command": "openscad.cheatsheet", 38 | "title": "Open Cheatsheet", 39 | "category": "OpenSCAD" 40 | }, 41 | { 42 | "command": "openscad.preview", 43 | "title": "Preview in OpenSCAD", 44 | "category": "OpenSCAD", 45 | "icon": { 46 | "light": "./media/icons/preview-light.svg", 47 | "dark": "./media/icons/preview-dark.svg" 48 | } 49 | }, 50 | { 51 | "command": "openscad.kill", 52 | "title": "Kill OpenSCAD Previews", 53 | "category": "OpenSCAD", 54 | "icon": { 55 | "light": "./media/icons/kill-light.svg", 56 | "dark": "./media/icons/kill-dark.svg" 57 | } 58 | }, 59 | { 60 | "command": "openscad.autoKill", 61 | "title": "Kill OpenSCAD Previews", 62 | "category": "OpenSCAD", 63 | "icon": { 64 | "light": "./media/icons/kill-light.svg", 65 | "dark": "./media/icons/kill-dark.svg" 66 | } 67 | }, 68 | { 69 | "command": "openscad.killAll", 70 | "title": "Kill All OpenSCAD Previews", 71 | "category": "OpenSCAD" 72 | }, 73 | { 74 | "command": "openscad.exportByType", 75 | "title": "Export Model (Select File Type)", 76 | "category": "OpenSCAD", 77 | "icon": { 78 | "light": "./media/icons/export-light.svg", 79 | "dark": "./media/icons/export-dark.svg" 80 | } 81 | }, 82 | { 83 | "command": "openscad.exportByConfig", 84 | "title": "Export Model", 85 | "category": "OpenSCAD", 86 | "icon": { 87 | "light": "./media/icons/export-light.svg", 88 | "dark": "./media/icons/export-dark.svg" 89 | } 90 | }, 91 | { 92 | "command": "openscad.exportWithSaveDialogue", 93 | "title": "Export Model with Save Dialogue", 94 | "category": "OpenSCAD", 95 | "icon": { 96 | "light": "./media/icons/export-light.svg", 97 | "dark": "./media/icons/export-dark.svg" 98 | } 99 | }, 100 | { 101 | "command": "openscad.showOutput", 102 | "title": "Show OpenSCAD output channel", 103 | "category": "OpenSCAD" 104 | } 105 | ], 106 | "menus": { 107 | "editor/title": [ 108 | { 109 | "command": "openscad.preview", 110 | "when": "editorLangId == scad && config.openscad.interface.showPreviewIconInEditorTitleMenu && !virtualWorkspace", 111 | "group": "navigation@-1.2" 112 | }, 113 | { 114 | "command": "openscad.autoKill", 115 | "alt": "openscad.kill", 116 | "when": "editorLangId == scad && config.openscad.interface.showKillIconInEditorTitleMenu && !virtualWorkspace", 117 | "group": "navigation@-1.3" 118 | }, 119 | { 120 | "command": "openscad.exportByConfig", 121 | "alt": "openscad.exportWithSaveDialogue", 122 | "when": "editorLangId == scad && config.openscad.interface.showExportIconInEditorTitleMenu && !virtualWorkspace", 123 | "group": "navigation@-1.1" 124 | } 125 | ], 126 | "editor/title/context": [ 127 | { 128 | "command": "openscad.preview", 129 | "when": "resourceLangId == scad && config.openscad.interface.showPreviewInContextMenus && config.openscad.interface.showCommandsInEditorTitleContextMenu", 130 | "group": "navigation@0" 131 | }, 132 | { 133 | "command": "openscad.exportByConfig", 134 | "alt": "openscad.exportWithSaveDialogue", 135 | "when": "resourceLangId == scad && config.openscad.interface.showExportInContextMenus && config.openscad.interface.showCommandsInEditorTitleContextMenu", 136 | "group": "navigation@1" 137 | } 138 | ], 139 | "explorer/context": [ 140 | { 141 | "command": "openscad.preview", 142 | "when": "resourceLangId == scad && config.openscad.interface.showPreviewInContextMenus && config.openscad.interface.showCommandsInExplorerContextMenu", 143 | "group": "navigation@-1.3" 144 | }, 145 | { 146 | "command": "openscad.exportByConfig", 147 | "alt": "openscad.exportWithSaveDialogue", 148 | "when": "resourceLangId == scad && config.openscad.interface.showExportInContextMenus && config.openscad.interface.showCommandsInExplorerContextMenu", 149 | "group": "navigation@-1.1" 150 | } 151 | ], 152 | "commandPalette": [ 153 | { 154 | "command": "openscad.preview", 155 | "when": "editorLangId == scad || resourceLangId == scad" 156 | }, 157 | { 158 | "command": "openscad.kill", 159 | "when": "areOpenScadPreviews" 160 | }, 161 | { 162 | "command": "openscad.autoKill", 163 | "when": "never" 164 | }, 165 | { 166 | "command": "openscad.killAll", 167 | "when": "areOpenScadPreviews" 168 | }, 169 | { 170 | "command": "openscad.exportByType", 171 | "when": "editorLangId == scad || resourceLangId == scad" 172 | }, 173 | { 174 | "command": "openscad.exportWithSaveDialogue", 175 | "when": "editorLangId == scad || resourceLangId == scad" 176 | }, 177 | { 178 | "command": "openscad.exportByConfig", 179 | "when": "editorLangId == scad || resourceLangId == scad" 180 | } 181 | ] 182 | }, 183 | "languages": [ 184 | { 185 | "id": "scad", 186 | "aliases": [ 187 | "OpenSCAD", 188 | "openscad", 189 | "scad" 190 | ], 191 | "extensions": [ 192 | ".scad" 193 | ], 194 | "configuration": "./language-configuration.json", 195 | "icon": { 196 | "light": "./media/openscad.svg", 197 | "dark": "./media/openscad.svg" 198 | } 199 | } 200 | ], 201 | "snippets": [ 202 | { 203 | "language": "scad", 204 | "path": "./snippets/snippets.json" 205 | } 206 | ], 207 | "grammars": [ 208 | { 209 | "language": "scad", 210 | "scopeName": "source.scad", 211 | "path": "./syntaxes/scad.tmLanguage.json" 212 | } 213 | ], 214 | "configuration": { 215 | "title": "OpenSCAD", 216 | "properties": { 217 | "openscad.launchPath": { 218 | "type": "string", 219 | "default": "", 220 | "order": 0, 221 | "markdownDescription": "Command to launch `openscad`. Either the path to the openscad executable, or just \"`openscad`\" (no quotes) if the executable is in the path. If left blank, it will use the default path for your system noted below:\n- Windows: `C:\\Program Files\\Openscad\\openscad.exe`\n - _Note:_ Must specify path to `openscad.exe` on Windows for kill command to work properly.\n- MacOS: `/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD`\n- Linux: `openscad` (Automatically in path)", 222 | "scope": "machine-overridable" 223 | }, 224 | "openscad.launchArgs": { 225 | "type": "array", 226 | "order": 1, 227 | "description": "Extra arguments to pass to the openscad executable when launching. Useful for package managers like Flatpak.", 228 | "scope": "machine-overridable", 229 | "items": { 230 | "type": "string" 231 | } 232 | }, 233 | "openscad.experimental.skipLaunchPathValidation": { 234 | "type": "boolean", 235 | "default": false, 236 | "markdownDescription": "*WARNING: Do not enable this unles you know what you are doing.*\n\nSkip validation of the `openscad.launchPath` setting. Use this if you are sure the path is correct but it is not being accepted. This may occur if you are using a openscad executable which does not output a version when calling `openscad --version`.", 237 | "scope": "machine-overridable" 238 | }, 239 | "openscad.maxInstances": { 240 | "type": "number", 241 | "default": 0, 242 | "markdownDescription": "The maximum number of instances of OpenSCAD allowed.\nSet to `0` for no limit. Instances include any open previews and any active exports.\n> _Note:_ If you decrease this to less than the number of open editors, they will remain open but you will not be able to open more." 243 | }, 244 | "openscad.showKillMessage": { 245 | "type": "boolean", 246 | "default": true, 247 | "description": "Show message when a preview is killed." 248 | }, 249 | "openscad.logLevel": { 250 | "type": "string", 251 | "default": "INFO", 252 | "description": "Log level to output to OpenSCAD output channel.", 253 | "enum": [ 254 | "DEBUG", 255 | "INFO", 256 | "WARN", 257 | "ERROR", 258 | "NONE" 259 | ] 260 | }, 261 | "openscad.export.preferredExportFileExtension": { 262 | "type": "string", 263 | "enum": [ 264 | "none", 265 | "stl", 266 | "off", 267 | "amf", 268 | "3mf", 269 | "csg", 270 | "dxf", 271 | "svg", 272 | "png", 273 | "echo", 274 | "ast", 275 | "term", 276 | "nef3", 277 | "nefdbg" 278 | ], 279 | "default": "stl", 280 | "markdownDescription": "Preferred file extension to use when exporting using the **Export Model** button in the editor title bar. Set to `none` to select the file extension each time." 281 | }, 282 | "openscad.export.autoNamingFormat": { 283 | "type": "deprecated", 284 | "deprecationMessage": "Please use `openscad.export.exportNameFormat` instead." 285 | }, 286 | "openscad.export.exportNameFormat": { 287 | "type": "string", 288 | "default": "${fileBasenameNoExtension}.${exportExtension}", 289 | "markdownDescription": "The default format pattern to name exported files. Based on variables used in `tasks.json`. For a full list of variables, see the wiki page [here](https://github.com/Antyos/vscode-openscad/wiki/Configuring-Export-Name-Format). Commonly used variables include:\n- `${workspaceFolder}` - The path of the folder open in VS Code\n- `${fileBasenameNoExtension}` - The current opened file's basename with no file extension\n- `${exportExtension}` - The file extension used in the export\n- `${#}` - Auto versioning placeholder. Starts at '1' and increments each time you export a file. Prevents accidental overrides.\n- `${date}` - Date placeholder. Defaults to ISO 8601 formatting. Specify a custom format with `${date:FORMAT}` using [Luxon tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens)\n\nThis option can be overriden on a per-file basis by specifying a comment: `// exportExtension=${fileBasenameNoExtension}.${exportExtension}`\nNote: If the path is not specified absolutely, the extension assumes it starts in the location of the `.scad` file being exported." 290 | }, 291 | "openscad.export.useAutoNamingExport": { 292 | "type": "deprecated", 293 | "deprecationMessage": "Please use `openscad.export.skipSaveDialog` instead." 294 | }, 295 | "openscad.export.skipSaveDialog": { 296 | "type": "boolean", 297 | "default": false, 298 | "markdownDescription": "When enabled, the standard action of **Export Model** will automatically choose a name based on the configuration `openscad.export.exportNameFormat` and export without a save dialog.\n\nNote that when this option is enabled, the save dialogue can still be opened by `alt + clicking` the **Export Model** button. Also, the save dialog can always be accessed by running **Export Model with Save Dialog** through the command palette." 299 | }, 300 | "openscad.export.useAutoNamingInSaveDialogues": { 301 | "type": "deprecated", 302 | "deprecationMessage": "Please use `openscad.export.saveDialogExportNameFormat` instead." 303 | }, 304 | "openscad.export.saveDialogExportNameFormat": { 305 | "type": "string", 306 | "default": "", 307 | "markdownDescription": "The default name of to-be exported files in save dialogs. Defaults to `openscad.export.exportNameFormat` if left blank." 308 | }, 309 | "openscad.interface.showPreviewIconInEditorTitleMenu": { 310 | "type": "boolean", 311 | "default": true, 312 | "markdownDescription": "Show **Preview in OpenSCAD** button in editor title menu (right side of tabs)." 313 | }, 314 | "openscad.interface.showKillIconInEditorTitleMenu": { 315 | "type": "boolean", 316 | "default": true, 317 | "markdownDescription": "Show **Kill OpenSCAD Previews** button in editor title menu (right side of tabs)." 318 | }, 319 | "openscad.interface.showExportIconInEditorTitleMenu": { 320 | "type": "boolean", 321 | "default": true, 322 | "markdownDescription": "Show **Export** button in editor title menu (right side of tabs)." 323 | }, 324 | "openscad.interface.showCommandsInEditorTitleContextMenu": { 325 | "type": "boolean", 326 | "default": true, 327 | "description": "Show commands in editor title (tab) context menu." 328 | }, 329 | "openscad.interface.showCommandsInExplorerContextMenu": { 330 | "type": "boolean", 331 | "default": true, 332 | "description": "Show commands in explorer context menu." 333 | }, 334 | "openscad.interface.showPreviewInContextMenus": { 335 | "type": "boolean", 336 | "default": true, 337 | "markdownDescription": "Show **Preview in OpenSCAD** command in context menus." 338 | }, 339 | "openscad.interface.showExportInContextMenus": { 340 | "type": "boolean", 341 | "default": true, 342 | "markdownDescription": "Show **Export Model** command in context menus." 343 | }, 344 | "openscad.cheatsheet.displayInStatusBar": { 345 | "type": "string", 346 | "enum": [ 347 | "always", 348 | "openDoc", 349 | "activeDoc", 350 | "never" 351 | ], 352 | "default": "openDoc", 353 | "description": "When to display the \"Open Cheatsheet\" status bar icon.", 354 | "enumDescriptions": [ 355 | "Always displays \"Open Cheatsheet\" in status bar", 356 | "Only display \"Open Cheatsheet\" in status bar when a '.scad' is in an open editor", 357 | "Only display \"Open Cheatsheet\" in status bar when a '.scad' is the active text editor", 358 | "Never display \"Open Cheatsheet\" in status bar" 359 | ] 360 | }, 361 | "openscad.cheatsheet.colorScheme": { 362 | "type": "string", 363 | "enum": [ 364 | "original", 365 | "auto" 366 | ], 367 | "default": "auto", 368 | "description": "The color scheme to use when displaying the OpenSCAD cheatsheet.", 369 | "enumDescriptions": [ 370 | "Use original OpenSCAD cheatsheet color scheme", 371 | "Use VSCode's current theme for cheatsheet color sceheme" 372 | ] 373 | }, 374 | "openscad.cheatsheet.openToSide": { 375 | "type": "string", 376 | "enum": [ 377 | "beside", 378 | "currentGroup" 379 | ], 380 | "default": "beside", 381 | "description": "Controls which column OpenSCAD cheatsheet is opened.", 382 | "enumDescriptions": [ 383 | "Open cheatsheet in column beside current one", 384 | "Open cheatsheet in current column/group" 385 | ] 386 | } 387 | } 388 | }, 389 | "keybindings": [ 390 | { 391 | "command": "openscad.preview", 392 | "key": "f5", 393 | "when": "resourceLangId == scad" 394 | }, 395 | { 396 | "command": "openscad.exportByConfig", 397 | "key": "f6", 398 | "when": "resourceLangId == scad" 399 | } 400 | ] 401 | }, 402 | "scripts": { 403 | "test-web": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/index.js", 404 | "pretest": "pnpm run compile-web", 405 | "vscode:prepublish": "pnpm run package", 406 | "lint": "eslint -c .eslintrc.json --ext .ts src", 407 | "format": "prettier --config .prettierrc.json --write ./src/**/*.ts && eslint -c .eslintrc.json --ext .ts src --fix", 408 | "clean": "del-cli dist", 409 | "compile": "webpack", 410 | "compile-clean": "pnpm run clean && pnpm run compile", 411 | "watch": "webpack --watch", 412 | "package": "webpack --mode production --devtool hidden-source-map", 413 | "update-grammar": "js-yaml syntaxes/scad.yaml-tmLanguage > syntaxes/scad.tmLanguage.json", 414 | "run-in-browser": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. ." 415 | }, 416 | "devDependencies": { 417 | "@types/command-exists": "^1.2.0", 418 | "@types/glob": "^7.2.0", 419 | "@types/jsdom": "^16.2.15", 420 | "@types/luxon": "^3.4.0", 421 | "@types/mocha": "^9.1.1", 422 | "@types/node": "^16.18.16", 423 | "@types/vscode": "^1.67.0", 424 | "@types/webpack-env": "^1.18.0", 425 | "@typescript-eslint/eslint-plugin": "^5.55.0", 426 | "@typescript-eslint/parser": "^5.55.0", 427 | "@vscode/test-electron": "^2.3.0", 428 | "@vscode/test-web": "^0.0.15", 429 | "@vscode/vsce": "^2.18.0", 430 | "assert": "^2.0.0", 431 | "del-cli": "^5.0.0", 432 | "eslint": "^8.36.0", 433 | "eslint-config-prettier": "^8.7.0", 434 | "eslint-config-vscode-ext": "^1.1.0", 435 | "eslint-plugin-import": "^2.27.5", 436 | "eslint-plugin-prettier": "^4.2.1", 437 | "eslint-plugin-simple-import-sort": "^7.0.0", 438 | "eslint-plugin-unicorn": "^39.0.0", 439 | "js-yaml": "^4.1.0", 440 | "mocha": "^9.2.2", 441 | "prettier": "^2.8.4", 442 | "process": "^0.11.10", 443 | "ts-loader": "^9.4.2", 444 | "ts-node": "^10.9.1", 445 | "typescript": "^4.9.5", 446 | "util": "^0.12.5", 447 | "webpack": "^5.95.0", 448 | "webpack-cli": "^5.1.4" 449 | }, 450 | "dependencies": { 451 | "command-exists": "^1.2.9", 452 | "escape-string-regexp": "^4.0.0", 453 | "luxon": "^3.4.4", 454 | "node-html-parser": "^6.1.5" 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /snippets/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "ass": { 3 | "prefix": "ass", 4 | "body": "assign (${1:x} = ${2:0}) {\r\n\t${}$0\r\n}\r\n", 5 | "description": "assign", 6 | "scope": "source.scad" 7 | }, 8 | "cir": { 9 | "prefix": "cir", 10 | "body": "circle(r=${1:10});", 11 | "description": "circle", 12 | "scope": "source.scad" 13 | }, 14 | "col": { 15 | "prefix": "col", 16 | "body": "color([${1:0}/255, ${2:0}/255, ${3:0}/255]) {\r\n\t${0}\r\n}", 17 | "description": "color", 18 | "scope": "source.scad" 19 | }, 20 | "cu": { 21 | "prefix": "cu", 22 | "body": "cube(size=[${1:10}, ${2:10}, ${3:10}], center=${4:true});", 23 | "description": "cube", 24 | "scope": "source.scad" 25 | }, 26 | "cyl": { 27 | "prefix": "cyl", 28 | "body": "cylinder(r=${1:10}, h=${2:10}, center=${3:true});", 29 | "description": "cylinder", 30 | "scope": "source.scad" 31 | }, 32 | "dif": { 33 | "prefix": "dif", 34 | "body": "difference() {\r\n\t${0}\r\n}", 35 | "description": "difference", 36 | "scope": "source.scad" 37 | }, 38 | "echo": { 39 | "prefix": "echo", 40 | "body": "echo(str(\"${1:Variable = }\", ${2:x}));", 41 | "description": "echo", 42 | "scope": "source.scad" 43 | }, 44 | "for": { 45 | "prefix": "for", 46 | "body": "for (${20:i}=[${1:0}:${2:10}]) {\r\n\t${}$0\r\n}\r\n", 47 | "description": "for (...) {...}", 48 | "scope": "source.scad" 49 | }, 50 | "fun": { 51 | "prefix": "fun", 52 | "body": "function ${1:function_name}(${2:args}) = ${0:// body...};", 53 | "description": "Function", 54 | "scope": "source.scad" 55 | }, 56 | "hul": { 57 | "prefix": "hul", 58 | "body": "hull() {\r\n\t${0}\r\n}", 59 | "description": "hull", 60 | "scope": "source.scad" 61 | }, 62 | "ife": { 63 | "prefix": "ife", 64 | "body": "if (${1:true}) {\r\n\t${0:$TM_SELECTED_TEXT}\r\n} else {\r\n\t\r\n}", 65 | "description": "if ... else", 66 | "scope": "source.scad" 67 | }, 68 | "if": { 69 | "prefix": "if", 70 | "body": "if (${1:true}) {\r\n\t${0:$TM_SELECTED_TEXT}\r\n}", 71 | "description": "if", 72 | "scope": "source.scad" 73 | }, 74 | "is": { 75 | "prefix": "is", 76 | "body": "import_stl(\"${1:filename.stl}\", convexity=${2:10});", 77 | "description": "import_stl", 78 | "scope": "source.scad" 79 | }, 80 | "inc": { 81 | "prefix": "inc", 82 | "body": "include <${1:filename.scad}>", 83 | "description": "include", 84 | "scope": "source.scad" 85 | }, 86 | "ifor": { 87 | "prefix": "ifor", 88 | "body": "for (${20:i} = [${1:0}:${2:10}]) {\r\n\t${}$0\r\n}\r\n", 89 | "description": "intersection_for (...) {...}", 90 | "scope": "source.scad" 91 | }, 92 | "le": { 93 | "prefix": "le", 94 | "body": "linear_extrude(height=${1:10}, center=${2:true}, convexity=${3:10}, twist=${4:0}) {\r\n\t${0}\r\n}", 95 | "description": "linear_extrude", 96 | "scope": "source.scad" 97 | }, 98 | "mink": { 99 | "prefix": "mink", 100 | "body": "minkowski() {\r\n\t${0}\r\n}", 101 | "description": "minkowski", 102 | "scope": "source.scad" 103 | }, 104 | "mir": { 105 | "prefix": "mir", 106 | "body": "mirror([${1:1}, ${2:0}, ${3:0}]) {\r\n\t${0}\r\n}\r\n", 107 | "description": "mirror", 108 | "scope": "source.scad" 109 | }, 110 | "mod": { 111 | "prefix": "mod", 112 | "body": "module ${1:module_name}(${2:args}) {\r\n\t${0:// body...}\r\n}", 113 | "description": "Module", 114 | "scope": "source.scad" 115 | }, 116 | "mul": { 117 | "prefix": "mul", 118 | "body": "multimatrix([\r\n\t[${1:1}, ${2:0}, ${3:0}, ${4:10}],\r\n\t[${5:0}, ${6:1}, ${7:0}, ${8:20}],\r\n\t[${9:0}, ${10:0}, ${11:1}, ${12:30}],\r\n\t[${13:0}, ${14:0}, ${15:0}, ${16:1}]\r\n]) {\r\n\t${0}\r\n}\r\n", 119 | "description": "multimatrix", 120 | "scope": "source.scad" 121 | }, 122 | "polyg": { 123 | "prefix": "polyg", 124 | "body": "polygon(points=[${1:[0,0],[100,0],[0,100],[10,10],[80,10],[10,80]}], paths=[${2:[0,1,2],[3,4,5]}]);\r\n", 125 | "description": "polygon", 126 | "scope": "source.scad" 127 | }, 128 | "poly": { 129 | "prefix": "poly", 130 | "body": "polyhedron(points=[${1:[0,0,0],[100,0,0],[0,100,0],[0,100,100]}], faces=[${2:[0,1,2],[1,0,3],[0,2,3],[2,1,3]}]);", 131 | "description": "polyhedron", 132 | "scope": "source.scad" 133 | }, 134 | "proj": { 135 | "prefix": "proj", 136 | "body": "projection(cut=${1:true}) import_stl(\"${2:filename.stl}\");\r\n", 137 | "description": "projection", 138 | "scope": "source.scad" 139 | }, 140 | "ren": { 141 | "prefix": "ren", 142 | "body": "render() {\r\n\t${0:${TM_SELECTED_TEXT/\\n/\\n\\t/g}}\r\n}\r\n", 143 | "description": "render", 144 | "scope": "source.scad" 145 | }, 146 | "r": { 147 | "prefix": "r", 148 | "body": "rotate([${1:0}, ${2:0}, ${3:0}]) ${0}", 149 | "description": "rotate(...)", 150 | "scope": "source.scad" 151 | }, 152 | "rot": { 153 | "prefix": "rot", 154 | "body": "rotate([${1:10}, ${2:10}, ${3:10}]) {\r\n\t${0}\r\n}\r\n", 155 | "description": "rotate(...) {...}", 156 | "scope": "source.scad" 157 | }, 158 | "re": { 159 | "prefix": "re", 160 | "body": "rotate_extrude(convexity=${1:10}) {\r\n\t${0}\r\n}", 161 | "description": "rotate_extrude", 162 | "scope": "source.scad" 163 | }, 164 | "s": { 165 | "prefix": "s", 166 | "body": "scale([${1:1}, ${2:1}, ${3:1}]) ${0}", 167 | "description": "scale(...)", 168 | "scope": "source.scad" 169 | }, 170 | "sca": { 171 | "prefix": "sca", 172 | "body": "scale([${1:1}, ${2:1}, ${3:1}]) {\r\n\t${0}\r\n}", 173 | "description": "scale(...) {...}", 174 | "scope": "source.scad" 175 | }, 176 | "sph": { 177 | "prefix": "sph", 178 | "body": "sphere(r=${1:10});", 179 | "description": "sphere", 180 | "scope": "source.scad" 181 | }, 182 | "squ": { 183 | "prefix": "squ", 184 | "body": "square(size=[${1:10}, ${2:10}], center=${3:true});", 185 | "description": "square", 186 | "scope": "source.scad" 187 | }, 188 | "surf": { 189 | "prefix": "surf", 190 | "body": "surface(file=\"${1:filename.dat}\", center=${2:true}${0:, convexity=5});\r\n", 191 | "description": "surface", 192 | "scope": "source.scad" 193 | }, 194 | "t": { 195 | "prefix": "t", 196 | "body": "translate([${1:0}, ${2:0}, ${3:0}]) ${0}", 197 | "description": "translate(...)", 198 | "scope": "source.scad" 199 | }, 200 | "tran": { 201 | "prefix": "tran", 202 | "body": "translate([${1:0}, ${2:0}, ${3:0}]) {\r\n\t${0}\r\n}", 203 | "description": "translate(...) {...}", 204 | "scope": "source.scad" 205 | }, 206 | "use": { 207 | "prefix": "use", 208 | "body": "use <${1:filename.scad}>", 209 | "description": "use", 210 | "scope": "source.scad" 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/cheatsheet/cheatsheet-content.ts: -------------------------------------------------------------------------------- 1 | // node-html-parser only has appendChild(), not append(); disable the warning. 2 | /* eslint-disable unicorn/prefer-dom-node-append */ 3 | 4 | import { HTMLElement, parse } from 'node-html-parser'; 5 | import * as vscode from 'vscode'; 6 | 7 | import { CheatsheetStyles } from './styles'; 8 | 9 | /** Get HTML content of the OpenSCAD cheatsheet */ 10 | export class CheatsheetContent { 11 | /** Uri to the directory containing the cheatsheet html and style sheets */ 12 | private readonly _cheatsheetUri: vscode.Uri; 13 | /** HTMLElement of document */ 14 | private _document?: HTMLElement; 15 | /** The last key used to get the cheatsheet stylesheet */ 16 | private _lastStyleKey?: string = undefined; 17 | /** Styles container */ 18 | private _cheatsheetStyles: CheatsheetStyles; 19 | /** Content Security Policy */ 20 | private _csp: string; 21 | private _webview: vscode.Webview; 22 | 23 | public constructor(cheatsheetUri: vscode.Uri, webview: vscode.Webview) { 24 | this._cheatsheetUri = cheatsheetUri; 25 | this._cheatsheetStyles = new CheatsheetStyles(cheatsheetUri); 26 | this._webview = webview; 27 | this._csp = `default-src 'none'; style-src ${this._webview.cspSource};`; 28 | } 29 | 30 | /** Get cheatsheet HTML content. Stores HTML from lastStyleKey. */ 31 | public async getContent(styleKey: string): Promise { 32 | // Load cheatsheet if it hasn't been loaded yet 33 | if (this._document === undefined) { 34 | this._document = await this.getCheatsheetHTML().then((x) => x); 35 | } 36 | 37 | // If the styleKey hasn't changed, return the stored copy of the document 38 | if (styleKey === this._lastStyleKey) { 39 | return this._document.toString(); 40 | } 41 | 42 | // Update lastStyleKey. If we do this inside the next promise, we may 43 | // create a race condition if two calls to getContent() are made before 44 | // the promises can be resolved. 45 | this._lastStyleKey = styleKey; 46 | 47 | // Turn off all stylesheets 48 | this.disableAllStylesheets(); 49 | this.enableStylesheet(styleKey); 50 | 51 | // Return document content 52 | return this._document.toString(); 53 | } 54 | 55 | /** 56 | * Disable all stylesheet links in `this._document`. 57 | * Returns false if `this._document` is undefined. 58 | */ 59 | public disableAllStylesheets(): boolean { 60 | // Return if document is undefined 61 | if (!this._document) { 62 | return false; 63 | } 64 | 65 | // Get the stylesheet links. We need to cast to HTMLLinkElement so we 66 | // can set the 'disabled' property. 67 | const stylesheets = this._document.querySelectorAll( 68 | 'link[rel=stylesheet]' 69 | ); 70 | 71 | // Set 'disabled' property of all stylesheets 72 | for (const style of stylesheets) { 73 | style.setAttribute('disabled', ''); 74 | } 75 | 76 | return true; 77 | } 78 | 79 | /** 80 | * Enable a stylesheet link by id from `this._document`. 81 | * Returns false if `this._document` is undefined or no stylesheet exists 82 | * with the passed id. 83 | */ 84 | public enableStylesheet(id: string): boolean { 85 | // Return if document is undefined 86 | if (!this._document) { 87 | return false; 88 | } 89 | 90 | // Get link element of stylesheet we want to enable 91 | const linkElement = this._document.querySelector( 92 | `link[rel=stylesheet][id=${id}]` 93 | ); 94 | 95 | // Return false if element is undefined 96 | if (!linkElement) { 97 | return false; 98 | } 99 | 100 | // Remove disabled property 101 | linkElement.removeAttribute('disabled'); 102 | return true; 103 | } 104 | 105 | public setStylesheet(id: string): boolean { 106 | return this.disableAllStylesheets() && this.enableStylesheet(id); 107 | } 108 | 109 | /** The key used the last time getContent() was called */ 110 | public get lastStyleKey(): string | undefined { 111 | return this._lastStyleKey; 112 | } 113 | 114 | /** 115 | * Get a HTMLElement for a stylesheet. 116 | * @param {string} href Reference to stylesheet 117 | * @param {string | undefined} id of element 118 | * @returns HTMLElement 119 | */ 120 | private getStyleSheetElement(href: string, id?: string): HTMLElement { 121 | // HTMLElement `parent` argument cannot be type 'undefined', so we have 122 | // to disable the check here 123 | const element = new HTMLElement( 124 | 'link', 125 | { id: id ?? '' }, 126 | '', 127 | // eslint-disable-next-line unicorn/no-null 128 | null, 129 | [0, 0] 130 | ); 131 | const attributes = { 132 | type: 'text/css', 133 | rel: 'stylesheet', 134 | href: href, 135 | media: 'all', 136 | id: id ?? '', 137 | }; 138 | 139 | element.setAttributes(attributes); 140 | 141 | return element; 142 | } 143 | 144 | /** 145 | * Get a Content-Security-Policy element for the webview 146 | * 147 | * See: 148 | * - https://code.visualstudio.com/api/extension-guides/webview#content-security-policy 149 | * - https://developers.google.com/web/fundamentals/security/csp/ 150 | */ 151 | protected getCSPElement(): HTMLElement { 152 | // eslint-disable-next-line unicorn/no-null 153 | const element = new HTMLElement('meta', { id: '' }, '', null, [0, 0]); 154 | 155 | element.setAttributes({ 156 | 'http-equiv': 'Content-Security-Policy', 157 | content: this._csp, 158 | }); 159 | 160 | return element; 161 | } 162 | 163 | /** Get the cheatsheet html content for webview */ 164 | private async getCheatsheetHTML(): Promise { 165 | // Read and parse HTML from file 166 | const htmlDocument = await vscode.workspace.fs 167 | .readFile( 168 | vscode.Uri.joinPath(this._cheatsheetUri, 'cheatsheet.html') 169 | ) 170 | .then((uint8array) => { 171 | const fileContent = new TextDecoder().decode(uint8array); 172 | // this.loggingService.logDebug(fileContent.toString()); 173 | return parse(fileContent.toString()); 174 | }); 175 | 176 | // Get document head 177 | const head = htmlDocument.querySelector('head'); 178 | 179 | // ! FIXME 180 | if (!head) { 181 | throw 'No head found'; 182 | } 183 | 184 | // Remove existing css 185 | for (const element of head.querySelectorAll('link')) { 186 | element.remove(); 187 | } 188 | 189 | // Add our css 190 | for (const styleKey of this._cheatsheetStyles) { 191 | // Get Uri of stylesheet for webview 192 | const styleUri = this._webview.asWebviewUri( 193 | this._cheatsheetStyles.styles[styleKey] 194 | ); 195 | 196 | // Create new style element 197 | const newStyle = this.getStyleSheetElement( 198 | styleUri.toString(), 199 | styleKey 200 | ); 201 | 202 | // Append style element 203 | head.appendChild(newStyle); 204 | } 205 | 206 | // Add CSP 207 | head.appendChild(this.getCSPElement()); 208 | 209 | // Return document as html string 210 | return htmlDocument; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/cheatsheet/cheatsheet-panel.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Cheatsheet 3 | * 4 | * Generates a webview panel containing the OpenSCAD cheatsheet 5 | *----------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from 'vscode'; 8 | 9 | import { ScadConfig } from 'src/config'; 10 | import { CheatsheetContent } from './cheatsheet-content'; 11 | 12 | /** 13 | * OpenSCAD Cheatsheet webview and commands. 14 | * 15 | * Only one instance of cheatsheet panel is allowed, so most things are delcared 16 | * `static`. 17 | */ 18 | export class Cheatsheet { 19 | public static readonly csCommandId = 'openscad.cheatsheet'; // Command id for opening the cheatsheet 20 | public static readonly viewType = 'cheatsheet'; // Internal reference to cheatsheet panel 21 | 22 | public static currentPanel: Cheatsheet | undefined; // Webview Panel 23 | private static csStatusBarItem: vscode.StatusBarItem | undefined; // Cheatsheet status bar item 24 | 25 | private readonly _panel: vscode.WebviewPanel; // Webview panels 26 | private static config: ScadConfig = {}; // Extension config 27 | 28 | private cheatsheetContent: CheatsheetContent; 29 | private _disposables: vscode.Disposable[] = []; 30 | 31 | /** Create or show cheatsheet panel */ 32 | public static createOrShowPanel(extensionPath: vscode.Uri): void { 33 | // Determine which column to show cheatsheet in 34 | // If not active editor, check config to open in current window to to the side 35 | const column = vscode.window.activeTextEditor 36 | ? Cheatsheet.config.openToSide === 'beside' 37 | ? vscode.ViewColumn.Beside 38 | : vscode.window.activeTextEditor.viewColumn 39 | : undefined; 40 | 41 | if (Cheatsheet.currentPanel) { 42 | // If we already have a panel, show it in the target column 43 | Cheatsheet.currentPanel._panel.reveal(column); 44 | return; 45 | } 46 | 47 | // Otherwise, create and show new panel 48 | const panel = vscode.window.createWebviewPanel( 49 | Cheatsheet.viewType, // Indentifies the type of webview. Used internally 50 | 'OpenSCAD Cheat Sheet', // Title of panel displayed to the user 51 | column || vscode.ViewColumn.One, // Editor column 52 | { 53 | // Only allow webview to access certain directory 54 | localResourceRoots: [ 55 | vscode.Uri.joinPath(extensionPath, 'media', 'cheatsheet'), 56 | ], 57 | // Disable scripts 58 | // (defaults to false, but no harm in explcit declaration) 59 | enableScripts: false, 60 | // Enable search tool 61 | enableFindWidget: true, 62 | } // Webview options 63 | ); 64 | 65 | // Create new panel 66 | Cheatsheet.currentPanel = new Cheatsheet(panel, extensionPath); 67 | } 68 | 69 | /** Recreate panel in case vscode restarts */ 70 | public static revive( 71 | panel: vscode.WebviewPanel, 72 | extensionPath: vscode.Uri 73 | ): void { 74 | Cheatsheet.currentPanel = new Cheatsheet(panel, extensionPath); 75 | } 76 | 77 | /** Create a new Cheatsheet */ 78 | private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { 79 | this._panel = panel; 80 | 81 | // Listen for when panel is disposed 82 | // This happens when user closes the panel or when the panel is closed progamatically 83 | this._panel.onDidDispose( 84 | () => this.dispose(), 85 | undefined, 86 | this._disposables 87 | ); 88 | 89 | // Cheatsheet content 90 | this.cheatsheetContent = new CheatsheetContent( 91 | vscode.Uri.joinPath(extensionUri, 'media', 'cheatsheet'), 92 | this._panel.webview 93 | ); 94 | 95 | // Set HTML content 96 | this.updateWebviewContent(); 97 | } 98 | 99 | /** Dispose of panel and clean up resources */ 100 | public dispose(): void { 101 | Cheatsheet.currentPanel = undefined; 102 | 103 | // Clean up resources 104 | this._panel.dispose(); 105 | 106 | while (this._disposables.length > 0) { 107 | const x = this._disposables.pop(); 108 | if (x) { 109 | x.dispose; 110 | } 111 | } 112 | } 113 | 114 | /** Initializes the status bar (if not yet) and return the status bar */ 115 | public static getStatusBarItem(): vscode.StatusBarItem { 116 | if (!Cheatsheet.csStatusBarItem) { 117 | Cheatsheet.csStatusBarItem = vscode.window.createStatusBarItem( 118 | vscode.StatusBarAlignment.Left 119 | ); 120 | Cheatsheet.csStatusBarItem.command = Cheatsheet.csCommandId; 121 | } 122 | 123 | return Cheatsheet.csStatusBarItem; 124 | } 125 | 126 | /** Dispose of status bar */ 127 | public static disposeStatusBar(): void { 128 | if (!Cheatsheet.csStatusBarItem) { 129 | return; 130 | } 131 | Cheatsheet.csStatusBarItem.dispose(); 132 | // Cheatsheet.csStatusBarItem = null; // Typescript doesn't like this... 133 | } 134 | 135 | // Show or hide status bar item (OpenSCAD Cheatsheet) 136 | public static updateStatusBar(): void { 137 | let showCsStatusBarItem = false; // Show cheatsheet status bar item or not 138 | 139 | // Determine to show cheatsheet status bar icon based on extension config 140 | switch (Cheatsheet.config.displayInStatusBar) { 141 | case 'always': 142 | showCsStatusBarItem = true; 143 | break; 144 | case 'openDoc': 145 | showCsStatusBarItem = Cheatsheet.isAnyOpenDocumentScad(); 146 | break; 147 | case 'activeDoc': 148 | // Check the languageId of the active text document 149 | if (vscode.window.activeTextEditor) { 150 | showCsStatusBarItem = Cheatsheet.isDocumentScad( 151 | vscode.window.activeTextEditor.document 152 | ); 153 | } 154 | break; 155 | case 'never': 156 | showCsStatusBarItem = false; 157 | break; 158 | } 159 | 160 | // Show or hide `Open Cheatsheet` button 161 | if (Cheatsheet.csStatusBarItem) { 162 | if (showCsStatusBarItem) { 163 | Cheatsheet.csStatusBarItem.text = 'Open Cheatsheet'; 164 | Cheatsheet.csStatusBarItem.show(); 165 | } else { 166 | Cheatsheet.csStatusBarItem.hide(); 167 | } 168 | } 169 | } 170 | 171 | /** Run on change active text editor */ 172 | public static onDidChangeActiveTextEditor(): void { 173 | // Update to the "Open Cheatsheet" status bar icon 174 | Cheatsheet.updateStatusBar(); 175 | } 176 | 177 | /** Run when configurations are changed */ 178 | public static onDidChangeConfiguration( 179 | config: vscode.WorkspaceConfiguration 180 | ): void { 181 | // Load the configuration changes 182 | Cheatsheet.config.displayInStatusBar = config.get( 183 | 'cheatsheet.displayInStatusBar', 184 | 'openDoc' 185 | ); 186 | Cheatsheet.config.colorScheme = config.get( 187 | 'cheatsheet.colorScheme', 188 | 'auto' 189 | ); 190 | Cheatsheet.config.openToSide = config.get( 191 | 'cheatsheet.openToSide', 192 | 'beside' 193 | ); 194 | 195 | // Update the status bar 196 | this.updateStatusBar(); 197 | 198 | // Update css of webview (if config option has changed) 199 | if ( 200 | Cheatsheet.currentPanel && 201 | Cheatsheet.config.colorScheme !== 202 | Cheatsheet.currentPanel.cheatsheetContent.lastStyleKey 203 | ) { 204 | Cheatsheet.currentPanel.updateWebviewContent(); // Update webview html content 205 | } 206 | } 207 | 208 | /** Updates webview html content */ 209 | public updateWebviewContent(): void { 210 | // If config.colorScheme isn't defined, use colorScheme 'auto' 211 | const colorScheme: string = Cheatsheet.config.colorScheme || 'auto'; 212 | 213 | // Set webview content 214 | this.cheatsheetContent.getContent(colorScheme).then((content) => { 215 | this._panel.webview.html = content; 216 | }); 217 | } 218 | 219 | //***************************************************************************** 220 | // Private Methods 221 | //***************************************************************************** 222 | 223 | /** True if there at least one open document of languageId `scad`? */ 224 | private static isAnyOpenDocumentScad(): boolean { 225 | const openDocuments = vscode.workspace.textDocuments; 226 | let isScadDocumentOpen = false; 227 | 228 | // Iterate through open text documents 229 | for (const document of openDocuments) { 230 | if (this.isDocumentScad(document)) 231 | // If document is of type 'scad' return true 232 | isScadDocumentOpen = true; 233 | } 234 | 235 | return isScadDocumentOpen; 236 | } 237 | 238 | /** True if a document languageId is `scad` */ 239 | private static isDocumentScad(document: vscode.TextDocument): boolean { 240 | // vscode.window.showInformationMessage("Doc: " + doc.fileName + "\nLang id: " + langId); // DEBUG 241 | return document.languageId === 'scad'; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/cheatsheet/styles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cheatsheet styles 3 | */ 4 | 5 | import * as vscode from 'vscode'; 6 | 7 | // We could make some wrapper class for this, but we don't have enough .css files for that to 8 | // be worth it. 9 | 10 | /** 11 | * Available css styles for Cheatsheet. Paths are relative to [extensionUri]. 12 | */ 13 | export const STYLES = { 14 | auto: 'cheatsheet-auto.css', 15 | original: 'cheatsheet-original.css', 16 | }; 17 | 18 | type StyleKey = keyof typeof STYLES; 19 | 20 | /** Default style */ 21 | export const DEFAULT_STYLE: StyleKey = 'auto'; 22 | 23 | export class CheatsheetStyles { 24 | public readonly styles: { [key in StyleKey]: vscode.Uri }; 25 | public readonly defaultStyle: vscode.Uri; 26 | 27 | // Allow `___ of Styles` to iterate over _styles 28 | *[Symbol.iterator](): Iterator { 29 | yield* Object.keys(this.styles) as StyleKey[]; 30 | } 31 | 32 | public constructor(stylesUri: vscode.Uri) { 33 | // Map STYLES to Uris relative to `extensionUri`. 34 | // 35 | // Note: because we are compiling to ES6, we can't use 36 | // Object.fromEntries(), so we have to emulate it: 37 | // https://stackoverflow.com/a/43682482 38 | this.styles = Object.assign( 39 | {}, 40 | ...Object.entries(STYLES).map(([styleKey, stylePath]) => { 41 | return { 42 | [styleKey]: vscode.Uri.joinPath(stylesUri, stylePath).with({ 43 | scheme: 'file', 44 | }), 45 | }; 46 | }) 47 | ); 48 | 49 | // Set the default style 50 | this.defaultStyle = this.styles[DEFAULT_STYLE]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Config 3 | * 4 | * Interface containing all configurations that are used by Typescript parts of extension 5 | *----------------------------------------------------------------------------*/ 6 | 7 | import { LogLevel } from './logging-service'; 8 | 9 | /** Extension config values */ 10 | export interface ScadConfig { 11 | openscadPath?: string; 12 | launchArgs?: string[]; 13 | skipLaunchPathValidation?: boolean; 14 | maxInstances?: number; 15 | showKillMessage?: boolean; 16 | preferredExportFileExtension?: string; 17 | exportNameFormat?: string; 18 | skipSaveDialog?: boolean; 19 | saveDialogExportNameFormat?: string; 20 | displayInStatusBar?: string; 21 | colorScheme?: string; 22 | openToSide?: string; 23 | logLevel?: LogLevel; 24 | } 25 | 26 | // Reflects the defaults configuration in package.json 27 | export const DEFAULT_CONFIG: Required = { 28 | openscadPath: '', 29 | skipLaunchPathValidation: false, 30 | launchArgs: [], 31 | maxInstances: 0, 32 | showKillMessage: true, 33 | logLevel: 'INFO', 34 | preferredExportFileExtension: 'stl', 35 | exportNameFormat: '${fileBasenameNoExtension}.${exportExtension}', 36 | skipSaveDialog: false, 37 | saveDialogExportNameFormat: '', 38 | displayInStatusBar: 'openDoc', 39 | colorScheme: 'auto', 40 | openToSide: 'beside', 41 | }; 42 | -------------------------------------------------------------------------------- /src/export/export-file-extensions.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Export File Extensions 3 | * 4 | * Contains types and objects relating to exportable file types 5 | *----------------------------------------------------------------------------*/ 6 | 7 | /** List of all file exportable extensions */ 8 | export const ExportFileExtensionList = [ 9 | 'stl', 10 | 'off', 11 | 'amf', 12 | '3mf', 13 | 'csg', 14 | 'dxf', 15 | 'svg', 16 | 'png', 17 | 'echo', 18 | 'ast', 19 | 'term', 20 | 'nef3', 21 | 'nefdbg', 22 | ] as const; 23 | 24 | /** Avaiable file extensions for export */ 25 | export type ExportFileExtension = (typeof ExportFileExtensionList)[number]; 26 | 27 | /** File types used in save dialogue */ 28 | export const ExportExtensionsForSave = { 29 | STL: ['stl'], 30 | OFF: ['off'], 31 | AMF: ['amf'], 32 | '3MF': ['3mf'], 33 | CSG: ['csg'], 34 | DXF: ['dxf'], 35 | SVG: ['svg'], 36 | PNG: ['png'], 37 | 'Echo file output': ['echo'], 38 | AST: ['ast'], 39 | TERM: ['term'], 40 | NEF3: ['nef3'], 41 | NEFDBG: ['nefdbg'], 42 | }; 43 | -------------------------------------------------------------------------------- /src/export/variable-resolver.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Variable Resolver 3 | * 4 | * Resolves variables in a string with respect to a workspace or file 5 | * 6 | * Based on code from: 7 | * - https://github.com/microsoft/vscode/blob/9450b5e5fb04f2a180cfffc4d27f52f972b1f369/src/vs/workbench/services/configurationResolver/common/variableResolver.ts 8 | * - https://github.com/microsoft/vscode/blob/9f1aa3c9feecd04a79d22fd6752ba14a83b48f1b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts 9 | *----------------------------------------------------------------------------*/ 10 | 11 | import escapeStringRegexp = require('escape-string-regexp'); 12 | import * as fs from 'fs'; // node:fs 13 | import * as luxon from 'luxon'; 14 | import { platform } from 'os'; // node:os 15 | import * as path from 'path'; // node:path 16 | import * as vscode from 'vscode'; 17 | 18 | import { LoggingService } from 'src/logging-service'; 19 | 20 | /** Get file name without extension */ 21 | export function fileBasenameNoExtension(uri: vscode.Uri): string { 22 | return path.basename(uri.fsPath, path.extname(uri.fsPath)); 23 | } 24 | 25 | /** Resolves variables formatted like `${VAR_NAME}` within a string */ 26 | export class VariableResolver { 27 | // Regex patterns to identify variables 28 | private static readonly VARIABLE_REGEXP = /\${(.*?)}/g; 29 | // private static readonly VARIABLE_REGEXP_SINGLE = /\$\{(.*?)\}/; // Unused 30 | private static readonly VERSION_FORMAT = /\${#}/g; 31 | 32 | private readonly _variables = [ 33 | 'workspaceFolder', 34 | 'workspaceFolderBasename', 35 | 'file', 36 | 'relativeFile', 37 | 'relativeFileDirname', 38 | 'fileBasename', 39 | 'fileBasenameNoExtension', 40 | 'fileDirname', 41 | 'fileExtname', 42 | 'exportExtension', 43 | '#', 44 | 'noMatch', 45 | ] as const; 46 | 47 | private readonly _isWindows: boolean; 48 | // private _config: ScadConfig; 49 | 50 | constructor(private loggingService: LoggingService) { 51 | // this._config = config 52 | this._isWindows = platform() === 'win32'; 53 | } 54 | 55 | /** Resolve variables in string given a file URI */ 56 | public async resolveString( 57 | pattern: string, 58 | resource: vscode.Uri, 59 | exportExtension?: string 60 | ): Promise { 61 | // this.loggingService.logDebug(`resolveString pattern: ${pattern}`); // DEBUG 62 | 63 | // Replace all variable pattern matches '${VAR_NAME}' 64 | const replaced = pattern.replace( 65 | VariableResolver.VARIABLE_REGEXP, 66 | (match: string, variable: string) => { 67 | return this.evaluateSingleVariable( 68 | match, 69 | variable, 70 | resource, 71 | exportExtension 72 | ); 73 | } 74 | ); 75 | 76 | // Get dynamic version number 77 | const version = await this.getVersionNumber(replaced, resource); 78 | 79 | this.loggingService.logDebug(`Version number: ${version}`); 80 | 81 | // Cases for version number 82 | switch (version) { 83 | // No version number 84 | case -1: 85 | return replaced; 86 | // Error while parsing files in export directory 87 | case -2: 88 | vscode.window.showErrorMessage( 89 | `Could not read files in directory specified for export` 90 | ); 91 | return replaced; 92 | // Create an empty directory; version 1 by default 93 | case -3: 94 | return replaced.replace( 95 | VariableResolver.VERSION_FORMAT, 96 | String(1) 97 | ); 98 | default: 99 | // Substitute version number 100 | return replaced.replace( 101 | VariableResolver.VERSION_FORMAT, 102 | String(version) 103 | ); 104 | } 105 | } 106 | 107 | /** Tests all variables */ 108 | public testVars(resource: vscode.Uri): void { 109 | this.loggingService.logDebug('Testing evaluateSingleVariable()...'); 110 | 111 | for (const variable of this._variables) { 112 | this.loggingService.logDebug( 113 | `${variable} : ${this.evaluateSingleVariable( 114 | '${' + variable + '}', 115 | variable, 116 | resource, 117 | 'test' 118 | )}` 119 | ); 120 | } 121 | } 122 | 123 | /** Evaluate a single variable in format '${VAR_NAME}' 124 | * 125 | * See also: https://code.visualstudio.com/docs/editor/variables-reference 126 | */ 127 | private evaluateSingleVariable( 128 | match: string, 129 | variable: string, 130 | resource: vscode.Uri, 131 | exportExtension = 'scad' 132 | ): string { 133 | const workspaceFolder = 134 | vscode.workspace.getWorkspaceFolder(resource)?.uri.fsPath; 135 | 136 | // Note the ':' after 'date' 137 | if (variable.startsWith('date:')) { 138 | return this.evaluateDateTime(variable); 139 | } 140 | 141 | switch (variable) { 142 | case 'date': 143 | return luxon.DateTime.now().toISODate(); 144 | case 'workspaceFolder': 145 | return workspaceFolder ?? match; 146 | case 'workspaceFolderBasename': 147 | return path.basename(workspaceFolder ?? '') ?? match; 148 | case 'file': 149 | return resource.fsPath; 150 | case 'relativeFile': 151 | return path.relative(workspaceFolder ?? '', resource.fsPath); 152 | case 'relativeFileDirname': 153 | return path.basename(path.dirname(resource.fsPath)); 154 | case 'fileBasename': 155 | return path.basename(resource.fsPath); 156 | case 'fileBasenameNoExtension': 157 | return fileBasenameNoExtension(resource); 158 | case 'fileDirname': 159 | return path.dirname(resource.fsPath); 160 | case 'fileExtname': 161 | return path.extname(resource.fsPath); 162 | case 'exportExtension': 163 | return exportExtension ?? match; 164 | // We will evaluate the number later 165 | case '#': 166 | return match; 167 | default: 168 | this.loggingService.logWarning(`Unknown variable: ${match}`); 169 | return match; 170 | } 171 | } 172 | 173 | /** Return the current date formatted according to the Luxon format 174 | * specified in the 'date:FORMAT' input string. 175 | * 176 | * Note: The 'date:' prefix is removed before formatting, and any '/' or ':' 177 | * in the evaluated date string is replaced with '_'. 178 | * 179 | * See: https://moment.github.io/luxon/#/formatting?id=table-of-tokens 180 | */ 181 | private evaluateDateTime(variable: string): string { 182 | const dateTemplate = variable.split(':')[1]; 183 | const dateString = luxon.DateTime.now().toFormat(dateTemplate); 184 | // Replace invalid characters with '_' (e.g. '/' or ':') 185 | return dateString.replace(/[/:]/, '_'); 186 | } 187 | 188 | /** Evaluate version number in format '${#}' */ 189 | private async getVersionNumber( 190 | pattern: string, 191 | resource: vscode.Uri 192 | ): Promise { 193 | // No version number in string: return -1 194 | if (!VariableResolver.VERSION_FORMAT.test(pattern)) { 195 | return -1; 196 | } 197 | 198 | // Replace the number placeholder with a regex number capture pattern 199 | // Regexp is case insensitive if OS is Windows 200 | const patternAsRegexp = new RegExp( 201 | escapeStringRegexp(path.basename(pattern)).replace( 202 | '\\$\\{#\\}', 203 | '([1-9][0-9]*)' 204 | ), 205 | this._isWindows ? 'i' : '' 206 | ); 207 | 208 | // Get file directory. If the path is not absolute, get the path of 209 | // `resource`. Note that `pattern` may contain a directory 210 | const fileDirectory = path.isAbsolute(pattern) 211 | ? path.dirname(pattern) 212 | : path.dirname(path.join(path.dirname(resource.fsPath), pattern)); 213 | 214 | // Make export directory if it doesn't exist 215 | try { 216 | await fs.promises.access(fileDirectory, fs.constants.W_OK); 217 | } catch { 218 | await fs.promises.mkdir(fileDirectory); 219 | return -3; 220 | } 221 | 222 | // Read all files in directory 223 | const versionNumber: number = await new Promise((resolve, reject) => { 224 | fs.readdir(fileDirectory, (error, files) => { 225 | // Error; Return -2 (dir read error) 226 | if (error) { 227 | this.loggingService.logError( 228 | 'Cannot read directory: ', 229 | error 230 | ); 231 | reject(-2); // File read error 232 | } 233 | 234 | // Get all the files that match the pattern (with different 235 | // version numbers) 236 | const fileVersions = files.map((fileName) => { 237 | return Number(patternAsRegexp.exec(fileName)?.[1] ?? 0); 238 | }); 239 | 240 | if (fileVersions.length === 0) { 241 | resolve(-3); 242 | } 243 | 244 | resolve(Math.max(...fileVersions)); 245 | }); 246 | }); 247 | 248 | // this.loggingService.logDebug(`Version num: ${versionNum}`); // DEBUG 249 | 250 | // Return next version 251 | return versionNumber < 0 ? versionNumber : versionNumber + 1; 252 | 253 | // Consider adding case for MAX_SAFE_NUMBER (despite it's unlikeliness) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/extension.node.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Extension 3 | * 4 | * Main file for activating extension 5 | *----------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from 'vscode'; 8 | 9 | import { Cheatsheet } from 'src/cheatsheet/cheatsheet-panel'; 10 | import { PreviewManager } from 'src/preview/preview-manager'; 11 | import { LoggingService } from './logging-service'; 12 | 13 | const extensionName = process.env.EXTENSION_NAME || 'antyos.openscad'; 14 | const extensionVersion = process.env.EXTENSION_VERSION || '0.0.0'; 15 | 16 | /** Called when extension is activated */ 17 | export function activate(context: vscode.ExtensionContext): void { 18 | const loggingService = new LoggingService(); 19 | 20 | loggingService.logInfo(`Activating ${extensionName} v${extensionVersion}`); 21 | 22 | /** New launch object */ 23 | const previewManager = new PreviewManager(loggingService, context); 24 | 25 | // Register commands 26 | const commands = [ 27 | vscode.commands.registerCommand(Cheatsheet.csCommandId, () => 28 | Cheatsheet.createOrShowPanel(context.extensionUri) 29 | ), 30 | vscode.commands.registerCommand( 31 | 'openscad.preview', 32 | (mainUri, allUris) => previewManager.openFile(mainUri, allUris) 33 | ), 34 | vscode.commands.registerCommand( 35 | 'openscad.exportByType', 36 | (mainUri, allUris) => previewManager.exportFile(mainUri, allUris) 37 | ), 38 | vscode.commands.registerCommand( 39 | 'openscad.exportByConfig', 40 | (mainUri, allUris) => 41 | previewManager.exportFile(mainUri, allUris, 'auto') 42 | ), 43 | vscode.commands.registerCommand( 44 | 'openscad.exportWithSaveDialogue', 45 | (mainUri, allUris) => 46 | previewManager.exportFile(mainUri, allUris, 'auto', true) 47 | ), 48 | vscode.commands.registerCommand('openscad.kill', () => 49 | previewManager.kill() 50 | ), 51 | vscode.commands.registerCommand('openscad.autoKill', () => 52 | previewManager.kill(true) 53 | ), 54 | vscode.commands.registerCommand('openscad.killAll', () => 55 | previewManager.killAll() 56 | ), 57 | vscode.commands.registerCommand('openscad.showOutput', () => { 58 | loggingService.show(); 59 | }), 60 | ]; 61 | 62 | // Register commands, event listeners, and status bar item 63 | context.subscriptions.push( 64 | ...commands, 65 | Cheatsheet.getStatusBarItem(), 66 | vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), 67 | vscode.workspace.onDidChangeConfiguration(onDidChangeConfiguration) 68 | ); 69 | // onDidChangeConfiguration(); 70 | 71 | // Update status bar item once at start 72 | Cheatsheet.updateStatusBar(); 73 | 74 | // Register serializer event action to recreate webview panel if vscode restarts 75 | if (vscode.window.registerWebviewPanelSerializer) { 76 | // Make sure we register a serializer in action event 77 | vscode.window.registerWebviewPanelSerializer(Cheatsheet.viewType, { 78 | async deserializeWebviewPanel( 79 | webviewPanel: vscode.WebviewPanel, 80 | state: unknown 81 | ) { 82 | loggingService.logInfo( 83 | `Got webview state: ${state}. Reviving Cheatsheet` 84 | ); 85 | Cheatsheet.revive(webviewPanel, context.extensionUri); 86 | }, 87 | }); 88 | } 89 | 90 | /** Run on active change text editor */ 91 | function onDidChangeActiveTextEditor() { 92 | Cheatsheet.onDidChangeActiveTextEditor(); 93 | } 94 | 95 | /** Run when configuration is changed */ 96 | function onDidChangeConfiguration() { 97 | const config = vscode.workspace.getConfiguration('openscad'); // Get new config 98 | Cheatsheet.onDidChangeConfiguration(config); // Update the cheatsheet with new config 99 | previewManager.onDidChangeConfiguration(config); // Update launcher with new config 100 | loggingService.logDebug('Config change!'); 101 | loggingService.setOutputLevel(config.get('logLevel') ?? 'NONE'); 102 | } 103 | } 104 | 105 | /** Called when extension is deactivated */ 106 | // export function deactivate() {} 107 | -------------------------------------------------------------------------------- /src/extension.web.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Web Extension 3 | * 4 | * Main file for activating extension on the web. The web extension only has 5 | * the Cheatsheet webview panel. 6 | *----------------------------------------------------------------------------*/ 7 | 8 | import * as vscode from 'vscode'; 9 | 10 | import { Cheatsheet } from 'src/cheatsheet/cheatsheet-panel'; 11 | import { LoggingService } from './logging-service'; 12 | 13 | const extensionName = process.env.EXTENSION_NAME || 'antyos.openscad'; 14 | const extensionVersion = process.env.EXTENSION_VERSION || '0.0.0'; 15 | 16 | /** 17 | * Register a command that is not supported in VS Code web. 18 | * The command will display an error message. 19 | * 20 | * Commands may be invalid for many reasons, but primarily due to the lack of 21 | * executable support for web extensions. 22 | */ 23 | function unsupportedWebCommand(commandId: string): vscode.Disposable { 24 | return vscode.commands.registerCommand(commandId, () => 25 | vscode.window.showErrorMessage( 26 | `Command '${commandId}' is currently not supported in VS Code Web.` 27 | ) 28 | ); 29 | } 30 | 31 | /** Called when extension is activated */ 32 | export function activate(context: vscode.ExtensionContext): void { 33 | const loggingService = new LoggingService(); 34 | 35 | loggingService.logInfo(`Activating ${extensionName} v${extensionVersion}`); 36 | context.subscriptions.push( 37 | vscode.workspace.onDidChangeConfiguration((event) => { 38 | if (event.affectsConfiguration('openscad.logLevel')) { 39 | loggingService.setOutputLevel( 40 | vscode.workspace 41 | .getConfiguration('openscad') 42 | .get('logLevel') ?? 'NONE' 43 | ); 44 | } 45 | }) 46 | ); 47 | 48 | // Register commands 49 | const commands = [ 50 | vscode.commands.registerCommand(Cheatsheet.csCommandId, () => 51 | Cheatsheet.createOrShowPanel(context.extensionUri) 52 | ), 53 | vscode.commands.registerCommand('openscad.showOutput', () => { 54 | loggingService.show(); 55 | }), 56 | unsupportedWebCommand('openscad.preview'), 57 | unsupportedWebCommand('openscad.exportByType'), 58 | unsupportedWebCommand('openscad.exportByConfig'), 59 | unsupportedWebCommand('openscad.exportWithSaveDialogue'), 60 | unsupportedWebCommand('openscad.kill'), 61 | unsupportedWebCommand('openscad.autoKill'), 62 | unsupportedWebCommand('openscad.killAll'), 63 | ]; 64 | 65 | // Register commands, event listeners, and status bar item 66 | context.subscriptions.push( 67 | ...commands, 68 | Cheatsheet.getStatusBarItem(), 69 | vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), 70 | vscode.workspace.onDidChangeConfiguration(onDidChangeConfiguration) 71 | ); 72 | onDidChangeConfiguration(); 73 | 74 | // Update status bar item once at start 75 | Cheatsheet.updateStatusBar(); 76 | 77 | // Register serializer event action to recreate webview panel if vscode restarts 78 | if (vscode.window.registerWebviewPanelSerializer) { 79 | // Make sure we register a serializer in action event 80 | vscode.window.registerWebviewPanelSerializer(Cheatsheet.viewType, { 81 | async deserializeWebviewPanel( 82 | webviewPanel: vscode.WebviewPanel, 83 | state: unknown 84 | ) { 85 | loggingService.logInfo(`Got webview state: ${state}`); 86 | Cheatsheet.revive(webviewPanel, context.extensionUri); 87 | }, 88 | }); 89 | } 90 | } 91 | 92 | /** Called when extension is deactivated */ 93 | // export function deactivate() {} 94 | 95 | /** Run on active change text editor */ 96 | function onDidChangeActiveTextEditor() { 97 | Cheatsheet.onDidChangeActiveTextEditor(); 98 | } 99 | 100 | /** Run when configuration is changed */ 101 | function onDidChangeConfiguration() { 102 | const config = vscode.workspace.getConfiguration('openscad'); // Get new config 103 | Cheatsheet.onDidChangeConfiguration(config); // Update the cheatsheet with new config 104 | // vscode.window.showInformationMessage("Config change!"); // DEBUG 105 | } 106 | -------------------------------------------------------------------------------- /src/logging-service.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | 3 | const LOG_LEVEL = { 4 | DEBUG: 'DEBUG', 5 | INFO: 'INFO', 6 | WARN: 'WARN', 7 | ERROR: 'ERROR', 8 | NONE: 'NONE', 9 | } as const; 10 | 11 | type ObjectValues = T[keyof T]; 12 | 13 | export type LogLevel = ObjectValues; 14 | 15 | export class LoggingService { 16 | private outputChannel = window.createOutputChannel('OpenSCAD'); 17 | private logLevel: LogLevel = LOG_LEVEL.INFO; 18 | 19 | public setOutputLevel(logLevel: LogLevel) { 20 | this.logLevel = logLevel; 21 | } 22 | 23 | /** 24 | * Append messages to the output channel and format it with a title 25 | * 26 | * @param message The message to append to the output channel 27 | */ 28 | public logDebug(message: string, data?: unknown): void { 29 | if ( 30 | this.logLevel === LOG_LEVEL.NONE || 31 | this.logLevel === LOG_LEVEL.INFO || 32 | this.logLevel === LOG_LEVEL.WARN || 33 | this.logLevel === LOG_LEVEL.ERROR 34 | ) { 35 | return; 36 | } 37 | this.logMessage(message, LOG_LEVEL.DEBUG); 38 | if (data) { 39 | this.logObject(data); 40 | } 41 | } 42 | 43 | /** 44 | * Append messages to the output channel and format it with a title 45 | * 46 | * @param message The message to append to the output channel 47 | */ 48 | public logInfo(message: string, data?: unknown): void { 49 | if ( 50 | this.logLevel === LOG_LEVEL.NONE || 51 | this.logLevel === LOG_LEVEL.WARN || 52 | this.logLevel === LOG_LEVEL.ERROR 53 | ) { 54 | return; 55 | } 56 | this.logMessage(message, LOG_LEVEL.INFO); 57 | if (data) { 58 | this.logObject(data); 59 | } 60 | } 61 | 62 | /** 63 | * Append messages to the output channel and format it with a title 64 | * 65 | * @param message The message to append to the output channel 66 | */ 67 | public logWarning(message: string, data?: unknown): void { 68 | if ( 69 | this.logLevel === LOG_LEVEL.NONE || 70 | this.logLevel === LOG_LEVEL.ERROR 71 | ) { 72 | return; 73 | } 74 | this.logMessage(message, LOG_LEVEL.WARN); 75 | if (data) { 76 | this.logObject(data); 77 | } 78 | } 79 | 80 | public logError(message: string, error?: unknown) { 81 | if (this.logLevel === LOG_LEVEL.NONE) { 82 | return; 83 | } 84 | this.logMessage(message, LOG_LEVEL.ERROR); 85 | if (typeof error === 'string') { 86 | // Errors as a string usually only happen with 87 | // plugins that don't return the expected error. 88 | this.outputChannel.appendLine(error); 89 | } else if (error instanceof Error) { 90 | if (error?.message) { 91 | this.logMessage(error.message, LOG_LEVEL.ERROR); 92 | } 93 | if (error?.stack) { 94 | this.outputChannel.appendLine(error.stack); 95 | } 96 | } else if (error) { 97 | this.logObject(error); 98 | } 99 | } 100 | 101 | public show() { 102 | this.outputChannel.show(); 103 | } 104 | 105 | private logObject(data: unknown): void { 106 | // const message = JSON.parser 107 | // .format(JSON.stringify(data, null, 2), { 108 | // parser: "json", 109 | // }) 110 | // .trim(); 111 | const message = JSON.stringify(data, undefined, 4); 112 | 113 | this.outputChannel.appendLine(message); 114 | } 115 | 116 | /** 117 | * Append messages to the output channel and format it with a title 118 | * 119 | * @param message The message to append to the output channel 120 | */ 121 | private logMessage(message: string, logLevel: LogLevel): void { 122 | const title = new Date().toLocaleTimeString(); 123 | this.outputChannel.appendLine(`["${logLevel}" - ${title}] ${message}`); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/preview/openscad-exe.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * openscad-exe 3 | * 4 | * Manages access to the Openscad executable file 5 | *----------------------------------------------------------------------------*/ 6 | 7 | import * as child from 'child_process'; // node:child_process 8 | import { type } from 'os'; // node:os 9 | import { promisify } from 'util'; 10 | 11 | import commandExists = require('command-exists'); 12 | import { realpath } from 'fs/promises'; 13 | import { ExtensionContext } from 'vscode'; 14 | 15 | import { LoggingService } from 'src/logging-service'; 16 | 17 | const execFile = promisify(child.execFile); 18 | 19 | const pathByPlatform = { 20 | Linux: 'openscad', 21 | Darwin: '/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD', 22 | Windows_NT: 'C:\\Program Files\\Openscad\\openscad.exe', 23 | } as const; 24 | 25 | export interface OpenscadExecutable { 26 | version: string; 27 | filePath: string; 28 | arguments_: string[]; 29 | } 30 | 31 | /** Open an instance of OpenSCAD to preview a file */ 32 | export class OpenscadExecutableManager { 33 | // Paths 34 | private openscadExecutable?: OpenscadExecutable; 35 | private openscadPath?: string; 36 | private arguments_: string[] = []; 37 | 38 | public constructor( 39 | private readonly loggingService: LoggingService, 40 | private readonly context: ExtensionContext 41 | ) {} 42 | 43 | private async getOpenscadVersion( 44 | openscadPath: string, 45 | arguments_: string[] = [] 46 | ): Promise { 47 | try { 48 | const { stdout, stderr } = await execFile( 49 | openscadPath, 50 | [...arguments_, '--version'], 51 | { cwd: this.context.extensionPath.toString() } 52 | ); 53 | 54 | // For some reason, OpenSCAD seems to use stderr for all console output... 55 | // If there is no error, assume stderr should be treated as stdout 56 | // For more info. see: https://github.com/openscad/openscad/issues/3358 57 | const output = stdout || stderr; 58 | 59 | return output.trim().match(/version (\S+)/)?.[1]; 60 | } catch (error) { 61 | this.loggingService.logError( 62 | `Error getting OpenSCAD version: ${error}` 63 | ); 64 | return undefined; 65 | } 66 | } 67 | 68 | /** Set the path to `openscad.exe` on the system. 69 | * 70 | * Note: Must be called before opening children. 71 | */ 72 | public async updateScadPath( 73 | newOpenscadPath?: string, 74 | newArguments: string[] = [], 75 | skipLaunchPathValidation = false 76 | ): Promise { 77 | if ( 78 | newOpenscadPath === this.openscadPath && 79 | newArguments === this.arguments_ 80 | ) { 81 | return; 82 | } 83 | 84 | this.openscadPath = newOpenscadPath; 85 | this.arguments_ = newArguments; 86 | this.openscadExecutable = undefined; 87 | 88 | // Use platform default if not specified 89 | let openscadPath = this.getPath(); 90 | 91 | this.loggingService.logInfo( 92 | `Checking OpenSCAD path: '${openscadPath}'` 93 | ); 94 | 95 | // Resolve potential symlinks 96 | try { 97 | const newPath = await realpath(openscadPath); 98 | if (newPath !== openscadPath) { 99 | this.loggingService.logInfo( 100 | `Configured path is a link. Using resolved path: '${openscadPath}'` 101 | ); 102 | openscadPath = newPath; 103 | } 104 | } catch (error) { 105 | // An ENOENT error (Error No Entity) is expected if openscadPath is 106 | // invalid and is ok. Otherwise, throw the error 107 | if ( 108 | !(error instanceof Error) || 109 | !('code' in error) || 110 | error.code !== 'ENOENT' 111 | ) { 112 | throw error; 113 | } 114 | } 115 | 116 | // TODO: Replace with something less nested 117 | commandExists(openscadPath, async (error: null, exists: boolean) => { 118 | if (!exists) { 119 | this.loggingService.logWarning( 120 | `'${openscadPath}' is not a valid path or command.` 121 | ); 122 | if (!skipLaunchPathValidation) { 123 | return; 124 | } 125 | this.loggingService.logInfo('Skipping OpenSCAD path check.'); 126 | } 127 | let version = await this.getOpenscadVersion( 128 | openscadPath, 129 | this.arguments_ 130 | ); 131 | // Should we throw an error here? 132 | if (!version) { 133 | this.loggingService.logWarning( 134 | `Unable to determine OpenSCAD version with 'openscad --version'.` 135 | ); 136 | if (!skipLaunchPathValidation) { 137 | return; 138 | } 139 | this.loggingService.logInfo('Skipping OpenSCAD version check.'); 140 | version = 'unknown'; 141 | } 142 | this.openscadExecutable = { 143 | version: version, 144 | filePath: openscadPath, 145 | arguments_: this.arguments_, 146 | }; 147 | this.loggingService.logInfo( 148 | 'Using OpenSCAD:', 149 | this.openscadExecutable 150 | ); 151 | }); 152 | } 153 | 154 | /** A valid openscad executable or undefined */ 155 | public get executable() { 156 | return this.openscadExecutable; 157 | } 158 | 159 | /** The current path the manager is looking for openscad at. Not guaranteed 160 | * to be valid. */ 161 | public getPath() { 162 | return ( 163 | this.openscadPath || 164 | pathByPlatform[type() as keyof typeof pathByPlatform] 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/preview/preview-manager.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Preview Manager 3 | * 4 | * Class for adding / removing OpenSCAD previews to a previewStore 5 | *----------------------------------------------------------------------------*/ 6 | 7 | import * as fs from 'fs'; // node:fs 8 | import * as path from 'path'; // node:path 9 | import * as vscode from 'vscode'; 10 | 11 | import { DEFAULT_CONFIG, ScadConfig } from 'src/config'; 12 | import { 13 | ExportExtensionsForSave, 14 | ExportFileExtension, 15 | ExportFileExtensionList, 16 | } from 'src/export/export-file-extensions'; 17 | import { VariableResolver } from 'src/export/variable-resolver'; 18 | import { LoggingService } from 'src/logging-service'; 19 | import { 20 | OpenscadExecutable, 21 | OpenscadExecutableManager, 22 | } from 'src/preview/openscad-exe'; 23 | import { Preview } from 'src/preview/preview'; 24 | import { PreviewStore } from 'src/preview/preview-store'; 25 | 26 | /** PreviewItems used for `scad.kill` quick pick menu */ 27 | class PreviewItem implements vscode.QuickPickItem { 28 | label: string; // File name 29 | description: string; // File path 30 | uri: vscode.Uri; // Raw file uri 31 | 32 | constructor(public preview: Preview) { 33 | const fileName = path.basename(preview.uri.fsPath); 34 | this.label = (preview.hasGui ? '' : 'Exporting: ') + fileName; // Remove path before filename 35 | this.description = preview.uri.path.slice(1); // Remove first '/' 36 | this.uri = preview.uri; 37 | } 38 | } 39 | 40 | class MessageItem implements vscode.QuickPickItem { 41 | label: string; 42 | 43 | constructor(public message: string) { 44 | this.label = message; 45 | } 46 | } 47 | 48 | const mKillAll = new MessageItem('Kill All'); 49 | const mNoPreviews = new MessageItem('No open previews'); 50 | 51 | /** Manager of multiple Preview objects */ 52 | export class PreviewManager { 53 | private previewStore: PreviewStore; 54 | private config: ScadConfig = {}; 55 | private variableResolver: VariableResolver; 56 | private openscadExecutableManager: OpenscadExecutableManager; 57 | 58 | // public activate() {} 59 | 60 | /** Opens file in OpenSCAD */ 61 | public async openFile( 62 | mainUri?: vscode.Uri, 63 | allUris?: vscode.Uri[], 64 | arguments_?: string[] 65 | ): Promise { 66 | for (const uri of Array.isArray(allUris) ? allUris : [mainUri]) { 67 | let resource: vscode.Uri; 68 | 69 | this.loggingService.logDebug( 70 | `openFile: { main: ${mainUri}, all: ${allUris}, args: ${arguments_}}` 71 | ); // DEBUG 72 | 73 | // If uri not given, try opening activeTextEditor 74 | if (!(uri instanceof vscode.Uri)) { 75 | const newUri = await this.getActiveEditorUri(); 76 | if (newUri) { 77 | resource = newUri; 78 | } else { 79 | return; 80 | } 81 | } else { 82 | resource = uri; 83 | } 84 | 85 | // Check if a new preview can be opened 86 | if ( 87 | !this.canOpenNewPreview( 88 | this.openscadExecutableManager.executable, 89 | resource, 90 | arguments_ 91 | ) 92 | ) { 93 | return; 94 | } 95 | 96 | this.loggingService.logDebug(`uri: ${resource}`); // DEBUG 97 | 98 | // Create and add new OpenSCAD preview to PreviewStore 99 | this.previewStore.createAndAdd( 100 | this.openscadExecutableManager.executable, 101 | resource, 102 | arguments_ 103 | ); 104 | } 105 | } 106 | 107 | private async getExportExtension( 108 | fileExtension?: ExportFileExtension | 'auto' 109 | ): Promise { 110 | // If file extension is not provided, prompt user 111 | const promptForFileExtension = 112 | !fileExtension || 113 | (fileExtension === 'auto' && 114 | this.config.preferredExportFileExtension === 'none'); 115 | if (promptForFileExtension) { 116 | const pick = await vscode.window.showQuickPick( 117 | ExportFileExtensionList, 118 | { placeHolder: 'Select file extension for export' } 119 | ); 120 | return pick; 121 | } 122 | // Get file extension from config 123 | else if (fileExtension === 'auto') { 124 | return ( 125 | this.config.preferredExportFileExtension 126 | ); 127 | } 128 | return fileExtension; 129 | } 130 | 131 | /** Export file */ 132 | public async exportFile( 133 | mainUri?: vscode.Uri, 134 | allUris?: vscode.Uri[], 135 | fileExtension?: ExportFileExtension | 'auto', 136 | useSaveDialogue = false 137 | ): Promise { 138 | const exportExtension = await this.getExportExtension(fileExtension); 139 | if (!exportExtension) { 140 | return; 141 | } 142 | // Iterate through uris. As a vscode action, may be given multiple uris 143 | // or just one 144 | for (const uri of Array.isArray(allUris) ? allUris : [mainUri]) { 145 | let resource: vscode.Uri; 146 | // If uri not given, try opening activeTextEditor 147 | if (!(uri instanceof vscode.Uri)) { 148 | const newUri = await this.getActiveEditorUri(); 149 | if (!newUri) { 150 | continue; 151 | } 152 | resource = newUri; 153 | } else { 154 | resource = uri; 155 | } 156 | await this.exportSingleFile( 157 | resource, 158 | exportExtension, 159 | useSaveDialogue 160 | ); 161 | } 162 | } 163 | 164 | private async exportSingleFile( 165 | resource: vscode.Uri, 166 | exportExtension: ExportFileExtension, 167 | useSaveDialogue: boolean 168 | ): Promise { 169 | let filePath: string; 170 | const arguments_: string[] = []; 171 | const exportNameFormat = 172 | (await this.getFileExportNameFormat(resource)) || 173 | this.config.exportNameFormat || 174 | DEFAULT_CONFIG.exportNameFormat; 175 | // Open save dialogue 176 | if (useSaveDialogue || !this.config.skipSaveDialog) { 177 | // Get Uri from save dialogue prompt 178 | const newUri = await this.promptForExport( 179 | this.config.saveDialogExportNameFormat || exportNameFormat, 180 | resource, 181 | exportExtension 182 | ); 183 | // If valid, set filePath. Otherwise, return 184 | if (!newUri) { 185 | return; 186 | } 187 | filePath = newUri.fsPath; 188 | } 189 | // Use config for auto generation of filename 190 | else { 191 | // Filename for export 192 | const fileName = await this.variableResolver.resolveString( 193 | exportNameFormat, 194 | resource, 195 | exportExtension 196 | ); 197 | // Set full file path; Make sure fileName is not already an absolute path 198 | filePath = path.isAbsolute(fileName) 199 | ? fileName 200 | : path.join(path.dirname(resource.fsPath), fileName); 201 | } 202 | 203 | // this.variableResolver.testVars(resource); // TESTING / DEBUG 204 | 205 | // Set arguments 206 | arguments_.push('-o', filePath); // Filename for export 207 | 208 | // Check if a new preview can be opened 209 | if ( 210 | !this.canOpenNewPreview( 211 | this.openscadExecutableManager.executable, 212 | resource, 213 | arguments_ 214 | ) 215 | ) { 216 | return; 217 | } 218 | 219 | this.loggingService.logInfo(`Export uri: ${resource}`); 220 | 221 | this.previewStore.createAndAdd( 222 | this.openscadExecutableManager.executable, 223 | resource, 224 | arguments_ 225 | ); 226 | } 227 | 228 | private async getFileExportNameFormat( 229 | resource: vscode.Uri 230 | ): Promise { 231 | // Scan the file for the exportNameFormat 232 | const exportNameFormatPattern = /\/\/\s*exportNameFormat\s*=\s*(.*)/; 233 | const exportNameFormatPromise = new Promise( 234 | (resolve, reject) => { 235 | fs.readFile( 236 | resource.fsPath, 237 | 'utf-8', 238 | (error: NodeJS.ErrnoException | null, data: string) => { 239 | if (error) { 240 | reject(error); 241 | } 242 | const match = exportNameFormatPattern.exec(data); 243 | resolve(match?.[1]); 244 | } 245 | ); 246 | } 247 | ); 248 | try { 249 | const exportNameFormat = await exportNameFormatPromise; 250 | if (exportNameFormat) { 251 | this.loggingService.logInfo( 252 | `Using file exportNameFormat override: ${exportNameFormat}` 253 | ); 254 | } 255 | return exportNameFormat; 256 | } catch (error) { 257 | this.loggingService.logWarning('Error reading file: ', error); 258 | } 259 | return; 260 | } 261 | 262 | /** Prompt user for instances to kill */ 263 | public async kill(autoKill?: boolean): Promise { 264 | // If autoKill (for menu button usage), don't display the menu for 0 or 1 open previews 265 | if (autoKill) { 266 | // No active previews: Inform user 267 | if (this.previewStore.size === 0) { 268 | vscode.window.showInformationMessage('No open previews.'); 269 | return; 270 | } 271 | // 1 active preview: delete it 272 | else if (this.previewStore.size === 1) { 273 | this.previewStore.deleteAll(this.config.showKillMessage); 274 | return; 275 | } 276 | } 277 | // Create list for menu items 278 | const menuItems: (PreviewItem | MessageItem)[] = []; 279 | menuItems.push(this.previewStore.size > 0 ? mKillAll : mNoPreviews); // Push MessageItem depending on num open previews 280 | 281 | // Populate quickpick list with open previews 282 | for (const preview of this.previewStore) { 283 | menuItems.push(new PreviewItem(preview)); 284 | } 285 | 286 | // Get from user 287 | const selected = await vscode.window.showQuickPick(menuItems, { 288 | placeHolder: 'Select open preview to kill', 289 | }); 290 | if (!selected) { 291 | return; 292 | } 293 | 294 | // Check for message item 295 | if (selected instanceof MessageItem) { 296 | switch (selected) { 297 | case mKillAll: 298 | this.killAll(); 299 | break; 300 | default: 301 | break; 302 | } 303 | return; 304 | } 305 | 306 | // Get preview to delete 307 | const previewToDelete = this.previewStore.get(selected.uri); 308 | if (!previewToDelete) { 309 | return; 310 | } 311 | 312 | this.previewStore.delete(previewToDelete, this.config.showKillMessage); 313 | } 314 | 315 | /** Kill all the current previews */ 316 | public killAll(): void { 317 | // Check that there are open previews 318 | if (this.previewStore.size <= 0) { 319 | this.loggingService.logError('No open previews'); 320 | vscode.window.showInformationMessage('No open previews.'); 321 | return; 322 | } 323 | 324 | this.previewStore.deleteAll(this.config.showKillMessage); 325 | // this._previews = undefined; 326 | } 327 | 328 | /** Constructor */ 329 | 330 | public constructor( 331 | private loggingService: LoggingService, 332 | private context: vscode.ExtensionContext 333 | ) { 334 | this.previewStore = new PreviewStore(this.loggingService, this.context); 335 | this.variableResolver = new VariableResolver(this.loggingService); 336 | this.openscadExecutableManager = new OpenscadExecutableManager( 337 | this.loggingService, 338 | this.context 339 | ); 340 | // Load configutation 341 | this.onDidChangeConfiguration( 342 | vscode.workspace.getConfiguration('openscad') 343 | ); 344 | } 345 | 346 | /** Run when change configuration event */ 347 | public onDidChangeConfiguration( 348 | config: vscode.WorkspaceConfiguration 349 | ): void { 350 | // Update configuration 351 | this.config.openscadPath = config.get('launchPath'); 352 | this.config.launchArgs = config.get('launchArgs'); 353 | this.config.maxInstances = config.get('maxInstances'); 354 | this.config.showKillMessage = config.get('showKillMessage'); 355 | this.config.preferredExportFileExtension = config.get( 356 | 'export.preferredExportFileExtension' 357 | ); 358 | this.config.exportNameFormat = config.get( 359 | 'export.exportNameFormat' 360 | ); 361 | this.config.skipSaveDialog = config.get( 362 | 'export.skipSaveDialog' 363 | ); 364 | this.config.saveDialogExportNameFormat = config.get( 365 | 'export.saveDialogExportNameFormat' 366 | ); 367 | this.config.skipLaunchPathValidation = config.get( 368 | 'experimental.skipLaunchPathValidation' 369 | ); 370 | 371 | this.loggingService.logDebug('Launch args:', this.config.launchArgs); 372 | 373 | this.openscadExecutableManager.updateScadPath( 374 | this.config.openscadPath, 375 | this.config.launchArgs, 376 | this.config.skipLaunchPathValidation 377 | ); 378 | // Set the max previews 379 | this.previewStore.maxPreviews = this.config.maxInstances ?? 0; 380 | 381 | // Convert deprecated configuration to current configuration. Only use 382 | // the deprecated configs if they are present and the current config is 383 | // the default. 384 | const autoNamingFormat = config.get('export.autoNamingFormat'); 385 | if ( 386 | autoNamingFormat !== undefined && 387 | autoNamingFormat !== null && 388 | this.config.exportNameFormat === DEFAULT_CONFIG.exportNameFormat 389 | ) { 390 | this.loggingService.logWarning( 391 | '`openscad.export.autoNamingFormat` is deprecated. Use `openscad.export.exportNameFormat` instead. See: [#58](https://github.com/Antyos/vscode-openscad/pull/58) for more information.' 392 | ); 393 | vscode.window.showWarningMessage( 394 | '`openscad.export.autoNamingFormat` is deprecated. Use `openscad.export.exportNameFormat` instead. See: [#58](https://github.com/Antyos/vscode-openscad/pull/58) for more information.' 395 | ); 396 | this.config.exportNameFormat = autoNamingFormat; 397 | } 398 | 399 | const useAutoNamingExport = config.get( 400 | 'export.useAutoNamingExport' 401 | ); 402 | if ( 403 | useAutoNamingExport !== undefined && 404 | useAutoNamingExport !== null && 405 | this.config.skipSaveDialog === DEFAULT_CONFIG.skipSaveDialog 406 | ) { 407 | this.loggingService.logWarning( 408 | '`openscad.export.useAutoNamingExport` is deprecated. Use `openscad.export.skipSaveDialog` instead. See: [#58](https://github.com/Antyos/vscode-openscad/pull/58) for more information' 409 | ); 410 | vscode.window.showWarningMessage( 411 | '`openscad.export.useAutoNamingExport` is deprecated. Use `openscad.export.skipSaveDialog` instead. See: [#58](https://github.com/Antyos/vscode-openscad/pull/58) for more information' 412 | ); 413 | this.config.skipSaveDialog = useAutoNamingExport; 414 | } 415 | 416 | // To preserve original behavior, use default exportNameFormat in save 417 | // dialogs only if the user had previously specified not to use 418 | // autonamingFormatting in save dialogs 419 | const useAutoNamingInSaveDialogues = config.get( 420 | 'export.useAutoNamingInSaveDialogues' 421 | ); 422 | if ( 423 | useAutoNamingInSaveDialogues === false && 424 | this.config.saveDialogExportNameFormat === 425 | DEFAULT_CONFIG.saveDialogExportNameFormat 426 | ) { 427 | this.loggingService.logWarning( 428 | '`openscad.export.useAutoNamingInSaveDialogues` is deprecated. Use `openscad.export.saveDialogExportNameFormat` instead. See: [#58](https://github.com/Antyos/vscode-openscad/pull/58) for more information' 429 | ); 430 | vscode.window.showWarningMessage( 431 | '`openscad.export.useAutoNamingInSaveDialogues` is deprecated. Use `openscad.export.saveDialogExportNameFormat` instead. See: [#58](https://github.com/Antyos/vscode-openscad/pull/58) for more information' 432 | ); 433 | this.config.saveDialogExportNameFormat = 434 | DEFAULT_CONFIG.exportNameFormat; 435 | } 436 | } 437 | 438 | /** Gets the uri of the active editor */ 439 | private async getActiveEditorUri(): Promise { 440 | const editor = vscode.window.activeTextEditor; 441 | if (!editor) { 442 | return undefined; 443 | } 444 | 445 | // If document is already saved, set `resource` 446 | if (!editor.document.isUntitled) { 447 | return editor.document.uri; 448 | } 449 | // Make user save their document before previewing if it is untitled 450 | // TODO: Consider implementing as virtual (or just temp) document in the future 451 | vscode.window.showInformationMessage( 452 | 'Save untitled document before previewing' 453 | ); 454 | // Prompt save window 455 | return await vscode.window.showSaveDialog({ 456 | defaultUri: editor.document.uri, 457 | filters: { 'OpenSCAD Designs': ['scad'] }, 458 | }); 459 | } 460 | 461 | /** Prompts user for export name and location */ 462 | private async promptForExport( 463 | exportNameFormat: string, 464 | resource: vscode.Uri, 465 | exportExtension: ExportFileExtension 466 | ): Promise { 467 | // Replace the `.scad` file extrension with the preferred type (or default to stl) 468 | const fileName = await this.variableResolver.resolveString( 469 | exportNameFormat, 470 | resource, 471 | exportExtension 472 | ); 473 | const filePath = path.isAbsolute(fileName) 474 | ? fileName 475 | : path.join(path.dirname(resource.fsPath), fileName); // Full file path 476 | const resourceNewExtension = vscode.Uri.file(filePath); // Resource URI with new file extension 477 | 478 | this.loggingService.logDebug(`Opening Save Dialogue to: ${filePath}`); 479 | 480 | // Open save dialogue 481 | return await vscode.window.showSaveDialog({ 482 | defaultUri: resourceNewExtension, 483 | filters: ExportExtensionsForSave, 484 | }); 485 | } 486 | 487 | /** Returns if the current URI with arguments (output Y/N) can be opened */ 488 | private canOpenNewPreview( 489 | openscadExecutable: OpenscadExecutable | undefined, 490 | resource: vscode.Uri, 491 | arguments_?: string[] 492 | ): openscadExecutable is OpenscadExecutable { 493 | // Make sure path to openscad.exe is valid 494 | if (!openscadExecutable) { 495 | // Error message for default 496 | const openscadPath = this.openscadExecutableManager.getPath(); 497 | 498 | this.loggingService.logError( 499 | `Path to openscad command is invalid: "${openscadPath}"` 500 | ); 501 | vscode.window.showErrorMessage( 502 | `Cannot find the command: "${openscadPath}". Make sure OpenSCAD is installed. You may need to specify the installation path under \`Settings > OpenSCAD > Launch Path\`` 503 | ); 504 | return false; 505 | } 506 | 507 | // Make sure we don't surpass max previews allowed 508 | if ( 509 | this.previewStore.size >= this.previewStore.maxPreviews && 510 | this.previewStore.maxPreviews > 0 511 | ) { 512 | this.loggingService.logError( 513 | 'Max number of OpenSCAD previews already open.' 514 | ); 515 | vscode.window.showErrorMessage( 516 | 'Max number of OpenSCAD previews already open. Try increasing the max instances in the config.' 517 | ); 518 | return false; 519 | } 520 | 521 | // Make sure file is not already open 522 | if (this.previewStore.get(resource, PreviewStore.hasGui(arguments_))) { 523 | this.loggingService.logInfo( 524 | `File is already open: "${resource.fsPath}"` 525 | ); 526 | vscode.window.showInformationMessage( 527 | `${path.basename(resource.fsPath)} is already open: "${ 528 | resource.fsPath 529 | }"` 530 | ); 531 | return false; 532 | } 533 | return true; 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /src/preview/preview-store.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Preview Store 3 | * 4 | * Class to manage a Set of previews 5 | *----------------------------------------------------------------------------*/ 6 | 7 | import { basename } from 'path'; // node:path 8 | import * as vscode from 'vscode'; 9 | 10 | import { LoggingService } from 'src/logging-service'; 11 | import { OpenscadExecutable } from './openscad-exe'; 12 | import { Preview } from './preview'; 13 | 14 | /** Container of several Preview */ 15 | export class PreviewStore /* extends vscode.Disposable */ { 16 | private static readonly areOpenScadPreviewsContextKey = 17 | 'areOpenScadPreviews'; 18 | 19 | private readonly _previews = new Set(); 20 | private _maxPreviews: number; 21 | 22 | /** Dispose of the PreviewStore */ 23 | public dispose(): void { 24 | // super.dispose(); 25 | for (const preview of this._previews) { 26 | preview.dispose(); 27 | } 28 | this._previews.clear(); 29 | } 30 | 31 | /** Defines behavior for `PreviewStore[]` */ 32 | [Symbol.iterator](): Iterator { 33 | return this._previews[Symbol.iterator](); 34 | } 35 | 36 | /** Create a new PreviewStore with a max number of previews */ 37 | public constructor( 38 | private readonly loggingService: LoggingService, 39 | private readonly context: vscode.ExtensionContext, 40 | maxPreviews = 0 41 | ) { 42 | this._maxPreviews = maxPreviews; 43 | this.setAreOpenPreviews(false); 44 | } 45 | 46 | /** 47 | * Find a resource in the PreviewStore by uri 48 | * @returns {Preview | undefined} Preview if found, otherwise undefined 49 | */ 50 | public get(resource: vscode.Uri, hasGui?: boolean): Preview | undefined { 51 | for (const preview of this._previews) { 52 | if (preview.match(resource, hasGui)) { 53 | return preview; 54 | } 55 | } 56 | return undefined; 57 | } 58 | 59 | /** Add a preview to PreviewStore */ 60 | public add(preview: Preview): void { 61 | this._previews.add(preview); 62 | preview.onKilled.push(() => this._previews.delete(preview)); // Auto delete when killed 63 | this.setAreOpenPreviews(true); 64 | } 65 | 66 | /** Create new preview (if not one with same uri) and then add it. */ 67 | public createAndAdd( 68 | openscadExecutable: OpenscadExecutable, 69 | uri: vscode.Uri, 70 | arguments_?: string[] 71 | ): Preview | undefined { 72 | const hasGui = PreviewStore.hasGui(arguments_); 73 | 74 | // Don't create a new preview if we already have one 75 | if (this.get(uri, hasGui)) { 76 | return undefined; 77 | } 78 | 79 | const preview = new Preview( 80 | this.loggingService, 81 | this.context, 82 | openscadExecutable, 83 | uri, 84 | hasGui, 85 | arguments_ 86 | ); 87 | 88 | this.add(preview); 89 | if (!preview.hasGui) { 90 | this.makeExportProgressBar(preview); 91 | } 92 | 93 | return preview; 94 | } 95 | 96 | /** Delete and dispose of a preview. */ 97 | public delete(preview: Preview, informUser?: boolean): void { 98 | preview.dispose(); 99 | if (informUser) { 100 | vscode.window.showInformationMessage( 101 | `Killed: ${basename(preview.uri.fsPath)}` 102 | ); 103 | } 104 | this._previews.delete(preview); 105 | 106 | if (this.size === 0) { 107 | this.setAreOpenPreviews(false); 108 | } 109 | } 110 | 111 | /** Functionally same as dispose() but without super.dispose(). */ 112 | public deleteAll(informUser?: boolean): void { 113 | for (const preview of this._previews) { 114 | preview.dispose(); 115 | if (informUser) { 116 | vscode.window.showInformationMessage( 117 | `Killed: ${basename(preview.uri.fsPath)}` 118 | ); 119 | } 120 | } 121 | this._previews.clear(); 122 | 123 | this.setAreOpenPreviews(false); 124 | } 125 | 126 | /** Get the list of all open URIs. */ 127 | public getUris(): vscode.Uri[] { 128 | const uris: vscode.Uri[] = []; 129 | 130 | // this.cleanup(); // Clean up any killed instances that weren't caught 131 | 132 | for (const preview of this._previews) { 133 | uris.push(preview.uri); 134 | } 135 | 136 | return uris; 137 | } 138 | 139 | /** Create progress bar for exporting. */ 140 | public makeExportProgressBar(preview: Preview): void { 141 | // Progress window 142 | vscode.window.withProgress( 143 | { 144 | location: vscode.ProgressLocation.Notification, 145 | title: `Exporting: ${basename(preview.uri.fsPath)}`, 146 | cancellable: true, 147 | }, 148 | (progress, token) => { 149 | // Create and add new OpenSCAD preview to PreviewStore 150 | 151 | // Cancel export 152 | token.onCancellationRequested(() => { 153 | this.loggingService.logInfo('Canceled Export'); 154 | this.delete(preview); 155 | }); 156 | 157 | // Return promise that resolve the progress bar when the preview is killed 158 | return new Promise((resolve) => { 159 | preview.onKilled.push(() => resolve()); 160 | }); 161 | } 162 | ); 163 | } 164 | 165 | /** True if '-o' or '--o' (output) are not in the arguments list */ 166 | public static hasGui(arguments_?: string[]): boolean { 167 | return !arguments_?.some((item) => ['-o', '--o'].includes(item)); 168 | } 169 | 170 | /** Returns size (length) of PreviewStore. */ 171 | public get size(): number { 172 | return this._previews.size; 173 | } 174 | 175 | public get maxPreviews(): number { 176 | return this._maxPreviews; 177 | } 178 | public set maxPreviews(number_: number) { 179 | this._maxPreviews = number_; 180 | } 181 | 182 | /** Set vscode context 'areOpenPreviews'. Used in 'when' clauses. */ 183 | private setAreOpenPreviews(value: boolean): void { 184 | vscode.commands.executeCommand( 185 | 'setContext', 186 | PreviewStore.areOpenScadPreviewsContextKey, 187 | value 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/preview/preview.ts: -------------------------------------------------------------------------------- 1 | /**----------------------------------------------------------------------------- 2 | * Preview 3 | * 4 | * Stores a single instance of OpenSCAD 5 | *----------------------------------------------------------------------------*/ 6 | 7 | import * as child from 'child_process'; // node:child_process 8 | import * as vscode from 'vscode'; 9 | 10 | import { LoggingService } from 'src/logging-service'; 11 | import { OpenscadExecutable } from 'src/preview/openscad-exe'; 12 | 13 | /** Open an instance of OpenSCAD to preview a file */ 14 | export class Preview { 15 | private readonly _process: child.ChildProcess; 16 | private _isRunning: boolean; 17 | private _onKilledCallbacks: (() => void)[] = []; 18 | 19 | /** Launch an instance of OpenSCAD to prview a file */ 20 | constructor( 21 | private readonly loggingService: LoggingService, 22 | private readonly context: vscode.ExtensionContext, 23 | private readonly openscadExecutable: OpenscadExecutable, 24 | public readonly uri: vscode.Uri, 25 | public readonly hasGui: boolean, 26 | arguments_: string[] = [] 27 | ) { 28 | // Set local arguments 29 | this.uri = uri; 30 | 31 | // Prepend arguments to path if they exist 32 | const commandArguments: string[] = [ 33 | ...this.openscadExecutable.arguments_, 34 | ...arguments_, 35 | this.uri.fsPath, 36 | ]; 37 | 38 | this.loggingService.logDebug( 39 | `Executing with args: ${commandArguments}` 40 | ); 41 | 42 | // New process 43 | this._process = child.execFile( 44 | this.openscadExecutable.filePath, 45 | commandArguments, 46 | { cwd: this.context.extensionPath.toString() }, 47 | (error, stdout, stderr) => { 48 | // If there's an error 49 | if (error) { 50 | // this.loggingService.logError(`exec error: ${error}`); 51 | this.loggingService.logError( 52 | `OpenSCAD exited with the error code: ${error}.`, 53 | stderr 54 | ); 55 | vscode.window.showErrorMessage(stderr); // Display error message 56 | } 57 | // No error 58 | else { 59 | // For some reason, OpenSCAD seems to use stderr for all console output... 60 | // If there is no error, assume stderr should be treated as stdout 61 | // For more info. see: https://github.com/openscad/openscad/issues/3358 62 | const message = stdout || stderr; 63 | this.loggingService.logDebug( 64 | `OpenSCAD exited with the following message: ${message}` 65 | ); 66 | vscode.window.showInformationMessage(message); 67 | } 68 | 69 | // this.loggingService.logDebug(`real stdout: ${stdout}`); 70 | 71 | this._isRunning = false; 72 | // Dispatch 'onKilled' event 73 | for (const callback of this._onKilledCallbacks) { 74 | callback(); 75 | } 76 | } 77 | ); 78 | 79 | // Child process is now running 80 | this._isRunning = true; 81 | } 82 | 83 | /** Kill child process */ 84 | public dispose(): void { 85 | if (this._isRunning) { 86 | this._process.kill(); 87 | } 88 | // this._isRunning = false; 89 | } 90 | 91 | /** Returns if the given Uri is equivalent to the preview's Uri */ 92 | public match(uri: vscode.Uri, hasGui?: boolean): boolean { 93 | return ( 94 | this.uri.toString() === uri.toString() && 95 | (hasGui === undefined || this.hasGui === hasGui) 96 | ); 97 | } 98 | 99 | public get isRunning() { 100 | return this._isRunning; 101 | } 102 | 103 | /** On killed handlers */ 104 | public get onKilled() { 105 | return this._onKilledCallbacks; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/run-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-process-exit */ 2 | /* eslint-disable unicorn/prefer-module */ 3 | 4 | import { runTests } from '@vscode/test-electron'; 5 | import * as path from 'path'; 6 | 7 | async function main() { 8 | try { 9 | // The folder containing the Extension Manifest package.json 10 | // Passed to `--extensionDevelopmentPath` 11 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 12 | 13 | // The path to the extension test runner script 14 | // Passed to --extensionTestsPath 15 | const extensionTestsPath = path.resolve(__dirname, './index'); 16 | 17 | // Download VS Code, unzip it and run the integration test 18 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 19 | } catch (error) { 20 | console.error(error); 21 | console.error('Failed to run tests'); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | main(); 27 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | // You can import and use all API from the 'vscode' module 3 | // as well as import your extension to test it 4 | import * as vscode from 'vscode'; 5 | // import * as myExtension from '../../extension'; 6 | 7 | suite('Extension Test Suite', () => { 8 | vscode.window.showInformationMessage('Start all tests.'); 9 | 10 | test('Sample test', () => { 11 | assert.strictEqual([1, 2, 3].indexOf(5), -1); 12 | assert.strictEqual([1, 2, 3].indexOf(0), -1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-module */ 2 | 3 | import * as glob from 'glob'; 4 | import * as Mocha from 'mocha'; 5 | import * as path from 'path'; 6 | 7 | export function run(): Promise { 8 | // Create the mocha test 9 | const mocha = new Mocha({ 10 | ui: 'tdd', 11 | color: true, 12 | }); 13 | 14 | const testsRoot = path.resolve(__dirname, '..'); 15 | 16 | return new Promise((resolve, reject) => { 17 | glob('**/**.test.js', { cwd: testsRoot }, (error, files) => { 18 | if (error) { 19 | return reject(error); 20 | } 21 | 22 | // Add files to the test suite 23 | for (const f of files) mocha.addFile(path.resolve(testsRoot, f)); 24 | 25 | try { 26 | // Run the mocha test 27 | mocha.run((failures) => { 28 | if (failures > 0) { 29 | reject(new Error(`${failures} tests failed.`)); 30 | } else { 31 | resolve(); 32 | } 33 | }); 34 | } catch (error) { 35 | console.error(error); 36 | reject(error); 37 | } 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /syntaxes/scad.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [ 3 | "scad" 4 | ], 5 | "foldingStartMarker": "/\\*\\*|\\{\\s*$", 6 | "foldingStopMarker": "\\*\\*/|^\\s*\\}", 7 | "keyEquivalent": "^~S", 8 | "name": "OpenSCAD", 9 | "patterns": [ 10 | { 11 | "include": "#comments" 12 | }, 13 | { 14 | "name": "invalid.string.quoted.single.scad", 15 | "begin": "'", 16 | "beginCaptures": { 17 | "0": { 18 | "name": "punctuation.definition.string.begin.scad" 19 | } 20 | }, 21 | "end": "'", 22 | "endCaptures": { 23 | "0": { 24 | "name": "punctuation.definition.string.end.scad" 25 | } 26 | }, 27 | "patterns": [ 28 | { 29 | "name": "constant.character.escape.scad", 30 | "match": "\\\\(x[0-7]?[0-9A-Fa-f]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{6}|.)" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "string.quoted.double.scad", 36 | "begin": "\"", 37 | "beginCaptures": { 38 | "0": { 39 | "name": "punctuation.definition.string.begin.scad" 40 | } 41 | }, 42 | "end": "\"", 43 | "endCaptures": { 44 | "0": { 45 | "name": "punctuation.definition.string.end.scad" 46 | } 47 | }, 48 | "patterns": [ 49 | { 50 | "name": "constant.character.escape.scad", 51 | "match": "\\\\(x[0-7]?[0-9A-Fa-f]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{6}|.)" 52 | } 53 | ] 54 | }, 55 | { 56 | "include": "#include" 57 | }, 58 | { 59 | "name": "invalid.deprecated", 60 | "match": "\\b(assign|child|import_dxf|import_stl)\\b" 61 | }, 62 | { 63 | "name": "punctuation.terminator.statement.scad", 64 | "match": "\\;" 65 | }, 66 | { 67 | "name": "meta.delimiter.object.comma.scad", 68 | "match": ",[ |\\t]*" 69 | }, 70 | { 71 | "name": "meta.dot.scad", 72 | "match": "\\.(?![0-9])" 73 | }, 74 | { 75 | "include": "#brace_block" 76 | }, 77 | { 78 | "include": "#bracket_block" 79 | }, 80 | { 81 | "include": "#paren_block" 82 | }, 83 | { 84 | "name": "keyword.operator.assignment.scad", 85 | "match": "=(?!=)" 86 | }, 87 | { 88 | "name": "keyword.operator.arithmetic.scad", 89 | "match": "\\+|\\-|\\*|\\/|%" 90 | }, 91 | { 92 | "name": "keyword.operator.logical.scad", 93 | "match": "!|&&|\\|\\|" 94 | }, 95 | { 96 | "name": "keyword.operator.relational.scad", 97 | "match": "<=|<|==|!=|>=|>" 98 | }, 99 | { 100 | "name": "keyword.operator.conditional.scad", 101 | "match": "\\?|\\:" 102 | }, 103 | { 104 | "name": "keyword.operator.other.scad", 105 | "match": "#|%|!" 106 | }, 107 | { 108 | "name": "variable.language.scad", 109 | "match": "\\$(children|fn|fa|fs|t|preview|vpr|vpt|vpf|vpd|parent_modules)\\b" 110 | }, 111 | { 112 | "include": "#constants" 113 | }, 114 | { 115 | "include": "#keyword_control" 116 | }, 117 | { 118 | "comment": "Ummm... why do we have three of these? This is one...", 119 | "name": "constant.numeric.float.scad", 120 | "match": "\\b(?i:(\\d+\\.\\d*(e[\\-\\+]?\\d+)?))(?=[^[:alpha:]_])" 121 | }, 122 | { 123 | "comment": "This is two...", 124 | "name": "constant.numeric.float.scad", 125 | "match": "(?<=[^[:alnum:]_])(?i:(\\.\\d+(e[\\-\\+]?\\d+)?))" 126 | }, 127 | { 128 | "comment": "And this is three...", 129 | "name": "constant.numeric.float.scad", 130 | "match": "\\b(?i:(\\d+e[\\-\\+]?\\d+))" 131 | }, 132 | { 133 | "name": "constant.numeric.integer.decimal.scad", 134 | "match": "\\b([1-9]+[0-9]*|0)" 135 | }, 136 | { 137 | "name": "meta.function.scad", 138 | "begin": "\\b(((?:module|function)))\\s+(?=[[:alpha:]_][[:alnum:]_]*)\\s*(?=\\()", 139 | "beginCaptures": { 140 | "1": { 141 | "name": "storage.type.$2.scad" 142 | } 143 | }, 144 | "end": "(?<=\\))", 145 | "endCaptures": { 146 | "1": { 147 | "name": "punctuation.definition.parameters.begin.scad" 148 | } 149 | }, 150 | "patterns": [ 151 | { 152 | "contentName": "entity.name.function.scad", 153 | "begin": "(?=[[:alpha:]_][[:alnum:]_]*)", 154 | "end": "(?![[:alnum:]_])", 155 | "patterns": [ 156 | { 157 | "include": "#illegal_names" 158 | } 159 | ] 160 | }, 161 | { 162 | "contentName": "meta.function.parameters.scad", 163 | "begin": "(\\()", 164 | "beginCaptures": { 165 | "1": { 166 | "name": "punctuation.definition.parameters.begin.scad" 167 | } 168 | }, 169 | "end": "(?=\\))", 170 | "patterns": [ 171 | { 172 | "include": "#keyword_arguments" 173 | }, 174 | { 175 | "include": "#illegal_names" 176 | }, 177 | { 178 | "match": "\\b(?:([[:alpha:]_][[:alnum:]_]*))\\s*(?:(,)|(?=[\\n\\)]))", 179 | "captures": { 180 | "1": { 181 | "name": "variable.parameter.function.language.scad" 182 | }, 183 | "2": { 184 | "name": "punctuation.separator.parameters.scad" 185 | }, 186 | "3": { 187 | "name": "punctuation.separator.parameters.scad" 188 | } 189 | } 190 | } 191 | ] 192 | }, 193 | { 194 | "begin": "(\\))", 195 | "beginCaptures": { 196 | "1": { 197 | "name": "punctuation.definition.parameters.end.scad" 198 | } 199 | }, 200 | "end": "(?<=\\))", 201 | "patterns": [ 202 | { 203 | "include": "$self" 204 | } 205 | ] 206 | } 207 | ] 208 | }, 209 | { 210 | "match": "\\b((module|function))\\b", 211 | "captures": { 212 | "1": { 213 | "name": "storage.type.$2.scad" 214 | } 215 | } 216 | }, 217 | { 218 | "name": "meta.function-call.scad", 219 | "begin": "(?:\\.)?([[:alpha:]_][[:alnum:]_]*)\\s*(?=(\\())", 220 | "beginCaptures": { 221 | "1": { 222 | "name": "entity.name.function.call.scad" 223 | }, 224 | "2": { 225 | "name": "punctuation.definition.arguments.begin.scad" 226 | } 227 | }, 228 | "end": "(\\))", 229 | "endCaptures": { 230 | "1": { 231 | "name": "punctuation.definition.arguments.end.scad" 232 | } 233 | }, 234 | "patterns": [ 235 | { 236 | "contentName": "meta.function-call.arguments.scad", 237 | "begin": "(\\()", 238 | "beginCaptures": { 239 | "1": { 240 | "name": "punctuation.definition.arguments.begin.scad" 241 | } 242 | }, 243 | "end": "(?=(\\)))", 244 | "endCaptures": { 245 | "1": { 246 | "name": "punctuation.definition.arguments.end.scad" 247 | } 248 | }, 249 | "patterns": [ 250 | { 251 | "include": "#keyword_arguments" 252 | }, 253 | { 254 | "include": "$self" 255 | } 256 | ] 257 | } 258 | ] 259 | }, 260 | { 261 | "include": "#let_keyword" 262 | }, 263 | { 264 | "include": "#function_builtin" 265 | } 266 | ], 267 | "repository": { 268 | "brace_block": { 269 | "name": "meta.block.scad", 270 | "begin": "\\{", 271 | "end": "\\}", 272 | "beginCaptures": { 273 | "0": { 274 | "name": "punctuation.section.block.begin.bracket.curly.scad" 275 | } 276 | }, 277 | "endCaptures": { 278 | "0": { 279 | "name": "punctuation.section.block.end.bracket.curly.scad" 280 | } 281 | }, 282 | "patterns": [ 283 | { 284 | "include": "$self" 285 | } 286 | ] 287 | }, 288 | "bracket_block": { 289 | "name": "meta.block.scad", 290 | "begin": "\\[", 291 | "end": "\\]", 292 | "beginCaptures": { 293 | "0": { 294 | "name": "punctuation.section.block.begin.bracket.square.scad" 295 | } 296 | }, 297 | "endCaptures": { 298 | "0": { 299 | "name": "punctuation.section.block.end.bracket.square.scad" 300 | } 301 | }, 302 | "patterns": [ 303 | { 304 | "include": "$self" 305 | } 306 | ] 307 | }, 308 | "comments": { 309 | "patterns": [ 310 | { 311 | "include": "#export_name_format" 312 | }, 313 | { 314 | "include": "#customizer_comments" 315 | }, 316 | { 317 | "name": "comment.block.documentation.scad", 318 | "begin": "/\\*\\*(?!/)", 319 | "captures": { 320 | "0": { 321 | "name": "punctuation.definition.comment.scad" 322 | } 323 | }, 324 | "end": "\\*/" 325 | }, 326 | { 327 | "name": "comment.block.scad", 328 | "begin": "/\\*", 329 | "captures": { 330 | "0": { 331 | "name": "punctuation.definition.comment.scad" 332 | } 333 | }, 334 | "end": "\\*/" 335 | }, 336 | { 337 | "name": "comment.line.double-slash.scad", 338 | "match": "(//).*$\\n?", 339 | "captures": { 340 | "1": { 341 | "name": "punctuation.definition.comment.scad" 342 | } 343 | } 344 | } 345 | ] 346 | }, 347 | "constants": { 348 | "patterns": [ 349 | { 350 | "name": "constant.language.boolean.true.scad", 351 | "match": "\\btrue\\b" 352 | }, 353 | { 354 | "name": "constant.language.boolean.false.scad", 355 | "match": "\\bfalse\\b" 356 | }, 357 | { 358 | "name": "constant.language.undef.scad", 359 | "match": "\\bundef\\b" 360 | }, 361 | { 362 | "name": "constant.language.pi.scad", 363 | "match": "\\bPI\\b" 364 | } 365 | ] 366 | }, 367 | "customizer_comments": { 368 | "patterns": [ 369 | { 370 | "name": "comment.block.scad", 371 | "match": "((?:/\\*)(?:\\*)?(?!\\/))(.*?)(\\*/)", 372 | "captures": { 373 | "1": { 374 | "name": "punctuation.definition.comment.begin.scad" 375 | }, 376 | "2": { 377 | "patterns": [ 378 | { 379 | "begin": "\\[", 380 | "end": "\\]", 381 | "name": "keyword.other.customizer.scad" 382 | } 383 | ] 384 | }, 385 | "3": { 386 | "name": "punctuation.definition.comment.end.scad" 387 | } 388 | } 389 | }, 390 | { 391 | "name": "comment.line.double-slash.scad", 392 | "match": "(?x)\n (?<=\\S\\s*)\n (\\/\\/\\s*)\n (\n \\[(?:\n (?:-?\\d+(?:.\\d)*)(?:\\s*:\\s*-?\\d+(?:.\\d)*){0,2} |\n (?:(?:[^:,]+:)?[^:,]+,)*(?:(?:[^:,]+:)?[^:,]+)\n )\n \\]\n )\n [ \\t]*$\\n?", 393 | "captures": { 394 | "1": { 395 | "name": "punctuation.definition.comment.scad" 396 | }, 397 | "2": { 398 | "name": "keyword.other.customizer.scad" 399 | } 400 | } 401 | } 402 | ] 403 | }, 404 | "export_name_format": { 405 | "name": "comment.line.double-slash.scad", 406 | "match": "(\\/\\/\\s*)(exportNameFormat)\\s*=\\s*(.*)", 407 | "captures": { 408 | "1": { 409 | "name": "punctuation.definition.comment.scad" 410 | }, 411 | "2": { 412 | "name": "keyword.other.config.exportNameFormat.scad" 413 | }, 414 | "3": { 415 | "patterns": [ 416 | { 417 | "match": "\\$\\{[\\w#]+(:.*?)?\\}", 418 | "name": "constant.character.format.exportNameFormat.scad" 419 | } 420 | ] 421 | } 422 | } 423 | }, 424 | "function_builtin": { 425 | "patterns": [ 426 | { 427 | "name": "support.function.scad", 428 | "match": "\\b(concat|lookupstr|chr|ord|search|version|version_num|parent_module)\\b" 429 | }, 430 | { 431 | "name": "support.function.scad", 432 | "match": "\\b(children|echo|group|offset|render)\\b" 433 | }, 434 | { 435 | "name": "support.function.type-test.scad", 436 | "match": "\\b(is_undef|is_bool|is_num|is_string|is_list)\\b" 437 | }, 438 | { 439 | "name": "support.function.math.scad", 440 | "match": "\\b(abs|sign|floor|round|ceil|ln|len|log|pow|sqrt|exp|rands|min|max|norm|cross)\\b" 441 | }, 442 | { 443 | "name": "support.function.math.trig.scad", 444 | "match": "\\b(sin|cos|asin|acos|tan|atan|atan2)\\b" 445 | }, 446 | { 447 | "name": "support.function.transform.scad", 448 | "match": "\\b(scale|translate|rotate|multmatrix|color|projection|hull|resize|mirror|minkowski)\\b" 449 | }, 450 | { 451 | "name": "support.function.boolean.scad", 452 | "match": "\\b(union|difference|intersection)\\b" 453 | }, 454 | { 455 | "name": "support.function.prim3d.scad", 456 | "match": "\\b(cube|sphere|cylinder|polyhedron)\\b" 457 | }, 458 | { 459 | "name": "support.function.prim2d.scad", 460 | "match": "\\b(square|circle|polygon|text)\\b" 461 | }, 462 | { 463 | "name": "support.function.extrude.scad", 464 | "match": "\\b(linear_extrude|rotate_extrude)\\b" 465 | } 466 | ] 467 | }, 468 | "include": { 469 | "match": "\\b(((?:include|use)))\\s*((<)[^>]*(>?))", 470 | "captures": { 471 | "1": { 472 | "name": "keyword.control.$2.scad" 473 | }, 474 | "3": { 475 | "name": "string.quoted.other.lt-gt.include.scad" 476 | }, 477 | "4": { 478 | "name": "punctuation.definition.string.begin.scad" 479 | }, 480 | "5": { 481 | "name": "punctuation.definition.string.begin.scad" 482 | } 483 | } 484 | }, 485 | "illegal_names": { 486 | "name": "invalid.illegal.name.scad", 487 | "match": "(?x)\n\\b (\n true | false | module | function | include | use | undef |\n for | intersection_for | if | else | let)\n\\b" 488 | }, 489 | "keyword_control": { 490 | "patterns": [ 491 | { 492 | "name": "keyword.control.scad", 493 | "match": "\\b(for|intersection_for|each|assert)\\b" 494 | }, 495 | { 496 | "name": "keyword.control.conditional.scad", 497 | "match": "\\b(if|else)\\b" 498 | }, 499 | { 500 | "name": "keyword.control.import.scad", 501 | "match": "\\b(import|dxf_dim|dxf_cross|surface)\\b" 502 | } 503 | ] 504 | }, 505 | "keyword_arguments": { 506 | "comment": "Incorporated from PythonImproved grammar", 507 | "begin": "\\b([[:alpha:]_][[:alnum:]_]*)\\s*(=)(?!=)", 508 | "beginCaptures": { 509 | "1": { 510 | "name": "variable.parameter.function.keyword.scad" 511 | }, 512 | "2": { 513 | "name": "keyword.operator.assignment.scad" 514 | } 515 | }, 516 | "end": "\\s*(?:(,)|(?=\\)))", 517 | "endCaptures": { 518 | "1": { 519 | "name": "punctuation.separator.parameters.scad" 520 | } 521 | }, 522 | "patterns": [ 523 | { 524 | "include": "$self" 525 | } 526 | ] 527 | }, 528 | "let_keyword": { 529 | "name": "keyword.control.scad", 530 | "match": "\\b(let)\\b" 531 | }, 532 | "paren_block": { 533 | "name": "meta.block.parens.scad", 534 | "begin": "\\(", 535 | "end": "\\)", 536 | "beginCaptures": { 537 | "0": { 538 | "name": "punctuation.section.parens.begin.bracket.round.scad" 539 | } 540 | }, 541 | "endCaptures": { 542 | "0": { 543 | "name": "punctuation.section.parens.end.bracket.round.scad" 544 | } 545 | }, 546 | "patterns": [ 547 | { 548 | "include": "$self" 549 | } 550 | ] 551 | } 552 | }, 553 | "scopeName": "source.scad", 554 | "uuid": "ED71CA06-521E-4D30-B9C0-480808749662" 555 | } 556 | -------------------------------------------------------------------------------- /syntaxes/scad.yaml-tmLanguage: -------------------------------------------------------------------------------- 1 | fileTypes: 2 | - scad 3 | foldingStartMarker: /\*\*|\{\s*$ 4 | foldingStopMarker: \*\*/|^\s*\} 5 | keyEquivalent: ^~S 6 | name: OpenSCAD 7 | patterns: 8 | - include: '#comments' 9 | # Strings 10 | - name: invalid.string.quoted.single.scad 11 | begin: '''' 12 | beginCaptures: 13 | '0': {name: punctuation.definition.string.begin.scad} 14 | end: '''' 15 | endCaptures: 16 | '0': {name: punctuation.definition.string.end.scad} 17 | patterns: 18 | - name: constant.character.escape.scad 19 | match: \\(x[0-7]?[0-9A-Fa-f]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{6}|.) 20 | 21 | - name: string.quoted.double.scad 22 | begin: '"' 23 | beginCaptures: 24 | '0': {name: punctuation.definition.string.begin.scad} 25 | end: '"' 26 | endCaptures: 27 | '0': {name: punctuation.definition.string.end.scad} 28 | patterns: 29 | - name: constant.character.escape.scad 30 | match: \\(x[0-7]?[0-9A-Fa-f]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{6}|.) 31 | 32 | # Include and use 33 | - include: '#include' 34 | 35 | - name: invalid.deprecated 36 | match: \b(assign|child|import_dxf|import_stl)\b 37 | - name: punctuation.terminator.statement.scad 38 | match: \; 39 | - name: meta.delimiter.object.comma.scad 40 | match: ',[ |\t]*' 41 | - name: meta.dot.scad 42 | match: '\.(?![0-9])' 43 | 44 | # Blocks of {}, [], () 45 | - include: '#brace_block' 46 | - include: '#bracket_block' 47 | - include: '#paren_block' 48 | 49 | # Operators 50 | - name: keyword.operator.assignment.scad 51 | match: '=(?!=)' 52 | - name: keyword.operator.arithmetic.scad 53 | match: '\+|\-|\*|\/|%' 54 | - name: keyword.operator.logical.scad 55 | match: '!|&&|\|\|' 56 | - name: keyword.operator.relational.scad 57 | match: '<=|<|==|!=|>=|>' 58 | - name: keyword.operator.conditional.scad 59 | match: '\?|\:' 60 | - name: keyword.operator.other.scad 61 | match: '#|%|!' # Add '*' for disable? 62 | 63 | # Special variables 64 | - name: variable.language.scad 65 | match: '\$(children|fn|fa|fs|t|preview|vpr|vpt|vpf|vpd|parent_modules)\b' 66 | 67 | # Constants 68 | - include: '#constants' 69 | - include: '#keyword_control' 70 | 71 | ################################################################# 72 | # Incorporated from PythonImproved grammar 73 | 74 | # Numbers 75 | - comment: Ummm... why do we have three of these? This is one... 76 | name: constant.numeric.float.scad 77 | match: \b(?i:(\d+\.\d*(e[\-\+]?\d+)?))(?=[^[:alpha:]_]) 78 | 79 | - comment: This is two... 80 | name: constant.numeric.float.scad 81 | match: (?<=[^[:alnum:]_])(?i:(\.\d+(e[\-\+]?\d+)?)) 82 | 83 | - comment: And this is three... 84 | name: constant.numeric.float.scad 85 | match: \b(?i:(\d+e[\-\+]?\d+)) 86 | 87 | - name: constant.numeric.integer.decimal.scad 88 | match: \b([1-9]+[0-9]*|0) 89 | 90 | # Function definition 91 | - name: meta.function.scad 92 | begin: \b(((?:module|function)))\s+(?=[[:alpha:]_][[:alnum:]_]*)\s*(?=\() 93 | beginCaptures: 94 | '1': {name: storage.type.$2.scad} 95 | # Add invalid name 96 | end: (?<=\)) 97 | endCaptures: 98 | '1': {name: punctuation.definition.parameters.begin.scad} 99 | patterns: 100 | - contentName: entity.name.function.scad 101 | begin: (?=[[:alpha:]_][[:alnum:]_]*) 102 | end: (?![[:alnum:]_]) 103 | patterns: 104 | - include: '#illegal_names' 105 | # - include: '#entity_name_function' 106 | - contentName: meta.function.parameters.scad 107 | begin: (\() 108 | beginCaptures: 109 | '1': {name: punctuation.definition.parameters.begin.scad} 110 | end: (?=\)) 111 | patterns: 112 | # - include: '#annotated_arguments' 113 | - include: '#keyword_arguments' 114 | - include: '#illegal_names' 115 | # - include: '#comments' 116 | - match: \b(?:([[:alpha:]_][[:alnum:]_]*))\s*(?:(,)|(?=[\n\)])) 117 | captures: 118 | '1': {name: variable.parameter.function.language.scad} 119 | '2': {name: punctuation.separator.parameters.scad} 120 | '3': {name: punctuation.separator.parameters.scad} 121 | - begin: (\)) 122 | beginCaptures: 123 | '1': {name: punctuation.definition.parameters.end.scad} 124 | end: (?<=\)) 125 | patterns: 126 | - include: $self 127 | 128 | # After main function decleration to catch stray module / funciton 129 | - match: '\b((module|function))\b' 130 | captures: 131 | '1': { name: storage.type.$2.scad } 132 | 133 | # Function call (does not start with 'module' or 'function' but may have a dot separator) 134 | - name: meta.function-call.scad 135 | begin: (?:\.)?([[:alpha:]_][[:alnum:]_]*)\s*(?=(\()) 136 | beginCaptures: 137 | # NOTE: Should also probably have map: 'meta.function-call.generic.scad' 138 | # but opted for 'entity.name.function.call.scad' for coloring purposes 139 | '1': {name: entity.name.function.call.scad} 140 | '2': {name: punctuation.definition.arguments.begin.scad} 141 | end: (\)) 142 | endCaptures: 143 | '1': {name: punctuation.definition.arguments.end.scad} 144 | patterns: 145 | - contentName: meta.function-call.arguments.scad 146 | begin: (\() 147 | beginCaptures: 148 | '1': {name: punctuation.definition.arguments.begin.scad} 149 | end: (?=(\))) 150 | endCaptures: 151 | '1': {name: punctuation.definition.arguments.end.scad} 152 | patterns: 153 | - include: '#keyword_arguments' 154 | - include: $self 155 | ################################################################# 156 | 157 | - include: '#let_keyword' 158 | - include: '#function_builtin' 159 | 160 | repository: 161 | brace_block: 162 | name: meta.block.scad 163 | begin: '\{' 164 | end: '\}' 165 | beginCaptures: 166 | '0': {name: punctuation.section.block.begin.bracket.curly.scad} 167 | endCaptures: 168 | '0': {name: punctuation.section.block.end.bracket.curly.scad} 169 | patterns: 170 | - include: $self 171 | 172 | bracket_block: 173 | name: meta.block.scad 174 | begin: '\[' 175 | end: '\]' 176 | beginCaptures: 177 | '0': {name: punctuation.section.block.begin.bracket.square.scad} 178 | endCaptures: 179 | '0': {name: punctuation.section.block.end.bracket.square.scad} 180 | patterns: 181 | - include: $self 182 | 183 | comments: 184 | patterns: 185 | - include: '#export_name_format' 186 | - include: '#customizer_comments' 187 | - name: comment.block.documentation.scad 188 | begin: /\*\*(?!/) 189 | captures: 190 | '0': {name: punctuation.definition.comment.scad} 191 | end: \*/ 192 | - name: comment.block.scad 193 | begin: /\* 194 | captures: 195 | '0': {name: punctuation.definition.comment.scad} 196 | end: \*/ 197 | - name: comment.line.double-slash.scad 198 | match: (//).*$\n? 199 | captures: 200 | '1': {name: punctuation.definition.comment.scad} 201 | 202 | constants: 203 | patterns: 204 | - name: constant.language.boolean.true.scad 205 | match: '\btrue\b' 206 | - name: constant.language.boolean.false.scad 207 | match: '\bfalse\b' 208 | - name: constant.language.undef.scad 209 | match: '\bundef\b' 210 | - name: constant.language.pi.scad 211 | match: '\bPI\b' 212 | 213 | customizer_comments: 214 | patterns: 215 | # Customizer section header (single line block comment) 216 | - name: comment.block.scad 217 | match: ((?:/\*)(?:\*)?(?!\/))(.*?)(\*/) 218 | captures: 219 | '1': {name: punctuation.definition.comment.begin.scad} 220 | '2': 221 | patterns: 222 | - begin: '\[' 223 | end: '\]' 224 | name: keyword.other.customizer.scad 225 | '3': {name: punctuation.definition.comment.end.scad} 226 | 227 | # Single line comment with customizer syntax 228 | # Can match: [10], [10:20], [-10:0.2:10], [foo, bar, baz], [10:S, 20:M, 30:L], [S:Small, M:Medium, L:Large] 229 | - name: comment.line.double-slash.scad 230 | match: |- 231 | (?x) 232 | (?<=\S\s*) 233 | (\/\/\s*) 234 | ( 235 | \[(?: 236 | (?:-?\d+(?:.\d)*)(?:\s*:\s*-?\d+(?:.\d)*){0,2} | 237 | (?:(?:[^:,]+:)?[^:,]+,)*(?:(?:[^:,]+:)?[^:,]+) 238 | ) 239 | \] 240 | ) 241 | [ \t]*$\n? 242 | captures: 243 | '1': {name: punctuation.definition.comment.scad} 244 | '2': {name: keyword.other.customizer.scad} 245 | 246 | export_name_format: 247 | name: comment.line.double-slash.scad 248 | match: (\/\/\s*)(exportNameFormat)\s*=\s*(.*) 249 | captures: 250 | '1': {name: punctuation.definition.comment.scad} 251 | '2': {name: keyword.other.config.exportNameFormat.scad} 252 | '3': 253 | patterns: 254 | - match: \$\{[\w#]+(:.*?)?\} 255 | name: constant.character.format.exportNameFormat.scad 256 | 257 | function_builtin: 258 | patterns: 259 | - name: support.function.scad 260 | match: \b(concat|lookupstr|chr|ord|search|version|version_num|parent_module)\b 261 | - name: support.function.scad 262 | match: \b(children|echo|group|offset|render)\b 263 | - name: support.function.type-test.scad 264 | match: \b(is_undef|is_bool|is_num|is_string|is_list)\b 265 | - name: support.function.math.scad 266 | match: \b(abs|sign|floor|round|ceil|ln|len|log|pow|sqrt|exp|rands|min|max|norm|cross)\b 267 | - name: support.function.math.trig.scad 268 | match: \b(sin|cos|asin|acos|tan|atan|atan2)\b 269 | - name: support.function.transform.scad 270 | match: \b(scale|translate|rotate|multmatrix|color|projection|hull|resize|mirror|minkowski)\b 271 | - name: support.function.boolean.scad 272 | match: \b(union|difference|intersection)\b 273 | - name: support.function.prim3d.scad 274 | match: \b(cube|sphere|cylinder|polyhedron)\b 275 | - name: support.function.prim2d.scad 276 | match: \b(square|circle|polygon|text)\b 277 | - name: support.function.extrude.scad 278 | match: \b(linear_extrude|rotate_extrude)\b 279 | 280 | include: 281 | # include|use 282 | match: \b(((?:include|use)))\s*((<)[^>]*(>?)) 283 | captures: 284 | '1': { name: keyword.control.$2.scad } 285 | '3': { name: string.quoted.other.lt-gt.include.scad } 286 | '4': { name: punctuation.definition.string.begin.scad } 287 | '5': { name: punctuation.definition.string.begin.scad } 288 | 289 | illegal_names: 290 | name: invalid.illegal.name.scad 291 | match: |- 292 | (?x) 293 | \b ( 294 | true | false | module | function | include | use | undef | 295 | for | intersection_for | if | else | let) 296 | \b 297 | 298 | keyword_control: 299 | patterns: 300 | # - include: '#let_keyword' 301 | - name: keyword.control.scad 302 | match: \b(for|intersection_for|each|assert)\b 303 | - name: keyword.control.conditional.scad 304 | match: \b(if|else)\b 305 | - name: keyword.control.import.scad 306 | match: \b(import|dxf_dim|dxf_cross|surface)\b 307 | 308 | keyword_arguments: 309 | comment: Incorporated from PythonImproved grammar 310 | begin: \b([[:alpha:]_][[:alnum:]_]*)\s*(=)(?!=) 311 | beginCaptures: 312 | '1': {name: variable.parameter.function.keyword.scad} 313 | '2': {name: keyword.operator.assignment.scad} 314 | end: \s*(?:(,)|(?=\))) 315 | endCaptures: 316 | '1': {name: punctuation.separator.parameters.scad} 317 | patterns: 318 | - include: $self 319 | 320 | let_keyword: 321 | name: keyword.control.scad 322 | match: '\b(let)\b' 323 | 324 | paren_block: 325 | name: meta.block.parens.scad 326 | begin: '\(' 327 | end: '\)' 328 | beginCaptures: 329 | '0': {name: punctuation.section.parens.begin.bracket.round.scad} 330 | endCaptures: 331 | '0': {name: punctuation.section.parens.end.bracket.round.scad} 332 | patterns: 333 | - include: $self 334 | 335 | scopeName: source.scad 336 | uuid: ED71CA06-521E-4D30-B9C0-480808749662 337 | -------------------------------------------------------------------------------- /test/customizer-sample.scad: -------------------------------------------------------------------------------- 1 | // Test file for SCAD customizer syntax 2 | // Can match: [10], [10:20], [-10:0.2:10], [foo, bar, baz], [10:S, 20:M, 30:L], [S:Small, M:Medium, L:Large] 3 | 4 | /* [Numerical Values] */ 5 | a = 5; // [5] 6 | b = 3; // [ 10:20 ] 7 | c = 1; // [1 : 0.1 : 1.5] 8 | d = 1; // [-10:0.1:10] 9 | 10 | /* [Values] [Text] */ 11 | e = "hi"; // [hi, hello, howdy, what's up, what:?!] 12 | f = 10; // [10:S, 20:M, 30:L] 13 | g = "S"; // [S:Small, M*:Medium, L:Large] 14 | 15 | // Not a valid category below 16 | /* [ 17 | abc 18 | ] */ 19 | 20 | h = "Ss"; // [Ss:Small, M m:Med i*um, X-L:Large] 21 | 22 | /** [] */ // Blank content and/or two *'s works too 23 | i = 20; // [10, 20, 30, 40] 24 | 25 | echo(a,b,c,d,e,f,g,h,i); 26 | 27 | // Only [a[Test] and [123] should be highlighted 28 | /* aa] [a[Test]aa[123]aa] */ 29 | 30 | j = "This"; 31 | // [This, should, not, be, highlighted] 32 | 33 | /* Below from: https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Customizer */ 34 | 35 | /* [Drop down box] */ 36 | // combo box for number 37 | Numbers=2; // [0, 1, 2, 3] 38 | 39 | // combo box for string 40 | Strings="foo"; // [foo, bar, baz] 41 | 42 | //labeled combo box for numbers 43 | Labeled_values=10; // [10:S, 20:M, 30:L] 44 | 45 | //labeled combo box for string 46 | Labeled_value="S"; // [S:Small, M:Medium, L:Large] 47 | 48 | /* [Slider] */ 49 | // slider widget for number with max. value 50 | sliderWithMax =34; // [50] 51 | 52 | // slider widget for number in range 53 | sliderWithRange =34; // [10:100] 54 | 55 | //step slider for number 56 | stepSlider=2; //[0:5:100] 57 | 58 | // slider widget for number in range 59 | sliderCentered =0; // [-10:0.1:10] -------------------------------------------------------------------------------- /test/highlight-sample.scad: -------------------------------------------------------------------------------- 1 | // Test file for general highlighting 2 | // exportNameFormat=${filenameNoExtension}.${exportExtension} 3 | include ; 4 | use ; 5 | 6 | // Variables 7 | /* [General] */ 8 | id = 8.2; 9 | od = 12.5; 10 | h = 30; 11 | chamfer = [1.5, 2]; // Chamfer: [h-dist, v-dist] 12 | 13 | /* [Gap] */ 14 | generate_gap = 1; // [0:off, 1:slot, 2:fancy gap] 15 | gap_split = .4; // [foo] 16 | gap_thickness = 1.6; // [10] 17 | gap_id_extra = 1.6; // [1:10] 18 | gap_width = 10; // [-10:1:10] 19 | 20 | /* [Knurls] */ 21 | knurl_depth = 0.5; 22 | knurl_count = 20; 23 | 24 | $fn=50; 25 | 26 | 27 | // Generate pencil grip thing 28 | difference() 29 | { 30 | linear_extrude(h) difference() 31 | { 32 | ring(od=od, id=id); 33 | if (generate_gap == 1) translate([-gap_split/2,0,0]) square([gap_split, od/2]); 34 | else if (generate_gap == 2) gap(); 35 | } 36 | 37 | rot_chamfer(chamfer, od/2); 38 | translate([0,0,h]) mirror([0,0,1]) 39 | rot_chamfer(chamfer, od/2); 40 | 41 | translate([0, 0, h/2]) 42 | for (i = [0 : knurl_count]) 43 | { 44 | rotate([0, 0, i * (360/knurl_count)]) 45 | { 46 | mirror([0,0,1]) knurl_cut(h, od, 50); 47 | knurl_cut(h, od, 50); 48 | } 49 | } 50 | } 51 | 52 | // Sub-routines 53 | module ring(od=0, id) 54 | { 55 | difference() 56 | { 57 | circle(d=od); 58 | circle(d=id); 59 | } 60 | } 61 | 62 | module gap() 63 | { 64 | difference() 65 | { 66 | ring(id+gap_id_extra+gap_thickness, id+gap_id_extra); 67 | translate([0,od/4,0]) square(size=[gap_width, od/2], center=true); 68 | } 69 | translate([0,-(id+gap_id_extra+gap_thickness)/4,0]) square(size=[gap_split, (id+gap_id_extra)/2], center=true); 70 | } 71 | 72 | module rot_chamfer(dist, r) 73 | { 74 | buf = 0.5; // buffer value 75 | rotate_extrude() 76 | translate([-(r+buf/2), -buf, 0]) 77 | polygon(points=[[0,0],[dist[0]+buf,0], [0,dist[1]+buf]]); 78 | } 79 | 80 | module knurl_cut(height, diameter, incline_angle=45) 81 | { 82 | phi = 360 * (height/(diameter*PI))/tan(incline_angle); // linear extrude twist degrees 83 | //echo(phi); 84 | 85 | linear_extrude(height=height, convexity=10, center=true, twist=phi, slices=height) 86 | { 87 | translate([od/2,0,0]) 88 | rotate(45) 89 | square(size=knurl_depth*sqrt(2), center=true); 90 | } 91 | } 92 | 93 | // Module without braces is still valid 94 | module cir() circle(r=1); 95 | 96 | function rad(degrees) = degrees/180*PI; 97 | function deg(radians) = radians*180/PI; 98 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "dist", 8 | "strict": true, 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "baseUrl": ".", 12 | "paths": { 13 | "src/*": ["./src/*"], 14 | }, 15 | "composite": true, 16 | }, 17 | "exclude": [ 18 | // 19 | "node_modules", 20 | ".vscode-test", 21 | ".vscode-test-web", 22 | "webpack.config.ts", // Without this, ts complains not all source files are included 23 | "dist", 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as path from 'node:path'; 4 | import { Configuration, EnvironmentPlugin, ProvidePlugin } from 'webpack'; 5 | 6 | // This doesn't work if it's not a require() 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires, unicorn/prefer-module 8 | const extensionPackage = require('./package.json'); 9 | 10 | // eslint-disable-next-line unicorn/prefer-module 11 | const projectRoot = __dirname; 12 | 13 | const nodeConfig: Configuration = { 14 | // VS Code client extensions run in Node context. See: https://webpack.js.org/configuration/node/ 15 | target: 'node', 16 | // Leaves the source code as close as possible to the original (when packaging we set this to 'production') 17 | mode: 'none', 18 | // Entry point into extension (in package.json). See: https://webpack.js.org/configuration/entry-context/ 19 | entry: { 20 | 'extension.node': './src/extension.node.ts', 21 | }, 22 | // Bundle output location. See: https://webpack.js.org/configuration/output/ 23 | output: { 24 | filename: '[name].js', 25 | path: path.join(projectRoot, 'dist'), 26 | libraryTarget: 'commonjs', 27 | devtoolModuleFilenameTemplate: '../[resource-path]', 28 | }, 29 | plugins: [ 30 | new EnvironmentPlugin({ 31 | EXTENSION_NAME: `${extensionPackage.publisher}.${extensionPackage.name}`, 32 | EXTENSION_VERSION: extensionPackage.version, 33 | }), 34 | ], 35 | devtool: 'nosources-source-map', 36 | // Support reading TypeScript and JavaScript files. See: https://github.com/TypeStrong/ts-loader 37 | resolve: { 38 | extensions: ['.ts', '.js'], 39 | alias: { 40 | src: path.resolve(projectRoot, 'src'), 41 | }, 42 | }, 43 | // Modules that cannot be added through Webpack. See: https://webpack.js.org/configuration/externals/ 44 | externals: { 45 | vscode: 'commonjs vscode', // ignored because 'vscode' module is created on the fly and doesn't really exist 46 | }, 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.ts$/, 51 | exclude: /node_modules/, 52 | use: [ 53 | { 54 | loader: 'ts-loader', 55 | }, 56 | ], 57 | }, 58 | ], 59 | }, 60 | performance: { 61 | hints: false, 62 | }, 63 | infrastructureLogging: { 64 | level: 'log', // enables logging required for problem matchers 65 | }, 66 | }; 67 | 68 | const browserConfig: Configuration = { 69 | // extensions run in a webworker context 70 | ...nodeConfig, 71 | target: 'webworker', 72 | entry: { 73 | 'extension.web': './src/extension.web.ts', 74 | // 'test/suite/index': './src/web/test/suite/index.ts', 75 | }, 76 | resolve: { 77 | ...nodeConfig.resolve, 78 | mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules 79 | }, 80 | plugins: [ 81 | new ProvidePlugin({ 82 | process: 'process/browser', // provide a shim for the global `process` variable 83 | }), 84 | ], 85 | }; 86 | 87 | // eslint-disable-next-line unicorn/prefer-module 88 | module.exports = [nodeConfig, browserConfig]; 89 | --------------------------------------------------------------------------------